From 73b97d909dd66b3230689250e32acc0b0e2044c0 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 1 Sep 2026 16:18:10 +0000 Subject: [PATCH] fix(devx): re-pin the publish smoke to the declared first-run contract MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The packed-tarball smoke asserted that the first POST /auth/sign-up/email after `objectstack dev --fresh` returns 200 — an expectation authored before #11739/#11767 made `invite_only` the default audience posture. It is not the first account that reaches that probe. `--fresh` seeds a dev admin in-process at boot through the real signUpEmail pipeline, and THAT creation is the zero-user one the bootstrap bypass admits. The smoke's own sign-up is the SECOND self-serve account, which invite_only refuses with SELF_REGISTRATION_CLOSED + 403 — correctly. The release candidate had therefore never once passed this gate with the posture default aboard. Per the maintainer-approved ruling (option A, director batch #23): - assert the seeded admin can SIGN IN, which is the packed install's proof that the bootstrap bypass really fired; - assert the uninvited second self-serve sign-up is REFUSED, pinning both the 403 and the envelope code so an accidental widening of the unauthenticated surface reds the RC instead of shipping; - obtain the non-admin probe user through the operator path (the invitation carve-out) before the existing session/CRUD probes. Behaviour only — no vendor-internal symbol is pinned, and no auth or audience runtime code is touched. The posture is READ from the artifact's own public /auth/config rather than hard-coded, because the two smoke modes install different products: `registry` smokes the last PUBLISHED release, and plugin-auth 17.2.0 (2026-08-23) predates #11767 (2026-08-25). In `pack` mode the advertised posture must BE invite_only; in both modes enforcement must match what the artifact advertises. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01WLJQhde67SeTccsmnBVarV --- scripts/publish-smoke.sh | 216 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 201 insertions(+), 15 deletions(-) diff --git a/scripts/publish-smoke.sh b/scripts/publish-smoke.sh index f8e0cef579..4ef12a1e0c 100644 --- a/scripts/publish-smoke.sh +++ b/scripts/publish-smoke.sh @@ -26,14 +26,64 @@ # Both modes then run the scaffolded project's own `build` script and assert it # exits 0 — the step section 1b explains, and the one this gate used to lack. # -# Both modes then boot `objectstack dev --fresh` and assert: -# - GET /api/v1/auth/get-session → 200 (anonymous) -# - POST /api/v1/auth/sign-up/email → 200 -# - POST /api/v1/auth/sign-in/email → 200, session established +# Both modes then boot `objectstack dev --fresh` and assert the DECLARED +# first-run contract (see "the first run this asserts" below): +# - GET /api/v1/auth/get-session → 200 (anonymous) +# - GET /api/v1/auth/config → 200, and in `pack` mode the +# advertised audience posture +# IS the declared `invite_only` +# - POST /api/v1/auth/sign-in/email → 200 (the SEEDED dev admin) +# then, under a closed posture (always in `pack` mode — see the mode note at +# the `/auth/config` probe for why `registry` mode reads it rather than +# assuming it): +# - POST /api/v1/auth/sign-up/email → 403 SELF_REGISTRATION_CLOSED +# (an UNINVITED second account) +# - POST /api/v1/auth/organization/invite-member → 200 (the operator path) +# - POST /api/v1/auth/sign-up/email → 200 (the invitee, admitted +# by the invitation carve-out) +# - POST /api/v1/auth/sign-in/email → 200, session established # - REST CRUD on the scaffolded object (POST/GET/PATCH/DELETE /api/v1/data/…) # - zero error/fatal log lines (specifically the #3091 signature: # "Failed to register OIDC discovery routes") # +# ── the first run this asserts (#14000) ───────────────────────────────────── +# +# Until #14000 this script asserted that the first `POST /auth/sign-up/email` +# after `objectstack dev --fresh` returns 200 — an expectation authored before +# #11767/#11739 made `invite_only` the platform's DEFAULT audience posture. It +# is not the first account that reaches this probe: `objectstack dev --fresh` +# seeds a dev admin IN-PROCESS at boot (`maybeSeedDevAdmin`, plugin-auth) via +# the real `signUpEmail` pipeline, and THAT is the zero-user creation the +# bootstrap bypass admits. The smoke's own sign-up is therefore the SECOND +# self-serve account, which `invite_only` refuses — correctly — with +# `SELF_REGISTRATION_CLOSED` + 403. +# +# So the gate had been red for ~7 days on a product that was behaving exactly +# as declared, and the release candidate had NEVER once passed this smoke with +# the posture default aboard. Ruling (issue #14000, maintainer-approved, +# director batch #23) — re-pin the smoke TO the declared contract, and do not +# touch auth/audience runtime code: +# +# 1. the seeded admin can SIGN IN ⇒ the bootstrap bypass really +# fired inside the packed install +# (it is the only thing that could have created that account); +# 2. the second self-serve sign-up is REFUSED with the envelope `code` +# SELF_REGISTRATION_CLOSED and status 403 ⇒ the closed-by-default posture +# is now load-bearing in CI: an accidental widening of the unauthenticated +# surface turns the RC red instead of shipping; +# 3. the non-admin probe user arrives through the OPERATOR path (the +# invitation carve-out) before the session/CRUD probes run as before. +# +# ⛔ Assert BEHAVIOUR, never a vendor-internal symbol. Wiring a release gate to +# better-auth's own internals (dist line numbers, private helpers) is the shape +# that made #11767's breakage possible; every assertion here is an HTTP status +# plus a `code` this repo owns and publishes. +# +# ⛔ Do NOT "fix" a red here by relaxing an assertion back toward 200. The whole +# reason this gate exists (#3091) is that 15.1.0 shipped with every fresh +# project's auth endpoints broken because in-repo settings hid the user's real +# first run. A refusal that this script asserts is a refusal a user gets. +# # better-sqlite3 is an optionalDependency of @objectstack/driver-sql: if the # runner cannot build the native addon the install still succeeds and the # runtime falls back to the WASM sqlite driver (#2229) — the smoke must never @@ -717,6 +767,19 @@ COOKIES_USER="$SMOKE_ROOT/cookies-user.txt" COOKIES_ADMIN="$SMOKE_ROOT/cookies-admin.txt" BODY="$SMOKE_ROOT/body.json" +# The credentials `objectstack dev --fresh` seeds in-process, spelled once. +# Defaults live in plugin-auth (`maybeSeedDevAdmin`) and are echoed by the CLI +# startup banner (packages/cli/src/utils/format.ts). This script deliberately +# passes NO --admin-email/--admin-password: the values a user is handed on a +# fresh install are what this gate must exercise. +ADMIN_EMAIL="admin@objectos.ai" +ADMIN_PASSWORD="admin123" +# The non-admin probe user. Spelled once because the operator invitation and +# the sign-up that redeems it MUST address the same mailbox — the carve-out is +# a lookup for a pending `sys_invitation` row on exactly this address. +USER_EMAIL="smoke@example.com" +USER_PASSWORD="Sm0ke-Pass!42" + # probe