Repository navigation
Commit 688052e
fix(objectql, plugin-auth)!: organization delete refuses while records remain, removes its own auth structure as one unit, and never orphans a tenant column (#25016)
Fixes #24948
Clause-②: yes (narrowing)
Executes the maintainer's ruling on #24948 (letter B, record
6303358360), against the measurement 6297602613 and the claim
6303408016. Also executes the seat decisions 6305179819 (append-only
ledgers exempt and kept; business units, positions, sharing rules and
settings stay records; the deviations accepted), 6305996369 (the answers
to contract review 6305988777, addressed item by item below) 6307135388
(option B: engine-owned rows are treated like the ledgers; see **Round
4** below) and 6307815346 (the declaration decides, with no per-table
exceptions). It also answers delta review 6308041940 (**Round 5**
below).
## What changes
**1. Refused while the organization owns records, before anything is
deleted.** `POST /api/v1/auth/organization/delete` answers `409` with
the standard `DELETE_RESTRICTED` code and `dependents: [{ object, count
}]`, one entry per table that still names the organization in its tenant
column. It carries counts only, never a row value.
- The refusal runs in better-auth's global `hooks.before`, ahead of the
vendor handler, so a refused delete writes nothing, the caller's active
organization included.
- It runs only for a caller the vendor itself would admit (a member
whose role holds `organization: ['delete']`, asked of the vendor's own
`hasPermission`), so a caller the vendor refuses gets the vendor's 403
and no count.
- `packages/plugins/plugin-auth/src/auth-manager.ts:2453` (the
`hooks.before` branch), `:8122` (`assertOrganizationDeletable`);
`packages/plugins/plugin-auth/src/organization-delete.ts:134`
(`organizationDeleteRefusal`).
**2. The organization's own auth structure goes with it.** Measured on a
booted showcase: a door-created organization owns 1 `sys_member`, 1
`sys_team`, 1 `sys_team_member` and 1 `sys_user_permission_set`, and
nothing else. The creator's session also names it, through
`active_organization_id`, which is a pointer, not a tenant column.
| Table | How it names the organization | Removed by |
|---|---|---|
| `sys_member` | `organization_id`,
`packages/platform-objects/src/identity/sys-member.object.ts:312` |
better-auth `adapter.deleteOrganization` (better-auth 1.7.3
`dist/plugins/organization/adapter.mjs:370-395`) |
| `sys_invitation` | `organization_id`, `sys-invitation.object.ts:255` |
better-auth, same call |
| `sys_team` (the default team: `teams: { enabled: true }`,
`auth-manager.ts:3579`; created by better-auth
`routes/crud-org.mjs:106-126`) | `organization_id`,
`sys-team.object.ts:133` | plugin-auth
`removeOrganizationAuthStructure`, `organization-delete.ts:240`, from
`beforeDeleteOrganization`, `auth-manager.ts:3910` |
| `sys_team_member` | only through `team_id`,
`sys-team-member.object.ts:135` | same, by team, before the teams
(`organization-delete.ts:246`) |
| `sys_user_permission_set`, **only** grants of `organization_admin` /
`organization_admin_no_bypass` | `organization_id`,
`packages/plugins/plugin-security/src/objects/sys-user-permission-set.object.ts:110`
| same (`organization-delete.ts:256`); plugin-security's member-delete
reconcile did not remove them in time on a booted showcase. Every other
grant scoped to the organization counts as a record. |
The exclusion list is `ORGANIZATION_AUTH_STRUCTURE`,
`organization-delete.ts:94`.
**3. Rows that outlive the organization, and the references the delete
keeps: never NULL, never deleted, never counted.** The set is read from
a declaration the objects already carry, never from a list of names
(`ObjectQL.outlivesOrganization`, `engine.ts:20042`). Both the engine
refusal and the door's count leave it out, and the walk keeps every
reference such a row holds (`keepsOrganizationReference`,
`engine.ts:20065`, applied at `:19640`):
- **`managedBy: 'append-only'`, the ledgers** (seat decision
6305179819). There are exactly **eight**: `sys_activity`,
`sys_audit_log`, `sys_email`, `sys_identity_email`, `sys_job_run`,
`sys_metadata_audit`, `sys_approval_action`, `sys_presence`. Four of
them hold a reference to an organization:
- `sys_activity.organization_id`, `sys_email.organization_id` and
`sys_approval_action.organization_id`;
- `sys_audit_log`'s attribution `tenant_id` (`systemFields.tenant:
false`, so it is not the tenant column).
- **`managedBy: 'engine-owned'`, rows the platform writes** (seat
decision 6307135388). Seventeen of them have an `organization_id`; they
are listed in **Round 4** below.
- **An attribution `organization_id` on a tenancy-disabled object**
(seat decision 6305996369, B5, the closed rule). Measured over every
object registered on a booted showcase with audit, approvals,
notifications, mail and sharing composed: **no object matches**.
`sys_api_key` is the only tenancy-disabled object with an organization
lookup, and its `active_organization_id` is a pointer.
- **Pointers keep `set_null`**: `sys_session.active_organization_id`,
`sys_api_key.active_organization_id`,
`sys_organization.parent_organization_id`.
**4. Atomic.** `/organization/delete` runs through the same
request-level unit of work as subject erasure (`auth-manager.ts:6740`):
one engine transaction, rolled back on any 4xx/5xx. On a driver without
transactions (Turso remote, `transactionsUnsupported`) the refusals are
the guarantee, because both run before the first write: the record count
(1), and the Default Organization's guard question (6).
**5. The engine never clears a tenant column.**
`refuseOrganizationDeleteWhileOwned` (`engine.ts:20160`, called at
`:19624`) refuses a by-id `sys_organization` delete while any
tenant-column row outside the cascade set names it. It refuses with `409
DELETE_RESTRICTED` and the same `dependents` list, before the walk
cascades or clears anything.
- It **counts** each table (`countReferencingRowsOutside`, `:20079`),
and never reads rows. The exception is a table the cascade itself
deletes rows of, which no platform object is.
- A count is disclosed only when the caller's own identity counts the
same (`:20122`).
- A **predicate (`multi`) delete of `sys_organization` is refused**
before any hook or write, with `409 DELETE_RESTRICTED` naming the by-id
route (`organizationPredicateDeleteRefusal`, `:754`, applied at
`:20509`): only the by-id path runs this referential pass.
- The door's 409 and the adapter's mapping of the engine refusal both
carry `dependents` to the wire
(`packages/plugins/plugin-auth/src/objectql-adapter.ts:596`, `:650`).
- Nothing in `packages/spec` changes.
**6. The Default Organization.** It is identified by
`isDefaultOrganizationTarget` (`organization-delete.ts:216`: slug
`default`, else the only organization, never under a wall). That
decision is made outside the door's fall-through
(`auth-manager.ts:8160`), so a failed lookup refuses instead of handing
the delete to better-auth. Its structure is not removed. It is asked two
questions, both **in `hooks.before`, before better-auth deletes any
member**:
1. The last-administrator guard's question (`auth-manager.ts:8175`),
through the guard's own judgement
(`LastAdminGuardHandle.judgeOrganizationDelete`,
`last-admin-guard.ts:900`, `:1892`, wired in `auth-plugin.ts:1701`). It
answers `403 PERMISSION_DENIED` while the delete would leave no platform
administrator.
2. Once the guard admits (an organization-less `admin_full_access`
grant, or a verified `OS_PLATFORM_OWNER_EMAIL` user, stands), its record
count (`defaultOrganizationDeleteRefusal`, `organization-delete.ts:178`,
asked at `auth-manager.ts:8184`). This leaves out only the members and
invitations better-auth removes, so its teams, its grants and its
records count. A refusal is the same `409 DELETE_RESTRICTED`.
- Asked before the vendor's writes, the guard judges the whole change:
the organization row **and the members that leave with it**
(`pendingFor`, `last-admin-guard.ts:640`). Otherwise an owner's
membership of the very organization being deleted would still read as
standing.
- On main the 403 arrived after the members and invitations were gone.
### The wire code: `DELETE_RESTRICTED`, not a new
`ORGANIZATION_NOT_EMPTY`
A new code needs a row in
`packages/spec/src/api/error-code-ledger.zod.ts`, outside the claim's
surface. The ledger also refuses a registered synonym of a standard
member, and the condition is the standard catalog's `DELETE_RESTRICTED`.
Accepted by the seat.
## Review round: contract review 6305988777 (FAIL), answered
- **B1 (CI red).** Two suites pinned the superseded contract and are
rewritten to the ruled one, each with a positive control; neither is
deleted or weakened.
- `packages/runtime/src/default-organization-statements.golden.test.ts`,
"the Default Organization deleted and recreated". The direct delete is
first refused (`DELETE_RESTRICTED`, 409, standing survives). Then the
ruled remedy runs: the rows it owns are moved to a holding organization,
the organization is deleted and recreated, the rows are moved onto the
new one, and the next request reads the new id, as the memo pin asks.
-
`packages/plugins/plugin-sharing/src/business-unit-parent-delete-restrict.test.ts`.
"An organization's delete completes, its units stay" becomes "refused
while it owns units, `dependents: [{ sys_business_unit, 3 }]`, tree
whole", plus the CONTROL "once its units are deleted, leaf to root, the
organization deletes".
- Every test in the tree that deletes an organization was found by grep
(objectql, runtime, plugin-auth, plugin-sharing, platform-objects, the
hono adapter, dogfood). Their packages' full suites are in **Final
readings**.
- **B2.** A predicate delete of `sys_organization` is refused (item 5).
Pinned, with a control that another object's multi delete is not caught.
- **B3.** The Default Organization's guard question is asked before the
first write (item 6).
- Seam pins (`last-admin-guard.default-organization.test.ts`): it
refuses on the grant anchor; it refuses when the admin's only standing
is owning the Default Organization, which needs the member companion
(ablated: without it, exactly that case goes red); it admits when
another administrator stands elsewhere.
- Door pin (federated dogfood (e)): an engine `beforeDelete` recorder on
`sys_member` and `sys_invitation` sees **no delete attempted** by the
refused request, and members, invitations and teams are unchanged. That
does not depend on the transaction. A positive control on the 200 delete
in the same file proves the recorder fires.
- The changeset's no-transactions sentence is kept, now true for both
refusals.
- **B4.** Every lookup to `sys_organization` on an append-only ledger is
kept (item 3). Pinned in objectql (an append-only object's non-tenant
lookup keeps naming it, with no write) and in dogfood (b) (every
`sys_audit_log` row whose `tenant_id` named the organization still does
after the delete).
- **B5.** The closed rule is implemented as written and pinned in
objectql (a tenancy-disabled object's `organization_id` is kept and not
counted; a pointer is still cleared). It touches no object in this tree.
`sys_approval_request` is walled, so the decision's premise about it did
not hold. Decision 6307135388 settles it (**Round 4**).
- **B6.** The changeset names the seed replay and its remedy. Measured
on a walled `os serve` (showcase + `@objectstack/organizations`): a
fresh organization owns **132 rows in 19 tables**:
- `showcase_account` 14, `showcase_announcement` 2,
`showcase_business_unit` 9, `showcase_category` 4,
`showcase_client_brief` 2, `showcase_contact` 32,
`showcase_expense_line` 13;
- `showcase_expense_report` 4, `showcase_field_zoo` 2,
`showcase_inquiry` 3, `showcase_invoice` 12, `showcase_invoice_line` 5,
`showcase_preference` 1, `showcase_product` 4;
- `showcase_project` 5, `showcase_project_membership` 3, `showcase_task`
10, `showcase_team` 2, `sys_business_unit` 5.
- **B8.** The adapter carries `dependents` through to the wire 409 (item
5). Pinned in `objectql-adapter.test.ts`, a withheld count staying
absent.
- **B9.** The engine refusal counts (item 5). Pinned: refusing an
organization that owns 5 rows issues no row read of the owning table.
- **B7, B10, B11.** Accepted as declared. The ledger count is corrected:
eight objects, not nine.
## Measurements (counts only)
**Upgrade fixtures**, re-measured on `e184b513c`, after the engine-owned
exemption; the figures are identical to round 3's (walled boot over
each, `AuditPlugin` composed). Three door-created organizations per
fixture: untouched; holding one `sys_business_unit`; holding one record.
| Fixture | `sys_team.organization_id` NOT NULL | Untouched | With a
business unit | With a record |
|---|---|---|---|---|
| ceremonied 18.0 (after `os migrate organization-ownership --apply`:
complete, 0 blocking) | yes | `200`; organization, members, invitations,
teams, team members, grants all 0; `sys_activity` naming it 4 → 7 (kept,
plus the delete's own), 0 NULL | `409` `sys_business_unit (1)`, every
count unchanged | `409` `showcase_category (1)`, every count unchanged |
| `fixture-18.0.0` | no | the same | the same | the same |
After that delete, on the ceremonied database: the ceremony re-run
answers `complete: true`, `blocking: 0` (its post-check holds only
`organization_id IS NULL` rows), and HEAD boots it through
`OrganizationOwnershipBootGuardPlugin` (which decides on one field of
the `sys_migration` marker). `check:tenant-audit-census` is a static
count of source. A REST read over the kept ledger rows answers `200`.
The lifecycle sweep's dangling-reference audit only reports, into its
sweep report.
**Walled `os serve`**, re-measured on `e184b513c`
(`OS_TENANCY_POSTURE=isolated`, `OS_PLATFORM_OWNER_EMAIL`,
`OS_AUTH_MEMBERSHIP_POLICY=invite-only`, a scratch copy of the showcase
declaring `@objectstack/organizations`, the RUNNER.md recipe;
`Organizations`, `Security` and `Audit` mounted):
- health `200`, sign-in `200`, 0 error lines;
- a fresh organization: `409` naming the 19 tables / 132 rows above,
every count unchanged;
- after the operator's remedy (its records deleted over `DELETE
/api/v1/data/...`, three passes for ordering): `200`, with organization,
members, teams and grants 0;
- read from the databases: `sys_activity.organization_id` 312 rows still
name the deleted organization, 0 NULL; `sys_audit_log.tenant_id` 312
rows still name it. `sys_approval_request`, `sys_setting`,
`sys_record_share` and `sys_notification` hold no row naming it on this
boot, and none with a NULL `organization_id`. The 11 audit rows with a
NULL `tenant_id` are deployment-level rows written at boot and by the
delete itself, not cleared ones; dogfood (b) pins that every row naming
the organization before the delete still does after it.
**R1** fresh `pnpm dev -- --fresh` (showcase, `single`): health `200`,
seeded-admin sign-in `200`, 0 error lines; Default Organization delete
`403`, members 3 → 3, records 14 → 14 (measured last round; the Default
Organization path is re-pinned this round by dogfood (e)).
**R2** `upgrade-18x-harness.dogfood.test.ts`: see **Final readings**.
**Main baseline** (`e4d604efd`): Default Organization delete `403` with
`sys_member` 1 → 0.
## Round 5: delta review 6308041940 (R1 blocking, R2, R3), answered
- **R1 (blocking): the Default Organization's delete is counted before
the first write.** Once the last-administrator guard admits the delete,
`hooks.before` asks `defaultOrganizationDeleteRefusal`
(`organization-delete.ts:178`, asked at `auth-manager.ts:8184`). It
counts every table holding rows that name the organization, leaving out
only the members and invitations better-auth removes
(`VENDOR_REMOVED_ON_DELETE`). Its teams, its grants and its records
count, since plugin-auth removes none of its structure. A refusal is the
same `409 DELETE_RESTRICTED`, before better-auth's first write, so a
driver without transactions no longer gets a partial delete on this
path.
- Pinned in federated dogfood under a config anchor: the admin is named
in `OS_PLATFORM_OWNER_EMAIL` and verified, so the guard admits.
- The response is `409 DELETE_RESTRICTED`. It names the Default
Organization's records and does not list `sys_member` or
`sys_invitation`.
- An engine recorder on `sys_member` and `sys_invitation` sees **no
delete attempted**.
- Members, invitations, teams and the organization row are unchanged.
- Ablated (the count skipped for the Default Organization): exactly that
pin goes red, and the other 5 stay green.
- At the seam (`organization-delete.test.ts`): the exclusion is exactly
members and invitations, and no organization-admin grant is subtracted.
- Re-read and corrected so they hold:
- the changeset's opening paragraph (`single` deployments) and its
no-transactions bullet;
- a new Default Organization bullet;
- the contract notes in `organization-delete.ts`, items 3 and the
closing paragraph, and `isDefaultOrganizationTarget`'s doc.
- **R2: the organization counts carry none of the caller's tenant
scope.** `countReferencingRowsOutside` and `organizationOwnedRowCounts`
now count under `organizationCountContext` (`engine.ts:20109`). That
context is system identity, keeping only the request's transaction (so
the count sees the request's own writes) and its trace. Ordinary
reference probes keep `referenceCheckContext` and the caller's scope.
- Pinned in objectql with a tenant-scoping middleware. CONTROL: the
caller's own count for the organization reads 0 under it.
- `organizationOwnedRowCounts` under a foreign `tenantId` still counts
2.
- The engine refusal still refuses a caller scoped to another
organization.
- Ablated (back to `referenceCheckContext`): both pins go red.
- **R3: deciding whether the target is the Default Organization is
outside the fall-through.** `isDefaultOrganizationTarget` runs after the
`try` (`auth-manager.ts:8160`), so a lookup that fails there refuses
instead of handing the delete to better-auth.
- Pinned in federated dogfood. A non-default organization the admin owns
is made active, and the organization count fails (a middleware on
`sys_organization`). Under these conditions:
- the request is refused (≥ 400);
- a recorder on the session's active-organization write (better-auth's
first) and on member, invitation and organization deletes sees nothing
attempted;
- the organization and its member are unchanged.
- CONTROL: with the lookup back, the same organization deletes with
`200`.
- Ablated (the decision moved back into the `try`): the pin goes red,
because better-auth's session write was attempted.
- Before this round the pin recorded deletes only, and stayed green
under the same ablation, because the last-administrator guard's own
member hook threw first. The session write is what makes it
discriminating.
## Round 4: seat decision 6307135388 (option B), answered
**Exempt by declaration.** `managedBy: 'engine-owned'` joins `managedBy:
'append-only'` in `ObjectQL.outlivesOrganization` (`engine.ts:20042`),
which is the one predicate behind all three of these:
- the engine refusal (`refuseOrganizationDeleteWhileOwned`);
- the door's count (`organizationOwnedRowCounts`, which plugin-auth
asks);
- the walk's kept references (`keepsOrganizationReference`).
No list of names is involved. Such rows are not counted. They are kept,
and go on naming the deleted organization: never NULL, never deleted.
**The 17 engine-owned objects with an `organization_id`**, measured over
every object registered on a booted showcase (audit, approvals,
automation, messaging, mail and sharing composed: 109 objects). The
other fourteen of the 31 engine-owned objects carry no organization
reference and are untouched: the `sys_metadata*` family (with
`sys_metadata_activation`), `sys_platform_setting*`, `sys_migration*`,
`sys_secret`, `sys_flow_dispatch`, `sys_flow_credential`, `sys_job`,
`sys_job_queue`, `sys_scim_connection_credential`.
| Object | What writes its rows |
|---|---|
| `sys_approval_request` | plugin-approvals' approval service: one row
per submission, advanced by the flow until it completes. |
| `sys_approval_approver` | the approval service: the normalized
pending-approver rows behind the inbox query, one per approver per step.
|
| `sys_approval_token` | the approval service: the single-use tokens
behind actionable approval links. |
| `sys_automation_run` | service-automation's flow runner: paused runs
waiting to resume, and the history of finished runs. |
| `sys_notification` | service-messaging: one row per notification event
the platform sent. |
| `sys_notification_receipt` | service-messaging: per recipient ×
channel. Read state is set by the recipient through the messaging API. |
| `sys_notification_delivery` | service-messaging: the durable
per-recipient × channel delivery outbox. |
| `sys_http_delivery` | service-messaging: one row per outgoing webhook
or flow HTTP call, with its delivery status. |
| `sys_mailbox` | **see below.** The mailbox sync writes its IMAP
cursor. |
| `sys_mail_message` | service-mailbox's sync and ingest: inbound mail
mirrored from a mailbox. |
| `sys_mail_association` | service-mailbox's ingest: links a mirrored
message to the records it matched. |
| `sys_audience_binding_suggestion` | a package's suggestion, written by
the platform when the package is installed. A `manage_metadata` holder
accepts or dismisses it. |
| `sys_record_share` | plugin-sharing: rule grants (`source = 'rule'`)
written and reconciled by the sharing-rule evaluator, and **see below**
for `source = 'manual'`. |
| `sys_share_link` | **see below.** |
| `sys_import_job` | the import service: the state, progress and history
of a bulk import. |
| `sys_setting` | **see below.** |
| `sys_setting_audit` | `SettingsService`: the audit trail of every
settings change. |
**Rows that are an administrator's or a user's own act, with a door
other than the data API.** These were named for a separate decision.
**Decision 6307815346 settles them: the declaration decides, with no
per-table exceptions, so all are kept, and it amends 6305179819 on
settings.**
1. **`sys_setting`** (organization-level settings values). An
administrator sets them through `SettingsService.set` (the Setup
settings pages), and `resetNamespace` removes them. Decision 6305179819
said "settings stay records". Under 6307135388's rule, a `sys_setting`
row no longer refuses the delete. If 6305179819 meant `sys_setting`, the
two decisions now conflict on this table. (If it meant
organization-level settings stored elsewhere, there is no conflict: no
other object named for settings has an `organization_id`.)
2. **`sys_mailbox`.** It is a mailbox's identity (address, owner), and
only `manage_mailboxes` holders can read the directory. No code in this
tree creates one; tests insert them directly, so in a deployment the
operator provisions them. It is administrator configuration with no
delete door.
3. **`sys_record_share` with `source = 'manual'`, and
`sys_share_link`.** A user creates them through `POST
/data/:object/:id/shares` and the share-link service's `createLink`, and
revokes them through the same service: `revokeLink` sets `revoked_at`
and keeps the row. Decision 6307135388 names shares as engine-derived,
so they are exempt as decided.
**Pinned.**
- objectql (`engine-organization-delete-refusal.test.ts`):
- an organization whose only rows are an engine-owned approval request
and its append-only actions deletes, and both are kept naming it with no
write;
- CONTROL: beside them, a record still refuses, and only the record is
named.
- Ablated (`engine-owned` removed from the predicate): exactly these two
pins go red, and the other 15 stay green.
- dogfood (`organization-delete-refuse-nonempty.dogfood.test.ts`), on a
booted showcase:
- an organization created through the door, holding one real
`sys_approval_request` and its two `sys_approval_action` rows, deletes
with `200`;
- the request and both actions are unchanged and still name it;
- 0 rows in either table have a NULL `organization_id`.
**Changeset.** It says that engine-owned and append-only rows outlive
the organization and go on naming it, and it lists what engine-owned
covers. The **Fix** bullet no longer lists settings values among the
records.
**Merged `main`** (`3b3830ca2`, which includes `62bc12930`) with a merge
commit. It merged cleanly.
## File surface
Inside the claim: `packages/objectql/src/**`,
`packages/plugins/plugin-auth/src/**`, `packages/qa/dogfood/test/**`,
one changeset. Outside it:
- **Accepted by the seat for generated artefacts only:**
`content/docs/permissions/tenant-audit-census.mdx` and
`docs/audits/2026-08-tenant-audit-write-call-sites.counts.md`
(`tenant-audit-census.mjs --write`),
`scripts/engine-double-contract.pinned.json`
(`check-engine-double-contract --write`).
- **This round, by the review's B1:**
`packages/runtime/src/default-organization-statements.golden.test.ts`
and
`packages/plugins/plugin-sharing/src/business-unit-parent-delete-restrict.test.ts`,
the two suites the review named.
No `packages/spec/src/**`, no `docs/adr/**`, no governed surface.
## Tests
- `packages/objectql/src/engine-organization-delete-refusal.test.ts`:
- the refusal, nothing written, a declared `set_null` tenant column
refused;
- controls: an empty organization deletes, a pointer is cleared, a
missing table refuses nothing;
- ledgers: kept, a record still refusing beside one, the count excluding
them;
- B4 (non-tenant ledger lookup kept), B5 (tenancy-disabled attribution
kept and not counted, pointer cleared);
- B2 (predicate delete refused, control);
- B9 (no row read while refusing);
- R2 (round 5): the counts under a foreign tenant scope, through a
tenant-scoping middleware with its CONTROL;
- `organizationOwnedRowCounts`.
- engine-owned (round 4): an approval request and its actions are kept
and the organization deletes; a record beside them still refuses.
- `packages/objectql/src/federated-injected-column-readers.test.ts`:
census rows for the new tenant-column readers.
- `packages/plugins/plugin-auth/src/organization-delete.test.ts`,
`last-admin-guard.default-organization.test.ts` (B3),
`objectql-adapter.test.ts` (B8).
-
`packages/qa/dogfood/test/organization-delete-refuse-nonempty.dogfood.test.ts`,
booted with `AuditPlugin`:
- (a) (c): a refused delete changes nothing;
- (b): the empty delete, with `sys_activity` and `sys_audit_log` rows
kept;
- an organization holding only an approval request and its actions
deletes, and they are kept (round 4);
- a business-unit-only refusal;
- (d): the engine refusal;
- the vendor-refused caller, and the remedy.
- `organization-delete-federated-fixture.dogfood.test.ts`: nothing names
a deleted organization; (e) the Default Organization's 403 with no
member or invitation delete attempted, plus its recorder control; (round
5) R1, the Default Organization admitted by the guard on a config anchor
and refused 409 with no member or invitation delete attempted; R3, a
failed Default Organization lookup refusing before better-auth's first
write, with its CONTROL; the count skipping the federated anchor.
- `org-record-admin-reach.dogfood.test.ts`: structure gone, no orphan,
409 control.
- The B1 rewrites above.
- **Ablations** (restored to HEAD's blob):
- the engine refusal call replaced: its 3 pins red;
- the door gate inverted (rebuilt, dist preflight): dogfood (a) red;
- the guard's member companion deleted: exactly the membership case red,
8 others green.
- `engine-owned` removed from `outlivesOrganization`: exactly the two
round-4 pins red, 15 others green.
- Round 5:
- the Default Organization's count skipped: R1's pin red, 5 others
green;
- `organizationCountContext` back to `referenceCheckContext`: both R2
pins red;
- the Default Organization decision moved back into the `try`: R3's pin
red.
- A first attempt at the last one used an empty replacement, which the
tool refuses, and it restored the file to HEAD before that work was
committed. It was re-applied from the edit scripts, committed, then
ablated.
## Verification on the head (139abc4)
Every reading below was taken on 139abc4, which is `e184b513c` plus
the round-5 commits `e40aea862` and `139abc45a`, rebuilt.
| Check | Result |
|---|---|
| `turbo run build --filter='@objectstack/dogfood^...'` | exit 0 |
| objectql tests | 452 files, 8526 passed |
| plugin-auth tests | 176 files, 3356 passed, 10 skipped |
| runtime tests | 432 files, 6794 passed, 19 skipped |
| plugin-sharing tests | 53 files, 1200 passed |
| platform-objects tests | 83 files, 1239 passed |
| hono tests | 12 files, 225 passed |
| plugin-approvals tests | 93 files, 1208 passed |
| typecheck: objectql, plugin-auth, runtime, plugin-sharing, dogfood |
exit 0 each |
| dogfood organization files (refuse-nonempty, federated-fixture,
org-record-admin-reach, organization-update-door) | 4 files, 32 passed |
| upgrade harness | 1 file, 12 passed |
| `dispatch-gates.mjs --commands` | 117 commands, all exit 0 |
| `dispatch-gates.mjs --ran` | 117 derived families: 117 run, 0
NOT-MEASURED |
| `check-governed-merges --branch` | exit 0. 2233 changed lines (≤ 3000)
|
The fixture and walled `os serve` measurements above were taken on
`e184b513c`. Round 5 changes only the Default Organization's path and
the count's context; neither fixture's organizations nor the serve's
fresh organization is the Default Organization. **19 tables / 132 rows
is unchanged by the engine-owned exemption.** Every seeded row is in a
`showcase_*` table or `sys_business_unit`, and none of those is
engine-owned or append-only, so the changeset's figure stands as
written. CI check-runs were not waited on.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_016gmigttoA9DyRNtz6SepWf
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 4bf079c commit 688052e
20 files changed
Lines changed: 2189 additions & 44 deletions
File tree
- .changeset
- content/docs/permissions
- docs/audits
- packages
- objectql/src
- plugins
- plugin-auth/src
- plugin-sharing/src
- qa/dogfood/test
- runtime/src
- scripts
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
122 | 122 | | |
123 | 123 | | |
124 | 124 | | |
125 | | - | |
| 125 | + | |
126 | 126 | | |
127 | 127 | | |
128 | 128 | | |
| |||
187 | 187 | | |
188 | 188 | | |
189 | 189 | | |
190 | | - | |
| 190 | + | |
191 | 191 | | |
192 | | - | |
193 | | - | |
| 192 | + | |
| 193 | + | |
194 | 194 | | |
195 | 195 | | |
196 | 196 | | |
| |||
201 | 201 | | |
202 | 202 | | |
203 | 203 | | |
204 | | - | |
| 204 | + | |
205 | 205 | | |
206 | 206 | | |
207 | 207 | | |
208 | 208 | | |
209 | 209 | | |
210 | | - | |
| 210 | + | |
211 | 211 | | |
212 | 212 | | |
213 | 213 | | |
214 | | - | |
| 214 | + | |
215 | 215 | | |
216 | 216 | | |
217 | 217 | | |
| |||
223 | 223 | | |
224 | 224 | | |
225 | 225 | | |
226 | | - | |
227 | | - | |
| 226 | + | |
| 227 | + | |
228 | 228 | | |
229 | | - | |
| 229 | + | |
230 | 230 | | |
231 | | - | |
| 231 | + | |
232 | 232 | | |
233 | 233 | | |
234 | 234 | | |
235 | 235 | | |
236 | | - | |
| 236 | + | |
237 | 237 | | |
238 | 238 | | |
239 | 239 | | |
240 | 240 | | |
241 | 241 | | |
242 | | - | |
| 242 | + | |
243 | 243 | | |
244 | 244 | | |
245 | 245 | | |
246 | 246 | | |
247 | | - | |
248 | | - | |
| 247 | + | |
| 248 | + | |
249 | 249 | | |
250 | 250 | | |
251 | 251 | | |
| |||
297 | 297 | | |
298 | 298 | | |
299 | 299 | | |
300 | | - | |
| 300 | + | |
301 | 301 | | |
302 | 302 | | |
303 | 303 | | |
304 | | - | |
| 304 | + | |
305 | 305 | | |
306 | 306 | | |
307 | 307 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
33 | 33 | | |
34 | 34 | | |
35 | 35 | | |
36 | | - | |
37 | | - | |
| 36 | + | |
| 37 | + | |
38 | 38 | | |
39 | | - | |
| 39 | + | |
40 | 40 | | |
41 | | - | |
| 41 | + | |
42 | 42 | | |
43 | 43 | | |
44 | 44 | | |
45 | 45 | | |
46 | | - | |
| 46 | + | |
47 | 47 | | |
48 | 48 | | |
49 | 49 | | |
| |||
90 | 90 | | |
91 | 91 | | |
92 | 92 | | |
93 | | - | |
| 93 | + | |
94 | 94 | | |
95 | 95 | | |
96 | 96 | | |
97 | | - | |
| 97 | + | |
98 | 98 | | |
99 | 99 | | |
100 | 100 | | |
| |||
155 | 155 | | |
156 | 156 | | |
157 | 157 | | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
158 | 161 | | |
159 | 162 | | |
160 | 163 | | |
| |||
0 commit comments