|
| 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` (#17235). The sibling `auth.login` / `auth.register`, which |
| 64 | +normalize into `data` but set no `success`, are #17234. |
0 commit comments