@oxy.so/contracts 1.2.0 → 1.4.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 (62) hide show
  1. package/LICENSE +675 -201
  2. package/NOTICE +7 -2
  3. package/dist/cjs/.tsbuildinfo +1 -1
  4. package/dist/cjs/identity.js +3 -2
  5. package/dist/cjs/identityMove.js +111 -40
  6. package/dist/cjs/identityProof.js +164 -0
  7. package/dist/cjs/identityRecovery.js +51 -0
  8. package/dist/cjs/index.js +52 -21
  9. package/dist/cjs/inference/catalogue.js +23 -1
  10. package/dist/cjs/inference/identifiers.js +3 -1
  11. package/dist/cjs/inference/providerConnection.js +1 -1
  12. package/dist/cjs/inference/request.js +27 -1
  13. package/dist/cjs/inference/streamEvents.js +24 -2
  14. package/dist/cjs/inference/version.js +1 -1
  15. package/dist/cjs/username.js +70 -2
  16. package/dist/cjs/webIdentityCarrier.js +112 -53
  17. package/dist/cjs/webauthn.js +14 -0
  18. package/dist/esm/.tsbuildinfo +1 -1
  19. package/dist/esm/identity.js +3 -2
  20. package/dist/esm/identityMove.js +105 -39
  21. package/dist/esm/identityProof.js +159 -0
  22. package/dist/esm/identityRecovery.js +48 -0
  23. package/dist/esm/index.js +12 -9
  24. package/dist/esm/inference/catalogue.js +22 -0
  25. package/dist/esm/inference/identifiers.js +2 -0
  26. package/dist/esm/inference/providerConnection.js +2 -2
  27. package/dist/esm/inference/request.js +27 -1
  28. package/dist/esm/inference/streamEvents.js +24 -2
  29. package/dist/esm/inference/version.js +1 -1
  30. package/dist/esm/username.js +69 -1
  31. package/dist/esm/webIdentityCarrier.js +111 -52
  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 +7 -7
  39. package/dist/types/identity.d.ts +3 -2
  40. package/dist/types/identityMove.d.ts +119 -43
  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 +15 -13
  44. package/dist/types/inference/catalogue.d.ts +51 -0
  45. package/dist/types/inference/identifiers.d.ts +2 -0
  46. package/dist/types/inference/modelDocumentation.d.ts +12 -0
  47. package/dist/types/inference/providerConnection.d.ts +40 -40
  48. package/dist/types/inference/request.d.ts +119 -36
  49. package/dist/types/inference/streamEvents.d.ts +57 -1
  50. package/dist/types/inference/version.d.ts +1 -1
  51. package/dist/types/keyRotation.d.ts +2 -2
  52. package/dist/types/oauth.d.ts +30 -30
  53. package/dist/types/sessionStatus.d.ts +6 -6
  54. package/dist/types/transparency.d.ts +10 -10
  55. package/dist/types/userResponse.d.ts +18 -18
  56. package/dist/types/username.d.ts +25 -2
  57. package/dist/types/webIdentityCarrier.d.ts +767 -159
  58. package/dist/types/webauthn.d.ts +208 -0
  59. package/package.json +2 -2
  60. package/dist/cjs/devicePairing.js +0 -138
  61. package/dist/esm/devicePairing.js +0 -135
  62. package/dist/types/devicePairing.d.ts +0 -130
@@ -14,8 +14,9 @@
14
14
  * - DID = `did:web:oxy.so:u:<userId>` — anchored on the stable account id, NOT
15
15
  * the keypair. The keypair is a *verification method* that maps 1:1 to the
16
16
  * existing `authMethods[]`. Custodial (password-only) users get a DID
17
- * controlled solely by Oxy (`OXY_DID`); creating a Commons key upgrades them
18
- * to self-sovereign (`controller = [userDid, OXY_DID]`); fully reversible.
17
+ * controlled solely by Oxy (`OXY_DID`); linking a root makes them
18
+ * self-sovereign (`controller = [userDid]`, ADR 0024 D9). A root is never
19
+ * unlinked back into a custodial account.
19
20
  * - Verification methods use the secp256k1 `EcdsaSecp256k1VerificationKey2019`
20
21
  * type with `publicKeyHex` for now (a `Multikey`/`publicKeyMultibase` form may
21
22
  * be added later — see the plan's open risks).
@@ -1,34 +1,38 @@
1
1
  /**
2
- * Identity move contract — take a web identity INTO Commons (a MOVE, not a copy).
2
+ * Identity move contract — give a web root to Commons (add it, or keep it only in
3
+ * Commons; ADR 0024 D6).
3
4
  *
4
- * Design: `docs/superpowers/specs/2026-09-15-one-identity-two-carriers-design.md` §5.
5
+ * The web (holding the root) shows a QR carrying only a move id; Commons scans.
6
+ * The relay in between must not be able to take the root, so:
5
7
  *
6
- * Flow (the web — the OLD carrier — shows the QR; Commons — the NEW carrier —
7
- * scans it):
8
- * 1. `id.oxy.so` generates an ephemeral secp256k1 pair and calls
9
- * `POST /identity/move { initiatorEphemeralPublicKey }` (bearer) →
10
- * `{ moveId, expiresAt }`. The QR carries `moveId` only.
11
- * 2. Commons scans, generates its own ephemeral pair, and calls
12
- * `POST /identity/move/:moveId/join { responderEphemeralPublicKey }` (no
13
- * bearer — Commons has no identity yet).
14
- * 3. Both sides derive the same 6-digit SAS from the move id and BOTH ephemeral
15
- * keys and show it. The person confirms on the web that they match. A relay
16
- * that substituted either key produces two different codes.
17
- * 4. The web seals the BIP-39 entropy under
18
- * `HKDF(ECDH(initiatorEph, responderEph), moveId, 'oxy-identity-move-v1')` and
19
- * calls `POST /identity/move/:moveId/seal` (bearer + identity-key proof).
20
- * 5. Commons decrypts, imports the identity, and posts a RECEIPT: a signature by
21
- * the identity key over `{ action:'identity_move_received', moveId, timestamp }`
22
- * (`POST /identity/move/:moveId/receipt`). The server checks it against the
23
- * account's key; the WEB verifies it again locally before destroying its copy,
24
- * so not even the server can fake a completed move.
8
+ * 1. `POST /identity/move { initiatorCommitment }` — the web publishes only
9
+ * `H(initiatorKey, nonce)`, never the key.
10
+ * 2. `POST /identity/move/:moveId/join { responderEphemeralPublicKey }` —
11
+ * Commons, having read the commitment first, joins with its own key.
12
+ * 3. `POST /identity/move/:moveId/reveal` — only then does the web reveal its
13
+ * key and nonce; Commons checks them against the commitment it read.
14
+ * 4. Both screens show a 6-digit code over `(moveId, both keys, commitment)`;
15
+ * the person confirms on the web that they match.
16
+ * 5. `POST /identity/move/:moveId/seal` — the web seals the phrase entropy under
17
+ * `HKDF(ECDH(both keys), moveId)`, authorized by a one-use root proof over the
18
+ * exact sealed bytes.
19
+ * 6. `POST /identity/move/:moveId/receipt` — Commons stores the root, reads it
20
+ * back from its keychain, and signs a receipt over the move, the root, both
21
+ * keys and the digest of the ciphertext it opened. The server verifies it; the
22
+ * web verifies it again from what IT sealed before removing anything.
25
23
  *
26
- * The server holds two ephemeral public keys, an opaque ciphertext, and a
27
- * receipt. It never holds anything that decrypts the ciphertext.
24
+ * Why the commitment: without it the relay sees both keys before committing to
25
+ * anything and can grind a substituted key until the two codes agree (~10⁶
26
+ * tries, seconds). With it, the relay must fix the key it shows Commons before
27
+ * learning Commons' key, and the key it shows the web before the web reveals.
28
+ *
29
+ * The server holds a commitment, two ephemeral public keys, an opaque ciphertext
30
+ * and a receipt — nothing that decrypts the ciphertext.
28
31
  *
29
32
  * Platform-agnostic — zod only, ESM-safe (no `require()`).
30
33
  */
31
34
  import { z } from 'zod';
35
+ import { canonicalJson, identityProofSchema } from './identityProof.js';
32
36
  /** A move lives this long: one interactive handoff. */
33
37
  export const IDENTITY_MOVE_TTL_MS = 5 * 60 * 1000;
34
38
  /** 128-bit move id, lowercase hex. */
@@ -44,9 +48,13 @@ export const identityMoveEphemeralKeySchema = z
44
48
  export const IDENTITY_MOVE_STATUSES = ['pending', 'joined', 'sealed', 'completed', 'cancelled', 'expired'];
45
49
  /** The QR payload Commons scans. Carries the move id only. */
46
50
  export const IDENTITY_MOVE_QR_PREFIX = 'oxycommons://move?id=';
47
- export const identityMoveCreateRequestSchema = z.object({
48
- initiatorEphemeralPublicKey: identityMoveEphemeralKeySchema,
49
- });
51
+ const hex64 = (label) => z.string().trim().regex(/^[0-9a-f]{64}$/, `${label} must be 64 lowercase hex characters`);
52
+ export const identityMoveCreateRequestSchema = z
53
+ .object({
54
+ /** `H(initiator ephemeral key, nonce)` — the key itself is revealed only after Commons joins. */
55
+ initiatorCommitment: hex64('initiatorCommitment'),
56
+ })
57
+ .strict();
50
58
  export const identityMoveCreateResponseSchema = z.object({
51
59
  moveId: identityMoveIdSchema,
52
60
  expiresAt: z.string().datetime(),
@@ -54,29 +62,87 @@ export const identityMoveCreateResponseSchema = z.object({
54
62
  export const identityMoveJoinRequestSchema = z.object({
55
63
  responderEphemeralPublicKey: identityMoveEphemeralKeySchema,
56
64
  });
57
- export const identityMoveSealRequestSchema = z.object({
65
+ /** After Commons joined: the committed key and its nonce. */
66
+ export const identityMoveRevealRequestSchema = z
67
+ .object({
68
+ initiatorEphemeralPublicKey: identityMoveEphemeralKeySchema,
69
+ commitmentNonce: hex64('commitmentNonce'),
70
+ })
71
+ .strict();
72
+ export const identityMoveSealRequestSchema = z
73
+ .object({
58
74
  /** 24-byte XChaCha20-Poly1305 nonce, hex. */
59
75
  nonce: z.string().trim().regex(/^[0-9a-f]{48}$/, 'nonce must be 48 lowercase hex characters'),
60
- /** The 16-byte entropy, tag appended (32 bytes), hex. */
61
- ciphertext: z.string().trim().regex(/^[0-9a-f]{64}$/, 'ciphertext must be 64 lowercase hex characters'),
62
- /** Identity-key proof over `{ action:'identity_move_seal', moveId, timestamp }`. */
63
- signature: z.string().trim().min(1).max(512),
64
- timestamp: z.number().int().positive(),
65
- });
66
- export const identityMoveReceiptRequestSchema = z.object({
67
- /** Identity-key signature over `{ action:'identity_move_received', moveId, timestamp }`. */
76
+ /** The 16–32-byte phrase entropy (12–24 words), tag appended, hex. */
77
+ ciphertext: z
78
+ .string()
79
+ .trim()
80
+ .regex(/^(?:[0-9a-f]{64}|[0-9a-f]{72}|[0-9a-f]{80}|[0-9a-f]{88}|[0-9a-f]{96})$/, 'ciphertext has an unsupported length'),
81
+ /**
82
+ * Root proof (`identity_move_seal`), payload `{ moveId, nonce, ciphertext }`,
83
+ * over a one-use challenge from `POST /identity/proof-challenge`.
84
+ */
85
+ proof: identityProofSchema,
86
+ })
87
+ .strict();
88
+ export const identityMoveReceiptRequestSchema = z
89
+ .object({
90
+ /** Root signature over `buildMoveReceiptMessage(...)`. */
68
91
  signature: z.string().trim().min(1).max(512),
69
- timestamp: z.number().int().positive(),
70
- });
92
+ })
93
+ .strict();
71
94
  export const identityMoveStateSchema = z.object({
72
95
  moveId: identityMoveIdSchema,
73
96
  status: z.enum(IDENTITY_MOVE_STATUSES),
97
+ initiatorCommitment: z.string(),
98
+ initiatorCommitmentNonce: z.string().nullable(),
74
99
  publicKey: z.string().regex(/^04[0-9a-f]{128}$/),
75
- initiatorEphemeralPublicKey: identityMoveEphemeralKeySchema,
100
+ initiatorEphemeralPublicKey: identityMoveEphemeralKeySchema.nullable(),
76
101
  responderEphemeralPublicKey: identityMoveEphemeralKeySchema.nullable(),
77
102
  nonce: z.string().nullable(),
78
103
  ciphertext: z.string().nullable(),
79
104
  receiptSignature: z.string().nullable(),
80
- receiptTimestamp: z.number().int().nullable(),
81
105
  expiresAt: z.string().datetime(),
82
106
  });
107
+ /**
108
+ * The initiator's commitment input, `canonicalJson({ v, purpose, key, nonce })`,
109
+ * which the caller hashes with SHA-256.
110
+ */
111
+ export function buildMoveCommitmentInput(initiatorEphemeralPublicKey, nonce) {
112
+ return canonicalJson({
113
+ v: 2,
114
+ purpose: 'oxy-identity-move-initiator-commitment',
115
+ initiatorEphemeralPublicKey: initiatorEphemeralPublicKey.toLowerCase(),
116
+ nonce: nonce.toLowerCase(),
117
+ });
118
+ }
119
+ /** The bytes both sides hash for the 6-digit code. */
120
+ export function buildMoveSasInput(input) {
121
+ return canonicalJson({
122
+ v: 'oxy-identity-transfer-sas-v2',
123
+ moveId: input.moveId.toLowerCase(),
124
+ initiator: input.initiatorEphemeralPublicKey.toLowerCase(),
125
+ responder: input.responderEphemeralPublicKey.toLowerCase(),
126
+ commitment: input.initiatorCommitment.toLowerCase(),
127
+ });
128
+ }
129
+ /** The ciphertext digest input the receipt binds: `canonicalJson({ nonce, ciphertext })`. */
130
+ export function buildMoveCiphertextDigestInput(sealed) {
131
+ return canonicalJson({ nonce: sealed.nonce.toLowerCase(), ciphertext: sealed.ciphertext.toLowerCase() });
132
+ }
133
+ /** The payload a seal proof digests. */
134
+ export function buildMoveSealPayload(moveId, sealed) {
135
+ return { moveId: moveId.toLowerCase(), nonce: sealed.nonce.toLowerCase(), ciphertext: sealed.ciphertext.toLowerCase() };
136
+ }
137
+ /** The exact bytes a receipt signs. */
138
+ export function buildMoveReceiptMessage(input) {
139
+ return canonicalJson({
140
+ v: 2,
141
+ domain: 'oxy-identity-move-receipt',
142
+ moveId: input.moveId.toLowerCase(),
143
+ rootPublicKey: input.rootPublicKey.toLowerCase(),
144
+ initiator: input.initiatorEphemeralPublicKey.toLowerCase(),
145
+ responder: input.responderEphemeralPublicKey.toLowerCase(),
146
+ ciphertextDigest: input.ciphertextDigest.toLowerCase(),
147
+ });
148
+ }
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Identity proof contract — the ONE signed format for operations on a personal root.
3
+ *
4
+ * ADR 0024 D7. A proof is a signature by the root key over the canonical bytes
5
+ * of {@link IdentityProofClaims}. Every field the verifier cares about is IN the
6
+ * signed bytes, so a signature cannot be moved to another operation, account,
7
+ * root, payload, revision or audience, and the one-use `challenge` means it
8
+ * cannot be replayed either (a timestamp window is not replay protection).
9
+ *
10
+ * Both sides build the bytes with {@link buildIdentityProofMessage} and hash
11
+ * payloads with {@link canonicalJson}; neither writes its own JSON template, so
12
+ * the client and the verifier cannot drift apart.
13
+ *
14
+ * Platform-agnostic — zod only, ESM-safe (no `require()`), no hashing here (the
15
+ * caller hashes `canonicalJson(payload)` with SHA-256 using its platform's
16
+ * primitive and passes the hex digest in).
17
+ */
18
+ import { z } from 'zod';
19
+ export const IDENTITY_PROOF_VERSION = 2;
20
+ export const IDENTITY_PROOF_DOMAIN = 'oxy-identity-proof';
21
+ /** The audience every API-verified identity proof names. */
22
+ export const IDENTITY_PROOF_AUDIENCE = 'oxy-api/identity';
23
+ /** How long a proof challenge lives: one interactive ceremony. */
24
+ export const IDENTITY_PROOF_CHALLENGE_TTL_MS = 5 * 60 * 1000;
25
+ /**
26
+ * Everything a root proof may authorize. A challenge is minted for exactly one
27
+ * action and spent only by a proof for that action.
28
+ */
29
+ export const IDENTITY_PROOF_ACTIONS = {
30
+ /** A keyless account's FIRST root, stored with its web envelope. */
31
+ establish: 'web_envelope_establish',
32
+ /** Replace the web envelope (add or remove a wrap, re-seal). */
33
+ put: 'web_envelope_put',
34
+ /** Record that the recovery material is written down. */
35
+ phraseConfirmed: 'web_envelope_phrase_confirmed',
36
+ /** Record that the recovery material re-derived the root. */
37
+ recoveryVerified: 'web_envelope_recovery_verified',
38
+ /** Remove the web holder. */
39
+ delete: 'web_envelope_delete',
40
+ /** Link a keyless account's first root without a web envelope (`POST /auth/link`). */
41
+ link: 'link_identity',
42
+ /** Create a personal account together with its root (passkey sign-up). */
43
+ enroll: 'enroll_identity',
44
+ /** Prove the root to start signed-out recovery. */
45
+ recoverStart: 'recover_account_start',
46
+ /** Bind the new passkey and envelope when completing signed-out recovery. */
47
+ recoverComplete: 'recover_account_complete',
48
+ /** Seal the root for the Commons device that joined a move (payload: move id + sealed bytes). */
49
+ moveSeal: 'identity_move_seal',
50
+ };
51
+ export const IDENTITY_PROOF_ACTION_VALUES = Object.values(IDENTITY_PROOF_ACTIONS);
52
+ const HEX_DIGEST = /^[0-9a-f]{64}$/;
53
+ const ROOT_KEY = /^04[0-9a-f]{128}$/;
54
+ const CHALLENGE = /^[0-9a-f]{64}$/;
55
+ /**
56
+ * Canonical JSON: object keys sorted by UTF-16 code unit, no whitespace,
57
+ * `undefined` members omitted, arrays in order. Numbers must be finite. This is
58
+ * the ONLY serializer for anything digested into a proof.
59
+ */
60
+ export function canonicalJson(value) {
61
+ if (value === null)
62
+ return 'null';
63
+ switch (typeof value) {
64
+ case 'string':
65
+ case 'boolean':
66
+ return JSON.stringify(value);
67
+ case 'number':
68
+ if (!Number.isFinite(value))
69
+ throw new Error('canonicalJson: non-finite number');
70
+ return JSON.stringify(value);
71
+ case 'object': {
72
+ if (Array.isArray(value)) {
73
+ return `[${value.map((entry) => (entry === undefined ? 'null' : canonicalJson(entry))).join(',')}]`;
74
+ }
75
+ const record = value;
76
+ const keys = Object.keys(record)
77
+ .filter((key) => record[key] !== undefined)
78
+ .sort();
79
+ return `{${keys.map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`).join(',')}}`;
80
+ }
81
+ default:
82
+ throw new Error(`canonicalJson: unsupported ${typeof value}`);
83
+ }
84
+ }
85
+ /**
86
+ * The exact bytes a root signs. Throws on a malformed claim rather than signing
87
+ * (or verifying) something ambiguous.
88
+ */
89
+ export function buildIdentityProofMessage(claims) {
90
+ if (!IDENTITY_PROOF_ACTION_VALUES.includes(claims.action))
91
+ throw new Error('identity proof: unknown action');
92
+ if (!claims.subject || !claims.actor)
93
+ throw new Error('identity proof: subject and actor are required');
94
+ if (!ROOT_KEY.test(claims.rootPublicKey))
95
+ throw new Error('identity proof: rootPublicKey must be canonical');
96
+ if (claims.payloadDigest !== null && !HEX_DIGEST.test(claims.payloadDigest)) {
97
+ throw new Error('identity proof: payloadDigest must be a lowercase SHA-256 hex digest');
98
+ }
99
+ if (claims.expectedRevision !== null && (!Number.isSafeInteger(claims.expectedRevision) || claims.expectedRevision < 0)) {
100
+ throw new Error('identity proof: expectedRevision must be a non-negative integer');
101
+ }
102
+ if (!CHALLENGE.test(claims.challenge))
103
+ throw new Error('identity proof: challenge must be 64 lowercase hex characters');
104
+ if (!Number.isSafeInteger(claims.expiresAt) || claims.expiresAt <= 0)
105
+ throw new Error('identity proof: expiresAt must be unix milliseconds');
106
+ return canonicalJson({
107
+ v: IDENTITY_PROOF_VERSION,
108
+ domain: IDENTITY_PROOF_DOMAIN,
109
+ action: claims.action,
110
+ subject: claims.subject,
111
+ actor: claims.actor,
112
+ rootPublicKey: claims.rootPublicKey,
113
+ payloadDigest: claims.payloadDigest,
114
+ expectedRevision: claims.expectedRevision,
115
+ audience: claims.audience,
116
+ challenge: claims.challenge,
117
+ expiresAt: claims.expiresAt,
118
+ });
119
+ }
120
+ /** The proof as it travels: the signature plus the two claims the verifier cannot derive. */
121
+ export const identityProofSchema = z.object({
122
+ v: z.literal(IDENTITY_PROOF_VERSION),
123
+ challenge: z.string().trim().regex(CHALLENGE, 'challenge must be 64 lowercase hex characters'),
124
+ expiresAt: z.number().int().positive(),
125
+ signature: z.string().trim().min(1).max(512),
126
+ });
127
+ /** `POST /identity/proof-challenge` */
128
+ export const identityProofChallengeRequestSchema = z.object({
129
+ action: z.enum(IDENTITY_PROOF_ACTION_VALUES),
130
+ });
131
+ export const identityProofChallengeResponseSchema = z.object({
132
+ challenge: z.string().regex(CHALLENGE),
133
+ expiresAt: z.number().int().positive(),
134
+ audience: z.string().min(1),
135
+ });
136
+ /**
137
+ * Stable error codes the root routes answer with (`error.code` in the API error
138
+ * body). Clients map these through their localization, never the English message.
139
+ */
140
+ export const IDENTITY_ERROR_CODES = {
141
+ proofInvalid: 'IDENTITY_PROOF_INVALID',
142
+ revisionConflict: 'IDENTITY_ENVELOPE_REVISION_CONFLICT',
143
+ rootAlreadyLinked: 'IDENTITY_ROOT_ALREADY_LINKED',
144
+ rootLinkedElsewhere: 'IDENTITY_ROOT_LINKED_ELSEWHERE',
145
+ noRoot: 'IDENTITY_NO_ROOT',
146
+ freshFactorRequired: 'IDENTITY_FRESH_FACTOR_REQUIRED',
147
+ lastWebHolder: 'IDENTITY_LAST_WEB_HOLDER',
148
+ enrollmentRequired: 'IDENTITY_ENROLLMENT_REQUIRED',
149
+ enrollmentInvalid: 'IDENTITY_ENROLLMENT_INVALID',
150
+ notPersonal: 'IDENTITY_NOT_PERSONAL_ACCOUNT',
151
+ recoveryFailed: 'IDENTITY_RECOVERY_FAILED',
152
+ };
153
+ export const identityRootStatusSchema = z.object({
154
+ rootLinked: z.boolean(),
155
+ webHolder: z.object({ passkeys: z.number().int().nonnegative(), verifiedPasskeys: z.number().int().nonnegative() }).nullable(),
156
+ hasPhrase: z.boolean().nullable(),
157
+ phraseConfirmedAt: z.string().datetime().nullable(),
158
+ recoveryVerifiedAt: z.string().datetime().nullable(),
159
+ });
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Signed-out recovery contract (ADR 0024 D5) — get an EXISTING account back from
3
+ * its root alone, with no passkey, session or email.
4
+ *
5
+ * 1. `POST /identity/recovery/challenge` → a one-use challenge. It says nothing
6
+ * about any account.
7
+ * 2. The holder derives the root locally from recovery material (12/24 words or a
8
+ * raw key) and signs `recover_account_start` over that challenge:
9
+ * `POST /identity/recovery/start { publicKey, proof }`. Only a valid proof
10
+ * learns which account the root belongs to; the response carries passkey
11
+ * registration options for THAT account and a short-lived ticket.
12
+ * 3. The holder creates the passkey, seals the root under it (PRF), and signs
13
+ * `recover_account_complete` over the registration challenge and the envelope
14
+ * digest: `POST /identity/recovery/complete`. The passkey, the envelope and a
15
+ * session are created in one transaction.
16
+ *
17
+ * The recovery material and the root never leave the holder. Platform-agnostic —
18
+ * zod only, ESM-safe.
19
+ */
20
+ import { z } from 'zod';
21
+ import { identityProofSchema } from './identityProof.js';
22
+ import { webIdentityEnvelopeSchema, webIdentityPublicKeySchema } from './webIdentityCarrier.js';
23
+ /** A recovery attempt lives this long between steps. */
24
+ export const IDENTITY_RECOVERY_TTL_MS = 5 * 60 * 1000;
25
+ export const identityRecoveryChallengeResponseSchema = z.object({
26
+ challenge: z.string().regex(/^[0-9a-f]{64}$/),
27
+ expiresAt: z.number().int().positive(),
28
+ });
29
+ export const identityRecoveryStartRequestSchema = z
30
+ .object({
31
+ publicKey: webIdentityPublicKeySchema,
32
+ /** `recover_account_start`, subject `root:<publicKey>`, actor `anonymous`. */
33
+ proof: identityProofSchema,
34
+ })
35
+ .strict();
36
+ export const identityRecoveryCompleteRequestSchema = z
37
+ .object({
38
+ ticket: z.string().regex(/^[0-9a-f]{64}$/),
39
+ /** The browser `RegistrationResponseJSON`, verified by the API's WebAuthn library. */
40
+ response: z.record(z.string(), z.unknown()),
41
+ /** The root sealed under the new passkey — exactly one wrap, for that credential. */
42
+ envelope: webIdentityEnvelopeSchema,
43
+ /** `recover_account_complete`, subject = account id, actor `credential:<id>`, challenge = registration challenge (hex). */
44
+ proof: identityProofSchema,
45
+ deviceName: z.string().trim().max(100).optional(),
46
+ deviceFingerprint: z.string().trim().max(512).optional(),
47
+ })
48
+ .strict();
package/dist/esm/index.js CHANGED
@@ -10,7 +10,7 @@
10
10
  * expo, no `require()` in the ESM build.
11
11
  */
12
12
  export { ACCOUNT_KINDS, accountKindSchema, CHILD_ACCOUNT_KINDS, childAccountKindSchema, isAccountKind, isDelegatedActAsEligibleKind, isOperatorSwitchTargetKind, ACCOUNT_CATEGORY_IDS, ACCOUNT_CATEGORY_KINDS, accountCategoriesSchema, accountCategoryIdSchema, isSelectableAccountCategoryId, kindAcceptsAccountCategories, MAX_ACCOUNT_CATEGORIES, newlyAddedRetiredCategories, RETIRED_ACCOUNT_CATEGORY_IDS, SELECTABLE_ACCOUNT_CATEGORY_IDS, createAccountRequestSchema, } from './accountGraph.js';
13
- export { usernameSchema, usernameSchemaForAccountKind, isValidUsername, stripDisallowedUsernameCharacters, applyBotUsernameSuffix, USERNAME_MIN_LENGTH, USERNAME_MAX_LENGTH, USERNAME_INVALID_MESSAGE, BOT_USERNAME_INVALID_MESSAGE, } from './username.js';
13
+ export { usernameSchema, usernameSchemaForAccountKind, isValidUsername, stripDisallowedUsernameCharacters, applyBotUsernameSuffix, USERNAME_MIN_LENGTH, USERNAME_MAX_LENGTH, USERNAME_INVALID_MESSAGE, BOT_USERNAME_INVALID_MESSAGE, RESERVED_USERNAME_MESSAGE, NUMERIC_USERNAME_MESSAGE, } from './username.js';
14
14
  export {
15
15
  // Schemas
16
16
  userNameSchema, userRelationshipSchema, themePreferenceSchema, dateOfBirthSchema, userResponseSchema, userProfileUpdateSchema, currentUserResponseSchema, deviceLinkedSessionSchema, deviceLinkedSessionsResponseSchema,
@@ -82,10 +82,16 @@ export {
82
82
  backupLookupIdSchema, encryptedBackupEnvelopeSchema, backupUploadRequestSchema, backupStatusResponseSchema, } from './keyRecovery.js';
83
83
  export {
84
84
  // Schemas — web identity carrier (one identity, two carriers)
85
- WEB_IDENTITY_ENVELOPE_VERSION, webIdentityPublicKeySchema, webauthnCredentialIdSchema, webIdentityWrapSchema, webIdentityEnvelopeSchema, webIdentityEnvelopeUploadSchema, webIdentityEnvelopeResponseSchema, webIdentityEnvelopeProofSchema, webIdentityEnvelopePutSchema, webIdentityEnvelopeEstablishSchema, } from './webIdentityCarrier.js';
85
+ WEB_IDENTITY_ENVELOPE_VERSION, WEB_IDENTITY_SECRET_KINDS, webIdentityPublicKeySchema, webauthnCredentialIdSchema, webauthnRpIdSchema, webIdentityWrapSchema, webIdentityEnvelopeSchema, webIdentityEnvelopeUploadSchema, webIdentityHolderSchema, webIdentityEnvelopeResponseSchema, webIdentityEnvelopeProofFieldsSchema, webIdentityEnvelopeActionSchema, webIdentityEnvelopePutSchema, webauthnAssertionResponseSchema, webIdentityEnvelopeEstablishSchema, } from './webIdentityCarrier.js';
86
+ export {
87
+ // Identity proofs (ADR 0024 D7) — the one signed format for root operations
88
+ IDENTITY_PROOF_VERSION, IDENTITY_PROOF_DOMAIN, IDENTITY_PROOF_AUDIENCE, IDENTITY_PROOF_CHALLENGE_TTL_MS, IDENTITY_PROOF_ACTIONS, IDENTITY_PROOF_ACTION_VALUES, IDENTITY_ERROR_CODES, canonicalJson, buildIdentityProofMessage, identityProofSchema, identityProofChallengeRequestSchema, identityProofChallengeResponseSchema, identityRootStatusSchema, } from './identityProof.js';
89
+ export {
90
+ // Signed-out recovery (ADR 0024 D5)
91
+ IDENTITY_RECOVERY_TTL_MS, identityRecoveryChallengeResponseSchema, identityRecoveryStartRequestSchema, identityRecoveryCompleteRequestSchema, } from './identityRecovery.js';
86
92
  export {
87
93
  // Schemas — moving a web identity into Commons
88
- IDENTITY_MOVE_TTL_MS, IDENTITY_MOVE_STATUSES, IDENTITY_MOVE_QR_PREFIX, identityMoveIdSchema, identityMoveEphemeralKeySchema, identityMoveCreateRequestSchema, identityMoveCreateResponseSchema, identityMoveJoinRequestSchema, identityMoveSealRequestSchema, identityMoveReceiptRequestSchema, identityMoveStateSchema, } from './identityMove.js';
94
+ IDENTITY_MOVE_TTL_MS, IDENTITY_MOVE_STATUSES, IDENTITY_MOVE_QR_PREFIX, identityMoveRevealRequestSchema, buildMoveCommitmentInput, buildMoveSasInput, buildMoveSealPayload, buildMoveCiphertextDigestInput, buildMoveReceiptMessage, identityMoveIdSchema, identityMoveEphemeralKeySchema, identityMoveCreateRequestSchema, identityMoveCreateResponseSchema, identityMoveJoinRequestSchema, identityMoveSealRequestSchema, identityMoveReceiptRequestSchema, identityMoveStateSchema, } from './identityMove.js';
89
95
  export {
90
96
  // Shared primitives
91
97
  updatePlatformSchema, updateStatusSchema, updateAssetStatusSchema, sha256HexSchema, channelNameSchema, runtimeVersionSchema, rolloutPercentSchema,
@@ -101,9 +107,6 @@ export {
101
107
  // Schemas
102
108
  webauthnRegisterOptionsRequestSchema, webauthnLoginOptionsRequestSchema, webauthnRegisterVerifyRequestSchema, webauthnLoginVerifyRequestSchema, } from './webauthn.js';
103
109
  export {
104
- // Schemas
105
- devicePairingStatusSchema, deviceTransferInitRequestSchema, deviceTransferInitResponseSchema, deviceTransferInfoResponseSchema, deviceTransferApproveRequestSchema, deviceTransferApproveResponseSchema, deviceTransferDenyResponseSchema, } from './devicePairing.js';
106
- export {
107
110
  // Schemas — transparency log (checkpoints + inclusion proofs)
108
111
  transparencyCheckpointSignatureSchema, transparencyAnchorSchema, transparencyCheckpointSchema, transparencyInclusionProofSchema, transparencyCheckpointListSchema, } from './transparency.js';
109
112
  /* -------------------------------------------------------------------------- */
@@ -135,7 +138,7 @@ export {
135
138
  priceVersionStatusSchema, priceVersionSchema, priceSnapshotSchema, } from './inference/priceVersion.js';
136
139
  export {
137
140
  // The six distinct catalogue objects + the customer-safe projection.
138
- inferenceModalitySchema, modelCapabilitiesSchema, modelLicenseSchema, modelProvenanceSchema, inferenceDataPolicySchema, availabilityScopeSchema, commercialPermissionSchema, modelDeprecationSchema, modelEvaluationResultSchema, modelSafetyMetadataSchema, modelPublisherSchema, catalogueModelSchema, modelRevisionSchema, inferenceProviderSchema, modelDeploymentSchema, routingProfileCandidateSchema, routingProfileSchema, cataloguePublisherSummarySchema, catalogueServingProviderSummarySchema, modelCatalogueEntrySchema, } from './inference/catalogue.js';
141
+ inferenceModalitySchema, reasoningEffortSchema, modelCapabilitiesSchema, modelLicenseSchema, modelProvenanceSchema, inferenceDataPolicySchema, availabilityScopeSchema, commercialPermissionSchema, modelDeprecationSchema, modelEvaluationResultSchema, modelSafetyMetadataSchema, modelPublisherSchema, catalogueModelSchema, modelRevisionSchema, inferenceProviderSchema, modelDeploymentSchema, routingProfileCandidateSchema, routingProfileSchema, cataloguePublisherSummarySchema, catalogueServingProviderSummarySchema, modelCatalogueEntrySchema, } from './inference/catalogue.js';
139
142
  export {
140
143
  // Routing policy: every control, plus the refinement that rejects a policy
141
144
  // no route could ever satisfy.
@@ -152,10 +155,10 @@ export {
152
155
  modelDistributionMethodSchema, modelSystemicRiskTierSchema, trainingComputeFlopsSchema, SYSTEMIC_RISK_COMPUTE_THRESHOLD_FLOPS, modelDownstreamDocumentationSchema, modelGpaiDocumentationSchema, modelLineDeclarationSchema, modelReleaseIngestionRequestSchema, modelReleaseIngestionResultSchema, modelDocumentationSchema, } from './inference/modelDocumentation.js';
153
156
  export {
154
157
  // The normalized Oxy→data-plane request envelope.
155
- inferenceContentSourceSchema, inferenceContentPartSchema, inferenceToolCallSchema, inferenceMessageRoleSchema, inferenceMessageSchema, inferenceInputSchema, samplingParametersSchema, toolDefinitionSchema, toolChoiceSchema, responseFormatSchema, clientRequestMetadataSchema, inferenceRequestSchema, } from './inference/request.js';
158
+ inferenceContentSourceSchema, inferenceContentPartSchema, inferenceToolCallSchema, inferenceMessageRoleSchema, inferenceMessageSchema, inferenceInputSchema, samplingParametersSchema, inferenceReasoningSchema, toolDefinitionSchema, toolChoiceSchema, responseFormatSchema, clientRequestMetadataSchema, inferenceRequestSchema, inferenceSpeechParametersSchema, } from './inference/request.js';
156
159
  export {
157
160
  // Normalized SSE events.
158
- inferenceStreamStartEventSchema, inferenceStreamDeltaEventSchema, inferenceStreamToolCallEventSchema, inferenceStreamUsageEventSchema, inferenceRouteSwitchDetailSchema, inferenceRouteSwitchReasonSchema, inferenceStreamRouteSwitchEventSchema, inferenceStreamErrorEventSchema, inferenceFinishReasonSchema, inferenceStreamDoneEventSchema, inferenceStreamEventSchema, } from './inference/streamEvents.js';
161
+ inferenceStreamStartEventSchema, inferenceStreamDeltaEventSchema, inferenceAudioMediaTypeSchema, MAX_INFERENCE_AUDIO_BYTES, inferenceStreamAudioEventSchema, inferenceStreamToolCallEventSchema, inferenceStreamUsageEventSchema, inferenceRouteSwitchDetailSchema, inferenceRouteSwitchReasonSchema, inferenceStreamRouteSwitchEventSchema, inferenceStreamErrorEventSchema, inferenceFinishReasonSchema, inferenceStreamDoneEventSchema, inferenceStreamEventSchema, } from './inference/streamEvents.js';
159
162
  export {
160
163
  // Reserve → settle → refund.
161
164
  usageReservationRequestSchema, usageReservationStatusSchema, usageReservationSchema, inferenceRequestOutcomeSchema, normalizedUsageReportSchema, usageReceiptSchema, usageRefundSubjectSchema, usageRefundReasonSchema, usageRefundSchema, } from './inference/usage.js';
@@ -40,6 +40,15 @@ export const inferenceModalitySchema = z.enum([
40
40
  'video',
41
41
  'embedding',
42
42
  ]);
43
+ /**
44
+ * How hard a reasoning model is asked to think before it answers.
45
+ *
46
+ * A closed, provider-neutral vocabulary: Kaana translates each value into the
47
+ * serving provider's own control. A model advertises the subset it accepts in
48
+ * `modelCapabilitiesSchema.reasoningEfforts`, and Oxy refuses a request for an
49
+ * effort the resolved model does not advertise rather than dropping it.
50
+ */
51
+ export const reasoningEffortSchema = z.enum(['low', 'medium', 'high']);
43
52
  /**
44
53
  * What a model can do, in the terms a caller has to decide against before
45
54
  * sending a request: can it call tools, does it accept images, will it honour a
@@ -54,6 +63,13 @@ export const modelCapabilitiesSchema = z
54
63
  structuredOutput: z.boolean(),
55
64
  jsonMode: z.boolean(),
56
65
  reasoning: z.boolean(),
66
+ /**
67
+ * The reasoning efforts a request may name for this model, in the order
68
+ * low → high. Empty means the model takes no effort control at all (it may
69
+ * still reason: `reasoning` answers that). Added in contract set 3.1.0;
70
+ * absent on the wire from an older producer, it parses as `[]`.
71
+ */
72
+ reasoningEfforts: z.array(reasoningEffortSchema).default([]),
57
73
  streaming: z.boolean(),
58
74
  promptCaching: z.boolean(),
59
75
  maxContextTokens: z.number().int().positive().safe(),
@@ -469,6 +485,12 @@ export const modelCatalogueEntrySchema = z.object({
469
485
  provenance: modelProvenanceSchema,
470
486
  knowledgeCutoff: inferenceDateSchema.optional(),
471
487
  releasedOn: inferenceDateSchema.optional(),
488
+ /**
489
+ * When the upstream provider first published this model, as that provider
490
+ * reports it. Present only when a provider reported it; never an Oxy or Kaana
491
+ * observation time. Lets a picker order models newest first.
492
+ */
493
+ releasedAt: inferenceTimestampSchema.optional(),
472
494
  regions: z.array(inferenceRegionSchema).default([]),
473
495
  servingProviders: z.array(catalogueServingProviderSummarySchema).default([]),
474
496
  dataPolicy: inferenceDataPolicySchema,
@@ -126,6 +126,8 @@ export const inferenceHttpsUrlSchema = z
126
126
  export const sha256DigestSchema = z
127
127
  .string()
128
128
  .regex(/^sha256:[a-f0-9]{64}$/, 'digest must be sha256:<64 lowercase hex>');
129
+ /** Standard base64 in whole, padded 4-character groups. */
130
+ export const PADDED_BASE64_PATTERN = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
129
131
  /* -------------------------------------------------------------------------- */
130
132
  /* Catalogue references */
131
133
  /* -------------------------------------------------------------------------- */
@@ -24,7 +24,7 @@
24
24
  * Decided in: issue #972 workstream 10.
25
25
  */
26
26
  import { z } from 'zod';
27
- import { deploymentIdSchema, inferenceEnvironmentSchema, inferenceProviderSlugSchema, inferenceTimestampSchema, oxyAccountIdSchema, oxyApplicationIdSchema, } from './identifiers.js';
27
+ import { deploymentIdSchema, inferenceEnvironmentSchema, inferenceProviderSlugSchema, inferenceTimestampSchema, oxyAccountIdSchema, oxyApplicationIdSchema, PADDED_BASE64_PATTERN, } from './identifiers.js';
28
28
  /**
29
29
  * How widely a connection applies.
30
30
  *
@@ -111,7 +111,7 @@ const kaanaCredentialSecretBase64Schema = z
111
111
  .string()
112
112
  .min(1)
113
113
  .max(8192)
114
- .regex(/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/)
114
+ .regex(PADDED_BASE64_PATTERN)
115
115
  .refine(isVisibleASCIIProviderCredential, {
116
116
  message: 'a decoded provider credential is 1-4096 visible ASCII bytes',
117
117
  });
@@ -25,7 +25,7 @@
25
25
  */
26
26
  import { z } from "zod";
27
27
  import { inferenceAttributionSchema } from "./attribution.js";
28
- import { inferenceModalitySchema } from "./catalogue.js";
28
+ import { inferenceModalitySchema, reasoningEffortSchema } from "./catalogue.js";
29
29
  import { idempotencyKeySchema, inferenceTimestampSchema } from "./identifiers.js";
30
30
  import { authorizedRouteSchema, routingPolicyReferenceSchema, routingTargetSchema, } from "./routingPolicy.js";
31
31
  /* -------------------------------------------------------------------------- */
@@ -260,6 +260,16 @@ export const responseFormatSchema = z.discriminatedUnion("type", [
260
260
  })
261
261
  .strict(),
262
262
  ]);
263
+ /**
264
+ * The caller's reasoning control. Strict, like every leaf: a provider-specific
265
+ * knob (`budget_tokens`, `summary`) is refused rather than silently dropped.
266
+ *
267
+ * Oxy forwards it only after checking the resolved model advertises the effort
268
+ * in its catalogue `reasoningEfforts`; Kaana translates it per provider.
269
+ */
270
+ export const inferenceReasoningSchema = z
271
+ .object({ effort: reasoningEffortSchema })
272
+ .strict();
263
273
  /* -------------------------------------------------------------------------- */
264
274
  /* Client metadata */
265
275
  /* -------------------------------------------------------------------------- */
@@ -311,6 +321,12 @@ const modelLineOf = (reference) => {
311
321
  const at = reference.indexOf("@");
312
322
  return at === -1 ? reference : reference.slice(0, at);
313
323
  };
324
+ /** Parameters for text-to-speech, preserved in the signed request. */
325
+ export const inferenceSpeechParametersSchema = z.object({
326
+ voice: z.string().min(1).max(64),
327
+ responseFormat: z.enum(["mp3", "opus", "aac", "flac", "wav", "pcm"]),
328
+ speed: z.number().min(0.25).max(4).optional(),
329
+ }).strict();
314
330
  /**
315
331
  * The canonical internal request Oxy forwards to the data plane.
316
332
  *
@@ -330,6 +346,9 @@ export const inferenceRequestSchema = z
330
346
  stream: z.boolean(),
331
347
  maxOutputTokens: z.number().int().positive().safe().optional(),
332
348
  sampling: samplingParametersSchema,
349
+ /** Absent means the route's own default reasoning behaviour. */
350
+ reasoning: inferenceReasoningSchema.optional(),
351
+ speech: inferenceSpeechParametersSchema.optional(),
333
352
  tools: z.array(toolDefinitionSchema).default([]),
334
353
  toolChoice: toolChoiceSchema.optional(),
335
354
  responseFormat: responseFormatSchema.optional(),
@@ -372,6 +391,13 @@ export const inferenceRequestSchema = z
372
391
  authorizedRoutes: z.array(authorizedRouteSchema).min(1).optional(),
373
392
  })
374
393
  .superRefine((request, ctx) => {
394
+ const isSpeech = request.client.apiFormat === "audio_speech";
395
+ if (isSpeech && request.speech !== undefined && (request.modality !== "audio" || request.input.format !== "text" || request.stream)) {
396
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["speech"], message: "speech requires audio modality, text input, parameters and non-streaming output" });
397
+ }
398
+ if (!isSpeech && request.speech !== undefined) {
399
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["speech"], message: "speech parameters require the audio_speech API format" });
400
+ }
375
401
  if (request.toolChoice !== undefined && request.tools.length === 0) {
376
402
  ctx.addIssue({
377
403
  code: z.ZodIssueCode.custom,