Repository navigation
Commit 6ff6ed6
fix(plugin-auth): the owner-bind gate decides once, so a first boot logs no refused sys_migration insert (#22336)
Fixes #22099
Clause-②: no
The one-time owner-bind gate (`createEnsureDefaultOrganizationOnce`,
`packages/plugins/plugin-auth/src/default-org-bootstrap-once.ts`) now
marks its decision in flight synchronously, before its first `await`. A
call that finds the decision in flight runs `ensure(ql, { bindOwner:
false })` and records nothing. The deciding call records its own outcome
once. A call that did not act (`no_admin`, a refused write) clears the
mark on the way out, so the next trigger still decides. This is the
direction triage ruled (`6042758404`, option A). There is no promise
chain, no ledger upsert or insert-if-absent, and no catch-all.
Triage folded #22102 into this card as a
duplicate (same row, same mechanism); #22102 remains open, and its
disposition is the seat's.
## Re-measured on the landed shape (`origin/main` `28bff18d`, after PR
#22186)
The gate still re-enters itself. The Default Organization boot invariant
(ADR-0131 D3) changes which write re-enters it: the outer call now
PROMOTES the reconciler-written `member` row instead of inserting
`sys_member`.
The chain on a first boot, outermost first (probe stacks through
`bootStack`):
- `kernel:ready`, `security-plugin.ts:4701` `runBootstrap`,
`bootstrap-platform-admin.ts:1170` `promote`, `:407` insert
`sys_user_permission_set`;
- plugin-auth middleware `auth-plugin.ts:1255`, `runEnsure` `:1224`: the
OUTER call;
- `ensure-default-organization.ts:498`, `promoteReconciledMemberToOwner`
`:379`, `ql.update('sys_member', { role: 'owner' })`;
- security-plugin middleware `security-plugin.ts:5145`,
`reconcileOrgAdminGrant` `auto-org-admin-grant.ts:777`, `:238` insert
`sys_user_permission_set`;
- plugin-auth middleware `auth-plugin.ts:1255` again: the INNER call. It
records `{ outcome: 'admin-already-member' }` first. The outer call's `{
outcome: 'promoted', organizationId }` is then refused by the primary
key.
| boot (CLI `os serve`, `examples/app-crm`, `NODE_ENV=development`) |
`sys_migration` failed-insert lines, before | after |
|---|---|---|
| `:memory:` | 1 | 0 |
| file SQLite, first boot | 1 | 0 |
| same file, second boot | 0 | 0 |
| `bootStack` (dogfood harness, empty app) |
`adr-0093-default-org-owner-bind` insert attempts, before | after |
|---|---|---|
| `:memory:` | 2 (second refused) | 1 |
| file, first boot | 2 (second refused) | 1 |
| same file, second boot | 0 | 0 |
Persisted owner-bind `details` (file DB, read back):
- before: `{"outcome":"admin-already-member"}`, while `sys_member` held
the admin as `owner` of the one default organization;
- after:
`{"outcome":"promoted","organizationId":"org_muzty5e5bxpe2dtt"}`, which
is the organization of the admin's `owner` row.
Walled first boot (`bootStack`, `isolated`, `OrganizationsPlugin`
mounted), measured on `main`: ONE owner-bind insert,
`{"outcome":"bound","organizationId":…}` matching the operator's
`sys_member` row, and no warning. Under a wall the grant-insert arm of
`isDefaultOrganizationBootstrapTrigger` is retired, so this chain does
not re-enter the walled wiring today. The walled wiring calls the same
gate and gets the same rule.
## Pins
- `packages/plugins/plugin-auth/src/default-org-bootstrap-once.test.ts`:
the gate over a fake engine whose `sys_migration` insert refuses a
duplicate id and whose `sys_member` writes re-enter the gate.
- a fresh bind records `bound` once, with the org `sys_member` points
at;
- the promotion (the landed boot shape) records `promoted` once;
- a concurrent trigger binds nobody: one owner row, one record;
- a call that did not act clears the mark;
- CONTROL: a real ledger insert failure still reaches
`recordLedgerDecision`'s `error` branch.
-
`packages/plugins/organizations/src/walled-default-org-owner-bind-in-flight.pin.test.ts`
sits beside `walled-default-org-self-registrant.pin.test.ts`. A
`sys_user` `email_verified` update reaches `OrganizationsPlugin`'s own
bootstrap middleware while the bind is in flight. The ledger is written
once, `bound`, with the org `sys_member` points at.
-
`packages/qa/dogfood/test/sys-migration-boot-ledger-once.dogfood.test.ts`
is the family's enumeration pin. It boots a fresh database twice over
one file through `bootStack`. The ids come from the table's own rows,
and each one must have been inserted exactly once. No line at `WARN` or
above may name `sys_migration`. The owner-bind row must read the
deciding call's outcome with its owner's organization. The control
requires the capture to have parsed an `INFO` line naming the ledger.
- **Deviation from the ruled pin text:** triage's pin says the persisted
`details` read `bound`. On the landed full boot the accurate outcome is
`promoted` (ADR-0131 D3 binds the admin as `member` before the gate
runs). So the dogfood pin asserts `promoted`, and the `bound` arm is
pinned at the unit and walled layers, where the gate inserts the owner
row.
## Ablation (at `d286091a08`, fix committed first; delete the
synchronous mark)
The mark (`deciding = true;`) was deleted with
`scripts/ablation-replace.mjs`: anchor 1 → 0, blob `27c001b5` →
`635eb0e8`. plugin-auth was rebuilt (exit 0), and
`ablation-dist-preflight --absent 'deciding = true'` passed.
- plugin-auth pin: 3 failed, 2 passed. The re-entry, promotion and
concurrent cases are red; the clears-the-mark case and the CONTROL are
green, as predicted.
- walled pin: 1 failed. `expected [ …(2) ] to have a length of 1 but got
2`.
- dogfood pin: 3 failed, 1 passed. `adr-0093-default-org-owner-bind: 2`
against 1; `boot 1: expected [ Array(1) ] to deeply equal []` (the
warning); `{ outcome: 'admin-already-member' }` (the lost update). The
control was green.
The restore was proved: blob `27c001b5` matches HEAD and `git diff HEAD`
is empty. plugin-auth was rebuilt and the preflight found `deciding =
true` in dist again. `git status --porcelain` was empty.
A first ablation leg deleted only the `|| deciding` read. Its JS reached
dist, but the build exited 1 at the DTS step (TS6133: `deciding` was
never read). That leg is VOIDED as a reading, and the leg above replaces
it. Its colours were the same.
## Verification
The package runs below are at `33520be606`. The final commit
`82ae109f5e` changed only the plugin-auth pin's `findOne` double and the
generated `scripts/engine-double-contract.pinned.json`, and the runs it
can move were repeated there. The gate union ran at `82ae109f5e`.
- `pnpm --filter @objectstack/plugin-auth exec vitest run
--maxWorkers=2`: 130 files, 2662 passed, 10 skipped. At `82ae109f5e` the
new file re-ran 5/5 and `check:test-typecheck` re-ran OK.
- `pnpm --filter @objectstack/plugin-auth typecheck`: exit 0.
`check:test-typecheck` held 10 files / 94 errors, unchanged.
- `pnpm --filter @objectstack/organizations exec vitest run
--maxWorkers=2`: 12 files, 152 passed. Its `typecheck` exited 0.
- `pnpm --filter @objectstack/dogfood typecheck`: exit 0. The
enumeration pin: 4/4.
- The full dogfood suite is NOT run locally; it is declared to CI's
`Dogfood Regression Gate`. Only the new file ran here. No importer of
`plugin-auth` owes a test: the diff changes a function body and a module
comment, and no exported declaration.
- ① `turbo run build --filter='@objectstack/plugin-auth^...'
--filter='@objectstack/organizations^...'
--filter='@objectstack/dogfood^...' --concurrency=1`: 63/63.
- Gates: `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 76 commands from this branch's
change set at `82ae109f5e`. All 76 ran there and exited 0. `--ran`
reconciled them: `76 derived famil(ies) accounted for — 76 run, 0
NOT-MEASURED`. Sample verdict lines:
- `check-engine-double-contract: OK — 986 pinned, 129 in the DEBT
ledger, 3 exempt.`
- `check-nul-bytes: OK (scanned 10308 text file(s) …; no raw ASCII
control bytes).`
- `check-test-source-alias OK — 73 packages with tests scanned; 60
registered as still resolving a workspace dep through dist/ …`
- `OK: 30 package(s) read outside themselves, all declared …`
(`check:cross-package-test-inputs`).
- The first pass at `33520be606` had two non-zero lines.
`check:engine-double-contract` was red: the new findOne double was not
routed through `assertEngineFindOnePredicate`, and the ledger lacked the
new rows. Repaired in `82ae109f5e`. `check:dual-build-cjs-loads` printed
`PREREQUISITE NOT MET` (8 packages had no dist). It was NOT MEASURED
then, and it exited 0 after those dists were restored from the turbo
cache.
- eslint, narrowed (CI owns `pnpm lint`): 4 files, 0 errors and 0
warnings by `--format json`. Each file is matched by `eslint.config.mjs`
(`--print-config`), and none is ignored. `eslint.config.mjs` enables no
type-aware linting (no `parserOptions.project`), so this diff cannot
move the verdict on any untouched file.
## Acceptance notes
- `scripts/engine-double-contract.pinned.json` gains three generated
rows (`--write`, grow-only coverage ledger) for the two new pinned
doubles. This file is outside the claim's declared surface, and the gate
requires it for any new pinned double.
- Read-only inference, not reproduced, and no card filed (carrier:
none). On a kernel with NO ledger, an in-flight caller runs `ensure`
with `bindOwner: false` and may still CREATE the default organization
while the deciding call waits. The decider then sees an existing
organization under `bindOnlyOnCreate` and binds nobody.
`ensureDefaultOrganization`'s org creation was already not
concurrency-safe (two concurrent calls can both insert one). Every
served kernel composes the ledger (`PlatformObjectsPlugin`), and
`single` creates the organization at boot.
- Not measured: a production first sign-up (no dev admin). The gate's
rule does not depend on which trigger re-enters it.
- Rows already written as `admin-already-member` are not rewritten.
Nothing reads `details`: `readLedgerDecision` answers existence only.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01WkL6Eijt432S1Y7ekb6ovQ)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent e9a1f5c commit 6ff6ed6
6 files changed
Lines changed: 683 additions & 40 deletions
File tree
- .changeset
- packages
- plugins
- organizations/src
- plugin-auth/src
- qa/dogfood/test
- 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 | + | |
Lines changed: 180 additions & 0 deletions
| 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 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
0 commit comments