|
| 1 | +--- |
| 2 | +"@objectstack/objectql": patch |
| 3 | +--- |
| 4 | + |
| 5 | +fix(objectql): a published `BulkDataEvent` now names the ONE organization the tenant wall named for the batch |
| 6 | + |
| 7 | +`BulkDataEventSchema.organizationId` (`@objectstack/spec/api`, declared by the |
| 8 | +contract half) is one organization for a whole predicate write, or absent. The |
| 9 | +only bulk producer — `publishBulkDataEvent`, behind the `multi: true` branches |
| 10 | +of `update()` / `delete()` — never set it, so every `data.records.updated` / |
| 11 | +`data.records.deleted` event read "not asserted" and a tenant-scoped consumer |
| 12 | +could deliver nothing per organization on the bulk path. This is the bulk half |
| 13 | +of the cross-tenant webhook fan-out leak; the single-record half (`DataEvent`) |
| 14 | +landed separately. |
| 15 | + |
| 16 | +The producer now stamps the key from what it already holds — no second query |
| 17 | +on the publish path: under `isolated` the caller's active organization (the |
| 18 | +Layer 0 wall's equality term), under `group` the caller's membership set when |
| 19 | +it names exactly one organization. It is OMITTED — never the caller's active |
| 20 | +organization standing in — on a `single`-posture deployment, on an `isSystem` |
| 21 | +context (no wall composed), on a multi-membership `group` sweep, when no |
| 22 | +enforcement layer injected a posture (the `OS_TENANCY_POSTURE` env fallback is |
| 23 | +deliberately not consulted), when the caller may have crossed the wall as a |
| 24 | +`PLATFORM_ADMIN` or carries no resolved posture rung, and on an object the wall |
| 25 | +does not key on. `absent` here means "the producer did not assert one |
| 26 | +organization for the batch", deliberately NOT the `DataEvent` reading |
| 27 | +"belongs to no organization". |
| 28 | + |
| 29 | +Which objects "the wall does not key on", stated exactly rather than claimed as |
| 30 | +a mirror: plugin-security's Layer 0 composes no wall when its `tenancyDisabled` |
| 31 | +input is true or the object carries no `organization_id`, and it folds THREE |
| 32 | +clauses into `tenancyDisabled` — `tenancy.enabled === false`, |
| 33 | +`systemFields.tenant === false`, and the deployment's `platformGlobalObjects` |
| 34 | +carve-out. The producer reads the registry's binding of that predicate |
| 35 | +(`carriesTenantScopeColumn`: the first two clauses plus the column clause) and |
| 36 | +answers absent on a federated (`external`) object; a custom |
| 37 | +`tenancy.tenantField` is therefore not an exit by itself — the object is walled |
| 38 | +iff it carries `organization_id`, and the key follows the wall. The third |
| 39 | +clause is deployment-declared and not readable by the engine: a |
| 40 | +deployment-exempted object under an armed wall is still stamped with the |
| 41 | +caller's organization by this producer alone, and that population's exact |
| 42 | +answer is decided by the seam ruled on in #15706. |
| 43 | + |
| 44 | +`patch`, not `minor`: the act adds no member to this package's published |
| 45 | +surface. `carriesTenantScopeColumn` is exported at module level inside |
| 46 | +`registry.ts` only — `@objectstack/objectql`'s entries (`.`, `./core`) re-export |
| 47 | +named members and never `export *`, so `dist/index.d.ts`, `dist/core.d.ts` and |
| 48 | +both entries' runtime export lists are unchanged (measured on the built `dist`, |
| 49 | +with a firing control) — and the emitted event's member was declared, typed |
| 50 | +and paid for at `minor` by the spec half. Producer conformance to an existing |
| 51 | +optional member under `fix(` changes no public surface of this package. |
0 commit comments