Skip to content

Commit e34ab34

Browse files
committed
Merge commit 'ce8caba91403c8f160cb7764c63b08371a13db99' into claude/issue-16147-page-header-canonical-i18n-route
2 parents 59d91d1 + ce8caba commit e34ab34

33 files changed

Lines changed: 2128 additions & 104 deletions
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
---
2+
"@objectstack/client": minor
3+
---
4+
5+
fix(client)!: the `auth.*` family declares the wire shapes better-auth actually sends — thirteen published `Promise< any >` returns narrowed (#14313)
6+
7+
**BREAKING** for a typed caller, and it breaks nothing that ever worked at runtime. No request bytes, no URL and no response handling change: this is a declaration catching up with what the routes have always answered. It ships as `minor` under the lockstep launch-window convention (`scripts/check-changeset-no-major.mjs`) — the version number is not the migration signal here, this entry is.
8+
9+
<!-- adr-0087: not-required (type-surface-only packages/client/src/index.ts#auth.updateUser, packages/client/src/index.ts#auth.changePassword, packages/client/src/index.ts#auth.setInitialPassword, packages/client/src/index.ts#auth.changeEmail, packages/client/src/index.ts#auth.sendVerificationEmail, packages/client/src/index.ts#auth.verifyEmail, packages/client/src/index.ts#auth.sessions.revoke, packages/client/src/index.ts#auth.sessions.revokeOthers, packages/client/src/index.ts#auth.sessions.revokeAll, packages/client/src/index.ts#auth.twoFactor.verifyTotp, packages/client/src/index.ts#auth.twoFactor.disable, packages/client/src/index.ts#auth.twoFactor.verifyBackupCode, packages/client/src/index.ts#auth.accounts.unlink) A published TYPE-SURFACE narrowing. Each of the thirteen members was UNANNOTATED at the merge base, so lib.dom's `Response.json()` published it as an erased `any`; each now declares the shape its route already answered, read off the wire against a real server. No method body changed, so no request or response byte moves, and the diff touches no `packages/spec` path and no ADR-0087 shape surface. The affected party is a TypeScript consumer and the compiler delivers the break at their own call site; `objectstack migrate meta`, `spec-changes.json` and the upgrade guide have nothing to rewrite, so a ledger entry would be false data in the one ledger this gate keeps true. The fourteenth member, `auth.deleteUser`, is deliberately left unannotated and is not named here. -->
10+
11+
Card 2 of 3 of the #12104 family, under the maintainer's 2026-08-31 ruling: the wire contract is the only source of truth, better-auth's own `Date`-typed fields are the pre-serialization SERVER shape, and every timestamp is declared as the ISO-8601 `string` the wire carries — no `Date`, no revival layer.
12+
13+
## What changed
14+
15+
Thirteen `auth.*` methods ended `return res.json()` with no return annotation, so `lib.dom`'s `Response.json(): Promise< any >` was their published type. Each now declares the shape its route serves, and its `exported-any-returns.json` entry is deleted in the same change (35 entries before, 22 after):
16+
17+
| method | resolved to (before) | resolves to (now) |
18+
|:--|:--|:--|
19+
| `client.auth.updateUser(data)` | `any` | `AuthStatusReceipt` |
20+
| `client.auth.changePassword(req)` | `any` | `AuthPasswordChangeResult` |
21+
| `client.auth.setInitialPassword(req)` | `any` | `AuthSetInitialPasswordResult` |
22+
| `client.auth.changeEmail(req)` | `any` | `AuthStatusReceipt` |
23+
| `client.auth.sendVerificationEmail(req)` | `any` | `AuthStatusReceipt` |
24+
| `client.auth.verifyEmail(params)` | `any` | `AuthEmailVerificationResult` |
25+
| `client.auth.sessions.revoke(token)` | `any` | `AuthStatusReceipt` |
26+
| `client.auth.sessions.revokeOthers()` | `any` | `AuthStatusReceipt` |
27+
| `client.auth.sessions.revokeAll()` | `any` | `AuthStatusReceipt` |
28+
| `client.auth.twoFactor.verifyTotp(req)` | `any` | `AuthTwoFactorVerificationResult` |
29+
| `client.auth.twoFactor.disable(req)` | `any` | `AuthStatusReceipt` |
30+
| `client.auth.twoFactor.verifyBackupCode(req)` | `any` | `AuthTwoFactorVerificationResult` |
31+
| `client.auth.accounts.unlink(req)` | `any` | `AuthStatusReceipt` |
32+
33+
`AuthWireUser`, `AuthStatusReceipt`, `AuthPasswordChangeResult`, `AuthEmailVerificationResult`, `AuthTwoFactorVerificationResult` and `AuthSetInitialPasswordResult` are newly exported from `@objectstack/client`. Twelve of these routes are served BARE by better-auth (`auth-route-ledger.ts` records them `source: 'better-auth'`) — there is no `{ success, data }` envelope to unwrap and none is introduced; `setInitialPassword` is ObjectStack's own mount and answers the platform's `{ success: true }` envelope.
34+
35+
## The exact reads that stop compiling
36+
37+
Everything below compiled before only because `any` is assignable to, and indexable by, everything.
38+
39+
```ts
40+
const r = await client.auth.updateUser({ name: 'Ada' });
41+
r.user; // now TS2339 — the route answers `{ status: true }`, NOT the updated user
42+
r.data; // now TS2339 — these routes carry NO envelope
43+
44+
const cp = await client.auth.changePassword({ currentPassword, newPassword });
45+
cp.user.createdAt.getTime(); // now TS2339 — the wire sends an ISO-8601 STRING, not a Date
46+
new Date(cp.user.createdAt); // the correct rewrite
47+
cp.token.length; // now TS18047 — `token` is `string | null` (null unless other sessions were revoked)
48+
49+
const v = await client.auth.verifyEmail({ token });
50+
v.user.email; // now TS18047 — `user` is `AuthWireUser | null` (null on a plain verification)
51+
52+
const ok = await client.auth.setInitialPassword({ newPassword });
53+
ok.status; // now TS2339 — ObjectStack's mount answers `{ success: true }`, not `{ status }`
54+
55+
const t = await client.auth.twoFactor.verifyTotp({ code });
56+
t.user.locale; // now TS2339 — ObjectStack's own sys_user columns are not on better-auth's wire user
57+
```
58+
59+
A caller that only read `status`, `success`, `token` (guarding `null`) or the base user columns needs no change.
60+
61+
## Timestamps: ISO-8601 `string`, never `Date`
62+
63+
`AuthWireUser.createdAt` / `updatedAt` (and `banExpires`) are the vendor's `Date`-typed fields. The adapter is declared `supportsDates: false`, better-auth revives the stored string into a `Date` server-side, and `JSON.stringify` puts an ISO-8601 string back on the wire — measured `"createdAt":"2026-09-07T07:02:20.593Z"` on a real SQL driver. They are declared `string`, a type-level pin holds them there, and no revival layer exists in the SDK.
64+
65+
## Where the vendor's own declarations were the wrong answer
66+
67+
- `updateUser`'s OpenAPI stub promises `{ user }`; its handler answers `{ status: true }` and puts the new fields into the session cookie. The receipt is what is declared.
68+
- `verifyEmail`'s stub declares `user` required; the handler answers `user: null` on a plain verification and the updated user only on a change-email verification.
69+
- A nullable column (`image`, `banReason`, `banExpires`) arrives as `null` on the SQL drivers and as an ABSENT key on a store that does not materialise unset columns — both measured — so each is `?: … | null`.
70+
71+
## `auth.deleteUser` is deliberately NOT bound
72+
73+
The fourteenth method keeps its `Promise< any >` and its ledger entry. Its route is switched off by maintainer ruling (2026-08-12 on #7735; `auth-route-ledger.ts` books it `disabled`), and measured against a real server it answers HTTP 404 with a ZERO-BYTE body once the last-local-credential guard is satisfied — so `this.fetch` throws before `res.json()` ever runs and the method has no success path a caller can observe. No declared return type can be honest for a value the runtime never delivers. That the shrink-only ledger still carries exactly this one `auth.*` entry is the mechanism working.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
`GetMetaItemsRequestSchema.organizationId` no longer documents itself as always consulted.
6+
7+
The published `describe()` opened with "Selects the org partition in the ADR-0005 overlay read order" and closed with "Absent = environment-wide read: only env-level overlays apply and no org partition is consulted." Stating only the absent case invites the converse, and an integrator reading it completes it as *present ⇒ consulted* — so a caller who supplies an organization believes it has scoped a read that can in fact be environment-wide. A supplied organization is not consulted on every `getMetaItems` read.
8+
9+
The corrected text qualifies the promise instead of implying its converse: the parameter selects the org partition **when an org partition applies**, and supplying a value "does not by itself guarantee an org partition is consulted; where none applies, and whenever it is absent, the read is environment-wide and only env-level overlays apply."
10+
11+
Prose only. No key is added, removed or renamed, no export moves, no accept set changes and no runtime behaviour changes — the schema's shape and validation are byte-for-byte what they were. What ships is the JSON-Schema `description` for the existing `organizationId` key and the matching row in the generated API reference, which is why this is user-visible enough to owe an entry and narrow enough to be a patch.
12+
13+
The three sibling `organizationId` describes on `GetMetaItemRequestSchema`, `GetMetaItemLayeredRequestSchema` and `GetMetaItemCachedRequestSchema` are deliberately left alone.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
`KnowledgeRefreshPolicy.cron` no longer tells authors that the `cron` dialect engine judges their syntax "when the expression is evaluated". Both halves of that sentence were false: nothing evaluates `refresh.cron``service-knowledge` reads `refresh.onRecordChange` and never `refresh.cron` — and `@objectstack/formula`'s registered `cron` engine has no caller outside that package, so it was never going to issue that verdict either. The claim shipped to authors through the generated reference page (`content/docs/references/ai/knowledge-source.mdx`), naming both an engine that never sees the value and an event that never happens.
6+
7+
The docblock, the `.describe()` and the slot's two pin-test comments now say what is true today, matching the wording of the already-corrected Expression Protocol dialect table: cron syntax is not checked at parse time and no engine evaluates this slot — `croner` judges a cron pattern only where a schedule is wired (`CronSchedule.expression`, a different slot) — so the verdict belongs to whatever external scheduler the author hands the value to. Documentation only: no exported symbol, no authorable key and no accept-set movement; the parse behaviour is byte-for-byte unchanged, and the pin that proves `'not a cron'` still normalizes is untouched.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
Liveness ledger: four verdicts re-derived and corrected ahead of the author-warning flip.
6+
7+
The ledger's `dead` and `live-elsewhere` verdicts are about to start warning downstream authors, so each row was re-measured against a pinned tree — objectstack `5d55afec4d`, objectui `a472b071` — with a firing positive control on the same instrument and corpus before any zero was read as a reading.
8+
9+
- **`validation.label` / `.description` / `.tags`: `dead``live`.** The 2026-08-10 sweep upheld `dead` on *reachability*, not on the read: `ValidationPreview` genuinely rendered all three, but the only route that mounted it was the standalone `validation` resource door, and ADR-0088 had retired that kind — so on the governed path (a rule embedded in its object) the preview was never handed a draft. That note named its own falsifier, and it has since landed: the standalone door is gone, and `EmbeddedItemEditor` now resolves `getMetadataPreview(editAs)` and mounts the preview on the live draft, with the embedded anchor binding `editAs: 'validation'`. Under the ruling that a designer preview rendering a key to a human is a runtime consumer, these three display keys are live. They remain docs-shaped and are still not author-warned.
10+
- **`view` `list.tabs`: `live``dead`.** The previous note was wrong in both directions at once. It credited objectui's `TabBar` with reading `icon`/`visible`/`pinned`/`filter` — true of the component, but **nothing mounts it**: every `TabBar` render site in the whole renderer tree is its own definition or one of its two test files, and `ListView` never reads `tabs` off the view schema, so authoring `list.tabs` draws no tab bar. And it called `tabs[].order` a dead sub-surface while `getVisibleTabs` sorts on exactly that key. The two author-time readers that do walk the key (a field-reference lint and the metadata diagnostics) check `tabs[].filter[].field` for reference integrity and deliver none of the key's declared effect — validated-then-ignored is accept/reject, which this ledger has always kept separate from liveness.
11+
12+
No published surface moves: these are ledger JSON rows plus the generated count table, with no export, key, or accept-set change. The `list.tabs` re-grade does mean an author who writes tabs on a list view will be told the key is inert — which it is, and was.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
`manifest.integrity`'s TSDoc no longer asserts an unpack-time verification that nobody performs.
6+
7+
The `integrity` docblocks said that per-file re-verification at unpack is the **cloud control plane's** obligation. The cloud repo's own design docs said it is the **runtime's**. Neither side unpacks anything, so the two published texts pointed at each other and a reader of either learned that a verification exists when none does. This text ships in `@objectstack/spec`'s `.d.ts`, so the wrong claim reached every consumer that hovered the field.
8+
9+
Both `integrity` docblocks in `manifest.zod.ts``PluginIntegritySchema` and the `ManifestSchema` field — now state what is true:
10+
11+
- **Computed and self-checked by the publisher.** `os plugin build` computes the map into the compiled manifest, and the `os plugin publish` preflight re-hashes the artifact bytes against it, refusing the upload on a digest mismatch, a declared entry with no file, or a packaged file the map does not declare. An absent map is a permissive pass — the field is `.optional()`.
12+
- **Not re-verified at unpack.** That leg is not implemented: there is no `os plugin install`, and the archive reader's only production caller is the publish preflight reading back its own output. It is owned by the **future runtime loader** (ADR-0025 §3.5 steps 4–7), not by the control plane, which stores the artifact blob opaquely. The enforce leg is tracked on #11331.
13+
14+
No schema, export, key or accept-set changes — the field's shape, optionality and `.describe()` are untouched, and a present `integrity` map validates exactly as before. What changes is that the documentation no longer advertises a guarantee the runtime does not deliver.
15+
16+
The liveness ledger row for `integrity` and its README note carry the same corrected attribution. The row's `status` (`dead`) and `verifiedAt` are deliberately unchanged: this is a prose correction, not a re-measurement.

.changeset/quiet-donkeys-smoke.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'@objectstack/cli': patch
3+
---
4+
5+
`os create plugin <name>` now derives the exported plugin symbol as a JavaScript identifier rather than copying the project name into an identifier position.
6+
7+
`validateProjectName` accepts exactly what npm accepts — a dot, an underscore and a leading digit included — so `os create plugin foo.bar` used to exit 0 having written `export const foo.barPlugin: Plugin = {`, a property access where a binding name belongs. The scaffolded project did not parse.
8+
9+
What the command accepts is unchanged, and so is what it emits as a name: the package name, its scope and the project directory stay byte-for-byte what was typed. Only the code identifier is normalised — every run of characters that is legal in an npm name but illegal in a JavaScript identifier now folds the way `-` already did, and a leading digit takes an `a` prefix. Ordinary names are unaffected (`my-app` still exports `myAppPlugin`). The emitted README names the derived symbol in prose, so the mapping is stated where it is read.

.github/workflows/ci.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,14 @@ jobs:
497497
- name: Run this shard's tests
498498
env:
499499
NODE_OPTIONS: --report-on-signal --report-signal=SIGUSR2 --report-directory=${{ runner.temp }}/stall-reports
500+
# The `e2e` and `live` filename tiers run NIGHTLY on main
501+
# (test-nightly-tiers.yml), not here: `queue` is the per-PR and
502+
# merge-queue setting, read once in scripts/nightly-tiers.mjs. Spelled
503+
# explicitly even though unset reads the same, so the setting this
504+
# required check verifies is written where the check runs.
505+
# turbo.json hashes it in the `test` task's `env`, which is what lets
506+
# it reach vitest under strict env mode at all.
507+
OS_TEST_TIERS: queue
500508
run: |
501509
if [ ! -s "$RUNNER_TEMP/shard-packages.txt" ]; then
502510
echo "No packages on this shard — nothing to test."

0 commit comments

Comments
 (0)