@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
@@ -49,12 +49,220 @@ export declare const webauthnRegisterVerifyRequestSchema: z.ZodObject<{
49
49
  deviceName: z.ZodOptional<z.ZodString>;
50
50
  deviceFingerprint: z.ZodOptional<z.ZodString>;
51
51
  username: z.ZodOptional<z.ZodString>;
52
+ /**
53
+ * Sign-up only (ADR 0024 D4): the account's root, created on the holder BEFORE
54
+ * this request — sealed under the passkey being registered — and a root proof
55
+ * (`enroll_identity`) whose challenge is the registration challenge. The
56
+ * account, passkey, root and envelope are then created in one transaction.
57
+ */
58
+ identity: z.ZodOptional<z.ZodObject<{
59
+ envelope: z.ZodEffects<z.ZodObject<{
60
+ version: z.ZodLiteral<2>;
61
+ algorithm: z.ZodLiteral<"xchacha20poly1305">;
62
+ publicKey: z.ZodString;
63
+ secretKind: z.ZodEnum<["mnemonic-entropy", "raw-private-key"]>;
64
+ secretNonce: z.ZodString;
65
+ sealedSecret: z.ZodString;
66
+ wraps: z.ZodArray<z.ZodObject<{
67
+ credentialId: z.ZodString;
68
+ nonce: z.ZodString;
69
+ wrappedKey: z.ZodString;
70
+ createdAt: z.ZodString;
71
+ rpId: z.ZodString;
72
+ verifiedAt: z.ZodOptional<z.ZodString>;
73
+ }, "strip", z.ZodTypeAny, {
74
+ credentialId: string;
75
+ createdAt: string;
76
+ nonce: string;
77
+ wrappedKey: string;
78
+ rpId: string;
79
+ verifiedAt?: string | undefined;
80
+ }, {
81
+ credentialId: string;
82
+ createdAt: string;
83
+ nonce: string;
84
+ wrappedKey: string;
85
+ rpId: string;
86
+ verifiedAt?: string | undefined;
87
+ }>, "many">;
88
+ }, "strip", z.ZodTypeAny, {
89
+ version: 2;
90
+ publicKey: string;
91
+ algorithm: "xchacha20poly1305";
92
+ secretKind: "mnemonic-entropy" | "raw-private-key";
93
+ secretNonce: string;
94
+ sealedSecret: string;
95
+ wraps: {
96
+ credentialId: string;
97
+ createdAt: string;
98
+ nonce: string;
99
+ wrappedKey: string;
100
+ rpId: string;
101
+ verifiedAt?: string | undefined;
102
+ }[];
103
+ }, {
104
+ version: 2;
105
+ publicKey: string;
106
+ algorithm: "xchacha20poly1305";
107
+ secretKind: "mnemonic-entropy" | "raw-private-key";
108
+ secretNonce: string;
109
+ sealedSecret: string;
110
+ wraps: {
111
+ credentialId: string;
112
+ createdAt: string;
113
+ nonce: string;
114
+ wrappedKey: string;
115
+ rpId: string;
116
+ verifiedAt?: string | undefined;
117
+ }[];
118
+ }>, {
119
+ version: 2;
120
+ publicKey: string;
121
+ algorithm: "xchacha20poly1305";
122
+ secretKind: "mnemonic-entropy" | "raw-private-key";
123
+ secretNonce: string;
124
+ sealedSecret: string;
125
+ wraps: {
126
+ credentialId: string;
127
+ createdAt: string;
128
+ nonce: string;
129
+ wrappedKey: string;
130
+ rpId: string;
131
+ verifiedAt?: string | undefined;
132
+ }[];
133
+ }, {
134
+ version: 2;
135
+ publicKey: string;
136
+ algorithm: "xchacha20poly1305";
137
+ secretKind: "mnemonic-entropy" | "raw-private-key";
138
+ secretNonce: string;
139
+ sealedSecret: string;
140
+ wraps: {
141
+ credentialId: string;
142
+ createdAt: string;
143
+ nonce: string;
144
+ wrappedKey: string;
145
+ rpId: string;
146
+ verifiedAt?: string | undefined;
147
+ }[];
148
+ }>;
149
+ proof: z.ZodObject<{
150
+ v: z.ZodLiteral<2>;
151
+ challenge: z.ZodString;
152
+ expiresAt: z.ZodNumber;
153
+ signature: z.ZodString;
154
+ }, "strip", z.ZodTypeAny, {
155
+ expiresAt: number;
156
+ signature: string;
157
+ v: 2;
158
+ challenge: string;
159
+ }, {
160
+ expiresAt: number;
161
+ signature: string;
162
+ v: 2;
163
+ challenge: string;
164
+ }>;
165
+ }, "strip", z.ZodTypeAny, {
166
+ proof: {
167
+ expiresAt: number;
168
+ signature: string;
169
+ v: 2;
170
+ challenge: string;
171
+ };
172
+ envelope: {
173
+ version: 2;
174
+ publicKey: string;
175
+ algorithm: "xchacha20poly1305";
176
+ secretKind: "mnemonic-entropy" | "raw-private-key";
177
+ secretNonce: string;
178
+ sealedSecret: string;
179
+ wraps: {
180
+ credentialId: string;
181
+ createdAt: string;
182
+ nonce: string;
183
+ wrappedKey: string;
184
+ rpId: string;
185
+ verifiedAt?: string | undefined;
186
+ }[];
187
+ };
188
+ }, {
189
+ proof: {
190
+ expiresAt: number;
191
+ signature: string;
192
+ v: 2;
193
+ challenge: string;
194
+ };
195
+ envelope: {
196
+ version: 2;
197
+ publicKey: string;
198
+ algorithm: "xchacha20poly1305";
199
+ secretKind: "mnemonic-entropy" | "raw-private-key";
200
+ secretNonce: string;
201
+ sealedSecret: string;
202
+ wraps: {
203
+ credentialId: string;
204
+ createdAt: string;
205
+ nonce: string;
206
+ wrappedKey: string;
207
+ rpId: string;
208
+ verifiedAt?: string | undefined;
209
+ }[];
210
+ };
211
+ }>>;
52
212
  }, "strip", z.ZodTypeAny, {
53
213
  username?: string | undefined;
214
+ identity?: {
215
+ proof: {
216
+ expiresAt: number;
217
+ signature: string;
218
+ v: 2;
219
+ challenge: string;
220
+ };
221
+ envelope: {
222
+ version: 2;
223
+ publicKey: string;
224
+ algorithm: "xchacha20poly1305";
225
+ secretKind: "mnemonic-entropy" | "raw-private-key";
226
+ secretNonce: string;
227
+ sealedSecret: string;
228
+ wraps: {
229
+ credentialId: string;
230
+ createdAt: string;
231
+ nonce: string;
232
+ wrappedKey: string;
233
+ rpId: string;
234
+ verifiedAt?: string | undefined;
235
+ }[];
236
+ };
237
+ } | undefined;
54
238
  deviceName?: string | undefined;
55
239
  deviceFingerprint?: string | undefined;
56
240
  }, {
57
241
  username?: string | undefined;
242
+ identity?: {
243
+ proof: {
244
+ expiresAt: number;
245
+ signature: string;
246
+ v: 2;
247
+ challenge: string;
248
+ };
249
+ envelope: {
250
+ version: 2;
251
+ publicKey: string;
252
+ algorithm: "xchacha20poly1305";
253
+ secretKind: "mnemonic-entropy" | "raw-private-key";
254
+ secretNonce: string;
255
+ sealedSecret: string;
256
+ wraps: {
257
+ credentialId: string;
258
+ createdAt: string;
259
+ nonce: string;
260
+ wrappedKey: string;
261
+ rpId: string;
262
+ verifiedAt?: string | undefined;
263
+ }[];
264
+ };
265
+ } | undefined;
58
266
  deviceName?: string | undefined;
59
267
  deviceFingerprint?: string | undefined;
60
268
  }>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxy.so/contracts",
3
- "version": "1.1.1",
3
+ "version": "1.3.0",
4
4
  "description": "OxyHQ API contracts — single source of truth for request/response Zod schemas and inferred types, shared by the backend and the client SDKs",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -1,138 +0,0 @@
1
- "use strict";
2
- /**
3
- * Device-to-device identity transfer contracts (b3 Feature 2 — "add a device").
4
- *
5
- * SINGLE SOURCE OF TRUTH for the wire shape of the short-lived, unauthenticated
6
- * relay that carries E2E-encrypted key material from an existing (old) device to
7
- * a fresh (new) device so both end up holding the SAME secp256k1 private key
8
- * (key cloning). The relay is E2E-encrypted via an ephemeral secp256k1 ECDH
9
- * handshake: the server stores only the two ephemeral public keys plus an opaque
10
- * AEAD ciphertext + nonce and NEVER holds a decryption key.
11
- *
12
- * Flow:
13
- * 1. New device (no identity) generates an ephemeral pair and calls
14
- * `POST /identity/device-transfer/init { newEphPub, newDeviceLabel? }` →
15
- * `{ pairingId, expiresAt }`. The QR carries ONLY `pairingId` (not
16
- * self-contained — mirrors the QR sign-in `approve-info` resolution).
17
- * 2. Old device (has identity) scans, resolves the request via
18
- * `GET /identity/device-transfer/:pairingId` (returns `newEphPub` + label),
19
- * derives `transferKey = HKDF(ECDH(oldEphPriv, newEphPub), pairingId,
20
- * 'oxy-device-transfer-v1')`, AEAD-encrypts `{ privateKey, publicKey }`, and
21
- * calls `POST /identity/device-transfer/:pairingId/approve` with the
22
- * ciphertext PLUS a fresh signature over
23
- * `{ action:'approve_device_transfer', pairingId, timestamp }` made with the
24
- * CURRENT identity key (dual-proof: a bearer alone cannot exfiltrate the key).
25
- * 3. New device (socket push or poll fallback) re-derives the same
26
- * `transferKey` from `ECDH(newEphPriv, oldEphPub)`, decrypts, and imports the
27
- * private key, then completes a NORMAL challenge/verify sign-in.
28
- *
29
- * The load-bearing response shapes are declared as explicit `interface`s (same
30
- * `moduleResolution: node` rationale as `UserNameResponse` / the identity/civic
31
- * contracts: a nested `z.infer<>` can degrade to `{}` under a consumer's
32
- * `moduleResolution: "node"`), with the runtime schemas annotated
33
- * `z.ZodType<Interface>`.
34
- *
35
- * Platform-agnostic — zod only, no react/react-native/expo, ESM-safe.
36
- */
37
- Object.defineProperty(exports, "__esModule", { value: true });
38
- exports.deviceTransferDenyResponseSchema = exports.deviceTransferApproveResponseSchema = exports.deviceTransferApproveRequestSchema = exports.deviceTransferInfoResponseSchema = exports.deviceTransferInitResponseSchema = exports.deviceTransferInitRequestSchema = exports.devicePairingStatusSchema = void 0;
39
- const zod_1 = require("zod");
40
- /* -------------------------------------------------------------------------- */
41
- /* Shared field validators */
42
- /* -------------------------------------------------------------------------- */
43
- /** Lowercase/uppercase hex string (no `0x` prefix). */
44
- const hexString = zod_1.z
45
- .string()
46
- .trim()
47
- .regex(/^[0-9a-fA-F]+$/, 'must be a hex string');
48
- /**
49
- * A secp256k1 public key, hex-encoded — compressed (`02`/`03` + 32 bytes = 66
50
- * chars) or uncompressed (`04` + 64 bytes = 130 chars). The exact curve-point
51
- * validity is re-checked server-side; this only bounds the shape/length.
52
- */
53
- const publicKeyHex = hexString.min(66).max(130);
54
- /** DER-encoded ECDSA signature, hex (variable length, ~140–144 chars). */
55
- const signatureHex = hexString.min(2).max(256);
56
- /**
57
- * The 24-byte XChaCha20-Poly1305 nonce, hex (exactly 48 chars). Matches
58
- * `@oxy.so/core` `AEAD_NONCE_LENGTH` (24 bytes).
59
- */
60
- const nonceHex = hexString.length(48);
61
- /**
62
- * The AEAD ciphertext (Poly1305 tag appended), hex. The plaintext is the small
63
- * JSON `{ privateKey, publicKey }` (~200 bytes), so the ciphertext stays well
64
- * under the cap; the bound blunts relay-abuse via oversized blobs.
65
- */
66
- const ciphertextHex = hexString.min(2).max(8192);
67
- /* -------------------------------------------------------------------------- */
68
- /* Status */
69
- /* -------------------------------------------------------------------------- */
70
- /**
71
- * Pairing lifecycle:
72
- * - `pending` — created by the new device, awaiting the old device's approval.
73
- * - `approved` — the old device sealed and posted the encrypted key material.
74
- * - `denied` — the old device explicitly cancelled the transfer.
75
- * - `expired` — the 3-minute TTL elapsed before approval.
76
- */
77
- exports.devicePairingStatusSchema = zod_1.z.enum([
78
- 'pending',
79
- 'approved',
80
- 'denied',
81
- 'expired',
82
- ]);
83
- /* -------------------------------------------------------------------------- */
84
- /* POST /identity/device-transfer/init */
85
- /* -------------------------------------------------------------------------- */
86
- /** Request body for `POST /identity/device-transfer/init` (public). */
87
- exports.deviceTransferInitRequestSchema = zod_1.z.object({
88
- /** The new device's ephemeral secp256k1 public key (single-use). */
89
- newEphPub: publicKeyHex,
90
- /** Optional human-readable label for the new device (e.g. "iPhone 15"). */
91
- newDeviceLabel: zod_1.z.string().trim().min(1).max(120).optional(),
92
- });
93
- exports.deviceTransferInitResponseSchema = zod_1.z.object({
94
- pairingId: zod_1.z.string(),
95
- expiresAt: zod_1.z.string(),
96
- });
97
- exports.deviceTransferInfoResponseSchema = zod_1.z.object({
98
- pairingId: zod_1.z.string(),
99
- newDeviceEphemeralPublicKey: zod_1.z.string(),
100
- newDeviceLabel: zod_1.z.string().nullable(),
101
- status: exports.devicePairingStatusSchema,
102
- expiresAt: zod_1.z.string(),
103
- oldDeviceEphemeralPublicKey: zod_1.z.string().nullable(),
104
- ciphertext: zod_1.z.string().nullable(),
105
- nonce: zod_1.z.string().nullable(),
106
- });
107
- /* -------------------------------------------------------------------------- */
108
- /* POST /identity/device-transfer/:pairingId/approve */
109
- /* -------------------------------------------------------------------------- */
110
- /**
111
- * Request body for `POST /identity/device-transfer/:pairingId/approve`
112
- * (bearer-authenticated AND signature-proven). The `signature` covers
113
- * `JSON.stringify({ action:'approve_device_transfer', pairingId, timestamp })`
114
- * made with the caller's CURRENT identity key — dual-proof so a bearer token
115
- * alone can never exfiltrate the private key.
116
- */
117
- exports.deviceTransferApproveRequestSchema = zod_1.z.object({
118
- /** The old device's ephemeral secp256k1 public key (single-use). */
119
- oldEphPub: publicKeyHex,
120
- /** AEAD ciphertext of `{ privateKey, publicKey }`, hex. */
121
- ciphertext: ciphertextHex,
122
- /** AEAD nonce, hex (24 bytes). */
123
- nonce: nonceHex,
124
- /** ECDSA (DER, hex) signature proving control of the CURRENT identity key. */
125
- signature: signatureHex,
126
- /** Signing timestamp (ms since epoch) — freshness-checked server-side. */
127
- timestamp: zod_1.z.number().int().positive(),
128
- });
129
- exports.deviceTransferApproveResponseSchema = zod_1.z.object({
130
- success: zod_1.z.boolean(),
131
- pairingId: zod_1.z.string(),
132
- status: exports.devicePairingStatusSchema,
133
- });
134
- exports.deviceTransferDenyResponseSchema = zod_1.z.object({
135
- success: zod_1.z.boolean(),
136
- pairingId: zod_1.z.string(),
137
- status: exports.devicePairingStatusSchema,
138
- });
@@ -1,135 +0,0 @@
1
- /**
2
- * Device-to-device identity transfer contracts (b3 Feature 2 — "add a device").
3
- *
4
- * SINGLE SOURCE OF TRUTH for the wire shape of the short-lived, unauthenticated
5
- * relay that carries E2E-encrypted key material from an existing (old) device to
6
- * a fresh (new) device so both end up holding the SAME secp256k1 private key
7
- * (key cloning). The relay is E2E-encrypted via an ephemeral secp256k1 ECDH
8
- * handshake: the server stores only the two ephemeral public keys plus an opaque
9
- * AEAD ciphertext + nonce and NEVER holds a decryption key.
10
- *
11
- * Flow:
12
- * 1. New device (no identity) generates an ephemeral pair and calls
13
- * `POST /identity/device-transfer/init { newEphPub, newDeviceLabel? }` →
14
- * `{ pairingId, expiresAt }`. The QR carries ONLY `pairingId` (not
15
- * self-contained — mirrors the QR sign-in `approve-info` resolution).
16
- * 2. Old device (has identity) scans, resolves the request via
17
- * `GET /identity/device-transfer/:pairingId` (returns `newEphPub` + label),
18
- * derives `transferKey = HKDF(ECDH(oldEphPriv, newEphPub), pairingId,
19
- * 'oxy-device-transfer-v1')`, AEAD-encrypts `{ privateKey, publicKey }`, and
20
- * calls `POST /identity/device-transfer/:pairingId/approve` with the
21
- * ciphertext PLUS a fresh signature over
22
- * `{ action:'approve_device_transfer', pairingId, timestamp }` made with the
23
- * CURRENT identity key (dual-proof: a bearer alone cannot exfiltrate the key).
24
- * 3. New device (socket push or poll fallback) re-derives the same
25
- * `transferKey` from `ECDH(newEphPriv, oldEphPub)`, decrypts, and imports the
26
- * private key, then completes a NORMAL challenge/verify sign-in.
27
- *
28
- * The load-bearing response shapes are declared as explicit `interface`s (same
29
- * `moduleResolution: node` rationale as `UserNameResponse` / the identity/civic
30
- * contracts: a nested `z.infer<>` can degrade to `{}` under a consumer's
31
- * `moduleResolution: "node"`), with the runtime schemas annotated
32
- * `z.ZodType<Interface>`.
33
- *
34
- * Platform-agnostic — zod only, no react/react-native/expo, ESM-safe.
35
- */
36
- import { z } from 'zod';
37
- /* -------------------------------------------------------------------------- */
38
- /* Shared field validators */
39
- /* -------------------------------------------------------------------------- */
40
- /** Lowercase/uppercase hex string (no `0x` prefix). */
41
- const hexString = z
42
- .string()
43
- .trim()
44
- .regex(/^[0-9a-fA-F]+$/, 'must be a hex string');
45
- /**
46
- * A secp256k1 public key, hex-encoded — compressed (`02`/`03` + 32 bytes = 66
47
- * chars) or uncompressed (`04` + 64 bytes = 130 chars). The exact curve-point
48
- * validity is re-checked server-side; this only bounds the shape/length.
49
- */
50
- const publicKeyHex = hexString.min(66).max(130);
51
- /** DER-encoded ECDSA signature, hex (variable length, ~140–144 chars). */
52
- const signatureHex = hexString.min(2).max(256);
53
- /**
54
- * The 24-byte XChaCha20-Poly1305 nonce, hex (exactly 48 chars). Matches
55
- * `@oxy.so/core` `AEAD_NONCE_LENGTH` (24 bytes).
56
- */
57
- const nonceHex = hexString.length(48);
58
- /**
59
- * The AEAD ciphertext (Poly1305 tag appended), hex. The plaintext is the small
60
- * JSON `{ privateKey, publicKey }` (~200 bytes), so the ciphertext stays well
61
- * under the cap; the bound blunts relay-abuse via oversized blobs.
62
- */
63
- const ciphertextHex = hexString.min(2).max(8192);
64
- /* -------------------------------------------------------------------------- */
65
- /* Status */
66
- /* -------------------------------------------------------------------------- */
67
- /**
68
- * Pairing lifecycle:
69
- * - `pending` — created by the new device, awaiting the old device's approval.
70
- * - `approved` — the old device sealed and posted the encrypted key material.
71
- * - `denied` — the old device explicitly cancelled the transfer.
72
- * - `expired` — the 3-minute TTL elapsed before approval.
73
- */
74
- export const devicePairingStatusSchema = z.enum([
75
- 'pending',
76
- 'approved',
77
- 'denied',
78
- 'expired',
79
- ]);
80
- /* -------------------------------------------------------------------------- */
81
- /* POST /identity/device-transfer/init */
82
- /* -------------------------------------------------------------------------- */
83
- /** Request body for `POST /identity/device-transfer/init` (public). */
84
- export const deviceTransferInitRequestSchema = z.object({
85
- /** The new device's ephemeral secp256k1 public key (single-use). */
86
- newEphPub: publicKeyHex,
87
- /** Optional human-readable label for the new device (e.g. "iPhone 15"). */
88
- newDeviceLabel: z.string().trim().min(1).max(120).optional(),
89
- });
90
- export const deviceTransferInitResponseSchema = z.object({
91
- pairingId: z.string(),
92
- expiresAt: z.string(),
93
- });
94
- export const deviceTransferInfoResponseSchema = z.object({
95
- pairingId: z.string(),
96
- newDeviceEphemeralPublicKey: z.string(),
97
- newDeviceLabel: z.string().nullable(),
98
- status: devicePairingStatusSchema,
99
- expiresAt: z.string(),
100
- oldDeviceEphemeralPublicKey: z.string().nullable(),
101
- ciphertext: z.string().nullable(),
102
- nonce: z.string().nullable(),
103
- });
104
- /* -------------------------------------------------------------------------- */
105
- /* POST /identity/device-transfer/:pairingId/approve */
106
- /* -------------------------------------------------------------------------- */
107
- /**
108
- * Request body for `POST /identity/device-transfer/:pairingId/approve`
109
- * (bearer-authenticated AND signature-proven). The `signature` covers
110
- * `JSON.stringify({ action:'approve_device_transfer', pairingId, timestamp })`
111
- * made with the caller's CURRENT identity key — dual-proof so a bearer token
112
- * alone can never exfiltrate the private key.
113
- */
114
- export const deviceTransferApproveRequestSchema = z.object({
115
- /** The old device's ephemeral secp256k1 public key (single-use). */
116
- oldEphPub: publicKeyHex,
117
- /** AEAD ciphertext of `{ privateKey, publicKey }`, hex. */
118
- ciphertext: ciphertextHex,
119
- /** AEAD nonce, hex (24 bytes). */
120
- nonce: nonceHex,
121
- /** ECDSA (DER, hex) signature proving control of the CURRENT identity key. */
122
- signature: signatureHex,
123
- /** Signing timestamp (ms since epoch) — freshness-checked server-side. */
124
- timestamp: z.number().int().positive(),
125
- });
126
- export const deviceTransferApproveResponseSchema = z.object({
127
- success: z.boolean(),
128
- pairingId: z.string(),
129
- status: devicePairingStatusSchema,
130
- });
131
- export const deviceTransferDenyResponseSchema = z.object({
132
- success: z.boolean(),
133
- pairingId: z.string(),
134
- status: devicePairingStatusSchema,
135
- });
@@ -1,130 +0,0 @@
1
- /**
2
- * Device-to-device identity transfer contracts (b3 Feature 2 — "add a device").
3
- *
4
- * SINGLE SOURCE OF TRUTH for the wire shape of the short-lived, unauthenticated
5
- * relay that carries E2E-encrypted key material from an existing (old) device to
6
- * a fresh (new) device so both end up holding the SAME secp256k1 private key
7
- * (key cloning). The relay is E2E-encrypted via an ephemeral secp256k1 ECDH
8
- * handshake: the server stores only the two ephemeral public keys plus an opaque
9
- * AEAD ciphertext + nonce and NEVER holds a decryption key.
10
- *
11
- * Flow:
12
- * 1. New device (no identity) generates an ephemeral pair and calls
13
- * `POST /identity/device-transfer/init { newEphPub, newDeviceLabel? }` →
14
- * `{ pairingId, expiresAt }`. The QR carries ONLY `pairingId` (not
15
- * self-contained — mirrors the QR sign-in `approve-info` resolution).
16
- * 2. Old device (has identity) scans, resolves the request via
17
- * `GET /identity/device-transfer/:pairingId` (returns `newEphPub` + label),
18
- * derives `transferKey = HKDF(ECDH(oldEphPriv, newEphPub), pairingId,
19
- * 'oxy-device-transfer-v1')`, AEAD-encrypts `{ privateKey, publicKey }`, and
20
- * calls `POST /identity/device-transfer/:pairingId/approve` with the
21
- * ciphertext PLUS a fresh signature over
22
- * `{ action:'approve_device_transfer', pairingId, timestamp }` made with the
23
- * CURRENT identity key (dual-proof: a bearer alone cannot exfiltrate the key).
24
- * 3. New device (socket push or poll fallback) re-derives the same
25
- * `transferKey` from `ECDH(newEphPriv, oldEphPub)`, decrypts, and imports the
26
- * private key, then completes a NORMAL challenge/verify sign-in.
27
- *
28
- * The load-bearing response shapes are declared as explicit `interface`s (same
29
- * `moduleResolution: node` rationale as `UserNameResponse` / the identity/civic
30
- * contracts: a nested `z.infer<>` can degrade to `{}` under a consumer's
31
- * `moduleResolution: "node"`), with the runtime schemas annotated
32
- * `z.ZodType<Interface>`.
33
- *
34
- * Platform-agnostic — zod only, no react/react-native/expo, ESM-safe.
35
- */
36
- import { z } from 'zod';
37
- /**
38
- * Pairing lifecycle:
39
- * - `pending` — created by the new device, awaiting the old device's approval.
40
- * - `approved` — the old device sealed and posted the encrypted key material.
41
- * - `denied` — the old device explicitly cancelled the transfer.
42
- * - `expired` — the 3-minute TTL elapsed before approval.
43
- */
44
- export declare const devicePairingStatusSchema: z.ZodEnum<["pending", "approved", "denied", "expired"]>;
45
- export type DevicePairingStatus = z.infer<typeof devicePairingStatusSchema>;
46
- /** Request body for `POST /identity/device-transfer/init` (public). */
47
- export declare const deviceTransferInitRequestSchema: z.ZodObject<{
48
- /** The new device's ephemeral secp256k1 public key (single-use). */
49
- newEphPub: z.ZodString;
50
- /** Optional human-readable label for the new device (e.g. "iPhone 15"). */
51
- newDeviceLabel: z.ZodOptional<z.ZodString>;
52
- }, "strip", z.ZodTypeAny, {
53
- newEphPub: string;
54
- newDeviceLabel?: string | undefined;
55
- }, {
56
- newEphPub: string;
57
- newDeviceLabel?: string | undefined;
58
- }>;
59
- export type DeviceTransferInitRequest = z.infer<typeof deviceTransferInitRequestSchema>;
60
- export interface DeviceTransferInitResponse {
61
- /** 128-bit single-use handle carried in the QR. Also the HKDF salt. */
62
- pairingId: string;
63
- /** ISO-8601 expiry (3 minutes from creation). */
64
- expiresAt: string;
65
- }
66
- export declare const deviceTransferInitResponseSchema: z.ZodType<DeviceTransferInitResponse>;
67
- export interface DeviceTransferInfoResponse {
68
- pairingId: string;
69
- /** The new device's ephemeral public key (so the old device can ECDH). */
70
- newDeviceEphemeralPublicKey: string;
71
- /** Optional new-device label supplied at init. */
72
- newDeviceLabel: string | null;
73
- status: DevicePairingStatus;
74
- /** ISO-8601 expiry. */
75
- expiresAt: string;
76
- /**
77
- * The old device's ephemeral public key — present ONLY once `status` is
78
- * `approved` (so the new device can re-derive the shared secret).
79
- */
80
- oldDeviceEphemeralPublicKey: string | null;
81
- /** AEAD ciphertext (hex) — present ONLY once `status` is `approved`. */
82
- ciphertext: string | null;
83
- /** AEAD nonce (hex) — present ONLY once `status` is `approved`. */
84
- nonce: string | null;
85
- }
86
- export declare const deviceTransferInfoResponseSchema: z.ZodType<DeviceTransferInfoResponse>;
87
- /**
88
- * Request body for `POST /identity/device-transfer/:pairingId/approve`
89
- * (bearer-authenticated AND signature-proven). The `signature` covers
90
- * `JSON.stringify({ action:'approve_device_transfer', pairingId, timestamp })`
91
- * made with the caller's CURRENT identity key — dual-proof so a bearer token
92
- * alone can never exfiltrate the private key.
93
- */
94
- export declare const deviceTransferApproveRequestSchema: z.ZodObject<{
95
- /** The old device's ephemeral secp256k1 public key (single-use). */
96
- oldEphPub: z.ZodString;
97
- /** AEAD ciphertext of `{ privateKey, publicKey }`, hex. */
98
- ciphertext: z.ZodString;
99
- /** AEAD nonce, hex (24 bytes). */
100
- nonce: z.ZodString;
101
- /** ECDSA (DER, hex) signature proving control of the CURRENT identity key. */
102
- signature: z.ZodString;
103
- /** Signing timestamp (ms since epoch) — freshness-checked server-side. */
104
- timestamp: z.ZodNumber;
105
- }, "strip", z.ZodTypeAny, {
106
- signature: string;
107
- nonce: string;
108
- ciphertext: string;
109
- oldEphPub: string;
110
- timestamp: number;
111
- }, {
112
- signature: string;
113
- nonce: string;
114
- ciphertext: string;
115
- oldEphPub: string;
116
- timestamp: number;
117
- }>;
118
- export type DeviceTransferApproveRequest = z.infer<typeof deviceTransferApproveRequestSchema>;
119
- export interface DeviceTransferApproveResponse {
120
- success: boolean;
121
- pairingId: string;
122
- status: DevicePairingStatus;
123
- }
124
- export declare const deviceTransferApproveResponseSchema: z.ZodType<DeviceTransferApproveResponse>;
125
- export interface DeviceTransferDenyResponse {
126
- success: boolean;
127
- pairingId: string;
128
- status: DevicePairingStatus;
129
- }
130
- export declare const deviceTransferDenyResponseSchema: z.ZodType<DeviceTransferDenyResponse>;