@oxyhq/contracts 0.9.0 → 0.10.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.
@@ -48,17 +48,36 @@ import { userResponseSchema } from './userResponse.js';
48
48
  export const deviceBootReasonSchema = z.enum(['session', 'no_session', 'new_device']);
49
49
  /**
50
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`, an
52
- * optional single-use exchange `code` (present iff `reason === 'session'`), and
53
- * the opaque `deviceToken`. NEVER carries tokens or a deviceId.
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.
54
60
  */
55
- export const deviceBootFragmentSchema = z.object({
61
+ const deviceBootFragmentBase = {
56
62
  v: z.literal(1),
57
63
  state: z.string().min(1).max(256),
58
- reason: deviceBootReasonSchema,
59
- code: z.string().min(20).max(128).optional(),
60
64
  deviceToken: z.string().min(20).max(512),
61
- });
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
+ ]);
62
81
  /* -------------------------------------------------------------------------- */
63
82
  /* Boot-code exchange */
64
83
  /* -------------------------------------------------------------------------- */
@@ -73,6 +92,30 @@ export const authTokenBundleSchema = z.object({
73
92
  expiresAt: z.string(),
74
93
  user: userResponseSchema,
75
94
  });
95
+ // Internal (unexported) arm schemas — PLAIN `z.object` literals (no
96
+ // `z.ZodType<>` annotation) so `z.discriminatedUnion` can introspect the
97
+ // `reason` discriminator. The node10 `.d.ts`-degradation safety comes from the
98
+ // EXPORTED symbols instead: the public types are explicit interfaces
99
+ // (`WebSessionSession` / `WebSessionNoSession` / `WebSessionResult`) and the
100
+ // exported schema is annotated `z.ZodType<WebSessionResult>` below, so the
101
+ // emitted declaration states the shape literally rather than a degradable
102
+ // `z.infer<>` of the nested `session` object.
103
+ const webSessionSessionSchema = z.object({
104
+ reason: z.literal('session'),
105
+ session: authTokenBundleSchema,
106
+ deviceToken: z.string().min(1),
107
+ });
108
+ const webSessionNoSessionSchema = z.object({
109
+ reason: z.enum(['no_session', 'new_device']),
110
+ deviceToken: z.string().min(1),
111
+ });
112
+ // Discriminated on `reason` — a true `discriminatedUnion` (not `z.union`): it
113
+ // dispatches on the discriminator instead of sequentially probing each arm,
114
+ // giving precise per-arm errors.
115
+ export const webSessionResultSchema = z.discriminatedUnion('reason', [
116
+ webSessionSessionSchema,
117
+ webSessionNoSessionSchema,
118
+ ]);
76
119
  /* -------------------------------------------------------------------------- */
77
120
  /* Refresh-token rotation (web + native, one implementation) */
78
121
  /* -------------------------------------------------------------------------- */
package/dist/esm/index.js CHANGED
@@ -11,7 +11,7 @@
11
11
  */
12
12
  export {
13
13
  // Schemas
14
- userNameSchema, userResponseSchema, userProfileUpdateSchema, refreshAllAccountSchema, refreshAllResponseSchema, currentUserResponseSchema, deviceSessionAccountSchema, deviceSessionsResponseSchema,
14
+ userNameSchema, userResponseSchema, userProfileUpdateSchema, currentUserResponseSchema, deviceSessionAccountSchema, deviceSessionsResponseSchema,
15
15
  // Helpers
16
16
  resolveUserId, safeParseContract, } from './userResponse.js';
17
17
  export {
@@ -43,4 +43,4 @@ linkPreviewSchema, linkPreviewBatchRequestSchema, linkPreviewBatchResponseSchema
43
43
  export { sessionAccountSchema, deviceSessionStateSchema, activeTokenSchema, deviceSessionSyncSchema, } from './deviceSession.js';
44
44
  export {
45
45
  // Schemas
46
- deviceBootReasonSchema, deviceBootFragmentSchema, deviceExchangeRequestSchema, authTokenBundleSchema, tokenRefreshRequestSchema, tokenRefreshResponseSchema, deviceTokenIssueResponseSchema, loginResultSchema, deviceResolveRequestSchema, deviceResolveResponseSchema, } from './deviceBoot.js';
46
+ deviceBootReasonSchema, deviceBootFragmentSchema, deviceExchangeRequestSchema, authTokenBundleSchema, webSessionResultSchema, tokenRefreshRequestSchema, tokenRefreshResponseSchema, deviceTokenIssueResponseSchema, loginResultSchema, deviceResolveRequestSchema, deviceResolveResponseSchema, } from './deviceBoot.js';
@@ -106,7 +106,6 @@ export const userProfileUpdateSchema = z
106
106
  phone: z.string().optional(),
107
107
  address: z.string().optional(),
108
108
  birthday: z.string().optional(),
109
- location: z.string().optional(),
110
109
  locations: z.array(z.unknown()).optional(),
111
110
  links: z.array(z.string()).optional(),
112
111
  linksMetadata: z
@@ -132,28 +131,6 @@ export const userProfileUpdateSchema = z
132
131
  export function resolveUserId(user) {
133
132
  return user.id ?? user._id;
134
133
  }
135
- /**
136
- * One rotated account entry from `POST /auth/refresh-all`.
137
- *
138
- * `authuser` is the device-local slot index (`0..N-1`). `user` is the canonical
139
- * {@link userResponseSchema} shape (the handler projects a whitelist and runs it
140
- * through `formatUserResponse`).
141
- */
142
- export const refreshAllAccountSchema = z.object({
143
- authuser: z.number().int().nonnegative(),
144
- accessToken: z.string(),
145
- expiresAt: z.string(),
146
- sessionId: z.string(),
147
- user: userResponseSchema,
148
- });
149
- /**
150
- * Wire shape of `POST /auth/refresh-all`: every valid device-local account,
151
- * sorted by `authuser` ascending. An empty `accounts` array means "no signed-in
152
- * accounts on this device" — the IdP must show the sign-in form.
153
- */
154
- export const refreshAllResponseSchema = z.object({
155
- accounts: z.array(refreshAllAccountSchema),
156
- });
157
134
  /**
158
135
  * Wire shape of `GET /users/me` — the API success envelope (`{ data: <user> }`)
159
136
  * wrapping the current-user DTO. Some older producers use `_id` instead of