@@ -302,6 +302,25 @@ contradicts it, and correct it here when it does.
302302 the real port). This is "zero hits needs a positive control" applied to one tool: a
303303 negative from a command that never ran is not evidence.
304304
305+ - ** A SIGTERM to the ` pnpm ` wrapper does not stop the server under it — start a
306+ backgrounded boot with ` setsid ` and stop its process group.** The canonical boot (in the
307+ ` --no-ui ` entry below) is ` pnpm -C … exec objectstack dev … ` : pnpm spawns ` os dev ` , which
308+ spawns ` serve --dev ` (` packages/cli/src/commands/dev.ts ` ). ` os dev ` forwards a SIGINT or
309+ SIGTERM sent to its own pid to that child (its ` process.on('SIGTERM', …) ` handler calls
310+ ` beginShutdown() ` of ` ServeRestartCoordinator ` , ` packages/cli/src/utils/dev-restart.ts ` ),
311+ but nothing forwards one sent to pnpm, so ` kill ` on the pnpm pid leaves the ` serve `
312+ grandchild listening. Both runners of the 18.0 sweep's round 3 met it (wave anchor
313+ #24579 §6). Measured at
314+ ` 46bb5e3e54 ` with a stand-in tree, ` pnpm -C examples/app-showcase exec node parent.js `
315+ where ` parent.js ` forwards SIGTERM to an HTTP child as ` os dev ` does: two seconds after a
316+ SIGTERM to the pnpm pid the child still answered 200, and a SIGTERM to the process group
317+ stopped all four processes. ⚠️ ` $! ` is not the group id: a backgrounded ` setsid ` forks
318+ when the shell made it a group leader, so ` $! ` names a process that has already exited.
319+ ** So:** ` setsid pnpm -C … exec objectstack dev … > LOG 2>&1 & ` ; once the port answers,
320+ read the group from the listener, ` PGID=$(ps -o pgid= -p "$(lsof -ti tcp:PORT)" | tr -d ' ') ` ;
321+ stop it with ` kill -TERM -- -$PGID ` , and confirm ` lsof -ti tcp:PORT ` then prints nothing.
322+ ⛔ Never stop a server by name (` pkill ` ), and never a group you did not start.
323+
305324- ** Console session auth is a bearer token in ` localStorage ` , and a form sign-in ALSO
306325 sets a cookie — you need both halves.** The console's auth client stores the session
307326 under ` auth-session-token ` (objectui ` packages/auth/src/createAuthClient.ts ` :
@@ -466,17 +485,67 @@ contradicts it, and correct it here when it does.
466485 So on 18.0.0
467486 ` OS_TENANCY_POSTURE=group ` or ` isolated ` (or the legacy ` OS_MULTI_ORG_ENABLED=true ` , which
468487 ` resolveTenancyPosture() ` in ` packages/types/src/env.ts ` reads as ` isolated ` ) boots through
469- ` os serve ` , and so ` os dev ` and ` os start ` . Each prerequisite below refuses the boot with a
470- FATAL naming itself when absent, so a refusal tells the runner what is missing, never that
471- the posture is unsupported:
488+ ` os serve ` , and so ` os dev ` and ` os start ` . Through ` os dev ` , each prerequisite below
489+ refuses the boot with a FATAL naming itself when absent (QA run # 24783 measured all three),
490+ so a refusal tells the runner what is missing, never that the posture is unsupported:
472491 1 . the multi-org runtime ** declared by the app** : ` @objectstack/organizations ` in the app's
473492 own ` package.json ` and installed there. Reachable only through ` NODE_PATH ` or a hoisted
474493 store is refused on purpose (ADR-0093 D5). No example app declares it, so a walled run
475- uses a scratch app that does;
494+ uses a scratch app that does: a ** full copy** of ` examples/app-showcase ` , ` src/ `
495+ included (without ` node_modules ` , ` .objectstack ` or ` dist ` ), whose own ` package.json `
496+ carries every showcase dependency as a ` link: ` to its workspace package plus
497+ ` "@objectstack/organizations": "link:<abs>/packages/plugins/organizations" ` , installed
498+ from the copy with ` pnpm install --ignore-workspace ` (QA run #24783 ). A scratch config
499+ that only re-exports the showcase's ` objectstack.config.ts ` does not boot: a package-file
500+ reference resolves against the root being served, and the showcase's OpenAPI connector
501+ names its document that way (` spec: './src/system/connectors/status-openapi.json' ` in
502+ ` examples/app-showcase/src/system/connectors/index.ts ` , read relative to the package
503+ root at materialization, where a missing document fails the boot);
476504 2 . ` OS_PLATFORM_OWNER_EMAIL ` , the deployment's platform administrators: first-registrant
477505 promotion is off under a wall;
478506 3 . a declared membership policy, ` OS_AUTH_MEMBERSHIP_POLICY ` (for example ` invite-only ` ).
479507
508+ ⚠️ ** A bare ` os serve ` in production mode judges none of the three until it has an auth
509+ secret.** Without ` --dev ` , and with ` NODE_ENV ` anything but ` development ` , ` serve ` composes
510+ the auth family only when ` OS_AUTH_SECRET ` (or a legacy spelling) resolves —
511+ ` resolvePlatformAuthComposition() ` in ` packages/core/src/stack-auth.ts ` , reason
512+ ` no-secret ` — and all three checks sit behind it: the multi-org runtime is loaded inside
513+ ` serve.ts ` 's auth-composition branch, the owner-email check runs in ` AuthPlugin ` 's ` init() `
514+ (` packages/plugins/plugin-auth/src/auth-plugin.ts ` ), and the membership-policy gate skips a
515+ kernel with no ` auth ` service (` assertWalledMembershipPolicyDeclared() ` in
516+ ` packages/plugins/organizations/src/membership-policy-gate.ts ` ). With the secret unset the
517+ boot prints ` ⚠ AuthPlugin skipped — set OS_AUTH_SECRET … ` and comes up
518+ ` ⚠ Server is ready — DEGRADED: missing core services: auth ` (` printServerReady() ` in
519+ ` packages/cli/src/utils/format.ts ` ), not a FATAL: QA run #24783 measured it with
520+ ` os serve --no-ui ` from the scratch app, once with prerequisite 2 absent and once with
521+ prerequisite 3 absent. A production ` serve ` also needs a stable encryption key,
522+ ` OS_SECRET_KEY ` or a key file an earlier boot persisted; without one ` LocalCryptoProvider `
523+ refuses the boot naming ` OS_SECRET_KEY `
524+ (` packages/services/service-settings/src/local-crypto-provider.ts ` ). ` os dev ` needs
525+ neither: it spawns ` serve --dev ` , whose development boot falls back to
526+ ` DEV_AUTH_SECRET_FALLBACK ` (` resolveAuthSecret() ` , same ` stack-auth.ts ` ) and persists a key.
527+ ` os start ` hands its ` serve ` child an auth secret (its ` --auth-secret ` flag, the
528+ environment, or the ` auth-secret ` file it persists in its home directory) and opts the
529+ provider into a persisted key (` OS_CRYPTO_AUTOKEY ` ), in ` packages/cli/src/commands/start.ts `
530+ — read from the code, not booted. ** So:** boot a walled run through ` os dev ` , or give a bare
531+ ` serve ` ` OS_AUTH_SECRET ` and a stable key first. ⛔ Never read a DEGRADED walled boot as
532+ "the prerequisites are met", and never score a walled clause on it.
533+
534+ ** Walled personas come from the admin door.** The walled boot keeps self-registration
535+ closed: the showcase declares no ` auth.audience ` , so ` POST /api/v1/auth/sign-up/email `
536+ answers ** ` 403 SELF_REGISTRATION_CLOSED ` ** exactly as on a stock boot
537+ (` decideAudienceAdmission() ` in ` packages/plugins/plugin-auth/src/audience-posture.ts ` ; the
538+ entry "A stock showcase boot refuses self-registration" above). QA run #24783 minted every
539+ walled persona as the operator (the seeded admin, the address ` OS_PLATFORM_OWNER_EMAIL `
540+ names) with ` POST /api/v1/auth/admin/create-user ` (` runAdminCreateUser() ` in
541+ ` packages/plugins/plugin-auth/src/admin-user-endpoints.ts ` ), then had each persona
542+ ` POST /api/v1/auth/change-password ` . Under ` invite-only ` each lands with
543+ ` membershipCreated: false ` and in no organization (the reconciler's ` policy-skip ` ,
544+ ` reconcileMembership() ` in ` packages/plugins/plugin-auth/src/reconcile-membership.ts ` ); a
545+ persona that needs an organization creates its own with
546+ ` POST /api/v1/auth/organization/create ` and switches to it with ` set-active ` (the next
547+ entry).
548+
480549 Measured at ` 45898dbf6 ` : a scratch app declaring the real runtime, booted by the built CLI
481550 under ` isolated ` with the other two set, mounted ` Organizations ` , answered
482551 ` GET /api/v1/discovery ` 200 and logged no system-write refusal. The seven walled
0 commit comments