@oxy.so/contracts 1.1.1 → 1.3.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.
Files changed (65) hide show
  1. package/dist/cjs/.tsbuildinfo +1 -1
  2. package/dist/cjs/identity.js +3 -2
  3. package/dist/cjs/identityMove.js +156 -0
  4. package/dist/cjs/identityProof.js +164 -0
  5. package/dist/cjs/identityRecovery.js +51 -0
  6. package/dist/cjs/index.js +75 -19
  7. package/dist/cjs/inference/catalogue.js +7 -6
  8. package/dist/cjs/inference/identifiers.js +3 -1
  9. package/dist/cjs/inference/providerConnection.js +1 -1
  10. package/dist/cjs/inference/request.js +15 -1
  11. package/dist/cjs/inference/streamEvents.js +24 -2
  12. package/dist/cjs/inference/version.js +1 -1
  13. package/dist/cjs/userResponse.js +82 -1
  14. package/dist/cjs/username.js +70 -2
  15. package/dist/cjs/webIdentityCarrier.js +181 -0
  16. package/dist/cjs/webauthn.js +14 -0
  17. package/dist/esm/.tsbuildinfo +1 -1
  18. package/dist/esm/identity.js +3 -2
  19. package/dist/esm/identityMove.js +148 -0
  20. package/dist/esm/identityProof.js +159 -0
  21. package/dist/esm/identityRecovery.js +48 -0
  22. package/dist/esm/index.js +16 -7
  23. package/dist/esm/inference/catalogue.js +7 -6
  24. package/dist/esm/inference/identifiers.js +2 -0
  25. package/dist/esm/inference/providerConnection.js +2 -2
  26. package/dist/esm/inference/request.js +14 -0
  27. package/dist/esm/inference/streamEvents.js +24 -2
  28. package/dist/esm/inference/version.js +1 -1
  29. package/dist/esm/userResponse.js +81 -0
  30. package/dist/esm/username.js +69 -1
  31. package/dist/esm/webIdentityCarrier.js +178 -0
  32. package/dist/esm/webauthn.js +14 -0
  33. package/dist/types/.tsbuildinfo +1 -1
  34. package/dist/types/accountGraph.d.ts +7 -7
  35. package/dist/types/agency.d.ts +24 -24
  36. package/dist/types/browserHub.d.ts +16 -16
  37. package/dist/types/deviceDirectory.d.ts +28 -28
  38. package/dist/types/externalIdentity.d.ts +21 -7
  39. package/dist/types/identity.d.ts +3 -2
  40. package/dist/types/identityMove.d.ts +185 -0
  41. package/dist/types/identityProof.d.ts +156 -0
  42. package/dist/types/identityRecovery.d.ts +246 -0
  43. package/dist/types/index.d.ts +14 -8
  44. package/dist/types/inference/catalogue.d.ts +21 -20
  45. package/dist/types/inference/embeddings.d.ts +12 -12
  46. package/dist/types/inference/errors.d.ts +4 -4
  47. package/dist/types/inference/identifiers.d.ts +2 -0
  48. package/dist/types/inference/inbox.d.ts +6 -6
  49. package/dist/types/inference/providerConnection.d.ts +40 -40
  50. package/dist/types/inference/request.d.ts +84 -36
  51. package/dist/types/inference/streamEvents.d.ts +69 -13
  52. package/dist/types/inference/usage.d.ts +8 -8
  53. package/dist/types/inference/version.d.ts +1 -1
  54. package/dist/types/keyRotation.d.ts +2 -2
  55. package/dist/types/oauth.d.ts +30 -30
  56. package/dist/types/sessionStatus.d.ts +6 -6
  57. package/dist/types/transparency.d.ts +10 -10
  58. package/dist/types/userResponse.d.ts +386 -18
  59. package/dist/types/username.d.ts +25 -2
  60. package/dist/types/webIdentityCarrier.d.ts +1130 -0
  61. package/dist/types/webauthn.d.ts +208 -0
  62. package/package.json +1 -1
  63. package/dist/cjs/devicePairing.js +0 -138
  64. package/dist/esm/devicePairing.js +0 -135
  65. package/dist/types/devicePairing.d.ts +0 -130
@@ -137,12 +137,13 @@ exports.inferenceDataPolicySchema = zod_1.z
137
137
  }
138
138
  });
139
139
  /**
140
- * Who a route may be served to. Availability inside Alia never implies
141
- * permission to resell the same provider/model publicly, which is why this is
142
- * an explicit scope on the route rather than a boolean derived from "it works".
140
+ * Who a route may be served to. Availability to an official Oxy product never
141
+ * implies permission to resell the same provider/model publicly, which is why
142
+ * this is an explicit scope on the route rather than a boolean derived from
143
+ * "it works".
143
144
  */
144
145
  exports.availabilityScopeSchema = zod_1.z.enum([
145
- 'internal_alia',
146
+ 'platform_internal',
146
147
  'public_payg',
147
148
  'enterprise',
148
149
  'byok_only',
@@ -335,7 +336,7 @@ exports.inferenceProviderSchema = zod_1.z.object({
335
336
  exports.modelDeploymentSchema = zod_1.z
336
337
  .object({
337
338
  /** See `version.ts`: exchanged with the data plane on its own. */
338
- schemaVersion: zod_1.z.literal(1),
339
+ schemaVersion: zod_1.z.literal(2),
339
340
  deploymentId: identifiers_1.deploymentIdSchema,
340
341
  provider: identifiers_1.inferenceProviderSlugSchema,
341
342
  /** Always revision-pinned: a deployment serves specific weights. */
@@ -458,7 +459,7 @@ exports.catalogueServingProviderSummarySchema = zod_1.z
458
459
  */
459
460
  exports.modelCatalogueEntrySchema = zod_1.z.object({
460
461
  /** See `version.ts`: this is the public catalogue response shape. */
461
- schemaVersion: zod_1.z.literal(2),
462
+ schemaVersion: zod_1.z.literal(3),
462
463
  modelId: identifiers_1.modelIdSchema,
463
464
  publisher: exports.cataloguePublisherSummarySchema,
464
465
  displayName: zod_1.z.string().min(1).max(200),
@@ -19,7 +19,7 @@
19
19
  * Decided in: docs/adr/0007-canonical-request-attribution.md, docs/adr/0008-catalogue-concept-separation.md.
20
20
  */
21
21
  Object.defineProperty(exports, "__esModule", { value: true });
22
- exports.RESERVED_ALIA_PUBLISHER = exports.inferenceRegionSchema = exports.deploymentIdSchema = exports.inferenceProviderSlugSchema = exports.routingProfileIdSchema = exports.routingProfileSlugSchema = exports.modelReferenceSchema = exports.modelRevisionLabelSchema = exports.modelIdSchema = exports.modelSlugSchema = exports.publisherSlugSchema = exports.sha256DigestSchema = exports.inferenceHttpsUrlSchema = exports.inferenceDateSchema = exports.inferenceTimestampSchema = exports.inferenceEnvironmentSchema = exports.idempotencyKeySchema = exports.generationIdSchema = exports.requestIdSchema = exports.oxyCredentialIdSchema = exports.oxyApplicationIdSchema = exports.delegatedUserIdSchema = exports.oxyAccountIdSchema = void 0;
22
+ exports.RESERVED_ALIA_PUBLISHER = exports.inferenceRegionSchema = exports.deploymentIdSchema = exports.inferenceProviderSlugSchema = exports.routingProfileIdSchema = exports.routingProfileSlugSchema = exports.modelReferenceSchema = exports.modelRevisionLabelSchema = exports.modelIdSchema = exports.modelSlugSchema = exports.publisherSlugSchema = exports.PADDED_BASE64_PATTERN = exports.sha256DigestSchema = exports.inferenceHttpsUrlSchema = exports.inferenceDateSchema = exports.inferenceTimestampSchema = exports.inferenceEnvironmentSchema = exports.idempotencyKeySchema = exports.generationIdSchema = exports.requestIdSchema = exports.oxyCredentialIdSchema = exports.oxyApplicationIdSchema = exports.delegatedUserIdSchema = exports.oxyAccountIdSchema = void 0;
23
23
  const zod_1 = require("zod");
24
24
  /* -------------------------------------------------------------------------- */
25
25
  /* Principal identifiers */
@@ -129,6 +129,8 @@ exports.inferenceHttpsUrlSchema = zod_1.z
129
129
  exports.sha256DigestSchema = zod_1.z
130
130
  .string()
131
131
  .regex(/^sha256:[a-f0-9]{64}$/, 'digest must be sha256:<64 lowercase hex>');
132
+ /** Standard base64 in whole, padded 4-character groups. */
133
+ exports.PADDED_BASE64_PATTERN = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
132
134
  /* -------------------------------------------------------------------------- */
133
135
  /* Catalogue references */
134
136
  /* -------------------------------------------------------------------------- */
@@ -114,7 +114,7 @@ const kaanaCredentialSecretBase64Schema = zod_1.z
114
114
  .string()
115
115
  .min(1)
116
116
  .max(8192)
117
- .regex(/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/)
117
+ .regex(identifiers_1.PADDED_BASE64_PATTERN)
118
118
  .refine(isVisibleASCIIProviderCredential, {
119
119
  message: 'a decoded provider credential is 1-4096 visible ASCII bytes',
120
120
  });
@@ -25,7 +25,7 @@
25
25
  * docs/adr/0017-authorized-routes-in-the-envelope.md.
26
26
  */
27
27
  Object.defineProperty(exports, "__esModule", { value: true });
28
- exports.inferenceRequestSchema = exports.clientRequestMetadataSchema = exports.responseFormatSchema = exports.toolChoiceSchema = exports.toolDefinitionSchema = exports.samplingParametersSchema = exports.inferenceInputSchema = exports.inferenceMessageSchema = exports.inferenceMessageRoleSchema = exports.inferenceToolCallSchema = exports.inferenceContentPartSchema = exports.inferenceContentSourceSchema = void 0;
28
+ exports.inferenceRequestSchema = exports.inferenceSpeechParametersSchema = exports.clientRequestMetadataSchema = exports.responseFormatSchema = exports.toolChoiceSchema = exports.toolDefinitionSchema = exports.samplingParametersSchema = exports.inferenceInputSchema = exports.inferenceMessageSchema = exports.inferenceMessageRoleSchema = exports.inferenceToolCallSchema = exports.inferenceContentPartSchema = exports.inferenceContentSourceSchema = void 0;
29
29
  const zod_1 = require("zod");
30
30
  const attribution_1 = require("./attribution");
31
31
  const catalogue_1 = require("./catalogue");
@@ -314,6 +314,12 @@ const modelLineOf = (reference) => {
314
314
  const at = reference.indexOf("@");
315
315
  return at === -1 ? reference : reference.slice(0, at);
316
316
  };
317
+ /** Parameters for text-to-speech, preserved in the signed request. */
318
+ exports.inferenceSpeechParametersSchema = zod_1.z.object({
319
+ voice: zod_1.z.string().min(1).max(64),
320
+ responseFormat: zod_1.z.enum(["mp3", "opus", "aac", "flac", "wav", "pcm"]),
321
+ speed: zod_1.z.number().min(0.25).max(4).optional(),
322
+ }).strict();
317
323
  /**
318
324
  * The canonical internal request Oxy forwards to the data plane.
319
325
  *
@@ -333,6 +339,7 @@ exports.inferenceRequestSchema = zod_1.z
333
339
  stream: zod_1.z.boolean(),
334
340
  maxOutputTokens: zod_1.z.number().int().positive().safe().optional(),
335
341
  sampling: exports.samplingParametersSchema,
342
+ speech: exports.inferenceSpeechParametersSchema.optional(),
336
343
  tools: zod_1.z.array(exports.toolDefinitionSchema).default([]),
337
344
  toolChoice: exports.toolChoiceSchema.optional(),
338
345
  responseFormat: exports.responseFormatSchema.optional(),
@@ -375,6 +382,13 @@ exports.inferenceRequestSchema = zod_1.z
375
382
  authorizedRoutes: zod_1.z.array(routingPolicy_1.authorizedRouteSchema).min(1).optional(),
376
383
  })
377
384
  .superRefine((request, ctx) => {
385
+ const isSpeech = request.client.apiFormat === "audio_speech";
386
+ if (isSpeech && request.speech !== undefined && (request.modality !== "audio" || request.input.format !== "text" || request.stream)) {
387
+ ctx.addIssue({ code: zod_1.z.ZodIssueCode.custom, path: ["speech"], message: "speech requires audio modality, text input, parameters and non-streaming output" });
388
+ }
389
+ if (!isSpeech && request.speech !== undefined) {
390
+ ctx.addIssue({ code: zod_1.z.ZodIssueCode.custom, path: ["speech"], message: "speech parameters require the audio_speech API format" });
391
+ }
378
392
  if (request.toolChoice !== undefined && request.tools.length === 0) {
379
393
  ctx.addIssue({
380
394
  code: zod_1.z.ZodIssueCode.custom,
@@ -3,7 +3,7 @@
3
3
  * Normalized stream events — what the data plane emits and the Oxy edge
4
4
  * forwards as SSE.
5
5
  *
6
- * One discriminated union, seven shapes, all carrying `requestId` and a
6
+ * One discriminated union, eight shapes, all carrying `requestId` and a
7
7
  * monotonic `sequence`. `requestId` is on EVERY event rather than only the
8
8
  * first because a proxy that re-frames or a client that reconnects would
9
9
  * otherwise be holding events it cannot attribute; `sequence` is what makes a
@@ -22,7 +22,7 @@
22
22
  * Decided in: docs/adr/0010-public-api-compatibility.md, docs/adr/0008-catalogue-concept-separation.md.
23
23
  */
24
24
  Object.defineProperty(exports, "__esModule", { value: true });
25
- exports.inferenceStreamEventSchema = exports.inferenceStreamDoneEventSchema = exports.inferenceFinishReasonSchema = exports.inferenceStreamErrorEventSchema = exports.inferenceStreamRouteSwitchEventSchema = exports.inferenceRouteSwitchReasonSchema = exports.inferenceRouteSwitchDetailSchema = exports.inferenceStreamUsageEventSchema = exports.inferenceStreamToolCallEventSchema = exports.inferenceStreamDeltaEventSchema = exports.inferenceStreamStartEventSchema = void 0;
25
+ exports.inferenceStreamEventSchema = exports.inferenceStreamDoneEventSchema = exports.inferenceFinishReasonSchema = exports.inferenceStreamErrorEventSchema = exports.inferenceStreamRouteSwitchEventSchema = exports.inferenceRouteSwitchReasonSchema = exports.inferenceRouteSwitchDetailSchema = exports.inferenceStreamUsageEventSchema = exports.inferenceStreamToolCallEventSchema = exports.inferenceStreamAudioEventSchema = exports.MAX_INFERENCE_AUDIO_BYTES = exports.inferenceAudioMediaTypeSchema = exports.inferenceStreamDeltaEventSchema = exports.inferenceStreamStartEventSchema = void 0;
26
26
  const zod_1 = require("zod");
27
27
  const identifiers_1 = require("./identifiers");
28
28
  const errors_1 = require("./errors");
@@ -65,6 +65,27 @@ exports.inferenceStreamDeltaEventSchema = zod_1.z.object({
65
65
  channel: zod_1.z.enum(['output_text', 'reasoning', 'refusal']),
66
66
  text: zod_1.z.string(),
67
67
  });
68
+ /** The encodings an audio output may carry. */
69
+ exports.inferenceAudioMediaTypeSchema = zod_1.z.enum([
70
+ 'audio/mpeg',
71
+ 'audio/wav',
72
+ 'audio/ogg',
73
+ 'audio/aac',
74
+ 'audio/flac',
75
+ 'audio/pcm',
76
+ ]);
77
+ /** The most bytes one folded audio output may hold, at the edge and in the SDK. */
78
+ exports.MAX_INFERENCE_AUDIO_BYTES = 20 * 1024 * 1024;
79
+ /** A bounded, independently base64-encoded chunk of one audio output. */
80
+ exports.inferenceStreamAudioEventSchema = zod_1.z.object({
81
+ schemaVersion: zod_1.z.literal(1),
82
+ type: zod_1.z.literal('audio'),
83
+ requestId: identifiers_1.requestIdSchema,
84
+ sequence: zod_1.z.number().int().nonnegative().safe(),
85
+ outputIndex: zod_1.z.number().int().nonnegative().safe(),
86
+ mediaType: exports.inferenceAudioMediaTypeSchema,
87
+ data: zod_1.z.string().min(4).max(65536).regex(identifiers_1.PADDED_BASE64_PATTERN),
88
+ });
68
89
  /**
69
90
  * A tool call being streamed.
70
91
  *
@@ -250,6 +271,7 @@ exports.inferenceStreamDoneEventSchema = zod_1.z.object({
250
271
  exports.inferenceStreamEventSchema = zod_1.z.discriminatedUnion('type', [
251
272
  exports.inferenceStreamStartEventSchema,
252
273
  exports.inferenceStreamDeltaEventSchema,
274
+ exports.inferenceStreamAudioEventSchema,
253
275
  exports.inferenceStreamToolCallEventSchema,
254
276
  exports.inferenceStreamUsageEventSchema,
255
277
  exports.inferenceStreamRouteSwitchEventSchema,
@@ -102,4 +102,4 @@ exports.INFERENCE_CONTRACT_VERSION = void 0;
102
102
  * change to, say, the catalogue reject every in-flight inference request; the
103
103
  * per-shape `schemaVersion` is what a message is validated against.
104
104
  */
105
- exports.INFERENCE_CONTRACT_VERSION = '2.0.0';
105
+ exports.INFERENCE_CONTRACT_VERSION = '3.0.0';
@@ -30,7 +30,7 @@
30
30
  * `require()`).
31
31
  */
32
32
  Object.defineProperty(exports, "__esModule", { value: true });
33
- exports.deviceLinkedSessionsResponseSchema = exports.deviceLinkedSessionSchema = exports.currentUserResponseSchema = exports.userProfileUpdateSchema = exports.userResponseSchema = exports.themePreferenceSchema = exports.userRelationshipSchema = exports.userNameSchema = void 0;
33
+ exports.deviceLinkedSessionsResponseSchema = exports.deviceLinkedSessionSchema = exports.currentUserResponseSchema = exports.userProfileUpdateSchema = exports.userResponseSchema = exports.dateOfBirthSchema = exports.themePreferenceSchema = exports.userRelationshipSchema = exports.userNameSchema = void 0;
34
34
  exports.resolveUserId = resolveUserId;
35
35
  exports.safeParseContract = safeParseContract;
36
36
  const zod_1 = require("zod");
@@ -52,6 +52,60 @@ exports.themePreferenceSchema = zod_1.z.object({
52
52
  mode: zod_1.z.enum(['light', 'dark', 'system']),
53
53
  colorPreset: zod_1.z.string(),
54
54
  });
55
+ /**
56
+ * The earliest calendar year `dateOfBirthSchema` accepts.
57
+ *
58
+ * Not a real biological bound — it exists to catch an obviously-transposed
59
+ * year (`1027` for `2027`, a stray OCR/typo digit) with a clear message
60
+ * instead of the value quietly becoming a 150-year-old account. 1900 is
61
+ * generous enough that no living person's real birthdate is rejected by it.
62
+ */
63
+ const MIN_BIRTH_YEAR = 1900;
64
+ /**
65
+ * `true` when `year`/`month`/`day` name a date that actually exists on the
66
+ * Gregorian calendar — the check `z.string().regex(...)` alone cannot make,
67
+ * since the regex only constrains digit COUNT and would pass `2024-02-30`.
68
+ */
69
+ function isRealCalendarDate(year, month, day) {
70
+ const isLeapYear = (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
71
+ const daysInMonth = [31, isLeapYear ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
72
+ return month >= 1 && month <= 12 && day >= 1 && day <= daysInMonth[month - 1];
73
+ }
74
+ /**
75
+ * A date of birth, `YYYY-MM-DD`, the sole structured representation this
76
+ * platform stores going forward (`users.date_of_birth` — see
77
+ * `packages/api/src/db/schema/users.ts`). `birthday` (below) stays as the
78
+ * legacy free-text field for backward compatibility with existing readers;
79
+ * this schema is what both the write path (`user.service.ts`) and the read
80
+ * path (nothing — a date of birth is owner-only, never echoed to another
81
+ * viewer) validate against.
82
+ *
83
+ * Three checks, in order, because a regex alone would accept a string-shaped
84
+ * lie:
85
+ * 1. Exactly `YYYY-MM-DD` — the wire format, nothing looser.
86
+ * 2. A real Gregorian date — rejects `2024-02-30`, a date the regex cannot see
87
+ * is impossible.
88
+ * 3. Bounded to a plausible human lifetime — not before {@link MIN_BIRTH_YEAR}
89
+ * and not after today (comparing the zero-padded ISO strings directly is
90
+ * a valid, simpler stand-in for a numeric comparison here, since two
91
+ * `YYYY-MM-DD` strings of equal length sort exactly the way their dates
92
+ * do). "Today" is UTC — see `computeIsAdult` in `user.service.ts` for why
93
+ * a date with no timezone of its own is evaluated in UTC rather than any
94
+ * particular caller's local zone.
95
+ */
96
+ exports.dateOfBirthSchema = zod_1.z
97
+ .string()
98
+ .regex(/^\d{4}-\d{2}-\d{2}$/, 'dateOfBirth must be an ISO 8601 calendar date (YYYY-MM-DD)')
99
+ .refine((value) => {
100
+ const [year, month, day] = value.split('-').map(Number);
101
+ return isRealCalendarDate(year, month, day);
102
+ }, { message: 'dateOfBirth is not a real calendar date' })
103
+ .refine((value) => Number(value.slice(0, 4)) >= MIN_BIRTH_YEAR, {
104
+ message: `dateOfBirth must not be before ${MIN_BIRTH_YEAR}`,
105
+ })
106
+ .refine((value) => value <= new Date().toISOString().slice(0, 10), {
107
+ message: 'dateOfBirth must not be in the future',
108
+ });
55
109
  /**
56
110
  * The canonical user object emitted by `formatUserResponse`.
57
111
  *
@@ -81,6 +135,26 @@ exports.userResponseSchema = zod_1.z
81
135
  phone: zod_1.z.string().optional(),
82
136
  address: zod_1.z.string().optional(),
83
137
  birthday: zod_1.z.string().optional(),
138
+ /**
139
+ * Structured date of birth, `YYYY-MM-DD`. Present only on the
140
+ * account's OWN profile response (`GET /users/me`, `PUT /users/me`
141
+ * with `includePrivateFields`) — never on another account's profile,
142
+ * the same visibility `phone`/`address`/`birthday` already have. See
143
+ * {@link dateOfBirthSchema}.
144
+ */
145
+ dateOfBirth: exports.dateOfBirthSchema.optional(),
146
+ /**
147
+ * Derived, non-PII signal: whether the account holder is at least 18
148
+ * (see `computeIsAdult` in `user.service.ts` for the exact threshold
149
+ * and the UTC-"today" choice). Computed fresh on every read — age
150
+ * changes daily, so this is never stored. `undefined` when
151
+ * `dateOfBirth` is unset ("unknown"), distinct from `false` ("known,
152
+ * not yet 18"). Rides the same owner-only visibility as
153
+ * `dateOfBirth`; a future pass may widen this specific field to
154
+ * other viewers without exposing the birthdate itself, but that is
155
+ * not decided here.
156
+ */
157
+ isAdult: zod_1.z.boolean().optional(),
84
158
  /** Avatar file id (string) or null. */
85
159
  avatar: zod_1.z.string().nullable().optional(),
86
160
  /** Named Bloom color preset (e.g. `"blue"`) or null. */
@@ -176,6 +250,13 @@ exports.userProfileUpdateSchema = zod_1.z
176
250
  phone: zod_1.z.string().optional(),
177
251
  address: zod_1.z.string().optional(),
178
252
  birthday: zod_1.z.string().optional(),
253
+ /**
254
+ * Structured date of birth. `null` (or `''`, at the service layer)
255
+ * clears it. Independently settable from `birthday` — see
256
+ * `user.service.ts`'s `updateUserProfile` for why the two legacy and
257
+ * structured fields are not kept in sync with each other.
258
+ */
259
+ dateOfBirth: exports.dateOfBirthSchema.nullable().optional(),
179
260
  locations: zod_1.z.array(zod_1.z.unknown()).optional(),
180
261
  links: zod_1.z.array(zod_1.z.string()).optional(),
181
262
  linksMetadata: zod_1.z
@@ -100,7 +100,7 @@
100
100
  * knows what is taken, and it must not pretend to.
101
101
  */
102
102
  Object.defineProperty(exports, "__esModule", { value: true });
103
- exports.botUsernameSchema = exports.BOT_USERNAME_INVALID_MESSAGE = exports.BOT_USERNAME_SUFFIX = exports.usernameSchema = exports.USERNAME_PATTERN_SOURCE = exports.USERNAME_INVALID_MESSAGE = exports.USERNAME_MAX_LENGTH = exports.USERNAME_MIN_LENGTH = void 0;
103
+ exports.botUsernameSchema = exports.BOT_USERNAME_INVALID_MESSAGE = exports.BOT_USERNAME_SUFFIX = exports.usernameSchema = exports.USERNAME_PATTERN_SOURCE = exports.NUMERIC_USERNAME_MESSAGE = exports.RESERVED_USERNAME_MESSAGE = exports.RESERVED_USERNAMES = exports.USERNAME_INVALID_MESSAGE = exports.USERNAME_MAX_LENGTH = exports.USERNAME_MIN_LENGTH = void 0;
104
104
  exports.isValidUsername = isValidUsername;
105
105
  exports.stripDisallowedUsernameCharacters = stripDisallowedUsernameCharacters;
106
106
  exports.usernameSchemaForAccountKind = usernameSchemaForAccountKind;
@@ -113,6 +113,71 @@ exports.USERNAME_MAX_LENGTH = 30;
113
113
  /** The 400 / inline-validation copy for every path that rejects a handle. */
114
114
  exports.USERNAME_INVALID_MESSAGE = 'Username must be 3-30 characters, use only letters, numbers, hyphens and underscores, ' +
115
115
  'start and end with a letter or number, and never repeat a separator';
116
+ /**
117
+ * Exact-match handles nobody may newly claim, regardless of account kind.
118
+ *
119
+ * This is NOT a namespace tightening like the bot suffix — it is a list of
120
+ * names withheld from `users_lower_username_key` before anybody asks for
121
+ * them. Compared against the same normalization the unique index applies
122
+ * (`lower(btrim(username))`), so `Admin`, ` ADMIN ` and `admin` are the one
123
+ * name this list means.
124
+ *
125
+ * `oxy`, `mention`, `homiio`, `clarity`, `faircoin`, `astro` and `mercaria`
126
+ * are deliberately NOT here: each already has an `organization`/`project`
127
+ * row, so the unique index already refuses a second one, and `oxy` is the
128
+ * name `USERNAME_MIN_LENGTH` is pinned against in
129
+ * `__tests__/username.test.ts` — listing it would make
130
+ * `usernameSchema.safeParse('oxy')` fail and falsify that comment. A brand
131
+ * with no account of its own yet (measured 2026-09-18: Alia, Allo, TNP,
132
+ * Kaana, Bloom) has no row to protect it, so it is listed until one exists.
133
+ */
134
+ exports.RESERVED_USERNAMES = new Set([
135
+ // Oxy product lines with no account row of their own yet.
136
+ 'alia',
137
+ 'allo',
138
+ 'tnp',
139
+ 'kaana',
140
+ 'bloom',
141
+ // System / role words a signup impersonating staff or Oxy itself would reach for.
142
+ 'admin',
143
+ 'administrator',
144
+ 'root',
145
+ 'superadmin',
146
+ 'super',
147
+ 'superuser',
148
+ 'support',
149
+ 'staff',
150
+ 'moderator',
151
+ 'mod',
152
+ 'security',
153
+ 'system',
154
+ 'official',
155
+ 'help',
156
+ 'noreply',
157
+ 'anonymous',
158
+ 'everyone',
159
+ 'owner',
160
+ ]);
161
+ /** The 400 / inline-validation copy for a handle on {@link RESERVED_USERNAMES}. */
162
+ exports.RESERVED_USERNAME_MESSAGE = 'This username is reserved and cannot be registered';
163
+ /**
164
+ * {@link RESERVED_USERNAMES}, minus `alia`, checked against each `-`/`_`
165
+ * separated SEGMENT of a candidate rather than the whole string — so
166
+ * `official-oxy`, `super-admin` and `team-kaana` are refused the same as the
167
+ * bare words, without banning every word that merely CONTAINS one as a
168
+ * substring (`superman`, `modern`, `grassroot`, `homeowner` all stay legal:
169
+ * none of them separates the reserved word from the rest with `-` or `_`).
170
+ *
171
+ * `alia` is excluded because `alia-` is the live internal-cost-centre
172
+ * namespace: `alia-production-chat` is a minted account and
173
+ * `alia-research` / `alia-voice` / `alia-evaluations` are pinned as legal
174
+ * slugs in `__tests__/username.test.ts` and `internalCostCenterSpecs.test.ts`.
175
+ * Segment-matching `alia` would refuse all four. The bare word `alia` is
176
+ * still refused — {@link RESERVED_USERNAMES} above catches it exactly.
177
+ */
178
+ const RESERVED_USERNAME_SEGMENTS = new Set([...exports.RESERVED_USERNAMES].filter((word) => word !== 'alia'));
179
+ /** The 400 / inline-validation copy for a handle that is only digits. */
180
+ exports.NUMERIC_USERNAME_MESSAGE = 'Username cannot be only numbers';
116
181
  /**
117
182
  * Alphanumeric runs joined by single separators, as a SOURCE string.
118
183
  *
@@ -149,7 +214,10 @@ exports.usernameSchema = zod_1.z
149
214
  .trim()
150
215
  .min(exports.USERNAME_MIN_LENGTH, exports.USERNAME_INVALID_MESSAGE)
151
216
  .max(exports.USERNAME_MAX_LENGTH, exports.USERNAME_INVALID_MESSAGE)
152
- .regex(USERNAME_PATTERN, exports.USERNAME_INVALID_MESSAGE);
217
+ .regex(USERNAME_PATTERN, exports.USERNAME_INVALID_MESSAGE)
218
+ .refine((username) => !/^[0-9]+$/.test(username), exports.NUMERIC_USERNAME_MESSAGE)
219
+ .refine((username) => !exports.RESERVED_USERNAMES.has(username.toLowerCase()), exports.RESERVED_USERNAME_MESSAGE)
220
+ .refine((username) => !username.split(/[-_]/).some((segment) => RESERVED_USERNAME_SEGMENTS.has(segment.toLowerCase())), exports.RESERVED_USERNAME_MESSAGE);
153
221
  /**
154
222
  * Whether a candidate handle is storable — the boolean form, for input surfaces
155
223
  * that show a message as somebody types rather than throwing.
@@ -0,0 +1,181 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.webIdentityEnvelopeEstablishSchema = exports.webauthnAssertionResponseSchema = exports.webIdentityEnvelopePutSchema = exports.webIdentityEnvelopeActionSchema = exports.webIdentityEnvelopeProofFieldsSchema = exports.webIdentityEnvelopeResponseSchema = exports.webIdentityHolderSchema = exports.webIdentityEnvelopeUploadSchema = exports.webIdentityEnvelopeSchema = exports.webIdentityWrapSchema = exports.webauthnRpIdSchema = exports.webauthnCredentialIdSchema = exports.webIdentityPublicKeySchema = exports.WEB_IDENTITY_SECRET_KINDS = exports.WEB_IDENTITY_ENVELOPE_VERSION = void 0;
4
+ /**
5
+ * Web identity holder contract — the sealed envelope that lets a browser hold an
6
+ * account's self-custody root without Oxy ever being able to use it (ADR 0024).
7
+ *
8
+ * A root is a BIP-39 phrase (12–24 words) whose seed's first 32 bytes are the
9
+ * secp256k1 key — exactly the Commons derivation — or, for a few imported
10
+ * identities, a raw private key that never had a phrase. On the web it travels as:
11
+ *
12
+ * secret ── XChaCha20-Poly1305 under a random DEK ──▶ sealedSecret
13
+ * DEK ── XChaCha20-Poly1305 under KEK_i ──▶ wraps[i]
14
+ * KEK_i = HKDF(PRF output of passkey i)
15
+ *
16
+ * The server stores the envelope and can open NONE of it: the PRF output never
17
+ * leaves the user's authenticator, and the secret is never uploaded. The AEAD
18
+ * associated data binds the secret to the root's public key and kind, and each
19
+ * wrap to its credential and RP ID, so a re-labelled or transplanted envelope
20
+ * fails to open instead of decrypting into the wrong identity.
21
+ *
22
+ * Platform-agnostic — zod only, ESM-safe (no `require()`).
23
+ */
24
+ const zod_1 = require("zod");
25
+ const identityProof_1 = require("./identityProof");
26
+ /** The envelope scheme. A scheme change is a new literal, never a mutation. */
27
+ exports.WEB_IDENTITY_ENVELOPE_VERSION = 2;
28
+ /**
29
+ * What an envelope seals. A raw-key identity stays a raw-key identity:
30
+ * nothing ever derives or displays a phrase for it.
31
+ */
32
+ exports.WEB_IDENTITY_SECRET_KINDS = ['mnemonic-entropy', 'raw-private-key'];
33
+ const hex = (bytes, label) => zod_1.z
34
+ .string()
35
+ .trim()
36
+ .regex(new RegExp(`^[0-9a-fA-F]{${bytes * 2}}$`), `${label} must be ${bytes * 2} hex characters`);
37
+ /**
38
+ * The identity's secp256k1 public key in Oxy's canonical form: uncompressed SEC1
39
+ * (`04` + 64 bytes), lowercase hex — what `KeyManager.derivePublicKey` produces
40
+ * and `users.public_key` stores.
41
+ */
42
+ exports.webIdentityPublicKeySchema = zod_1.z
43
+ .string()
44
+ .trim()
45
+ .regex(/^04[0-9a-f]{128}$/, 'publicKey must be an uncompressed, lowercase secp256k1 key (130 hex characters)');
46
+ /** A WebAuthn credential id, base64url as the browser reports it. */
47
+ exports.webauthnCredentialIdSchema = zod_1.z
48
+ .string()
49
+ .trim()
50
+ .min(16)
51
+ .max(1024)
52
+ .regex(/^[A-Za-z0-9_-]+$/, 'credentialId must be base64url');
53
+ /** A WebAuthn RP ID: a bare registrable host name, lowercase. */
54
+ exports.webauthnRpIdSchema = zod_1.z
55
+ .string()
56
+ .trim()
57
+ .min(1)
58
+ .max(253)
59
+ .regex(/^(localhost|[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+)$/, 'rpId must be a lowercase host name');
60
+ /** One passkey's wrap of the envelope's data key. */
61
+ exports.webIdentityWrapSchema = zod_1.z.object({
62
+ credentialId: exports.webauthnCredentialIdSchema,
63
+ /** 24-byte XChaCha20-Poly1305 nonce. */
64
+ nonce: hex(24, 'nonce'),
65
+ /** The 32-byte DEK sealed under this passkey's KEK, with the 16-byte tag appended (48 bytes). */
66
+ wrappedKey: hex(48, 'wrappedKey'),
67
+ createdAt: zod_1.z.string().datetime(),
68
+ /** The RP ID the passkey was created under, asserted explicitly by every later ceremony (ADR 0024 D2). */
69
+ rpId: exports.webauthnRpIdSchema,
70
+ /**
71
+ * When this passkey's PRF output was shown to open the envelope. A wrap is a
72
+ * root HOLDER only once this is set; a login passkey never is by default.
73
+ */
74
+ verifiedAt: zod_1.z.string().datetime().optional(),
75
+ });
76
+ /**
77
+ * The sealed identity as it is stored (server copy and local copy alike).
78
+ *
79
+ * `wraps` holds one entry per passkey able to open it; at least one, and a
80
+ * bounded number so an envelope cannot grow without limit.
81
+ */
82
+ exports.webIdentityEnvelopeSchema = zod_1.z
83
+ .object({
84
+ version: zod_1.z.literal(exports.WEB_IDENTITY_ENVELOPE_VERSION),
85
+ algorithm: zod_1.z.literal('xchacha20poly1305'),
86
+ publicKey: exports.webIdentityPublicKeySchema,
87
+ secretKind: zod_1.z.enum(exports.WEB_IDENTITY_SECRET_KINDS),
88
+ /** 24-byte nonce of the secret seal. */
89
+ secretNonce: hex(24, 'secretNonce'),
90
+ /**
91
+ * The sealed secret, tag appended: 16/20/24/28/32 bytes of BIP-39 entropy
92
+ * (12–24 words) or a 32-byte private key, plus 16.
93
+ */
94
+ sealedSecret: zod_1.z
95
+ .string()
96
+ .trim()
97
+ .regex(/^(?:[0-9a-fA-F]{64}|[0-9a-fA-F]{72}|[0-9a-fA-F]{80}|[0-9a-fA-F]{88}|[0-9a-fA-F]{96})$/, 'sealedSecret has an unsupported length'),
98
+ wraps: zod_1.z.array(exports.webIdentityWrapSchema).min(1).max(10),
99
+ })
100
+ .refine((envelope) => envelope.secretKind === 'mnemonic-entropy' || envelope.sealedSecret.length === 96, {
101
+ message: 'a raw private key seals to 48 bytes',
102
+ path: ['sealedSecret'],
103
+ });
104
+ /**
105
+ * `PUT /identity/web-envelope` — store or replace the caller's envelope.
106
+ *
107
+ * Refused unless `envelope.publicKey` is the identity key already linked to the
108
+ * account: an envelope can only ever carry the account's own identity.
109
+ */
110
+ exports.webIdentityEnvelopeUploadSchema = zod_1.z.object({
111
+ envelope: exports.webIdentityEnvelopeSchema,
112
+ });
113
+ /** A root holder as the status read reports it — metadata only, nothing that opens anything. */
114
+ exports.webIdentityHolderSchema = zod_1.z.object({
115
+ credentialId: exports.webauthnCredentialIdSchema,
116
+ rpId: exports.webauthnRpIdSchema,
117
+ verifiedAt: zod_1.z.string().datetime().nullable(),
118
+ createdAt: zod_1.z.string().datetime(),
119
+ });
120
+ /**
121
+ * `GET /identity/web-envelope` — the caller's envelope and the readiness facts
122
+ * ADR 0024 D5 keeps separate. A client decides what to show from these fields
123
+ * WITHOUT decrypting anything.
124
+ */
125
+ exports.webIdentityEnvelopeResponseSchema = zod_1.z.object({
126
+ envelope: exports.webIdentityEnvelopeSchema.nullable(),
127
+ /** The revision a write must name as `expectedRevision`; `0` when there is no envelope. */
128
+ revision: zod_1.z.number().int().nonnegative(),
129
+ /** Whether the account has a linked root at all (it may live only in Commons). */
130
+ rootLinked: zod_1.z.boolean(),
131
+ /** The web wraps, as metadata. */
132
+ holders: zod_1.z.array(exports.webIdentityHolderSchema),
133
+ /** When the owner confirmed the recovery material is written down, or `null`. */
134
+ phraseConfirmedAt: zod_1.z.string().datetime().nullable(),
135
+ /** When the recovery material was shown to re-derive this root, or `null`. */
136
+ recoveryVerifiedAt: zod_1.z.string().datetime().nullable(),
137
+ updatedAt: zod_1.z.string().datetime().nullable(),
138
+ });
139
+ /** A root proof, plus the envelope revision the write expects to replace. */
140
+ exports.webIdentityEnvelopeProofFieldsSchema = zod_1.z.object({
141
+ proof: identityProof_1.identityProofSchema,
142
+ expectedRevision: zod_1.z.number().int().nonnegative(),
143
+ });
144
+ /**
145
+ * `POST /identity/web-envelope/phrase-confirmed`, `/recovery-verified` and
146
+ * `DELETE /identity/web-envelope` prove control of the root, not just a bearer.
147
+ */
148
+ exports.webIdentityEnvelopeActionSchema = exports.webIdentityEnvelopeProofFieldsSchema.strict();
149
+ /** `PUT /identity/web-envelope` body. */
150
+ exports.webIdentityEnvelopePutSchema = exports.webIdentityEnvelopeUploadSchema.extend(exports.webIdentityEnvelopeProofFieldsSchema.shape).strict();
151
+ /**
152
+ * A WebAuthn assertion by one of the account's EXISTING passkeys whose
153
+ * `clientDataJSON.challenge` is the proof challenge — the fresh use of the
154
+ * existing factor a keyless account needs before its first root is linked.
155
+ */
156
+ exports.webauthnAssertionResponseSchema = zod_1.z
157
+ .object({
158
+ id: exports.webauthnCredentialIdSchema,
159
+ rawId: zod_1.z.string().min(1).max(2048),
160
+ type: zod_1.z.literal('public-key'),
161
+ response: zod_1.z
162
+ .object({
163
+ clientDataJSON: zod_1.z.string().min(1).max(8192),
164
+ authenticatorData: zod_1.z.string().min(1).max(8192),
165
+ signature: zod_1.z.string().min(1).max(2048),
166
+ userHandle: zod_1.z.string().max(2048).optional(),
167
+ })
168
+ .passthrough(),
169
+ clientExtensionResults: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()).optional(),
170
+ authenticatorAttachment: zod_1.z.string().optional(),
171
+ })
172
+ .passthrough();
173
+ /**
174
+ * `POST /identity/web-envelope/establish` body — an account's FIRST root, linked
175
+ * and stored with its envelope in ONE transaction: one root proof
176
+ * (`web_envelope_establish`, digest of the envelope) plus a fresh `assertion` by
177
+ * an existing passkey over the same challenge.
178
+ */
179
+ exports.webIdentityEnvelopeEstablishSchema = exports.webIdentityEnvelopeUploadSchema
180
+ .extend({ proof: identityProof_1.identityProofSchema, assertion: exports.webauthnAssertionResponseSchema })
181
+ .strict();
@@ -14,6 +14,8 @@
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.webauthnLoginVerifyRequestSchema = exports.webauthnRegisterVerifyRequestSchema = exports.webauthnLoginOptionsRequestSchema = exports.webauthnRegisterOptionsRequestSchema = void 0;
16
16
  const zod_1 = require("zod");
17
+ const identityProof_1 = require("./identityProof");
18
+ const webIdentityCarrier_1 = require("./webIdentityCarrier");
17
19
  /**
18
20
  * Device-session options shared by every first-party sign-in body
19
21
  * (`deviceName`/`deviceFingerprint`). Mirrors what
@@ -59,6 +61,18 @@ exports.webauthnLoginOptionsRequestSchema = zod_1.z.object({
59
61
  */
60
62
  exports.webauthnRegisterVerifyRequestSchema = zod_1.z.object({
61
63
  username: zod_1.z.string().trim().min(1).max(60).optional(),
64
+ /**
65
+ * Sign-up only (ADR 0024 D4): the account's root, created on the holder BEFORE
66
+ * this request — sealed under the passkey being registered — and a root proof
67
+ * (`enroll_identity`) whose challenge is the registration challenge. The
68
+ * account, passkey, root and envelope are then created in one transaction.
69
+ */
70
+ identity: zod_1.z
71
+ .object({
72
+ envelope: webIdentityCarrier_1.webIdentityEnvelopeSchema,
73
+ proof: identityProof_1.identityProofSchema,
74
+ })
75
+ .optional(),
62
76
  ...deviceSessionEnvelope,
63
77
  });
64
78
  /**