Skip to content

Commit 80ad225

Browse files
committed
chore(changeset): patch entry for the /get-session envelope fix
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015QE8qk46e5CHJxyQEUjbf8
1 parent dc00e02 commit 80ad225

1 file changed

Lines changed: 63 additions & 0 deletions

File tree

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
---
2+
"@objectstack/client": patch
3+
---
4+
5+
fix(client): `auth.me` / `auth.refreshToken` deliver the `SessionResponse` envelope they declare, and `refreshToken` reads the token the route actually serves (#16760)
6+
7+
Both methods annotate their return as `SessionResponse` — ObjectStack's REST
8+
`{ success, data }` envelope — for `GET /api/v1/auth/get-session`. better-auth
9+
owns those bytes and answers **bare**. Measured against a real `AuthManager`
10+
(better-auth 1.7.2, organization plugin) over a real driver:
11+
12+
```
13+
GET /api/v1/auth/get-session (signed in) -> 200 {"user":{…},"session":{…,"token":"…"}}
14+
GET /api/v1/auth/get-session (anonymous) -> 200 null
15+
```
16+
17+
So `(await client.auth.me()).data.user` type-checked and was `undefined` at
18+
runtime, while `.user` — the real payload — did not type-check. The annotation
19+
pointed every caller at the wrong key.
20+
21+
## What changed
22+
23+
- The bare answer is now lifted into the declared envelope, the same lift
24+
`auth.login` has always carried for `/sign-in/email`. `SessionResponse` is
25+
**unchanged** and so is each method's published return annotation: the fix is
26+
in what the methods produce, not in what they promise.
27+
- The lift fills `success` as well as `data`. `SessionResponseSchema` is
28+
`BaseResponseSchema.extend(…)` and that base declares `success` as a required
29+
boolean, so a body carrying `data` alone still would not parse as the declared
30+
type.
31+
- The raw `.user` / `.session` keys are **kept** alongside `data`. They are what
32+
callers were pushed onto while the declared shape was unreachable; dropping
33+
them would trade one silent breakage for another.
34+
- `auth.refreshToken` now reads `data.session.token`. It used to read
35+
`data.data?.token` — a field this route does not produce at any nesting, so
36+
the method returned successfully having captured nothing. A bearer-mode client
37+
calling it to refresh kept whatever credential it already had, silently.
38+
39+
## The read was not a consequence of the envelope
40+
41+
Worth stating because the reverse is the natural assumption: enveloping the body
42+
does **not** put a token at `data.token`, because the route serves no top-level
43+
`token` to lift. The only credential in the body is `session.token`, and that is
44+
now the read. Fixing the shape alone would have left `refreshToken` exactly as
45+
inert as it was.
46+
47+
## FROM → TO
48+
49+
| you wrote | write instead |
50+
|:--|:--|
51+
| `(await client.auth.me()).user` | still works — kept deliberately |
52+
| `(await client.auth.me()).data.user` | now populated (was `undefined`) |
53+
| `(await client.auth.refreshToken(t)).data.token` | `.data.session.token` |
54+
55+
`refreshToken` stores the **unsigned** session token, which is the spelling
56+
`/get-session` serves; `bearer()` accepts it and the signed
57+
`token.signature` form interchangeably, so a client that held the signed form
58+
stays signed in across the call.
59+
60+
Two answers stay outside the declared type and are **not** addressed here: the
61+
anonymous `null`, which would need the published return annotation to widen, and
62+
`SessionUser.image`, declared `z.string().optional()` against a route that
63+
serves `null`. Both are filed separately.

0 commit comments

Comments
 (0)