@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.
- package/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/identity.js +3 -2
- package/dist/cjs/identityMove.js +156 -0
- package/dist/cjs/identityProof.js +164 -0
- package/dist/cjs/identityRecovery.js +51 -0
- package/dist/cjs/index.js +75 -19
- package/dist/cjs/inference/catalogue.js +7 -6
- package/dist/cjs/inference/identifiers.js +3 -1
- package/dist/cjs/inference/providerConnection.js +1 -1
- package/dist/cjs/inference/request.js +15 -1
- package/dist/cjs/inference/streamEvents.js +24 -2
- package/dist/cjs/inference/version.js +1 -1
- package/dist/cjs/userResponse.js +82 -1
- package/dist/cjs/username.js +70 -2
- package/dist/cjs/webIdentityCarrier.js +181 -0
- package/dist/cjs/webauthn.js +14 -0
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/identity.js +3 -2
- package/dist/esm/identityMove.js +148 -0
- package/dist/esm/identityProof.js +159 -0
- package/dist/esm/identityRecovery.js +48 -0
- package/dist/esm/index.js +16 -7
- package/dist/esm/inference/catalogue.js +7 -6
- package/dist/esm/inference/identifiers.js +2 -0
- package/dist/esm/inference/providerConnection.js +2 -2
- package/dist/esm/inference/request.js +14 -0
- package/dist/esm/inference/streamEvents.js +24 -2
- package/dist/esm/inference/version.js +1 -1
- package/dist/esm/userResponse.js +81 -0
- package/dist/esm/username.js +69 -1
- package/dist/esm/webIdentityCarrier.js +178 -0
- package/dist/esm/webauthn.js +14 -0
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/accountGraph.d.ts +7 -7
- package/dist/types/agency.d.ts +24 -24
- package/dist/types/browserHub.d.ts +16 -16
- package/dist/types/deviceDirectory.d.ts +28 -28
- package/dist/types/externalIdentity.d.ts +21 -7
- package/dist/types/identity.d.ts +3 -2
- package/dist/types/identityMove.d.ts +185 -0
- package/dist/types/identityProof.d.ts +156 -0
- package/dist/types/identityRecovery.d.ts +246 -0
- package/dist/types/index.d.ts +14 -8
- package/dist/types/inference/catalogue.d.ts +21 -20
- package/dist/types/inference/embeddings.d.ts +12 -12
- package/dist/types/inference/errors.d.ts +4 -4
- package/dist/types/inference/identifiers.d.ts +2 -0
- package/dist/types/inference/inbox.d.ts +6 -6
- package/dist/types/inference/providerConnection.d.ts +40 -40
- package/dist/types/inference/request.d.ts +84 -36
- package/dist/types/inference/streamEvents.d.ts +69 -13
- package/dist/types/inference/usage.d.ts +8 -8
- package/dist/types/inference/version.d.ts +1 -1
- package/dist/types/keyRotation.d.ts +2 -2
- package/dist/types/oauth.d.ts +30 -30
- package/dist/types/sessionStatus.d.ts +6 -6
- package/dist/types/transparency.d.ts +10 -10
- package/dist/types/userResponse.d.ts +386 -18
- package/dist/types/username.d.ts +25 -2
- package/dist/types/webIdentityCarrier.d.ts +1130 -0
- package/dist/types/webauthn.d.ts +208 -0
- package/package.json +1 -1
- package/dist/cjs/devicePairing.js +0 -138
- package/dist/esm/devicePairing.js +0 -135
- package/dist/types/devicePairing.d.ts +0 -130
package/dist/types/webauthn.d.ts
CHANGED
|
@@ -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.
|
|
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>;
|