Skip to content

fix(client): auth.login / auth.register deliver the SessionResponse envelope they declare - #17791

Merged
os-sales merged 2 commits into
mainfrom
claude/issue-17234-auth-login-register-envelope
Sep 12, 2026
Merged

fix(client): auth.login / auth.register deliver the SessionResponse envelope they declare#17791
os-sales merged 2 commits into
mainfrom
claude/issue-17234-auth-login-register-envelope

Conversation

@claude

@claude claude Bot commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

Refs #17234

⚠️ Deliberately Refs, not a closing keyword. The card names two departures from the declared SessionResponse; this PR closes one of them and leaves the other open with the measurement that says why, per the dispatch rule for this card. A half-closed defect behind a closing keyword is exactly what that rule prevents.

departure state after this PR
success is absent closed — both methods now deliver it, judged by a parse against the declaration
data.session is absent still open — measured unobtainable on these routes without a second network call; nothing synthesized

Premise, re-measured against origin/main before writing code

All three faces read the same as the dispatch recorded them, on 15805ea3:

  • git log --oneline -12 origin/main -- packages/client/src/index.ts — tip 7baf04ae (a docs-only OAuth change). Nothing touches login / register.
  • Both methods still carried the inline const data = raw && (raw.data ?? …); const normalized = data ? { ...raw, data } : raw; and neither wrote success.
  • Driven through a real AuthManager (better-auth 1.7.3, organization plugin) over a real ObjectQL on a real SqliteWasmDriver:
register -> top-level keys ["token","user","data"]             success === undefined
login    -> top-level keys ["redirect","token","user","data"]  success === undefined
both     -> data keys ["token","user"]
both     -> SessionResponseSchema.safeParse issues:
              success         : Invalid input: expected boolean, received undefined
              data.session    : Invalid input: expected object, received undefined
              data.user.image : Invalid input: expected string, received null

One correction to the card's own header, not to its finding: the family is on better-auth 1.7.3 (lifted by #17454), not 1.7.2.

The change

packages/client/src/index.ts only.

login and register now run the same normalizeSessionResponse lift auth.me / auth.refreshToken use, instead of a second inline copy of it. The helper is extended to carry a body's own top-level token into data.token, and to copy only the members a body really has — the two route families answer disjoint sets ({ user, session } vs { token, user }), so writing a fixed triple would file an undefined under a key the route never served.

Routing them through the helper unchanged was the trap the dispatch flagged: the old helper built data: { user, session } and nothing else, which would have dropped data.token and silently stopped login's auto-set of the client bearer token. That is pinned now (block ④), against the wire bytes of the same call.

⛔ No packages/spec edit. SessionResponseSchema and BaseResponseSchema are untouched — the declared contract is satisfied, never widened.

Driven readings — after

Same instrument, same arrangement. packages/client/src/auth-login-register-envelope.test.ts, 12 cases:

register -> success === true    BaseResponseSchema.safeParse -> ok
login    -> success === true    BaseResponseSchema.safeParse -> ok
both     -> SessionResponseSchema.safeParse issue paths === ["data.session","data.user.image"]

The success issue is gone from both. The remaining two are pinned exhaustively, so a regression on success reappears here as a third issue rather than hiding inside "it already failed". data.user.image is #17235 and is not specific to these methods.

Credential fidelity (block ④), measured against the wire body of the same call, not against a remembered constant:

login  -> res.data.token === wireBody.token        (byte-identical)
       -> client.token   === wireBody.token        (auto-set still fires)
       -> principalFor(client.token) === res.data.user.id   (a WORKING credential)
       -> principalFor('not-the-session-token-17234') === null   (the control that must not resolve)
register -> res.data.token === wireBody.token, client.token === wireBody.token

Negative control that can fail (block ③): the value the method really returned, with success taken back out, is fed to the same schema — it reports ["success","data.session","data.user.image"] again. So a green reading above is a reading, not a broken assertion.

Why data.session is not delivered

Measured on the same instrument (block ⑤), and this is the report the ruling asked for rather than an invention:

POST /sign-up/email -> body top-level keys ["token","user"]
POST /sign-in/email -> body top-level keys ["redirect","token","user"]
response headers    -> no header named for a session; set-auth-token carries a bare
                       token STRING, not a session object (SessionSchema rejects it)
auth.me()  (a SECOND call to GET /get-session) -> data.session parses as SessionSchema

SessionSchema requires id, expiresAt and userId. userId is derivable from user.id; id and expiresAt are nowhere on these two calls, body or header. So delivering data.session here means either a second round trip inside login() — a behaviour change no ruling authorises — or fabricating an id and an expiry under a declared type, which is forbidden outright. The card stays open for that shape decision.

Block ⑤ is written so that it reddens if better-auth ever starts serving a session on these routes, so the reason this half is open stays a measured fact.

Reverse verification

The fix was committed first, then mutated on disk and restored, so both legs ran from a real commit.

mutation : drop `success: true` from the lift's return
on-disk  : anchor count 1 -> 0, replacement count 0 -> 1
           blob a4d0655f… -> a4ac6283…   (proved changed, not an exit-0 no-op edit)
result   : Tests 6 failed | 12 passed (18)   — VERDICT command-exit 1
           4 of the 6 are this PR's blocks ①/②; the other 2 are #16760's own
           `auth-get-session-envelope.test.ts` — one shared lift, load-bearing for both families
restore  : git checkout HEAD -- packages/client/src/index.ts
           blob back to a4d0655f…, `git diff HEAD` empty, `git status --porcelain` empty
re-run   : Tests 518 passed (518), Test Files 43 passed (43)   — VERDICT command-exit 0

Blocks ③④⑤⑥ stayed green under the mutation, which is correct: ③ asserts the instrument can still see a missing success, and ④⑤⑥ are about the credential and the session residue, which the mutation does not touch.

Local verification

Every command below reports the verdict line the runner itself printed, with the exit code captured before any pipe.

command verdict
pnpm --filter "@objectstack/client^..." build (dependency closure) VERDICT command-exit 0
pnpm --filter @objectstack/client test VERDICT command-exit 043 files / 518 tests passed
pnpm --filter @objectstack/client typecheck VERDICT command-exit 0tsc --noEmit clean; check:test-typecheck 0 files / 0 errors in the shrink-only debt ledger
pnpm build (full, for one gate's prerequisite) VERDICT command-exit 0 — 73/73 tasks
pnpm lint (eslint . --no-inline-config, repo-wide) exit 0 at a0ab9789e — the whole population, so no narrowing is claimed and none is needed

Gate families, derived from the real change set rather than from a hand-written list —
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, then reconciled with --ran:

60 derived, 60 run, 0 NOT-MEASURED, 0 UNRUN
(a DERIVED zero — all 60 recorded an exit code and none of them is 3)

Three of the sixty were re-run because their first result was not a measurement, and are reported at their real verdict:

  • check:skill-examples — first run PREREQUISITE NOT MET (packages/client-react/dist absent). After building that package: exit 0, 258 prose examples type-check across 3 surfaces, including the 23 client-SDK blocks that read these very methods.
  • check:dual-build-cjs-loads — first run exit 3, PREREQUISITE NOT MET (34 packages with no dist/). After pnpm build: exit 0, 104 require entry points across 67 packages load.
  • check:type-check-debt — first run exit 3, FATAL ERROR: Reached heap limit, caused by my own NODE_OPTIONS=--max-old-space-size=4096 being tighter than the gate's own ceiling (the gate says so in its output). Re-run without that cap: exit 0, 55 raw tsc errors, none above its recorded number.

⛔ None of those three was reported as a pass on its first result. An exit 3 here means nothing was measured, which is neither green nor red.

#16760's work is not disturbed

git show of both method bodies at the merge base 15805ea32 and at HEAD is byte-identical:

  • auth.me — 7 lines, unchanged.
  • auth.refreshToken — 14 lines, unchanged.
  • packages/client/src/auth-get-session-envelope.test.ts — not in git diff --name-only, and green in the run above.

The shared lift is changed, deliberately — and the ablation above shows it is load-bearing for both families, which is the point of there being one of it.

Docs

No documentation change is owed, measured rather than assumed. At origin/main 8fa3fe63:

git grep -c 'data\.session' -- content/docs   ->  0 hits   (exit 1)
git grep -c 'data\.user'    -- content/docs   ->  3 hits   (live positive control)

The only page that reads fields off these two calls is content/docs/permissions/authentication.mdx, which reads result.data.user, result.data.token and session.data.user — all three preserved by this change. No published page tells a reader to reach for data.session, so leaving that half undelivered falsifies no documentation. ⛔ Nothing under content/docs/releases/ is touched: that tree is release-owned.

Clause-②: no

This pulls an implementation back to a contract that is already declared. No published surface widens, no accepted set widens, and nothing in packages/spec is edited — the triage ruling on this card says the same in its own words.

Acceptance notes

Found while measuring, not filed and not fixed here:

  • The normalizeSessionResponse docblock stated that the token login puts at data.token is the SIGNED token.signature form. Measured, it is the unsigned one: the response BODY's token and the session.token a following /get-session serves are the same string, while the signed form is what bearer() publishes in the set-auth-token header. Corrected in this PR — same file, same docblock, same subject, as the dispatch order directs — and the corrected reading is now pinned by a case in block ④ so it cannot rot again. The rule the sentence justified (never synthesize data.token from a session) is unchanged and now rests on the right ground.
  • The same docblock said auth.login "has carried the same lift … since long before this card". That was loose — login's inline copy filled data and never success, which is this defect. After this PR it is literally the same lift, and the sentence is rewritten to say so.
  • noted, not filed: login's own if (!res.ok) { … throw … } block is unreachable. ObjectStackClient.fetch already throws on every non-2xx before login can inspect res.ok (observed directly: a SELF_REGISTRATION_CLOSED sign-up threw from fetch, never reaching the caller's branch). Dead code, not a defect — left untouched. Successor: whoever converges the SDK's two error envelopes (Envelope drift is not just service-storage: four more route modules emit bare bodies, two of them the pre-#3675 { error: '<string>' } #3843 is the line that would reach it).
  • noted, not filed: the first sign-up on a fresh environment provisions the owner and the audience posture then closes self-registration, so a second register() against the same AuthManager is refused with SELF_REGISTRATION_CLOSED. Correct behaviour, and a real trap for anyone writing a driven auth test — recorded in the suite's own comments. Successor: the next author writing a multi-user driven auth test in packages/client.
  • auth.me() returns the literal null for an anonymous caller, which no value of its declared SessionResponse can express #17238 (the anonymous-caller case) is not addressed here and is out of scope: different defect, domain:services lane, unruled. SessionUser.image is declared z.string().optional(), but every /auth/* session route serves "image": null — no real session body parses as SessionResponse #17235 (data.user.image served as null) is likewise out of scope and stays pinned as residue.

Authored by an agent session: https://claude.ai/code/session_01TSf4DV7ziu4V5j73e46b7c


Generated by Claude Code

…lope they declare

`login` and `register` annotate their return as `SessionResponse`, whose base
`BaseResponseSchema` declares `success` as a REQUIRED boolean. Both carried an
inline lift that filled `data` and never wrote `success`, so neither delivered
the type it advertises and every consumer keying on the envelope flag --
`unwrapResponse` keys on exactly this -- read `undefined`.

Route both through the existing `normalizeSessionResponse` instead of a second
inline copy, extended to carry a body's own top-level `token` into `data.token`
so the credential `login` arms `this.token` from survives byte-identical. The
lift now copies only the members a body really has: `/get-session` answers
`{ user, session }` and `/sign-in|sign-up/email` answer `{ token, user }`.

`data.session` is NOT closed: measured against a real AuthManager, neither
credential route serves a session object, id or expiry in body or header, so it
is unobtainable without a second `/get-session` call. Nothing is synthesized;
#17234 stays open for that shape decision.

Claude-Session: https://claude.ai/code/session_01TSf4DV7ziu4V5j73e46b7c
Co-authored-by: Claude <noreply@anthropic.com>
…in both departures

Twelve driven cases over a real AuthManager (better-auth 1.7.3) on a real
ObjectQL / SqliteWasmDriver, with the client's fetch keeping a clone of each
wire Response so the bytes and the SDK return value come from one call.

Closes the `success` half by PARSE against the declaration, pins the remaining
issue list exhaustively so a regression reappears as an extra issue, and carries
a negative control that takes `success` back out of the value the method really
returned and watches the same parse report it again.

Block ⑤ is the measurement for the half this does NOT close: no session in
either body, none in any header, and the value reachable only on a second
/get-session call.

Claude-Session: https://claude.ai/code/session_01TSf4DV7ziu4V5j73e46b7c
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Sep 12, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/api/client-sdk.mdx (via auth.login (sdk, the route ledger binds it to POST /api/v1/auth/sign-in/email), auth.me (sdk, the route ledger binds it to GET /api/v1/auth/get-session), auth.register (sdk, the route ledger binds it to POST /api/v1/auth/sign-up/email))
  • content/docs/api/index.mdx (via /api/v1/auth/sign-in/email (route, a path literal on a changed line))
  • content/docs/deployment/self-hosting.mdx (via /api/v1/auth/sign-up/email (route, a path literal on a changed line))
  • content/docs/getting-started/your-first-project.mdx (via /api/v1/auth/sign-in/email (route, a path literal on a changed line))
  • content/docs/permissions/authentication.mdx (via auth.login (sdk, the route ledger binds it to POST /api/v1/auth/sign-in/email), auth.me (sdk, the route ledger binds it to GET /api/v1/auth/get-session), auth.register (sdk, the route ledger binds it to POST /api/v1/auth/sign-up/email), /api/v1/auth/get-session (route, a path literal on a changed line), /api/v1/auth/sign-in/email (route, a path literal on a changed line), /api/v1/auth/sign-up/email (route, a path literal on a changed line))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17/17-4.mdx (via /api/v1/auth/get-session (route, a path literal on a changed line))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • 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 — 15 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 8fa3fe63d9e8dbbc507c0735d918c99db41df31dpackageMentionDocs.

Which tree this was computed on

This run read content/docs from a34f3de8fab3becf668276858d8ee3314d3373d7 — the merge of head a0ab9789ef18cf475c828feb03dd3eda8d09b835 into base 8fa3fe63d9e8dbbc507c0735d918c99db41df31d, 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 a34f3de8fab3becf668276858d8ee3314d3373d7 && git checkout a34f3de8fab3becf668276858d8ee3314d3373d7
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 8fa3fe63d9e8dbbc507c0735d918c99db41df31d a0ab9789ef18cf475c828feb03dd3eda8d09b835 && git checkout -B drift-repro 8fa3fe63d9e8dbbc507c0735d918c99db41df31d && git merge --no-ff a0ab9789ef18cf475c828feb03dd3eda8d09b835

node scripts/docs-audit/affected-docs.mjs --json 8fa3fe63d9e8dbbc507c0735d918c99db41df31d

⚠️ 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 8fa3fe63d9e8dbbc507c0735d918c99db41df31d → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@claude

claude Bot commented Sep 12, 2026

Copy link
Copy Markdown
Contributor Author

ACCEPTdomain:cli execution seat (#6024), 2026-09-12T05:47Z, against head a0ab9789ef18cf475c828feb03dd3eda8d09b835. Readings below are mine, taken on this head; ⛔ none of them is the report read back.

Gate state

GET /commits/a0ab9789ef/check-runs45 checks: 38 success, 7 skipped, 0 red, 0 pending; mergeable_state: clean. Diff is 3 files: packages/client/src/index.ts, one new driven test, one changeset. ⛔ packages/spec untouched, as the dispatch required.

⭐ The docblock correction — re-examined, because the dispatch order quoted the OLD claim

The dispatch order handed the dev this sentence as a constraint, quoting the then-current docblock: "session.token is the UNSIGNED session token, while the token login puts there is the SIGNED token.signature form." The dev measured it false and reversed it. ⇒ this seat repeated an inaccurate claim about a credential, so it is not a thing to accept on report.

What the PR pins, at auth-login-register-envelope.test.ts:306-:311, read on this head:

const signed = wireRes.headers.get('set-auth-token') ?? '';
expect(signed, 'sign-in emitted no set-auth-token — this control cannot fire').toBeTruthy();
expect(signed).not.toBe(wireBody.token);
expect(signed.startsWith(`${String(wireBody.token)}.`)).toBe(true);

expect(res.data.token).toBe(wireBody.token);

That is a falsifiable assertion against wire bytes, not an argument: it fires only if the header value is the body token followed by . and more — which is what makes the body token the unsigned prefix and the header the signed form. It carries its own can-fire guard, with a message that says so. And res.data.token === wireBody.token ties the finding to what the SDK actually exposes. ⇒ the corrected docblock rests on ground that can be knocked out; the sentence this seat quoted was wrong and is now right. ⭐ The rule it justified — never synthesize data.token from a session — is unchanged, which is why the correction improves the docblock without loosening anything.

#16760 undisturbed — extracted and byte-compared, not taken on report

Method bodies pulled by brace-walk from origin/main and from this head:

method main head verdict
auth.me 307 B 307 B byte-identical
auth.refreshToken 554 B 554 B byte-identical
auth.login 1521 B 1657 B changed, as claimed
auth.register 636 B 701 B changed, as claimed

⚠️ An instrument fault of mine, recorded rather than quietly fixed. My first extraction of auth.register selected the wrong method: packages/client/src/index.ts contains two register: async definitions, and str.index(...) took oauth.applications.register — which is untouched, so it read 417 B identical on both sides. Left there, that becomes a defect report against a correct PR: "the dev says register was changed and it is byte-identical." Re-selected by the occurrence whose body contains sign-up/email, it is 636 → 701 B. ⭐ Same class as a stale line number: a selector that is not unique in the file is not a selector, and a false "identical" is indistinguishable from a real finding until you check the selector.

The lift, read on this head

if (body.user === undefined && body.session === undefined && body.token === undefined) return body 
const data: { user?: unknown; session?: unknown; token?: unknown } = {};
if (body.user    !== undefined) data.user    = body.user;
if (body.session !== undefined) data.session = body.session;
if (body.token   !== undefined) data.token   = body.token;
return { success: true, ...body, data } 

Four properties, each load-bearing and each visible in those six lines:

  • The trap the dispatch flagged is closed. The old helper built data: { user, session } and nothing else; routing login through it unchanged would have dropped data.token and silently stopped the bearer auto-set. Only-present-member construction keeps it, and both methods still do if (normalized.data?.token) this.token = normalized.data.token.
  • success: true sits BEFORE the spread, so a producer that sent its own success still wins.
  • The token === undefined clause was added to the not-recognisable guard, so a { token, user } body lifts instead of falling through untouched.
  • if (!body …) return body is unchanged ⇒ the anonymous null answer is still handed back as-is. auth.me() returns the literal null for an anonymous caller, which no value of its declared SessionResponse can express #17238 stays exactly as out of scope as the dispatch required.

Instrumentation — the parts that can fail

  • The safeParse residue is pinned exhaustively (toEqual(RESIDUE)), not by containment, so a success regression reappears as a third issue rather than hiding inside a loose assertion.
  • Negative control (block 3): the value the method really returned, with success taken back out, reports [success, data.session, data.user.image] again ⇒ the instrument still sees the defect it was built to see.
  • Ablation: dropping success: true from the lift reddens 6 of 18 — 4 in this PR's own blocks and 2 in client SDK auth.me / auth.refreshToken declare the REST { success, data } envelope for /get-session, which answers the bare { user, session } — and refreshToken never captures a token because of it #16760's auth-get-session-envelope.test.ts ⇒ the one shared lift is load-bearing for both families, which is the point of replacing the second inline copy. Blocks 3/4/5/6 stayed green, which is the correct direction for an ablation rather than a blanket red. Restore leg proved on disk (blob hash back, git diff HEAD empty, git status --porcelain empty).
  • The wire key sets are pinned exactly — ['token','user'] for sign-up, ['redirect','token','user'] for sign-in — and res.data.session is asserted undefined on both, so the undelivered half is measured, ⛔ not merely unclaimed.

Refs #17234, and the card stays open

The body says Refs, ⛔ not a closing keyword, and states the undelivered half in its own table. That is what the dispatch rule required and it is the right call: success is closed, data.session is not, and a half-closed defect behind a closing keyword is exactly what that rule exists to prevent. ⇒ this card must NOT close on merge; its remaining half goes to the decision box with the dev's three options and its measurement.

Carrier

node scripts/pm/check-clause2-carriers.mjs --pair 17791 now reads ✓ legible, both carriers agree. It did not at first: the card-side declaration was missing entirely, because this seat's compact claim template had dropped the line. Repaired on the claim comment itself (the only place that limb is read), reasoning at 5643904057, and the template fault filed as #17800 so it stops recurring across seats. ⚠️ The dev surfaced this and deserves the credit for it; the mechanism it named (a missing Branch: line, exit 2) was not the one that fired (exit 4, the declaration limb) — reported out because a wrong mechanism sends the next reader to the wrong line.

Proceeding to ready + merge queue.

⚠️ Stamp corrected in place: this comment first read 2026-09-12T06:06Z, 18 minutes ahead of the clock — the platform created_at is 05:47:37Z, outside the 15-minute tolerance the board judges typed stamps by. Cause is a construction, not a typo: the stamp was ESTIMATED while composing instead of read in the same call that posts it. ⛔ A reading stamped in the future is not a reading. Every other artefact this seat posted in round 22 was re-measured against its own created_at in the same pass; the rest sit within tolerance and are left as posted, and the readings themselves were never affected.


Generated by Claude Code

@os-sales
os-sales marked this pull request as ready for review September 12, 2026 05:47
@os-sales
os-sales enabled auto-merge September 12, 2026 05:47
@os-sales
os-sales added this pull request to the merge queue Sep 12, 2026
Merged via the queue into main with commit 01388fe Sep 12, 2026
47 checks passed
@os-sales
os-sales deleted the claude/issue-17234-auth-login-register-envelope branch September 12, 2026 06:11
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/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants