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

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
  }
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;
1138
+ }
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;
1215
+ }
1216
+
1345
1217
  /**
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.
1218
+ * HTTP client for the accelerator's `/api/auth/*` endpoints.
1219
+ *
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.
1224
+ *
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).
1348
1228
  */
1349
- interface FileManifest {
1350
- /** Stable file ID. Assigned by the SDK at write time; used to read or delete. */
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;
1239
+ }
1240
+ interface ZkRegisterResponse {
1241
+ success: boolean;
1242
+ }
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 {
1351
1271
  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;
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). */
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>;
1301
+ /**
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.
1305
+ */
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
+ }>;
1313
+ /**
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.
1317
+ */
1318
+ oprfEvaluateGated(username: string, blinded: string, factorType: "email" | "google", purpose: "enroll" | "recover", verifyToken: string): Promise<{
1319
+ evaluated: string;
1320
+ evaluated2: string;
1321
+ }>;
1322
+ /**
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).
1326
+ */
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;
1340
+ }>;
1341
+ /**
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`.
1345
+ */
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
+ }>;
1376
+ /**
1377
+ * Read a factor record for `username` (returns an indistinguishable decoy if
1378
+ * absent). Gated types (email) require the recover `verifyToken`.
1379
+ */
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
+ }>;
1389
+ /**
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.
1393
+ */
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;
1409
+ }
1410
+
1411
+ /**
1412
+ * Deterministic ZK identity derivation from `(username, password)`.
1413
+ *
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.
1418
+ *
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)
1425
+ *
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.
1434
+ */
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;
1444
+ }
1445
+ /**
1446
+ * Derive a deterministic ZK identity from `(username, password)`.
1447
+ *
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.
1451
+ */
1452
+ declare function deriveIdentity(username: string, password: string): Promise<ZkIdentity>;
1453
+ /**
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.
1461
+ */
1462
+ declare function deriveMasterSeedFromPassword(username: string, password: string): Promise<Uint8Array>;
1463
+ /**
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.
1468
+ */
1469
+ declare function deriveIdentityFromSeed(seed: Uint8Array): Promise<ZkIdentity>;
1470
+
1471
+ /**
1472
+ * Poseidon hash wrapper used by the ZK auth flow.
1473
+ *
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.
1477
+ *
1478
+ * Returns hashes as decimal-string BigInts — same format snarkjs and the
1479
+ * `preimagePoK` circuit expect for inputs.
1480
+ *
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.
1484
+ */
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.
1549
+ *
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.
1553
+ */
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);
1562
+ }
1563
+
1564
+ /**
1565
+ * Groth16 proof generation for the `preimagePoK` circuit.
1566
+ *
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.
1570
+ *
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.
1597
+ *
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.
1604
+ *
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.
1610
+ */
1611
+ declare function toField(value: string | bigint | number): string;
1612
+ /**
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.
1622
+ *
1623
+ * Throws if circuit URLs are unreachable or snarkjs decides the inputs are
1624
+ * malformed.
1625
+ */
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
+ }>;
1640
+
1641
+ /**
1642
+ * Conversions between SubtleCrypto `CryptoKey`s and the wire formats the
1643
+ * accelerator's auth endpoints + `preimagePoK` circuit expect.
1644
+ *
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.
1652
+ *
1653
+ * Both go through `crypto.subtle.exportKey("raw", ...)` which yields the
1654
+ * uncompressed SEC1 form (`0x04 || X || Y`).
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>;
1662
+
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;
1710
+ }
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;
1730
+ /**
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.
1734
+ */
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>;
1368
1917
  }
1369
1918
  /**
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.
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.
1373
1922
  */
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;
1384
- }
1385
- /** Distil a `FileManifest` into a `FileStat`. */
1386
- declare function manifestToStat(manifest: FileManifest): FileStat;
1387
-
1923
+ declare function configureDeviceStore(opts?: {
1924
+ keys?: DeviceKeyVault | null;
1925
+ blobs?: DeviceBlobStore | null;
1926
+ keystore?: MuhkooKeystoreBridge | null;
1927
+ }): void;
1388
1928
  /**
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
1400
- *
1401
- * GET /api/shards/:hash 200 with shard bytes (application/octet-stream)
1402
- * 404 if absent
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.
1403
1940
  *
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.
1941
+ * The private half is non-extractable and never leaves IndexedDB as bytes.
1407
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>;
1408
1948
  /**
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).
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.
1413
1952
  */
1414
- interface ShardByteCache {
1415
- get(hash: string): Promise<Uint8Array | null>;
1416
- put(hash: string, bytes: Uint8Array): Promise<void>;
1417
- }
1953
+ declare function persistDeviceIdentity(v: PersistedDeviceIdentity): Promise<void>;
1418
1954
  /**
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).
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.
1423
1959
  */
1424
- interface PeerBlockSource {
1425
- getBlock(hash: string, opts?: {
1426
- timeoutMs?: number;
1427
- }): Promise<Uint8Array | null>;
1428
- announce(hash: string): void;
1429
- }
1430
- interface ShardClientOptions {
1431
- /** Base URL of the accelerator (e.g. `https://muhkoo-accelerator.workers.dev`). */
1432
- baseUrl: string;
1433
- /**
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)}`.
1442
- */
1443
- pathPrefix?: string;
1444
- /**
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.
1483
- */
1484
- backgroundUpload?: boolean;
1485
- }
1960
+ declare function loadDeviceIdentity(): Promise<PersistedDeviceIdentity | null>;
1486
1961
  /**
1487
- * Compute the lowercase hex SHA-256 of `bytes`. Available via WebCrypto in
1488
- * every runtime this SDK targets.
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).
1489
1965
  */
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;
1507
- /**
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.
1512
- */
1513
- putShard(hash: string, bytes: Uint8Array): Promise<{
1514
- dedup: boolean;
1515
- }>;
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
- /**
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".
1529
- */
1530
- getShard(hash: string): Promise<Uint8Array | null>;
1531
- /**
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.
1549
- */
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;
1556
- /**
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.
1560
- */
1561
- hasShard(hash: string): Promise<boolean>;
1562
- }
1966
+ declare function clearDeviceIdentity(): Promise<void>;
1563
1967
 
1564
1968
  /**
1565
- * HTTP client for the gated file-manifest store on a SharedSpace
1566
- * (`/api/spaces/:spaceId/files`).
1567
- *
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.
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?
1572
2001
  *
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.
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.
1578
2007
  *
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 }
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.
1584
2012
  */
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;
1595
- }
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
-
2013
+ declare function rpIdUsableForOrigin(rpId: string | undefined | null, host?: string): boolean;
1612
2014
  /**
1613
- * `FileStorage` — top-level entry point for chunked, encrypted, erasure-coded
1614
- * file storage.
2015
+ * Would {@link AuthNamespace.loginWithPasskey} accept a factor recorded with
2016
+ * this `rpId`, from `host`?
1615
2017
  *
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.
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.
1627
2022
  *
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.
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.
1635
2026
  *
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.
2027
+ * The `?? host` fallback applies to nullish only, matching `loginWithPasskey`:
2028
+ * an empty-string rpId stays unusable rather than resolving to the host.
1639
2029
  */
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;
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);
1678
2036
  }
1679
- /**
1680
- * Input to {@link FileStorage.writeFile} (which writes the manifest to a
1681
- * `SharedSpaceClient`). Carries the destination `spaceId`.
1682
- */
1683
- interface WriteFileInput extends WriteToShardsInput {
1684
- /** Space to write the manifest to — must allow the caller `write`. */
1685
- spaceId: string;
2037
+ interface PasskeyEnrollment {
2038
+ credentialId: Uint8Array;
2039
+ prfSalt: Uint8Array;
2040
+ prfOutput: Uint8Array;
1686
2041
  }
1687
2042
  /**
1688
- * Input to {@link FileStorage.writeFileToShards} (which doesn't write a
1689
- * manifest anywhere — that's the caller's job).
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.
1690
2045
  */
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
- }
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>;
1776
2053
 
1777
2054
  /**
1778
- * Reed–Solomon erasure-coding codec.
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.
1779
2067
  *
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.
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).
1783
2073
  *
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);
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}.
1791
2081
  *
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).
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)`.
1796
2085
  */
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 {
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;
1810
2099
  /**
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.
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`.
1817
2105
  */
1818
- ready(wasmModule?: WebAssembly.Module): Promise<void>;
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[]>;
1819
2130
  /**
1820
- * Split `data` into `dataShards` equal-size chunks (zero-padded to fit),
1821
- * compute `parityShards` parity shards, and return all of them.
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.
1822
2134
  */
1823
- encode(data: Uint8Array, dataShards: number, parityShards: number): EncodeResult;
2135
+ private proveFreshChallenge;
2136
+ /** Hit `POST /challenge` and return the issued nonce + ID. */
2137
+ private requestChallenge;
1824
2138
  /**
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`.
1829
- *
1830
- * `originalSize` is required so the trailing zero-padding can be trimmed.
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.
1831
2142
  */
1832
- decode(shards: Uint8Array[], parityShards: number, deadShardIndexes: number[], originalSize: number): Uint8Array;
2143
+ private fetch;
1833
2144
  }
1834
2145
 
1835
2146
  /**
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`).
1838
- *
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.
1849
- *
1850
- * Only the two public functions consumers actually need (`encode` and
1851
- * `reconstruct`) are exported. The rest is the wbindgen heap-table dance.
1852
- *
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.
2147
+ * Wire types for the storage layer.
1859
2148
  *
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.
1864
- */
1865
- /**
1866
- * Load and instantiate the WASM module. Idempotent — repeated calls await the
1867
- * same one-shot promise.
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.
1868
2155
  *
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.).
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.
1876
2160
  */
1877
- declare function initRsWasm(wasmModule?: WebAssembly.Module): Promise<void>;
1878
2161
 
1879
2162
  /**
1880
- * HTTP client for the accelerator's `/api/auth/*` endpoints.
1881
- *
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.
2163
+ * One chunk's metadata. A file is the concatenation of `chunks` in order.
1886
2164
  *
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).
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.
1890
2172
  */
1891
-
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;
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;
1926
2192
  }
1927
- interface AuthSuccess {
1928
- token: string;
1929
- username: string;
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;
1930
2216
  }
1931
- /** A vault factor record (M1.0 + M2 gated types). Wrap/iv are base64 AES-GCM; absent for phrase. */
1932
- interface VaultFactor {
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 {
1933
2223
  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;
2224
+ name: string;
2225
+ size: number;
2226
+ type: string;
2227
+ path?: string;
2228
+ lastModified: number;
2229
+ createdAt: number;
2230
+ chunkCount: number;
2231
+ shardCount: number;
1946
2232
  }
1947
- interface AuthClientOptions {
1948
- /** Absolute URL of the accelerator worker (no trailing slash required). */
2233
+ /** Distil a `FileManifest` into a `FileStat`. */
2234
+ declare function manifestToStat(manifest: FileManifest): FileStat;
2235
+
2236
+ /**
2237
+ * HTTP client for the open content-addressed shard store.
2238
+ *
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.
2243
+ *
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
2248
+ *
2249
+ * GET /api/shards/:hash 200 with shard bytes (application/octet-stream)
2250
+ * 404 if absent
2251
+ *
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.
2255
+ */
2256
+ /**
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).
2261
+ */
2262
+ interface ShardByteCache {
2263
+ get(hash: string): Promise<Uint8Array | null>;
2264
+ put(hash: string, bytes: Uint8Array): Promise<void>;
2265
+ }
2266
+ /**
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).
2271
+ */
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`). */
1949
2280
  baseUrl: string;
1950
- /** Optional custom fetch — defaults to `globalThis.fetch`. */
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
+ */
1951
2296
  fetch?: typeof fetch;
1952
- }
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>;
1963
2297
  /**
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.
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}.
1967
2301
  */
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
- }>;
2302
+ cache?: ShardByteCache;
1975
2303
  /**
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.
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.
1979
2308
  */
1980
- oprfEvaluateGated(username: string, blinded: string, factorType: "email" | "google", purpose: "enroll" | "recover", verifyToken: string): Promise<{
1981
- evaluated: string;
1982
- evaluated2: string;
1983
- }>;
2309
+ batchShards?: boolean;
1984
2310
  /**
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).
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.
1988
2314
  */
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
- }>;
2315
+ deferUpload?: (hash: string) => Promise<void>;
2003
2316
  /**
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`.
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.
2007
2320
  */
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;
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
+ }
2334
+ /**
2335
+ * Compute the lowercase hex SHA-256 of `bytes`. Available via WebCrypto in
2336
+ * every runtime this SDK targets.
2337
+ */
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;
2037
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;
2038
2374
  /**
2039
- * Read a factor record for `username` (returns an indistinguishable decoy if
2040
- * absent). Gated types (email) require the recover `verifyToken`.
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.
2041
2397
  */
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
- }>;
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;
2051
2404
  /**
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.
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.
2055
2408
  */
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;
2409
+ hasShard(hash: string): Promise<boolean>;
2071
2410
  }
2072
2411
 
2073
2412
  /**
2074
- * Deterministic ZK identity derivation from `(username, password)`.
2075
- *
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.
2413
+ * HTTP client for the gated file-manifest store on a SharedSpace
2414
+ * (`/api/spaces/:spaceId/files`).
2080
2415
  *
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)
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.
2087
2420
  *
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`.
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.
2092
2426
  *
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.
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 }
2096
2432
  */
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;
2433
+
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;
2106
2458
  }
2107
- /**
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.
2113
- */
2114
- declare function deriveIdentity(username: string, password: string): Promise<ZkIdentity>;
2115
- /**
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.
2123
- */
2124
- declare function deriveMasterSeedFromPassword(username: string, password: string): Promise<Uint8Array>;
2125
- /**
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.
2130
- */
2131
- declare function deriveIdentityFromSeed(seed: Uint8Array): Promise<ZkIdentity>;
2132
2459
 
2133
2460
  /**
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.
2461
+ * `FileStorage` — top-level entry point for chunked, encrypted, erasure-coded
2462
+ * file storage.
2142
2463
  *
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.
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.
2156
2475
  *
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.
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.
2160
2483
  *
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`.
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.
2166
2487
  */
2167
2488
 
2168
- interface CircuitUrls {
2169
- /** preimagePoK.wasm — the circuit's witness generator. */
2170
- wasmUrl: string;
2171
- /** preimagePoK_0001.zkey — the proving key. */
2172
- zkeyUrl: string;
2173
- }
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>;
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;
2514
+ /**
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.
2518
+ */
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;
2526
+ }
2207
2527
  /**
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.
2528
+ * Input to {@link FileStorage.writeFile} (which writes the manifest to a
2529
+ * `SharedSpaceClient`). Carries the destination `spaceId`.
2215
2530
  */
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
-
2531
+ interface WriteFileInput extends WriteToShardsInput {
2532
+ /** Space to write the manifest to — must allow the caller `write`. */
2533
+ spaceId: string;
2534
+ }
2231
2535
  /**
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`).
2536
+ * Input to {@link FileStorage.writeFileToShards} (which doesn't write a
2537
+ * manifest anywhere — that's the caller's job).
2245
2538
  */
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>;
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
+ }
2252
2624
 
2253
2625
  /**
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)
2626
+ * Reed–Solomon erasure-coding codec.
2273
2627
  *
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:
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.
2279
2631
  *
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}).
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);
2288
2639
  *
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).
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).
2294
2644
  */
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;
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;
2300
2656
  }
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.
2312
- */
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;
2657
+ declare class ReedSolomonCodec {
2320
2658
  /**
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.
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.
2324
2665
  */
2325
- privateKey: CryptoKey;
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;
2326
2681
  }
2682
+
2327
2683
  /**
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.
2332
- */
2333
- declare function generateDevicePairingKeypair(): Promise<DevicePairingKeypair>;
2334
- /**
2335
- * (Hosted-page side) Seal the master seed *to a device's* pairing public key.
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`).
2336
2686
  *
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.
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:
2341
2691
  *
2342
- * Returns the v2 `sealedKeys` string, which slots into exactly the same server
2343
- * field as v1: relayed verbatim, never read.
2344
- */
2345
- declare function sealSeedToDevice(seed: Uint8Array, devicePublicKeyB64: string): Promise<string>;
2346
- /**
2347
- * (Device side) Recover the master seed from a v2 envelope using the ephemeral
2348
- * private key from {@link generateDevicePairingKeypair}.
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.
2349
2697
  *
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.
2698
+ * Only the two public functions consumers actually need (`encode` and
2699
+ * `reconstruct`) are exported. The rest is the wbindgen heap-table dance.
2353
2700
  *
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.
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.
2707
+ *
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.
2358
2712
  */
2359
- declare function unsealSeedFromDevice(sealedKeys: string, privateKey: CryptoKey, devicePublicKeyB64: string): Promise<Uint8Array>;
2360
2713
  /**
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.
2364
- *
2365
- * ## What this is worth (be honest about it)
2366
- *
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.
2373
- *
2374
- * Two attacks, and what each actually costs:
2375
- *
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.
2389
- *
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.
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
@@ -6317,205 +6635,56 @@ declare class Client {
6317
6635
  /** Per-user key/value storage — `client.kv.set(...)`, etc. */
6318
6636
  readonly kv: KvNamespace;
6319
6637
  /** 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>;
6638
+ readonly db: DbNamespace;
6639
+ /** File storage — `client.storage.writeFile(...)`, etc. */
6640
+ readonly storage: StorageNamespace;
6641
+ /** Filesystem over the personal space: directories, versions, real paths. */
6642
+ readonly vfs: VfsNamespace;
6643
+ /** Version control over the VFS: commits, branches, merges — per project. */
6644
+ readonly vcs: VcsNamespace;
6645
+ /** Realtime messaging — `client.message.subscribe(...)`, etc. */
6646
+ readonly message: MessageNamespace;
6647
+ /** Fan-out group spaces — `client.space.createSpace(...)`, etc. */
6648
+ readonly space: SpaceNamespace;
6649
+ /** Programmable Agents — `client.agents.create(appId, …)`, etc. */
6650
+ readonly agents: AgentsNamespace;
6651
+ /** Serverless functions — `client.functions.deploy(appId, …)`, etc. */
6652
+ readonly functions: FunctionsNamespace;
6653
+ /** Access tokens — `client.accessTokens.create(appId, …)`, etc. */
6654
+ readonly accessTokens: AccessTokensNamespace;
6655
+ /** Offline cache + sync — `client.offline.status`, `client.offline.snapshot`, etc. */
6656
+ readonly offline: OfflineManager;
6657
+ private readonly session;
6658
+ private readonly http;
6659
+ /** Subscribers notified when a session expires and can't be recovered. */
6660
+ private readonly sessionExpiredHandlers;
6661
+ /** Shared in-flight recovery so a burst of 401s triggers one re-auth. */
6662
+ private recoverInFlight;
6663
+ constructor(options?: ClientOptions);
6664
+ /** The currently signed-in user, or `null`. */
6665
+ get user(): AuthUser | null;
6666
+ /** Whether a token-bearing session is active. */
6667
+ get isAuthenticated(): boolean;
6668
+ /**
6669
+ * Subscribe to session expiry that couldn't be silently recovered (the
6670
+ * user's identity wasn't in memory to re-prove — typically after a reload
6671
+ * where only the token was persisted, and that token has since gone stale).
6672
+ * This is the signal to send the user back to the login screen.
6673
+ *
6674
+ * Returns an unsubscribe function.
6675
+ *
6676
+ * const off = client.onSessionExpired(() => router.push('/login'));
6677
+ */
6678
+ onSessionExpired(handler: () => void): () => void;
6679
+ /**
6680
+ * Attempt a silent re-auth of the current user. Concurrent callers share a
6681
+ * single in-flight attempt (a stale token usually 401s several requests at
6682
+ * once — we re-auth once, not once per request). On failure the
6683
+ * session-expired event fires so the app can react. Exposed so apps can
6684
+ * also drive recovery proactively (e.g. on `visibilitychange`).
6685
+ */
6686
+ recoverSession(): Promise<boolean>;
6469
6687
  }
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
6688
 
6520
6689
  /**
6521
6690
  * Three-way line merge.
@@ -6558,92 +6727,6 @@ declare function merge3Text(base: string, ours: string, theirs: string, labels?:
6558
6727
  conflicted: boolean;
6559
6728
  };
6560
6729
 
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
6730
  /**
6648
6731
  * Muhkoo app decorators — declare your app's agent-facing surface in code, then
6649
6732
  * "eject" a system prompt you hand to a Programmable Agent.
@@ -7548,5 +7631,271 @@ declare class KeyringClient implements KeyringTransport {
7548
7631
  fetchMetadata(): Promise<SpaceMetadata | null>;
7549
7632
  }
7550
7633
 
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 };
7634
+ declare class Logger {
7635
+ private prefix;
7636
+ private logLevel;
7637
+ private console;
7638
+ private showCaller;
7639
+ level: string;
7640
+ constructor(prefix?: string, LOGLEVEL?: string, showCaller?: boolean);
7641
+ private shouldLog;
7642
+ /**
7643
+ * Extract caller information from stack trace
7644
+ */
7645
+ private getCallerInfo;
7646
+ /**
7647
+ * Format caller info for display
7648
+ */
7649
+ private formatCallerInfo;
7650
+ setLevel(level: string): void;
7651
+ setShowCaller(show: boolean): void;
7652
+ info(...args: any[]): void;
7653
+ log(...args: any[]): void;
7654
+ verbose(...args: any[]): void;
7655
+ success(...args: any[]): void;
7656
+ warn(...args: any[]): void;
7657
+ error(...args: any[]): void;
7658
+ debug(...args: any[]): void;
7659
+ assert(condition?: boolean, ...data: any[]): void;
7660
+ clear(): void;
7661
+ count(label?: string): void;
7662
+ countReset(label?: string): void;
7663
+ dir(item?: any, options?: any): void;
7664
+ dirxml(...data: any[]): void;
7665
+ group(...data: any[]): void;
7666
+ groupCollapsed(...data: any[]): void;
7667
+ groupEnd(): void;
7668
+ table(tabularData?: any, properties?: string[]): void;
7669
+ time(label?: string): void;
7670
+ timeEnd(label?: string): void;
7671
+ timeLog(label?: string, ...data: any[]): void;
7672
+ trace(...data: any[]): void;
7673
+ profile(label?: string): void;
7674
+ profileEnd(label?: string): void;
7675
+ timeStamp(label?: string): void;
7676
+ }
7677
+
7678
+ declare const log: Logger;
7679
+
7680
+ declare global {
7681
+ interface Console {
7682
+ success(...args: any[]): void;
7683
+ verbose(...args: any[]): void;
7684
+ }
7685
+ }
7686
+
7687
+ /**
7688
+ * Byte conversion helpers usable in both Node and the browser.
7689
+ *
7690
+ * Replaces Node's `Buffer` API for the small set of operations we actually
7691
+ * need in the crypto layer: hex/base64/base64url encode + decode, and array
7692
+ * concatenation. Lets `DoubleRatchet`, `KeyStore`, etc. compile against
7693
+ * platforms without `Buffer` (browsers, CF Workers).
7694
+ */
7695
+ /** Encode a byte array as a lowercase hex string. */
7696
+ declare function toHex(bytes: Uint8Array): string;
7697
+ /** Decode a hex string into a byte array. Accepts upper/lowercase; rejects odd-length input. */
7698
+ declare function fromHex(hex: string): Uint8Array<ArrayBuffer>;
7699
+ /** Encode a byte array as a standard base64 string. */
7700
+ declare function toBase64(bytes: Uint8Array): string;
7701
+ /** Decode a standard base64 string to bytes. */
7702
+ declare function fromBase64(b64: string): Uint8Array<ArrayBuffer>;
7703
+ /** Encode a byte array as a base64url string (no padding). */
7704
+ declare function toBase64Url(bytes: Uint8Array): string;
7705
+ /** Decode a base64url string (with or without padding) to bytes. */
7706
+ declare function fromBase64Url(b64url: string): Uint8Array<ArrayBuffer>;
7707
+ /** Concatenate any number of byte arrays into a single Uint8Array. */
7708
+ declare function concatBytes(...arrays: (Uint8Array | ArrayBuffer)[]): Uint8Array<ArrayBuffer>;
7709
+ /** Encode a UTF-8 string as bytes. Thin wrapper around TextEncoder for symmetry. */
7710
+ declare function utf8Encode(s: string): Uint8Array<ArrayBuffer>;
7711
+ /** Decode UTF-8 bytes to a string. */
7712
+ declare function utf8Decode(bytes: Uint8Array): string;
7713
+
7714
+ /**
7715
+ * @public
7716
+ * @remarks Asserts the basic type of a variable
7717
+ * @param value - The value to check
7718
+ * @param type - The expected type
7719
+ *
7720
+ * @throws Error - If the type of the value does not match the expected type
7721
+ *
7722
+ */
7723
+ declare function assertType<T>(value: unknown, typeOrConstructors: string | (new (...args: any[]) => any) | (new (...args: any[]) => any)[]): asserts value is T;
7724
+ /**
7725
+ * @public
7726
+ * @remarks Encodes a string to Base58
7727
+ * @param input - the data to encode, must be a cloneable object
7728
+ * @returns string
7729
+ */
7730
+ declare function base58Encode(input: ArrayBuffer | string): string;
7731
+ /**
7732
+ * @public
7733
+ * @remarks Decodes a Base58 string to a Uint8Array
7734
+ * @param input - the Base58 string to decode
7735
+ * @returns Uint8Array
7736
+ */
7737
+ declare function base58Decode(input: string): ArrayBuffer;
7738
+ /**
7739
+ * @public
7740
+ * @remarks Generates a checksum for a given input
7741
+ * @param input - The input to generate a checksum for as a string
7742
+ * @returns string
7743
+ */
7744
+ declare function generateChecksum(input: string): string;
7745
+ /**
7746
+ * @public
7747
+ * @remarks Verifies a checksum for a given input
7748
+ * @param input - The input to verify as a string
7749
+ * @param checksum - The checksum to verify as a string
7750
+ * @returns boolean
7751
+ */
7752
+ declare function verifyChecksum(input: string, checksum: string): boolean;
7753
+ /**
7754
+ * @internal
7755
+ * @remarks Generates a random UUID for a given packet
7756
+ * @returns string
7757
+ */
7758
+ declare function _packetId(): string;
7759
+ /**
7760
+ * @internal
7761
+ * @remarks Generates a random UUID for a given object
7762
+ * @returns string
7763
+ *
7764
+ */
7765
+ declare function _objectId(): string;
7766
+ /**
7767
+ * @internal
7768
+ * @remarks Generates a random UUID for a given user
7769
+ * @returns string
7770
+ *
7771
+ */
7772
+ declare function _userId(): string;
7773
+ /**
7774
+ * @internal
7775
+ * @remarks Generates a random UUID for a given account
7776
+ * @returns string
7777
+ *
7778
+ */
7779
+ declare function _accountId(): string;
7780
+ /**
7781
+ * @internal
7782
+ * @remarks Generates a random UUID for a socket
7783
+ * @returns string
7784
+ *
7785
+ */
7786
+ declare function _socketId(): string;
7787
+ /**
7788
+ * @internal
7789
+ * @remarks Generates a random UUID for a message
7790
+ * @returns string
7791
+ */
7792
+ declare function _messageId(): string;
7793
+ /**
7794
+ * @public
7795
+ * @remarks Generates a random UUID and base58 encodes it
7796
+ * @returns string
7797
+ */
7798
+ declare function generateId(): string;
7799
+ /**
7800
+ * @public
7801
+ * @remarks Serializes data to a base58 encoded string
7802
+ * @param data - The data to serialize
7803
+ * @typeParam T - The type of the data to serialize
7804
+ * @returns base58 encoded string
7805
+ */
7806
+ declare function serialize<T>(data: T): string;
7807
+ /**
7808
+ * @public
7809
+ * @remarks Deserializes a base58 encoded string to data
7810
+ * @param base58String - The base58 encoded string to deserialize
7811
+ * @returns Deserialized data
7812
+ */
7813
+ declare function deserialize<T>(base58String: string): T;
7814
+ /**
7815
+ * @public
7816
+ * @remarks Retries a method a specified number of times with a delay between each attempt, this is a decorator
7817
+ * @param retries - The number of times to retry the method
7818
+ * @param delay - The delay in milliseconds between each attempt
7819
+ */
7820
+ declare function Retry(retries?: number, delay?: number): MethodDecorator;
7821
+ /**
7822
+ * @public
7823
+ * @remarks Waits for a class property to be ready before executing a method, this is a decorator.
7824
+ * @param readyPropertyName - The class property to check for readiness
7825
+ */
7826
+ declare function Ready(readyPropertyName?: string): MethodDecorator | PropertyDecorator;
7827
+ /**
7828
+ * Converts bytes into a more human-friendly format (e.g., KB, MB, GB).
7829
+ * @internal
7830
+ * @param bytes - The number of bytes to convert.
7831
+ * @param decimals - The number of decimals to display (default is 2).
7832
+ * @returns A formatted string representing the size in appropriate units.
7833
+ */
7834
+ declare function _formatBytes(bytes: number, decimals?: number): string;
7835
+ /**
7836
+ * Retrieves the IP address(es) for a given hostname using Cloudflare's DNS-over-HTTPS service.
7837
+ *
7838
+ * This function queries the Cloudflare DNS API to resolve the provided hostname to its corresponding
7839
+ * IPv4 address(es) (A records). It returns an array of IP addresses or logs an error if the resolution fails.
7840
+ *
7841
+ * @param hostname - The hostname to resolve (e.g., "example.com").
7842
+ * @returns A promise that resolves to an array of IP addresses (strings) associated with the hostname.
7843
+ * If no IP addresses are found or an error occurs, an empty array or undefined may be returned.
7844
+ * @throws Error If the DNS query fails due to a network error or invalid response from the server.
7845
+ * @public
7846
+ * @example
7847
+ * ```typescript
7848
+ * import { log } from '@muhkoo/connect/utilities';
7849
+ * getIPAddress("example.com").then(ips => log.debug(ips));
7850
+ * // Output: ["93.184.216.34"]
7851
+ * ```
7852
+ */
7853
+ declare function getIPAddress(hostname: string): Promise<any>;
7854
+ declare function isNumber(value: string | number): boolean;
7855
+ declare function getId(row: {
7856
+ insertId?: number;
7857
+ id?: number;
7858
+ }): number;
7859
+
7860
+ /**
7861
+ * SDK build version — stamped to the console when a {@link Client} is created.
7862
+ *
7863
+ * During active development, bump the `-alpha.N` suffix whenever we ship a build
7864
+ * so we can confirm in the browser console that the page is running the build we
7865
+ * expect (and isn't serving a stale cached bundle). Keep this in sync with
7866
+ * `package.json` `version`.
7867
+ */
7868
+ declare const VERSION = "0.14.0-alpha.8";
7869
+
7870
+ /**
7871
+ * The SDK's process-global logger.
7872
+ *
7873
+ * Eight modules under `src/` read a bare `appLogger` off `globalThis` rather
7874
+ * than importing one (`src/crypto/{KeyStore,DoubleRatchet,DoubleRatchetManager,
7875
+ * Authenticator}.ts`, `src/core/Client.ts`,
7876
+ * `src/core/namespaces/HostedAuth.ts`, `src/events/EventCore.ts`), so the
7877
+ * binding has to exist before any of them runs, and the `declare global` below
7878
+ * is what makes that typecheck.
7879
+ *
7880
+ * This module is the single place that bootstrap happens. It previously lived
7881
+ * inline in three entry files — `src/browser/index.ts`, `src/workers/index.ts`,
7882
+ * and `src/core/index.ts` (which `src/server/index.ts` picked up as an import
7883
+ * side effect) — three copies that could drift.
7884
+ *
7885
+ * `??=` makes it idempotent, so importing it from several entries in one
7886
+ * process is safe and the first writer wins.
7887
+ *
7888
+ * KNOWN HAZARD: package.json declares `"sideEffects": false`, which entitles a
7889
+ * tree-shaker to drop a module imported purely for effect. The entries import
7890
+ * this one for its `export`s as well, so it survives today — but the durable
7891
+ * fix is to convert those eight bare reads into real imports of `appLogger`
7892
+ * from here, which removes the reliance on a global entirely.
7893
+ */
7894
+
7895
+ declare global {
7896
+ var appLogger: InstanceType<typeof Logger>;
7897
+ }
7898
+ declare const appLogger: InstanceType<typeof Logger>;
7899
+
7900
+ 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, 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 };
7901
+ 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 };