@receiz/sdk 93.2.0 → 95.0.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.
@@ -0,0 +1,853 @@
1
+ export const RECEIZ_KEY_SCHEMA = "receiz.key.v1";
2
+ export const RECEIZ_KEY_NAME = "Receiz Key";
3
+ export const RECEIZ_ACCOUNT_STATE_SCHEMA = "receiz.account.state.v3";
4
+ export const RECEIZ_DEVICE_IDENTITY_SCHEMA = "receiz.device.identity.v1";
5
+ export const RECEIZ_IDENTITY_LOGIN_PROOF_SCHEMA = "receiz.identity.login_proof.v1";
6
+ export const RECEIZ_IDENTITY_ACCOUNT_PROJECTION_SCHEMA = "receiz.sdk.identity_account_projection.v1";
7
+ export const RECEIZ_IDENTITY_RECORD_KEY_CHUNK_KEY = "receiz.identity_record.keyfile.v1";
8
+ const KEY_ID_RE = /^[0-9a-f]{64}$/;
9
+ const HEX64_RE = /^[0-9a-f]{64}$/;
10
+ const BASE64URL_RE = /^[A-Za-z0-9_-]+$/;
11
+ const RECEIZ_KEYFILE_VERSION = 1;
12
+ const RECEIZ_KEYFILE_MAX_BYTES = 64 * 1024 * 1024;
13
+ const RECEIZ_KEYFILE_MIN_PASSPHRASE_LENGTH = 12;
14
+ const DEFAULT_PBKDF2_ITERATIONS = 210_000;
15
+ const RECEIZ_IDENTITY_RECORD_KEY_TRAILER_PREFIX = "\n--RECEIZ-IDENTITY-RECORD-KEYFILE-V1--\n";
16
+ const RECEIZ_IDENTITY_RECORD_KEY_TRAILER_SUFFIX = "\n--END-RECEIZ-IDENTITY-RECORD-KEYFILE-V1--\n";
17
+ const PNG_SIGNATURE = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]);
18
+ function isJsonPrimitive(value) {
19
+ return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
20
+ }
21
+ function normalizeNumber(value) {
22
+ if (!Number.isFinite(value))
23
+ return "null";
24
+ return JSON.stringify(value);
25
+ }
26
+ function jcsCanonicalizeValue(value) {
27
+ if (isJsonPrimitive(value)) {
28
+ if (typeof value === "number")
29
+ return normalizeNumber(value);
30
+ return JSON.stringify(value);
31
+ }
32
+ if (typeof value === "bigint")
33
+ throw new TypeError("JCS does not support bigint values.");
34
+ if (typeof value === "function" || typeof value === "symbol" || value === undefined)
35
+ return "null";
36
+ if (value && typeof value === "object") {
37
+ const withToJson = value;
38
+ if (typeof withToJson.toJSON === "function")
39
+ return jcsCanonicalizeValue(withToJson.toJSON());
40
+ if (Array.isArray(value)) {
41
+ return `[${value
42
+ .map((item) => item === undefined || typeof item === "function" || typeof item === "symbol" ? "null" : jcsCanonicalizeValue(item))
43
+ .join(",")}]`;
44
+ }
45
+ const record = value;
46
+ return `{${Object.keys(record)
47
+ .sort()
48
+ .filter((key) => record[key] !== undefined && typeof record[key] !== "function" && typeof record[key] !== "symbol")
49
+ .map((key) => `${JSON.stringify(key)}:${jcsCanonicalizeValue(record[key])}`)
50
+ .join(",")}}`;
51
+ }
52
+ return "null";
53
+ }
54
+ function jcsCanonicalize(value) {
55
+ return jcsCanonicalizeValue(value);
56
+ }
57
+ function asRecord(value) {
58
+ if (!value || typeof value !== "object" || Array.isArray(value))
59
+ return null;
60
+ return value;
61
+ }
62
+ function asTrimmedString(value) {
63
+ return typeof value === "string" ? value.trim() : "";
64
+ }
65
+ function asNullableTrimmedString(value) {
66
+ const normalized = asTrimmedString(value);
67
+ return normalized ? normalized : null;
68
+ }
69
+ function normalizeIdentityKeyId(value) {
70
+ if (typeof value !== "string")
71
+ return null;
72
+ const normalized = value.trim().toLowerCase();
73
+ return KEY_ID_RE.test(normalized) ? normalized : null;
74
+ }
75
+ function normalizeBase64Url(value) {
76
+ if (typeof value !== "string")
77
+ return null;
78
+ const normalized = value.trim();
79
+ if (!normalized || !BASE64URL_RE.test(normalized))
80
+ return null;
81
+ return normalized;
82
+ }
83
+ function normalizeOptionalBase64Url(value) {
84
+ if (typeof value !== "string")
85
+ return null;
86
+ const normalized = value.trim();
87
+ if (!normalized)
88
+ return null;
89
+ return BASE64URL_RE.test(normalized) ? normalized : null;
90
+ }
91
+ function normalizeReceizIdentityKeyAlgorithm(value) {
92
+ const normalized = asTrimmedString(value);
93
+ return normalized === "Ed25519" || normalized === "P-256" ? normalized : null;
94
+ }
95
+ function bufferCtor() {
96
+ return (globalThis.Buffer ?? null);
97
+ }
98
+ function bytesToBase64(bytes) {
99
+ const BufferLike = bufferCtor();
100
+ if (BufferLike)
101
+ return BufferLike.from(bytes).toString("base64");
102
+ let binary = "";
103
+ const chunkSize = 0x8000;
104
+ for (let i = 0; i < bytes.length; i += chunkSize) {
105
+ binary += String.fromCharCode(...bytes.subarray(i, i + chunkSize));
106
+ }
107
+ return btoa(binary);
108
+ }
109
+ function base64ToBytes(base64) {
110
+ const BufferLike = bufferCtor();
111
+ if (BufferLike) {
112
+ const buffer = BufferLike.from(base64, "base64");
113
+ const out = new Uint8Array(buffer.length);
114
+ for (let i = 0; i < buffer.length; i += 1)
115
+ out[i] = (buffer[i] ?? 0) & 0xff;
116
+ return out;
117
+ }
118
+ const binary = atob(base64);
119
+ const out = new Uint8Array(binary.length);
120
+ for (let i = 0; i < binary.length; i += 1)
121
+ out[i] = binary.charCodeAt(i);
122
+ return out;
123
+ }
124
+ export function receizBase64UrlEncode(bytes) {
125
+ return bytesToBase64(bytes).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
126
+ }
127
+ export function receizBase64UrlDecode(value) {
128
+ const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
129
+ const padding = "=".repeat((4 - (normalized.length % 4)) % 4);
130
+ return base64ToBytes(`${normalized}${padding}`);
131
+ }
132
+ function utf8(value) {
133
+ return new TextEncoder().encode(value);
134
+ }
135
+ function decodeUtf8(bytes) {
136
+ return new TextDecoder().decode(bytes);
137
+ }
138
+ function toStrictArrayBuffer(bytes) {
139
+ const out = new Uint8Array(bytes.byteLength);
140
+ out.set(bytes);
141
+ return out.buffer;
142
+ }
143
+ function randomBytes(size) {
144
+ if (!globalThis.crypto?.getRandomValues)
145
+ throw new Error("identity_key_not_supported");
146
+ const bytes = new Uint8Array(size);
147
+ globalThis.crypto.getRandomValues(bytes);
148
+ return bytes;
149
+ }
150
+ function getSubtle() {
151
+ if (!globalThis.crypto?.subtle)
152
+ throw new Error("identity_key_not_supported");
153
+ return globalThis.crypto.subtle;
154
+ }
155
+ function bytesToHex(bytes) {
156
+ return Array.from(bytes)
157
+ .map((part) => part.toString(16).padStart(2, "0"))
158
+ .join("");
159
+ }
160
+ async function sha256Hex(bytes) {
161
+ return bytesToHex(new Uint8Array(await getSubtle().digest("SHA-256", toStrictArrayBuffer(bytes))));
162
+ }
163
+ function normalizePassphrase(passphrase) {
164
+ return passphrase.normalize("NFKC");
165
+ }
166
+ function keyAad(keyId) {
167
+ return `RECEIZ_KEY_V1|${keyId}`;
168
+ }
169
+ function portableStateProofMethodForAlg(alg) {
170
+ return alg === "P-256" ? "ecdsa-p256-sha256.jcs.v1" : "ed25519.jcs.v1";
171
+ }
172
+ function validatePublicKeyRawBytesForAlg(bytes, alg) {
173
+ if (alg === "Ed25519")
174
+ return bytes.byteLength === 32;
175
+ return bytes.byteLength === 65 && bytes[0] === 4;
176
+ }
177
+ function keyGenerationAlgorithmForAlg(alg) {
178
+ return alg === "P-256" ? { name: "ECDSA", namedCurve: "P-256" } : { name: "Ed25519" };
179
+ }
180
+ function keyImportAlgorithmForAlg(alg) {
181
+ return alg === "P-256" ? { name: "ECDSA", namedCurve: "P-256" } : { name: "Ed25519" };
182
+ }
183
+ function keySignVerifyAlgorithmForAlg(alg) {
184
+ return alg === "P-256" ? { name: "ECDSA", hash: "SHA-256" } : "Ed25519";
185
+ }
186
+ function normalizedIterations(value) {
187
+ const parsed = typeof value === "number" ? value : Number.NaN;
188
+ if (!Number.isFinite(parsed))
189
+ return DEFAULT_PBKDF2_ITERATIONS;
190
+ return Math.max(100_000, Math.min(1_500_000, Math.trunc(parsed)));
191
+ }
192
+ async function deriveAesGcmKey(args) {
193
+ const subtle = getSubtle();
194
+ const keyMaterial = await subtle.importKey("raw", toStrictArrayBuffer(utf8(args.passphrase)), { name: "PBKDF2" }, false, ["deriveKey"]);
195
+ return subtle.deriveKey({
196
+ name: "PBKDF2",
197
+ hash: "SHA-256",
198
+ salt: toStrictArrayBuffer(args.salt),
199
+ iterations: args.iterations,
200
+ }, keyMaterial, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"]);
201
+ }
202
+ function normalizeOwner(owner) {
203
+ const uid = asTrimmedString(owner.uid);
204
+ if (!uid)
205
+ throw new Error("invalid_owner_uid");
206
+ return {
207
+ uid,
208
+ email: asNullableTrimmedString(owner.email),
209
+ username: asNullableTrimmedString(owner.username),
210
+ displayName: asNullableTrimmedString(owner.displayName),
211
+ };
212
+ }
213
+ function normalizePortableStateProof(value) {
214
+ const record = asRecord(value);
215
+ if (!record)
216
+ return null;
217
+ const method = asTrimmedString(record.method);
218
+ const digestSha256Hex = asTrimmedString(record.digestSha256Hex).toLowerCase();
219
+ const signatureB64u = normalizeBase64Url(record.signatureB64u);
220
+ const signedAt = asTrimmedString(record.signedAt);
221
+ if ((method !== "ed25519.jcs.v1" && method !== "ecdsa-p256-sha256.jcs.v1") ||
222
+ !HEX64_RE.test(digestSha256Hex) ||
223
+ !signatureB64u ||
224
+ !signedAt) {
225
+ return null;
226
+ }
227
+ return {
228
+ method: method,
229
+ digestSha256Hex,
230
+ signatureB64u,
231
+ signedAt,
232
+ };
233
+ }
234
+ function ensureKeyFileShape(file) {
235
+ const keyId = normalizeIdentityKeyId(file.keyId);
236
+ const alg = normalizeReceizIdentityKeyAlgorithm(file.alg);
237
+ const publicKeyRawB64u = normalizeBase64Url(file.crypto?.publicKeyRawB64u);
238
+ const privateCiphertextB64u = normalizeOptionalBase64Url(file.crypto?.privateKeyPkcs8CiphertextB64u);
239
+ const privateKeyPkcs8B64u = normalizeOptionalBase64Url(file.crypto?.privateKeyPkcs8B64u);
240
+ const saltB64u = normalizeBase64Url(file.crypto?.kdf?.saltB64u);
241
+ const ivB64u = normalizeBase64Url(file.crypto?.cipher?.ivB64u);
242
+ if (!keyId || !alg || !publicKeyRawB64u || (!privateCiphertextB64u && !privateKeyPkcs8B64u) || !saltB64u || !ivB64u) {
243
+ throw new Error("receiz_key_invalid");
244
+ }
245
+ if (!validatePublicKeyRawBytesForAlg(receizBase64UrlDecode(publicKeyRawB64u), alg))
246
+ throw new Error("receiz_key_invalid");
247
+ if (file.crypto.cipher.name !== "AES-GCM-256" || file.crypto.kdf.name !== "PBKDF2-SHA256")
248
+ throw new Error("receiz_key_invalid");
249
+ if (!Number.isFinite(file.crypto.kdf.iterations) || file.crypto.kdf.iterations < 1_000)
250
+ throw new Error("receiz_key_invalid");
251
+ const portableState = file.portableState
252
+ ? {
253
+ schema: asTrimmedString(file.portableState.schema) || "receiz.account.state.v1",
254
+ exportedAt: asTrimmedString(file.portableState.exportedAt) || file.issuedAt,
255
+ snapshot: file.portableState.snapshot,
256
+ proof: normalizePortableStateProof(file.portableState.proof),
257
+ }
258
+ : null;
259
+ return {
260
+ ...file,
261
+ alg,
262
+ keyId,
263
+ crypto: {
264
+ ...file.crypto,
265
+ publicKeyRawB64u,
266
+ privateKeyPkcs8CiphertextB64u: privateCiphertextB64u ?? "",
267
+ privateKeyPkcs8B64u: privateKeyPkcs8B64u ?? null,
268
+ kdf: {
269
+ ...file.crypto.kdf,
270
+ saltB64u,
271
+ },
272
+ cipher: {
273
+ ...file.crypto.cipher,
274
+ ivB64u,
275
+ },
276
+ },
277
+ portableState,
278
+ };
279
+ }
280
+ function parseReceizKeyFileObject(value) {
281
+ const record = asRecord(value);
282
+ if (!record)
283
+ throw new Error("receiz_key_invalid");
284
+ const schema = asTrimmedString(record.schema);
285
+ const name = asTrimmedString(record.name);
286
+ const version = typeof record.version === "number" ? Math.trunc(record.version) : Number.NaN;
287
+ const keyId = normalizeIdentityKeyId(record.keyId);
288
+ const alg = normalizeReceizIdentityKeyAlgorithm(record.alg);
289
+ const issuedAt = asTrimmedString(record.issuedAt);
290
+ const ownerRaw = asRecord(record.owner);
291
+ const cryptoRaw = asRecord(record.crypto);
292
+ const kdfRaw = asRecord(cryptoRaw?.kdf);
293
+ const cipherRaw = asRecord(cryptoRaw?.cipher);
294
+ if (schema !== RECEIZ_KEY_SCHEMA || name !== RECEIZ_KEY_NAME || version !== 1 || !keyId || !alg || !issuedAt) {
295
+ throw new Error("receiz_key_invalid");
296
+ }
297
+ if (!ownerRaw || !cryptoRaw || !kdfRaw || !cipherRaw)
298
+ throw new Error("receiz_key_invalid");
299
+ const portableStateRecord = asRecord(record.portableState);
300
+ const portableState = portableStateRecord
301
+ ? {
302
+ schema: asTrimmedString(portableStateRecord.schema) || "receiz.account.state.v1",
303
+ exportedAt: asTrimmedString(portableStateRecord.exportedAt) || issuedAt,
304
+ snapshot: portableStateRecord.snapshot ?? null,
305
+ proof: normalizePortableStateProof(portableStateRecord.proof),
306
+ }
307
+ : null;
308
+ const attestationRecord = asRecord(record.attestation);
309
+ const attestation = attestationRecord
310
+ ? {
311
+ pbiDecision: asTrimmedString(attestationRecord.pbiDecision),
312
+ pbiReceiptHash: asTrimmedString(attestationRecord.pbiReceiptHash).toLowerCase(),
313
+ pbiChallengeId: asTrimmedString(attestationRecord.pbiChallengeId),
314
+ pbiRequestId: asNullableTrimmedString(attestationRecord.pbiRequestId),
315
+ attestedAt: asTrimmedString(attestationRecord.attestedAt),
316
+ }
317
+ : null;
318
+ return ensureKeyFileShape({
319
+ schema: RECEIZ_KEY_SCHEMA,
320
+ name: RECEIZ_KEY_NAME,
321
+ version: RECEIZ_KEYFILE_VERSION,
322
+ issuedAt,
323
+ keyId,
324
+ alg,
325
+ owner: normalizeOwner({
326
+ uid: asTrimmedString(ownerRaw.uid),
327
+ email: asNullableTrimmedString(ownerRaw.email),
328
+ username: asNullableTrimmedString(ownerRaw.username),
329
+ displayName: asNullableTrimmedString(ownerRaw.displayName),
330
+ }),
331
+ crypto: {
332
+ publicKeyRawB64u: asTrimmedString(cryptoRaw.publicKeyRawB64u),
333
+ privateKeyPkcs8CiphertextB64u: asTrimmedString(cryptoRaw.privateKeyPkcs8CiphertextB64u),
334
+ privateKeyPkcs8B64u: asNullableTrimmedString(cryptoRaw.privateKeyPkcs8B64u),
335
+ kdf: {
336
+ name: asTrimmedString(kdfRaw.name),
337
+ iterations: Number(kdfRaw.iterations),
338
+ saltB64u: asTrimmedString(kdfRaw.saltB64u),
339
+ },
340
+ cipher: {
341
+ name: asTrimmedString(cipherRaw.name),
342
+ ivB64u: asTrimmedString(cipherRaw.ivB64u),
343
+ aad: asTrimmedString(cipherRaw.aad),
344
+ },
345
+ },
346
+ attestation,
347
+ portableState,
348
+ });
349
+ }
350
+ export function parseReceizIdentityArtifactText(text) {
351
+ if (utf8(text).byteLength > RECEIZ_KEYFILE_MAX_BYTES)
352
+ throw new Error("receiz_key_file_too_large");
353
+ try {
354
+ return parseReceizKeyFileObject(JSON.parse(text));
355
+ }
356
+ catch (error) {
357
+ if (error instanceof Error && (error.message === "receiz_key_file_too_large" || error.message === "receiz_key_invalid"))
358
+ throw error;
359
+ throw new Error("receiz_key_invalid");
360
+ }
361
+ }
362
+ function isPngBytes(bytes) {
363
+ if (bytes.byteLength < PNG_SIGNATURE.byteLength)
364
+ return false;
365
+ for (let i = 0; i < PNG_SIGNATURE.byteLength; i += 1) {
366
+ if (bytes[i] !== PNG_SIGNATURE[i])
367
+ return false;
368
+ }
369
+ return true;
370
+ }
371
+ function readUint32BE(bytes, offset) {
372
+ return (((bytes[offset] ?? 0) << 24) | ((bytes[offset + 1] ?? 0) << 16) | ((bytes[offset + 2] ?? 0) << 8) | (bytes[offset + 3] ?? 0)) >>> 0;
373
+ }
374
+ function chunkType(bytes, offset) {
375
+ return String.fromCharCode(bytes[offset] ?? 0, bytes[offset + 1] ?? 0, bytes[offset + 2] ?? 0, bytes[offset + 3] ?? 0);
376
+ }
377
+ function pngIendEndOffset(bytes) {
378
+ if (!isPngBytes(bytes))
379
+ return null;
380
+ let offset = PNG_SIGNATURE.byteLength;
381
+ while (offset + 12 <= bytes.byteLength) {
382
+ const length = readUint32BE(bytes, offset);
383
+ const type = chunkType(bytes, offset + 4);
384
+ const chunkEnd = offset + 8 + length + 4;
385
+ if (chunkEnd > bytes.byteLength)
386
+ return null;
387
+ if (type === "IEND")
388
+ return chunkEnd;
389
+ offset = chunkEnd;
390
+ }
391
+ return null;
392
+ }
393
+ function indexOfBytes(haystack, needle, start = 0) {
394
+ const lastStart = haystack.byteLength - needle.byteLength;
395
+ for (let i = Math.max(0, start); i <= lastStart; i += 1) {
396
+ let matched = true;
397
+ for (let j = 0; j < needle.byteLength; j += 1) {
398
+ if (haystack[i + j] !== needle[j]) {
399
+ matched = false;
400
+ break;
401
+ }
402
+ }
403
+ if (matched)
404
+ return i;
405
+ }
406
+ return -1;
407
+ }
408
+ function indexOfNull(bytes, start) {
409
+ for (let i = start; i < bytes.length; i += 1) {
410
+ if (bytes[i] === 0)
411
+ return i;
412
+ }
413
+ return -1;
414
+ }
415
+ function parseTextChunkData(data) {
416
+ const keywordEnd = indexOfNull(data, 0);
417
+ if (keywordEnd <= 0)
418
+ return null;
419
+ return {
420
+ keyword: decodeUtf8(data.slice(0, keywordEnd)),
421
+ text: decodeUtf8(data.slice(keywordEnd + 1)),
422
+ };
423
+ }
424
+ function parseITXtChunkData(data) {
425
+ const keywordEnd = indexOfNull(data, 0);
426
+ if (keywordEnd <= 0)
427
+ return null;
428
+ const keyword = decodeUtf8(data.slice(0, keywordEnd));
429
+ let cursor = keywordEnd + 1;
430
+ if (cursor + 2 > data.length)
431
+ return null;
432
+ const compressionFlag = data[cursor] ?? 0;
433
+ const compressionMethod = data[cursor + 1] ?? 0;
434
+ cursor += 2;
435
+ if (compressionFlag !== 0 || compressionMethod !== 0)
436
+ return null;
437
+ const langEnd = indexOfNull(data, cursor);
438
+ if (langEnd < 0)
439
+ return null;
440
+ cursor = langEnd + 1;
441
+ const translatedEnd = indexOfNull(data, cursor);
442
+ if (translatedEnd < 0)
443
+ return null;
444
+ cursor = translatedEnd + 1;
445
+ return { keyword, text: decodeUtf8(data.slice(cursor)) };
446
+ }
447
+ function readPngTextChunk(bytes, keyword) {
448
+ if (!isPngBytes(bytes))
449
+ return null;
450
+ let offset = PNG_SIGNATURE.byteLength;
451
+ while (offset + 12 <= bytes.byteLength) {
452
+ const length = readUint32BE(bytes, offset);
453
+ const type = chunkType(bytes, offset + 4);
454
+ const dataStart = offset + 8;
455
+ const dataEnd = dataStart + length;
456
+ if (dataEnd + 4 > bytes.byteLength)
457
+ break;
458
+ const data = bytes.slice(dataStart, dataEnd);
459
+ const parsed = type === "iTXt" ? parseITXtChunkData(data) : type === "tEXt" ? parseTextChunkData(data) : null;
460
+ if (parsed?.keyword === keyword)
461
+ return parsed.text;
462
+ offset = dataEnd + 4;
463
+ if (type === "IEND")
464
+ break;
465
+ }
466
+ return null;
467
+ }
468
+ function readReceizIdentityRecordKeyTrailer(pngBytes) {
469
+ const iendEnd = pngIendEndOffset(pngBytes);
470
+ if (iendEnd === null || iendEnd >= pngBytes.byteLength)
471
+ return null;
472
+ const trailerBytes = pngBytes.slice(iendEnd);
473
+ const prefixBytes = utf8(RECEIZ_IDENTITY_RECORD_KEY_TRAILER_PREFIX);
474
+ const suffixBytes = utf8(RECEIZ_IDENTITY_RECORD_KEY_TRAILER_SUFFIX);
475
+ const prefixStart = indexOfBytes(trailerBytes, prefixBytes);
476
+ if (prefixStart < 0)
477
+ return null;
478
+ const encodedStart = prefixStart + prefixBytes.byteLength;
479
+ const suffixStart = indexOfBytes(trailerBytes, suffixBytes, encodedStart);
480
+ if (suffixStart < 0)
481
+ return null;
482
+ const encoded = decodeUtf8(trailerBytes.slice(encodedStart, suffixStart)).trim();
483
+ if (!encoded || !BASE64URL_RE.test(encoded))
484
+ return null;
485
+ try {
486
+ return decodeUtf8(receizBase64UrlDecode(encoded));
487
+ }
488
+ catch {
489
+ return null;
490
+ }
491
+ }
492
+ export function appendReceizIdentityArtifactTrailerToPng(pngBytes, keyFile) {
493
+ const iendEnd = pngIendEndOffset(pngBytes);
494
+ if (iendEnd === null)
495
+ throw new Error("receiz_key_identity_record_png_required");
496
+ const encoded = receizBase64UrlEncode(utf8(serializeReceizIdentityArtifact(keyFile)));
497
+ const trailer = utf8(`${RECEIZ_IDENTITY_RECORD_KEY_TRAILER_PREFIX}${encoded}${RECEIZ_IDENTITY_RECORD_KEY_TRAILER_SUFFIX}`);
498
+ const pngBasis = pngBytes.slice(0, iendEnd);
499
+ const out = new Uint8Array(pngBasis.byteLength + trailer.byteLength);
500
+ out.set(pngBasis, 0);
501
+ out.set(trailer, pngBasis.byteLength);
502
+ return out;
503
+ }
504
+ async function inputToBytes(input) {
505
+ if (typeof input === "string")
506
+ return utf8(input);
507
+ if (input instanceof Uint8Array)
508
+ return input;
509
+ if (input instanceof ArrayBuffer)
510
+ return new Uint8Array(input);
511
+ if (typeof Blob !== "undefined" && input instanceof Blob) {
512
+ if (input.size > RECEIZ_KEYFILE_MAX_BYTES)
513
+ throw new Error("receiz_key_file_too_large");
514
+ return new Uint8Array(await input.arrayBuffer());
515
+ }
516
+ throw new Error("receiz_key_file_required");
517
+ }
518
+ export async function readReceizIdentityArtifact(input) {
519
+ const bytes = await inputToBytes(input);
520
+ if (bytes.byteLength > RECEIZ_KEYFILE_MAX_BYTES)
521
+ throw new Error("receiz_key_file_too_large");
522
+ if (isPngBytes(bytes)) {
523
+ const embedded = readReceizIdentityRecordKeyTrailer(bytes) ?? readPngTextChunk(bytes, RECEIZ_IDENTITY_RECORD_KEY_CHUNK_KEY);
524
+ if (!embedded)
525
+ throw new Error("receiz_key_identity_record_missing");
526
+ return parseReceizIdentityArtifactText(embedded);
527
+ }
528
+ return parseReceizIdentityArtifactText(decodeUtf8(bytes));
529
+ }
530
+ export function serializeReceizIdentityArtifact(keyFile) {
531
+ return `${JSON.stringify(ensureKeyFileShape(keyFile), null, 2)}\n`;
532
+ }
533
+ export async function deriveReceizIdentityKeyIdFromPublicKeyRawB64u(publicKeyRawB64u, alg = "Ed25519") {
534
+ const normalized = normalizeBase64Url(publicKeyRawB64u);
535
+ if (!normalized)
536
+ throw new Error("invalid_public_key");
537
+ const bytes = receizBase64UrlDecode(normalized);
538
+ if (!validatePublicKeyRawBytesForAlg(bytes, alg))
539
+ throw new Error("invalid_public_key_length");
540
+ return sha256Hex(bytes);
541
+ }
542
+ async function normalizePortableState(args) {
543
+ if (!args.portableState)
544
+ return null;
545
+ const schema = asTrimmedString(args.portableState.schema) || RECEIZ_ACCOUNT_STATE_SCHEMA;
546
+ const exportedAt = new Date().toISOString();
547
+ const canonical = jcsCanonicalize({
548
+ schema,
549
+ exportedAt,
550
+ snapshot: args.portableState.snapshot,
551
+ });
552
+ const canonicalBytes = utf8(canonical);
553
+ const digest = new Uint8Array(await getSubtle().digest("SHA-256", toStrictArrayBuffer(canonicalBytes)));
554
+ const signature = new Uint8Array(await getSubtle().sign(keySignVerifyAlgorithmForAlg(args.alg), args.privateKey, toStrictArrayBuffer(canonicalBytes)));
555
+ return {
556
+ schema,
557
+ exportedAt,
558
+ snapshot: args.portableState.snapshot,
559
+ proof: {
560
+ method: portableStateProofMethodForAlg(args.alg),
561
+ digestSha256Hex: bytesToHex(digest),
562
+ signatureB64u: receizBase64UrlEncode(signature),
563
+ signedAt: exportedAt,
564
+ },
565
+ };
566
+ }
567
+ export async function createReceizIdentityKeyFile(args) {
568
+ const subtle = getSubtle();
569
+ const passphrase = normalizePassphrase(args.passphrase ?? "");
570
+ if (passphrase && passphrase.length < RECEIZ_KEYFILE_MIN_PASSPHRASE_LENGTH)
571
+ throw new Error("receiz_key_passphrase_too_short");
572
+ let keyPair;
573
+ let alg = "Ed25519";
574
+ try {
575
+ keyPair = (await subtle.generateKey(keyGenerationAlgorithmForAlg("Ed25519"), true, ["sign", "verify"]));
576
+ }
577
+ catch {
578
+ if (args.allowP256Fallback === false)
579
+ throw new Error("identity_key_not_supported");
580
+ alg = "P-256";
581
+ keyPair = (await subtle.generateKey(keyGenerationAlgorithmForAlg(alg), true, ["sign", "verify"]));
582
+ }
583
+ const publicKeyRaw = new Uint8Array(await subtle.exportKey("raw", keyPair.publicKey));
584
+ const privateKeyPkcs8 = new Uint8Array(await subtle.exportKey("pkcs8", keyPair.privateKey));
585
+ const keyId = await sha256Hex(publicKeyRaw);
586
+ const publicKeyRawB64u = receizBase64UrlEncode(publicKeyRaw);
587
+ const iterations = normalizedIterations(args.pbkdf2Iterations);
588
+ const salt = randomBytes(16);
589
+ const iv = randomBytes(12);
590
+ const aad = keyAad(keyId);
591
+ let privateKeyPkcs8CiphertextB64u = "";
592
+ let privateKeyPkcs8B64u = null;
593
+ if (passphrase) {
594
+ const aesKey = await deriveAesGcmKey({ passphrase, salt, iterations });
595
+ const ciphertext = new Uint8Array(await subtle.encrypt({ name: "AES-GCM", iv: toStrictArrayBuffer(iv), additionalData: toStrictArrayBuffer(utf8(aad)) }, aesKey, toStrictArrayBuffer(privateKeyPkcs8)));
596
+ privateKeyPkcs8CiphertextB64u = receizBase64UrlEncode(ciphertext);
597
+ }
598
+ else {
599
+ privateKeyPkcs8B64u = receizBase64UrlEncode(privateKeyPkcs8);
600
+ }
601
+ const keyFile = ensureKeyFileShape({
602
+ schema: RECEIZ_KEY_SCHEMA,
603
+ name: RECEIZ_KEY_NAME,
604
+ version: RECEIZ_KEYFILE_VERSION,
605
+ issuedAt: asTrimmedString(args.issuedAt) || new Date().toISOString(),
606
+ keyId,
607
+ alg,
608
+ owner: normalizeOwner(args.owner),
609
+ crypto: {
610
+ publicKeyRawB64u,
611
+ privateKeyPkcs8CiphertextB64u,
612
+ privateKeyPkcs8B64u,
613
+ kdf: {
614
+ name: "PBKDF2-SHA256",
615
+ iterations,
616
+ saltB64u: receizBase64UrlEncode(salt),
617
+ },
618
+ cipher: {
619
+ name: "AES-GCM-256",
620
+ ivB64u: receizBase64UrlEncode(iv),
621
+ aad,
622
+ },
623
+ },
624
+ attestation: null,
625
+ portableState: await normalizePortableState({
626
+ portableState: args.portableState,
627
+ privateKey: keyPair.privateKey,
628
+ alg,
629
+ }),
630
+ });
631
+ return { keyFile, keyId, publicKeyRawB64u, alg };
632
+ }
633
+ function randomToken(size = 18) {
634
+ return receizBase64UrlEncode(randomBytes(size)).toLowerCase().replace(/[^a-z0-9]/g, "");
635
+ }
636
+ function normalizeReceizUsername(value, fallback) {
637
+ const normalized = (value ?? "")
638
+ .trim()
639
+ .toLowerCase()
640
+ .replace(/[^a-z0-9_]+/g, "_")
641
+ .replace(/^_+|_+$/g, "")
642
+ .slice(0, 30);
643
+ return normalized || fallback;
644
+ }
645
+ export async function createReceizIdIdentity(options = {}) {
646
+ const createdAt = asTrimmedString(options.createdAt) || new Date().toISOString();
647
+ const token = randomToken(18);
648
+ const localUid = `receiz_device_${token}`.slice(0, 64);
649
+ const generatedUsername = `receiz_${token.slice(0, 18)}`.slice(0, 30);
650
+ const username = normalizeReceizUsername(options.username, generatedUsername);
651
+ const displayName = asTrimmedString(options.displayName) || "Receiz ID";
652
+ const deviceName = asNullableTrimmedString(options.deviceName);
653
+ const created = await createReceizIdentityKeyFile({
654
+ passphrase: options.passphrase,
655
+ allowP256Fallback: options.allowP256Fallback ?? true,
656
+ pbkdf2Iterations: options.pbkdf2Iterations,
657
+ owner: {
658
+ uid: localUid,
659
+ email: null,
660
+ username,
661
+ displayName,
662
+ },
663
+ portableState: {
664
+ schema: RECEIZ_ACCOUNT_STATE_SCHEMA,
665
+ snapshot: {
666
+ schema: RECEIZ_DEVICE_IDENTITY_SCHEMA,
667
+ createdAt,
668
+ account: {
669
+ userId: localUid,
670
+ email: null,
671
+ username,
672
+ displayName,
673
+ },
674
+ identityKey: {
675
+ keyId: "pending",
676
+ authority: "receiz-device",
677
+ },
678
+ },
679
+ },
680
+ issuedAt: createdAt,
681
+ });
682
+ return {
683
+ schema: RECEIZ_DEVICE_IDENTITY_SCHEMA,
684
+ createdAt,
685
+ updatedAt: createdAt,
686
+ localUid,
687
+ username,
688
+ displayName,
689
+ deviceName,
690
+ keyFile: created.keyFile,
691
+ };
692
+ }
693
+ async function decryptPrivateKeyPkcs8Bytes(args) {
694
+ const keyFile = ensureKeyFileShape(args.keyFile);
695
+ const privateKeyPkcs8B64u = normalizeOptionalBase64Url(keyFile.crypto.privateKeyPkcs8B64u);
696
+ if (privateKeyPkcs8B64u)
697
+ return receizBase64UrlDecode(privateKeyPkcs8B64u);
698
+ const passphrase = normalizePassphrase(args.passphrase ?? "");
699
+ if (!passphrase)
700
+ throw new Error("receiz_key_legacy_passphrase_required");
701
+ const privateCiphertextB64u = normalizeOptionalBase64Url(keyFile.crypto.privateKeyPkcs8CiphertextB64u);
702
+ if (!privateCiphertextB64u)
703
+ throw new Error("receiz_key_invalid");
704
+ try {
705
+ const key = await deriveAesGcmKey({
706
+ passphrase,
707
+ salt: receizBase64UrlDecode(keyFile.crypto.kdf.saltB64u),
708
+ iterations: normalizedIterations(keyFile.crypto.kdf.iterations),
709
+ });
710
+ const plaintext = await getSubtle().decrypt({
711
+ name: "AES-GCM",
712
+ iv: toStrictArrayBuffer(receizBase64UrlDecode(keyFile.crypto.cipher.ivB64u)),
713
+ additionalData: toStrictArrayBuffer(utf8(keyFile.crypto.cipher.aad || keyAad(keyFile.keyId))),
714
+ }, key, toStrictArrayBuffer(receizBase64UrlDecode(privateCiphertextB64u)));
715
+ return new Uint8Array(plaintext);
716
+ }
717
+ catch {
718
+ throw new Error("receiz_key_decrypt_failed");
719
+ }
720
+ }
721
+ export async function decryptReceizIdentityPrivateSigningKey(args) {
722
+ const keyFile = ensureKeyFileShape(args.keyFile);
723
+ const derivedKeyId = await deriveReceizIdentityKeyIdFromPublicKeyRawB64u(keyFile.crypto.publicKeyRawB64u, keyFile.alg);
724
+ if (derivedKeyId !== keyFile.keyId)
725
+ throw new Error("receiz_key_invalid");
726
+ try {
727
+ return await getSubtle().importKey("pkcs8", toStrictArrayBuffer(await decryptPrivateKeyPkcs8Bytes({ keyFile, passphrase: args.passphrase })), keyImportAlgorithmForAlg(keyFile.alg), false, ["sign"]);
728
+ }
729
+ catch {
730
+ throw new Error("receiz_key_decrypt_failed");
731
+ }
732
+ }
733
+ export async function signReceizIdentityLoginProof(input) {
734
+ const keyFile = ensureKeyFileShape(input.keyFile);
735
+ const challengeB64Url = normalizeBase64Url(input.challengeB64Url) ?? (input.challengeText ? receizBase64UrlEncode(utf8(input.challengeText)) : null);
736
+ if (!challengeB64Url)
737
+ throw new Error("identity_key_challenge_invalid");
738
+ const privateKey = await decryptReceizIdentityPrivateSigningKey({ keyFile, passphrase: input.passphrase });
739
+ const signature = new Uint8Array(await getSubtle().sign(keySignVerifyAlgorithmForAlg(keyFile.alg), privateKey, toStrictArrayBuffer(receizBase64UrlDecode(challengeB64Url))));
740
+ return {
741
+ schema: RECEIZ_IDENTITY_LOGIN_PROOF_SCHEMA,
742
+ keyId: keyFile.keyId,
743
+ alg: keyFile.alg,
744
+ challengeB64Url,
745
+ signatureB64Url: receizBase64UrlEncode(signature),
746
+ };
747
+ }
748
+ export async function verifyReceizIdentityLoginProof(args) {
749
+ const keyFile = ensureKeyFileShape(args.keyFile);
750
+ const challengeB64Url = normalizeBase64Url(args.challengeB64Url);
751
+ const signatureB64Url = normalizeBase64Url(args.signatureB64Url);
752
+ if (!challengeB64Url || !signatureB64Url)
753
+ return false;
754
+ const derivedKeyId = await deriveReceizIdentityKeyIdFromPublicKeyRawB64u(keyFile.crypto.publicKeyRawB64u, keyFile.alg);
755
+ if (derivedKeyId !== keyFile.keyId)
756
+ return false;
757
+ try {
758
+ const publicKey = await getSubtle().importKey("raw", toStrictArrayBuffer(receizBase64UrlDecode(keyFile.crypto.publicKeyRawB64u)), keyImportAlgorithmForAlg(keyFile.alg), false, ["verify"]);
759
+ return await getSubtle().verify(keySignVerifyAlgorithmForAlg(keyFile.alg), publicKey, toStrictArrayBuffer(receizBase64UrlDecode(signatureB64Url)), toStrictArrayBuffer(receizBase64UrlDecode(challengeB64Url)));
760
+ }
761
+ catch {
762
+ return false;
763
+ }
764
+ }
765
+ export async function verifyReceizIdentityPortableStateProof(keyFileInput) {
766
+ const keyFile = ensureKeyFileShape(keyFileInput);
767
+ const envelope = keyFile.portableState;
768
+ if (!envelope?.proof)
769
+ return "missing";
770
+ if (envelope.proof.method !== portableStateProofMethodForAlg(keyFile.alg))
771
+ return "invalid";
772
+ const digestSha256Hex = asTrimmedString(envelope.proof.digestSha256Hex).toLowerCase();
773
+ const signatureB64u = normalizeBase64Url(envelope.proof.signatureB64u);
774
+ if (!HEX64_RE.test(digestSha256Hex) || !signatureB64u)
775
+ return "invalid";
776
+ const canonical = jcsCanonicalize({
777
+ schema: asTrimmedString(envelope.schema) || "receiz.account.state.v1",
778
+ exportedAt: asTrimmedString(envelope.exportedAt) || keyFile.issuedAt,
779
+ snapshot: envelope.snapshot,
780
+ });
781
+ const digest = bytesToHex(new Uint8Array(await getSubtle().digest("SHA-256", toStrictArrayBuffer(utf8(canonical)))));
782
+ if (digest !== digestSha256Hex)
783
+ return "invalid";
784
+ return (await verifyReceizIdentityLoginProof({
785
+ keyFile,
786
+ challengeB64Url: receizBase64UrlEncode(utf8(canonical)),
787
+ signatureB64Url: signatureB64u,
788
+ }))
789
+ ? "verified"
790
+ : "invalid";
791
+ }
792
+ function challengeNonce(options) {
793
+ const supplied = normalizeBase64Url(options.nonceB64Url);
794
+ if (supplied && receizBase64UrlDecode(supplied).byteLength >= 16)
795
+ return supplied;
796
+ return receizBase64UrlEncode(randomBytes(16));
797
+ }
798
+ function createReceizDeviceChallengeB64Url(identity, options) {
799
+ const issuedAtMs = Number.isFinite(options.nowMs) ? Math.trunc(options.nowMs ?? Date.now()) : Date.now();
800
+ return receizBase64UrlEncode(utf8([
801
+ "RECEIZ_DEVICE_SESSION_V1",
802
+ `keyId=${identity.keyFile.keyId}`,
803
+ `localUid=${identity.localUid}`,
804
+ `createdAt=${identity.createdAt}`,
805
+ `issuedAtMs=${issuedAtMs}`,
806
+ `nonce=${challengeNonce(options)}`,
807
+ ].join("\n")));
808
+ }
809
+ export async function buildReceizIdContinueRequest(identity, options = {}) {
810
+ const keyFile = ensureKeyFileShape(identity.keyFile);
811
+ const challengeB64Url = createReceizDeviceChallengeB64Url({ ...identity, keyFile }, options);
812
+ const signed = await signReceizIdentityLoginProof({ keyFile, challengeB64Url, passphrase: options.passphrase });
813
+ return {
814
+ keyId: keyFile.keyId,
815
+ alg: keyFile.alg,
816
+ publicKeyRawB64u: keyFile.crypto.publicKeyRawB64u,
817
+ localUid: identity.localUid,
818
+ username: identity.username,
819
+ displayName: identity.displayName,
820
+ deviceName: identity.deviceName,
821
+ createdAt: identity.createdAt,
822
+ challengeB64Url,
823
+ signatureB64Url: signed.signatureB64Url,
824
+ ...(options.next ? { next: options.next } : {}),
825
+ };
826
+ }
827
+ function domainPresent(snapshot, keys) {
828
+ if (!snapshot)
829
+ return false;
830
+ return keys.some((key) => snapshot[key] !== undefined && snapshot[key] !== null);
831
+ }
832
+ export async function projectReceizIdentityAccount(keyFileInput) {
833
+ const keyFile = ensureKeyFileShape(keyFileInput);
834
+ const portableStateStatus = await verifyReceizIdentityPortableStateProof(keyFile);
835
+ const snapshotRecord = asRecord(keyFile.portableState?.snapshot);
836
+ return {
837
+ schema: RECEIZ_IDENTITY_ACCOUNT_PROJECTION_SCHEMA,
838
+ keyId: keyFile.keyId,
839
+ alg: keyFile.alg,
840
+ owner: keyFile.owner,
841
+ accountStateSchema: keyFile.portableState?.schema ?? null,
842
+ portableStateVerified: portableStateStatus === "verified",
843
+ portableStateStatus,
844
+ snapshot: keyFile.portableState?.snapshot ?? null,
845
+ domains: {
846
+ profile: domainPresent(snapshotRecord, ["profile", "account", "profiles"]),
847
+ actionLedger: domainPresent(snapshotRecord, ["actionLedger", "actionLedgerEntries", "ledgerActions"]),
848
+ calendar: domainPresent(snapshotRecord, ["calendar", "calendarEvents", "calendarRecurrences"]),
849
+ wallet: domainPresent(snapshotRecord, ["wallet", "walletLedgerEntries", "walletNotes", "walletChatTransfers"]),
850
+ sports: domainPresent(snapshotRecord, ["sports", "sportsVaultCards", "sportsArenaCardAppends", "sportsArenaEventProofs"]),
851
+ },
852
+ };
853
+ }