Skip to content

fix(cli): os register requires a name, and the request-side as any that hid the mismatch is gone - #17455

Merged
os-justin merged 2 commits into
mainfrom
claude/issue-16932-register-name-cast
Sep 10, 2026
Merged

fix(cli): os register requires a name, and the request-side as any that hid the mismatch is gone#17455
os-justin merged 2 commits into
mainfrom
claude/issue-16932-register-name-cast

Conversation

@os-justin

Copy link
Copy Markdown
Collaborator

Fixes #16932

The card turned on a measurement nobody had taken: does the live route refuse a sign-up with no name, or accept it? It refuses. So the fix is the CLI's, packages/spec is untouched, and the whole card ships here.

The measurement

Driving the real command against a real route, not reading better-auth's source.

A showcase app booted --fresh --no-seed-admin on a random high port, so the environment carries no human user yet. That matters: the audience gate refuses self-registration under the default invite_only posture (the first attempt, against a seeded environment, came back 403 SELF_REGISTRATION_CLOSED and measured the gate rather than the route). With no human user the bootstrap bypass admits the request, and the route's own validation is the only judge left.

Leg 1 — empty answer at the Name prompt:

$ printf '\n' | os register --url http://localhost:38619 \
    --email measure-empty-name@example.com --password Passw0rd123 --json
Name (optional): ERROR HTTP request failed
  {"method":"POST","url":"http://localhost:38619/api/v1/auth/sign-up/email","status":400,
   "error":{"message":"[body.name] Invalid input: expected string, received undefined",
            "code":"VALIDATION_ERROR"}}
{ "success": false,
  "error": "[body.name] Invalid input: expected string, received undefined",
  "code": "VALIDATION_ERROR", "httpStatus": 400 }
exit 1

Leg 2 — positive control, same environment, same route, same run, name supplied:

$ printf 'Jane Doe\n' | os register --url http://localhost:38619 \
    --email measure-with-name@example.com --password Passw0rd123 --json
{ "success": true, "email": "measure-with-name@example.com", "userId": "0LTwRn6I7O6YC2n3VabQssWNi1ugxjg5" }
exit 0

The route REQUIRES name. The 400 is attributable to name and nothing else about the environment. RegisterRequestSchema's required name describes the door correctly — the card's neighbouring-line control (image on the next line carries .optional() and name does not) reads as deliberate because it is. os register failed on exactly the answer its own prompt invited, on the first door of the first-use experience.

What changed, all in packages/cli/src/commands/register.ts

The command disagreed with the route in four places; all four now agree.

before after
prompt Name (optional): — a false promise Name:
guard set email, password — no name name guarded beside them
payload type a local twin, name?: string the declared RegisterRequest
call site client.auth.register(payload as any) no cast

The cast is the card's real subject and comes out under either branch. With it gone the next divergence between this command and the declared request type is a compile error instead of a 400 a user meets on their first command.

The predicted compiler answer, verified — and it differs in wording

Dropping the cast on the unmodified command (mutation proven on disk, restored byte-identical to HEAD afterwards) yields exactly one error:

src/commands/register.ts(128,51): error TS2345: Argument of type
  '{ email: string; password: string; name?: string | undefined; }' is not assignable to parameter of type
  '{ email: string; password: string; name: string; image?: string | undefined; }'.

Same code, same call site, same declaration as the card predicted. The sub-message differs: the card quoted the "Property 'name' is missing" form, which is what a bare object literal produces — the form PR #16926 fixed in packages/client/README.md. Here the payload is a named variable annotated name?: string, so tsc reports the assignability failure on the optional member instead. The declaration has not moved; the two carriers just differ in shape.

⭐ And exactly one error, not two. The #5543 precedent the card cites (a cast also hiding genuinely misspelled keys) did not repeat here — nothing else was under the cast.

Tests

packages/cli/test/register-requires-name.test.ts (unit tier — no spawn, no kernel), four cases, pinning both halves:

  • refusal — an empty answer is refused by the CLI itself and fetch is never called. The "route was never called" half is what distinguishes the fix from the defect: the pre-fix command also failed, one HTTP round trip later.
  • prompt — the question asked is Name: and no prompt matches /optional/i.
  • preservation — with a name supplied the wire body is exactly { email, password, name } to POST .../api/v1/auth/sign-up/email. This is what stops the refusal being "fixed" by sending an empty string, which the route's z.string() accepts and which would create an account with a blank display name.
  • flag path--name is taken without prompting.

Reverse verification (fix committed first; pre-fix source restored tree-only from the base commit, proven on disk, then restored byte-identical to HEAD):

Tests  2 failed | 2 passed (4)
  FAIL  refuses an empty name WITHOUT calling the route
        AssertionError: expected undefined to be defined
  FAIL  no longer advertises the field as optional
        AssertionError: expected [ 'Name (optional): ' ] to include 'Name: '

The two defect pins go red. The two preservation pins stay green on both sides by construction — the pre-fix command also sent all three members once a name was supplied — and that is what they are for; they are not evidence about the defect and are not claimed as such.

Verification

what result
dispatch-gates --commands (derived from the real change set, --repo asserted) 62 families, 62 run, 62 exit 0; reconciled with --ran carrying exit codes: 62 accounted, 0 NOT-MEASURED, 0 UNRUN
pnpm check:type-check-debt first run exit 3 = PREREQUISITE NOT MET (tsc OOM under --max-old-space-size=4096; not a pass and not a finding). Re-run at the CI-shaped ceiling the script pins, --max-old-space-size=6144: exit 0
the 5 artifact-roster families whose roster sits under a path of mine, plus check:cli-examples-parity all exit 0
pnpm --filter '@objectstack/cli^...' build exit 0
pnpm --filter @objectstack/cli typecheck exit 0 (source layer and check:test-typecheck)
packages/cli unit tier 193 files / 2671 tests passed; the integration tier is declared to CI — the diff touches no integration-layer file, no spawn entry point and no driver/kernel boot path
pnpm lint (whole repo, eslint . --no-inline-config) exit 0 over 6558 files, 0 errors, 0 warnings — the full scan, not a narrowed one; both changed files are in the linted set

Acceptance notes

  • The measurement's own container side effect. A successful os register writes ~/.objectstack/credentials.json. Leg 2 therefore overwrote whatever that shared-container file held. Ephemeral dev state, but worth knowing before anyone drives this command as a measurement again.
  • response as any on the two lines below the call site stays. (response as any).token / .user read the raw wire keys beside the normalized data.* envelope, and @objectstack/client's register deliberately serves both — its TSDoc says the raw keys "are kept alongside for callers written against the wire". That is a declared compatibility read on the response seam, not the request-side mismatch this card is about, so it is out of scope here. noted, not filed — it is neither a reproducible defect nor a contract violation, and the successor is whoever next works the SessionResponse envelope on the client seam; naming no one, there is no queued PR it belongs to today.
  • Nothing else in the tree carried the false promise. Name (optional) had exactly one occurrence repo-wide, and the two docs pages that show os register (content/docs/permissions/authentication.mdx, content/docs/deployment/cli.mdx) never called the field optional — so no docs change rides along.
  • Coordinates re-derived by symbol. Every line number the card and the dispatch quote still held at the base commit ba9f0299: register.ts :115 / :122-123 / :126 / :128, auth.zod.ts :93-98, and the instrument control (9 export const in auth.zod.ts). None had moved.
  • packages/spec was not edited. The measurement landed on the CLI half, so the declaration half never came up; the packages/spec half of the card has nothing left in it.

Clause-②: no — re-declared from the delivered diff. Nothing published is narrowed and nothing is widened: no export, type or schema changes, no published request member becomes optional. @objectstack/cli's behaviour changes only in that an input the route already refused is now refused locally with a clear message; no input that previously worked stops working.

A patch changeset ships with it (@objectstack/cli publishes dist).


Generated by Claude Code

os-justin and others added 2 commits September 10, 2026 14:45
…id it

The live route refuses a sign-up without `name`: on a fresh environment (no
human user yet, so the audience gate's bootstrap bypass admits the request and
the route's own validation is the only judge left), `POST
/api/v1/auth/sign-up/email` answers 400 VALIDATION_ERROR "[body.name] Invalid
input: expected string, received undefined". The same run with a name supplied
answers 200 and creates the account.

So `RegisterRequestSchema` declares `name` correctly and the command was the
side that disagreed: it prompted "Name (optional)", typed its own payload with
`name?`, guarded email and password but not name, and cast the payload with `as
any` at the call site — which is the only reason that disagreement compiled.

- prompt: "Name (optional): " -> "Name: "
- guard: `if (!name) throw new Error('Name is required')`, beside email/password
- payload: annotated with the declared `RegisterRequest`, no local twin
- call site: the `as any` is gone, so the next divergence is a compile error

Claude-Session: https://claude.ai/code/session_01DapQyvYrFb1MxSYe7BL2nt
Co-authored-by: Claude <noreply@anthropic.com>
`tsconfig.test.json` covers this file, and `process.exitCode` is
`string | number | null | undefined` there — the narrower annotation was a
TS2322 the source-layer `tsc --noEmit` never sees.

Claude-Session: https://claude.ai/code/session_01DapQyvYrFb1MxSYe7BL2nt
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/cli, touching 1 documentable anchor(s).

2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/deployment/cli.mdx (via os register (command, read off packages/cli/src/commands/register.ts))
  • content/docs/permissions/authentication.mdx (via os register (command, read off packages/cli/src/commands/register.ts))
What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 23 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 9788f1e91983cb65413eb8e262716c9251900ac7packageMentionDocs.

Which tree this was computed on

This run read content/docs from 787a78840f65d0eb1db15f5bc0610278424cb128 — the merge of head 88e25d2985588d06ea6209c31446fcb8f9e43b0b into base 9788f1e91983cb65413eb8e262716c9251900ac7, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 787a78840f65d0eb1db15f5bc0610278424cb128 && git checkout 787a78840f65d0eb1db15f5bc0610278424cb128
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 9788f1e91983cb65413eb8e262716c9251900ac7 88e25d2985588d06ea6209c31446fcb8f9e43b0b && git checkout -B drift-repro 9788f1e91983cb65413eb8e262716c9251900ac7 && git merge --no-ff 88e25d2985588d06ea6209c31446fcb8f9e43b0b

node scripts/docs-audit/affected-docs.mjs --json 9788f1e91983cb65413eb8e262716c9251900ac7

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 9788f1e91983cb65413eb8e262716c9251900ac7 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 34501395133 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Dogfood Regression Gate (2/3) — 失败步骤: Boot example apps and exercise real user flows

    @objectstack/dogfood:test:  FAIL   isolated  test/schedule-sweep-organization-scope.dogfood.test.ts > dogfood [sqlite-wasm]: a time-relative sweep selects inside its declared organization (#16659)
      ↳ 失败原因: (这条 FAIL 之后 12 行内没有可识别的原因行 —— 点进 job 看)
    @objectstack/dogfood:test:  FAIL   isolated  test/schedule-sweep-organization-scope.dogfood.test.ts > dogfood [memory]: a time-relative sweep selects inside its declared organization (#16659)
      ↳ 失败原因: (这条 FAIL 之后 12 行内没有可识别的原因行 —— 点进 job 看)
    
  • Dogfood Regression Gate (3/3) — 失败步骤: Boot example apps and exercise real user flows

    @objectstack/dogfood:test:  FAIL   isolated  test/schedule-acting-organization.dogfood.test.ts > dogfood [sqlite-wasm]: a scheduled run executes as its declared organization (#16659)
      ↳ 失败原因: (这条 FAIL 之后 12 行内没有可识别的原因行 —— 点进 job 看)
    @objectstack/dogfood:test:  FAIL   isolated  test/schedule-acting-organization.dogfood.test.ts > dogfood [memory]: a scheduled run executes as its declared organization (#16659)
      ↳ 失败原因: (这条 FAIL 之后 12 行内没有可识别的原因行 —— 点进 job 看)
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️ 断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 2 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

1 participant