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