From 04c67ba039c32349485668fe19c40d8ceb4297b6 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 07:17:55 +0000 Subject: [PATCH 1/2] test(plugin-security): real-stack pin for the union-scope no-active-organization insert Reads red on the base: group answers ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED/500 where isolated answers PERMISSION_DENIED/403 for the same context. Claude-Session: https://claude.ai/code/session_011TdZ8GjixNZFLz1xJD9zzK Co-authored-by: Claude --- ...ve-organization-insert-union-scope.test.ts | 245 ++++++++++++++++++ 1 file changed, 245 insertions(+) create mode 100644 packages/plugins/plugin-security/src/no-active-organization-insert-union-scope.test.ts diff --git a/packages/plugins/plugin-security/src/no-active-organization-insert-union-scope.test.ts b/packages/plugins/plugin-security/src/no-active-organization-insert-union-scope.test.ts new file mode 100644 index 00000000000..c7c5abd48bc --- /dev/null +++ b/packages/plugins/plugin-security/src/no-active-organization-insert-union-scope.test.ts @@ -0,0 +1,245 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * [ADR-0123 D2 · ADR-0105 D2/D5] A tenant-scoped INSERT from a session with no + * active organization gets the ADR-0123 D2 refusal under EVERY walled posture, + * `group` included — measured on the real stack (ObjectQL + SqlDriver + + * SecurityPlugin), never a 500 and never a row. + * + * ## The rule + * + * Under `group` the caller's membership set (`accessible_org_ids`) bounds what + * it may READ (ADR-0105 D2), and a new row lands in the ACTIVE organization, + * the default write target the engine stamps from `tenantId` (ADR-0105 D5). A + * session with no active organization therefore has no organization to place a + * new row in. ADR-0123 D2 answers that state with `403 PERMISSION_DENIED`, a + * message naming the missing active organization, and `isolated` always did. + * + * `group` used to let that insert through the Layer 0 wall, whose scope test + * reads the membership set there, and the engine then refused the row as an + * organization-less SYSTEM write (`ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED`, + * status 500). Nothing was stored, so it failed closed; the defect was the + * answer. The wall now judges the stored image in that case and refuses it + * with the same D2 answer, before the engine is reached. + * + * ## What this file pins + * + * - `group`: refused with the D2 answer, and no row is stored; + * - `isolated` (control, unchanged): the same context gets the same answer — + * the two outcomes are compared whole, so "exactly what `isolated` answers" + * is measured rather than restated; + * - `group` (control): the same member WITH an active organization is + * admitted, and the row lands in that organization; + * - `group` (control): a session with NO membership at all keeps the D2 + * refusal it already had; + * - `group` (control): reads with no active organization still span the + * membership set — the posture's design, untouched. + */ + +import { describe, it, expect, afterEach, vi } from 'vitest'; +import { ObjectQL } from '@objectstack/objectql'; +import { SqlDriver } from '@objectstack/driver-sql'; +import type { PermissionSet } from '@objectstack/spec/security'; +import { SecurityPlugin } from './security-plugin.js'; + +/** Two organizations the member belongs to, and one it does not. */ +const OWN_ORG = 'org_a'; +const SISTER_ORG = 'org_c'; +const OTHER_ORG = 'org_b'; + +/** A public tenant object: the platform injects `organization_id`, so Layer 0 walls it. */ +const OBJECTS = [ + { + name: 'qa_task', + label: 'Task', + fields: { + id: { name: 'id', type: 'text', primaryKey: true }, + name: { name: 'name', type: 'text' }, + }, + }, +]; + +/** Plain CRUD and no row-level policy — Layer 0 is the only enforcer under test. */ +const MEMBER: PermissionSet = { + name: 'member_default', + label: 'Member', + objects: { '*': { allowRead: true, allowCreate: true, allowEdit: true, allowDelete: true } }, +} as unknown as PermissionSet; + +const SYS_CTX = { isSystem: true, userId: 'usr_system', tenantId: OWN_ORG }; + +/** + * The subject: an authenticated member of two organizations whose session has + * no active organization. ONE context for every posture, so the `isolated` + * control differs from the `group` case in the posture alone. + */ +const NO_ACTIVE_ORG_CTX = { + userId: 'usr_a', + positions: [], + permissions: [], + posture: 'MEMBER', + accessible_org_ids: [OWN_ORG, SISTER_ORG], +}; +/** The same member one `set-active` later — the single-fact control. */ +const ACTIVE_ORG_CTX = { ...NO_ACTIVE_ORG_CTX, tenantId: OWN_ORG }; +/** A session that belongs to no organization at all. */ +const NO_MEMBERSHIP_CTX = { userId: 'usr_z', positions: [], permissions: [], posture: 'MEMBER', accessible_org_ids: [] }; + +type Posture = 'isolated' | 'group'; + +const engines: ObjectQL[] = []; +afterEach(async () => { + while (engines.length) { + try { await engines.pop()?.destroy(); } catch { /* noop */ } + } +}); + +interface Booted { + engine: ObjectQL; + /** The table read straight off the driver, past every scope. */ + table: () => Promise>>; +} + +const SEEDED = [ + { id: 'r1', name: 'own', organization_id: OWN_ORG }, + { id: 'r2', name: 'other', organization_id: OTHER_ORG }, + { id: 'r3', name: 'sister', organization_id: SISTER_ORG }, +]; + +async function boot(posture: Posture): Promise { + const engine = new ObjectQL(); + engine.registerDriver( + new SqlDriver({ client: 'better-sqlite3', connection: { filename: ':memory:' }, useNullAsDefault: true } as never) as never, + true, + ); + await engine.init(); + engine.registerApp({ + id: 'com.objectstack.qa.no-active-organization-insert-union-scope', + name: 'No active organization, union-scope posture', + version: '1.0.0', + type: 'plugin', + scope: 'system', + objects: OBJECTS, + } as never); + await engine.syncSchemas(); + engines.push(engine); + + // The posture the package's other real-stack posture pins boot: the + // `org-scoping` sentinel activates the wall and `tenancy` names the posture, + // which SecurityPlugin also hands the engine (`setTenancyPostureProvider`). + const services: Record = { + manifest: { register: vi.fn() }, + objectql: engine, + metadata: { + get: async (_type: string, name: string) => engine.getSchema(name) ?? null, + list: async () => [MEMBER], + }, + 'org-scoping': { name: 'com.objectstack.org-scoping' }, + tenancy: { posture }, + }; + const ctx = { + logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() }, + registerService: vi.fn(), + getService: (name: string) => { + if (!(name in services)) throw new Error(`service not registered: ${name}`); + return services[name]; + }, + }; + const plugin = new SecurityPlugin({ fallbackPermissionSet: 'member_default' }); + await plugin.init(ctx as never); + await plugin.start(ctx as never); + vi.spyOn((engine as unknown as { logger: { warn: () => void } }).logger, 'warn').mockImplementation(() => undefined); + + await engine.insert('qa_task', SEEDED.map((r) => ({ ...r })), { context: SYS_CTX } as never); + + const table = async () => { + const driver = (engine as unknown as { getDriver(o: string): { knex: unknown } }).getDriver('qa_task'); + const knex = driver.knex as (t: string) => { select: (...c: string[]) => Promise>> }; + const rows = await knex('qa_task').select('id', 'name', 'organization_id'); + return [...rows].sort((a, b) => String(a.id).localeCompare(String(b.id))); + }; + return { engine, table }; +} + +interface Outcome { ok: boolean; code?: string; status?: number; message?: string } + +const attempt = async (run: () => Promise): Promise => { + try { + await run(); + return { ok: true }; + } catch (e) { + const err = e as { code?: string; statusCode?: number; status?: number; message?: string }; + return { ok: false, code: err.code, status: err.statusCode ?? err.status, message: String(err.message ?? e) }; + } +}; + +const create = (b: Booted, data: unknown, context: object) => + b.engine.insert('qa_task', data as never, { context } as never); + +/** The ADR-0123 D2 answer on the ADR-0112 envelope: `code` + `status`, then the D4 naming. */ +const expectNoActiveOrganizationRefusal = (outcome: Outcome) => { + expect(outcome.ok, 'expected a refusal, got a completed write').toBe(false); + expect(outcome.code, 'ADR-0112 error code').toBe('PERMISSION_DENIED'); + expect(outcome.status, 'ADR-0112 HTTP status').toBe(403); + // ADR-0123 D4: the refusal names what is missing, so it cannot be mistaken + // for an ordinary permission denial. + expect(outcome.message).toMatch(/no active organization/i); +}; + +describe('[ADR-0123 D2] a tenant-scoped insert from a session with no active organization — real stack', () => { + it('group: is refused 403 PERMISSION_DENIED naming the missing active organization, and stores nothing', async () => { + const b = await boot('group'); + + const outcome = await attempt(() => create(b, { id: 'n1', name: 'new' }, NO_ACTIVE_ORG_CTX)); + + expectNoActiveOrganizationRefusal(outcome); + expect(await b.table(), 'no row stored, NULL-organization or otherwise').toEqual(SEEDED); + }); + + it('group: a bulk insert is refused as a whole, and stores nothing', async () => { + const b = await boot('group'); + + const outcome = await attempt(() => create(b, [{ id: 'n1', name: 'a' }, { id: 'n2', name: 'b' }], NO_ACTIVE_ORG_CTX)); + + expectNoActiveOrganizationRefusal(outcome); + expect(await b.table()).toEqual(SEEDED); + }); + + it('isolated (control, unchanged): the same context gets the same refusal, and group answers exactly what isolated answers', async () => { + const isolated = await boot('isolated'); + const group = await boot('group'); + + const isolatedOutcome = await attempt(() => create(isolated, { id: 'n1', name: 'new' }, NO_ACTIVE_ORG_CTX)); + const groupOutcome = await attempt(() => create(group, { id: 'n1', name: 'new' }, NO_ACTIVE_ORG_CTX)); + + expectNoActiveOrganizationRefusal(isolatedOutcome); + expect(await isolated.table()).toEqual(SEEDED); + expect(groupOutcome, 'group answers exactly what isolated answers').toEqual(isolatedOutcome); + }); + + it('group (control): the same member WITH an active organization is admitted, and the row lands in it', async () => { + const b = await boot('group'); + + const outcome = await attempt(() => create(b, { id: 'n1', name: 'new' }, ACTIVE_ORG_CTX)); + + expect(outcome, 'expected the write to be admitted').toEqual({ ok: true }); + expect(await b.table()).toEqual([{ id: 'n1', name: 'new', organization_id: OWN_ORG }, ...SEEDED]); + }); + + it('group (control): a session with no membership at all keeps the D2 refusal', async () => { + const b = await boot('group'); + + const outcome = await attempt(() => create(b, { id: 'n1', name: 'new' }, NO_MEMBERSHIP_CTX)); + + expectNoActiveOrganizationRefusal(outcome); + expect(await b.table()).toEqual(SEEDED); + }); + + it('group (control): reads with no active organization still span the membership set', async () => { + const b = await boot('group'); + + const rows = (await b.engine.find('qa_task', { context: NO_ACTIVE_ORG_CTX } as never)) as Array>; + + expect(rows.map((r) => r.id).sort()).toEqual(['r1', 'r3']); + }); +}); From cca0ba958ab70ccc0868c3fb51a5c12b23887184 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 07:21:13 +0000 Subject: [PATCH 2/2] fix(plugin-security): a union-scope insert with no active organization gets the ADR-0123 D2 403 Under group, a member whose session has no active organization passed the Layer 0 scope pre-test (it reads the membership set) and the engine then refused the row as an organization-less system write with a 500. The wall now refuses that insert on its stored image with the same answer isolated gives. Claude-Session: https://claude.ai/code/session_011TdZ8GjixNZFLz1xJD9zzK Co-authored-by: Claude --- .../24784-group-no-active-org-write-403.md | 11 ++ ...-active-organization-write-refusal.test.ts | 29 +++-- .../plugin-security/src/security-plugin.ts | 108 ++++++++++++++++-- 3 files changed, 128 insertions(+), 20 deletions(-) create mode 100644 .changeset/24784-group-no-active-org-write-403.md diff --git a/.changeset/24784-group-no-active-org-write-403.md b/.changeset/24784-group-no-active-org-write-403.md new file mode 100644 index 00000000000..a2e4006d3a0 --- /dev/null +++ b/.changeset/24784-group-no-active-org-write-403.md @@ -0,0 +1,11 @@ +--- +'@objectstack/plugin-security': patch +--- + +fix(plugin-security): under the `group` posture, a tenant-scoped insert from a session with no active organization is refused 403 naming the missing organization, as `isolated` already answered, instead of a 500 + +Clause-②: no (no accept set moves: the insert was refused before and is refused now, with the ADR-0123 D2 answer in place of an internal error) + +- **Before.** On `group` (any union-scope posture), a member of an organization whose session had no active organization got `500 ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED` for a create on a tenant-scoped object, logged as an unhandled REST error. +- **After.** That create gets `403 PERMISSION_DENIED` with the message `isolated` gives the same session, which names the missing active organization and tells the user to join or select one. +- **Unchanged.** Nothing was written before and nothing is written now. The refusal judges the row as it is about to be stored, the same row the engine's own organization check reads, so it covers exactly the inserts that answered 500 and no insert that was admitted. Reads still span the member's organizations. Updates and deletes of rows the membership wall admits, inserts from a session with an active organization, system contexts, a platform operator on a posture-permitting object, the delegator's leg, and every `isolated` and `single` answer are as before. diff --git a/packages/plugins/plugin-security/src/no-active-organization-write-refusal.test.ts b/packages/plugins/plugin-security/src/no-active-organization-write-refusal.test.ts index 12126e8d6b2..43695b7e6f9 100644 --- a/packages/plugins/plugin-security/src/no-active-organization-write-refusal.test.ts +++ b/packages/plugins/plugin-security/src/no-active-organization-write-refusal.test.ts @@ -38,9 +38,9 @@ * * Every control below is paired with the case it controls: for each exemption * (system context, platform operator, non-tenant object, `single` posture, a - * non-empty `group` membership set) there is a sibling case differing in that - * one fact where the refusal DOES fire. A file of controls alone would pass on - * the pre-fix build. + * `group` membership set WITH an active organization) there is a sibling case + * differing in that one fact where the refusal DOES fire. A file of controls + * alone would pass on the pre-fix build. */ import { describe, it, expect, vi } from 'vitest'; @@ -289,15 +289,28 @@ describe('[ADR-0123 D2] tenant-scoped WRITES with no active organization are ref expectConformingRefusal(err); }); - it('CONTROL: a NON-EMPTY membership set lands, even with no active organization', async () => { - // The half a `tenantId`-only test would get wrong: under `group` the - // membership set IS the scope, so this caller is fully scoped despite - // carrying no active organization. + it('a NON-EMPTY membership set with NO active organization: an insert has nowhere to land, and is refused the same way', async () => { + // The membership set is the READ scope under `group`, so this caller + // passes the scope pre-test; a new row still lands in the ACTIVE + // organization (ADR-0105 D5), and there is none. The stored image is + // judged — the terminal here runs the seam on the rows as sent. The + // real-stack twin, with the engine and a driver, is + // `no-active-organization-insert-union-scope.test.ts`. const h = await boot({ posture: 'group' }); - await h.run({ + const err = await refusalFrom(h.run, { object: 'task', operation: 'insert', data: { name: 'A' }, context: { ...ORG_LESS, accessible_org_ids: ['org-1', 'org-2'] }, }); + expectConformingRefusal(err); + }); + + it('CONTROL: the same membership set WITH an active organization lands', async () => { + // The single-fact control for the case above: only `tenantId` differs. + const h = await boot({ posture: 'group' }); + await h.run({ + object: 'task', operation: 'insert', data: { name: 'A' }, + context: { ...ORG_BOUND, accessible_org_ids: ['org-1', 'org-2'] }, + }); }); }); diff --git a/packages/plugins/plugin-security/src/security-plugin.ts b/packages/plugins/plugin-security/src/security-plugin.ts index ebd640e30fc..5a87b566d37 100644 --- a/packages/plugins/plugin-security/src/security-plugin.ts +++ b/packages/plugins/plugin-security/src/security-plugin.ts @@ -652,12 +652,28 @@ const AUTHORED_ROW_WRITE_PROBE_CONTEXT = { isSystem: true, positions: [], permis * {@link isTenantWallDenial} over the Layer 0 filter, which is the one place * that knows about platform-admin exemptions, non-tenant objects and phantom * anchors. + * + * It answers the SCOPE question only. Under a union-scope posture a caller + * scoped by membership can still lack the organization a NEW row lands in — + * the active one ({@link carriesActiveOrganization}) — and step 3.7 judges + * that separately, on the stored image of an insert. */ function callerHasOrganizationScope(context: any, posture: TenancyPosture): boolean { if (postureUsesUnionScope(posture)) { const orgIds = context?.accessible_org_ids; return Array.isArray(orgIds) && orgIds.length > 0; } + return carriesActiveOrganization(context); +} + +/** + * [ADR-0105 D5 · ADR-0123 D2] Does this execution context carry an ACTIVE + * organization (`tenantId`)? In every posture that is where a new row lands: + * the engine stamps an insert's organization from it. Under `isolated` it is + * also the whole read scope; under `group` the membership set is the read + * scope and the active organization stays the write target (ADR-0105 D2). + */ +function carriesActiveOrganization(context: any): boolean { const tenantId = context?.tenantId; return tenantId != null && tenantId !== ''; } @@ -3930,6 +3946,9 @@ export class SecurityPlugin implements Plugin { // System / boot writes never reach this line at all (`isSystem` // short-circuits the middleware). Under `group`, "no organization scope" // means an EMPTY membership set — ADR-0105 D2's own fail-closed rule. + // A member WITH a membership set but no active organization passes this + // pre-test and still has nowhere for a NEW row to land; that insert is + // refused with this same answer on its stored image, below ([#24784]). // // The cheap posture/context pre-test comes first on purpose: the common // deployment is `single` (Layer 0 inert), and the common caller HAS an @@ -3944,16 +3963,13 @@ export class SecurityPlugin implements Plugin { // tenant, so its target is selected through the Layer 0 ROW wall, which // already resolves to nothing (ADR-0123 D2, stated there as a boundary // rather than left as an omission). - const denied = await this.organizationWallRefusal( - permissionSets, - opCtx.object, - opCtx.operation, - opCtx.context, - delegatorSets, - delegatorContext, - ); - if (denied) { - const principal = denied === 'delegator' ? 'the delegating principal' : 'this session'; + // + // The refusal has ONE spelling, thrown from two points: here on the + // scope pre-test, and on the stored image of a union-scope insert with + // no active organization (below). Only the operator half differs, + // because the two say different things about the context. + const refuseNoActiveOrganization = (refused: 'caller' | 'delegator', developerMessage: string): never => { + const principal = refused === 'delegator' ? 'the delegating principal' : 'this session'; this.logger.warn?.( `[Security] Layer 0 tenant wall REFUSED ${opCtx.operation} '${opCtx.object}' — ` + `${principal} has no active organization to place the record in (ADR-0123 D2, fail-closed)`, @@ -3963,6 +3979,20 @@ export class SecurityPlugin implements Plugin { `${principal} has no active organization — so this ${opCtx.operation} has no ` + `organization to place the record in. Join or select an active organization and retry.`, { operation: opCtx.operation, object: opCtx.object, positions, permissionSets: explicitPermissionSets }, + developerMessage, + ); + }; + const denied = await this.organizationWallRefusal( + permissionSets, + opCtx.object, + opCtx.operation, + opCtx.context, + delegatorSets, + delegatorContext, + ); + if (denied) { + refuseNoActiveOrganization( + denied, `[ADR-0123 D2] Tenant-scoped writes are refused when the execution context carries no active ` + `organization: tenancy posture '${this.tenancyPosture}' walls '${opCtx.object}', and ` + `${denied === 'delegator' ? "the delegator's" : "the caller's"} context resolved neither ` + @@ -3993,6 +4023,10 @@ export class SecurityPlugin implements Plugin { // below). The `single` posture composes no Layer 0 at all, so it pays // for nothing it did not already pay for. let tenantParts: Record[] = []; + // The CALLER's own Layer 0 for this write — `null` where the wall does + // not apply to it (a non-tenant object, a phantom anchor, a platform + // operator crossing on a posture-permitting object, `single`). + let callerTenantCheck: Record | null = null; if (suppliedRows.length > 0 || this.orgScopingEnabled) { const tenantCheck = await this.computeWriteTenantCheckFilter( permissionSets, @@ -4000,6 +4034,7 @@ export class SecurityPlugin implements Plugin { opCtx.operation, opCtx.context, ); + callerTenantCheck = tenantCheck; // [ADR-0090 D10] The post-image must also satisfy the delegator's tenant // wall — an on-behalf-of write may not land a row in a tenant the // delegator itself could not reach. @@ -4041,6 +4076,36 @@ export class SecurityPlugin implements Plugin { `every organization. System contexts and platform operators on posture-permitting objects are unaffected.`, ); }; + // ── [ADR-0123 D2 · ADR-0105 D2/D5 · #24784] A union-scope INSERT with no active organization ── + // + // Under a union-scope posture (`group`) the scope pre-test above reads + // the MEMBERSHIP SET, so a member whose session has no active + // organization passes it — rightly, for everything the set bounds: + // reads, and updates and deletes, whose rows the membership wall + // selected and which keep the organization they already have. An + // insert is the one write that needs somewhere NEW to land, and that is + // the ACTIVE organization: the engine stamps a new row from `tenantId` + // (ADR-0105 D5), and the column is read-only to a session payload. So + // that member's row had no organization to land in, and the engine met + // it as an organization-less SYSTEM write and refused it + // `ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED` with a 500. ADR-0123 D2's + // answer for the state is the 403 `isolated` gives the same session; + // this gives it here, before the engine is reached. + // + // Judged on the STORED image (the seam below), not on the payload: the + // image is the row after the `beforeInsert` chain, the same rows the + // engine's own organization check reads one step later. So the refusal + // covers exactly the rows that would have met that 500, and nothing the + // store admitted before is refused now. Only the CALLER's own wall + // decides whether the object is walled for it — a platform operator + // crossing on a posture-permitting object keeps its answer — and the + // delegator keeps the pre-test's leg, unchanged. `isolated` never + // reaches this: its pre-test reads the active organization itself. + const insertLacksActiveOrganization = + opCtx.operation === 'insert' && + postureUsesUnionScope(this.tenancyPosture) && + callerTenantCheck != null && + !carriesActiveOrganization(opCtx.context); // EVERY supplied row must clear the wall — one forged row in a bulk // payload denies the whole write (fail closed, no partial landing). if (tenantParts.length > 0 && !suppliedRows.every(clearsWall)) denyTenantPlacement('a supplied'); @@ -4098,6 +4163,24 @@ export class SecurityPlugin implements Plugin { const businessCheck = writeImageCheckSeam; writeImageCheckSeam = { evaluate: (rows) => { + // First, as the pre-test's refusal is first: the session's state + // is the actionable half. One row with nowhere to land refuses + // the whole write — no partial landing. + if ( + insertLacksActiveOrganization && + rows.some((row) => !!row && typeof row === 'object' && !namesOrganization(row)) + ) { + refuseNoActiveOrganization( + 'caller', + `[ADR-0123 D2] Tenant-scoped writes are refused when the execution context carries no active ` + + `organization: tenancy posture '${this.tenancyPosture}' walls '${opCtx.object}', and the ` + + `caller's context carries no \`tenantId\`. Under this posture the membership set ` + + `(\`accessible_org_ids\`) bounds what the caller reads, while a new row lands in the active ` + + `organization (ADR-0105 D5), so this insert has none to land in. Reads under this state keep ` + + `spanning the membership set; the insert is refused rather than reaching the engine as an ` + + `organization-less system write. System contexts and platform operators are unaffected.`, + ); + } businessCheck?.evaluate(rows); for (const row of rows) { if (!row || typeof row !== 'object') continue; @@ -6275,8 +6358,9 @@ export class SecurityPlugin implements Plugin { * - **Row-level and post-image refusals — the preview names no stored row.** * The step 2.7 `using` pre-image, the ADR-0055 controlled-by-parent master * edit (2.8), the RLS `check` post-image (3.6) and the Layer 0 tenant - * post-image (3.7), none of which this method can judge because it is - * asked about no ROW. + * post-image (3.7) — its union-scope no-active-organization insert arm + * included, which judges the row a hook may still place — none of which + * this method can judge because it is asked about no ROW. * - **Payload-VALUE refusals the same caller passes by not sending the * value** — the masked echo (2.5a) and the `owner_id` forge (3.5), which * therefore widen the caller class by nothing.