@oxyhq/contracts 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,27 +1,15 @@
1
1
  /**
2
- * Device-first bootstrap & token contracts (auth centralization, wave 1).
2
+ * First-party login result contract.
3
3
  *
4
- * SINGLE SOURCE OF TRUTH for the wire shape of the new device-first session
5
- * bootstrap: the top-level `#oxy_boot=…` fragment the API hands back from
6
- * `GET /auth/device/bootstrap`, the token bundle a boot code / web-session
7
- * fast-path exchanges into, the persisted-refresh rotation, the native
8
- * device-token issuance, the IdP chooser's device-resolve, and the first-party
9
- * password login result (2FA arm vs. session arm). The API validates its OUTPUT
10
- * against these schemas; every consumer (`@oxyhq/core`'s device-boot mixin, the
11
- * SDK cold boot, the IdP chooser) validates its INPUT against the same
12
- * definitions, so producer and consumers cannot drift.
4
+ * SINGLE SOURCE OF TRUTH for the first-party password login result (2FA arm vs.
5
+ * session arm). The API validates its OUTPUT against this schema; every consumer
6
+ * (`@oxyhq/core`'s auth mixin) validates its INPUT against the same definition,
7
+ * so producer and consumers cannot drift.
13
8
  *
14
- * Design anchors (from the auth-centralization plan):
15
- * - The bootstrap fragment carries NO tokens and NO deviceId — only a
16
- * `state` echo (CSRF), a `reason`, a short-lived single-use `code`, and an
17
- * opaque `deviceToken`. Tokens are obtained by exchanging the `code` at
18
- * `POST /auth/device/exchange` (origin-bound GETDEL burn).
19
- * - Refresh is ONE rotating, single-use family shared by web and native.
20
- * - `loginResult` mirrors what `POST /auth/login` returns today
21
- * (`buildSessionAuthResponse` in the API's `session.controller.ts`): either a
22
- * 2FA challenge (`{ twoFactorRequired: true, loginToken }`) or a session
23
- * payload. The session arm matches `SessionAuthResponse` EXACTLY, plus an
24
- * optional `refreshToken` the new server adds for the persisted-refresh lane.
9
+ * The device transport is `deviceId` + `deviceSecret` + `POST /session/device/token`
10
+ * (see `deviceSession.ts`). The legacy cookie/bootstrap/refresh-family lanes were
11
+ * removed in the zero-cookie cutover — nothing here carries a refresh token or a
12
+ * boot fragment.
25
13
  *
26
14
  * Nested-object response shapes are declared as explicit `interface`s with the
27
15
  * runtime schema annotated `z.ZodType<Interface>` — the same rationale as
@@ -34,129 +22,25 @@
34
22
  * `require()`).
35
23
  */
36
24
  import { z } from 'zod';
37
- import { userResponseSchema } from './userResponse.js';
38
- /* -------------------------------------------------------------------------- */
39
- /* Bootstrap fragment */
40
- /* -------------------------------------------------------------------------- */
41
- /**
42
- * Why the bootstrap hop resolved the way it did.
43
- * - `session` — the device cookie resolved an active session; a `code` is
44
- * present to exchange for tokens.
45
- * - `no_session` — the device is known but has no active session; no `code`.
46
- * - `new_device` — first contact; the cookie was just planted, no session yet.
47
- */
48
- export const deviceBootReasonSchema = z.enum(['session', 'no_session', 'new_device']);
49
- /**
50
- * The `#oxy_boot=<json>` fragment `GET /auth/device/bootstrap` appends to the
51
- * `return_to` URL. Carries the CSRF `state` echo, the resolution `reason`, the
52
- * opaque `deviceToken`, and — ONLY on the `session` arm — the single-use
53
- * exchange `code`. NEVER carries tokens or a deviceId.
54
- *
55
- * Discriminated on `reason` so the code↔reason coupling is enforced by the
56
- * schema, not by the consumer: the `session` arm REQUIRES `code`, and the
57
- * `no_session` / `new_device` arms omit it (a stray `code` on those arms is
58
- * stripped). A `session` fragment WITHOUT a code therefore fails to parse and
59
- * is treated as "no usable fragment" rather than half-processed.
60
- */
61
- const deviceBootFragmentBase = {
62
- v: z.literal(1),
63
- state: z.string().min(1).max(256),
64
- deviceToken: z.string().min(20).max(512),
65
- };
66
- export const deviceBootFragmentSchema = z.discriminatedUnion('reason', [
67
- z.object({
68
- ...deviceBootFragmentBase,
69
- reason: z.literal('session'),
70
- code: z.string().min(20).max(128),
71
- }),
72
- z.object({
73
- ...deviceBootFragmentBase,
74
- reason: z.literal('no_session'),
75
- }),
76
- z.object({
77
- ...deviceBootFragmentBase,
78
- reason: z.literal('new_device'),
79
- }),
80
- ]);
81
- /* -------------------------------------------------------------------------- */
82
- /* Boot-code exchange */
83
- /* -------------------------------------------------------------------------- */
84
- /** Request body for `POST /auth/device/exchange` — the single-use boot code. */
85
- export const deviceExchangeRequestSchema = z.object({
86
- code: z.string().min(20).max(128),
87
- });
88
- export const authTokenBundleSchema = z.object({
89
- sessionId: z.string(),
90
- accessToken: z.string(),
91
- refreshToken: z.string(),
92
- expiresAt: z.string(),
93
- user: userResponseSchema,
94
- deviceSecret: z.string().optional(),
95
- });
96
- // Internal (unexported) arm schemas — PLAIN `z.object` literals (no
97
- // `z.ZodType<>` annotation) so `z.discriminatedUnion` can introspect the
98
- // `reason` discriminator. The node10 `.d.ts`-degradation safety comes from the
99
- // EXPORTED symbols instead: the public types are explicit interfaces
100
- // (`WebSessionSession` / `WebSessionNoSession` / `WebSessionResult`) and the
101
- // exported schema is annotated `z.ZodType<WebSessionResult>` below, so the
102
- // emitted declaration states the shape literally rather than a degradable
103
- // `z.infer<>` of the nested `session` object.
104
- const webSessionSessionSchema = z.object({
105
- reason: z.literal('session'),
106
- session: authTokenBundleSchema,
107
- deviceToken: z.string().min(1),
108
- });
109
- const webSessionNoSessionSchema = z.object({
110
- reason: z.enum(['no_session', 'new_device']),
111
- deviceToken: z.string().min(1),
112
- });
113
- // Discriminated on `reason` — a true `discriminatedUnion` (not `z.union`): it
114
- // dispatches on the discriminator instead of sequentially probing each arm,
115
- // giving precise per-arm errors.
116
- export const webSessionResultSchema = z.discriminatedUnion('reason', [
117
- webSessionSessionSchema,
118
- webSessionNoSessionSchema,
119
- ]);
120
- /* -------------------------------------------------------------------------- */
121
- /* Refresh-token rotation (web + native, one implementation) */
122
- /* -------------------------------------------------------------------------- */
123
- /** Request body for `POST /auth/refresh-token` — the current refresh token. */
124
- export const tokenRefreshRequestSchema = z.object({
125
- refreshToken: z.string().min(20),
126
- });
127
- /**
128
- * Wire shape of `POST /auth/refresh-token`: the rotated (single-use) family —
129
- * a new access token, the next refresh token, the new access-token expiry, and
130
- * the owning session id. `expiresAt` is an ISO string.
131
- */
132
- export const tokenRefreshResponseSchema = z.object({
133
- accessToken: z.string(),
134
- refreshToken: z.string(),
135
- expiresAt: z.string(),
136
- sessionId: z.string(),
137
- });
138
- /* -------------------------------------------------------------------------- */
139
- /* Native device-token issuance */
140
- /* -------------------------------------------------------------------------- */
141
- /**
142
- * Wire shape of `POST /auth/device/token` — issues (or rotates) the opaque
143
- * device token for the native channel. The deviceId is taken from the bearer
144
- * JWT claims server-side; only the token comes back.
145
- */
146
- export const deviceTokenIssueResponseSchema = z.object({
147
- deviceToken: z.string(),
148
- });
149
25
  const loginTwoFactorRequiredSchema = z.object({
150
26
  twoFactorRequired: z.literal(true),
151
27
  loginToken: z.string(),
152
28
  });
29
+ const securityAlertSchema = z.object({
30
+ message: z.string(),
31
+ anomalies: z.array(z.object({
32
+ type: z.string(),
33
+ reason: z.string(),
34
+ details: z.string().optional(),
35
+ })),
36
+ });
153
37
  const loginSessionResultSchema = z.object({
154
38
  sessionId: z.string(),
155
39
  deviceId: z.string(),
156
40
  expiresAt: z.string(),
157
41
  accessToken: z.string().optional(),
158
- refreshToken: z.string().optional(),
159
42
  deviceSecret: z.string().optional(),
43
+ securityAlert: securityAlertSchema.optional(),
160
44
  user: z.object({
161
45
  id: z.string(),
162
46
  username: z.string().optional(),
@@ -167,24 +51,3 @@ export const loginResultSchema = z.union([
167
51
  loginTwoFactorRequiredSchema,
168
52
  loginSessionResultSchema,
169
53
  ]);
170
- /* -------------------------------------------------------------------------- */
171
- /* IdP chooser device-resolve */
172
- /* -------------------------------------------------------------------------- */
173
- /**
174
- * Request body for `POST /auth/device/resolve` (X-Oxy-Internal, called by the
175
- * IdP chooser) — the device key the chooser read from the first-party
176
- * `oxy_device` cookie.
177
- */
178
- export const deviceResolveRequestSchema = z.object({
179
- deviceKey: z.string().min(20),
180
- });
181
- const deviceResolveAccountSchema = z.object({
182
- user: userResponseSchema,
183
- sessionId: z.string(),
184
- accessToken: z.string(),
185
- expiresAt: z.string(),
186
- });
187
- export const deviceResolveResponseSchema = z.object({
188
- activeAccountId: z.string().nullable(),
189
- accounts: z.array(deviceResolveAccountSchema),
190
- });
package/dist/esm/index.js CHANGED
@@ -43,4 +43,4 @@ linkPreviewSchema, linkPreviewBatchRequestSchema, linkPreviewBatchResponseSchema
43
43
  export { sessionAccountSchema, deviceSessionStateSchema, activeTokenSchema, deviceSessionSyncSchema, deviceTokenMintRequestSchema, deviceTokenMintResponseSchema, } from './deviceSession.js';
44
44
  export {
45
45
  // Schemas
46
- deviceBootReasonSchema, deviceBootFragmentSchema, deviceExchangeRequestSchema, authTokenBundleSchema, webSessionResultSchema, tokenRefreshRequestSchema, tokenRefreshResponseSchema, deviceTokenIssueResponseSchema, loginResultSchema, deviceResolveRequestSchema, deviceResolveResponseSchema, } from './deviceBoot.js';
46
+ loginResultSchema, } from './deviceBoot.js';
@@ -16,8 +16,7 @@
16
16
  *
17
17
  * Faithful to the producers:
18
18
  * - `packages/api/src/utils/userTransform.ts` `formatUserResponse` — the
19
- * canonical serialization used by the device-first bootstrap/exchange
20
- * endpoints (`deviceAuth.ts`), login/signup, device sessions, etc.
19
+ * canonical serialization used by login/signup, device sessions, etc.
21
20
  * Emits `id` (NOT `_id`), forwards `username` verbatim (may be absent), and
22
21
  * emits `name` as the structured `{ first, last, full, displayName }`
23
22
  * subdocument.