@muhkoo/connect 0.14.0-alpha.7 → 0.14.0-alpha.9

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/connect.d.ts CHANGED
@@ -535,116 +535,6 @@ declare class DoubleRatchet {
535
535
  setState(state: RatchetState): void;
536
536
  }
537
537
 
538
- type SnarkProof = Groth16Proof;
539
-
540
- interface ZkCompiled {
541
- verificationKey: VerificationKey;
542
- }
543
- declare class AuthPublicInput {
544
- commitment: string;
545
- nonce: string;
546
- ecdsaPubHash: string;
547
- constructor(commitment: string, nonce: string, ecdsaPubHash: string);
548
- toPublicSignals(): string[];
549
- }
550
- declare class Field {
551
- private value;
552
- static readonly FIELD_SIZE: bigint;
553
- constructor(value: bigint | string | number);
554
- toBigInt(): bigint;
555
- toString(): string;
556
- toHex(): string;
557
- equals(other: Field): boolean;
558
- static fromHex(hex: string): Field;
559
- }
560
- declare class Poseidon {
561
- static hash(inputs: Field[]): Promise<Field>;
562
- }
563
- declare function encodeToHex(field: Field): string;
564
- declare function decodeFromHex(hexStr: string): Field;
565
- interface CircuitBufferConfig {
566
- wasm: Uint8Array;
567
- zkey: Uint8Array;
568
- verificationKey: VerificationKey;
569
- }
570
- declare class HashKnowledge {
571
- private static wasmBuffer?;
572
- private static zkeyBuffer?;
573
- private static verificationKey?;
574
- static setCircuitBuffers(wasm: Uint8Array, zkey: Uint8Array, verificationKey: VerificationKey): void;
575
- static compile(): Promise<ZkCompiled>;
576
- static prove(secret: Field): Promise<{
577
- proof: SnarkProof;
578
- publicSignals: string[];
579
- }>;
580
- static verify(proof: SnarkProof, publicSignals?: string[]): Promise<boolean>;
581
- }
582
- declare class PreimagePoK {
583
- private static wasmBuffer?;
584
- private static zkeyBuffer?;
585
- private static verificationKey?;
586
- static setCircuitBuffers(wasm: Uint8Array, zkey: Uint8Array, verificationKey: VerificationKey): void;
587
- static compile(): Promise<ZkCompiled>;
588
- static prove(publicInput: AuthPublicInput, secret: Field, salt: Field, ecdsaPub: Field): Promise<{
589
- proof: SnarkProof;
590
- publicSignals: string[];
591
- }>;
592
- static verify(proof: SnarkProof, publicSignals?: string[]): Promise<boolean>;
593
- }
594
- declare function verifyHashKnowledge(proof: SnarkProof, expectedOutput?: Field, publicSignals?: string[]): Promise<boolean>;
595
- declare function verifyPreimagePoK(proof: SnarkProof, publicSignals?: string[]): Promise<boolean>;
596
- declare function verify(programType: 'HashKnowledge' | 'PreimagePoK', proof: SnarkProof, options?: {
597
- expectedOutput?: Field;
598
- publicSignals?: string[];
599
- }): Promise<boolean>;
600
- declare function quickVerify(program: typeof HashKnowledge | typeof PreimagePoK, proof: SnarkProof, publicSignals?: string[]): Promise<boolean>;
601
- declare function initializeCircuits(config: {
602
- hashKnowledge?: CircuitBufferConfig;
603
- preimagePoK?: CircuitBufferConfig;
604
- }): void;
605
- declare function compilePrograms(): Promise<{
606
- hashKnowledge: ZkCompiled;
607
- preimagePoK: ZkCompiled;
608
- }>;
609
-
610
- interface AuthToken {
611
- peerId: string;
612
- timestamp: number;
613
- signature: string;
614
- }
615
- declare class Authenticator {
616
- private trustedKeys;
617
- private zkVerificationKey;
618
- initializeZK(): Promise<void>;
619
- getZKVerificationKey(): VerificationKey | null;
620
- addTrustedServer(serverId: string, publicKey: CryptoKey): void;
621
- generateAuthToken(peerId: string, privateKey: CryptoKey): Promise<AuthToken>;
622
- verifyAuthToken(token: AuthToken, clientPublicKey: CryptoKey): Promise<boolean>;
623
- verifyZKProof(proof: any, publicInput: AuthPublicInput, storedCommitment: Field, ecdsaPub: CryptoKey): Promise<boolean>;
624
- }
625
-
626
- declare class DoubleRatchetManager {
627
- private sessions;
628
- private sessionStates;
629
- private keyStore;
630
- private _authenticator;
631
- private registeredUsers;
632
- private id;
633
- constructor(id: string);
634
- get authenticator(): Authenticator;
635
- addTrustedServer(serverId: string, publicKey: CryptoKey): Promise<void>;
636
- registerZK(clientId: string, secret: Field, salt: Field, ecdsaPub: CryptoKey): Promise<void>;
637
- performHandshake(senderId: string, recipientId: string, zkProof: any, publicInput: AuthPublicInput, clientPublicKey: CryptoKey, // ECDH pub for session
638
- clientAuthPublicKey: CryptoKey, // ECDSA pub for auth
639
- authToken: AuthToken): Promise<void>;
640
- initializeSession(senderId: string, recipientId: string, isClient?: boolean, sessionType?: 'global' | 'specific', sessionId?: string, zkProof?: any, publicInput?: AuthPublicInput, clientPublicKey?: CryptoKey, clientAuthPublicKey?: CryptoKey, clientEcdhPrivateKey?: CryptoKey, clientAuthPrivateKey?: CryptoKey): Promise<string>;
641
- encrypt(senderId: string, recipientId: string, sessionId: string, plaintext: string, newDhKey?: boolean, sessionType?: 'global' | 'specific'): Promise<CipherMessage>;
642
- decrypt(message: CipherMessage, isClient?: boolean): Promise<string>;
643
- getSessionSharedSecret(sessionId: string): Promise<Uint8Array | null>;
644
- private saveState;
645
- private loadState;
646
- }
647
-
648
538
  /**
649
539
  * Per-chunk AES-256-GCM encryption with random keys and IVs.
650
540
  *
@@ -768,6 +658,11 @@ declare enum EventCoreEvents {
768
658
  */
769
659
  declare class EventCore {
770
660
  private static eventTarget;
661
+ /**
662
+ * `event -> (caller's handler -> the wrapper actually registered on the
663
+ * EventTarget)`. Two levels because one event can have many listeners and
664
+ * {@link off} must remove exactly the wrapper it registered for that caller.
665
+ */
771
666
  private static events;
772
667
  /**
773
668
  * @remarks Listen to an event.
@@ -928,7 +823,7 @@ declare class EncryptedSession {
928
823
  *
929
824
  * Hands raw frame strings up (via the `MESSAGE` event) and accepts raw frame
930
825
  * strings down (via `send`). Anything frame-shape, encryption, or serialization
931
- * related belongs in the layer above (e.g. `Network` for client↔server
826
+ * related belongs in the layer above (e.g. `BroadcastChannel` for client↔server
932
827
  * encrypted pipes, `BroadcastChannel` for multi-peer E2E rooms).
933
828
  *
934
829
  * Responsibilities:
@@ -1084,7 +979,7 @@ declare class WSTransport extends EventCore {
1084
979
  * anything else — passes through as `raw_frame`
1085
980
  * (the app decides what to do)
1086
981
  *
1087
- * **Event scoping.** Unlike `Network`, BroadcastChannel uses a per-instance
982
+ * **Event scoping.** BroadcastChannel uses a per-instance
1088
983
  * `EventTarget`, so two channels (e.g. two rooms in the same app) don't
1089
984
  * cross-contaminate each other's events.
1090
985
  *
@@ -1209,1202 +1104,1625 @@ declare function initBn128Wasm(wasmModule?: WebAssembly.Module): Promise<Bn128Wa
1209
1104
  */
1210
1105
  declare function verifyGroth16(wasmInstance: WebAssembly.Instance, memory: WebAssembly.Memory, initialPFree: number, verificationKey: VerificationKey, proof: Groth16Proof, publicSignals: string[]): Promise<boolean>;
1211
1106
 
1212
- /**
1213
- * `PersonalSpaceClient` — the client-side counterpart to the accelerator's
1214
- * `PersonalSpaceDO`. Wraps the `/api/personal/:commitment/*` HTTP protocol so
1215
- * callers can `put` / `get` / `delete` / `list` per-user KV blobs without
1216
- * thinking about challenges, nonces, or Groth16 proof generation.
1217
- *
1218
- * Every gated operation:
1219
- * 1. Requests a one-shot challenge from the DO (`POST /challenge`).
1220
- * 2. Converts the returned hex nonce into a BN254 field element.
1221
- * 3. Generates a fresh Groth16 proof for the `preimagePoK` circuit using
1222
- * snarkjs, with the user's secret/salt/ecdsaPub as private inputs and
1223
- * `{commitment, nonce, ecdsaPubHash}` as public signals.
1224
- * 4. POSTs the proof + publicSignals + (optional) value to the gated endpoint.
1225
- *
1226
- * snarkjs is loaded lazily (dynamic `import`) the first time a proof is
1227
- * generated — it's an optional peer dependency that only the ZK-proof path
1228
- * needs, so importing this module (or constructing a `Client`) never requires
1229
- * it. Consumers using the proof path install `snarkjs`, or provide it via an
1230
- * import map in the browser (see the README).
1231
- *
1232
- * Encryption of the stored values is the caller's responsibility — the DO
1233
- * sees only opaque JSON. The companion `wrap.ts` helpers in this folder are
1234
- * a convenient way to passphrase-encrypt a payload before putting it.
1235
- */
1236
- /**
1237
- * Constructor options for {@link PersonalSpaceClient}.
1238
- *
1239
- * All of `secret`, `salt`, `ecdsaPub`, and `ecdsaPubHash` are decimal-encoded
1240
- * BigInt strings (snarkjs convention). `commitment` is likewise decimal — it's
1241
- * the Poseidon hash of `(secret, salt, ecdsaPubHash)`.
1242
- */
1243
- interface PersonalSpaceClientOptions {
1244
- /** Base URL of the accelerator, e.g. `http://localhost:8787`. */
1245
- baseUrl: string;
1246
- /** The user's Poseidon commitment as a decimal string (~78 digits). */
1107
+ type SnarkProof = Groth16Proof;
1108
+
1109
+ interface ZkCompiled {
1110
+ verificationKey: VerificationKey;
1111
+ }
1112
+ declare class AuthPublicInput {
1247
1113
  commitment: string;
1248
- /** ZK identity material — private inputs to the proof. Decimal BigInt string. */
1249
- secret: string;
1250
- /** ZK identity salt. Decimal BigInt string. */
1251
- salt: string;
1252
- /** Raw ECDSA public-key field representation. Decimal BigInt string. */
1253
- ecdsaPub: string;
1254
- /** Poseidon hash of `ecdsaPub`, used as a public signal. Decimal BigInt string. */
1114
+ nonce: string;
1255
1115
  ecdsaPubHash: string;
1256
- /** Where the runtime can fetch the compiled circuit assets. */
1257
- circuits: {
1258
- /** URL of the `preimagePoK.wasm` witness generator. */
1259
- wasmUrl: string;
1260
- /** URL of the `preimagePoK_0001.zkey` proving key. */
1261
- zkeyUrl: string;
1262
- };
1116
+ constructor(commitment: string, nonce: string, ecdsaPubHash: string);
1117
+ toPublicSignals(): string[];
1263
1118
  }
1264
- declare class PersonalSpaceClient {
1265
- private readonly baseUrl;
1266
- private readonly commitment;
1267
- private readonly secret;
1268
- private readonly salt;
1269
- private readonly ecdsaPub;
1270
- private readonly ecdsaPubHash;
1271
- private readonly wasmUrl;
1272
- private readonly zkeyUrl;
1273
- constructor(opts: PersonalSpaceClientOptions);
1274
- /** Store `value` under `key`, replacing any prior value. */
1275
- put(key: string, value: unknown): Promise<void>;
1276
- /** Fetch the value stored under `key`. Returns `null` if absent. */
1277
- get<T = unknown>(key: string): Promise<T | null>;
1278
- /** Remove `key` from the user's space. Returns whether the key existed. */
1279
- delete(key: string): Promise<boolean>;
1280
- /** List every key currently stored in the user's space. */
1281
- list(): Promise<string[]>;
1282
- /**
1283
- * Run the full pre-flight: ask the DO for a challenge, reduce the nonce to
1284
- * a field element, and generate a fresh Groth16 proof. Returns everything
1285
- * the gated endpoints need in their body.
1286
- */
1287
- private proveFreshChallenge;
1288
- /** Hit `POST /challenge` and return the issued nonce + ID. */
1289
- private requestChallenge;
1290
- /**
1291
- * Thin wrapper over `globalThis.fetch` so all methods share a single set of
1292
- * headers and body-encoding behaviour. `globalThis.fetch` is available in
1293
- * modern browsers, Node 18+, Workers, and Bun.
1294
- */
1295
- private fetch;
1119
+ declare class Field {
1120
+ private value;
1121
+ static readonly FIELD_SIZE: bigint;
1122
+ constructor(value: bigint | string | number);
1123
+ toBigInt(): bigint;
1124
+ toString(): string;
1125
+ toHex(): string;
1126
+ equals(other: Field): boolean;
1127
+ static fromHex(hex: string): Field;
1296
1128
  }
1297
-
1298
- /**
1299
- * Wire types for the storage layer.
1300
- *
1301
- * The `FileManifest` is the single piece of metadata the system needs to
1302
- * reassemble a file: per-chunk encryption material, RS parameters, and the
1303
- * SHA-256 content hashes of every shard that belongs to the chunk. With the
1304
- * manifest in hand, a reader can pull each shard from the open content-
1305
- * addressed shard store, decode, decrypt, and concatenate to recover the
1306
- * original bytes.
1307
- *
1308
- * The manifest itself lives inside a `SharedSpaceDO` (Phase B) which gates
1309
- * access by ZK proof + ACL. Shards are stored in the open content-addressed
1310
- * `ShardStoreDO` — anyone can put/get any shard, but without the manifest
1311
- * (which has the keys) the ciphertext is just noise.
1312
- */
1313
-
1314
- /**
1315
- * One chunk's metadata. A file is the concatenation of `chunks` in order.
1316
- *
1317
- * - `originalSize` is the chunk's plaintext length, before AES-GCM expanded
1318
- * it by 16 bytes for the auth tag and before RS padded it for shard
1319
- * alignment.
1320
- * - `ciphertextSize` is the AES-GCM output length (plaintext + 16). This is
1321
- * what the RS decoder trims back to.
1322
- * - `shardHashes` lists every shard's SHA-256 in encoding order — the first
1323
- * `dataShards` are data shards, the trailing `parityShards` are parity.
1324
- */
1325
- interface ChunkManifest {
1326
- /** Stable per-chunk ID (uuid-like). Useful for client-side resume / dedup. */
1327
- id: string;
1328
- /** Position of this chunk in the file (0-indexed). */
1329
- chunkIndex: number;
1330
- /** Plaintext byte length of the chunk before encryption. */
1331
- originalSize: number;
1332
- /** AES-GCM ciphertext byte length (originalSize + 16 for the tag). */
1333
- ciphertextSize: number;
1334
- /** Number of RS data shards. */
1335
- dataShards: number;
1336
- /** Number of RS parity shards. */
1337
- parityShards: number;
1338
- /** Length in bytes of every shard (data and parity are equal). */
1339
- shardSize: number;
1340
- /** SHA-256 (hex) of each shard's ciphertext bytes, in shard order. */
1341
- shardHashes: string[];
1342
- /** Per-chunk AES-256-GCM key + IV. Plain in the manifest — gating happens at the SpaceDO. */
1343
- cipher: ChunkKeyMaterial;
1129
+ declare class Poseidon {
1130
+ static hash(inputs: Field[]): Promise<Field>;
1344
1131
  }
1345
- /**
1346
- * Top-level manifest for a single stored file. Persisted in the user's
1347
- * SharedSpace by file id; readable / writable per the space's ACL.
1348
- */
1349
- interface FileManifest {
1350
- /** Stable file ID. Assigned by the SDK at write time; used to read or delete. */
1351
- id: string;
1352
- /** Human-readable filename (display only — not used by the shard store). */
1353
- name: string;
1354
- /** Total plaintext byte length. Sum of chunk `originalSize`s. */
1355
- size: number;
1356
- /** MIME type the application supplied at write time. Opaque to storage. */
1357
- type: string;
1358
- /** Optional virtual path / folder hint for the application. */
1359
- path?: string;
1360
- /** When the file was last modified per the application (ms epoch). */
1361
- lastModified: number;
1362
- /** When this manifest was created on the SDK side (ms epoch). */
1363
- createdAt: number;
1364
- /** Ordered list of chunks. Concatenate them in index order to rebuild the file. */
1365
- chunks: ChunkManifest[];
1366
- /** SDK schema version. Bump on breaking manifest-shape changes. */
1367
- version: 1;
1132
+ declare function encodeToHex(field: Field): string;
1133
+ declare function decodeFromHex(hexStr: string): Field;
1134
+ interface CircuitBufferConfig {
1135
+ wasm: Uint8Array;
1136
+ zkey: Uint8Array;
1137
+ verificationKey: VerificationKey;
1368
1138
  }
1369
- /**
1370
- * Lightweight summary returned by `FileStorage.writeFile` and surfaced in
1371
- * directory-style listings. The full manifest is only fetched when actually
1372
- * reassembling the file.
1373
- */
1374
- interface FileStat {
1375
- id: string;
1376
- name: string;
1377
- size: number;
1378
- type: string;
1379
- path?: string;
1380
- lastModified: number;
1381
- createdAt: number;
1382
- chunkCount: number;
1383
- shardCount: number;
1139
+ declare class HashKnowledge {
1140
+ private static wasmBuffer?;
1141
+ private static zkeyBuffer?;
1142
+ private static verificationKey?;
1143
+ static setCircuitBuffers(wasm: Uint8Array, zkey: Uint8Array, verificationKey: VerificationKey): void;
1144
+ static compile(): Promise<ZkCompiled>;
1145
+ static prove(secret: Field): Promise<{
1146
+ proof: SnarkProof;
1147
+ publicSignals: string[];
1148
+ }>;
1149
+ static verify(proof: SnarkProof, publicSignals?: string[]): Promise<boolean>;
1150
+ }
1151
+ declare class PreimagePoK {
1152
+ private static wasmBuffer?;
1153
+ private static zkeyBuffer?;
1154
+ private static verificationKey?;
1155
+ static setCircuitBuffers(wasm: Uint8Array, zkey: Uint8Array, verificationKey: VerificationKey): void;
1156
+ static compile(): Promise<ZkCompiled>;
1157
+ static prove(publicInput: AuthPublicInput, secret: Field, salt: Field, ecdsaPub: Field): Promise<{
1158
+ proof: SnarkProof;
1159
+ publicSignals: string[];
1160
+ }>;
1161
+ static verify(proof: SnarkProof, publicSignals?: string[]): Promise<boolean>;
1162
+ }
1163
+ declare function verifyHashKnowledge(proof: SnarkProof, expectedOutput?: Field, publicSignals?: string[]): Promise<boolean>;
1164
+ declare function verifyPreimagePoK(proof: SnarkProof, publicSignals?: string[]): Promise<boolean>;
1165
+ declare function verify(programType: 'HashKnowledge' | 'PreimagePoK', proof: SnarkProof, options?: {
1166
+ expectedOutput?: Field;
1167
+ publicSignals?: string[];
1168
+ }): Promise<boolean>;
1169
+ declare function quickVerify(program: typeof HashKnowledge | typeof PreimagePoK, proof: SnarkProof, publicSignals?: string[]): Promise<boolean>;
1170
+ declare function initializeCircuits(config: {
1171
+ hashKnowledge?: CircuitBufferConfig;
1172
+ preimagePoK?: CircuitBufferConfig;
1173
+ }): void;
1174
+ declare function compilePrograms(): Promise<{
1175
+ hashKnowledge: ZkCompiled;
1176
+ preimagePoK: ZkCompiled;
1177
+ }>;
1178
+
1179
+ interface AuthToken {
1180
+ peerId: string;
1181
+ timestamp: number;
1182
+ signature: string;
1183
+ }
1184
+ declare class Authenticator {
1185
+ private trustedKeys;
1186
+ private zkVerificationKey;
1187
+ initializeZK(): Promise<void>;
1188
+ getZKVerificationKey(): VerificationKey | null;
1189
+ addTrustedServer(serverId: string, publicKey: CryptoKey): void;
1190
+ generateAuthToken(peerId: string, privateKey: CryptoKey): Promise<AuthToken>;
1191
+ verifyAuthToken(token: AuthToken, clientPublicKey: CryptoKey): Promise<boolean>;
1192
+ verifyZKProof(proof: any, publicInput: AuthPublicInput, storedCommitment: Field, ecdsaPub: CryptoKey): Promise<boolean>;
1193
+ }
1194
+
1195
+ declare class DoubleRatchetManager {
1196
+ private sessions;
1197
+ private sessionStates;
1198
+ private keyStore;
1199
+ private _authenticator;
1200
+ private registeredUsers;
1201
+ private id;
1202
+ constructor(id: string);
1203
+ get authenticator(): Authenticator;
1204
+ addTrustedServer(serverId: string, publicKey: CryptoKey): Promise<void>;
1205
+ registerZK(clientId: string, secret: Field, salt: Field, ecdsaPub: CryptoKey): Promise<void>;
1206
+ performHandshake(senderId: string, recipientId: string, zkProof: any, publicInput: AuthPublicInput, clientPublicKey: CryptoKey, // ECDH pub for session
1207
+ clientAuthPublicKey: CryptoKey, // ECDSA pub for auth
1208
+ authToken: AuthToken): Promise<void>;
1209
+ initializeSession(senderId: string, recipientId: string, isClient?: boolean, sessionType?: 'global' | 'specific', sessionId?: string, zkProof?: any, publicInput?: AuthPublicInput, clientPublicKey?: CryptoKey, clientAuthPublicKey?: CryptoKey, clientEcdhPrivateKey?: CryptoKey, clientAuthPrivateKey?: CryptoKey): Promise<string>;
1210
+ encrypt(senderId: string, recipientId: string, sessionId: string, plaintext: string, newDhKey?: boolean, sessionType?: 'global' | 'specific'): Promise<CipherMessage>;
1211
+ decrypt(message: CipherMessage, isClient?: boolean): Promise<string>;
1212
+ getSessionSharedSecret(sessionId: string): Promise<Uint8Array | null>;
1213
+ private saveState;
1214
+ private loadState;
1384
1215
  }
1385
- /** Distil a `FileManifest` into a `FileStat`. */
1386
- declare function manifestToStat(manifest: FileManifest): FileStat;
1387
1216
 
1388
1217
  /**
1389
- * HTTP client for the open content-addressed shard store.
1390
- *
1391
- * Each shard is named by the lowercase hex SHA-256 of its ciphertext bytes.
1392
- * The server verifies the hash on PUT, so the address authenticates the
1393
- * contents — there's no benefit to ACLing PUT/GET, and making them open
1394
- * enables cross-user dedup down the line.
1395
- *
1396
- * Wire protocol:
1397
- * PUT /api/shards/:hash body: shard bytes (application/octet-stream)
1398
- * 200 { ok: true, hash, size, dedup: boolean }
1399
- * 400 if hash doesn't match body
1218
+ * HTTP client for the accelerator's `/api/auth/*` endpoints.
1400
1219
  *
1401
- * GET /api/shards/:hash 200 with shard bytes (application/octet-stream)
1402
- * 404 if absent
1220
+ * Owns the wire shapes for register / challenge / authenticate / verify so
1221
+ * the React SPA (and any future consumers) don't reimplement them. The ZK
1222
+ * identity derivation + Groth16 proof generation stay on the application
1223
+ * side — this client handles only the round-trips.
1403
1224
  *
1404
- * HEAD /api/shards/:hash 200 if present (no body) — used by callers to
1405
- * skip a PUT they'd otherwise dedup-rewrite. The
1406
- * server can ignore the optimization if it wants.
1407
- */
1408
- /**
1409
- * A content-addressed byte cache for shards (the offline layer's `ShardCache`
1410
- * satisfies this structurally). Declared here as a minimal interface so the
1411
- * storage layer doesn't import the offline module — keeping the dependency
1412
- * arrow one-way (offline → storage).
1225
+ * Mirrors `PersonalSpaceClient` / `ShardClient` / `SharedSpaceClient`: takes
1226
+ * a `baseUrl`, optional custom `fetch`, throws typed `Error`s on failure
1227
+ * (with the worker's message inlined where available).
1413
1228
  */
1414
- interface ShardByteCache {
1415
- get(hash: string): Promise<Uint8Array | null>;
1416
- put(hash: string, bytes: Uint8Array): Promise<void>;
1229
+
1230
+ interface ZkRegisterRequest {
1231
+ username: string;
1232
+ /** Decimal-string Poseidon commitment. */
1233
+ commitment: string;
1234
+ /** Base64-encoded raw uncompressed ECDH public key (SEC1 0x04‖X‖Y). */
1235
+ ecdhPublicKey: string;
1236
+ /** Base64-encoded raw uncompressed ECDSA public key. */
1237
+ ecdsaPublicKey: string;
1238
+ email?: string | null;
1417
1239
  }
1418
- /**
1419
- * A peer block source (the p2p layer's `PeerExchange` satisfies this
1420
- * structurally). Consulted between the local cache and origin: a block fetched
1421
- * from a peer is verified by hash before use. Declared here so the storage layer
1422
- * doesn't import the p2p module (dependency arrow stays p2p → storage).
1423
- */
1424
- interface PeerBlockSource {
1425
- getBlock(hash: string, opts?: {
1426
- timeoutMs?: number;
1427
- }): Promise<Uint8Array | null>;
1428
- announce(hash: string): void;
1240
+ interface ZkRegisterResponse {
1241
+ success: boolean;
1429
1242
  }
1430
- interface ShardClientOptions {
1431
- /** Base URL of the accelerator (e.g. `https://muhkoo-accelerator.workers.dev`). */
1243
+ interface ZkChallengeResponse {
1244
+ challengeId: string;
1245
+ /** Hex-encoded random nonce — caller reduces into a BN254 field element for the proof. */
1246
+ nonce: string;
1247
+ /** Decimal-string Poseidon commitment the server has on file for this user. */
1248
+ commitment: string;
1249
+ }
1250
+ interface ZkAuthenticateRequest {
1251
+ challengeId: string;
1252
+ proof: {
1253
+ commitment: string;
1254
+ /** Original hex nonce from the challenge (NOT the field-reduced version). */
1255
+ nonce: string;
1256
+ response: {
1257
+ proof: Groth16Proof;
1258
+ publicSignals: string[];
1259
+ };
1260
+ /** Base64 ECDSA-SHA256 signature over `JSON.stringify(response.proof)`. */
1261
+ signature: string;
1262
+ };
1263
+ rememberMe?: boolean;
1264
+ }
1265
+ interface AuthSuccess {
1266
+ token: string;
1267
+ username: string;
1268
+ }
1269
+ /** A vault factor record (M1.0 + M2 gated types). Wrap/iv are base64 AES-GCM; absent for phrase. */
1270
+ interface VaultFactor {
1271
+ id: string;
1272
+ type: "password" | "passkey" | "phrase-marker" | "email" | "google" | "device";
1273
+ wrap?: string;
1274
+ iv?: string;
1275
+ params?: Record<string, unknown>;
1276
+ label?: string;
1277
+ createdAt?: number;
1278
+ /** Display hint for gated factors ("m•••@gmail.com") — list responses only. */
1279
+ masked?: string;
1280
+ /** Passkey's origin (RP id) — list responses only. A passkey only unlocks
1281
+ * from this host or a subdomain of it, so apps on several hosts enroll one
1282
+ * per origin. */
1283
+ rpId?: string;
1284
+ }
1285
+ interface AuthClientOptions {
1286
+ /** Absolute URL of the accelerator worker (no trailing slash required). */
1432
1287
  baseUrl: string;
1288
+ /** Optional custom fetch — defaults to `globalThis.fetch`. */
1289
+ fetch?: typeof fetch;
1290
+ }
1291
+ declare class AuthClient {
1292
+ private readonly baseUrl;
1293
+ private readonly fetchFn;
1294
+ constructor(opts: AuthClientOptions);
1295
+ /** Submit ZK registration. Caller has already derived the identity + commitment. */
1296
+ register(req: ZkRegisterRequest): Promise<ZkRegisterResponse>;
1297
+ /** Request an auth challenge for `username` (server issues a fresh nonce). */
1298
+ getChallenge(username: string): Promise<ZkChallengeResponse>;
1299
+ /** Trade a challenge + proof for a session token. */
1300
+ authenticate(req: ZkAuthenticateRequest): Promise<AuthSuccess>;
1433
1301
  /**
1434
- * Path prefix for the shard endpoints, joined to `baseUrl` before the
1435
- * hash. Defaults to `/api/shards`, which targets a global shard store
1436
- * (e.g. a dedicated `ShardStoreDO`). Pass a room-scoped value like
1437
- * `/api/spaces/{roomId}/shards` to keep shards bound to a specific
1438
- * `SharedSpaceDO` — that's the path the chat application uses today.
1439
- *
1440
- * Leading slash optional; trailing slash stripped. The final URL is
1441
- * always `${baseUrl}${normalizedPrefix}/${encodeURIComponent(hash)}`.
1302
+ * Validate a previously stored session token. Returns `{ username }` on
1303
+ * success; throws on 401/404 — caller uses that to drop a stale token
1304
+ * when restoring a session after a reload.
1442
1305
  */
1443
- pathPrefix?: string;
1306
+ verify(token: string): Promise<{
1307
+ username: string;
1308
+ }>;
1309
+ /** Blinded OPRF evaluation — the server-gated half of a password/recovery wrap key. */
1310
+ oprfEvaluate(username: string, blinded: string): Promise<{
1311
+ evaluated: string;
1312
+ }>;
1444
1313
  /**
1445
- * Optional custom fetch implementation. Defaults to `globalThis.fetch`.
1446
- * Provided so callers can inject retries / auth / instrumentation.
1447
- */
1448
- fetch?: typeof fetch;
1449
- /**
1450
- * Optional offline byte cache. When present, GETs are served cache-first and
1451
- * populated on success, and PUTs write the cache before the network so a
1452
- * just-uploaded file reads back offline. See {@link ShardByteCache}.
1453
- */
1454
- cache?: ShardByteCache;
1455
- /**
1456
- * Coalesce concurrent origin reads into `POST {pathPrefix}/batch`. On by
1457
- * default: it is what keeps opening a project from costing one request per
1458
- * shard. Set false to force one-at-a-time — useful against a server without
1459
- * the route, though that is detected and handled automatically.
1460
- */
1461
- batchShards?: boolean;
1462
- /**
1463
- * Called when a PUT can't reach the network (offline). Lets the caller queue
1464
- * the upload for replay on reconnect; the bytes are already in {@link cache}
1465
- * keyed by `hash`, so only the hash needs recording.
1466
- */
1467
- deferUpload?: (hash: string) => Promise<void>;
1468
- /**
1469
- * Optional peer block source. When present, GETs try peers between the local
1470
- * cache and origin (peer blocks are verified by hash); PUTs announce the new
1471
- * block to peers. Best-effort — any failure falls through to origin.
1472
- */
1473
- peers?: PeerBlockSource;
1474
- /**
1475
- * When true, {@link ShardClient.putShard} returns as soon as the shard is
1476
- * written to {@link cache} and announced to {@link peers} — the origin PUT is
1477
- * fired in the BACKGROUND rather than awaited. This makes a large write's
1478
- * manifest available immediately (so a peer/worker can start pulling the
1479
- * shards over P2P right away) while origin catches up as a durable fallback.
1480
- * On a background-PUT failure the shard is handed to {@link deferUpload} (if
1481
- * present) for replay. Requires a {@link cache} so the bytes survive locally
1482
- * until the background upload lands. No-op without a cache.
1314
+ * Gated split-key OPRF evaluation (M2 — email/google factors). Requires a
1315
+ * live `verifyToken` from {@link emailOtpVerify}; returns BOTH trust
1316
+ * domains' evaluations. The recover token is consumed by this call.
1483
1317
  */
1484
- backgroundUpload?: boolean;
1485
- }
1486
- /**
1487
- * Compute the lowercase hex SHA-256 of `bytes`. Available via WebCrypto in
1488
- * every runtime this SDK targets.
1489
- */
1490
- declare function shardHash(bytes: Uint8Array): Promise<string>;
1491
- declare class ShardClient {
1492
- private readonly baseUrl;
1493
- private readonly pathPrefix;
1494
- private readonly fetchFn;
1495
- private readonly cache?;
1496
- /** Hashes waiting for the next batch, each with everyone awaiting it. */
1497
- private pendingShards;
1498
- private batchTimer;
1499
- /** False once the server is known not to have the batch route. */
1500
- private batchSupported;
1501
- private readonly deferUpload?;
1502
- private readonly peers?;
1503
- private readonly backgroundUpload;
1504
- constructor(opts: ShardClientOptions);
1505
- /** Build the URL for a given shard hash — exposed for callers that want to log or pre-resolve. */
1506
- urlForHash(hash: string): string;
1318
+ oprfEvaluateGated(username: string, blinded: string, factorType: "email" | "google", purpose: "enroll" | "recover", verifyToken: string): Promise<{
1319
+ evaluated: string;
1320
+ evaluated2: string;
1321
+ }>;
1507
1322
  /**
1508
- * Upload a shard. The server verifies SHA-256(body) matches `:hash` and
1509
- * rejects with 400 if not, so the caller is the source of truth on
1510
- * naming. Re-uploading an existing shard is idempotent — the server
1511
- * returns `dedup: true` in that case.
1323
+ * Request an email OTP. With a session `token` this is the ENROLL flow
1324
+ * (code sent to `email`); without one it's RECOVER (code sent to the
1325
+ * account's enrolled address — the response is uniform either way).
1512
1326
  */
1513
- putShard(hash: string, bytes: Uint8Array): Promise<{
1514
- dedup: boolean;
1327
+ emailOtpStart(opts: {
1328
+ token?: string;
1329
+ email?: string;
1330
+ username?: string;
1331
+ }): Promise<void>;
1332
+ /** Trade an emailed code for a single-use verifyToken (+ the proven address). */
1333
+ emailOtpVerify(opts: {
1334
+ code: string;
1335
+ token?: string;
1336
+ username?: string;
1337
+ }): Promise<{
1338
+ verifyToken: string;
1339
+ email: string;
1515
1340
  }>;
1516
- private bgActive;
1517
- private readonly bgQueue;
1518
- private static readonly BG_CONCURRENCY;
1519
- /** Queue a shard's origin PUT to run in the background under a concurrency
1520
- * cap. Bytes are re-read from {@link cache} at upload time (not held in the
1521
- * queue) so a large file's backlog doesn't pin a second copy in memory. */
1522
- private enqueueBackgroundUpload;
1523
- /** PUT the shard bytes to origin, verifying the server accepted them.
1524
- * Used by the background-upload path; throws on network/HTTP failure. */
1525
- private uploadToOrigin;
1526
1341
  /**
1527
- * Fetch a shard by hash. Throws if the server returns a non-200; returns
1528
- * `null` for 404 so callers can distinguish "missing" from "errored".
1342
+ * Verify a Google ID token (M2.3). With a session `token` it's the ENROLL
1343
+ * gate; without, it's RECOVER/LOGIN (the Google account must already be
1344
+ * linked). Returns a single-use verifyToken bound to the account's `sub`.
1529
1345
  */
1530
- getShard(hash: string): Promise<Uint8Array | null>;
1346
+ googleVerify(opts: {
1347
+ idToken: string;
1348
+ token?: string;
1349
+ username?: string;
1350
+ }): Promise<{
1351
+ verifyToken: string;
1352
+ sub: string;
1353
+ emailHint: string | null;
1354
+ }>;
1355
+ /** Mint a single-use authorization code for an app (called by the hosted page; session-authed). */
1356
+ grant(token: string, body: {
1357
+ appId: string;
1358
+ redirectUri: string;
1359
+ codeChallenge: string;
1360
+ sealedKeys: string;
1361
+ }): Promise<{
1362
+ code: string;
1363
+ }>;
1364
+ /** Exchange an authorization code + PKCE verifier for the session + sealed key material. */
1365
+ token(body: {
1366
+ code: string;
1367
+ codeVerifier: string;
1368
+ appId?: string;
1369
+ }): Promise<{
1370
+ sessionToken: string;
1371
+ username: string;
1372
+ commitment: string;
1373
+ sealedKeys: string;
1374
+ appId: string;
1375
+ }>;
1531
1376
  /**
1532
- * Fetch from origin, COALESCING concurrent requests into one batch.
1533
- *
1534
- * A shard is not a file. A project is thousands of small source modules,
1535
- * each split into shards, so reading one at a time made opening a project
1536
- * cost thousands of requests — 2,086 for a 519-file project. Latency, not
1537
- * bytes, is what that spends: Chromium took 18.4s over those requests and
1538
- * Brave, which filters every one, took 37.5s for identical work. Batching
1539
- * collapses whatever is in flight at the same moment into a single POST,
1540
- * which is why it helps most exactly where it hurt most.
1541
- *
1542
- * Callers are unchanged: `getShard` still asks for one shard and gets one
1543
- * shard. The window is a few milliseconds — long enough to gather the reads
1544
- * a concurrent walk issues together, short enough not to be felt by a lone
1545
- * read.
1546
- *
1547
- * Falls back permanently to one-at-a-time if the server has no batch route,
1548
- * so a new SDK keeps working against an older deployment.
1377
+ * Read a factor record for `username` (returns an indistinguishable decoy if
1378
+ * absent). Gated types (email) require the recover `verifyToken`.
1549
1379
  */
1550
- private fetchFromOrigin;
1551
- /** Send one batch of whatever has accumulated, and settle those callers. */
1552
- private flushShardBatch;
1553
- /** Fetch a shard from origin. Returns `null` for 404 (or offline when a
1554
- * cache is present, so RS recovery can proceed); throws otherwise. */
1555
- private fetchOneFromOrigin;
1380
+ vaultRead(username: string, factorType: VaultFactor["type"], verifyToken?: string,
1381
+ /** Passkeys are per-origin: naming the caller's host returns THAT origin's
1382
+ * passkey instead of whichever was enrolled first (which WebAuthn would
1383
+ * refuse). Ignored for other factor types. */
1384
+ rpId?: string,
1385
+ /** Device factors are selected by id — a user may have many machines. */
1386
+ factorId?: string): Promise<{
1387
+ factor: VaultFactor | null;
1388
+ }>;
1556
1389
  /**
1557
- * Check whether a shard exists. Lets the caller skip a PUT they'd otherwise
1558
- * dedup-rewrite. The server can implement this as a HEAD or return a small
1559
- * JSON body — we don't depend on either.
1390
+ * Add/replace a factor (session-authed; the seed is already wrapped
1391
+ * client-side). Email factors additionally require (and consume) the
1392
+ * enroll `verifyToken`; the stored address must be the verified one.
1560
1393
  */
1561
- hasShard(hash: string): Promise<boolean>;
1394
+ vaultPutFactor(token: string, factor: VaultFactor, verifyToken?: string): Promise<{
1395
+ ok: boolean;
1396
+ id: string;
1397
+ }>;
1398
+ /** List the user's factor metadata (no ciphertext). Session-authed. */
1399
+ vaultFactors(token: string): Promise<{
1400
+ factors: VaultFactor[];
1401
+ migrated: boolean;
1402
+ }>;
1403
+ /** Remove a factor by id (can't remove the last one). Session-authed. */
1404
+ vaultDeleteFactor(token: string, id: string): Promise<{
1405
+ deleted: boolean;
1406
+ }>;
1407
+ private json;
1408
+ private parse;
1562
1409
  }
1563
1410
 
1564
1411
  /**
1565
- * HTTP client for the gated file-manifest store on a SharedSpace
1566
- * (`/api/spaces/:spaceId/files`).
1412
+ * Deterministic ZK identity derivation from `(username, password)`.
1567
1413
  *
1568
- * A "file" in a space is its **manifest** (per-chunk keys + the SHA-256 of each
1569
- * global shard). The shard bytes live in the open `/api/shards` store; the
1570
- * manifest is the capability. This client only moves manifests in/out of the
1571
- * space's gated store.
1414
+ * Same inputs on ANY device produce the same secret, salt, and P-256
1415
+ * ECDSA + ECDH keypairs — which is what makes federated login from a
1416
+ * fresh browser possible. There is no localStorage involvement: every
1417
+ * register and login call re-derives the identity on demand.
1572
1418
  *
1573
- * Authorization is **session-based**, consistent with the rest of the
1574
- * SharedSpace (keyring, websocket): the SDK's credential-stamping `fetch`
1575
- * (from {@link HttpClient}) attaches the app key + session token, the platform
1576
- * validates the session and injects a trusted `X-Muhkoo-User-Context`, and the
1577
- * DO authorizes on that + channel membership. There is no per-op Groth16 proof.
1419
+ * Pipeline:
1420
+ * master_seed = PBKDF2-SHA256(password, "muhkoo-zk-v1:" || username, 200k iters)
1421
+ * secret = HKDF-Expand-SHA256(master_seed, "zk-secret", 32 bytes) → hex
1422
+ * salt = HKDF-Expand-SHA256(master_seed, "zk-salt", 32 bytes) → hex
1423
+ * ecdsa_priv = HKDF-Expand-SHA256(master_seed, "ecdsa-priv", 32 bytes)
1424
+ * ecdh_priv = HKDF-Expand-SHA256(master_seed, "ecdh-priv", 32 bytes)
1578
1425
  *
1579
- * Wire protocol (server: `SharedSpaceDO`):
1580
- * POST /api/spaces/:spaceId/files body: { manifest } → { ok, fileId }
1581
- * GET /api/spaces/:spaceId/files → { files: FileStat[] }
1582
- * GET /api/spaces/:spaceId/files/:fileId → { manifest }
1583
- * DELETE /api/spaces/:spaceId/files/:fileId → { ok, existed }
1426
+ * P-256 public points are computed via `@noble/curves` (WebCrypto has no
1427
+ * derive-public-from-private-scalar API), then both keypairs are imported
1428
+ * into SubtleCrypto in JWK form so the rest of the crypto stack can keep
1429
+ * using `crypto.subtle.sign` / `crypto.subtle.deriveBits`.
1430
+ *
1431
+ * Originally lived in `accelerator/public/js/zk-identity.js` and was ported
1432
+ * to `muhkoo/web/src/zk/identity.ts`; consolidated here so the SDK owns
1433
+ * identity derivation end-to-end.
1584
1434
  */
1585
-
1586
- interface SharedSpaceClientOptions {
1587
- /** Base URL of the accelerator (e.g. `https://api.muhkoo.dev`). */
1588
- baseUrl: string;
1589
- /**
1590
- * Credential-stamping fetch — pass {@link HttpClient.fetch} so requests
1591
- * carry the app key + session token. Defaults to `globalThis.fetch`
1592
- * (unauthenticated; only useful in tests).
1593
- */
1594
- fetch?: typeof fetch;
1435
+ interface ZkIdentity {
1436
+ /** 32-byte secret, hex-encoded. Private witness for the ZK circuit. */
1437
+ secretHex: string;
1438
+ /** 32-byte salt, hex-encoded. Private witness for the ZK circuit. */
1439
+ saltHex: string;
1440
+ /** P-256 ECDSA keypair, used to sign auth proofs. */
1441
+ ecdsaKeyPair: CryptoKeyPair;
1442
+ /** P-256 ECDH keypair, used by the chat ratchet. */
1443
+ ecdhKeyPair: CryptoKeyPair;
1595
1444
  }
1596
- declare class SharedSpaceClient {
1597
- private readonly baseUrl;
1598
- private readonly fetchFn;
1599
- constructor(opts: SharedSpaceClientOptions);
1600
- /** Write a file manifest into the space. Requires space membership. */
1601
- writeFileManifest(spaceId: string, manifest: FileManifest): Promise<void>;
1602
- /** Read a file manifest by id. Requires space membership. */
1603
- readFileManifest(spaceId: string, fileId: string): Promise<FileManifest>;
1604
- /** Delete a file manifest by id. Requires space membership. */
1605
- deleteFileManifest(spaceId: string, fileId: string): Promise<boolean>;
1606
- /** List file summaries in the space. Requires space membership. */
1607
- listFiles(spaceId: string): Promise<FileStat[]>;
1608
- private filesBase;
1609
- private jsonRequest;
1610
- }
1611
-
1612
1445
  /**
1613
- * `FileStorage` — top-level entry point for chunked, encrypted, erasure-coded
1614
- * file storage.
1615
- *
1616
- * Pipeline for writing a file:
1617
- * 1. Split the plaintext into fixed-size chunks (default 4 MiB).
1618
- * 2. For each chunk:
1619
- * a. Generate a fresh AES-256-GCM key + 12-byte IV (`ChunkCipher`).
1620
- * b. Encrypt the chunk → ciphertext (plaintext.length + 16 bytes tag).
1621
- * c. Reed–Solomon encode the ciphertext into `dataShards + parityShards`
1622
- * equal-length shards (`ReedSolomonCodec`).
1623
- * d. PUT every shard to the content-addressed shard store, keyed by
1624
- * SHA-256 of the shard bytes (`ShardClient`).
1625
- * 3. Build a `FileManifest` listing every chunk's keys, IVs, sizes, and
1626
- * the SHA-256 of every shard in order.
1627
- *
1628
- * Two ways to use the manifest:
1629
- * - {@link FileStorage.writeFileToShards} returns it to the caller, who is
1630
- * then responsible for delivering it to readers (e.g. by E2E-encrypting
1631
- * it as a chat message). The chat application takes this path.
1632
- * - {@link FileStorage.writeFile} additionally POSTs the manifest to a
1633
- * gated multi-user space (`SharedSpaceClient`) so any participant of the
1634
- * space can read the file by manifest id later.
1446
+ * Derive a deterministic ZK identity from `(username, password)`.
1635
1447
  *
1636
- * Reading is the inverse: {@link FileStorage.readFileFromShards} takes a
1637
- * manifest and reassembles the plaintext; {@link FileStorage.readFile} first
1638
- * fetches the manifest from the space.
1448
+ * Same inputs always produce the same identity, on any device — that's
1449
+ * what makes federated login work without dragging encrypted state across
1450
+ * devices.
1639
1451
  */
1640
-
1641
- interface FileStorageOptions {
1642
- /** Talks to the content-addressed shard store. Always required. */
1643
- shards: ShardClient;
1644
- /**
1645
- * Talks to the gated multi-user space holding manifests + ACLs.
1646
- * Optional — only required by {@link FileStorage.writeFile} /
1647
- * {@link FileStorage.readFile} / {@link FileStorage.deleteFile} /
1648
- * {@link FileStorage.listFiles} / {@link FileStorage.readManifest}.
1649
- *
1650
- * Callers that ship the manifest themselves (e.g. via an encrypted chat
1651
- * message) can omit `space` and use the `*FromShards` / `*ToShards`
1652
- * methods directly.
1653
- */
1654
- space?: SharedSpaceClient;
1655
- /**
1656
- * Plaintext chunk size in bytes. After encryption each chunk gains 16
1657
- * bytes (GCM tag); after RS encoding the size is rounded up to a multiple
1658
- * of `dataShards` per chunk. Default 4 MiB — a balance between
1659
- * round-trip cost and parallelism on the upload path.
1660
- */
1661
- chunkSize?: number;
1662
- /** RS data shards per chunk (default 4). */
1663
- dataShards?: number;
1664
- /** RS parity shards per chunk (default 2). */
1665
- parityShards?: number;
1666
- /**
1667
- * Cap on concurrent shard uploads / downloads. Tune for the network. The
1668
- * default of 8 saturates most home connections without overwhelming the
1669
- * server's per-IP rate limiter.
1670
- */
1671
- concurrency?: number;
1672
- /**
1673
- * Optional pre-compiled RS WebAssembly module. Defaults to the bundled
1674
- * loader. Pass this in environments where the bundled loader path isn't
1675
- * available (vitest, deploy-time-precompiled CF Workers).
1676
- */
1677
- rsWasmModule?: WebAssembly.Module;
1678
- }
1452
+ declare function deriveIdentity(username: string, password: string): Promise<ZkIdentity>;
1679
1453
  /**
1680
- * Input to {@link FileStorage.writeFile} (which writes the manifest to a
1681
- * `SharedSpaceClient`). Carries the destination `spaceId`.
1454
+ * Legacy master-seed derivation: `PBKDF2(password, "muhkoo-zk-v1:"||username)`.
1455
+ *
1456
+ * This was the *only* path pre-vault, where the identity descended directly from
1457
+ * the password. It's kept so existing accounts still re-derive their seed (and
1458
+ * the vault can adopt that exact seed on migration, preserving the commitment).
1459
+ * New accounts use a random seed (`vault.randomSeed`) instead, so their identity
1460
+ * is no longer a function of the password.
1682
1461
  */
1683
- interface WriteFileInput extends WriteToShardsInput {
1684
- /** Space to write the manifest to — must allow the caller `write`. */
1685
- spaceId: string;
1686
- }
1462
+ declare function deriveMasterSeedFromPassword(username: string, password: string): Promise<Uint8Array>;
1687
1463
  /**
1688
- * Input to {@link FileStorage.writeFileToShards} (which doesn't write a
1689
- * manifest anywhere — that's the caller's job).
1464
+ * Derive the (unchanged) ZK identity from a 32-byte **master seed**. The seed is
1465
+ * HKDF-expanded into secret/salt + the P-256 ECDSA & ECDH keypairs exactly as
1466
+ * before — so the resulting commitment is byte-identical for a given seed. The
1467
+ * vault wraps THIS seed under each recovery factor.
1690
1468
  */
1691
- interface WriteToShardsInput {
1692
- /** Raw data, a Blob, or a File. */
1693
- data: Uint8Array | Blob | File;
1694
- /** Application-supplied display metadata. */
1695
- metadata: {
1696
- name: string;
1697
- type: string;
1698
- path?: string;
1699
- lastModified?: number;
1700
- };
1701
- /**
1702
- * Optional pre-allocated file id. Defaults to a fresh one. Useful for
1703
- * resumable uploads: the same id always maps to the same manifest slot.
1704
- */
1705
- fileId?: string;
1706
- /**
1707
- * Optional upload-progress callback, invoked after each chunk finishes
1708
- * (`completed` of `total` chunks). Lets a UI show a progress bar. Called once
1709
- * with `(0, total)` before the first chunk so a bar can render immediately.
1710
- */
1711
- onProgress?: (completed: number, total: number) => void;
1712
- }
1713
- declare class FileStorage {
1714
- private readonly space;
1715
- private readonly shards;
1716
- private readonly chunkSize;
1717
- private readonly dataShards;
1718
- private readonly parityShards;
1719
- private readonly concurrency;
1720
- private readonly cipher;
1721
- private readonly codec;
1722
- private readonly rsWasmModule?;
1723
- constructor(opts: FileStorageOptions);
1724
- /**
1725
- * Chunk → encrypt → encode → upload-shards. Returns the resulting manifest
1726
- * along with a `FileStat` summary. The caller is responsible for storing
1727
- * or transmitting the manifest however it likes.
1728
- */
1729
- writeFileToShards(input: WriteToShardsInput): Promise<{
1730
- manifest: FileManifest;
1731
- stat: FileStat;
1732
- }>;
1733
- /**
1734
- * Inverse of {@link writeFileToShards}. Takes a manifest the caller
1735
- * obtained somehow (chat message, share link, etc.) and reassembles the
1736
- * plaintext. Throws if too many shards are missing or any chunk's auth
1737
- * tag fails.
1738
- */
1739
- readFileFromShards(manifest: FileManifest): Promise<{
1740
- data: Uint8Array;
1741
- stat: FileStat;
1742
- }>;
1743
- /**
1744
- * Streaming inverse of {@link writeFileToShards}: yields each chunk's
1745
- * decrypted plaintext in order WITHOUT concatenating. Peak memory stays at
1746
- * ~one chunk, so multi-GB files (which overflow a single `Uint8Array`) can be
1747
- * piped straight to a file/socket. Same shard sourcing (cache → peers →
1748
- * origin) and per-chunk auth as {@link readFileFromShards}.
1749
- */
1750
- readChunksFromShards(manifest: FileManifest): AsyncGenerator<Uint8Array>;
1751
- /** As {@link writeFileToShards}, plus POSTs the manifest to `input.spaceId`. */
1752
- writeFile(input: WriteFileInput): Promise<FileStat>;
1753
- /** As {@link readFileFromShards}, but fetches the manifest from a space first. */
1754
- readFile(spaceId: string, fileId: string): Promise<{
1755
- data: Uint8Array;
1756
- stat: FileStat;
1757
- }>;
1758
- /**
1759
- * Delete the manifest from a space. **Does not GC the shards** — those
1760
- * are content-addressed and may be referenced by other manifests.
1761
- * Garbage collection is a separate server-side concern (Phase D / D-ish).
1762
- */
1763
- deleteFile(spaceId: string, fileId: string): Promise<boolean>;
1764
- /** Convenience pass-through. */
1765
- listFiles(spaceId: string): Promise<FileStat[]>;
1766
- /** Read a manifest verbatim — useful for "share file" / export flows. */
1767
- readManifest(spaceId: string, fileId: string): Promise<FileManifest>;
1768
- private requireSpace;
1769
- /** Encrypt + encode + upload one chunk; return its manifest entry. */
1770
- private writeChunk;
1771
- /** Inverse of `writeChunk`. */
1772
- private readChunk;
1773
- /** Run `tasks` with at most `this.concurrency` in flight. */
1774
- private runWithConcurrency;
1775
- }
1469
+ declare function deriveIdentityFromSeed(seed: Uint8Array): Promise<ZkIdentity>;
1776
1470
 
1777
1471
  /**
1778
- * Reed–Solomon erasure-coding codec.
1472
+ * Poseidon hash wrapper used by the ZK auth flow.
1779
1473
  *
1780
- * Wraps the WASM-backed `encode` / `reconstruct` primitives in `wasm/rs.ts`
1781
- * with shard-padding bookkeeping so callers can hand it any-length input and
1782
- * get back uniform-size shards.
1474
+ * Backed by `circomlibjs`'s `buildPoseidon`. Builder is lazy and cached
1475
+ * because the JS-side Babyjubjub field reps take ~100ms to construct, and
1476
+ * we'd rather not pay that on every page load.
1783
1477
  *
1784
- * Usage:
1785
- * const codec = new ReedSolomonCodec();
1786
- * await codec.ready();
1787
- * const { shards, shardSize, originalSize } = codec.encode(plaintext, 4, 2);
1788
- * // 6 shards (4 data + 2 parity), each `shardSize` bytes
1789
- * // …ship them, lose up to 2, then …
1790
- * const recovered = codec.decode(shards, 2, deadIndexes, originalSize);
1478
+ * Returns hashes as decimal-string BigInts — same format snarkjs and the
1479
+ * `preimagePoK` circuit expect for inputs.
1791
1480
  *
1792
- * Why the explicit `originalSize`: RS pads input to make every data shard the
1793
- * same length. The decoder must trim that padding back off after recovery.
1794
- * The caller is responsible for stashing `originalSize` somewhere durable
1795
- * (the file manifest, in this codebase).
1481
+ * Note: circomlibjs is `optionalDependencies` in connect's package.json.
1482
+ * Consumers that use the ZK auth flow (browser/server, not workers) need
1483
+ * it installed. The workers build does not import this module.
1796
1484
  */
1797
- interface EncodeResult {
1798
- /** All `dataShards + parityShards` shards. All have length `shardSize`. */
1799
- shards: Uint8Array[];
1800
- /** Length of every shard (data and parity). */
1801
- shardSize: number;
1802
- /** Length of the original input before padding. Needed to trim on decode. */
1803
- originalSize: number;
1804
- /** How many of the leading shards are data (the rest are parity). */
1805
- dataShards: number;
1806
- /** How many parity shards trail the data shards. */
1807
- parityShards: number;
1808
- }
1809
- declare class ReedSolomonCodec {
1810
- /**
1811
- * Resolves once the WASM module is loaded; safe to call repeatedly.
1812
- *
1813
- * Pass `wasmModule` to use a pre-compiled module (e.g. in tests where the
1814
- * rollup-inlined loader path isn't available, or in CF Workers where you
1815
- * want to load the module at deploy time). Default path uses the bundled
1816
- * base64-inlined wasm — works in Node, browsers, and Workers.
1817
- */
1818
- ready(wasmModule?: WebAssembly.Module): Promise<void>;
1819
- /**
1820
- * Split `data` into `dataShards` equal-size chunks (zero-padded to fit),
1821
- * compute `parityShards` parity shards, and return all of them.
1822
- */
1823
- encode(data: Uint8Array, dataShards: number, parityShards: number): EncodeResult;
1824
- /**
1825
- * Reconstruct the original `data` from `shards`. Up to `parityShards`
1826
- * shards may be missing; for each missing shard, pass an empty
1827
- * `Uint8Array` placeholder at its position and list the position in
1828
- * `deadShardIndexes`.
1485
+ /**
1486
+ * Poseidon-hash `inputs` and return the result as a decimal-string BigInt.
1487
+ * Inputs are converted to BigInt — pass either bigints, numbers, or numeric
1488
+ * strings.
1489
+ */
1490
+ declare function poseidonHash(inputs: Array<bigint | string | number>): Promise<string>;
1491
+
1492
+ /**
1493
+ * Integrity pinning for the ZK circuit artifacts.
1494
+ *
1495
+ * THE PROBLEM. `generateAuthProof` hands the user's `secret` and `salt` — the
1496
+ * inputs the whole zero-knowledge design exists to keep on the device — to a
1497
+ * witness generator downloaded at runtime from the accelerator. That is the
1498
+ * party the proof is meant to convince. With no integrity check, a malicious or
1499
+ * compromised server serves a modified `preimagePoK.wasm` that copies `secret`
1500
+ * into a public-signal slot (public signals are just `witness[1..nPublic]`), and
1501
+ * the client forwards them without ever comparing against what it computed
1502
+ * locally. The server then derives the `client.kv` at-rest key through
1503
+ * `StorageCipher` and decrypts everything it stores. Controlling the `.zkey` is
1504
+ * just as bad by itself: a subverted CRS breaks Groth16's zero-knowledge
1505
+ * property outright.
1506
+ *
1507
+ * THE FIX. Fetch the bytes here, SHA-256 them against digests pinned at build
1508
+ * time, and hand snarkjs verified BUFFERS rather than a URL it will fetch for
1509
+ * itself. snarkjs accepts `Uint8Array` for both artifacts.
1510
+ *
1511
+ * THREAT-MODEL BOUNDARY, and why local paths are exempt. The attack is "a
1512
+ * SERVER serves you a modified artifact". A filesystem path is not
1513
+ * server-controlled — it is the caller's own machine — so non-`http(s)`
1514
+ * specifiers pass through untouched (this is also what lets tests point at
1515
+ * `circuits/build/` directly, where snarkjs reads them as paths under Node).
1516
+ * Anything fetched over the network must be pinned, or explicitly opted out of
1517
+ * with {@link CircuitIntegrity.allowUnpinned}.
1518
+ *
1519
+ * There is deliberately NO silent fallback. An unpinned `https:` URL throws
1520
+ * rather than quietly proving with unverified bytes — the failure mode this
1521
+ * codebase already has elsewhere (see the SHA-256-for-Poseidon substitution in
1522
+ * `crypto/ZeroKnowledge.ts`) is exactly what makes a security control useless.
1523
+ */
1524
+ /**
1525
+ * SHA-256 of the `preimagePoK` artifacts this SDK version was built against.
1526
+ *
1527
+ * Verified identical to what the accelerator serves from
1528
+ * `public/circuits/build/` at the time of pinning. **If the circuit is ever
1529
+ * recompiled, these must be regenerated in the same commit as
1530
+ * `PREIMAGE_POK_VERIFICATION_KEY`, or every login fails closed** — which is the
1531
+ * intended direction for a mismatch.
1532
+ *
1533
+ * Regenerate with:
1534
+ * shasum -a 256 circuits/build/preimagePoK_js/preimagePoK.wasm \
1535
+ * circuits/build/preimagePoK_0001.zkey
1536
+ */
1537
+ declare const PINNED_CIRCUIT_DIGESTS: {
1538
+ readonly preimagePoKWasm: "9230ce3a883ca11e618639ac14fe0048b5dc4491c77b1dde3e41eb0896c33e10";
1539
+ readonly preimagePoKZkey: "087f4ef09b6afbacad8e47b0029ded408a0ac3b5c9d5a1624e94c06bc258b871";
1540
+ };
1541
+ /** Integrity options carried alongside a circuit URL. */
1542
+ interface CircuitIntegrity {
1543
+ /** Expected SHA-256 of the witness generator, lowercase hex. */
1544
+ wasmSha256?: string;
1545
+ /** Expected SHA-256 of the proving key, lowercase hex. */
1546
+ zkeySha256?: string;
1547
+ /**
1548
+ * Proceed without verification for network-fetched artifacts.
1829
1549
  *
1830
- * `originalSize` is required so the trailing zero-padding can be trimmed.
1550
+ * DANGEROUS: it re-opens the attack described in this module's docs. Only
1551
+ * for local development against a circuit you compiled yourself and have
1552
+ * not yet pinned.
1831
1553
  */
1832
- decode(shards: Uint8Array[], parityShards: number, deadShardIndexes: number[], originalSize: number): Uint8Array;
1554
+ allowUnpinned?: boolean;
1555
+ }
1556
+ /** Thrown when a fetched artifact does not match its pinned digest. */
1557
+ declare class CircuitIntegrityError extends Error {
1558
+ readonly url: string;
1559
+ readonly expected: string;
1560
+ readonly actual: string;
1561
+ constructor(url: string, expected: string, actual: string);
1833
1562
  }
1834
1563
 
1835
1564
  /**
1836
- * Universal wasm-bindgen glue for `wasm-reed-solomon-erasure` (v0.2.2 — Rust
1837
- * Reed–Solomon erasure-coding library compiled with `wasm-pack --target nodejs`).
1565
+ * Groth16 proof generation for the `preimagePoK` circuit.
1838
1566
  *
1839
- * The upstream npm package ships `wasm_reed_solomon_erasure.js` which uses
1840
- * `require('fs').readFileSync()` to load the WASM file at module load time —
1841
- * that hard-codes Node and breaks browsers / CF Workers. This file is a port
1842
- * of that glue with the Node-only pieces removed:
1843
- *
1844
- * - `TextDecoder` is taken from the global (available in Node 12+, every
1845
- * modern browser, and Workers — no `require('util')`).
1846
- * - The WASM module is loaded via the rollup-inlined `.wasm` import, the
1847
- * same `targetEnv: 'auto-inline'` path the bn128 verifier uses. Works in
1848
- * Node, browser, and Workers without runtime fs access.
1567
+ * The circuit proves knowledge of `(secret, salt, ecdsaPub)` such that
1568
+ * commitment = Poseidon(secret, salt, Poseidon(ecdsaPub))
1569
+ * while only revealing `(commitment, nonce, ecdsaPubHash)` as public signals.
1849
1570
  *
1850
- * Only the two public functions consumers actually need (`encode` and
1851
- * `reconstruct`) are exported. The rest is the wbindgen heap-table dance.
1571
+ * Circuit assets (wasm + zkey) are served by the accelerator worker. Their
1572
+ * URLs are a required argument of {@link generateAuthProof} — there is no
1573
+ * default. The web app constructs them relative to its configured worker
1574
+ * `baseUrl` so the SPA can target either the local wrangler dev or a deployed
1575
+ * worker depending on `VITE_WORKER_URL`.
1576
+ */
1577
+
1578
+ interface CircuitUrls extends CircuitIntegrity {
1579
+ /** preimagePoK.wasm — the circuit's witness generator. */
1580
+ wasmUrl: string;
1581
+ /** preimagePoK_0001.zkey — the proving key. */
1582
+ zkeyUrl: string;
1583
+ }
1584
+ /**
1585
+ * Canonical {@link CircuitUrls} for the `preimagePoK` login circuit, anchored
1586
+ * at a base URL where the accelerator serves the circuit assets out of
1587
+ * `circuits/build/`. The filenames here are fixed by the circom build output
1588
+ * (the witness generator lands inside a `preimagePoK_js/` directory); callers
1589
+ * shouldn't hard-code them. `Client` uses this to default its `circuits`
1590
+ * option, and `PersonalSpaceClient` consumers can pass `defaultCircuitUrls(baseUrl)`.
1591
+ */
1592
+ declare function defaultCircuitUrls(baseUrl: string): CircuitUrls;
1593
+ /**
1594
+ * Reduce any hex/decimal/byte input to a decimal-string field element in
1595
+ * `[0, FIELD_SIZE)`. snarkjs and the `preimagePoK` circuit both want decimal
1596
+ * strings; everything else gets normalized here.
1852
1597
  *
1853
- * Encoding:
1854
- * `encode(dataShards: Uint8Array[], parityShard: number)` → array of
1855
- * `dataShards.length + parityShard` Uint8Arrays. The first N are the input
1856
- * shards unchanged; the trailing M are the freshly computed parity shards.
1857
- * All shards are the same length — caller is responsible for padding the
1858
- * data into equal-size data shards.
1598
+ * **Behavior is intentionally byte-for-byte compatible with the legacy
1599
+ * `accelerator/public/js/zk-snarkjs-client.js#toField`.** Diverging produces
1600
+ * different commitments than accounts registered via the legacy flow,
1601
+ * which locks those users out forever — the worker stores whatever
1602
+ * commitment the client sent at register time and compares it as a string
1603
+ * at login time.
1859
1604
  *
1860
- * Reconstruction:
1861
- * `reconstruct(corruptedShards, parityShard, deadIndexes)` → fully recovered
1862
- * array. Pass placeholders (empty Uint8Arrays) at the dead indexes; the
1863
- * reconstructor fills them in. Up to `parityShard` shards may be missing.
1605
+ * In particular: a 32+ char string of hex chars is always treated as hex,
1606
+ * even if every character happens to be 0-9 with no a-f letters. An
1607
+ * earlier version of this function gated the hex branch on "has at least
1608
+ * one letter" — that broke the digit-only-hex edge case (rare in practice,
1609
+ * but real). DO NOT add that letter check back.
1864
1610
  */
1611
+ declare function toField(value: string | bigint | number): string;
1865
1612
  /**
1866
- * Load and instantiate the WASM module. Idempotent — repeated calls await the
1867
- * same one-shot promise.
1613
+ * Build the user's Poseidon commitment binding `(secret, salt, ecdsaPubHash)`.
1614
+ * Same formula the worker verifies against on register and authenticate.
1615
+ */
1616
+ declare function buildCommitment(secretHex: string, saltHex: string, ecdsaPubHex: string): Promise<string>;
1617
+ /**
1618
+ * Generate a Groth16 proof for the `preimagePoK` circuit. Public signals end
1619
+ * up `[commitment, nonce, ecdsaPubHash]` — the worker checks the commitment
1620
+ * matches the stored one and the nonce matches the issued challenge nonce
1621
+ * reduced into the field.
1868
1622
  *
1869
- * Two ways to initialize, matching the pattern used by `workers/groth16-verifier`:
1870
- * 1. `await initRsWasm()` — uses the bundled .wasm (base64-inlined at build
1871
- * time by `@rollup/plugin-wasm`). Works in any runtime that allows
1872
- * runtime `WebAssembly.compile()`.
1873
- * 2. `await initRsWasm(myWasmModule)` — accepts a pre-compiled
1874
- * `WebAssembly.Module`. Useful when the bundled loader path isn't
1875
- * available (vitest, CF Workers wanting deploy-time precompile, etc.).
1623
+ * Throws if circuit URLs are unreachable or snarkjs decides the inputs are
1624
+ * malformed.
1876
1625
  */
1877
- declare function initRsWasm(wasmModule?: WebAssembly.Module): Promise<void>;
1626
+ declare function generateAuthProof(args: {
1627
+ secretHex: string;
1628
+ saltHex: string;
1629
+ ecdsaPubHex: string;
1630
+ nonceHex: string;
1631
+ /** Required — caller supplies URLs scoped to the worker they want to talk to. */
1632
+ circuits: CircuitUrls;
1633
+ }): Promise<{
1634
+ proof: Groth16Proof;
1635
+ publicSignals: string[];
1636
+ commitment: string;
1637
+ nonceField: string;
1638
+ ecdsaPubHash: string;
1639
+ }>;
1878
1640
 
1879
1641
  /**
1880
- * HTTP client for the accelerator's `/api/auth/*` endpoints.
1642
+ * Conversions between SubtleCrypto `CryptoKey`s and the wire formats the
1643
+ * accelerator's auth endpoints + `preimagePoK` circuit expect.
1881
1644
  *
1882
- * Owns the wire shapes for register / challenge / authenticate / verify so
1883
- * the React SPA (and any future consumers) don't reimplement them. The ZK
1884
- * identity derivation + Groth16 proof generation stay on the application
1885
- * side — this client handles only the round-trips.
1645
+ * Two outputs we need from an ECDSA `CryptoKey`:
1646
+ * - **base64 raw** — passed to `/api/auth/zk-register` as
1647
+ * `ecdsaPublicKey` / `ecdhPublicKey`. (Name is historical;
1648
+ * the legacy client called it `exportPublicKeyToBase58`
1649
+ * but actually used base64 — kept as base64.)
1650
+ * - **hex raw** — fed to the proof as `ecdsaPub` input; the circuit
1651
+ * reduces it into a field element.
1886
1652
  *
1887
- * Mirrors `PersonalSpaceClient` / `ShardClient` / `SharedSpaceClient`: takes
1888
- * a `baseUrl`, optional custom `fetch`, throws typed `Error`s on failure
1889
- * (with the worker's message inlined where available).
1653
+ * Both go through `crypto.subtle.exportKey("raw", ...)` which yields the
1654
+ * uncompressed SEC1 form (`0x04 || X || Y`).
1890
1655
  */
1656
+ /** Raw uncompressed (`0x04 || X || Y`) public key bytes, as lowercase hex. */
1657
+ declare function exportPublicKeyHex(publicKey: CryptoKey): Promise<string>;
1658
+ /** Raw uncompressed public key as base64 — wire format for the auth endpoints. */
1659
+ declare function exportPublicKeyBase64(publicKey: CryptoKey): Promise<string>;
1660
+ /** Sign a UTF-8 `message` with an ECDSA private key. Returns base64. */
1661
+ declare function signMessage(message: string, privateKey: CryptoKey): Promise<string>;
1891
1662
 
1892
- interface ZkRegisterRequest {
1893
- username: string;
1894
- /** Decimal-string Poseidon commitment. */
1895
- commitment: string;
1896
- /** Base64-encoded raw uncompressed ECDH public key (SEC1 0x04‖X‖Y). */
1897
- ecdhPublicKey: string;
1898
- /** Base64-encoded raw uncompressed ECDSA public key. */
1899
- ecdsaPublicKey: string;
1900
- email?: string | null;
1901
- }
1902
- interface ZkRegisterResponse {
1903
- success: boolean;
1904
- }
1905
- interface ZkChallengeResponse {
1906
- challengeId: string;
1907
- /** Hex-encoded random nonce — caller reduces into a BN254 field element for the proof. */
1908
- nonce: string;
1909
- /** Decimal-string Poseidon commitment the server has on file for this user. */
1910
- commitment: string;
1911
- }
1912
- interface ZkAuthenticateRequest {
1913
- challengeId: string;
1914
- proof: {
1915
- commitment: string;
1916
- /** Original hex nonce from the challenge (NOT the field-reduced version). */
1917
- nonce: string;
1918
- response: {
1919
- proof: Groth16Proof;
1920
- publicSignals: string[];
1921
- };
1922
- /** Base64 ECDSA-SHA256 signature over `JSON.stringify(response.proof)`. */
1923
- signature: string;
1924
- };
1925
- rememberMe?: boolean;
1926
- }
1927
- interface AuthSuccess {
1928
- token: string;
1929
- username: string;
1930
- }
1931
- /** A vault factor record (M1.0 + M2 gated types). Wrap/iv are base64 AES-GCM; absent for phrase. */
1932
- interface VaultFactor {
1933
- id: string;
1934
- type: "password" | "passkey" | "phrase-marker" | "email" | "google" | "device";
1935
- wrap?: string;
1936
- iv?: string;
1937
- params?: Record<string, unknown>;
1938
- label?: string;
1939
- createdAt?: number;
1940
- /** Display hint for gated factors ("m•••@gmail.com") — list responses only. */
1941
- masked?: string;
1942
- /** Passkey's origin (RP id) — list responses only. A passkey only unlocks
1943
- * from this host or a subdomain of it, so apps on several hosts enroll one
1944
- * per origin. */
1945
- rpId?: string;
1946
- }
1947
- interface AuthClientOptions {
1948
- /** Absolute URL of the accelerator worker (no trailing slash required). */
1949
- baseUrl: string;
1950
- /** Optional custom fetch — defaults to `globalThis.fetch`. */
1951
- fetch?: typeof fetch;
1663
+ /**
1664
+ * Hosted-auth handoff crypto (auth.muhkoo.dev — AUTH_HOSTED_PLAN.md §3).
1665
+ *
1666
+ * The hosted page authenticates the user and ends up holding the master seed.
1667
+ * To pass it to the developer's app without it ever sitting server-readable or
1668
+ * landing in browser history:
1669
+ *
1670
+ * 1. {@link sealSeed} generates a one-time key K_t, AES-256-GCM-seals the seed
1671
+ * under it, and returns the ciphertext envelope (`sealedKeys`, stored in
1672
+ * the grant the server relays but can't read) plus K_t (`fragmentKey`,
1673
+ * placed ONLY in the redirect URL fragment — fragments never hit a server).
1674
+ * 2. The app exchanges the code for `sealedKeys`, reads K_t from the fragment,
1675
+ * and {@link unsealSeed} recovers the seed, then strips the fragment.
1676
+ *
1677
+ * Trust-equivalent to today's embedded SDK (the app still ends up with the
1678
+ * seed) while moving credential entry onto the Muhkoo origin. The envelope is
1679
+ * versioned so a future per-app-key handoff (v2) can swap in without a protocol
1680
+ * change.
1681
+ *
1682
+ * ## v2 — device pairing (TV / no-redirect handoff)
1683
+ *
1684
+ * v1's K_t rides in the redirect URL *fragment*, which works only because the
1685
+ * receiving app is a browser page that can read `location.hash`. A TV app has
1686
+ * no redirect to receive: it polls. So v2 keeps the *entire* v1 shape — the
1687
+ * server relays a sealed envelope it cannot read — and replaces only the key
1688
+ * agreement:
1689
+ *
1690
+ * - the TV generates an ephemeral P-256 ECDH keypair in memory and publishes
1691
+ * the public half ({@link generateDevicePairingKeypair});
1692
+ * - the hosted page (the component that already holds the unlocked seed and
1693
+ * already owns sealing) seals *to that public key* with a fresh ephemeral
1694
+ * keypair of its own ({@link sealSeedToDevice}) — ECDH → HKDF-SHA256 →
1695
+ * AES-256-GCM;
1696
+ * - the TV recovers the seed with its private key
1697
+ * ({@link unsealSeedFromDevice}).
1698
+ *
1699
+ * The server stores and relays the v2 envelope byte-for-byte, exactly as it
1700
+ * does for v1, and can read neither. The one thing ECDH cannot supply is proof
1701
+ * that the public key the hosted page sealed to is *the TV's* rather than an
1702
+ * attacker's — that is what {@link pairingVerificationCode} is for, and it is
1703
+ * the only MITM defence in the flow (see the entropy note on that function).
1704
+ */
1705
+ interface SealedHandoff {
1706
+ /** base64 JSON envelope — relayed by the server in the grant; server-opaque. */
1707
+ sealedKeys: string;
1708
+ /** base64url one-time key — rides ONLY in the redirect URL fragment. */
1709
+ fragmentKey: string;
1952
1710
  }
1953
- declare class AuthClient {
1954
- private readonly baseUrl;
1955
- private readonly fetchFn;
1956
- constructor(opts: AuthClientOptions);
1957
- /** Submit ZK registration. Caller has already derived the identity + commitment. */
1958
- register(req: ZkRegisterRequest): Promise<ZkRegisterResponse>;
1959
- /** Request an auth challenge for `username` (server issues a fresh nonce). */
1960
- getChallenge(username: string): Promise<ZkChallengeResponse>;
1961
- /** Trade a challenge + proof for a session token. */
1962
- authenticate(req: ZkAuthenticateRequest): Promise<AuthSuccess>;
1711
+ /** Seal the master seed under a fresh one-time key for the redirect handoff. */
1712
+ declare function sealSeed(seed: Uint8Array): Promise<SealedHandoff>;
1713
+ /** Recover the master seed from the sealed envelope + the fragment key. */
1714
+ declare function unsealSeed(sealedKeys: string, fragmentKey: string): Promise<Uint8Array>;
1715
+ /**
1716
+ * Alphabet for the pairing verification code: 30 symbols, every confusable pair
1717
+ * removed in BOTH directions — no `0`/`O`, no `1`/`I`/`L`, and no `U` (which is
1718
+ * read as `V` on the low-resolution, far-viewed type a TV renders). Because the
1719
+ * user compares this string across two screens by eye, unambiguity beats the
1720
+ * convenience of a power-of-two alphabet; the derivation below rejection-samples
1721
+ * to avoid the modulo bias that costs us.
1722
+ */
1723
+ declare const PAIRING_CODE_ALPHABET = "ABCDEFGHJKMNPQRSTVWXYZ23456789";
1724
+ /** Length of the pairing verification code, in characters. 30^8 ≈ 39.3 bits. */
1725
+ declare const PAIRING_CODE_LENGTH = 8;
1726
+ /** An ephemeral ECDH keypair generated by the pairing device (the TV). */
1727
+ interface DevicePairingKeypair {
1728
+ /** base64 raw (SEC1 uncompressed, 65 bytes) public key — safe to publish. */
1729
+ publicKeyB64: string;
1963
1730
  /**
1964
- * Validate a previously stored session token. Returns `{ username }` on
1965
- * success; throws on 401/404 — caller uses that to drop a stale token
1966
- * when restoring a session after a reload.
1967
- */
1968
- verify(token: string): Promise<{
1969
- username: string;
1970
- }>;
1971
- /** Blinded OPRF evaluation — the server-gated half of a password/recovery wrap key. */
1972
- oprfEvaluate(username: string, blinded: string): Promise<{
1973
- evaluated: string;
1974
- }>;
1975
- /**
1976
- * Gated split-key OPRF evaluation (M2 — email/google factors). Requires a
1977
- * live `verifyToken` from {@link emailOtpVerify}; returns BOTH trust
1978
- * domains' evaluations. The recover token is consumed by this call.
1979
- */
1980
- oprfEvaluateGated(username: string, blinded: string, factorType: "email" | "google", purpose: "enroll" | "recover", verifyToken: string): Promise<{
1981
- evaluated: string;
1982
- evaluated2: string;
1983
- }>;
1984
- /**
1985
- * Request an email OTP. With a session `token` this is the ENROLL flow
1986
- * (code sent to `email`); without one it's RECOVER (code sent to the
1987
- * account's enrolled address — the response is uniform either way).
1731
+ * Non-extractable private half. In-memory only, one per pairing attempt:
1732
+ * it is never persisted and never leaves the device, so a captured pairing
1733
+ * transcript is undecryptable once the attempt ends.
1988
1734
  */
1989
- emailOtpStart(opts: {
1990
- token?: string;
1991
- email?: string;
1992
- username?: string;
1993
- }): Promise<void>;
1994
- /** Trade an emailed code for a single-use verifyToken (+ the proven address). */
1995
- emailOtpVerify(opts: {
1996
- code: string;
1997
- token?: string;
1998
- username?: string;
1999
- }): Promise<{
2000
- verifyToken: string;
2001
- email: string;
2002
- }>;
1735
+ privateKey: CryptoKey;
1736
+ }
1737
+ /**
1738
+ * (Device side) Generate the ephemeral P-256 ECDH keypair for one pairing
1739
+ * attempt. Call this once per attempt and throw it away when the attempt ends —
1740
+ * reusing a keypair across attempts would let an old, observed ciphertext be
1741
+ * decrypted by whoever later obtains the key.
1742
+ */
1743
+ declare function generateDevicePairingKeypair(): Promise<DevicePairingKeypair>;
1744
+ /**
1745
+ * (Hosted-page side) Seal the master seed *to a device's* pairing public key.
1746
+ *
1747
+ * A fresh ephemeral keypair is generated per call and its public half is
1748
+ * carried in the envelope (`epk`), so sealing the same seed to the same device
1749
+ * twice yields unrelated ciphertext — and the sealer retains nothing that could
1750
+ * later reopen the envelope.
1751
+ *
1752
+ * Returns the v2 `sealedKeys` string, which slots into exactly the same server
1753
+ * field as v1: relayed verbatim, never read.
1754
+ */
1755
+ declare function sealSeedToDevice(seed: Uint8Array, devicePublicKeyB64: string): Promise<string>;
1756
+ /**
1757
+ * (Device side) Recover the master seed from a v2 envelope using the ephemeral
1758
+ * private key from {@link generateDevicePairingKeypair}.
1759
+ *
1760
+ * Throws on any tamper: a substituted `epk` yields a different shared secret
1761
+ * (or fails point validation outright), and any edit to `iv`/`ct` fails the
1762
+ * AES-GCM tag. There is no path that returns partially-authenticated bytes.
1763
+ *
1764
+ * `devicePublicKeyB64` is the caller's OWN public half (from
1765
+ * {@link generateDevicePairingKeypair}). It is required, not optional, because
1766
+ * it is mixed into the KDF — see {@link deriveDeviceSealKey} for why binding the
1767
+ * recipient matters. A device always holds both halves, so this costs nothing.
1768
+ */
1769
+ declare function unsealSeedFromDevice(sealedKeys: string, privateKey: CryptoKey, devicePublicKeyB64: string): Promise<Uint8Array>;
1770
+ /**
1771
+ * Short code derived deterministically from the device's pairing public key,
1772
+ * displayed on BOTH the TV and the approving browser so the human can confirm
1773
+ * they match before approving.
1774
+ *
1775
+ * ## What this is worth (be honest about it)
1776
+ *
1777
+ * ~39.3 bits (30^8), rendered in an unambiguous 30-symbol alphabet and grouped
1778
+ * as `XXXX-XXXX` for comparison. Alongside the recipient-key binding in
1779
+ * {@link deriveDeviceSealKey}, this is what stops a substituted device key: the
1780
+ * KDF rejects a *negated* key, and this code is what a human uses to reject a
1781
+ * wholly *attacker-generated* one, which the crypto cannot detect on its own —
1782
+ * ECDH proves nothing about WHOSE public key it is.
1783
+ *
1784
+ * Two attacks, and what each actually costs:
1785
+ *
1786
+ * 1. **Online grind.** Generate keypairs until one's code equals the honest
1787
+ * TV's. ~2^39 trials. Naively that reads as "keygen + hash", but a real
1788
+ * attacker never generates keys: walking `P_{i+1} = P_i + G` while tracking
1789
+ * `d_i = d_0 + i` makes each trial one point-add plus one derivation. With
1790
+ * a plain hash that is well under a core-day — which is why this uses
1791
+ * PBKDF2-SHA256 at {@link PAIRING_CODE_ITERATIONS} iterations instead. The
1792
+ * work factor applies per trial, pushing the same grind to the order of
1793
+ * 10^5 core-days. The target is also only learned when pairing starts, so
1794
+ * the grind is online against the pairing TTL (keep it minutes).
1795
+ * 2. **Precomputed table.** The reason this is not a plain hash. A table over
1796
+ * candidate keys is computed once and reused forever; with SHA-256 that is
1797
+ * cheap enough to be the preferred attack. The iteration count multiplies
1798
+ * the table's build cost by the same 2 * 10^5.
1799
+ *
1800
+ * Neither is a proof of security, and an adversary with ASIC-scale hardware
1801
+ * changes these numbers. If the margin is ever judged insufficient, raise
1802
+ * {@link PAIRING_CODE_LENGTH} — 10 characters buys ~49 bits for two more
1803
+ * characters of human comparison, and is display-only: it does not touch the
1804
+ * envelope or the wire format.
1805
+ *
1806
+ * The cost is paid once per pairing, on two idle devices, so ~200k iterations
1807
+ * is imperceptible here in a way it would not be on a hot path.
1808
+ */
1809
+ declare function pairingVerificationCode(devicePublicKeyB64: string): Promise<string>;
1810
+ interface Pkce {
1811
+ codeVerifier: string;
1812
+ codeChallenge: string;
1813
+ }
1814
+ /** Generate a PKCE verifier + S256 challenge for the authorize→token exchange. */
1815
+ declare function generatePkce(): Promise<Pkce>;
1816
+ /** A random URL-safe value for the OAuth `state` (CSRF binding on the callback). */
1817
+ declare function randomState(): string;
1818
+
1819
+ /**
1820
+ * `deviceStore` — at-rest persistence for a paired device (the TV).
1821
+ *
1822
+ * A device that has completed {@link HostedAuth.startDevicePairing} holds the
1823
+ * account's **master seed**. Android WebView has no WebAuthn, so the passkey
1824
+ * wrapping used elsewhere isn't available, and re-pairing on every cold start
1825
+ * is not acceptable UX for a set-top box. So the seed is persisted, encrypted:
1826
+ *
1827
+ * | Item | Where | Form |
1828
+ * | ------------------------ | ------------------------------------ | ---- |
1829
+ * | wrapping key | IndexedDB (`muhkoo.device.v1`) | `CryptoKey`, AES-GCM-256, **non-extractable** |
1830
+ * | device identity keypair | IndexedDB (`muhkoo.device.v1`) | `CryptoKeyPair`, ECDSA P-256, private half **non-extractable** |
1831
+ * | sealed identity blob | localStorage `muhkoo.tv.identity.v1` | `{"v":1,"iv":…,"ct":…}` over `JSON{username, commitment, seed(b64), pairedAt}` |
1832
+ *
1833
+ * The split is deliberate — ciphertext in one backend, key in another, and the
1834
+ * key non-extractable so JS can *use* it but not export it.
1835
+ *
1836
+ * ## Be honest about what this is worth
1837
+ *
1838
+ * **This is obfuscation, not protection.** It raises the cost of a casual
1839
+ * `localStorage` dump to "nothing useful", and that is all:
1840
+ *
1841
+ * - **XSS on the device origin** cannot *exfiltrate* the wrapping key (it is
1842
+ * non-extractable) but can absolutely call `decrypt` and read the seed. The
1843
+ * only mitigation is that there is no third-party script in the shell —
1844
+ * enforce a strict CSP with no `unsafe-inline`, no CDN, no remote fonts.
1845
+ * - **ADB / root / a backup extraction / a forensic image recovers the seed.**
1846
+ * The IndexedDB LevelDB files hold the AES key bytes in the clear; WebCrypto
1847
+ * "non-extractable" is a JS-API boundary, not an OS one. **A stolen device is
1848
+ * a stolen identity.**
1849
+ *
1850
+ * That is not an implementation shortcut, it is the shape of the problem: any
1851
+ * wrapping key usable without user interaction lives somewhere equally readable
1852
+ * on a keyboard-less device with no hardware key store exposed to WebView.
1853
+ * Reduce the blast radius with `android:allowBackup="false"`,
1854
+ * `android:debuggable="false"`, a strict CSP, and by storing *only* the 32-byte
1855
+ * seed + username + commitment (never a session token, never a recovery phrase).
1856
+ *
1857
+ * ## Documented upgrade path — no protocol change
1858
+ *
1859
+ * If the native shell exposes a Keystore bridge:
1860
+ *
1861
+ * ```java
1862
+ * @JavascriptInterface public String wrap(String b64plain); // AES-GCM, StrongBox when present
1863
+ * @JavascriptInterface public String unwrap(String b64cipher);
1864
+ * ```
1865
+ *
1866
+ * this module prefers `globalThis.MuhkooKeystore` automatically and falls back
1867
+ * to the WebCrypto scheme otherwise. The Keystore key is hardware-backed and
1868
+ * non-exportable at the OS level, so a copied filesystem image alone becomes
1869
+ * useless. Nothing on the wire, in the endpoint contracts, or in the SDK surface
1870
+ * changes — only the two functions below do, internally.
1871
+ *
1872
+ * ## Degradation
1873
+ *
1874
+ * Nothing here throws at import time. On a runtime with neither backend (SSR,
1875
+ * Workers, Node) the module still loads: {@link loadDeviceIdentity} returns
1876
+ * `null`, {@link persistDeviceIdentity} throws a typed
1877
+ * {@link DeviceStoreUnavailableError} the caller can swallow, and
1878
+ * {@link deviceIdentityKey} falls back to an **in-memory** keypair for the
1879
+ * lifetime of the process (so pairing still works; the device is just never
1880
+ * recognised as "known" on a later run).
1881
+ */
1882
+ /** localStorage key holding the AES-GCM-sealed identity blob. */
1883
+ declare const DEVICE_IDENTITY_STORAGE_KEY = "muhkoo.tv.identity.v1";
1884
+ /** IndexedDB database holding the non-extractable keys. */
1885
+ declare const DEVICE_KEY_DB_NAME = "muhkoo.device.v1";
1886
+ /** What a paired device persists. The seed **is** the identity — treat it as such. */
1887
+ interface PersistedDeviceIdentity {
1888
+ username: string;
1889
+ /** Decimal-string Poseidon commitment. Pinned: a later pairing that yields a
1890
+ * different commitment for this device must be refused, not adopted. */
1891
+ commitment: string;
1892
+ /** The 32-byte master seed. */
1893
+ seed: Uint8Array;
1894
+ pairedAt: number;
1895
+ }
1896
+ /** Thrown when persistence is impossible (no IndexedDB and/or no localStorage). */
1897
+ declare class DeviceStoreUnavailableError extends Error {
1898
+ constructor(what: string);
1899
+ }
1900
+ /** Key/value store for live `CryptoKey` objects (IndexedDB in the browser). */
1901
+ interface DeviceKeyVault {
1902
+ get(id: string): Promise<unknown | null>;
1903
+ put(id: string, value: unknown): Promise<void>;
1904
+ /** Delete the entire store — used by `clearDeviceIdentity`. */
1905
+ destroy(): Promise<void>;
1906
+ }
1907
+ /** String store for the sealed blob (localStorage in the browser). */
1908
+ interface DeviceBlobStore {
1909
+ get(key: string): string | null;
1910
+ set(key: string, value: string): void;
1911
+ remove(key: string): void;
1912
+ }
1913
+ /** The native Keystore bridge, when the shell provides one (see module docs). */
1914
+ interface MuhkooKeystoreBridge {
1915
+ wrap(b64plain: string): string | Promise<string>;
1916
+ unwrap(b64cipher: string): string | Promise<string>;
1917
+ }
1918
+ /**
1919
+ * Point the store at explicit backends (or `null` to force "unavailable"), and
1920
+ * reset the memoized device keypair. Testing / embedding seam — pass `{}` to
1921
+ * restore the default browser backends.
1922
+ */
1923
+ declare function configureDeviceStore(opts?: {
1924
+ keys?: DeviceKeyVault | null;
1925
+ blobs?: DeviceBlobStore | null;
1926
+ keystore?: MuhkooKeystoreBridge | null;
1927
+ }): void;
1928
+ /**
1929
+ * True when {@link deviceIdentityKey} had to fall back to an in-memory keypair
1930
+ * because no key vault was available. Such a device can pair, but will never be
1931
+ * recognised as "known" afterwards (so approval always needs a fresh factor).
1932
+ */
1933
+ declare function deviceIdentityIsEphemeral(): boolean;
1934
+ /**
1935
+ * The device's **persistent** ECDSA P-256 identity keypair, created on first
1936
+ * call. This is not a browser fingerprint — it is a key, and every
1937
+ * `POST /api/auth/device/code` proves possession of it. Without that proof,
1938
+ * "known device" would be an unauthenticated string anyone could claim in order
1939
+ * to skip the approver's re-authentication.
1940
+ *
1941
+ * The private half is non-extractable and never leaves IndexedDB as bytes.
1942
+ */
1943
+ declare function deviceIdentityKey(): Promise<CryptoKeyPair>;
1944
+ /** `base64url(SHA-256(raw SEC1 device identity public key))` — 43 chars. */
1945
+ declare function deviceFingerprint(): Promise<string>;
1946
+ /** Whether a sealed identity blob exists. Cheap: no crypto, no IndexedDB, no network. */
1947
+ declare function hasDeviceIdentity(): Promise<boolean>;
1948
+ /**
1949
+ * Seal and store the identity. Throws {@link DeviceStoreUnavailableError} when
1950
+ * there is nowhere to put it — callers should treat that as "this device won't
1951
+ * resume", not as a pairing failure.
1952
+ */
1953
+ declare function persistDeviceIdentity(v: PersistedDeviceIdentity): Promise<void>;
1954
+ /**
1955
+ * Recover the persisted identity, or `null` when there is none / it can't be
1956
+ * opened (wiped IndexedDB, a different browser profile, a corrupted blob).
1957
+ * Never throws — a cold start with unreadable storage must fall back to a fresh
1958
+ * pairing, not to an error screen.
1959
+ */
1960
+ declare function loadDeviceIdentity(): Promise<PersistedDeviceIdentity | null>;
1961
+ /**
1962
+ * Wipe everything: the sealed blob **and** the whole key database — so the
1963
+ * wrapping key never outlives the ciphertext it opens, and the device identity
1964
+ * key is regenerated (a re-pair is then correctly treated as a new device).
1965
+ */
1966
+ declare function clearDeviceIdentity(): Promise<void>;
1967
+
1968
+ /**
1969
+ * Passkey factor (M1.1) — WebAuthn with the **PRF extension**.
1970
+ *
1971
+ * A passkey becomes a device-bound, platform-synced source of a stable secret:
1972
+ * for a fixed salt, the authenticator's PRF returns the same 32 bytes, which we
1973
+ * HKDF into an AES key that wraps the master seed. The server never sees the
1974
+ * passkey — it's purely a local key source; the ZK proof remains the server auth.
1975
+ *
1976
+ * BROWSER ONLY (`navigator.credentials`). The PRF extension fields aren't in the
1977
+ * standard DOM types yet, so they're reached through narrow casts.
1978
+ */
1979
+ /** Whether this environment can do WebAuthn at all. (PRF support is checked at enroll.) */
1980
+ declare function passkeySupported(): boolean;
1981
+ /**
1982
+ * Whether this browser's authenticator can do the **PRF extension** our passkey
1983
+ * factor needs. A browser can save passkeys (e.g. for password autofill) yet not
1984
+ * expose PRF — PRF is what derives the stable secret we wrap the seed under, so a
1985
+ * passkey without it is useless to us.
1986
+ *
1987
+ * Uses `PublicKeyCredential.getClientCapabilities()` (the `extension:prf` cap)
1988
+ * where available. Returns:
1989
+ * - `true` — PRF is supported,
1990
+ * - `false` — definitively NOT supported (hide the option),
1991
+ * - `null` — couldn't determine (older browser without the capabilities API);
1992
+ * the caller may optimistically allow and let enroll surface a real
1993
+ * error if it turns out PRF isn't there.
1994
+ */
1995
+ declare function passkeyPrfCapable(): Promise<boolean | null>;
1996
+ /** Best-effort registrable RP id — the host. Apps spanning subdomains should pass
1997
+ * the parent domain (e.g. "muhkoo.dev") explicitly. */
1998
+ declare function defaultRpId(): string;
1999
+ /**
2000
+ * Can a passkey enrolled under `rpId` be used from the CURRENT origin?
2001
+ *
2002
+ * WebAuthn only accepts an RP id that is the origin's own host or a registrable
2003
+ * *parent* of it — so a passkey enrolled on `app.example.com` is unusable on
2004
+ * `app.example.net`, and one enrolled on a full host is unusable from a sibling
2005
+ * host. An app served on BOTH a custom domain and the platform host therefore
2006
+ * needs one passkey per origin.
2007
+ *
2008
+ * Call this BEFORE `navigator.credentials.get()`: passing a foreign rpId throws
2009
+ * a raw "The requested RPID did not match the origin or related origins", which
2010
+ * is a dead end for the user. Checking first lets the caller fall back to normal
2011
+ * sign-in (and offer to enroll a passkey for this origin) instead.
2012
+ */
2013
+ declare function rpIdUsableForOrigin(rpId: string | undefined | null, host?: string): boolean;
2014
+ /**
2015
+ * Would {@link AuthNamespace.loginWithPasskey} accept a factor recorded with
2016
+ * this `rpId`, from `host`?
2017
+ *
2018
+ * This is deliberately NOT the same as {@link rpIdUsableForOrigin}. That answers
2019
+ * a raw WebAuthn question and is `false` for a missing rpId. But a factor with
2020
+ * NO recorded rpId predates per-origin enrolment, and `loginWithPasskey`
2021
+ * resolves it as `params.rpId ?? host` — so it IS usable, from anywhere.
2022
+ *
2023
+ * Callers deciding "should I even try a passkey here?" must use this one.
2024
+ * Reaching for `rpIdUsableForOrigin` instead silently excludes every legacy
2025
+ * passkey holder, which has locked real users out of production twice.
2026
+ *
2027
+ * The `?? host` fallback applies to nullish only, matching `loginWithPasskey`:
2028
+ * an empty-string rpId stays unusable rather than resolving to the host.
2029
+ */
2030
+ declare function passkeyUsableFromOrigin(rpId: string | undefined | null, host?: string): boolean;
2031
+ /** Thrown when the account's passkey belongs to a different origin. Callers
2032
+ * should treat this as "no passkey here" and fall back to another factor. */
2033
+ declare class PasskeyOriginError extends Error {
2034
+ readonly code = "passkey_wrong_origin";
2035
+ constructor(message?: string);
2036
+ }
2037
+ interface PasskeyEnrollment {
2038
+ credentialId: Uint8Array;
2039
+ prfSalt: Uint8Array;
2040
+ prfOutput: Uint8Array;
2041
+ }
2042
+ /**
2043
+ * Create a passkey with PRF and return its credential id, a fresh PRF salt, and
2044
+ * the PRF output for that salt. Throws if the authenticator can't do PRF.
2045
+ */
2046
+ declare function createPasskeyWithPrf(opts: {
2047
+ rpId: string;
2048
+ rpName: string;
2049
+ username: string;
2050
+ }): Promise<PasskeyEnrollment>;
2051
+ /** Evaluate a passkey's PRF for `salt` → 32 bytes. Used at recover/login. */
2052
+ declare function evaluatePasskeyPrf(rpId: string, credentialId: Uint8Array, salt: Uint8Array): Promise<Uint8Array>;
2053
+
2054
+ /**
2055
+ * `PersonalSpaceClient` — the client-side counterpart to the accelerator's
2056
+ * `PersonalSpaceDO`. Wraps the `/api/personal/:commitment/*` HTTP protocol so
2057
+ * callers can `put` / `get` / `delete` / `list` per-user KV blobs without
2058
+ * thinking about challenges, nonces, or Groth16 proof generation.
2059
+ *
2060
+ * Every gated operation:
2061
+ * 1. Requests a one-shot challenge from the DO (`POST /challenge`).
2062
+ * 2. Converts the returned hex nonce into a BN254 field element.
2063
+ * 3. Generates a fresh Groth16 proof for the `preimagePoK` circuit using
2064
+ * snarkjs, with the user's secret/salt/ecdsaPub as private inputs and
2065
+ * `{commitment, nonce, ecdsaPubHash}` as public signals.
2066
+ * 4. POSTs the proof + publicSignals + (optional) value to the gated endpoint.
2067
+ *
2068
+ * snarkjs is loaded lazily (dynamic `import`) the first time a proof is
2069
+ * generated — it's an optional peer dependency that only the ZK-proof path
2070
+ * needs, so importing this module (or constructing a `Client`) never requires
2071
+ * it. Consumers using the proof path install `snarkjs`, or provide it via an
2072
+ * import map in the browser (see the README).
2073
+ *
2074
+ * Encryption of the stored values is the caller's responsibility — the DO
2075
+ * sees only opaque JSON. The companion `wrap.ts` helpers in this folder are
2076
+ * a convenient way to passphrase-encrypt a payload before putting it.
2077
+ */
2078
+
2079
+ /**
2080
+ * Constructor options for {@link PersonalSpaceClient}.
2081
+ *
2082
+ * All of `secret`, `salt`, `ecdsaPub`, and `ecdsaPubHash` are decimal-encoded
2083
+ * BigInt strings (snarkjs convention). `commitment` is likewise decimal — it's
2084
+ * the Poseidon hash of `(secret, salt, ecdsaPubHash)`.
2085
+ */
2086
+ interface PersonalSpaceClientOptions {
2087
+ /** Base URL of the accelerator, e.g. `http://localhost:8787`. */
2088
+ baseUrl: string;
2089
+ /** The user's Poseidon commitment as a decimal string (~78 digits). */
2090
+ commitment: string;
2091
+ /** ZK identity material — private inputs to the proof. Decimal BigInt string. */
2092
+ secret: string;
2093
+ /** ZK identity salt. Decimal BigInt string. */
2094
+ salt: string;
2095
+ /** Raw ECDSA public-key field representation. Decimal BigInt string. */
2096
+ ecdsaPub: string;
2097
+ /** Poseidon hash of `ecdsaPub`, used as a public signal. Decimal BigInt string. */
2098
+ ecdsaPubHash: string;
2003
2099
  /**
2004
- * Verify a Google ID token (M2.3). With a session `token` it's the ENROLL
2005
- * gate; without, it's RECOVER/LOGIN (the Google account must already be
2006
- * linked). Returns a single-use verifyToken bound to the account's `sub`.
2100
+ * Where the runtime can fetch the compiled circuit assets, and the digests
2101
+ * to verify them against. Prefer `defaultCircuitUrls(baseUrl)`, which pins
2102
+ * them — this client passes `secret` and `salt` to the witness generator as
2103
+ * private witnesses, so an unverified one can exfiltrate them. See
2104
+ * `src/auth/circuitIntegrity.ts`.
2007
2105
  */
2008
- googleVerify(opts: {
2009
- idToken: string;
2010
- token?: string;
2011
- username?: string;
2012
- }): Promise<{
2013
- verifyToken: string;
2014
- sub: string;
2015
- emailHint: string | null;
2016
- }>;
2017
- /** Mint a single-use authorization code for an app (called by the hosted page; session-authed). */
2018
- grant(token: string, body: {
2019
- appId: string;
2020
- redirectUri: string;
2021
- codeChallenge: string;
2022
- sealedKeys: string;
2023
- }): Promise<{
2024
- code: string;
2025
- }>;
2026
- /** Exchange an authorization code + PKCE verifier for the session + sealed key material. */
2027
- token(body: {
2028
- code: string;
2029
- codeVerifier: string;
2030
- appId?: string;
2031
- }): Promise<{
2032
- sessionToken: string;
2033
- username: string;
2034
- commitment: string;
2035
- sealedKeys: string;
2036
- appId: string;
2037
- }>;
2106
+ circuits: CircuitIntegrity & {
2107
+ /** URL of the `preimagePoK.wasm` witness generator. */
2108
+ wasmUrl: string;
2109
+ /** URL of the `preimagePoK_0001.zkey` proving key. */
2110
+ zkeyUrl: string;
2111
+ };
2112
+ }
2113
+ declare class PersonalSpaceClient {
2114
+ private readonly baseUrl;
2115
+ private readonly commitment;
2116
+ private readonly secret;
2117
+ private readonly salt;
2118
+ private readonly ecdsaPub;
2119
+ private readonly ecdsaPubHash;
2120
+ private readonly circuits;
2121
+ constructor(opts: PersonalSpaceClientOptions);
2122
+ /** Store `value` under `key`, replacing any prior value. */
2123
+ put(key: string, value: unknown): Promise<void>;
2124
+ /** Fetch the value stored under `key`. Returns `null` if absent. */
2125
+ get<T = unknown>(key: string): Promise<T | null>;
2126
+ /** Remove `key` from the user's space. Returns whether the key existed. */
2127
+ delete(key: string): Promise<boolean>;
2128
+ /** List every key currently stored in the user's space. */
2129
+ list(): Promise<string[]>;
2038
2130
  /**
2039
- * Read a factor record for `username` (returns an indistinguishable decoy if
2040
- * absent). Gated types (email) require the recover `verifyToken`.
2131
+ * Run the full pre-flight: ask the DO for a challenge, reduce the nonce to
2132
+ * a field element, and generate a fresh Groth16 proof. Returns everything
2133
+ * the gated endpoints need in their body.
2041
2134
  */
2042
- vaultRead(username: string, factorType: VaultFactor["type"], verifyToken?: string,
2043
- /** Passkeys are per-origin: naming the caller's host returns THAT origin's
2044
- * passkey instead of whichever was enrolled first (which WebAuthn would
2045
- * refuse). Ignored for other factor types. */
2046
- rpId?: string,
2047
- /** Device factors are selected by id — a user may have many machines. */
2048
- factorId?: string): Promise<{
2049
- factor: VaultFactor | null;
2050
- }>;
2135
+ private proveFreshChallenge;
2136
+ /** Hit `POST /challenge` and return the issued nonce + ID. */
2137
+ private requestChallenge;
2051
2138
  /**
2052
- * Add/replace a factor (session-authed; the seed is already wrapped
2053
- * client-side). Email factors additionally require (and consume) the
2054
- * enroll `verifyToken`; the stored address must be the verified one.
2139
+ * Thin wrapper over `globalThis.fetch` so all methods share a single set of
2140
+ * headers and body-encoding behaviour. `globalThis.fetch` is available in
2141
+ * modern browsers, Node 18+, Workers, and Bun.
2055
2142
  */
2056
- vaultPutFactor(token: string, factor: VaultFactor, verifyToken?: string): Promise<{
2057
- ok: boolean;
2058
- id: string;
2059
- }>;
2060
- /** List the user's factor metadata (no ciphertext). Session-authed. */
2061
- vaultFactors(token: string): Promise<{
2062
- factors: VaultFactor[];
2063
- migrated: boolean;
2064
- }>;
2065
- /** Remove a factor by id (can't remove the last one). Session-authed. */
2066
- vaultDeleteFactor(token: string, id: string): Promise<{
2067
- deleted: boolean;
2068
- }>;
2069
- private json;
2070
- private parse;
2143
+ private fetch;
2144
+ }
2145
+
2146
+ /**
2147
+ * Wire types for the storage layer.
2148
+ *
2149
+ * The `FileManifest` is the single piece of metadata the system needs to
2150
+ * reassemble a file: per-chunk encryption material, RS parameters, and the
2151
+ * SHA-256 content hashes of every shard that belongs to the chunk. With the
2152
+ * manifest in hand, a reader can pull each shard from the open content-
2153
+ * addressed shard store, decode, decrypt, and concatenate to recover the
2154
+ * original bytes.
2155
+ *
2156
+ * The manifest itself lives inside a `SharedSpaceDO` (Phase B) which gates
2157
+ * access by ZK proof + ACL. Shards are stored in the open content-addressed
2158
+ * `ShardStoreDO` — anyone can put/get any shard, but without the manifest
2159
+ * (which has the keys) the ciphertext is just noise.
2160
+ */
2161
+
2162
+ /**
2163
+ * One chunk's metadata. A file is the concatenation of `chunks` in order.
2164
+ *
2165
+ * - `originalSize` is the chunk's plaintext length, before AES-GCM expanded
2166
+ * it by 16 bytes for the auth tag and before RS padded it for shard
2167
+ * alignment.
2168
+ * - `ciphertextSize` is the AES-GCM output length (plaintext + 16). This is
2169
+ * what the RS decoder trims back to.
2170
+ * - `shardHashes` lists every shard's SHA-256 in encoding order — the first
2171
+ * `dataShards` are data shards, the trailing `parityShards` are parity.
2172
+ */
2173
+ interface ChunkManifest {
2174
+ /** Stable per-chunk ID (uuid-like). Useful for client-side resume / dedup. */
2175
+ id: string;
2176
+ /** Position of this chunk in the file (0-indexed). */
2177
+ chunkIndex: number;
2178
+ /** Plaintext byte length of the chunk before encryption. */
2179
+ originalSize: number;
2180
+ /** AES-GCM ciphertext byte length (originalSize + 16 for the tag). */
2181
+ ciphertextSize: number;
2182
+ /** Number of RS data shards. */
2183
+ dataShards: number;
2184
+ /** Number of RS parity shards. */
2185
+ parityShards: number;
2186
+ /** Length in bytes of every shard (data and parity are equal). */
2187
+ shardSize: number;
2188
+ /** SHA-256 (hex) of each shard's ciphertext bytes, in shard order. */
2189
+ shardHashes: string[];
2190
+ /** Per-chunk AES-256-GCM key + IV. Plain in the manifest — gating happens at the SpaceDO. */
2191
+ cipher: ChunkKeyMaterial;
2192
+ }
2193
+ /**
2194
+ * Top-level manifest for a single stored file. Persisted in the user's
2195
+ * SharedSpace by file id; readable / writable per the space's ACL.
2196
+ */
2197
+ interface FileManifest {
2198
+ /** Stable file ID. Assigned by the SDK at write time; used to read or delete. */
2199
+ id: string;
2200
+ /** Human-readable filename (display only — not used by the shard store). */
2201
+ name: string;
2202
+ /** Total plaintext byte length. Sum of chunk `originalSize`s. */
2203
+ size: number;
2204
+ /** MIME type the application supplied at write time. Opaque to storage. */
2205
+ type: string;
2206
+ /** Optional virtual path / folder hint for the application. */
2207
+ path?: string;
2208
+ /** When the file was last modified per the application (ms epoch). */
2209
+ lastModified: number;
2210
+ /** When this manifest was created on the SDK side (ms epoch). */
2211
+ createdAt: number;
2212
+ /** Ordered list of chunks. Concatenate them in index order to rebuild the file. */
2213
+ chunks: ChunkManifest[];
2214
+ /** SDK schema version. Bump on breaking manifest-shape changes. */
2215
+ version: 1;
2216
+ }
2217
+ /**
2218
+ * Lightweight summary returned by `FileStorage.writeFile` and surfaced in
2219
+ * directory-style listings. The full manifest is only fetched when actually
2220
+ * reassembling the file.
2221
+ */
2222
+ interface FileStat {
2223
+ id: string;
2224
+ name: string;
2225
+ size: number;
2226
+ type: string;
2227
+ path?: string;
2228
+ lastModified: number;
2229
+ createdAt: number;
2230
+ chunkCount: number;
2231
+ shardCount: number;
2071
2232
  }
2233
+ /** Distil a `FileManifest` into a `FileStat`. */
2234
+ declare function manifestToStat(manifest: FileManifest): FileStat;
2072
2235
 
2073
2236
  /**
2074
- * Deterministic ZK identity derivation from `(username, password)`.
2237
+ * HTTP client for the open content-addressed shard store.
2075
2238
  *
2076
- * Same inputs on ANY device produce the same secret, salt, and P-256
2077
- * ECDSA + ECDH keypairs — which is what makes federated login from a
2078
- * fresh browser possible. There is no localStorage involvement: every
2079
- * register and login call re-derives the identity on demand.
2239
+ * Each shard is named by the lowercase hex SHA-256 of its ciphertext bytes.
2240
+ * The server verifies the hash on PUT, so the address authenticates the
2241
+ * contents — there's no benefit to ACLing PUT/GET, and making them open
2242
+ * enables cross-user dedup down the line.
2080
2243
  *
2081
- * Pipeline:
2082
- * master_seed = PBKDF2-SHA256(password, "muhkoo-zk-v1:" || username, 200k iters)
2083
- * secret = HKDF-Expand-SHA256(master_seed, "zk-secret", 32 bytes) → hex
2084
- * salt = HKDF-Expand-SHA256(master_seed, "zk-salt", 32 bytes) → hex
2085
- * ecdsa_priv = HKDF-Expand-SHA256(master_seed, "ecdsa-priv", 32 bytes)
2086
- * ecdh_priv = HKDF-Expand-SHA256(master_seed, "ecdh-priv", 32 bytes)
2244
+ * Wire protocol:
2245
+ * PUT /api/shards/:hash body: shard bytes (application/octet-stream)
2246
+ * 200 { ok: true, hash, size, dedup: boolean }
2247
+ * 400 if hash doesn't match body
2087
2248
  *
2088
- * P-256 public points are computed via `@noble/curves` (WebCrypto has no
2089
- * derive-public-from-private-scalar API), then both keypairs are imported
2090
- * into SubtleCrypto in JWK form so the rest of the crypto stack can keep
2091
- * using `crypto.subtle.sign` / `crypto.subtle.deriveBits`.
2249
+ * GET /api/shards/:hash 200 with shard bytes (application/octet-stream)
2250
+ * 404 if absent
2092
2251
  *
2093
- * Originally lived in `accelerator/public/js/zk-identity.js` and was ported
2094
- * to `muhkoo/web/src/zk/identity.ts`; consolidated here so the SDK owns
2095
- * identity derivation end-to-end.
2252
+ * HEAD /api/shards/:hash 200 if present (no body) — used by callers to
2253
+ * skip a PUT they'd otherwise dedup-rewrite. The
2254
+ * server can ignore the optimization if it wants.
2096
2255
  */
2097
- interface ZkIdentity {
2098
- /** 32-byte secret, hex-encoded. Private witness for the ZK circuit. */
2099
- secretHex: string;
2100
- /** 32-byte salt, hex-encoded. Private witness for the ZK circuit. */
2101
- saltHex: string;
2102
- /** P-256 ECDSA keypair, used to sign auth proofs. */
2103
- ecdsaKeyPair: CryptoKeyPair;
2104
- /** P-256 ECDH keypair, used by the chat ratchet. */
2105
- ecdhKeyPair: CryptoKeyPair;
2106
- }
2107
2256
  /**
2108
- * Derive a deterministic ZK identity from `(username, password)`.
2109
- *
2110
- * Same inputs always produce the same identity, on any device — that's
2111
- * what makes federated login work without dragging encrypted state across
2112
- * devices.
2257
+ * A content-addressed byte cache for shards (the offline layer's `ShardCache`
2258
+ * satisfies this structurally). Declared here as a minimal interface so the
2259
+ * storage layer doesn't import the offline module — keeping the dependency
2260
+ * arrow one-way (offline → storage).
2113
2261
  */
2114
- declare function deriveIdentity(username: string, password: string): Promise<ZkIdentity>;
2262
+ interface ShardByteCache {
2263
+ get(hash: string): Promise<Uint8Array | null>;
2264
+ put(hash: string, bytes: Uint8Array): Promise<void>;
2265
+ }
2115
2266
  /**
2116
- * Legacy master-seed derivation: `PBKDF2(password, "muhkoo-zk-v1:"||username)`.
2117
- *
2118
- * This was the *only* path pre-vault, where the identity descended directly from
2119
- * the password. It's kept so existing accounts still re-derive their seed (and
2120
- * the vault can adopt that exact seed on migration, preserving the commitment).
2121
- * New accounts use a random seed (`vault.randomSeed`) instead, so their identity
2122
- * is no longer a function of the password.
2267
+ * A peer block source (the p2p layer's `PeerExchange` satisfies this
2268
+ * structurally). Consulted between the local cache and origin: a block fetched
2269
+ * from a peer is verified by hash before use. Declared here so the storage layer
2270
+ * doesn't import the p2p module (dependency arrow stays p2p → storage).
2123
2271
  */
2124
- declare function deriveMasterSeedFromPassword(username: string, password: string): Promise<Uint8Array>;
2272
+ interface PeerBlockSource {
2273
+ getBlock(hash: string, opts?: {
2274
+ timeoutMs?: number;
2275
+ }): Promise<Uint8Array | null>;
2276
+ announce(hash: string): void;
2277
+ }
2278
+ interface ShardClientOptions {
2279
+ /** Base URL of the accelerator (e.g. `https://muhkoo-accelerator.workers.dev`). */
2280
+ baseUrl: string;
2281
+ /**
2282
+ * Path prefix for the shard endpoints, joined to `baseUrl` before the
2283
+ * hash. Defaults to `/api/shards`, which targets a global shard store
2284
+ * (e.g. a dedicated `ShardStoreDO`). Pass a room-scoped value like
2285
+ * `/api/spaces/{roomId}/shards` to keep shards bound to a specific
2286
+ * `SharedSpaceDO` — that's the path the chat application uses today.
2287
+ *
2288
+ * Leading slash optional; trailing slash stripped. The final URL is
2289
+ * always `${baseUrl}${normalizedPrefix}/${encodeURIComponent(hash)}`.
2290
+ */
2291
+ pathPrefix?: string;
2292
+ /**
2293
+ * Optional custom fetch implementation. Defaults to `globalThis.fetch`.
2294
+ * Provided so callers can inject retries / auth / instrumentation.
2295
+ */
2296
+ fetch?: typeof fetch;
2297
+ /**
2298
+ * Optional offline byte cache. When present, GETs are served cache-first and
2299
+ * populated on success, and PUTs write the cache before the network so a
2300
+ * just-uploaded file reads back offline. See {@link ShardByteCache}.
2301
+ */
2302
+ cache?: ShardByteCache;
2303
+ /**
2304
+ * Coalesce concurrent origin reads into `POST {pathPrefix}/batch`. On by
2305
+ * default: it is what keeps opening a project from costing one request per
2306
+ * shard. Set false to force one-at-a-time — useful against a server without
2307
+ * the route, though that is detected and handled automatically.
2308
+ */
2309
+ batchShards?: boolean;
2310
+ /**
2311
+ * Called when a PUT can't reach the network (offline). Lets the caller queue
2312
+ * the upload for replay on reconnect; the bytes are already in {@link cache}
2313
+ * keyed by `hash`, so only the hash needs recording.
2314
+ */
2315
+ deferUpload?: (hash: string) => Promise<void>;
2316
+ /**
2317
+ * Optional peer block source. When present, GETs try peers between the local
2318
+ * cache and origin (peer blocks are verified by hash); PUTs announce the new
2319
+ * block to peers. Best-effort — any failure falls through to origin.
2320
+ */
2321
+ peers?: PeerBlockSource;
2322
+ /**
2323
+ * When true, {@link ShardClient.putShard} returns as soon as the shard is
2324
+ * written to {@link cache} and announced to {@link peers} — the origin PUT is
2325
+ * fired in the BACKGROUND rather than awaited. This makes a large write's
2326
+ * manifest available immediately (so a peer/worker can start pulling the
2327
+ * shards over P2P right away) while origin catches up as a durable fallback.
2328
+ * On a background-PUT failure the shard is handed to {@link deferUpload} (if
2329
+ * present) for replay. Requires a {@link cache} so the bytes survive locally
2330
+ * until the background upload lands. No-op without a cache.
2331
+ */
2332
+ backgroundUpload?: boolean;
2333
+ }
2125
2334
  /**
2126
- * Derive the (unchanged) ZK identity from a 32-byte **master seed**. The seed is
2127
- * HKDF-expanded into secret/salt + the P-256 ECDSA & ECDH keypairs exactly as
2128
- * before — so the resulting commitment is byte-identical for a given seed. The
2129
- * vault wraps THIS seed under each recovery factor.
2335
+ * Compute the lowercase hex SHA-256 of `bytes`. Available via WebCrypto in
2336
+ * every runtime this SDK targets.
2130
2337
  */
2131
- declare function deriveIdentityFromSeed(seed: Uint8Array): Promise<ZkIdentity>;
2338
+ declare function shardHash(bytes: Uint8Array): Promise<string>;
2339
+ declare class ShardClient {
2340
+ private readonly baseUrl;
2341
+ private readonly pathPrefix;
2342
+ private readonly fetchFn;
2343
+ private readonly cache?;
2344
+ /** Hashes waiting for the next batch, each with everyone awaiting it. */
2345
+ private pendingShards;
2346
+ private batchTimer;
2347
+ /** False once the server is known not to have the batch route. */
2348
+ private batchSupported;
2349
+ private readonly deferUpload?;
2350
+ private readonly peers?;
2351
+ private readonly backgroundUpload;
2352
+ constructor(opts: ShardClientOptions);
2353
+ /** Build the URL for a given shard hash — exposed for callers that want to log or pre-resolve. */
2354
+ urlForHash(hash: string): string;
2355
+ /**
2356
+ * Upload a shard. The server verifies SHA-256(body) matches `:hash` and
2357
+ * rejects with 400 if not, so the caller is the source of truth on
2358
+ * naming. Re-uploading an existing shard is idempotent — the server
2359
+ * returns `dedup: true` in that case.
2360
+ */
2361
+ putShard(hash: string, bytes: Uint8Array): Promise<{
2362
+ dedup: boolean;
2363
+ }>;
2364
+ private bgActive;
2365
+ private readonly bgQueue;
2366
+ private static readonly BG_CONCURRENCY;
2367
+ /** Queue a shard's origin PUT to run in the background under a concurrency
2368
+ * cap. Bytes are re-read from {@link cache} at upload time (not held in the
2369
+ * queue) so a large file's backlog doesn't pin a second copy in memory. */
2370
+ private enqueueBackgroundUpload;
2371
+ /** PUT the shard bytes to origin, verifying the server accepted them.
2372
+ * Used by the background-upload path; throws on network/HTTP failure. */
2373
+ private uploadToOrigin;
2374
+ /**
2375
+ * Fetch a shard by hash. Throws if the server returns a non-200; returns
2376
+ * `null` for 404 so callers can distinguish "missing" from "errored".
2377
+ */
2378
+ getShard(hash: string): Promise<Uint8Array | null>;
2379
+ /**
2380
+ * Fetch from origin, COALESCING concurrent requests into one batch.
2381
+ *
2382
+ * A shard is not a file. A project is thousands of small source modules,
2383
+ * each split into shards, so reading one at a time made opening a project
2384
+ * cost thousands of requests — 2,086 for a 519-file project. Latency, not
2385
+ * bytes, is what that spends: Chromium took 18.4s over those requests and
2386
+ * Brave, which filters every one, took 37.5s for identical work. Batching
2387
+ * collapses whatever is in flight at the same moment into a single POST,
2388
+ * which is why it helps most exactly where it hurt most.
2389
+ *
2390
+ * Callers are unchanged: `getShard` still asks for one shard and gets one
2391
+ * shard. The window is a few milliseconds — long enough to gather the reads
2392
+ * a concurrent walk issues together, short enough not to be felt by a lone
2393
+ * read.
2394
+ *
2395
+ * Falls back permanently to one-at-a-time if the server has no batch route,
2396
+ * so a new SDK keeps working against an older deployment.
2397
+ */
2398
+ private fetchFromOrigin;
2399
+ /** Send one batch of whatever has accumulated, and settle those callers. */
2400
+ private flushShardBatch;
2401
+ /** Fetch a shard from origin. Returns `null` for 404 (or offline when a
2402
+ * cache is present, so RS recovery can proceed); throws otherwise. */
2403
+ private fetchOneFromOrigin;
2404
+ /**
2405
+ * Check whether a shard exists. Lets the caller skip a PUT they'd otherwise
2406
+ * dedup-rewrite. The server can implement this as a HEAD or return a small
2407
+ * JSON body — we don't depend on either.
2408
+ */
2409
+ hasShard(hash: string): Promise<boolean>;
2410
+ }
2132
2411
 
2133
2412
  /**
2134
- * Poseidon hash wrapper used by the ZK auth flow.
2135
- *
2136
- * Backed by `circomlibjs`'s `buildPoseidon`. Builder is lazy and cached
2137
- * because the JS-side Babyjubjub field reps take ~100ms to construct, and
2138
- * we'd rather not pay that on every page load.
2139
- *
2140
- * Returns hashes as decimal-string BigInts — same format snarkjs and the
2141
- * `preimagePoK` circuit expect for inputs.
2413
+ * HTTP client for the gated file-manifest store on a SharedSpace
2414
+ * (`/api/spaces/:spaceId/files`).
2142
2415
  *
2143
- * Note: circomlibjs is `optionalDependencies` in connect's package.json.
2144
- * Consumers that use the ZK auth flow (browser/server, not workers) need
2145
- * it installed. The workers build does not import this module.
2146
- */
2147
- /**
2148
- * Poseidon-hash `inputs` and return the result as a decimal-string BigInt.
2149
- * Inputs are converted to BigInt — pass either bigints, numbers, or numeric
2150
- * strings.
2151
- */
2152
- declare function poseidonHash(inputs: Array<bigint | string | number>): Promise<string>;
2153
-
2154
- /**
2155
- * Groth16 proof generation for the `preimagePoK` circuit.
2416
+ * A "file" in a space is its **manifest** (per-chunk keys + the SHA-256 of each
2417
+ * global shard). The shard bytes live in the open `/api/shards` store; the
2418
+ * manifest is the capability. This client only moves manifests in/out of the
2419
+ * space's gated store.
2156
2420
  *
2157
- * The circuit proves knowledge of `(secret, salt, ecdsaPub)` such that
2158
- * commitment = Poseidon(secret, salt, Poseidon(ecdsaPub))
2159
- * while only revealing `(commitment, nonce, ecdsaPubHash)` as public signals.
2421
+ * Authorization is **session-based**, consistent with the rest of the
2422
+ * SharedSpace (keyring, websocket): the SDK's credential-stamping `fetch`
2423
+ * (from {@link HttpClient}) attaches the app key + session token, the platform
2424
+ * validates the session and injects a trusted `X-Muhkoo-User-Context`, and the
2425
+ * DO authorizes on that + channel membership. There is no per-op Groth16 proof.
2160
2426
  *
2161
- * Circuit assets (wasm + zkey) are served by the accelerator worker. Their
2162
- * URLs are a required argument of {@link generateAuthProof} — there is no
2163
- * default. The web app constructs them relative to its configured worker
2164
- * `baseUrl` so the SPA can target either the local wrangler dev or a deployed
2165
- * worker depending on `VITE_WORKER_URL`.
2427
+ * Wire protocol (server: `SharedSpaceDO`):
2428
+ * POST /api/spaces/:spaceId/files body: { manifest } → { ok, fileId }
2429
+ * GET /api/spaces/:spaceId/files → { files: FileStat[] }
2430
+ * GET /api/spaces/:spaceId/files/:fileId → { manifest }
2431
+ * DELETE /api/spaces/:spaceId/files/:fileId → { ok, existed }
2166
2432
  */
2167
2433
 
2168
- interface CircuitUrls {
2169
- /** preimagePoK.wasm — the circuit's witness generator. */
2170
- wasmUrl: string;
2171
- /** preimagePoK_0001.zkey — the proving key. */
2172
- zkeyUrl: string;
2434
+ interface SharedSpaceClientOptions {
2435
+ /** Base URL of the accelerator (e.g. `https://api.muhkoo.dev`). */
2436
+ baseUrl: string;
2437
+ /**
2438
+ * Credential-stamping fetch — pass {@link HttpClient.fetch} so requests
2439
+ * carry the app key + session token. Defaults to `globalThis.fetch`
2440
+ * (unauthenticated; only useful in tests).
2441
+ */
2442
+ fetch?: typeof fetch;
2443
+ }
2444
+ declare class SharedSpaceClient {
2445
+ private readonly baseUrl;
2446
+ private readonly fetchFn;
2447
+ constructor(opts: SharedSpaceClientOptions);
2448
+ /** Write a file manifest into the space. Requires space membership. */
2449
+ writeFileManifest(spaceId: string, manifest: FileManifest): Promise<void>;
2450
+ /** Read a file manifest by id. Requires space membership. */
2451
+ readFileManifest(spaceId: string, fileId: string): Promise<FileManifest>;
2452
+ /** Delete a file manifest by id. Requires space membership. */
2453
+ deleteFileManifest(spaceId: string, fileId: string): Promise<boolean>;
2454
+ /** List file summaries in the space. Requires space membership. */
2455
+ listFiles(spaceId: string): Promise<FileStat[]>;
2456
+ private filesBase;
2457
+ private jsonRequest;
2173
2458
  }
2174
- /**
2175
- * Canonical {@link CircuitUrls} for the `preimagePoK` login circuit, anchored
2176
- * at a base URL where the accelerator serves the circuit assets out of
2177
- * `circuits/build/`. The filenames here are fixed by the circom build output
2178
- * (the witness generator lands inside a `preimagePoK_js/` directory); callers
2179
- * shouldn't hard-code them. `Client` uses this to default its `circuits`
2180
- * option, and `PersonalSpaceClient` consumers can pass `defaultCircuitUrls(baseUrl)`.
2181
- */
2182
- declare function defaultCircuitUrls(baseUrl: string): CircuitUrls;
2183
- /**
2184
- * Reduce any hex/decimal/byte input to a decimal-string field element in
2185
- * `[0, FIELD_SIZE)`. snarkjs and the `preimagePoK` circuit both want decimal
2186
- * strings; everything else gets normalized here.
2187
- *
2188
- * **Behavior is intentionally byte-for-byte compatible with the legacy
2189
- * `accelerator/public/js/zk-snarkjs-client.js#toField`.** Diverging produces
2190
- * different commitments than accounts registered via the legacy flow,
2191
- * which locks those users out forever — the worker stores whatever
2192
- * commitment the client sent at register time and compares it as a string
2193
- * at login time.
2194
- *
2195
- * In particular: a 32+ char string of hex chars is always treated as hex,
2196
- * even if every character happens to be 0-9 with no a-f letters. An
2197
- * earlier version of this function gated the hex branch on "has at least
2198
- * one letter" — that broke the digit-only-hex edge case (rare in practice,
2199
- * but real). DO NOT add that letter check back.
2200
- */
2201
- declare function toField(value: string | bigint | number): string;
2202
- /**
2203
- * Build the user's Poseidon commitment binding `(secret, salt, ecdsaPubHash)`.
2204
- * Same formula the worker verifies against on register and authenticate.
2205
- */
2206
- declare function buildCommitment(secretHex: string, saltHex: string, ecdsaPubHex: string): Promise<string>;
2207
- /**
2208
- * Generate a Groth16 proof for the `preimagePoK` circuit. Public signals end
2209
- * up `[commitment, nonce, ecdsaPubHash]` — the worker checks the commitment
2210
- * matches the stored one and the nonce matches the issued challenge nonce
2211
- * reduced into the field.
2212
- *
2213
- * Throws if circuit URLs are unreachable or snarkjs decides the inputs are
2214
- * malformed.
2215
- */
2216
- declare function generateAuthProof(args: {
2217
- secretHex: string;
2218
- saltHex: string;
2219
- ecdsaPubHex: string;
2220
- nonceHex: string;
2221
- /** Required — caller supplies URLs scoped to the worker they want to talk to. */
2222
- circuits: CircuitUrls;
2223
- }): Promise<{
2224
- proof: Groth16Proof;
2225
- publicSignals: string[];
2226
- commitment: string;
2227
- nonceField: string;
2228
- ecdsaPubHash: string;
2229
- }>;
2230
-
2231
- /**
2232
- * Conversions between SubtleCrypto `CryptoKey`s and the wire formats the
2233
- * accelerator's auth endpoints + `preimagePoK` circuit expect.
2234
- *
2235
- * Two outputs we need from an ECDSA `CryptoKey`:
2236
- * - **base64 raw** — passed to `/api/auth/zk-register` as
2237
- * `ecdsaPublicKey` / `ecdhPublicKey`. (Name is historical;
2238
- * the legacy client called it `exportPublicKeyToBase58`
2239
- * but actually used base64 — kept as base64.)
2240
- * - **hex raw** — fed to the proof as `ecdsaPub` input; the circuit
2241
- * reduces it into a field element.
2242
- *
2243
- * Both go through `crypto.subtle.exportKey("raw", ...)` which yields the
2244
- * uncompressed SEC1 form (`0x04 || X || Y`).
2245
- */
2246
- /** Raw uncompressed (`0x04 || X || Y`) public key bytes, as lowercase hex. */
2247
- declare function exportPublicKeyHex(publicKey: CryptoKey): Promise<string>;
2248
- /** Raw uncompressed public key as base64 — wire format for the auth endpoints. */
2249
- declare function exportPublicKeyBase64(publicKey: CryptoKey): Promise<string>;
2250
- /** Sign a UTF-8 `message` with an ECDSA private key. Returns base64. */
2251
- declare function signMessage(message: string, privateKey: CryptoKey): Promise<string>;
2252
2459
 
2253
2460
  /**
2254
- * Hosted-auth handoff crypto (auth.muhkoo.dev — AUTH_HOSTED_PLAN.md §3).
2255
- *
2256
- * The hosted page authenticates the user and ends up holding the master seed.
2257
- * To pass it to the developer's app without it ever sitting server-readable or
2258
- * landing in browser history:
2259
- *
2260
- * 1. {@link sealSeed} generates a one-time key K_t, AES-256-GCM-seals the seed
2261
- * under it, and returns the ciphertext envelope (`sealedKeys`, stored in
2262
- * the grant the server relays but can't read) plus K_t (`fragmentKey`,
2263
- * placed ONLY in the redirect URL fragment — fragments never hit a server).
2264
- * 2. The app exchanges the code for `sealedKeys`, reads K_t from the fragment,
2265
- * and {@link unsealSeed} recovers the seed, then strips the fragment.
2266
- *
2267
- * Trust-equivalent to today's embedded SDK (the app still ends up with the
2268
- * seed) while moving credential entry onto the Muhkoo origin. The envelope is
2269
- * versioned so a future per-app-key handoff (v2) can swap in without a protocol
2270
- * change.
2271
- *
2272
- * ## v2 — device pairing (TV / no-redirect handoff)
2461
+ * `FileStorage` — top-level entry point for chunked, encrypted, erasure-coded
2462
+ * file storage.
2273
2463
  *
2274
- * v1's K_t rides in the redirect URL *fragment*, which works only because the
2275
- * receiving app is a browser page that can read `location.hash`. A TV app has
2276
- * no redirect to receive: it polls. So v2 keeps the *entire* v1 shape — the
2277
- * server relays a sealed envelope it cannot read — and replaces only the key
2278
- * agreement:
2464
+ * Pipeline for writing a file:
2465
+ * 1. Split the plaintext into fixed-size chunks (default 4 MiB).
2466
+ * 2. For each chunk:
2467
+ * a. Generate a fresh AES-256-GCM key + 12-byte IV (`ChunkCipher`).
2468
+ * b. Encrypt the chunk → ciphertext (plaintext.length + 16 bytes tag).
2469
+ * c. Reed–Solomon encode the ciphertext into `dataShards + parityShards`
2470
+ * equal-length shards (`ReedSolomonCodec`).
2471
+ * d. PUT every shard to the content-addressed shard store, keyed by
2472
+ * SHA-256 of the shard bytes (`ShardClient`).
2473
+ * 3. Build a `FileManifest` listing every chunk's keys, IVs, sizes, and
2474
+ * the SHA-256 of every shard in order.
2279
2475
  *
2280
- * - the TV generates an ephemeral P-256 ECDH keypair in memory and publishes
2281
- * the public half ({@link generateDevicePairingKeypair});
2282
- * - the hosted page (the component that already holds the unlocked seed and
2283
- * already owns sealing) seals *to that public key* with a fresh ephemeral
2284
- * keypair of its own ({@link sealSeedToDevice}) — ECDH → HKDF-SHA256 →
2285
- * AES-256-GCM;
2286
- * - the TV recovers the seed with its private key
2287
- * ({@link unsealSeedFromDevice}).
2476
+ * Two ways to use the manifest:
2477
+ * - {@link FileStorage.writeFileToShards} returns it to the caller, who is
2478
+ * then responsible for delivering it to readers (e.g. by E2E-encrypting
2479
+ * it as a chat message). The chat application takes this path.
2480
+ * - {@link FileStorage.writeFile} additionally POSTs the manifest to a
2481
+ * gated multi-user space (`SharedSpaceClient`) so any participant of the
2482
+ * space can read the file by manifest id later.
2288
2483
  *
2289
- * The server stores and relays the v2 envelope byte-for-byte, exactly as it
2290
- * does for v1, and can read neither. The one thing ECDH cannot supply is proof
2291
- * that the public key the hosted page sealed to is *the TV's* rather than an
2292
- * attacker's — that is what {@link pairingVerificationCode} is for, and it is
2293
- * the only MITM defence in the flow (see the entropy note on that function).
2294
- */
2295
- interface SealedHandoff {
2296
- /** base64 JSON envelope — relayed by the server in the grant; server-opaque. */
2297
- sealedKeys: string;
2298
- /** base64url one-time key — rides ONLY in the redirect URL fragment. */
2299
- fragmentKey: string;
2300
- }
2301
- /** Seal the master seed under a fresh one-time key for the redirect handoff. */
2302
- declare function sealSeed(seed: Uint8Array): Promise<SealedHandoff>;
2303
- /** Recover the master seed from the sealed envelope + the fragment key. */
2304
- declare function unsealSeed(sealedKeys: string, fragmentKey: string): Promise<Uint8Array>;
2305
- /**
2306
- * Alphabet for the pairing verification code: 30 symbols, every confusable pair
2307
- * removed in BOTH directions — no `0`/`O`, no `1`/`I`/`L`, and no `U` (which is
2308
- * read as `V` on the low-resolution, far-viewed type a TV renders). Because the
2309
- * user compares this string across two screens by eye, unambiguity beats the
2310
- * convenience of a power-of-two alphabet; the derivation below rejection-samples
2311
- * to avoid the modulo bias that costs us.
2484
+ * Reading is the inverse: {@link FileStorage.readFileFromShards} takes a
2485
+ * manifest and reassembles the plaintext; {@link FileStorage.readFile} first
2486
+ * fetches the manifest from the space.
2312
2487
  */
2313
- declare const PAIRING_CODE_ALPHABET = "ABCDEFGHJKMNPQRSTVWXYZ23456789";
2314
- /** Length of the pairing verification code, in characters. 30^8 ≈ 39.3 bits. */
2315
- declare const PAIRING_CODE_LENGTH = 8;
2316
- /** An ephemeral ECDH keypair generated by the pairing device (the TV). */
2317
- interface DevicePairingKeypair {
2318
- /** base64 raw (SEC1 uncompressed, 65 bytes) public key — safe to publish. */
2319
- publicKeyB64: string;
2488
+
2489
+ interface FileStorageOptions {
2490
+ /** Talks to the content-addressed shard store. Always required. */
2491
+ shards: ShardClient;
2492
+ /**
2493
+ * Talks to the gated multi-user space holding manifests + ACLs.
2494
+ * Optional — only required by {@link FileStorage.writeFile} /
2495
+ * {@link FileStorage.readFile} / {@link FileStorage.deleteFile} /
2496
+ * {@link FileStorage.listFiles} / {@link FileStorage.readManifest}.
2497
+ *
2498
+ * Callers that ship the manifest themselves (e.g. via an encrypted chat
2499
+ * message) can omit `space` and use the `*FromShards` / `*ToShards`
2500
+ * methods directly.
2501
+ */
2502
+ space?: SharedSpaceClient;
2503
+ /**
2504
+ * Plaintext chunk size in bytes. After encryption each chunk gains 16
2505
+ * bytes (GCM tag); after RS encoding the size is rounded up to a multiple
2506
+ * of `dataShards` per chunk. Default 4 MiB — a balance between
2507
+ * round-trip cost and parallelism on the upload path.
2508
+ */
2509
+ chunkSize?: number;
2510
+ /** RS data shards per chunk (default 4). */
2511
+ dataShards?: number;
2512
+ /** RS parity shards per chunk (default 2). */
2513
+ parityShards?: number;
2320
2514
  /**
2321
- * Non-extractable private half. In-memory only, one per pairing attempt:
2322
- * it is never persisted and never leaves the device, so a captured pairing
2323
- * transcript is undecryptable once the attempt ends.
2515
+ * Cap on concurrent shard uploads / downloads. Tune for the network. The
2516
+ * default of 8 saturates most home connections without overwhelming the
2517
+ * server's per-IP rate limiter.
2324
2518
  */
2325
- privateKey: CryptoKey;
2519
+ concurrency?: number;
2520
+ /**
2521
+ * Optional pre-compiled RS WebAssembly module. Defaults to the bundled
2522
+ * loader. Pass this in environments where the bundled loader path isn't
2523
+ * available (vitest, deploy-time-precompiled CF Workers).
2524
+ */
2525
+ rsWasmModule?: WebAssembly.Module;
2326
2526
  }
2327
2527
  /**
2328
- * (Device side) Generate the ephemeral P-256 ECDH keypair for one pairing
2329
- * attempt. Call this once per attempt and throw it away when the attempt ends —
2330
- * reusing a keypair across attempts would let an old, observed ciphertext be
2331
- * decrypted by whoever later obtains the key.
2528
+ * Input to {@link FileStorage.writeFile} (which writes the manifest to a
2529
+ * `SharedSpaceClient`). Carries the destination `spaceId`.
2332
2530
  */
2333
- declare function generateDevicePairingKeypair(): Promise<DevicePairingKeypair>;
2531
+ interface WriteFileInput extends WriteToShardsInput {
2532
+ /** Space to write the manifest to — must allow the caller `write`. */
2533
+ spaceId: string;
2534
+ }
2334
2535
  /**
2335
- * (Hosted-page side) Seal the master seed *to a device's* pairing public key.
2336
- *
2337
- * A fresh ephemeral keypair is generated per call and its public half is
2338
- * carried in the envelope (`epk`), so sealing the same seed to the same device
2339
- * twice yields unrelated ciphertext — and the sealer retains nothing that could
2340
- * later reopen the envelope.
2341
- *
2342
- * Returns the v2 `sealedKeys` string, which slots into exactly the same server
2343
- * field as v1: relayed verbatim, never read.
2536
+ * Input to {@link FileStorage.writeFileToShards} (which doesn't write a
2537
+ * manifest anywhere — that's the caller's job).
2344
2538
  */
2345
- declare function sealSeedToDevice(seed: Uint8Array, devicePublicKeyB64: string): Promise<string>;
2539
+ interface WriteToShardsInput {
2540
+ /** Raw data, a Blob, or a File. */
2541
+ data: Uint8Array | Blob | File;
2542
+ /** Application-supplied display metadata. */
2543
+ metadata: {
2544
+ name: string;
2545
+ type: string;
2546
+ path?: string;
2547
+ lastModified?: number;
2548
+ };
2549
+ /**
2550
+ * Optional pre-allocated file id. Defaults to a fresh one. Useful for
2551
+ * resumable uploads: the same id always maps to the same manifest slot.
2552
+ */
2553
+ fileId?: string;
2554
+ /**
2555
+ * Optional upload-progress callback, invoked after each chunk finishes
2556
+ * (`completed` of `total` chunks). Lets a UI show a progress bar. Called once
2557
+ * with `(0, total)` before the first chunk so a bar can render immediately.
2558
+ */
2559
+ onProgress?: (completed: number, total: number) => void;
2560
+ }
2561
+ declare class FileStorage {
2562
+ private readonly space;
2563
+ private readonly shards;
2564
+ private readonly chunkSize;
2565
+ private readonly dataShards;
2566
+ private readonly parityShards;
2567
+ private readonly concurrency;
2568
+ private readonly cipher;
2569
+ private readonly codec;
2570
+ private readonly rsWasmModule?;
2571
+ constructor(opts: FileStorageOptions);
2572
+ /**
2573
+ * Chunk → encrypt → encode → upload-shards. Returns the resulting manifest
2574
+ * along with a `FileStat` summary. The caller is responsible for storing
2575
+ * or transmitting the manifest however it likes.
2576
+ */
2577
+ writeFileToShards(input: WriteToShardsInput): Promise<{
2578
+ manifest: FileManifest;
2579
+ stat: FileStat;
2580
+ }>;
2581
+ /**
2582
+ * Inverse of {@link writeFileToShards}. Takes a manifest the caller
2583
+ * obtained somehow (chat message, share link, etc.) and reassembles the
2584
+ * plaintext. Throws if too many shards are missing or any chunk's auth
2585
+ * tag fails.
2586
+ */
2587
+ readFileFromShards(manifest: FileManifest): Promise<{
2588
+ data: Uint8Array;
2589
+ stat: FileStat;
2590
+ }>;
2591
+ /**
2592
+ * Streaming inverse of {@link writeFileToShards}: yields each chunk's
2593
+ * decrypted plaintext in order WITHOUT concatenating. Peak memory stays at
2594
+ * ~one chunk, so multi-GB files (which overflow a single `Uint8Array`) can be
2595
+ * piped straight to a file/socket. Same shard sourcing (cache → peers →
2596
+ * origin) and per-chunk auth as {@link readFileFromShards}.
2597
+ */
2598
+ readChunksFromShards(manifest: FileManifest): AsyncGenerator<Uint8Array>;
2599
+ /** As {@link writeFileToShards}, plus POSTs the manifest to `input.spaceId`. */
2600
+ writeFile(input: WriteFileInput): Promise<FileStat>;
2601
+ /** As {@link readFileFromShards}, but fetches the manifest from a space first. */
2602
+ readFile(spaceId: string, fileId: string): Promise<{
2603
+ data: Uint8Array;
2604
+ stat: FileStat;
2605
+ }>;
2606
+ /**
2607
+ * Delete the manifest from a space. **Does not GC the shards** — those
2608
+ * are content-addressed and may be referenced by other manifests.
2609
+ * Garbage collection is a separate server-side concern (Phase D / D-ish).
2610
+ */
2611
+ deleteFile(spaceId: string, fileId: string): Promise<boolean>;
2612
+ /** Convenience pass-through. */
2613
+ listFiles(spaceId: string): Promise<FileStat[]>;
2614
+ /** Read a manifest verbatim — useful for "share file" / export flows. */
2615
+ readManifest(spaceId: string, fileId: string): Promise<FileManifest>;
2616
+ private requireSpace;
2617
+ /** Encrypt + encode + upload one chunk; return its manifest entry. */
2618
+ private writeChunk;
2619
+ /** Inverse of `writeChunk`. */
2620
+ private readChunk;
2621
+ /** Run `tasks` with at most `this.concurrency` in flight. */
2622
+ private runWithConcurrency;
2623
+ }
2624
+
2346
2625
  /**
2347
- * (Device side) Recover the master seed from a v2 envelope using the ephemeral
2348
- * private key from {@link generateDevicePairingKeypair}.
2626
+ * Reed–Solomon erasure-coding codec.
2349
2627
  *
2350
- * Throws on any tamper: a substituted `epk` yields a different shared secret
2351
- * (or fails point validation outright), and any edit to `iv`/`ct` fails the
2352
- * AES-GCM tag. There is no path that returns partially-authenticated bytes.
2628
+ * Wraps the WASM-backed `encode` / `reconstruct` primitives in `wasm/rs.ts`
2629
+ * with shard-padding bookkeeping so callers can hand it any-length input and
2630
+ * get back uniform-size shards.
2353
2631
  *
2354
- * `devicePublicKeyB64` is the caller's OWN public half (from
2355
- * {@link generateDevicePairingKeypair}). It is required, not optional, because
2356
- * it is mixed into the KDF — see {@link deriveDeviceSealKey} for why binding the
2357
- * recipient matters. A device always holds both halves, so this costs nothing.
2632
+ * Usage:
2633
+ * const codec = new ReedSolomonCodec();
2634
+ * await codec.ready();
2635
+ * const { shards, shardSize, originalSize } = codec.encode(plaintext, 4, 2);
2636
+ * // 6 shards (4 data + 2 parity), each `shardSize` bytes
2637
+ * // …ship them, lose up to 2, then …
2638
+ * const recovered = codec.decode(shards, 2, deadIndexes, originalSize);
2639
+ *
2640
+ * Why the explicit `originalSize`: RS pads input to make every data shard the
2641
+ * same length. The decoder must trim that padding back off after recovery.
2642
+ * The caller is responsible for stashing `originalSize` somewhere durable
2643
+ * (the file manifest, in this codebase).
2358
2644
  */
2359
- declare function unsealSeedFromDevice(sealedKeys: string, privateKey: CryptoKey, devicePublicKeyB64: string): Promise<Uint8Array>;
2645
+ interface EncodeResult {
2646
+ /** All `dataShards + parityShards` shards. All have length `shardSize`. */
2647
+ shards: Uint8Array[];
2648
+ /** Length of every shard (data and parity). */
2649
+ shardSize: number;
2650
+ /** Length of the original input before padding. Needed to trim on decode. */
2651
+ originalSize: number;
2652
+ /** How many of the leading shards are data (the rest are parity). */
2653
+ dataShards: number;
2654
+ /** How many parity shards trail the data shards. */
2655
+ parityShards: number;
2656
+ }
2657
+ declare class ReedSolomonCodec {
2658
+ /**
2659
+ * Resolves once the WASM module is loaded; safe to call repeatedly.
2660
+ *
2661
+ * Pass `wasmModule` to use a pre-compiled module (e.g. in tests where the
2662
+ * rollup-inlined loader path isn't available, or in CF Workers where you
2663
+ * want to load the module at deploy time). Default path uses the bundled
2664
+ * base64-inlined wasm — works in Node, browsers, and Workers.
2665
+ */
2666
+ ready(wasmModule?: WebAssembly.Module): Promise<void>;
2667
+ /**
2668
+ * Split `data` into `dataShards` equal-size chunks (zero-padded to fit),
2669
+ * compute `parityShards` parity shards, and return all of them.
2670
+ */
2671
+ encode(data: Uint8Array, dataShards: number, parityShards: number): EncodeResult;
2672
+ /**
2673
+ * Reconstruct the original `data` from `shards`. Up to `parityShards`
2674
+ * shards may be missing; for each missing shard, pass an empty
2675
+ * `Uint8Array` placeholder at its position and list the position in
2676
+ * `deadShardIndexes`.
2677
+ *
2678
+ * `originalSize` is required so the trailing zero-padding can be trimmed.
2679
+ */
2680
+ decode(shards: Uint8Array[], parityShards: number, deadShardIndexes: number[], originalSize: number): Uint8Array;
2681
+ }
2682
+
2360
2683
  /**
2361
- * Short code derived deterministically from the device's pairing public key,
2362
- * displayed on BOTH the TV and the approving browser so the human can confirm
2363
- * they match before approving.
2684
+ * Universal wasm-bindgen glue for `wasm-reed-solomon-erasure` (v0.2.2 — Rust
2685
+ * Reed–Solomon erasure-coding library compiled with `wasm-pack --target nodejs`).
2364
2686
  *
2365
- * ## What this is worth (be honest about it)
2687
+ * The upstream npm package ships `wasm_reed_solomon_erasure.js` which uses
2688
+ * `require('fs').readFileSync()` to load the WASM file at module load time —
2689
+ * that hard-codes Node and breaks browsers / CF Workers. This file is a port
2690
+ * of that glue with the Node-only pieces removed:
2366
2691
  *
2367
- * ~39.3 bits (30^8), rendered in an unambiguous 30-symbol alphabet and grouped
2368
- * as `XXXX-XXXX` for comparison. Alongside the recipient-key binding in
2369
- * {@link deriveDeviceSealKey}, this is what stops a substituted device key: the
2370
- * KDF rejects a *negated* key, and this code is what a human uses to reject a
2371
- * wholly *attacker-generated* one, which the crypto cannot detect on its own —
2372
- * ECDH proves nothing about WHOSE public key it is.
2692
+ * - `TextDecoder` is taken from the global (available in Node 12+, every
2693
+ * modern browser, and Workers — no `require('util')`).
2694
+ * - The WASM module is loaded via the rollup-inlined `.wasm` import, the
2695
+ * same `targetEnv: 'auto-inline'` path the bn128 verifier uses. Works in
2696
+ * Node, browser, and Workers without runtime fs access.
2373
2697
  *
2374
- * Two attacks, and what each actually costs:
2698
+ * Only the two public functions consumers actually need (`encode` and
2699
+ * `reconstruct`) are exported. The rest is the wbindgen heap-table dance.
2375
2700
  *
2376
- * 1. **Online grind.** Generate keypairs until one's code equals the honest
2377
- * TV's. ~2^39 trials. Naively that reads as "keygen + hash", but a real
2378
- * attacker never generates keys: walking `P_{i+1} = P_i + G` while tracking
2379
- * `d_i = d_0 + i` makes each trial one point-add plus one derivation. With
2380
- * a plain hash that is well under a core-day — which is why this uses
2381
- * PBKDF2-SHA256 at {@link PAIRING_CODE_ITERATIONS} iterations instead. The
2382
- * work factor applies per trial, pushing the same grind to the order of
2383
- * 10^5 core-days. The target is also only learned when pairing starts, so
2384
- * the grind is online against the pairing TTL (keep it minutes).
2385
- * 2. **Precomputed table.** The reason this is not a plain hash. A table over
2386
- * candidate keys is computed once and reused forever; with SHA-256 that is
2387
- * cheap enough to be the preferred attack. The iteration count multiplies
2388
- * the table's build cost by the same 2 * 10^5.
2701
+ * Encoding:
2702
+ * `encode(dataShards: Uint8Array[], parityShard: number)` → array of
2703
+ * `dataShards.length + parityShard` Uint8Arrays. The first N are the input
2704
+ * shards unchanged; the trailing M are the freshly computed parity shards.
2705
+ * All shards are the same length — caller is responsible for padding the
2706
+ * data into equal-size data shards.
2389
2707
  *
2390
- * Neither is a proof of security, and an adversary with ASIC-scale hardware
2391
- * changes these numbers. If the margin is ever judged insufficient, raise
2392
- * {@link PAIRING_CODE_LENGTH} — 10 characters buys ~49 bits for two more
2393
- * characters of human comparison, and is display-only: it does not touch the
2394
- * envelope or the wire format.
2708
+ * Reconstruction:
2709
+ * `reconstruct(corruptedShards, parityShard, deadIndexes)` → fully recovered
2710
+ * array. Pass placeholders (empty Uint8Arrays) at the dead indexes; the
2711
+ * reconstructor fills them in. Up to `parityShard` shards may be missing.
2712
+ */
2713
+ /**
2714
+ * Load and instantiate the WASM module. Idempotent — repeated calls await the
2715
+ * same one-shot promise.
2395
2716
  *
2396
- * The cost is paid once per pairing, on two idle devices, so ~200k iterations
2397
- * is imperceptible here in a way it would not be on a hot path.
2717
+ * Two ways to initialize, matching the pattern used by `workers/groth16-verifier`:
2718
+ * 1. `await initRsWasm()` — uses the bundled .wasm (base64-inlined at build
2719
+ * time by `@rollup/plugin-wasm`). Works in any runtime that allows
2720
+ * runtime `WebAssembly.compile()`.
2721
+ * 2. `await initRsWasm(myWasmModule)` — accepts a pre-compiled
2722
+ * `WebAssembly.Module`. Useful when the bundled loader path isn't
2723
+ * available (vitest, CF Workers wanting deploy-time precompile, etc.).
2398
2724
  */
2399
- declare function pairingVerificationCode(devicePublicKeyB64: string): Promise<string>;
2400
- interface Pkce {
2401
- codeVerifier: string;
2402
- codeChallenge: string;
2403
- }
2404
- /** Generate a PKCE verifier + S256 challenge for the authorize→token exchange. */
2405
- declare function generatePkce(): Promise<Pkce>;
2406
- /** A random URL-safe value for the OAuth `state` (CSRF binding on the callback). */
2407
- declare function randomState(): string;
2725
+ declare function initRsWasm(wasmModule?: WebAssembly.Module): Promise<void>;
2408
2726
 
2409
2727
  /**
2410
2728
  * `SessionState` — the in-memory source of truth for "who is signed in" on a
@@ -5602,6 +5920,8 @@ interface FileEntry {
5602
5920
  * would defeat the point of keeping history out of the directory record.
5603
5921
  */
5604
5922
  versions?: number;
5923
+ /** See `DirEntry.moving`. */
5924
+ moving?: true;
5605
5925
  }
5606
5926
  interface DirEntry {
5607
5927
  kind: "dir";
@@ -5610,6 +5930,25 @@ interface DirEntry {
5610
5930
  /** Base64 AES-256 key for that record. See the capability note above. */
5611
5931
  key: string;
5612
5932
  mtime: number;
5933
+ /**
5934
+ * Set on both copies while a move between two directories is under way.
5935
+ *
5936
+ * A move writes two records, attaching before it detaches, and one that
5937
+ * is interrupted leaves the same entry - the same directory record, or
5938
+ * the same file id, content and history - named in both. This is how a
5939
+ * delete of either copy knows to look for the other before letting go of
5940
+ * anything. Cleared by the move's last write, and from the one copy left
5941
+ * when a delete removes the other. Left in place, it costs a delete of
5942
+ * that entry one read of the tree, and a file keeps every version that
5943
+ * falls off its history.
5944
+ *
5945
+ * Not cleared when that last write is the one that fails: the move has
5946
+ * otherwise finished, only one copy is named anywhere, and nothing comes
5947
+ * back to unmark it. A retried move of the same entry carries the mark
5948
+ * over. That copy then keeps what falls off its history for good - a
5949
+ * storage cost, never a loss.
5950
+ */
5951
+ moving?: true;
5613
5952
  }
5614
5953
  type Entry = FileEntry | DirEntry;
5615
5954
  /** One directory record, as stored (before encryption). */
@@ -5665,6 +6004,57 @@ declare class VfsNotFoundError extends Error {
5665
6004
  declare class VfsConflictError extends Error {
5666
6005
  constructor(message: string);
5667
6006
  }
6007
+ /**
6008
+ * Raised when a directory record cannot be read: it does not decrypt or parse
6009
+ * with the key its parent holds, or it does but is not a shape this SDK can
6010
+ * read - not an object, a version other than 1, `entries` missing or not a map.
6011
+ *
6012
+ * Thrown, never read as an empty directory. The next write into an "empty"
6013
+ * directory would seal an empty record over the real one, and a record this
6014
+ * version cannot read may simply be one a NEWER version wrote. Nor is the
6015
+ * directory skipped: a sync that cannot see a subtree reads its absence as a
6016
+ * deletion. The record is left exactly as stored; a recursive delete
6017
+ * (`delete(path, { recursive: true })`) detaches it when the user wants it
6018
+ * gone.
6019
+ */
6020
+ declare class VfsCorruptRecordError extends Error {
6021
+ /** The record's id - `dirKey(id)` is where it is stored. */
6022
+ readonly id: string;
6023
+ /** The path it was reached through, when known. A moved subtree can have two. */
6024
+ readonly path?: string;
6025
+ constructor(id: string, path: string | undefined, reason: string);
6026
+ }
6027
+ /**
6028
+ * Raised when a walk or a copy would go past a size or depth ceiling, or a
6029
+ * write would put a directory deeper than a walk reads.
6030
+ *
6031
+ * Thrown rather than returning what was gathered so far, because every caller
6032
+ * of a walk treats a path that is missing from the result as a path that does
6033
+ * not exist - and a sync then plans a deletion for it. A tree this large or
6034
+ * deep is almost always one built to be expensive to read: directory records
6035
+ * can point at the same directory many times over, so a handful of tiny
6036
+ * records can describe more paths than there are atoms.
6037
+ */
6038
+ declare class VfsLimitError extends Error {
6039
+ /**
6040
+ * Which ceiling was hit: total entries, total characters of path text,
6041
+ * nesting depth, or - for a copy - how many places one directory may
6042
+ * appear. Entries alone do not bound memory - a name may be 255 bytes, so
6043
+ * one path can be ~16KB - which is why path text has its own.
6044
+ */
6045
+ readonly limit: "entries" | "pathChars" | "depth" | "shared";
6046
+ /** The ceiling itself. */
6047
+ readonly max: number;
6048
+ /** Where the operation started, or for a write what it was asked to write. */
6049
+ readonly path: string;
6050
+ /**
6051
+ * Where the ceiling was reached: the directory being listed, or for depth
6052
+ * the entry one level too deep. Walking the tree to find the culprit would
6053
+ * hit the same ceiling, so this is the only pointer the caller gets.
6054
+ */
6055
+ readonly at: string;
6056
+ constructor(limit: VfsLimitError["limit"], max: number, path: string, at: string, doing?: string);
6057
+ }
5668
6058
 
5669
6059
  /**
5670
6060
  * `client.vfs` — a real filesystem over Muhkoo primitives.
@@ -5735,6 +6125,18 @@ interface VfsNamespaceDeps {
5735
6125
  seed: () => Uint8Array | null;
5736
6126
  /** Versions retained per file. */
5737
6127
  historyLimit?: number;
6128
+ /**
6129
+ * Ceilings for the operations that visit a whole subtree - `walk` (and so
6130
+ * `glob`) and `copy`. `entries` is the most entries one call may visit;
6131
+ * `pathChars` the most characters of path text, summed over every path it
6132
+ * visits. The defaults are far past any real project; tests lower them so
6133
+ * a hostile tree hits them in milliseconds. `Client` does not pass these,
6134
+ * so `client.vfs` always has the defaults.
6135
+ */
6136
+ limits?: {
6137
+ entries?: number;
6138
+ pathChars?: number;
6139
+ };
5738
6140
  }
5739
6141
  declare class VfsNamespace {
5740
6142
  private readonly deps;
@@ -5748,6 +6150,8 @@ declare class VfsNamespace {
5748
6150
  */
5749
6151
  private cache;
5750
6152
  private readonly historyLimit;
6153
+ private readonly maxEntries;
6154
+ private readonly maxPathChars;
5751
6155
  constructor(deps: VfsNamespaceDeps);
5752
6156
  /**
5753
6157
  * The working directory relative paths resolve against.
@@ -5799,14 +6203,46 @@ declare class VfsNamespace {
5799
6203
  stat(path: string): Promise<VfsStat>;
5800
6204
  /** Immediate children of a directory. One record fetch. */
5801
6205
  list(path?: string): Promise<VfsStat[]>;
6206
+ /**
6207
+ * A directory's entries in listing order: directories first, then by name.
6208
+ * Shared by `list` and `walk`, so a walk's output order is exactly the order
6209
+ * of the listings it is made of.
6210
+ */
6211
+ private children;
5802
6212
  private warn;
5803
6213
  /**
5804
6214
  * Every file path under a prefix, depth-first.
5805
6215
  *
5806
6216
  * Deliberately separate from `list`: this walks the whole subtree, which is
5807
6217
  * one fetch per directory. Fine for a project, wrong as a default.
6218
+ *
6219
+ * Throws VfsLimitError past the entry budget or the depth ceiling, and
6220
+ * VfsCorruptRecordError for a directory it cannot read, rather than
6221
+ * returning part of the tree. Skips (with a warning) a directory that is
6222
+ * one of its own ancestors.
5808
6223
  */
5809
6224
  walk(path?: string): Promise<string[]>;
6225
+ /**
6226
+ * Every entry under a prefix - directories as well as files - depth-first,
6227
+ * each directory just before its contents. `walk` is this with the
6228
+ * directories filtered out, so the two can never disagree about what is in
6229
+ * a tree; version control uses this one because a commit records empty
6230
+ * directories too.
6231
+ */
6232
+ walkEntries(path?: string): Promise<VfsStat[]>;
6233
+ /**
6234
+ * `walkEntries` without the list: `visit` is called with each entry in
6235
+ * the same order, and for a file with its manifest - the handle
6236
+ * `statManifest` gives, without resolving the path all over again, which
6237
+ * on a deep tree cost far more than the walk itself. Null for a directory.
6238
+ *
6239
+ * For a caller that does not need every entry at once. Version control
6240
+ * compares the working tree with a commit as it goes, where holding the
6241
+ * whole walk beside the whole commit, and a map of one beside a map of
6242
+ * the other, ran a 512MB heap out of memory inside the default budgets.
6243
+ * The same guards and budgets as `walk`, which is built on this.
6244
+ */
6245
+ eachEntry(path: string, visit: (stat: VfsStat, manifest: FileManifest | null) => void): Promise<void>;
5810
6246
  readFile(path: string): Promise<Uint8Array>;
5811
6247
  readText(path: string): Promise<string>;
5812
6248
  /**
@@ -5852,6 +6288,16 @@ declare class VfsNamespace {
5852
6288
  mkdir(path: string, opts?: {
5853
6289
  recursive?: boolean;
5854
6290
  }): Promise<void>;
6291
+ /**
6292
+ * Refuse now what creating these directories with `mkdir -p` would refuse
6293
+ * part way: one deeper than MAX_DEPTH, or a name on the way held by an
6294
+ * entry this version cannot read. Writes nothing.
6295
+ *
6296
+ * For an operation that removes things before it writes - a checkout -
6297
+ * so that a refusal comes before anything is gone. A FILE in the way is
6298
+ * not refused: such an operation removes those first.
6299
+ */
6300
+ assertCreatable(dirs: Iterable<string>): Promise<void>;
5855
6301
  /**
5856
6302
  * Write a file, creating parent directories as needed.
5857
6303
  *
@@ -5865,10 +6311,10 @@ declare class VfsNamespace {
5865
6311
  /**
5866
6312
  * Delete a file, or a directory when `recursive`.
5867
6313
  *
5868
- * `keepContent` removes the entry WITHOUT releasing its shards. A checkout
5869
- * needs that: the file is leaving the working tree, but commits still
5870
- * reference the same manifest, and releasing would free content history
5871
- * depends on.
6314
+ * `keepContent` removes the entry WITHOUT releasing its shards - or, for
6315
+ * a directory, the shards of anything in it. A checkout needs that: the
6316
+ * file is leaving the working tree, but commits still reference the same
6317
+ * manifest, and releasing would free content history depends on.
5872
6318
  */
5873
6319
  delete(path: string, opts?: {
5874
6320
  recursive?: boolean;
@@ -5880,9 +6326,21 @@ declare class VfsNamespace {
5880
6326
  *
5881
6327
  * A directory move is O(1) regardless of how much is inside it — the
5882
6328
  * subtree records are not touched at all, because nothing in them records
5883
- * its own path.
6329
+ * its own path. Except a move to a DEEPER place, which reads the subtree
6330
+ * first: what is below must still be inside MAX_DEPTH once it is moved.
5884
6331
  */
5885
6332
  rename(from: string, to: string): Promise<void>;
6333
+ /**
6334
+ * Refuse to move the directory at `src` to `dst` when something in it
6335
+ * would then be deeper than MAX_DEPTH from the root. Writes nothing.
6336
+ *
6337
+ * Measured only moving deeper, because it reads the whole subtree: one
6338
+ * move of a 40-deep tree under a 30-deep directory was accepted when only
6339
+ * the moved directory was measured, and every walk of the root threw from
6340
+ * then on. A part that cannot be read cannot be measured, and does not
6341
+ * stop the move - that record stops every walk above it already.
6342
+ */
6343
+ private assertFitsAt;
5886
6344
  /**
5887
6345
  * Copy a file or a directory tree.
5888
6346
  *
@@ -5944,25 +6402,158 @@ declare class VfsNamespace {
5944
6402
  /** Open a directory by path, creating nothing. Throws if it is missing. */
5945
6403
  private openDir;
5946
6404
  private openDirOrNull;
6405
+ /** Open a directory by path, along with the id of every directory on the way to it. */
6406
+ private openChain;
6407
+ /**
6408
+ * `path` is only for error messages. The cache is keyed by id, so when two
6409
+ * paths reach one record - a move interrupted between attach and detach -
6410
+ * the error names whichever asked first.
6411
+ */
5947
6412
  private openDirById;
6413
+ /**
6414
+ * Turn a decrypted record into an OpenDir, or throw.
6415
+ *
6416
+ * The record is THROWN on, never read as empty, when its shape is wrong -
6417
+ * see VfsCorruptRecordError. An individual entry of the wrong shape is set
6418
+ * aside in `hidden` instead: out of every read, but kept for the write.
6419
+ */
6420
+ private readRecord;
5948
6421
  /** Apply an edit to a directory record and persist it. */
5949
6422
  private mutate;
5950
6423
  private createDir;
5951
- /** Recursively remove a subtree's records. Best-effort: unreachable ≠ fatal. */
6424
+ /**
6425
+ * Every directory record in a subtree, each once, opened - and nothing
6426
+ * changed. The first half of a recursive delete; see `delete`.
6427
+ *
6428
+ * Once per id, so a record that references another inside the subtree
6429
+ * cannot recurse forever, and a subtree reachable twice is not released
6430
+ * twice. And never a directory in `above` - the root and everything else
6431
+ * on the way down to the target - nor the root by any route. Deduplicating
6432
+ * alone let an entry naming the root pull the whole filesystem into the
6433
+ * delete, the root record included; a walk shows the same pointer as a
6434
+ * cycle and skips it, so this skips it too.
6435
+ *
6436
+ * `moving` says whether anything in it is marked as half of an
6437
+ * interrupted move, which is what makes a delete look for the other half.
6438
+ */
6439
+ private subtree;
6440
+ /**
6441
+ * Delete these records and let go of every file in them. The second half
6442
+ * of a recursive delete, run only once the subtree is detached. Cannot
6443
+ * throw: every step is best-effort, because unreachable is not fatal.
6444
+ *
6445
+ * With `elsewhere` - what the rest of the tree still names, read because
6446
+ * something here is half of an interrupted move - a record still named
6447
+ * elsewhere is kept, and so is a file.
6448
+ */
5952
6449
  private purge;
6450
+ /**
6451
+ * Unmark what is left of an interrupted move once a delete has removed
6452
+ * the other copy. Marked, a file keeps every version that falls off its
6453
+ * history, because the other copy might hold it - for good, once there
6454
+ * is no other copy - and every delete of the entry, or of a directory
6455
+ * above it, reads the whole tree. Only when exactly one marked copy is
6456
+ * left: more, and they are still shared. Best-effort, like everything
6457
+ * after a detach; failing, it stays marked, which costs only that.
6458
+ */
6459
+ private settle;
6460
+ /**
6461
+ * Let go of files that are leaving the tree: each one's content, every
6462
+ * version its history holds, and the history record. Best-effort, like
6463
+ * everything after a detach.
6464
+ *
6465
+ * A file id still named elsewhere is one file with two names, left by an
6466
+ * interrupted move: its content and history belong to the name that is
6467
+ * staying, and all that changes hands is the key its history opens with.
6468
+ * `keepContent` keeps every file's content and drops only its history
6469
+ * record, as a checkout needs.
6470
+ */
6471
+ private letGo;
6472
+ /**
6473
+ * What the tree still names with one entry left out: every directory
6474
+ * record, and every file id with a directory that holds it. Null when
6475
+ * part of the tree cannot be read or followed, so the answer is unknown.
6476
+ */
6477
+ private namedElsewhere;
6478
+ /**
6479
+ * Everything reachable from the root: every directory record's id and
6480
+ * key, and every file id with a directory that names it - following the
6481
+ * entries hidden only for their name, which still reference what they
6482
+ * point at. `skip` leaves one entry out: one about to be detached.
6483
+ *
6484
+ * Fetches up to WALK_CONCURRENCY records at once, each record once
6485
+ * however often it is named. Throws VfsCorruptRecordError for a record a
6486
+ * path reaches that cannot be read, unless `lenient`. `unknown` counts
6487
+ * what could not be followed at all: an entry this version cannot read,
6488
+ * and a record behind an unusable name that does not open - which no path
6489
+ * can reach, and so no recursive delete can remove - or, `lenient`, any
6490
+ * record that does not open.
6491
+ */
6492
+ private references;
5953
6493
  /**
5954
6494
  * Deep-copy an entry. Files reuse their manifest (no bytes move); each
5955
6495
  * directory gets a fresh id and key, so the copy is independent of the
5956
6496
  * original — writing into one must never appear in the other.
6497
+ *
6498
+ * `at` is the entry's SOURCE path, and `depth` how many directories deep
6499
+ * from the root the copy of it will be. With `run.write` false this is the
6500
+ * dry run: every check, no reference taken and no record stored.
5957
6501
  */
5958
6502
  private cloneEntry;
5959
6503
  /**
5960
- * Let go of a file's bytes: the current manifest and every retained
5961
- * version, then the history record itself.
6504
+ * Let go of one file's bytes: its content, every version its history
6505
+ * retains, and the history record itself.
6506
+ *
6507
+ * Each distinct manifest once. `copies` is every entry naming the file -
6508
+ * two after an interrupted move, which may by now hold different
6509
+ * versions - and `restore` puts a version back without taking a second
6510
+ * reference, so its manifest is in the entry and in the history both.
6511
+ * Releasing per mention released one reference twice, and so freed bytes
6512
+ * a copy elsewhere, holding its own reference, still needed.
5962
6513
  */
5963
6514
  private releaseFile;
6515
+ /**
6516
+ * A file's history record, opened with the first of `keys` that opens
6517
+ * it, and that key. Null when there is none, or none of them does.
6518
+ */
6519
+ private openHistory;
6520
+ /**
6521
+ * Seal a file's history with the key of the directory that holds it now.
6522
+ *
6523
+ * A history record is sealed with its directory's key, so a file moved
6524
+ * to another directory carried a history its new directory could not
6525
+ * open: every read of it, and every write to the file, failed from then
6526
+ * on. Nothing to do when there is no history, when it opens with `to`
6527
+ * already, or when none of the keys opens it.
6528
+ */
6529
+ private rekeyHistory;
6530
+ /**
6531
+ * The history of the file `entry` names as `name` in `parent`, and the
6532
+ * key to seal it with from now on.
6533
+ *
6534
+ * A history is sealed with its directory's key - usually. The two halves
6535
+ * of an interrupted move share one history, sealed with ONE of their
6536
+ * directories' keys, and a file moved between directories by a version
6537
+ * before `rename` re-sealed it still has its old directory's. So when the
6538
+ * directory's own key does not open it, every key in the tree is tried,
6539
+ * the other half's first: a read of the whole tree, and only then. It
6540
+ * need not be read in full, or understood - an entry this version cannot
6541
+ * follow may hide a key, but a key that opens the record is proof
6542
+ * enough. Found, it is re-sealed with this directory's key, so the search
6543
+ * is made once - except for half of a move, which keeps the key that
6544
+ * opened it and so keeps working for both halves.
6545
+ *
6546
+ * No record, or a value that is not a sealed record at all, is an empty
6547
+ * history, as it always was. One that no key opens is thrown on, with a
6548
+ * message rather than a bare decryption error: writing an empty history
6549
+ * over it would lose every version it holds.
6550
+ */
5964
6551
  private readHistory;
5965
- /** Push `entry` onto its history record. Returns the new version count. */
6552
+ /**
6553
+ * Push `entry` onto its history record. Returns the new version count.
6554
+ * `next` is the manifest the entry will hold once the caller has written
6555
+ * it.
6556
+ */
5966
6557
  private pushHistory;
5967
6558
  }
5968
6559
 
@@ -6068,21 +6659,88 @@ interface VcsNamespaceDeps {
6068
6659
  key: () => Promise<Uint8Array>;
6069
6660
  /** Who to record as the author. */
6070
6661
  author: () => string;
6662
+ /**
6663
+ * `entries` is the most tree entries one read of history may visit - one
6664
+ * `status`, `diff` or `checkout` - and `pathChars` the most characters of
6665
+ * path those entries may add up to. The defaults are far past any real
6666
+ * project; tests lower them so a hostile tree hits them in milliseconds.
6667
+ * `Client` does not pass these, so `client.vcs` always has the defaults.
6668
+ */
6669
+ limits?: {
6670
+ entries?: number;
6671
+ pathChars?: number;
6672
+ };
6071
6673
  }
6072
6674
  declare class Repo {
6073
6675
  private readonly slug;
6074
6676
  private readonly deps;
6677
+ private readonly maxEntries;
6678
+ private readonly maxPathChars;
6679
+ /**
6680
+ * How many directories deep below the project a tree may nest. The
6681
+ * working tree may be written MAX_DEPTH directories below the root, and
6682
+ * the project itself is some of those: a deeper tree would check out into
6683
+ * something no walk will read, and `status` - and every checkout after,
6684
+ * which walks the tree first - would throw from then on.
6685
+ */
6686
+ private readonly maxTreeDepth;
6075
6687
  constructor(slug: string, deps: VcsNamespaceDeps);
6076
6688
  private get root();
6077
6689
  /** Store an object, returning its hash and whether it was new. */
6078
6690
  private put;
6691
+ /** Does this stored record open to an object with this hash? */
6692
+ private holds;
6693
+ /**
6694
+ * Fetch an object, and prove it is the one that was asked for.
6695
+ *
6696
+ * Content addressing is only a guarantee if the address is checked. Without
6697
+ * this a stored object could claim any hash - including its own, so a tree
6698
+ * could contain itself and `status` would follow it for ever. Every object
6699
+ * this repository has ever written verifies: `put` hashes exactly the value
6700
+ * it seals, `canonical` is independent of key order and drops `undefined`
6701
+ * exactly as JSON does, and hashing has never been versioned.
6702
+ */
6079
6703
  private get;
6704
+ /** A commit, checked to be one. See `commitProblem`. */
6705
+ private readCommit;
6706
+ /**
6707
+ * Start a traversal of history: a budget shared by every branch of it, and
6708
+ * a cache of the tree objects it has fetched.
6709
+ *
6710
+ * The cache matters for the same reason the budget does. Identical
6711
+ * subtrees are one object, so a tree of honest-but-hostile shape is a
6712
+ * handful of fetches and a vast number of paths; caching keeps it to the
6713
+ * handful, and the budget stops the paths. It caches OBJECTS, never output
6714
+ * - each name a shared subtree appears at is still listed.
6715
+ */
6716
+ private treeWalk;
6717
+ /**
6718
+ * A tree object's entries, checked and ready to walk.
6719
+ *
6720
+ * Returned in a null-prototype map, so a name like `constructor` is looked
6721
+ * up as an entry and not found as an inherited property.
6722
+ */
6723
+ private readTree;
6724
+ /**
6725
+ * A record that is not an object - HEAD, a branch ref, the branch list,
6726
+ * MERGE - opened. Null when there is none; `problem` when there is one
6727
+ * that does not open. What is inside is the caller's to check.
6728
+ */
6729
+ private openRecord;
6080
6730
  private head;
6081
6731
  private setHead;
6082
6732
  private readRef;
6733
+ /** A branch's commit, null when there is no such branch, or "unreadable". */
6734
+ private refState;
6083
6735
  private writeRef;
6084
6736
  /** Every branch name in this repository. */
6085
6737
  branches(): Promise<string[]>;
6738
+ /**
6739
+ * The branch list, less any name a branch could not have been created
6740
+ * with - left out with a warning, like an unreadable entry in a tree, so
6741
+ * one planted name does not hide every real one.
6742
+ */
6743
+ private readIndex;
6086
6744
  /** The commit HEAD resolves to, or null in an empty repository. */
6087
6745
  current(): Promise<string | null>;
6088
6746
  /**
@@ -6100,7 +6758,27 @@ declare class Repo {
6100
6758
  theirs: string;
6101
6759
  name: string;
6102
6760
  } | null>;
6103
- /** Write a directory and everything under it, returning the tree's hash. */
6761
+ /**
6762
+ * Give up on a merge that stopped for conflicts: the next commit is an
6763
+ * ordinary one, with one parent. The working tree is left as it is,
6764
+ * conflict markers and all. To put it back, switch to the current branch
6765
+ * with `discardChanges` — not `checkout`, which always leaves HEAD
6766
+ * detached, so later commits would stop moving the branch.
6767
+ *
6768
+ * Also the way out when the record of the merge cannot be read, which
6769
+ * would otherwise stop every commit.
6770
+ */
6771
+ abortMerge(): Promise<void>;
6772
+ /**
6773
+ * Write a directory and everything under it, returning the tree's hash.
6774
+ *
6775
+ * Reads the working tree through ONE `walkEntries`, rather than listing
6776
+ * each directory by path and recursing. The recursion had no guard at all:
6777
+ * a directory record that names itself - which anything holding the seed
6778
+ * can plant - sent `commit` down /loop/loop/loop/... for ever. The walk has
6779
+ * the cycle guard, the budget and the depth ceiling, so a commit records
6780
+ * exactly the tree `status` compares it against.
6781
+ */
6104
6782
  private writeTree;
6105
6783
  /** Commits reachable from HEAD, newest first. */
6106
6784
  log(limit?: number): Promise<LogEntry[]>;
@@ -6128,8 +6806,6 @@ declare class Repo {
6128
6806
  * trees for states nobody chose to keep.
6129
6807
  */
6130
6808
  status(): Promise<Change[]>;
6131
- /** The working tree as a path → entry map, mirroring `filesOf`. */
6132
- private workingFiles;
6133
6809
  /**
6134
6810
  * Turn what a person typed into a commit hash.
6135
6811
  *
@@ -6221,6 +6897,11 @@ interface VcsNamespaceOptions {
6221
6897
  /** The in-memory master seed, or null when locked. Read per call. */
6222
6898
  seed: () => Uint8Array | null;
6223
6899
  author: () => string;
6900
+ /** See `VcsNamespaceDeps.limits`. */
6901
+ limits?: {
6902
+ entries?: number;
6903
+ pathChars?: number;
6904
+ };
6224
6905
  }
6225
6906
 
6226
6907
  /** The hosted Muhkoo Accelerator — the default {@link ClientOptions.baseUrl}. */
@@ -6317,205 +6998,56 @@ declare class Client {
6317
6998
  /** Per-user key/value storage — `client.kv.set(...)`, etc. */
6318
6999
  readonly kv: KvNamespace;
6319
7000
  /** App scalable database — `client.db.table('todos').query(...)`, etc. */
6320
- readonly db: DbNamespace;
6321
- /** File storage — `client.storage.writeFile(...)`, etc. */
6322
- readonly storage: StorageNamespace;
6323
- /** Filesystem over the personal space: directories, versions, real paths. */
6324
- readonly vfs: VfsNamespace;
6325
- /** Version control over the VFS: commits, branches, merges — per project. */
6326
- readonly vcs: VcsNamespace;
6327
- /** Realtime messaging — `client.message.subscribe(...)`, etc. */
6328
- readonly message: MessageNamespace;
6329
- /** Fan-out group spaces — `client.space.createSpace(...)`, etc. */
6330
- readonly space: SpaceNamespace;
6331
- /** Programmable Agents — `client.agents.create(appId, …)`, etc. */
6332
- readonly agents: AgentsNamespace;
6333
- /** Serverless functions — `client.functions.deploy(appId, …)`, etc. */
6334
- readonly functions: FunctionsNamespace;
6335
- /** Access tokens — `client.accessTokens.create(appId, …)`, etc. */
6336
- readonly accessTokens: AccessTokensNamespace;
6337
- /** Offline cache + sync — `client.offline.status`, `client.offline.snapshot`, etc. */
6338
- readonly offline: OfflineManager;
6339
- private readonly session;
6340
- private readonly http;
6341
- /** Subscribers notified when a session expires and can't be recovered. */
6342
- private readonly sessionExpiredHandlers;
6343
- /** Shared in-flight recovery so a burst of 401s triggers one re-auth. */
6344
- private recoverInFlight;
6345
- constructor(options?: ClientOptions);
6346
- /** The currently signed-in user, or `null`. */
6347
- get user(): AuthUser | null;
6348
- /** Whether a token-bearing session is active. */
6349
- get isAuthenticated(): boolean;
6350
- /**
6351
- * Subscribe to session expiry that couldn't be silently recovered (the
6352
- * user's identity wasn't in memory to re-prove — typically after a reload
6353
- * where only the token was persisted, and that token has since gone stale).
6354
- * This is the signal to send the user back to the login screen.
6355
- *
6356
- * Returns an unsubscribe function.
6357
- *
6358
- * const off = client.onSessionExpired(() => router.push('/login'));
6359
- */
6360
- onSessionExpired(handler: () => void): () => void;
6361
- /**
6362
- * Attempt a silent re-auth of the current user. Concurrent callers share a
6363
- * single in-flight attempt (a stale token usually 401s several requests at
6364
- * once — we re-auth once, not once per request). On failure the
6365
- * session-expired event fires so the app can react. Exposed so apps can
6366
- * also drive recovery proactively (e.g. on `visibilitychange`).
6367
- */
6368
- recoverSession(): Promise<boolean>;
6369
- }
6370
-
6371
- /**
6372
- * `deviceStore` — at-rest persistence for a paired device (the TV).
6373
- *
6374
- * A device that has completed {@link HostedAuth.startDevicePairing} holds the
6375
- * account's **master seed**. Android WebView has no WebAuthn, so the passkey
6376
- * wrapping used elsewhere isn't available, and re-pairing on every cold start
6377
- * is not acceptable UX for a set-top box. So the seed is persisted, encrypted:
6378
- *
6379
- * | Item | Where | Form |
6380
- * | ------------------------ | ------------------------------------ | ---- |
6381
- * | wrapping key | IndexedDB (`muhkoo.device.v1`) | `CryptoKey`, AES-GCM-256, **non-extractable** |
6382
- * | device identity keypair | IndexedDB (`muhkoo.device.v1`) | `CryptoKeyPair`, ECDSA P-256, private half **non-extractable** |
6383
- * | sealed identity blob | localStorage `muhkoo.tv.identity.v1` | `{"v":1,"iv":…,"ct":…}` over `JSON{username, commitment, seed(b64), pairedAt}` |
6384
- *
6385
- * The split is deliberate — ciphertext in one backend, key in another, and the
6386
- * key non-extractable so JS can *use* it but not export it.
6387
- *
6388
- * ## Be honest about what this is worth
6389
- *
6390
- * **This is obfuscation, not protection.** It raises the cost of a casual
6391
- * `localStorage` dump to "nothing useful", and that is all:
6392
- *
6393
- * - **XSS on the device origin** cannot *exfiltrate* the wrapping key (it is
6394
- * non-extractable) but can absolutely call `decrypt` and read the seed. The
6395
- * only mitigation is that there is no third-party script in the shell —
6396
- * enforce a strict CSP with no `unsafe-inline`, no CDN, no remote fonts.
6397
- * - **ADB / root / a backup extraction / a forensic image recovers the seed.**
6398
- * The IndexedDB LevelDB files hold the AES key bytes in the clear; WebCrypto
6399
- * "non-extractable" is a JS-API boundary, not an OS one. **A stolen device is
6400
- * a stolen identity.**
6401
- *
6402
- * That is not an implementation shortcut, it is the shape of the problem: any
6403
- * wrapping key usable without user interaction lives somewhere equally readable
6404
- * on a keyboard-less device with no hardware key store exposed to WebView.
6405
- * Reduce the blast radius with `android:allowBackup="false"`,
6406
- * `android:debuggable="false"`, a strict CSP, and by storing *only* the 32-byte
6407
- * seed + username + commitment (never a session token, never a recovery phrase).
6408
- *
6409
- * ## Documented upgrade path — no protocol change
6410
- *
6411
- * If the native shell exposes a Keystore bridge:
6412
- *
6413
- * ```java
6414
- * @JavascriptInterface public String wrap(String b64plain); // AES-GCM, StrongBox when present
6415
- * @JavascriptInterface public String unwrap(String b64cipher);
6416
- * ```
6417
- *
6418
- * this module prefers `globalThis.MuhkooKeystore` automatically and falls back
6419
- * to the WebCrypto scheme otherwise. The Keystore key is hardware-backed and
6420
- * non-exportable at the OS level, so a copied filesystem image alone becomes
6421
- * useless. Nothing on the wire, in the endpoint contracts, or in the SDK surface
6422
- * changes — only the two functions below do, internally.
6423
- *
6424
- * ## Degradation
6425
- *
6426
- * Nothing here throws at import time. On a runtime with neither backend (SSR,
6427
- * Workers, Node) the module still loads: {@link loadDeviceIdentity} returns
6428
- * `null`, {@link persistDeviceIdentity} throws a typed
6429
- * {@link DeviceStoreUnavailableError} the caller can swallow, and
6430
- * {@link deviceIdentityKey} falls back to an **in-memory** keypair for the
6431
- * lifetime of the process (so pairing still works; the device is just never
6432
- * recognised as "known" on a later run).
6433
- */
6434
- /** localStorage key holding the AES-GCM-sealed identity blob. */
6435
- declare const DEVICE_IDENTITY_STORAGE_KEY = "muhkoo.tv.identity.v1";
6436
- /** IndexedDB database holding the non-extractable keys. */
6437
- declare const DEVICE_KEY_DB_NAME = "muhkoo.device.v1";
6438
- /** What a paired device persists. The seed **is** the identity — treat it as such. */
6439
- interface PersistedDeviceIdentity {
6440
- username: string;
6441
- /** Decimal-string Poseidon commitment. Pinned: a later pairing that yields a
6442
- * different commitment for this device must be refused, not adopted. */
6443
- commitment: string;
6444
- /** The 32-byte master seed. */
6445
- seed: Uint8Array;
6446
- pairedAt: number;
6447
- }
6448
- /** Thrown when persistence is impossible (no IndexedDB and/or no localStorage). */
6449
- declare class DeviceStoreUnavailableError extends Error {
6450
- constructor(what: string);
6451
- }
6452
- /** Key/value store for live `CryptoKey` objects (IndexedDB in the browser). */
6453
- interface DeviceKeyVault {
6454
- get(id: string): Promise<unknown | null>;
6455
- put(id: string, value: unknown): Promise<void>;
6456
- /** Delete the entire store — used by `clearDeviceIdentity`. */
6457
- destroy(): Promise<void>;
6458
- }
6459
- /** String store for the sealed blob (localStorage in the browser). */
6460
- interface DeviceBlobStore {
6461
- get(key: string): string | null;
6462
- set(key: string, value: string): void;
6463
- remove(key: string): void;
6464
- }
6465
- /** The native Keystore bridge, when the shell provides one (see module docs). */
6466
- interface MuhkooKeystoreBridge {
6467
- wrap(b64plain: string): string | Promise<string>;
6468
- unwrap(b64cipher: string): string | Promise<string>;
7001
+ readonly db: DbNamespace;
7002
+ /** File storage — `client.storage.writeFile(...)`, etc. */
7003
+ readonly storage: StorageNamespace;
7004
+ /** Filesystem over the personal space: directories, versions, real paths. */
7005
+ readonly vfs: VfsNamespace;
7006
+ /** Version control over the VFS: commits, branches, merges — per project. */
7007
+ readonly vcs: VcsNamespace;
7008
+ /** Realtime messaging — `client.message.subscribe(...)`, etc. */
7009
+ readonly message: MessageNamespace;
7010
+ /** Fan-out group spaces — `client.space.createSpace(...)`, etc. */
7011
+ readonly space: SpaceNamespace;
7012
+ /** Programmable Agents — `client.agents.create(appId, …)`, etc. */
7013
+ readonly agents: AgentsNamespace;
7014
+ /** Serverless functions — `client.functions.deploy(appId, …)`, etc. */
7015
+ readonly functions: FunctionsNamespace;
7016
+ /** Access tokens — `client.accessTokens.create(appId, …)`, etc. */
7017
+ readonly accessTokens: AccessTokensNamespace;
7018
+ /** Offline cache + sync — `client.offline.status`, `client.offline.snapshot`, etc. */
7019
+ readonly offline: OfflineManager;
7020
+ private readonly session;
7021
+ private readonly http;
7022
+ /** Subscribers notified when a session expires and can't be recovered. */
7023
+ private readonly sessionExpiredHandlers;
7024
+ /** Shared in-flight recovery so a burst of 401s triggers one re-auth. */
7025
+ private recoverInFlight;
7026
+ constructor(options?: ClientOptions);
7027
+ /** The currently signed-in user, or `null`. */
7028
+ get user(): AuthUser | null;
7029
+ /** Whether a token-bearing session is active. */
7030
+ get isAuthenticated(): boolean;
7031
+ /**
7032
+ * Subscribe to session expiry that couldn't be silently recovered (the
7033
+ * user's identity wasn't in memory to re-prove — typically after a reload
7034
+ * where only the token was persisted, and that token has since gone stale).
7035
+ * This is the signal to send the user back to the login screen.
7036
+ *
7037
+ * Returns an unsubscribe function.
7038
+ *
7039
+ * const off = client.onSessionExpired(() => router.push('/login'));
7040
+ */
7041
+ onSessionExpired(handler: () => void): () => void;
7042
+ /**
7043
+ * Attempt a silent re-auth of the current user. Concurrent callers share a
7044
+ * single in-flight attempt (a stale token usually 401s several requests at
7045
+ * once — we re-auth once, not once per request). On failure the
7046
+ * session-expired event fires so the app can react. Exposed so apps can
7047
+ * also drive recovery proactively (e.g. on `visibilitychange`).
7048
+ */
7049
+ recoverSession(): Promise<boolean>;
6469
7050
  }
6470
- /**
6471
- * Point the store at explicit backends (or `null` to force "unavailable"), and
6472
- * reset the memoized device keypair. Testing / embedding seam — pass `{}` to
6473
- * restore the default browser backends.
6474
- */
6475
- declare function configureDeviceStore(opts?: {
6476
- keys?: DeviceKeyVault | null;
6477
- blobs?: DeviceBlobStore | null;
6478
- keystore?: MuhkooKeystoreBridge | null;
6479
- }): void;
6480
- /**
6481
- * True when {@link deviceIdentityKey} had to fall back to an in-memory keypair
6482
- * because no key vault was available. Such a device can pair, but will never be
6483
- * recognised as "known" afterwards (so approval always needs a fresh factor).
6484
- */
6485
- declare function deviceIdentityIsEphemeral(): boolean;
6486
- /**
6487
- * The device's **persistent** ECDSA P-256 identity keypair, created on first
6488
- * call. This is not a browser fingerprint — it is a key, and every
6489
- * `POST /api/auth/device/code` proves possession of it. Without that proof,
6490
- * "known device" would be an unauthenticated string anyone could claim in order
6491
- * to skip the approver's re-authentication.
6492
- *
6493
- * The private half is non-extractable and never leaves IndexedDB as bytes.
6494
- */
6495
- declare function deviceIdentityKey(): Promise<CryptoKeyPair>;
6496
- /** `base64url(SHA-256(raw SEC1 device identity public key))` — 43 chars. */
6497
- declare function deviceFingerprint(): Promise<string>;
6498
- /** Whether a sealed identity blob exists. Cheap: no crypto, no IndexedDB, no network. */
6499
- declare function hasDeviceIdentity(): Promise<boolean>;
6500
- /**
6501
- * Seal and store the identity. Throws {@link DeviceStoreUnavailableError} when
6502
- * there is nowhere to put it — callers should treat that as "this device won't
6503
- * resume", not as a pairing failure.
6504
- */
6505
- declare function persistDeviceIdentity(v: PersistedDeviceIdentity): Promise<void>;
6506
- /**
6507
- * Recover the persisted identity, or `null` when there is none / it can't be
6508
- * opened (wiped IndexedDB, a different browser profile, a corrupted blob).
6509
- * Never throws — a cold start with unreadable storage must fall back to a fresh
6510
- * pairing, not to an error screen.
6511
- */
6512
- declare function loadDeviceIdentity(): Promise<PersistedDeviceIdentity | null>;
6513
- /**
6514
- * Wipe everything: the sealed blob **and** the whole key database — so the
6515
- * wrapping key never outlives the ciphertext it opens, and the device identity
6516
- * key is regenerated (a re-pair is then correctly treated as a new device).
6517
- */
6518
- declare function clearDeviceIdentity(): Promise<void>;
6519
7051
 
6520
7052
  /**
6521
7053
  * Three-way line merge.
@@ -6558,92 +7090,6 @@ declare function merge3Text(base: string, ours: string, theirs: string, labels?:
6558
7090
  conflicted: boolean;
6559
7091
  };
6560
7092
 
6561
- /**
6562
- * Passkey factor (M1.1) — WebAuthn with the **PRF extension**.
6563
- *
6564
- * A passkey becomes a device-bound, platform-synced source of a stable secret:
6565
- * for a fixed salt, the authenticator's PRF returns the same 32 bytes, which we
6566
- * HKDF into an AES key that wraps the master seed. The server never sees the
6567
- * passkey — it's purely a local key source; the ZK proof remains the server auth.
6568
- *
6569
- * BROWSER ONLY (`navigator.credentials`). The PRF extension fields aren't in the
6570
- * standard DOM types yet, so they're reached through narrow casts.
6571
- */
6572
- /** Whether this environment can do WebAuthn at all. (PRF support is checked at enroll.) */
6573
- declare function passkeySupported(): boolean;
6574
- /**
6575
- * Whether this browser's authenticator can do the **PRF extension** our passkey
6576
- * factor needs. A browser can save passkeys (e.g. for password autofill) yet not
6577
- * expose PRF — PRF is what derives the stable secret we wrap the seed under, so a
6578
- * passkey without it is useless to us.
6579
- *
6580
- * Uses `PublicKeyCredential.getClientCapabilities()` (the `extension:prf` cap)
6581
- * where available. Returns:
6582
- * - `true` — PRF is supported,
6583
- * - `false` — definitively NOT supported (hide the option),
6584
- * - `null` — couldn't determine (older browser without the capabilities API);
6585
- * the caller may optimistically allow and let enroll surface a real
6586
- * error if it turns out PRF isn't there.
6587
- */
6588
- declare function passkeyPrfCapable(): Promise<boolean | null>;
6589
- /** Best-effort registrable RP id — the host. Apps spanning subdomains should pass
6590
- * the parent domain (e.g. "muhkoo.dev") explicitly. */
6591
- declare function defaultRpId(): string;
6592
- /**
6593
- * Can a passkey enrolled under `rpId` be used from the CURRENT origin?
6594
- *
6595
- * WebAuthn only accepts an RP id that is the origin's own host or a registrable
6596
- * *parent* of it — so a passkey enrolled on `app.example.com` is unusable on
6597
- * `app.example.net`, and one enrolled on a full host is unusable from a sibling
6598
- * host. An app served on BOTH a custom domain and the platform host therefore
6599
- * needs one passkey per origin.
6600
- *
6601
- * Call this BEFORE `navigator.credentials.get()`: passing a foreign rpId throws
6602
- * a raw "The requested RPID did not match the origin or related origins", which
6603
- * is a dead end for the user. Checking first lets the caller fall back to normal
6604
- * sign-in (and offer to enroll a passkey for this origin) instead.
6605
- */
6606
- declare function rpIdUsableForOrigin(rpId: string | undefined | null, host?: string): boolean;
6607
- /**
6608
- * Would {@link AuthNamespace.loginWithPasskey} accept a factor recorded with
6609
- * this `rpId`, from `host`?
6610
- *
6611
- * This is deliberately NOT the same as {@link rpIdUsableForOrigin}. That answers
6612
- * a raw WebAuthn question and is `false` for a missing rpId. But a factor with
6613
- * NO recorded rpId predates per-origin enrolment, and `loginWithPasskey`
6614
- * resolves it as `params.rpId ?? host` — so it IS usable, from anywhere.
6615
- *
6616
- * Callers deciding "should I even try a passkey here?" must use this one.
6617
- * Reaching for `rpIdUsableForOrigin` instead silently excludes every legacy
6618
- * passkey holder, which has locked real users out of production twice.
6619
- *
6620
- * The `?? host` fallback applies to nullish only, matching `loginWithPasskey`:
6621
- * an empty-string rpId stays unusable rather than resolving to the host.
6622
- */
6623
- declare function passkeyUsableFromOrigin(rpId: string | undefined | null, host?: string): boolean;
6624
- /** Thrown when the account's passkey belongs to a different origin. Callers
6625
- * should treat this as "no passkey here" and fall back to another factor. */
6626
- declare class PasskeyOriginError extends Error {
6627
- readonly code = "passkey_wrong_origin";
6628
- constructor(message?: string);
6629
- }
6630
- interface PasskeyEnrollment {
6631
- credentialId: Uint8Array;
6632
- prfSalt: Uint8Array;
6633
- prfOutput: Uint8Array;
6634
- }
6635
- /**
6636
- * Create a passkey with PRF and return its credential id, a fresh PRF salt, and
6637
- * the PRF output for that salt. Throws if the authenticator can't do PRF.
6638
- */
6639
- declare function createPasskeyWithPrf(opts: {
6640
- rpId: string;
6641
- rpName: string;
6642
- username: string;
6643
- }): Promise<PasskeyEnrollment>;
6644
- /** Evaluate a passkey's PRF for `salt` → 32 bytes. Used at recover/login. */
6645
- declare function evaluatePasskeyPrf(rpId: string, credentialId: Uint8Array, salt: Uint8Array): Promise<Uint8Array>;
6646
-
6647
7093
  /**
6648
7094
  * Muhkoo app decorators — declare your app's agent-facing surface in code, then
6649
7095
  * "eject" a system prompt you hand to a Programmable Agent.
@@ -7548,5 +7994,271 @@ declare class KeyringClient implements KeyringTransport {
7548
7994
  fetchMetadata(): Promise<SpaceMetadata | null>;
7549
7995
  }
7550
7996
 
7551
- export { ACCESS_TOKEN_SCOPES, AES_GCM_IV_BYTES, AES_GCM_KEY_BYTES, AES_GCM_TAG_BYTES, AccessTokensNamespace, AgentsNamespace, AuthClient, AuthNamespace, AuthPublicInput, Authenticator, BlockEngine, BroadcastChannel, BroadcastChannelEvents, ChannelExistsError, ChannelNotFoundError, ChunkCipher, Client, ConnectivityManager, DEFAULT_AUTH_BASE_URL, DEFAULT_BASE_URL, DEFAULT_BRANCH, DEFAULT_FN_HOST_SUFFIX, DEVICE_IDENTITY_STORAGE_KEY, DEVICE_KEY_DB_NAME, DbCache, DevicePairingError, DeviceRevokedError, DeviceStoreUnavailableError, DoubleRatchet, DoubleRatchetCipher, DoubleRatchetManager, EncryptedSession, EventCore, EventCoreEvents, Field, Field as FieldElement, FileStorage, FunctionsNamespace, HLC, HashKnowledge, HostedAuth, HttpClient, HttpError, IndexedDbStore, KEEPER_MEMBER_ID, KeyStore, KeyringClient, KvCache, KvNamespace, LocalEngineHost, LocalStorageSessionStore, MemorySessionStore, Message, Message as MessageDefault, MessageNamespace, MuhkooAgent, MuhkooDB, MuhkooFunction, MuhkooSpace, NoopStore, OfflineLockedError, OfflineManager, OutboundQueue, P2P_SUBJECT, PAIRING_CODE_ALPHABET, PAIRING_CODE_LENGTH, PBKDF2_DEFAULT_ITERATIONS, PREIMAGE_POK_VERIFICATION_KEY, Packet, PasskeyOriginError, PeerExchange, PeerNetwork, PersonalSpaceClient, Poseidon, PreimagePoK, ROOT_ID, ReauthRequiredError, ReedSolomonCodec, Repo, Space as Room, SerializeMessage, SessionState, ShardCache, ShardClient, SharedSpaceClient, SnapshotNamespace, Space, SpaceCache, SpaceKeyring, SpaceNamespace, SpacePacketCipher, SpaceSignaler, StorageNamespace, SyncEngine, VFS_ROOT_INFO, VaultUnavailableError, VcsError, VcsNamespace, VfsConflictError, VfsLockedError, VfsNamespace, VfsNotFoundError, WSTransport, WebRtcTransport, WorkerEngineHost, ZERO_HLC, ZkAuth, buildCommitment, canonicalUserCode, clearDeviceIdentity, compareHlc, compilePrograms, configureDeviceStore, createPasskeyWithPrf, decodeFromHex, decorators, decryptAesGcm, defaultCircuitUrls, defaultRpId, defaultSessionStore, deleteBeatsWrite, deleteRegister, deriveAesKeyFromPassphrase, deriveBitsHkdf, deriveIdentity, deriveIdentityFromSeed, deriveMasterSeedFromPassword, deviceFingerprint, deviceIdentityIsEphemeral, deviceIdentityKey, dirKey, ejectAgentPrompt, ejectAgentTools, emptyMap, emptyRegister, encodeToHex, encryptAesGcm, evaluatePasskeyPrf, exportPublicKeyBase64, exportPublicKeyHex, formatUserCode, generateAuthProof, generateDevicePairingKeypair, generatePkce, getMuhkooAppDescriptor, getSubtle, hasDeviceIdentity, headKey, historyKey, initBn128Wasm, initRsWasm, initializeCircuits, isNewer, isOfflineCapable, isP2pCapable, isProvisional, loadDeviceIdentity, manifestToStat, materializeRow, merge3, merge3Text, mergeKey, mergeMap, mergeMessage, mergeRegister, messageKey, objectKey, pack, padHandle, pairingVerificationCode, passkeyPrfCapable, passkeySupported, passkeyUsableFromOrigin, persistDeviceIdentity, poseidonHash, protocol, provisionalHandle, quickVerify, randomBytes, randomState, refKey, refsIndexKey, rpIdUsableForOrigin, sealSeed, sealSeedToDevice, shardHash, signMessage, stableStringify, toField, tombstoneRow, unpack, unsealSeed, unsealSeedFromDevice, unwrapWithPassphrase, verify, verifyGroth16, verifyHashKnowledge, verifyPreimagePoK, wrapWithPassphrase, writeBeatsTombstone, writeColumns, writeRegister };
7552
- export type { ACL, ACLEntry, AccessTokenInfo, AccessTokensNamespaceDeps, AgentConfig, AgentCreateInput, AgentDbToolMode, AgentProvisioned, AgentScopeOpts, AgentSkill, AgentToolsConfig, AgentTrigger, AgentTriggerType, AgentUpdateInput, AgentsNamespaceDeps, AttachedPeerNetwork, AuthClientOptions, AuthKeyPair, AuthSuccess, AuthToken, AuthType, AuthUser, BlockStore, Bn128WasmInstance, BroadcastChannelEvent, BroadcastChannelOptions, CachedSpaceMessage, Change, ChannelLike, ChunkKeyMaterial, ChunkManifest, CipherFrame, CipherMessage, CipherMessageHeader, CircuitBufferConfig, CircuitUrls, ClientOptions, Commit, Conflict, ConnectivityOptions, ConnectivityState, CreateAccessTokenInput, CreatedAccessToken, DbQueryPage, DehydratedKeypair, DehydratedKeys, DeviceBlobStore, DeviceKeyVault, DevicePairingErrorReason, DevicePairingKeypair, DevicePairingPoll, DevicePairingRequest, DevicePairingSession, DirEntry, DirNode, DoubleRatchetCipherDeps, ECDHCurve, EncodeResult, EngineHandle, EngineHost, Entry, EphemeralEvent, EpochKeyProvider, FileEntry, FileManifest, FileNamespaceDeps, FileStat, FileStorageOptions, FunctionCaps, FunctionConfig, FunctionDeployInput, FunctionInvokeOptions, FunctionScopeOpts, FunctionTrigger, FunctionTriggerType, FunctionTriggers, FunctionUpdateInput, FunctionsNamespaceDeps, Groth16Proof, Hasher, Head, HistoryRecord, HlcParts, HlcState, HostedAuthDeps, HttpClientOptions, IncomingFrame, KeyExchangeFrame, KeyPair, Keypair, KeyringClientDeps, KeyringTransport, KvEntry, KvNamespaceDeps, LogEntry, LoginOptions, LwwMap, LwwRegister, Merge3Result, MergeRegionConflict, MergeResult, MessageBody, MessageDeletedEvent, MessageEntry, MessageEvent, MessageHeaders, MessageNamespaceDeps, MessageOp, MessageOptions, MessageSubscription, MuhkooAgentMeta, MuhkooAgentToolsConfig, MuhkooAppDescriptor, MuhkooDBAccess, MuhkooDBMeta, MuhkooFunctionMeta, MuhkooKeystoreBridge, MuhkooMessageEvent, MuhkooSpaceMeta, OAuthProvider, OfflineEntry, OfflineManagerDeps, OfflineStore, OutSink, PacketCipher, PacketHeaders, PacketOptions, PairedDevice, Participant, PasskeyEnrollment, PeerBlockSource, PeerExchangeOptions, PeerId, PeerNetworkOptions, PeerTransport, Permission, PermissionCheckRequest, PermissionCheckResult, PersistedDeviceIdentity, PersonalSpaceClientOptions, Pkce, PublicKeyJWK, QueueEntry, RatchetState, ReceiveResult, RegisterParams, Replayer, Role, SpaceDeps as RoomDeps, SpaceFileMetadata as RoomFileMetadata, Scope, SealedHandoff, SealedHeaders, SealedRecord, Session, SessionOptions, SessionStore, SetOptions, ShardByteCache, ShardClientOptions, SharedSpaceClientOptions, SignalEnvelope, SignalKind, SignalingSpace, SnapshotNamespaceDeps, SnarkProof, SpaceChannelLike, SpaceCursor, SpaceDeps, SpaceFileMetadata, SpaceKeyCache, SpaceKeyringDeps, SpaceMessageEvent, SpaceNamespaceDeps, SpaceOfflineAdapter, StartDevicePairingOptions, StorageChangeEvent, StoreName, StoredSession, Subscription, Tree, TreeDir, TreeEntry, TreeFile, UserIdentity, VaultFactor, VcsNamespaceDeps, VcsNamespaceOptions, VerificationKey, VfsContentStore, VfsNamespaceDeps, VfsStat, VfsStore, WSMessage, WSMessageType, WSTransportOptions, WaitForDevicePairingOptions, WebRtcTransportOptions, WrappedPayload, WriteFileInput, WriteFileOptions, WriteToShardsInput, ZkAuthDeps, ZkAuthenticateRequest, ZkChallengeResponse, ZkCompiled, ZkIdentity, ZkRegisterRequest, ZkRegisterResponse };
7997
+ declare class Logger {
7998
+ private prefix;
7999
+ private logLevel;
8000
+ private console;
8001
+ private showCaller;
8002
+ level: string;
8003
+ constructor(prefix?: string, LOGLEVEL?: string, showCaller?: boolean);
8004
+ private shouldLog;
8005
+ /**
8006
+ * Extract caller information from stack trace
8007
+ */
8008
+ private getCallerInfo;
8009
+ /**
8010
+ * Format caller info for display
8011
+ */
8012
+ private formatCallerInfo;
8013
+ setLevel(level: string): void;
8014
+ setShowCaller(show: boolean): void;
8015
+ info(...args: any[]): void;
8016
+ log(...args: any[]): void;
8017
+ verbose(...args: any[]): void;
8018
+ success(...args: any[]): void;
8019
+ warn(...args: any[]): void;
8020
+ error(...args: any[]): void;
8021
+ debug(...args: any[]): void;
8022
+ assert(condition?: boolean, ...data: any[]): void;
8023
+ clear(): void;
8024
+ count(label?: string): void;
8025
+ countReset(label?: string): void;
8026
+ dir(item?: any, options?: any): void;
8027
+ dirxml(...data: any[]): void;
8028
+ group(...data: any[]): void;
8029
+ groupCollapsed(...data: any[]): void;
8030
+ groupEnd(): void;
8031
+ table(tabularData?: any, properties?: string[]): void;
8032
+ time(label?: string): void;
8033
+ timeEnd(label?: string): void;
8034
+ timeLog(label?: string, ...data: any[]): void;
8035
+ trace(...data: any[]): void;
8036
+ profile(label?: string): void;
8037
+ profileEnd(label?: string): void;
8038
+ timeStamp(label?: string): void;
8039
+ }
8040
+
8041
+ declare const log: Logger;
8042
+
8043
+ declare global {
8044
+ interface Console {
8045
+ success(...args: any[]): void;
8046
+ verbose(...args: any[]): void;
8047
+ }
8048
+ }
8049
+
8050
+ /**
8051
+ * Byte conversion helpers usable in both Node and the browser.
8052
+ *
8053
+ * Replaces Node's `Buffer` API for the small set of operations we actually
8054
+ * need in the crypto layer: hex/base64/base64url encode + decode, and array
8055
+ * concatenation. Lets `DoubleRatchet`, `KeyStore`, etc. compile against
8056
+ * platforms without `Buffer` (browsers, CF Workers).
8057
+ */
8058
+ /** Encode a byte array as a lowercase hex string. */
8059
+ declare function toHex(bytes: Uint8Array): string;
8060
+ /** Decode a hex string into a byte array. Accepts upper/lowercase; rejects odd-length input. */
8061
+ declare function fromHex(hex: string): Uint8Array<ArrayBuffer>;
8062
+ /** Encode a byte array as a standard base64 string. */
8063
+ declare function toBase64(bytes: Uint8Array): string;
8064
+ /** Decode a standard base64 string to bytes. */
8065
+ declare function fromBase64(b64: string): Uint8Array<ArrayBuffer>;
8066
+ /** Encode a byte array as a base64url string (no padding). */
8067
+ declare function toBase64Url(bytes: Uint8Array): string;
8068
+ /** Decode a base64url string (with or without padding) to bytes. */
8069
+ declare function fromBase64Url(b64url: string): Uint8Array<ArrayBuffer>;
8070
+ /** Concatenate any number of byte arrays into a single Uint8Array. */
8071
+ declare function concatBytes(...arrays: (Uint8Array | ArrayBuffer)[]): Uint8Array<ArrayBuffer>;
8072
+ /** Encode a UTF-8 string as bytes. Thin wrapper around TextEncoder for symmetry. */
8073
+ declare function utf8Encode(s: string): Uint8Array<ArrayBuffer>;
8074
+ /** Decode UTF-8 bytes to a string. */
8075
+ declare function utf8Decode(bytes: Uint8Array): string;
8076
+
8077
+ /**
8078
+ * @public
8079
+ * @remarks Asserts the basic type of a variable
8080
+ * @param value - The value to check
8081
+ * @param type - The expected type
8082
+ *
8083
+ * @throws Error - If the type of the value does not match the expected type
8084
+ *
8085
+ */
8086
+ declare function assertType<T>(value: unknown, typeOrConstructors: string | (new (...args: any[]) => any) | (new (...args: any[]) => any)[]): asserts value is T;
8087
+ /**
8088
+ * @public
8089
+ * @remarks Encodes a string to Base58
8090
+ * @param input - the data to encode, must be a cloneable object
8091
+ * @returns string
8092
+ */
8093
+ declare function base58Encode(input: ArrayBuffer | string): string;
8094
+ /**
8095
+ * @public
8096
+ * @remarks Decodes a Base58 string to a Uint8Array
8097
+ * @param input - the Base58 string to decode
8098
+ * @returns Uint8Array
8099
+ */
8100
+ declare function base58Decode(input: string): ArrayBuffer;
8101
+ /**
8102
+ * @public
8103
+ * @remarks Generates a checksum for a given input
8104
+ * @param input - The input to generate a checksum for as a string
8105
+ * @returns string
8106
+ */
8107
+ declare function generateChecksum(input: string): string;
8108
+ /**
8109
+ * @public
8110
+ * @remarks Verifies a checksum for a given input
8111
+ * @param input - The input to verify as a string
8112
+ * @param checksum - The checksum to verify as a string
8113
+ * @returns boolean
8114
+ */
8115
+ declare function verifyChecksum(input: string, checksum: string): boolean;
8116
+ /**
8117
+ * @internal
8118
+ * @remarks Generates a random UUID for a given packet
8119
+ * @returns string
8120
+ */
8121
+ declare function _packetId(): string;
8122
+ /**
8123
+ * @internal
8124
+ * @remarks Generates a random UUID for a given object
8125
+ * @returns string
8126
+ *
8127
+ */
8128
+ declare function _objectId(): string;
8129
+ /**
8130
+ * @internal
8131
+ * @remarks Generates a random UUID for a given user
8132
+ * @returns string
8133
+ *
8134
+ */
8135
+ declare function _userId(): string;
8136
+ /**
8137
+ * @internal
8138
+ * @remarks Generates a random UUID for a given account
8139
+ * @returns string
8140
+ *
8141
+ */
8142
+ declare function _accountId(): string;
8143
+ /**
8144
+ * @internal
8145
+ * @remarks Generates a random UUID for a socket
8146
+ * @returns string
8147
+ *
8148
+ */
8149
+ declare function _socketId(): string;
8150
+ /**
8151
+ * @internal
8152
+ * @remarks Generates a random UUID for a message
8153
+ * @returns string
8154
+ */
8155
+ declare function _messageId(): string;
8156
+ /**
8157
+ * @public
8158
+ * @remarks Generates a random UUID and base58 encodes it
8159
+ * @returns string
8160
+ */
8161
+ declare function generateId(): string;
8162
+ /**
8163
+ * @public
8164
+ * @remarks Serializes data to a base58 encoded string
8165
+ * @param data - The data to serialize
8166
+ * @typeParam T - The type of the data to serialize
8167
+ * @returns base58 encoded string
8168
+ */
8169
+ declare function serialize<T>(data: T): string;
8170
+ /**
8171
+ * @public
8172
+ * @remarks Deserializes a base58 encoded string to data
8173
+ * @param base58String - The base58 encoded string to deserialize
8174
+ * @returns Deserialized data
8175
+ */
8176
+ declare function deserialize<T>(base58String: string): T;
8177
+ /**
8178
+ * @public
8179
+ * @remarks Retries a method a specified number of times with a delay between each attempt, this is a decorator
8180
+ * @param retries - The number of times to retry the method
8181
+ * @param delay - The delay in milliseconds between each attempt
8182
+ */
8183
+ declare function Retry(retries?: number, delay?: number): MethodDecorator;
8184
+ /**
8185
+ * @public
8186
+ * @remarks Waits for a class property to be ready before executing a method, this is a decorator.
8187
+ * @param readyPropertyName - The class property to check for readiness
8188
+ */
8189
+ declare function Ready(readyPropertyName?: string): MethodDecorator | PropertyDecorator;
8190
+ /**
8191
+ * Converts bytes into a more human-friendly format (e.g., KB, MB, GB).
8192
+ * @internal
8193
+ * @param bytes - The number of bytes to convert.
8194
+ * @param decimals - The number of decimals to display (default is 2).
8195
+ * @returns A formatted string representing the size in appropriate units.
8196
+ */
8197
+ declare function _formatBytes(bytes: number, decimals?: number): string;
8198
+ /**
8199
+ * Retrieves the IP address(es) for a given hostname using Cloudflare's DNS-over-HTTPS service.
8200
+ *
8201
+ * This function queries the Cloudflare DNS API to resolve the provided hostname to its corresponding
8202
+ * IPv4 address(es) (A records). It returns an array of IP addresses or logs an error if the resolution fails.
8203
+ *
8204
+ * @param hostname - The hostname to resolve (e.g., "example.com").
8205
+ * @returns A promise that resolves to an array of IP addresses (strings) associated with the hostname.
8206
+ * If no IP addresses are found or an error occurs, an empty array or undefined may be returned.
8207
+ * @throws Error If the DNS query fails due to a network error or invalid response from the server.
8208
+ * @public
8209
+ * @example
8210
+ * ```typescript
8211
+ * import { log } from '@muhkoo/connect/utilities';
8212
+ * getIPAddress("example.com").then(ips => log.debug(ips));
8213
+ * // Output: ["93.184.216.34"]
8214
+ * ```
8215
+ */
8216
+ declare function getIPAddress(hostname: string): Promise<any>;
8217
+ declare function isNumber(value: string | number): boolean;
8218
+ declare function getId(row: {
8219
+ insertId?: number;
8220
+ id?: number;
8221
+ }): number;
8222
+
8223
+ /**
8224
+ * SDK build version — stamped to the console when a {@link Client} is created.
8225
+ *
8226
+ * During active development, bump the `-alpha.N` suffix whenever we ship a build
8227
+ * so we can confirm in the browser console that the page is running the build we
8228
+ * expect (and isn't serving a stale cached bundle). Keep this in sync with
8229
+ * `package.json` `version`.
8230
+ */
8231
+ declare const VERSION = "0.14.0-alpha.9";
8232
+
8233
+ /**
8234
+ * The SDK's process-global logger.
8235
+ *
8236
+ * Eight modules under `src/` read a bare `appLogger` off `globalThis` rather
8237
+ * than importing one (`src/crypto/{KeyStore,DoubleRatchet,DoubleRatchetManager,
8238
+ * Authenticator}.ts`, `src/core/Client.ts`,
8239
+ * `src/core/namespaces/HostedAuth.ts`, `src/events/EventCore.ts`), so the
8240
+ * binding has to exist before any of them runs, and the `declare global` below
8241
+ * is what makes that typecheck.
8242
+ *
8243
+ * This module is the single place that bootstrap happens. It previously lived
8244
+ * inline in three entry files — `src/browser/index.ts`, `src/workers/index.ts`,
8245
+ * and `src/core/index.ts` (which `src/server/index.ts` picked up as an import
8246
+ * side effect) — three copies that could drift.
8247
+ *
8248
+ * `??=` makes it idempotent, so importing it from several entries in one
8249
+ * process is safe and the first writer wins.
8250
+ *
8251
+ * KNOWN HAZARD: package.json declares `"sideEffects": false`, which entitles a
8252
+ * tree-shaker to drop a module imported purely for effect. The entries import
8253
+ * this one for its `export`s as well, so it survives today — but the durable
8254
+ * fix is to convert those eight bare reads into real imports of `appLogger`
8255
+ * from here, which removes the reliance on a global entirely.
8256
+ */
8257
+
8258
+ declare global {
8259
+ var appLogger: InstanceType<typeof Logger>;
8260
+ }
8261
+ declare const appLogger: InstanceType<typeof Logger>;
8262
+
8263
+ export { ACCESS_TOKEN_SCOPES, AES_GCM_IV_BYTES, AES_GCM_KEY_BYTES, AES_GCM_TAG_BYTES, AccessTokensNamespace, AgentsNamespace, AuthClient, AuthNamespace, AuthPublicInput, Authenticator, BlockEngine, BroadcastChannel, BroadcastChannelEvents, ChannelExistsError, ChannelNotFoundError, ChunkCipher, CircuitIntegrityError, Client, ConnectivityManager, DEFAULT_AUTH_BASE_URL, DEFAULT_BASE_URL, DEFAULT_BRANCH, DEFAULT_FN_HOST_SUFFIX, DEVICE_IDENTITY_STORAGE_KEY, DEVICE_KEY_DB_NAME, DbCache, DbNamespace, DbTable, DevicePairingError, DeviceRevokedError, DeviceStoreUnavailableError, DoubleRatchet, DoubleRatchetCipher, DoubleRatchetManager, EncryptedSession, EventCore, EventCoreEvents, Field, Field as FieldElement, FileStorage, FunctionsNamespace, HLC, HashKnowledge, HostedAuth, HttpClient, HttpError, IndexedDbStore, KEEPER_MEMBER_ID, KeyStore, KeyringClient, KvCache, KvNamespace, LocalEngineHost, LocalStorageSessionStore, Logger, MemorySessionStore, Message, Message as MessageDefault, MessageNamespace, MuhkooAgent, MuhkooDB, MuhkooFunction, MuhkooSpace, NoopStore, OfflineLockedError, OfflineManager, OutboundQueue, P2P_SUBJECT, PAIRING_CODE_ALPHABET, PAIRING_CODE_LENGTH, PBKDF2_DEFAULT_ITERATIONS, PINNED_CIRCUIT_DIGESTS, PREIMAGE_POK_VERIFICATION_KEY, Packet, PasskeyOriginError, PeerExchange, PeerNetwork, PersonalSpaceClient, Poseidon, PreimagePoK, ROOT_ID, Ready, ReauthRequiredError, ReedSolomonCodec, Repo, Retry, Space as Room, SerializeMessage, SessionState, ShardCache, ShardClient, SharedSpaceClient, SnapshotNamespace, Space, SpaceCache, SpaceKeyring, SpaceNamespace, SpacePacketCipher, SpaceSignaler, StorageNamespace, SyncEngine, VERSION, VFS_ROOT_INFO, VaultUnavailableError, VcsError, VcsNamespace, VfsConflictError, VfsCorruptRecordError, VfsLimitError, VfsLockedError, VfsNamespace, VfsNotFoundError, WSTransport, WebRtcTransport, WorkerEngineHost, ZERO_HLC, ZkAuth, _accountId, _formatBytes, _messageId, _objectId, _packetId, _socketId, _userId, appLogger, assertType, base58Decode, base58Encode, buildCommitment, canonicalUserCode, clearDeviceIdentity, compareHlc, compilePrograms, concatBytes, configureDeviceStore, createPasskeyWithPrf, decodeFromHex, decorators, decryptAesGcm, defaultCircuitUrls, defaultRpId, defaultSessionStore, deleteBeatsWrite, deleteRegister, deriveAesKeyFromPassphrase, deriveBitsHkdf, deriveIdentity, deriveIdentityFromSeed, deriveMasterSeedFromPassword, deserialize, deviceFingerprint, deviceIdentityIsEphemeral, deviceIdentityKey, dirKey, ejectAgentPrompt, ejectAgentTools, emptyMap, emptyRegister, encodeToHex, encryptAesGcm, evaluatePasskeyPrf, exportPublicKeyBase64, exportPublicKeyHex, formatUserCode, fromBase64, fromBase64Url, fromHex, generateAuthProof, generateChecksum, generateDevicePairingKeypair, generateId, generatePkce, getIPAddress, getId, getMuhkooAppDescriptor, getSubtle, hasDeviceIdentity, headKey, historyKey, initBn128Wasm, initRsWasm, initializeCircuits, isNewer, isNumber, isOfflineCapable, isP2pCapable, isProvisional, loadDeviceIdentity, log, manifestToStat, materializeRow, merge3, merge3Text, mergeKey, mergeMap, mergeMessage, mergeRegister, messageKey, objectKey, pack, padHandle, pairingVerificationCode, passkeyPrfCapable, passkeySupported, passkeyUsableFromOrigin, persistDeviceIdentity, poseidonHash, protocol, provisionalHandle, quickVerify, randomBytes, randomState, refKey, refsIndexKey, rpIdUsableForOrigin, sealSeed, sealSeedToDevice, serialize, shardHash, signMessage, stableStringify, toBase64, toBase64Url, toField, toHex, tombstoneRow, unpack, unsealSeed, unsealSeedFromDevice, unwrapWithPassphrase, utf8Decode, utf8Encode, verify, verifyChecksum, verifyGroth16, verifyHashKnowledge, verifyPreimagePoK, wrapWithPassphrase, writeBeatsTombstone, writeColumns, writeRegister };
8264
+ export type { ACL, ACLEntry, AccessTokenInfo, AccessTokensNamespaceDeps, AgentConfig, AgentCreateInput, AgentDbToolMode, AgentProvisioned, AgentScopeOpts, AgentSkill, AgentToolsConfig, AgentTrigger, AgentTriggerType, AgentUpdateInput, AgentsNamespaceDeps, AttachedPeerNetwork, AuthClientOptions, AuthKeyPair, AuthSuccess, AuthToken, AuthType, AuthUser, BlockStore, Bn128WasmInstance, BroadcastChannelEvent, BroadcastChannelOptions, CachedSpaceMessage, Change, ChannelLike, ChunkKeyMaterial, ChunkManifest, CipherFrame, CipherMessage, CipherMessageHeader, CircuitBufferConfig, CircuitIntegrity, CircuitUrls, ClientOptions, Commit, Conflict, ConnectivityOptions, ConnectivityState, CreateAccessTokenInput, CreatedAccessToken, DbFilterOp, DbNamespaceDeps, DbQuery, DbQueryPage, DbQueryResult, DbWhereCondition, DehydratedKeypair, DehydratedKeys, DeviceBlobStore, DeviceKeyVault, DevicePairingErrorReason, DevicePairingKeypair, DevicePairingPoll, DevicePairingRequest, DevicePairingSession, DirEntry, DirNode, DoubleRatchetCipherDeps, ECDHCurve, EncodeResult, EngineHandle, EngineHost, Entry, EphemeralEvent, EpochKeyProvider, FileEntry, FileManifest, FileNamespaceDeps, FileStat, FileStorageOptions, FunctionCaps, FunctionConfig, FunctionDeployInput, FunctionInvokeOptions, FunctionScopeOpts, FunctionTrigger, FunctionTriggerType, FunctionTriggers, FunctionUpdateInput, FunctionsNamespaceDeps, Groth16Proof, Hasher, Head, HistoryRecord, HlcParts, HlcState, HostedAuthDeps, HttpClientOptions, IncomingFrame, KeyExchangeFrame, KeyPair, Keypair, KeyringClientDeps, KeyringTransport, KvEntry, KvNamespaceDeps, LogEntry, LoginOptions, LwwMap, LwwRegister, Merge3Result, MergeRegionConflict, MergeResult, MessageBody, MessageDeletedEvent, MessageEntry, MessageEvent, MessageHeaders, MessageNamespaceDeps, MessageOp, MessageOptions, MessageSubscription, MuhkooAgentMeta, MuhkooAgentToolsConfig, MuhkooAppDescriptor, MuhkooDBAccess, MuhkooDBMeta, MuhkooFunctionMeta, MuhkooKeystoreBridge, MuhkooMessageEvent, MuhkooSpaceMeta, OAuthProvider, OfflineEntry, OfflineManagerDeps, OfflineStore, OutSink, PacketCipher, PacketHeaders, PacketOptions, PairedDevice, Participant, PasskeyEnrollment, PeerBlockSource, PeerExchangeOptions, PeerId, PeerNetworkOptions, PeerTransport, Permission, PermissionCheckRequest, PermissionCheckResult, PersistedDeviceIdentity, PersonalSpaceClientOptions, Pkce, PublicKeyJWK, QueueEntry, RatchetState, ReceiveResult, RegisterParams, Replayer, Role, SpaceDeps as RoomDeps, SpaceFileMetadata as RoomFileMetadata, Scope, SealedHandoff, SealedHeaders, SealedRecord, Session, SessionOptions, SessionStore, SetOptions, ShardByteCache, ShardClientOptions, SharedSpaceClientOptions, SignalEnvelope, SignalKind, SignalingSpace, SnapshotNamespaceDeps, SnarkProof, SpaceChannelLike, SpaceCursor, SpaceDeps, SpaceFileMetadata, SpaceKeyCache, SpaceKeyringDeps, SpaceMessageEvent, SpaceNamespaceDeps, SpaceOfflineAdapter, StartDevicePairingOptions, StorageChangeEvent, StoreName, StoredSession, Subscription, Tree, TreeDir, TreeEntry, TreeFile, UserIdentity, VaultFactor, VcsNamespaceDeps, VcsNamespaceOptions, VerificationKey, VfsContentStore, VfsNamespaceDeps, VfsStat, VfsStore, WSMessage, WSMessageType, WSTransportOptions, WaitForDevicePairingOptions, WebRtcTransportOptions, WrappedPayload, WriteFileInput, WriteFileOptions, WriteToShardsInput, ZkAuthDeps, ZkAuthenticateRequest, ZkChallengeResponse, ZkCompiled, ZkIdentity, ZkRegisterRequest, ZkRegisterResponse };