@muhkoo/connect 0.14.0-alpha.6 → 0.14.0-alpha.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -8
- package/dist/browser/index.js +5066 -4617
- package/dist/browser/index.js.map +1 -1
- package/dist/connect.d.ts +1830 -1481
- package/dist/connect.workers.d.ts +1188 -0
- package/dist/server/index.js +8019 -7771
- 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
|
}
|
|
1132
|
+
declare function encodeToHex(field: Field): string;
|
|
1133
|
+
declare function decodeFromHex(hexStr: string): Field;
|
|
1134
|
+
interface CircuitBufferConfig {
|
|
1135
|
+
wasm: Uint8Array;
|
|
1136
|
+
zkey: Uint8Array;
|
|
1137
|
+
verificationKey: VerificationKey;
|
|
1138
|
+
}
|
|
1139
|
+
declare class HashKnowledge {
|
|
1140
|
+
private static wasmBuffer?;
|
|
1141
|
+
private static zkeyBuffer?;
|
|
1142
|
+
private static verificationKey?;
|
|
1143
|
+
static setCircuitBuffers(wasm: Uint8Array, zkey: Uint8Array, verificationKey: VerificationKey): void;
|
|
1144
|
+
static compile(): Promise<ZkCompiled>;
|
|
1145
|
+
static prove(secret: Field): Promise<{
|
|
1146
|
+
proof: SnarkProof;
|
|
1147
|
+
publicSignals: string[];
|
|
1148
|
+
}>;
|
|
1149
|
+
static verify(proof: SnarkProof, publicSignals?: string[]): Promise<boolean>;
|
|
1150
|
+
}
|
|
1151
|
+
declare class PreimagePoK {
|
|
1152
|
+
private static wasmBuffer?;
|
|
1153
|
+
private static zkeyBuffer?;
|
|
1154
|
+
private static verificationKey?;
|
|
1155
|
+
static setCircuitBuffers(wasm: Uint8Array, zkey: Uint8Array, verificationKey: VerificationKey): void;
|
|
1156
|
+
static compile(): Promise<ZkCompiled>;
|
|
1157
|
+
static prove(publicInput: AuthPublicInput, secret: Field, salt: Field, ecdsaPub: Field): Promise<{
|
|
1158
|
+
proof: SnarkProof;
|
|
1159
|
+
publicSignals: string[];
|
|
1160
|
+
}>;
|
|
1161
|
+
static verify(proof: SnarkProof, publicSignals?: string[]): Promise<boolean>;
|
|
1162
|
+
}
|
|
1163
|
+
declare function verifyHashKnowledge(proof: SnarkProof, expectedOutput?: Field, publicSignals?: string[]): Promise<boolean>;
|
|
1164
|
+
declare function verifyPreimagePoK(proof: SnarkProof, publicSignals?: string[]): Promise<boolean>;
|
|
1165
|
+
declare function verify(programType: 'HashKnowledge' | 'PreimagePoK', proof: SnarkProof, options?: {
|
|
1166
|
+
expectedOutput?: Field;
|
|
1167
|
+
publicSignals?: string[];
|
|
1168
|
+
}): Promise<boolean>;
|
|
1169
|
+
declare function quickVerify(program: typeof HashKnowledge | typeof PreimagePoK, proof: SnarkProof, publicSignals?: string[]): Promise<boolean>;
|
|
1170
|
+
declare function initializeCircuits(config: {
|
|
1171
|
+
hashKnowledge?: CircuitBufferConfig;
|
|
1172
|
+
preimagePoK?: CircuitBufferConfig;
|
|
1173
|
+
}): void;
|
|
1174
|
+
declare function compilePrograms(): Promise<{
|
|
1175
|
+
hashKnowledge: ZkCompiled;
|
|
1176
|
+
preimagePoK: ZkCompiled;
|
|
1177
|
+
}>;
|
|
1178
|
+
|
|
1179
|
+
interface AuthToken {
|
|
1180
|
+
peerId: string;
|
|
1181
|
+
timestamp: number;
|
|
1182
|
+
signature: string;
|
|
1183
|
+
}
|
|
1184
|
+
declare class Authenticator {
|
|
1185
|
+
private trustedKeys;
|
|
1186
|
+
private zkVerificationKey;
|
|
1187
|
+
initializeZK(): Promise<void>;
|
|
1188
|
+
getZKVerificationKey(): VerificationKey | null;
|
|
1189
|
+
addTrustedServer(serverId: string, publicKey: CryptoKey): void;
|
|
1190
|
+
generateAuthToken(peerId: string, privateKey: CryptoKey): Promise<AuthToken>;
|
|
1191
|
+
verifyAuthToken(token: AuthToken, clientPublicKey: CryptoKey): Promise<boolean>;
|
|
1192
|
+
verifyZKProof(proof: any, publicInput: AuthPublicInput, storedCommitment: Field, ecdsaPub: CryptoKey): Promise<boolean>;
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
declare class DoubleRatchetManager {
|
|
1196
|
+
private sessions;
|
|
1197
|
+
private sessionStates;
|
|
1198
|
+
private keyStore;
|
|
1199
|
+
private _authenticator;
|
|
1200
|
+
private registeredUsers;
|
|
1201
|
+
private id;
|
|
1202
|
+
constructor(id: string);
|
|
1203
|
+
get authenticator(): Authenticator;
|
|
1204
|
+
addTrustedServer(serverId: string, publicKey: CryptoKey): Promise<void>;
|
|
1205
|
+
registerZK(clientId: string, secret: Field, salt: Field, ecdsaPub: CryptoKey): Promise<void>;
|
|
1206
|
+
performHandshake(senderId: string, recipientId: string, zkProof: any, publicInput: AuthPublicInput, clientPublicKey: CryptoKey, // ECDH pub for session
|
|
1207
|
+
clientAuthPublicKey: CryptoKey, // ECDSA pub for auth
|
|
1208
|
+
authToken: AuthToken): Promise<void>;
|
|
1209
|
+
initializeSession(senderId: string, recipientId: string, isClient?: boolean, sessionType?: 'global' | 'specific', sessionId?: string, zkProof?: any, publicInput?: AuthPublicInput, clientPublicKey?: CryptoKey, clientAuthPublicKey?: CryptoKey, clientEcdhPrivateKey?: CryptoKey, clientAuthPrivateKey?: CryptoKey): Promise<string>;
|
|
1210
|
+
encrypt(senderId: string, recipientId: string, sessionId: string, plaintext: string, newDhKey?: boolean, sessionType?: 'global' | 'specific'): Promise<CipherMessage>;
|
|
1211
|
+
decrypt(message: CipherMessage, isClient?: boolean): Promise<string>;
|
|
1212
|
+
getSessionSharedSecret(sessionId: string): Promise<Uint8Array | null>;
|
|
1213
|
+
private saveState;
|
|
1214
|
+
private loadState;
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1345
1217
|
/**
|
|
1346
|
-
*
|
|
1347
|
-
*
|
|
1218
|
+
* HTTP client for the accelerator's `/api/auth/*` endpoints.
|
|
1219
|
+
*
|
|
1220
|
+
* Owns the wire shapes for register / challenge / authenticate / verify so
|
|
1221
|
+
* the React SPA (and any future consumers) don't reimplement them. The ZK
|
|
1222
|
+
* identity derivation + Groth16 proof generation stay on the application
|
|
1223
|
+
* side — this client handles only the round-trips.
|
|
1224
|
+
*
|
|
1225
|
+
* Mirrors `PersonalSpaceClient` / `ShardClient` / `SharedSpaceClient`: takes
|
|
1226
|
+
* a `baseUrl`, optional custom `fetch`, throws typed `Error`s on failure
|
|
1227
|
+
* (with the worker's message inlined where available).
|
|
1348
1228
|
*/
|
|
1349
|
-
|
|
1350
|
-
|
|
1229
|
+
|
|
1230
|
+
interface ZkRegisterRequest {
|
|
1231
|
+
username: string;
|
|
1232
|
+
/** Decimal-string Poseidon commitment. */
|
|
1233
|
+
commitment: string;
|
|
1234
|
+
/** Base64-encoded raw uncompressed ECDH public key (SEC1 0x04‖X‖Y). */
|
|
1235
|
+
ecdhPublicKey: string;
|
|
1236
|
+
/** Base64-encoded raw uncompressed ECDSA public key. */
|
|
1237
|
+
ecdsaPublicKey: string;
|
|
1238
|
+
email?: string | null;
|
|
1239
|
+
}
|
|
1240
|
+
interface ZkRegisterResponse {
|
|
1241
|
+
success: boolean;
|
|
1242
|
+
}
|
|
1243
|
+
interface ZkChallengeResponse {
|
|
1244
|
+
challengeId: string;
|
|
1245
|
+
/** Hex-encoded random nonce — caller reduces into a BN254 field element for the proof. */
|
|
1246
|
+
nonce: string;
|
|
1247
|
+
/** Decimal-string Poseidon commitment the server has on file for this user. */
|
|
1248
|
+
commitment: string;
|
|
1249
|
+
}
|
|
1250
|
+
interface ZkAuthenticateRequest {
|
|
1251
|
+
challengeId: string;
|
|
1252
|
+
proof: {
|
|
1253
|
+
commitment: string;
|
|
1254
|
+
/** Original hex nonce from the challenge (NOT the field-reduced version). */
|
|
1255
|
+
nonce: string;
|
|
1256
|
+
response: {
|
|
1257
|
+
proof: Groth16Proof;
|
|
1258
|
+
publicSignals: string[];
|
|
1259
|
+
};
|
|
1260
|
+
/** Base64 ECDSA-SHA256 signature over `JSON.stringify(response.proof)`. */
|
|
1261
|
+
signature: string;
|
|
1262
|
+
};
|
|
1263
|
+
rememberMe?: boolean;
|
|
1264
|
+
}
|
|
1265
|
+
interface AuthSuccess {
|
|
1266
|
+
token: string;
|
|
1267
|
+
username: string;
|
|
1268
|
+
}
|
|
1269
|
+
/** A vault factor record (M1.0 + M2 gated types). Wrap/iv are base64 AES-GCM; absent for phrase. */
|
|
1270
|
+
interface VaultFactor {
|
|
1351
1271
|
id: string;
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
/**
|
|
1359
|
-
|
|
1360
|
-
/**
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
/**
|
|
1367
|
-
|
|
1272
|
+
type: "password" | "passkey" | "phrase-marker" | "email" | "google" | "device";
|
|
1273
|
+
wrap?: string;
|
|
1274
|
+
iv?: string;
|
|
1275
|
+
params?: Record<string, unknown>;
|
|
1276
|
+
label?: string;
|
|
1277
|
+
createdAt?: number;
|
|
1278
|
+
/** Display hint for gated factors ("m•••@gmail.com") — list responses only. */
|
|
1279
|
+
masked?: string;
|
|
1280
|
+
/** Passkey's origin (RP id) — list responses only. A passkey only unlocks
|
|
1281
|
+
* from this host or a subdomain of it, so apps on several hosts enroll one
|
|
1282
|
+
* per origin. */
|
|
1283
|
+
rpId?: string;
|
|
1284
|
+
}
|
|
1285
|
+
interface AuthClientOptions {
|
|
1286
|
+
/** Absolute URL of the accelerator worker (no trailing slash required). */
|
|
1287
|
+
baseUrl: string;
|
|
1288
|
+
/** Optional custom fetch — defaults to `globalThis.fetch`. */
|
|
1289
|
+
fetch?: typeof fetch;
|
|
1290
|
+
}
|
|
1291
|
+
declare class AuthClient {
|
|
1292
|
+
private readonly baseUrl;
|
|
1293
|
+
private readonly fetchFn;
|
|
1294
|
+
constructor(opts: AuthClientOptions);
|
|
1295
|
+
/** Submit ZK registration. Caller has already derived the identity + commitment. */
|
|
1296
|
+
register(req: ZkRegisterRequest): Promise<ZkRegisterResponse>;
|
|
1297
|
+
/** Request an auth challenge for `username` (server issues a fresh nonce). */
|
|
1298
|
+
getChallenge(username: string): Promise<ZkChallengeResponse>;
|
|
1299
|
+
/** Trade a challenge + proof for a session token. */
|
|
1300
|
+
authenticate(req: ZkAuthenticateRequest): Promise<AuthSuccess>;
|
|
1301
|
+
/**
|
|
1302
|
+
* Validate a previously stored session token. Returns `{ username }` on
|
|
1303
|
+
* success; throws on 401/404 — caller uses that to drop a stale token
|
|
1304
|
+
* when restoring a session after a reload.
|
|
1305
|
+
*/
|
|
1306
|
+
verify(token: string): Promise<{
|
|
1307
|
+
username: string;
|
|
1308
|
+
}>;
|
|
1309
|
+
/** Blinded OPRF evaluation — the server-gated half of a password/recovery wrap key. */
|
|
1310
|
+
oprfEvaluate(username: string, blinded: string): Promise<{
|
|
1311
|
+
evaluated: string;
|
|
1312
|
+
}>;
|
|
1313
|
+
/**
|
|
1314
|
+
* Gated split-key OPRF evaluation (M2 — email/google factors). Requires a
|
|
1315
|
+
* live `verifyToken` from {@link emailOtpVerify}; returns BOTH trust
|
|
1316
|
+
* domains' evaluations. The recover token is consumed by this call.
|
|
1317
|
+
*/
|
|
1318
|
+
oprfEvaluateGated(username: string, blinded: string, factorType: "email" | "google", purpose: "enroll" | "recover", verifyToken: string): Promise<{
|
|
1319
|
+
evaluated: string;
|
|
1320
|
+
evaluated2: string;
|
|
1321
|
+
}>;
|
|
1322
|
+
/**
|
|
1323
|
+
* Request an email OTP. With a session `token` this is the ENROLL flow
|
|
1324
|
+
* (code sent to `email`); without one it's RECOVER (code sent to the
|
|
1325
|
+
* account's enrolled address — the response is uniform either way).
|
|
1326
|
+
*/
|
|
1327
|
+
emailOtpStart(opts: {
|
|
1328
|
+
token?: string;
|
|
1329
|
+
email?: string;
|
|
1330
|
+
username?: string;
|
|
1331
|
+
}): Promise<void>;
|
|
1332
|
+
/** Trade an emailed code for a single-use verifyToken (+ the proven address). */
|
|
1333
|
+
emailOtpVerify(opts: {
|
|
1334
|
+
code: string;
|
|
1335
|
+
token?: string;
|
|
1336
|
+
username?: string;
|
|
1337
|
+
}): Promise<{
|
|
1338
|
+
verifyToken: string;
|
|
1339
|
+
email: string;
|
|
1340
|
+
}>;
|
|
1341
|
+
/**
|
|
1342
|
+
* Verify a Google ID token (M2.3). With a session `token` it's the ENROLL
|
|
1343
|
+
* gate; without, it's RECOVER/LOGIN (the Google account must already be
|
|
1344
|
+
* linked). Returns a single-use verifyToken bound to the account's `sub`.
|
|
1345
|
+
*/
|
|
1346
|
+
googleVerify(opts: {
|
|
1347
|
+
idToken: string;
|
|
1348
|
+
token?: string;
|
|
1349
|
+
username?: string;
|
|
1350
|
+
}): Promise<{
|
|
1351
|
+
verifyToken: string;
|
|
1352
|
+
sub: string;
|
|
1353
|
+
emailHint: string | null;
|
|
1354
|
+
}>;
|
|
1355
|
+
/** Mint a single-use authorization code for an app (called by the hosted page; session-authed). */
|
|
1356
|
+
grant(token: string, body: {
|
|
1357
|
+
appId: string;
|
|
1358
|
+
redirectUri: string;
|
|
1359
|
+
codeChallenge: string;
|
|
1360
|
+
sealedKeys: string;
|
|
1361
|
+
}): Promise<{
|
|
1362
|
+
code: string;
|
|
1363
|
+
}>;
|
|
1364
|
+
/** Exchange an authorization code + PKCE verifier for the session + sealed key material. */
|
|
1365
|
+
token(body: {
|
|
1366
|
+
code: string;
|
|
1367
|
+
codeVerifier: string;
|
|
1368
|
+
appId?: string;
|
|
1369
|
+
}): Promise<{
|
|
1370
|
+
sessionToken: string;
|
|
1371
|
+
username: string;
|
|
1372
|
+
commitment: string;
|
|
1373
|
+
sealedKeys: string;
|
|
1374
|
+
appId: string;
|
|
1375
|
+
}>;
|
|
1376
|
+
/**
|
|
1377
|
+
* Read a factor record for `username` (returns an indistinguishable decoy if
|
|
1378
|
+
* absent). Gated types (email) require the recover `verifyToken`.
|
|
1379
|
+
*/
|
|
1380
|
+
vaultRead(username: string, factorType: VaultFactor["type"], verifyToken?: string,
|
|
1381
|
+
/** Passkeys are per-origin: naming the caller's host returns THAT origin's
|
|
1382
|
+
* passkey instead of whichever was enrolled first (which WebAuthn would
|
|
1383
|
+
* refuse). Ignored for other factor types. */
|
|
1384
|
+
rpId?: string,
|
|
1385
|
+
/** Device factors are selected by id — a user may have many machines. */
|
|
1386
|
+
factorId?: string): Promise<{
|
|
1387
|
+
factor: VaultFactor | null;
|
|
1388
|
+
}>;
|
|
1389
|
+
/**
|
|
1390
|
+
* Add/replace a factor (session-authed; the seed is already wrapped
|
|
1391
|
+
* client-side). Email factors additionally require (and consume) the
|
|
1392
|
+
* enroll `verifyToken`; the stored address must be the verified one.
|
|
1393
|
+
*/
|
|
1394
|
+
vaultPutFactor(token: string, factor: VaultFactor, verifyToken?: string): Promise<{
|
|
1395
|
+
ok: boolean;
|
|
1396
|
+
id: string;
|
|
1397
|
+
}>;
|
|
1398
|
+
/** List the user's factor metadata (no ciphertext). Session-authed. */
|
|
1399
|
+
vaultFactors(token: string): Promise<{
|
|
1400
|
+
factors: VaultFactor[];
|
|
1401
|
+
migrated: boolean;
|
|
1402
|
+
}>;
|
|
1403
|
+
/** Remove a factor by id (can't remove the last one). Session-authed. */
|
|
1404
|
+
vaultDeleteFactor(token: string, id: string): Promise<{
|
|
1405
|
+
deleted: boolean;
|
|
1406
|
+
}>;
|
|
1407
|
+
private json;
|
|
1408
|
+
private parse;
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
/**
|
|
1412
|
+
* Deterministic ZK identity derivation from `(username, password)`.
|
|
1413
|
+
*
|
|
1414
|
+
* Same inputs on ANY device produce the same secret, salt, and P-256
|
|
1415
|
+
* ECDSA + ECDH keypairs — which is what makes federated login from a
|
|
1416
|
+
* fresh browser possible. There is no localStorage involvement: every
|
|
1417
|
+
* register and login call re-derives the identity on demand.
|
|
1418
|
+
*
|
|
1419
|
+
* Pipeline:
|
|
1420
|
+
* master_seed = PBKDF2-SHA256(password, "muhkoo-zk-v1:" || username, 200k iters)
|
|
1421
|
+
* secret = HKDF-Expand-SHA256(master_seed, "zk-secret", 32 bytes) → hex
|
|
1422
|
+
* salt = HKDF-Expand-SHA256(master_seed, "zk-salt", 32 bytes) → hex
|
|
1423
|
+
* ecdsa_priv = HKDF-Expand-SHA256(master_seed, "ecdsa-priv", 32 bytes)
|
|
1424
|
+
* ecdh_priv = HKDF-Expand-SHA256(master_seed, "ecdh-priv", 32 bytes)
|
|
1425
|
+
*
|
|
1426
|
+
* P-256 public points are computed via `@noble/curves` (WebCrypto has no
|
|
1427
|
+
* derive-public-from-private-scalar API), then both keypairs are imported
|
|
1428
|
+
* into SubtleCrypto in JWK form so the rest of the crypto stack can keep
|
|
1429
|
+
* using `crypto.subtle.sign` / `crypto.subtle.deriveBits`.
|
|
1430
|
+
*
|
|
1431
|
+
* Originally lived in `accelerator/public/js/zk-identity.js` and was ported
|
|
1432
|
+
* to `muhkoo/web/src/zk/identity.ts`; consolidated here so the SDK owns
|
|
1433
|
+
* identity derivation end-to-end.
|
|
1434
|
+
*/
|
|
1435
|
+
interface ZkIdentity {
|
|
1436
|
+
/** 32-byte secret, hex-encoded. Private witness for the ZK circuit. */
|
|
1437
|
+
secretHex: string;
|
|
1438
|
+
/** 32-byte salt, hex-encoded. Private witness for the ZK circuit. */
|
|
1439
|
+
saltHex: string;
|
|
1440
|
+
/** P-256 ECDSA keypair, used to sign auth proofs. */
|
|
1441
|
+
ecdsaKeyPair: CryptoKeyPair;
|
|
1442
|
+
/** P-256 ECDH keypair, used by the chat ratchet. */
|
|
1443
|
+
ecdhKeyPair: CryptoKeyPair;
|
|
1444
|
+
}
|
|
1445
|
+
/**
|
|
1446
|
+
* Derive a deterministic ZK identity from `(username, password)`.
|
|
1447
|
+
*
|
|
1448
|
+
* Same inputs always produce the same identity, on any device — that's
|
|
1449
|
+
* what makes federated login work without dragging encrypted state across
|
|
1450
|
+
* devices.
|
|
1451
|
+
*/
|
|
1452
|
+
declare function deriveIdentity(username: string, password: string): Promise<ZkIdentity>;
|
|
1453
|
+
/**
|
|
1454
|
+
* Legacy master-seed derivation: `PBKDF2(password, "muhkoo-zk-v1:"||username)`.
|
|
1455
|
+
*
|
|
1456
|
+
* This was the *only* path pre-vault, where the identity descended directly from
|
|
1457
|
+
* the password. It's kept so existing accounts still re-derive their seed (and
|
|
1458
|
+
* the vault can adopt that exact seed on migration, preserving the commitment).
|
|
1459
|
+
* New accounts use a random seed (`vault.randomSeed`) instead, so their identity
|
|
1460
|
+
* is no longer a function of the password.
|
|
1461
|
+
*/
|
|
1462
|
+
declare function deriveMasterSeedFromPassword(username: string, password: string): Promise<Uint8Array>;
|
|
1463
|
+
/**
|
|
1464
|
+
* Derive the (unchanged) ZK identity from a 32-byte **master seed**. The seed is
|
|
1465
|
+
* HKDF-expanded into secret/salt + the P-256 ECDSA & ECDH keypairs exactly as
|
|
1466
|
+
* before — so the resulting commitment is byte-identical for a given seed. The
|
|
1467
|
+
* vault wraps THIS seed under each recovery factor.
|
|
1468
|
+
*/
|
|
1469
|
+
declare function deriveIdentityFromSeed(seed: Uint8Array): Promise<ZkIdentity>;
|
|
1470
|
+
|
|
1471
|
+
/**
|
|
1472
|
+
* Poseidon hash wrapper used by the ZK auth flow.
|
|
1473
|
+
*
|
|
1474
|
+
* Backed by `circomlibjs`'s `buildPoseidon`. Builder is lazy and cached
|
|
1475
|
+
* because the JS-side Babyjubjub field reps take ~100ms to construct, and
|
|
1476
|
+
* we'd rather not pay that on every page load.
|
|
1477
|
+
*
|
|
1478
|
+
* Returns hashes as decimal-string BigInts — same format snarkjs and the
|
|
1479
|
+
* `preimagePoK` circuit expect for inputs.
|
|
1480
|
+
*
|
|
1481
|
+
* Note: circomlibjs is `optionalDependencies` in connect's package.json.
|
|
1482
|
+
* Consumers that use the ZK auth flow (browser/server, not workers) need
|
|
1483
|
+
* it installed. The workers build does not import this module.
|
|
1484
|
+
*/
|
|
1485
|
+
/**
|
|
1486
|
+
* Poseidon-hash `inputs` and return the result as a decimal-string BigInt.
|
|
1487
|
+
* Inputs are converted to BigInt — pass either bigints, numbers, or numeric
|
|
1488
|
+
* strings.
|
|
1489
|
+
*/
|
|
1490
|
+
declare function poseidonHash(inputs: Array<bigint | string | number>): Promise<string>;
|
|
1491
|
+
|
|
1492
|
+
/**
|
|
1493
|
+
* Integrity pinning for the ZK circuit artifacts.
|
|
1494
|
+
*
|
|
1495
|
+
* THE PROBLEM. `generateAuthProof` hands the user's `secret` and `salt` — the
|
|
1496
|
+
* inputs the whole zero-knowledge design exists to keep on the device — to a
|
|
1497
|
+
* witness generator downloaded at runtime from the accelerator. That is the
|
|
1498
|
+
* party the proof is meant to convince. With no integrity check, a malicious or
|
|
1499
|
+
* compromised server serves a modified `preimagePoK.wasm` that copies `secret`
|
|
1500
|
+
* into a public-signal slot (public signals are just `witness[1..nPublic]`), and
|
|
1501
|
+
* the client forwards them without ever comparing against what it computed
|
|
1502
|
+
* locally. The server then derives the `client.kv` at-rest key through
|
|
1503
|
+
* `StorageCipher` and decrypts everything it stores. Controlling the `.zkey` is
|
|
1504
|
+
* just as bad by itself: a subverted CRS breaks Groth16's zero-knowledge
|
|
1505
|
+
* property outright.
|
|
1506
|
+
*
|
|
1507
|
+
* THE FIX. Fetch the bytes here, SHA-256 them against digests pinned at build
|
|
1508
|
+
* time, and hand snarkjs verified BUFFERS rather than a URL it will fetch for
|
|
1509
|
+
* itself. snarkjs accepts `Uint8Array` for both artifacts.
|
|
1510
|
+
*
|
|
1511
|
+
* THREAT-MODEL BOUNDARY, and why local paths are exempt. The attack is "a
|
|
1512
|
+
* SERVER serves you a modified artifact". A filesystem path is not
|
|
1513
|
+
* server-controlled — it is the caller's own machine — so non-`http(s)`
|
|
1514
|
+
* specifiers pass through untouched (this is also what lets tests point at
|
|
1515
|
+
* `circuits/build/` directly, where snarkjs reads them as paths under Node).
|
|
1516
|
+
* Anything fetched over the network must be pinned, or explicitly opted out of
|
|
1517
|
+
* with {@link CircuitIntegrity.allowUnpinned}.
|
|
1518
|
+
*
|
|
1519
|
+
* There is deliberately NO silent fallback. An unpinned `https:` URL throws
|
|
1520
|
+
* rather than quietly proving with unverified bytes — the failure mode this
|
|
1521
|
+
* codebase already has elsewhere (see the SHA-256-for-Poseidon substitution in
|
|
1522
|
+
* `crypto/ZeroKnowledge.ts`) is exactly what makes a security control useless.
|
|
1523
|
+
*/
|
|
1524
|
+
/**
|
|
1525
|
+
* SHA-256 of the `preimagePoK` artifacts this SDK version was built against.
|
|
1526
|
+
*
|
|
1527
|
+
* Verified identical to what the accelerator serves from
|
|
1528
|
+
* `public/circuits/build/` at the time of pinning. **If the circuit is ever
|
|
1529
|
+
* recompiled, these must be regenerated in the same commit as
|
|
1530
|
+
* `PREIMAGE_POK_VERIFICATION_KEY`, or every login fails closed** — which is the
|
|
1531
|
+
* intended direction for a mismatch.
|
|
1532
|
+
*
|
|
1533
|
+
* Regenerate with:
|
|
1534
|
+
* shasum -a 256 circuits/build/preimagePoK_js/preimagePoK.wasm \
|
|
1535
|
+
* circuits/build/preimagePoK_0001.zkey
|
|
1536
|
+
*/
|
|
1537
|
+
declare const PINNED_CIRCUIT_DIGESTS: {
|
|
1538
|
+
readonly preimagePoKWasm: "9230ce3a883ca11e618639ac14fe0048b5dc4491c77b1dde3e41eb0896c33e10";
|
|
1539
|
+
readonly preimagePoKZkey: "087f4ef09b6afbacad8e47b0029ded408a0ac3b5c9d5a1624e94c06bc258b871";
|
|
1540
|
+
};
|
|
1541
|
+
/** Integrity options carried alongside a circuit URL. */
|
|
1542
|
+
interface CircuitIntegrity {
|
|
1543
|
+
/** Expected SHA-256 of the witness generator, lowercase hex. */
|
|
1544
|
+
wasmSha256?: string;
|
|
1545
|
+
/** Expected SHA-256 of the proving key, lowercase hex. */
|
|
1546
|
+
zkeySha256?: string;
|
|
1547
|
+
/**
|
|
1548
|
+
* Proceed without verification for network-fetched artifacts.
|
|
1549
|
+
*
|
|
1550
|
+
* DANGEROUS: it re-opens the attack described in this module's docs. Only
|
|
1551
|
+
* for local development against a circuit you compiled yourself and have
|
|
1552
|
+
* not yet pinned.
|
|
1553
|
+
*/
|
|
1554
|
+
allowUnpinned?: boolean;
|
|
1555
|
+
}
|
|
1556
|
+
/** Thrown when a fetched artifact does not match its pinned digest. */
|
|
1557
|
+
declare class CircuitIntegrityError extends Error {
|
|
1558
|
+
readonly url: string;
|
|
1559
|
+
readonly expected: string;
|
|
1560
|
+
readonly actual: string;
|
|
1561
|
+
constructor(url: string, expected: string, actual: string);
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
/**
|
|
1565
|
+
* Groth16 proof generation for the `preimagePoK` circuit.
|
|
1566
|
+
*
|
|
1567
|
+
* The circuit proves knowledge of `(secret, salt, ecdsaPub)` such that
|
|
1568
|
+
* commitment = Poseidon(secret, salt, Poseidon(ecdsaPub))
|
|
1569
|
+
* while only revealing `(commitment, nonce, ecdsaPubHash)` as public signals.
|
|
1570
|
+
*
|
|
1571
|
+
* Circuit assets (wasm + zkey) are served by the accelerator worker. Their
|
|
1572
|
+
* URLs are a required argument of {@link generateAuthProof} — there is no
|
|
1573
|
+
* default. The web app constructs them relative to its configured worker
|
|
1574
|
+
* `baseUrl` so the SPA can target either the local wrangler dev or a deployed
|
|
1575
|
+
* worker depending on `VITE_WORKER_URL`.
|
|
1576
|
+
*/
|
|
1577
|
+
|
|
1578
|
+
interface CircuitUrls extends CircuitIntegrity {
|
|
1579
|
+
/** preimagePoK.wasm — the circuit's witness generator. */
|
|
1580
|
+
wasmUrl: string;
|
|
1581
|
+
/** preimagePoK_0001.zkey — the proving key. */
|
|
1582
|
+
zkeyUrl: string;
|
|
1583
|
+
}
|
|
1584
|
+
/**
|
|
1585
|
+
* Canonical {@link CircuitUrls} for the `preimagePoK` login circuit, anchored
|
|
1586
|
+
* at a base URL where the accelerator serves the circuit assets out of
|
|
1587
|
+
* `circuits/build/`. The filenames here are fixed by the circom build output
|
|
1588
|
+
* (the witness generator lands inside a `preimagePoK_js/` directory); callers
|
|
1589
|
+
* shouldn't hard-code them. `Client` uses this to default its `circuits`
|
|
1590
|
+
* option, and `PersonalSpaceClient` consumers can pass `defaultCircuitUrls(baseUrl)`.
|
|
1591
|
+
*/
|
|
1592
|
+
declare function defaultCircuitUrls(baseUrl: string): CircuitUrls;
|
|
1593
|
+
/**
|
|
1594
|
+
* Reduce any hex/decimal/byte input to a decimal-string field element in
|
|
1595
|
+
* `[0, FIELD_SIZE)`. snarkjs and the `preimagePoK` circuit both want decimal
|
|
1596
|
+
* strings; everything else gets normalized here.
|
|
1597
|
+
*
|
|
1598
|
+
* **Behavior is intentionally byte-for-byte compatible with the legacy
|
|
1599
|
+
* `accelerator/public/js/zk-snarkjs-client.js#toField`.** Diverging produces
|
|
1600
|
+
* different commitments than accounts registered via the legacy flow,
|
|
1601
|
+
* which locks those users out forever — the worker stores whatever
|
|
1602
|
+
* commitment the client sent at register time and compares it as a string
|
|
1603
|
+
* at login time.
|
|
1604
|
+
*
|
|
1605
|
+
* In particular: a 32+ char string of hex chars is always treated as hex,
|
|
1606
|
+
* even if every character happens to be 0-9 with no a-f letters. An
|
|
1607
|
+
* earlier version of this function gated the hex branch on "has at least
|
|
1608
|
+
* one letter" — that broke the digit-only-hex edge case (rare in practice,
|
|
1609
|
+
* but real). DO NOT add that letter check back.
|
|
1610
|
+
*/
|
|
1611
|
+
declare function toField(value: string | bigint | number): string;
|
|
1612
|
+
/**
|
|
1613
|
+
* Build the user's Poseidon commitment binding `(secret, salt, ecdsaPubHash)`.
|
|
1614
|
+
* Same formula the worker verifies against on register and authenticate.
|
|
1615
|
+
*/
|
|
1616
|
+
declare function buildCommitment(secretHex: string, saltHex: string, ecdsaPubHex: string): Promise<string>;
|
|
1617
|
+
/**
|
|
1618
|
+
* Generate a Groth16 proof for the `preimagePoK` circuit. Public signals end
|
|
1619
|
+
* up `[commitment, nonce, ecdsaPubHash]` — the worker checks the commitment
|
|
1620
|
+
* matches the stored one and the nonce matches the issued challenge nonce
|
|
1621
|
+
* reduced into the field.
|
|
1622
|
+
*
|
|
1623
|
+
* Throws if circuit URLs are unreachable or snarkjs decides the inputs are
|
|
1624
|
+
* malformed.
|
|
1625
|
+
*/
|
|
1626
|
+
declare function generateAuthProof(args: {
|
|
1627
|
+
secretHex: string;
|
|
1628
|
+
saltHex: string;
|
|
1629
|
+
ecdsaPubHex: string;
|
|
1630
|
+
nonceHex: string;
|
|
1631
|
+
/** Required — caller supplies URLs scoped to the worker they want to talk to. */
|
|
1632
|
+
circuits: CircuitUrls;
|
|
1633
|
+
}): Promise<{
|
|
1634
|
+
proof: Groth16Proof;
|
|
1635
|
+
publicSignals: string[];
|
|
1636
|
+
commitment: string;
|
|
1637
|
+
nonceField: string;
|
|
1638
|
+
ecdsaPubHash: string;
|
|
1639
|
+
}>;
|
|
1640
|
+
|
|
1641
|
+
/**
|
|
1642
|
+
* Conversions between SubtleCrypto `CryptoKey`s and the wire formats the
|
|
1643
|
+
* accelerator's auth endpoints + `preimagePoK` circuit expect.
|
|
1644
|
+
*
|
|
1645
|
+
* Two outputs we need from an ECDSA `CryptoKey`:
|
|
1646
|
+
* - **base64 raw** — passed to `/api/auth/zk-register` as
|
|
1647
|
+
* `ecdsaPublicKey` / `ecdhPublicKey`. (Name is historical;
|
|
1648
|
+
* the legacy client called it `exportPublicKeyToBase58`
|
|
1649
|
+
* but actually used base64 — kept as base64.)
|
|
1650
|
+
* - **hex raw** — fed to the proof as `ecdsaPub` input; the circuit
|
|
1651
|
+
* reduces it into a field element.
|
|
1652
|
+
*
|
|
1653
|
+
* Both go through `crypto.subtle.exportKey("raw", ...)` which yields the
|
|
1654
|
+
* uncompressed SEC1 form (`0x04 || X || Y`).
|
|
1655
|
+
*/
|
|
1656
|
+
/** Raw uncompressed (`0x04 || X || Y`) public key bytes, as lowercase hex. */
|
|
1657
|
+
declare function exportPublicKeyHex(publicKey: CryptoKey): Promise<string>;
|
|
1658
|
+
/** Raw uncompressed public key as base64 — wire format for the auth endpoints. */
|
|
1659
|
+
declare function exportPublicKeyBase64(publicKey: CryptoKey): Promise<string>;
|
|
1660
|
+
/** Sign a UTF-8 `message` with an ECDSA private key. Returns base64. */
|
|
1661
|
+
declare function signMessage(message: string, privateKey: CryptoKey): Promise<string>;
|
|
1662
|
+
|
|
1663
|
+
/**
|
|
1664
|
+
* Hosted-auth handoff crypto (auth.muhkoo.dev — AUTH_HOSTED_PLAN.md §3).
|
|
1665
|
+
*
|
|
1666
|
+
* The hosted page authenticates the user and ends up holding the master seed.
|
|
1667
|
+
* To pass it to the developer's app without it ever sitting server-readable or
|
|
1668
|
+
* landing in browser history:
|
|
1669
|
+
*
|
|
1670
|
+
* 1. {@link sealSeed} generates a one-time key K_t, AES-256-GCM-seals the seed
|
|
1671
|
+
* under it, and returns the ciphertext envelope (`sealedKeys`, stored in
|
|
1672
|
+
* the grant the server relays but can't read) plus K_t (`fragmentKey`,
|
|
1673
|
+
* placed ONLY in the redirect URL fragment — fragments never hit a server).
|
|
1674
|
+
* 2. The app exchanges the code for `sealedKeys`, reads K_t from the fragment,
|
|
1675
|
+
* and {@link unsealSeed} recovers the seed, then strips the fragment.
|
|
1676
|
+
*
|
|
1677
|
+
* Trust-equivalent to today's embedded SDK (the app still ends up with the
|
|
1678
|
+
* seed) while moving credential entry onto the Muhkoo origin. The envelope is
|
|
1679
|
+
* versioned so a future per-app-key handoff (v2) can swap in without a protocol
|
|
1680
|
+
* change.
|
|
1681
|
+
*
|
|
1682
|
+
* ## v2 — device pairing (TV / no-redirect handoff)
|
|
1683
|
+
*
|
|
1684
|
+
* v1's K_t rides in the redirect URL *fragment*, which works only because the
|
|
1685
|
+
* receiving app is a browser page that can read `location.hash`. A TV app has
|
|
1686
|
+
* no redirect to receive: it polls. So v2 keeps the *entire* v1 shape — the
|
|
1687
|
+
* server relays a sealed envelope it cannot read — and replaces only the key
|
|
1688
|
+
* agreement:
|
|
1689
|
+
*
|
|
1690
|
+
* - the TV generates an ephemeral P-256 ECDH keypair in memory and publishes
|
|
1691
|
+
* the public half ({@link generateDevicePairingKeypair});
|
|
1692
|
+
* - the hosted page (the component that already holds the unlocked seed and
|
|
1693
|
+
* already owns sealing) seals *to that public key* with a fresh ephemeral
|
|
1694
|
+
* keypair of its own ({@link sealSeedToDevice}) — ECDH → HKDF-SHA256 →
|
|
1695
|
+
* AES-256-GCM;
|
|
1696
|
+
* - the TV recovers the seed with its private key
|
|
1697
|
+
* ({@link unsealSeedFromDevice}).
|
|
1698
|
+
*
|
|
1699
|
+
* The server stores and relays the v2 envelope byte-for-byte, exactly as it
|
|
1700
|
+
* does for v1, and can read neither. The one thing ECDH cannot supply is proof
|
|
1701
|
+
* that the public key the hosted page sealed to is *the TV's* rather than an
|
|
1702
|
+
* attacker's — that is what {@link pairingVerificationCode} is for, and it is
|
|
1703
|
+
* the only MITM defence in the flow (see the entropy note on that function).
|
|
1704
|
+
*/
|
|
1705
|
+
interface SealedHandoff {
|
|
1706
|
+
/** base64 JSON envelope — relayed by the server in the grant; server-opaque. */
|
|
1707
|
+
sealedKeys: string;
|
|
1708
|
+
/** base64url one-time key — rides ONLY in the redirect URL fragment. */
|
|
1709
|
+
fragmentKey: string;
|
|
1710
|
+
}
|
|
1711
|
+
/** Seal the master seed under a fresh one-time key for the redirect handoff. */
|
|
1712
|
+
declare function sealSeed(seed: Uint8Array): Promise<SealedHandoff>;
|
|
1713
|
+
/** Recover the master seed from the sealed envelope + the fragment key. */
|
|
1714
|
+
declare function unsealSeed(sealedKeys: string, fragmentKey: string): Promise<Uint8Array>;
|
|
1715
|
+
/**
|
|
1716
|
+
* Alphabet for the pairing verification code: 30 symbols, every confusable pair
|
|
1717
|
+
* removed in BOTH directions — no `0`/`O`, no `1`/`I`/`L`, and no `U` (which is
|
|
1718
|
+
* read as `V` on the low-resolution, far-viewed type a TV renders). Because the
|
|
1719
|
+
* user compares this string across two screens by eye, unambiguity beats the
|
|
1720
|
+
* convenience of a power-of-two alphabet; the derivation below rejection-samples
|
|
1721
|
+
* to avoid the modulo bias that costs us.
|
|
1722
|
+
*/
|
|
1723
|
+
declare const PAIRING_CODE_ALPHABET = "ABCDEFGHJKMNPQRSTVWXYZ23456789";
|
|
1724
|
+
/** Length of the pairing verification code, in characters. 30^8 ≈ 39.3 bits. */
|
|
1725
|
+
declare const PAIRING_CODE_LENGTH = 8;
|
|
1726
|
+
/** An ephemeral ECDH keypair generated by the pairing device (the TV). */
|
|
1727
|
+
interface DevicePairingKeypair {
|
|
1728
|
+
/** base64 raw (SEC1 uncompressed, 65 bytes) public key — safe to publish. */
|
|
1729
|
+
publicKeyB64: string;
|
|
1730
|
+
/**
|
|
1731
|
+
* Non-extractable private half. In-memory only, one per pairing attempt:
|
|
1732
|
+
* it is never persisted and never leaves the device, so a captured pairing
|
|
1733
|
+
* transcript is undecryptable once the attempt ends.
|
|
1734
|
+
*/
|
|
1735
|
+
privateKey: CryptoKey;
|
|
1736
|
+
}
|
|
1737
|
+
/**
|
|
1738
|
+
* (Device side) Generate the ephemeral P-256 ECDH keypair for one pairing
|
|
1739
|
+
* attempt. Call this once per attempt and throw it away when the attempt ends —
|
|
1740
|
+
* reusing a keypair across attempts would let an old, observed ciphertext be
|
|
1741
|
+
* decrypted by whoever later obtains the key.
|
|
1742
|
+
*/
|
|
1743
|
+
declare function generateDevicePairingKeypair(): Promise<DevicePairingKeypair>;
|
|
1744
|
+
/**
|
|
1745
|
+
* (Hosted-page side) Seal the master seed *to a device's* pairing public key.
|
|
1746
|
+
*
|
|
1747
|
+
* A fresh ephemeral keypair is generated per call and its public half is
|
|
1748
|
+
* carried in the envelope (`epk`), so sealing the same seed to the same device
|
|
1749
|
+
* twice yields unrelated ciphertext — and the sealer retains nothing that could
|
|
1750
|
+
* later reopen the envelope.
|
|
1751
|
+
*
|
|
1752
|
+
* Returns the v2 `sealedKeys` string, which slots into exactly the same server
|
|
1753
|
+
* field as v1: relayed verbatim, never read.
|
|
1754
|
+
*/
|
|
1755
|
+
declare function sealSeedToDevice(seed: Uint8Array, devicePublicKeyB64: string): Promise<string>;
|
|
1756
|
+
/**
|
|
1757
|
+
* (Device side) Recover the master seed from a v2 envelope using the ephemeral
|
|
1758
|
+
* private key from {@link generateDevicePairingKeypair}.
|
|
1759
|
+
*
|
|
1760
|
+
* Throws on any tamper: a substituted `epk` yields a different shared secret
|
|
1761
|
+
* (or fails point validation outright), and any edit to `iv`/`ct` fails the
|
|
1762
|
+
* AES-GCM tag. There is no path that returns partially-authenticated bytes.
|
|
1763
|
+
*
|
|
1764
|
+
* `devicePublicKeyB64` is the caller's OWN public half (from
|
|
1765
|
+
* {@link generateDevicePairingKeypair}). It is required, not optional, because
|
|
1766
|
+
* it is mixed into the KDF — see {@link deriveDeviceSealKey} for why binding the
|
|
1767
|
+
* recipient matters. A device always holds both halves, so this costs nothing.
|
|
1768
|
+
*/
|
|
1769
|
+
declare function unsealSeedFromDevice(sealedKeys: string, privateKey: CryptoKey, devicePublicKeyB64: string): Promise<Uint8Array>;
|
|
1770
|
+
/**
|
|
1771
|
+
* Short code derived deterministically from the device's pairing public key,
|
|
1772
|
+
* displayed on BOTH the TV and the approving browser so the human can confirm
|
|
1773
|
+
* they match before approving.
|
|
1774
|
+
*
|
|
1775
|
+
* ## What this is worth (be honest about it)
|
|
1776
|
+
*
|
|
1777
|
+
* ~39.3 bits (30^8), rendered in an unambiguous 30-symbol alphabet and grouped
|
|
1778
|
+
* as `XXXX-XXXX` for comparison. Alongside the recipient-key binding in
|
|
1779
|
+
* {@link deriveDeviceSealKey}, this is what stops a substituted device key: the
|
|
1780
|
+
* KDF rejects a *negated* key, and this code is what a human uses to reject a
|
|
1781
|
+
* wholly *attacker-generated* one, which the crypto cannot detect on its own —
|
|
1782
|
+
* ECDH proves nothing about WHOSE public key it is.
|
|
1783
|
+
*
|
|
1784
|
+
* Two attacks, and what each actually costs:
|
|
1785
|
+
*
|
|
1786
|
+
* 1. **Online grind.** Generate keypairs until one's code equals the honest
|
|
1787
|
+
* TV's. ~2^39 trials. Naively that reads as "keygen + hash", but a real
|
|
1788
|
+
* attacker never generates keys: walking `P_{i+1} = P_i + G` while tracking
|
|
1789
|
+
* `d_i = d_0 + i` makes each trial one point-add plus one derivation. With
|
|
1790
|
+
* a plain hash that is well under a core-day — which is why this uses
|
|
1791
|
+
* PBKDF2-SHA256 at {@link PAIRING_CODE_ITERATIONS} iterations instead. The
|
|
1792
|
+
* work factor applies per trial, pushing the same grind to the order of
|
|
1793
|
+
* 10^5 core-days. The target is also only learned when pairing starts, so
|
|
1794
|
+
* the grind is online against the pairing TTL (keep it minutes).
|
|
1795
|
+
* 2. **Precomputed table.** The reason this is not a plain hash. A table over
|
|
1796
|
+
* candidate keys is computed once and reused forever; with SHA-256 that is
|
|
1797
|
+
* cheap enough to be the preferred attack. The iteration count multiplies
|
|
1798
|
+
* the table's build cost by the same 2 * 10^5.
|
|
1799
|
+
*
|
|
1800
|
+
* Neither is a proof of security, and an adversary with ASIC-scale hardware
|
|
1801
|
+
* changes these numbers. If the margin is ever judged insufficient, raise
|
|
1802
|
+
* {@link PAIRING_CODE_LENGTH} — 10 characters buys ~49 bits for two more
|
|
1803
|
+
* characters of human comparison, and is display-only: it does not touch the
|
|
1804
|
+
* envelope or the wire format.
|
|
1805
|
+
*
|
|
1806
|
+
* The cost is paid once per pairing, on two idle devices, so ~200k iterations
|
|
1807
|
+
* is imperceptible here in a way it would not be on a hot path.
|
|
1808
|
+
*/
|
|
1809
|
+
declare function pairingVerificationCode(devicePublicKeyB64: string): Promise<string>;
|
|
1810
|
+
interface Pkce {
|
|
1811
|
+
codeVerifier: string;
|
|
1812
|
+
codeChallenge: string;
|
|
1813
|
+
}
|
|
1814
|
+
/** Generate a PKCE verifier + S256 challenge for the authorize→token exchange. */
|
|
1815
|
+
declare function generatePkce(): Promise<Pkce>;
|
|
1816
|
+
/** A random URL-safe value for the OAuth `state` (CSRF binding on the callback). */
|
|
1817
|
+
declare function randomState(): string;
|
|
1818
|
+
|
|
1819
|
+
/**
|
|
1820
|
+
* `deviceStore` — at-rest persistence for a paired device (the TV).
|
|
1821
|
+
*
|
|
1822
|
+
* A device that has completed {@link HostedAuth.startDevicePairing} holds the
|
|
1823
|
+
* account's **master seed**. Android WebView has no WebAuthn, so the passkey
|
|
1824
|
+
* wrapping used elsewhere isn't available, and re-pairing on every cold start
|
|
1825
|
+
* is not acceptable UX for a set-top box. So the seed is persisted, encrypted:
|
|
1826
|
+
*
|
|
1827
|
+
* | Item | Where | Form |
|
|
1828
|
+
* | ------------------------ | ------------------------------------ | ---- |
|
|
1829
|
+
* | wrapping key | IndexedDB (`muhkoo.device.v1`) | `CryptoKey`, AES-GCM-256, **non-extractable** |
|
|
1830
|
+
* | device identity keypair | IndexedDB (`muhkoo.device.v1`) | `CryptoKeyPair`, ECDSA P-256, private half **non-extractable** |
|
|
1831
|
+
* | sealed identity blob | localStorage `muhkoo.tv.identity.v1` | `{"v":1,"iv":…,"ct":…}` over `JSON{username, commitment, seed(b64), pairedAt}` |
|
|
1832
|
+
*
|
|
1833
|
+
* The split is deliberate — ciphertext in one backend, key in another, and the
|
|
1834
|
+
* key non-extractable so JS can *use* it but not export it.
|
|
1835
|
+
*
|
|
1836
|
+
* ## Be honest about what this is worth
|
|
1837
|
+
*
|
|
1838
|
+
* **This is obfuscation, not protection.** It raises the cost of a casual
|
|
1839
|
+
* `localStorage` dump to "nothing useful", and that is all:
|
|
1840
|
+
*
|
|
1841
|
+
* - **XSS on the device origin** cannot *exfiltrate* the wrapping key (it is
|
|
1842
|
+
* non-extractable) but can absolutely call `decrypt` and read the seed. The
|
|
1843
|
+
* only mitigation is that there is no third-party script in the shell —
|
|
1844
|
+
* enforce a strict CSP with no `unsafe-inline`, no CDN, no remote fonts.
|
|
1845
|
+
* - **ADB / root / a backup extraction / a forensic image recovers the seed.**
|
|
1846
|
+
* The IndexedDB LevelDB files hold the AES key bytes in the clear; WebCrypto
|
|
1847
|
+
* "non-extractable" is a JS-API boundary, not an OS one. **A stolen device is
|
|
1848
|
+
* a stolen identity.**
|
|
1849
|
+
*
|
|
1850
|
+
* That is not an implementation shortcut, it is the shape of the problem: any
|
|
1851
|
+
* wrapping key usable without user interaction lives somewhere equally readable
|
|
1852
|
+
* on a keyboard-less device with no hardware key store exposed to WebView.
|
|
1853
|
+
* Reduce the blast radius with `android:allowBackup="false"`,
|
|
1854
|
+
* `android:debuggable="false"`, a strict CSP, and by storing *only* the 32-byte
|
|
1855
|
+
* seed + username + commitment (never a session token, never a recovery phrase).
|
|
1856
|
+
*
|
|
1857
|
+
* ## Documented upgrade path — no protocol change
|
|
1858
|
+
*
|
|
1859
|
+
* If the native shell exposes a Keystore bridge:
|
|
1860
|
+
*
|
|
1861
|
+
* ```java
|
|
1862
|
+
* @JavascriptInterface public String wrap(String b64plain); // AES-GCM, StrongBox when present
|
|
1863
|
+
* @JavascriptInterface public String unwrap(String b64cipher);
|
|
1864
|
+
* ```
|
|
1865
|
+
*
|
|
1866
|
+
* this module prefers `globalThis.MuhkooKeystore` automatically and falls back
|
|
1867
|
+
* to the WebCrypto scheme otherwise. The Keystore key is hardware-backed and
|
|
1868
|
+
* non-exportable at the OS level, so a copied filesystem image alone becomes
|
|
1869
|
+
* useless. Nothing on the wire, in the endpoint contracts, or in the SDK surface
|
|
1870
|
+
* changes — only the two functions below do, internally.
|
|
1871
|
+
*
|
|
1872
|
+
* ## Degradation
|
|
1873
|
+
*
|
|
1874
|
+
* Nothing here throws at import time. On a runtime with neither backend (SSR,
|
|
1875
|
+
* Workers, Node) the module still loads: {@link loadDeviceIdentity} returns
|
|
1876
|
+
* `null`, {@link persistDeviceIdentity} throws a typed
|
|
1877
|
+
* {@link DeviceStoreUnavailableError} the caller can swallow, and
|
|
1878
|
+
* {@link deviceIdentityKey} falls back to an **in-memory** keypair for the
|
|
1879
|
+
* lifetime of the process (so pairing still works; the device is just never
|
|
1880
|
+
* recognised as "known" on a later run).
|
|
1881
|
+
*/
|
|
1882
|
+
/** localStorage key holding the AES-GCM-sealed identity blob. */
|
|
1883
|
+
declare const DEVICE_IDENTITY_STORAGE_KEY = "muhkoo.tv.identity.v1";
|
|
1884
|
+
/** IndexedDB database holding the non-extractable keys. */
|
|
1885
|
+
declare const DEVICE_KEY_DB_NAME = "muhkoo.device.v1";
|
|
1886
|
+
/** What a paired device persists. The seed **is** the identity — treat it as such. */
|
|
1887
|
+
interface PersistedDeviceIdentity {
|
|
1888
|
+
username: string;
|
|
1889
|
+
/** Decimal-string Poseidon commitment. Pinned: a later pairing that yields a
|
|
1890
|
+
* different commitment for this device must be refused, not adopted. */
|
|
1891
|
+
commitment: string;
|
|
1892
|
+
/** The 32-byte master seed. */
|
|
1893
|
+
seed: Uint8Array;
|
|
1894
|
+
pairedAt: number;
|
|
1895
|
+
}
|
|
1896
|
+
/** Thrown when persistence is impossible (no IndexedDB and/or no localStorage). */
|
|
1897
|
+
declare class DeviceStoreUnavailableError extends Error {
|
|
1898
|
+
constructor(what: string);
|
|
1899
|
+
}
|
|
1900
|
+
/** Key/value store for live `CryptoKey` objects (IndexedDB in the browser). */
|
|
1901
|
+
interface DeviceKeyVault {
|
|
1902
|
+
get(id: string): Promise<unknown | null>;
|
|
1903
|
+
put(id: string, value: unknown): Promise<void>;
|
|
1904
|
+
/** Delete the entire store — used by `clearDeviceIdentity`. */
|
|
1905
|
+
destroy(): Promise<void>;
|
|
1906
|
+
}
|
|
1907
|
+
/** String store for the sealed blob (localStorage in the browser). */
|
|
1908
|
+
interface DeviceBlobStore {
|
|
1909
|
+
get(key: string): string | null;
|
|
1910
|
+
set(key: string, value: string): void;
|
|
1911
|
+
remove(key: string): void;
|
|
1912
|
+
}
|
|
1913
|
+
/** The native Keystore bridge, when the shell provides one (see module docs). */
|
|
1914
|
+
interface MuhkooKeystoreBridge {
|
|
1915
|
+
wrap(b64plain: string): string | Promise<string>;
|
|
1916
|
+
unwrap(b64cipher: string): string | Promise<string>;
|
|
1368
1917
|
}
|
|
1369
1918
|
/**
|
|
1370
|
-
*
|
|
1371
|
-
*
|
|
1372
|
-
*
|
|
1919
|
+
* Point the store at explicit backends (or `null` to force "unavailable"), and
|
|
1920
|
+
* reset the memoized device keypair. Testing / embedding seam — pass `{}` to
|
|
1921
|
+
* restore the default browser backends.
|
|
1373
1922
|
*/
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
path?: string;
|
|
1380
|
-
lastModified: number;
|
|
1381
|
-
createdAt: number;
|
|
1382
|
-
chunkCount: number;
|
|
1383
|
-
shardCount: number;
|
|
1384
|
-
}
|
|
1385
|
-
/** Distil a `FileManifest` into a `FileStat`. */
|
|
1386
|
-
declare function manifestToStat(manifest: FileManifest): FileStat;
|
|
1387
|
-
|
|
1923
|
+
declare function configureDeviceStore(opts?: {
|
|
1924
|
+
keys?: DeviceKeyVault | null;
|
|
1925
|
+
blobs?: DeviceBlobStore | null;
|
|
1926
|
+
keystore?: MuhkooKeystoreBridge | null;
|
|
1927
|
+
}): void;
|
|
1388
1928
|
/**
|
|
1389
|
-
*
|
|
1390
|
-
*
|
|
1391
|
-
*
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
*
|
|
1396
|
-
*
|
|
1397
|
-
*
|
|
1398
|
-
*
|
|
1399
|
-
*
|
|
1400
|
-
*
|
|
1401
|
-
* GET /api/shards/:hash 200 with shard bytes (application/octet-stream)
|
|
1402
|
-
* 404 if absent
|
|
1929
|
+
* True when {@link deviceIdentityKey} had to fall back to an in-memory keypair
|
|
1930
|
+
* because no key vault was available. Such a device can pair, but will never be
|
|
1931
|
+
* recognised as "known" afterwards (so approval always needs a fresh factor).
|
|
1932
|
+
*/
|
|
1933
|
+
declare function deviceIdentityIsEphemeral(): boolean;
|
|
1934
|
+
/**
|
|
1935
|
+
* The device's **persistent** ECDSA P-256 identity keypair, created on first
|
|
1936
|
+
* call. This is not a browser fingerprint — it is a key, and every
|
|
1937
|
+
* `POST /api/auth/device/code` proves possession of it. Without that proof,
|
|
1938
|
+
* "known device" would be an unauthenticated string anyone could claim in order
|
|
1939
|
+
* to skip the approver's re-authentication.
|
|
1403
1940
|
*
|
|
1404
|
-
*
|
|
1405
|
-
* skip a PUT they'd otherwise dedup-rewrite. The
|
|
1406
|
-
* server can ignore the optimization if it wants.
|
|
1941
|
+
* The private half is non-extractable and never leaves IndexedDB as bytes.
|
|
1407
1942
|
*/
|
|
1943
|
+
declare function deviceIdentityKey(): Promise<CryptoKeyPair>;
|
|
1944
|
+
/** `base64url(SHA-256(raw SEC1 device identity public key))` — 43 chars. */
|
|
1945
|
+
declare function deviceFingerprint(): Promise<string>;
|
|
1946
|
+
/** Whether a sealed identity blob exists. Cheap: no crypto, no IndexedDB, no network. */
|
|
1947
|
+
declare function hasDeviceIdentity(): Promise<boolean>;
|
|
1408
1948
|
/**
|
|
1409
|
-
*
|
|
1410
|
-
*
|
|
1411
|
-
*
|
|
1412
|
-
* arrow one-way (offline → storage).
|
|
1949
|
+
* Seal and store the identity. Throws {@link DeviceStoreUnavailableError} when
|
|
1950
|
+
* there is nowhere to put it — callers should treat that as "this device won't
|
|
1951
|
+
* resume", not as a pairing failure.
|
|
1413
1952
|
*/
|
|
1414
|
-
|
|
1415
|
-
get(hash: string): Promise<Uint8Array | null>;
|
|
1416
|
-
put(hash: string, bytes: Uint8Array): Promise<void>;
|
|
1417
|
-
}
|
|
1953
|
+
declare function persistDeviceIdentity(v: PersistedDeviceIdentity): Promise<void>;
|
|
1418
1954
|
/**
|
|
1419
|
-
*
|
|
1420
|
-
*
|
|
1421
|
-
*
|
|
1422
|
-
*
|
|
1955
|
+
* Recover the persisted identity, or `null` when there is none / it can't be
|
|
1956
|
+
* opened (wiped IndexedDB, a different browser profile, a corrupted blob).
|
|
1957
|
+
* Never throws — a cold start with unreadable storage must fall back to a fresh
|
|
1958
|
+
* pairing, not to an error screen.
|
|
1423
1959
|
*/
|
|
1424
|
-
|
|
1425
|
-
getBlock(hash: string, opts?: {
|
|
1426
|
-
timeoutMs?: number;
|
|
1427
|
-
}): Promise<Uint8Array | null>;
|
|
1428
|
-
announce(hash: string): void;
|
|
1429
|
-
}
|
|
1430
|
-
interface ShardClientOptions {
|
|
1431
|
-
/** Base URL of the accelerator (e.g. `https://muhkoo-accelerator.workers.dev`). */
|
|
1432
|
-
baseUrl: string;
|
|
1433
|
-
/**
|
|
1434
|
-
* Path prefix for the shard endpoints, joined to `baseUrl` before the
|
|
1435
|
-
* hash. Defaults to `/api/shards`, which targets a global shard store
|
|
1436
|
-
* (e.g. a dedicated `ShardStoreDO`). Pass a room-scoped value like
|
|
1437
|
-
* `/api/spaces/{roomId}/shards` to keep shards bound to a specific
|
|
1438
|
-
* `SharedSpaceDO` — that's the path the chat application uses today.
|
|
1439
|
-
*
|
|
1440
|
-
* Leading slash optional; trailing slash stripped. The final URL is
|
|
1441
|
-
* always `${baseUrl}${normalizedPrefix}/${encodeURIComponent(hash)}`.
|
|
1442
|
-
*/
|
|
1443
|
-
pathPrefix?: string;
|
|
1444
|
-
/**
|
|
1445
|
-
* Optional custom fetch implementation. Defaults to `globalThis.fetch`.
|
|
1446
|
-
* Provided so callers can inject retries / auth / instrumentation.
|
|
1447
|
-
*/
|
|
1448
|
-
fetch?: typeof fetch;
|
|
1449
|
-
/**
|
|
1450
|
-
* Optional offline byte cache. When present, GETs are served cache-first and
|
|
1451
|
-
* populated on success, and PUTs write the cache before the network so a
|
|
1452
|
-
* just-uploaded file reads back offline. See {@link ShardByteCache}.
|
|
1453
|
-
*/
|
|
1454
|
-
cache?: ShardByteCache;
|
|
1455
|
-
/**
|
|
1456
|
-
* Coalesce concurrent origin reads into `POST {pathPrefix}/batch`. On by
|
|
1457
|
-
* default: it is what keeps opening a project from costing one request per
|
|
1458
|
-
* shard. Set false to force one-at-a-time — useful against a server without
|
|
1459
|
-
* the route, though that is detected and handled automatically.
|
|
1460
|
-
*/
|
|
1461
|
-
batchShards?: boolean;
|
|
1462
|
-
/**
|
|
1463
|
-
* Called when a PUT can't reach the network (offline). Lets the caller queue
|
|
1464
|
-
* the upload for replay on reconnect; the bytes are already in {@link cache}
|
|
1465
|
-
* keyed by `hash`, so only the hash needs recording.
|
|
1466
|
-
*/
|
|
1467
|
-
deferUpload?: (hash: string) => Promise<void>;
|
|
1468
|
-
/**
|
|
1469
|
-
* Optional peer block source. When present, GETs try peers between the local
|
|
1470
|
-
* cache and origin (peer blocks are verified by hash); PUTs announce the new
|
|
1471
|
-
* block to peers. Best-effort — any failure falls through to origin.
|
|
1472
|
-
*/
|
|
1473
|
-
peers?: PeerBlockSource;
|
|
1474
|
-
/**
|
|
1475
|
-
* When true, {@link ShardClient.putShard} returns as soon as the shard is
|
|
1476
|
-
* written to {@link cache} and announced to {@link peers} — the origin PUT is
|
|
1477
|
-
* fired in the BACKGROUND rather than awaited. This makes a large write's
|
|
1478
|
-
* manifest available immediately (so a peer/worker can start pulling the
|
|
1479
|
-
* shards over P2P right away) while origin catches up as a durable fallback.
|
|
1480
|
-
* On a background-PUT failure the shard is handed to {@link deferUpload} (if
|
|
1481
|
-
* present) for replay. Requires a {@link cache} so the bytes survive locally
|
|
1482
|
-
* until the background upload lands. No-op without a cache.
|
|
1483
|
-
*/
|
|
1484
|
-
backgroundUpload?: boolean;
|
|
1485
|
-
}
|
|
1960
|
+
declare function loadDeviceIdentity(): Promise<PersistedDeviceIdentity | null>;
|
|
1486
1961
|
/**
|
|
1487
|
-
*
|
|
1488
|
-
*
|
|
1962
|
+
* Wipe everything: the sealed blob **and** the whole key database — so the
|
|
1963
|
+
* wrapping key never outlives the ciphertext it opens, and the device identity
|
|
1964
|
+
* key is regenerated (a re-pair is then correctly treated as a new device).
|
|
1489
1965
|
*/
|
|
1490
|
-
declare function
|
|
1491
|
-
declare class ShardClient {
|
|
1492
|
-
private readonly baseUrl;
|
|
1493
|
-
private readonly pathPrefix;
|
|
1494
|
-
private readonly fetchFn;
|
|
1495
|
-
private readonly cache?;
|
|
1496
|
-
/** Hashes waiting for the next batch, each with everyone awaiting it. */
|
|
1497
|
-
private pendingShards;
|
|
1498
|
-
private batchTimer;
|
|
1499
|
-
/** False once the server is known not to have the batch route. */
|
|
1500
|
-
private batchSupported;
|
|
1501
|
-
private readonly deferUpload?;
|
|
1502
|
-
private readonly peers?;
|
|
1503
|
-
private readonly backgroundUpload;
|
|
1504
|
-
constructor(opts: ShardClientOptions);
|
|
1505
|
-
/** Build the URL for a given shard hash — exposed for callers that want to log or pre-resolve. */
|
|
1506
|
-
urlForHash(hash: string): string;
|
|
1507
|
-
/**
|
|
1508
|
-
* Upload a shard. The server verifies SHA-256(body) matches `:hash` and
|
|
1509
|
-
* rejects with 400 if not, so the caller is the source of truth on
|
|
1510
|
-
* naming. Re-uploading an existing shard is idempotent — the server
|
|
1511
|
-
* returns `dedup: true` in that case.
|
|
1512
|
-
*/
|
|
1513
|
-
putShard(hash: string, bytes: Uint8Array): Promise<{
|
|
1514
|
-
dedup: boolean;
|
|
1515
|
-
}>;
|
|
1516
|
-
private bgActive;
|
|
1517
|
-
private readonly bgQueue;
|
|
1518
|
-
private static readonly BG_CONCURRENCY;
|
|
1519
|
-
/** Queue a shard's origin PUT to run in the background under a concurrency
|
|
1520
|
-
* cap. Bytes are re-read from {@link cache} at upload time (not held in the
|
|
1521
|
-
* queue) so a large file's backlog doesn't pin a second copy in memory. */
|
|
1522
|
-
private enqueueBackgroundUpload;
|
|
1523
|
-
/** PUT the shard bytes to origin, verifying the server accepted them.
|
|
1524
|
-
* Used by the background-upload path; throws on network/HTTP failure. */
|
|
1525
|
-
private uploadToOrigin;
|
|
1526
|
-
/**
|
|
1527
|
-
* Fetch a shard by hash. Throws if the server returns a non-200; returns
|
|
1528
|
-
* `null` for 404 so callers can distinguish "missing" from "errored".
|
|
1529
|
-
*/
|
|
1530
|
-
getShard(hash: string): Promise<Uint8Array | null>;
|
|
1531
|
-
/**
|
|
1532
|
-
* Fetch from origin, COALESCING concurrent requests into one batch.
|
|
1533
|
-
*
|
|
1534
|
-
* A shard is not a file. A project is thousands of small source modules,
|
|
1535
|
-
* each split into shards, so reading one at a time made opening a project
|
|
1536
|
-
* cost thousands of requests — 2,086 for a 519-file project. Latency, not
|
|
1537
|
-
* bytes, is what that spends: Chromium took 18.4s over those requests and
|
|
1538
|
-
* Brave, which filters every one, took 37.5s for identical work. Batching
|
|
1539
|
-
* collapses whatever is in flight at the same moment into a single POST,
|
|
1540
|
-
* which is why it helps most exactly where it hurt most.
|
|
1541
|
-
*
|
|
1542
|
-
* Callers are unchanged: `getShard` still asks for one shard and gets one
|
|
1543
|
-
* shard. The window is a few milliseconds — long enough to gather the reads
|
|
1544
|
-
* a concurrent walk issues together, short enough not to be felt by a lone
|
|
1545
|
-
* read.
|
|
1546
|
-
*
|
|
1547
|
-
* Falls back permanently to one-at-a-time if the server has no batch route,
|
|
1548
|
-
* so a new SDK keeps working against an older deployment.
|
|
1549
|
-
*/
|
|
1550
|
-
private fetchFromOrigin;
|
|
1551
|
-
/** Send one batch of whatever has accumulated, and settle those callers. */
|
|
1552
|
-
private flushShardBatch;
|
|
1553
|
-
/** Fetch a shard from origin. Returns `null` for 404 (or offline when a
|
|
1554
|
-
* cache is present, so RS recovery can proceed); throws otherwise. */
|
|
1555
|
-
private fetchOneFromOrigin;
|
|
1556
|
-
/**
|
|
1557
|
-
* Check whether a shard exists. Lets the caller skip a PUT they'd otherwise
|
|
1558
|
-
* dedup-rewrite. The server can implement this as a HEAD or return a small
|
|
1559
|
-
* JSON body — we don't depend on either.
|
|
1560
|
-
*/
|
|
1561
|
-
hasShard(hash: string): Promise<boolean>;
|
|
1562
|
-
}
|
|
1966
|
+
declare function clearDeviceIdentity(): Promise<void>;
|
|
1563
1967
|
|
|
1564
1968
|
/**
|
|
1565
|
-
*
|
|
1566
|
-
*
|
|
1567
|
-
*
|
|
1568
|
-
*
|
|
1569
|
-
*
|
|
1570
|
-
*
|
|
1571
|
-
*
|
|
1969
|
+
* Passkey factor (M1.1) — WebAuthn with the **PRF extension**.
|
|
1970
|
+
*
|
|
1971
|
+
* A passkey becomes a device-bound, platform-synced source of a stable secret:
|
|
1972
|
+
* for a fixed salt, the authenticator's PRF returns the same 32 bytes, which we
|
|
1973
|
+
* HKDF into an AES key that wraps the master seed. The server never sees the
|
|
1974
|
+
* passkey — it's purely a local key source; the ZK proof remains the server auth.
|
|
1975
|
+
*
|
|
1976
|
+
* BROWSER ONLY (`navigator.credentials`). The PRF extension fields aren't in the
|
|
1977
|
+
* standard DOM types yet, so they're reached through narrow casts.
|
|
1978
|
+
*/
|
|
1979
|
+
/** Whether this environment can do WebAuthn at all. (PRF support is checked at enroll.) */
|
|
1980
|
+
declare function passkeySupported(): boolean;
|
|
1981
|
+
/**
|
|
1982
|
+
* Whether this browser's authenticator can do the **PRF extension** our passkey
|
|
1983
|
+
* factor needs. A browser can save passkeys (e.g. for password autofill) yet not
|
|
1984
|
+
* expose PRF — PRF is what derives the stable secret we wrap the seed under, so a
|
|
1985
|
+
* passkey without it is useless to us.
|
|
1986
|
+
*
|
|
1987
|
+
* Uses `PublicKeyCredential.getClientCapabilities()` (the `extension:prf` cap)
|
|
1988
|
+
* where available. Returns:
|
|
1989
|
+
* - `true` — PRF is supported,
|
|
1990
|
+
* - `false` — definitively NOT supported (hide the option),
|
|
1991
|
+
* - `null` — couldn't determine (older browser without the capabilities API);
|
|
1992
|
+
* the caller may optimistically allow and let enroll surface a real
|
|
1993
|
+
* error if it turns out PRF isn't there.
|
|
1994
|
+
*/
|
|
1995
|
+
declare function passkeyPrfCapable(): Promise<boolean | null>;
|
|
1996
|
+
/** Best-effort registrable RP id — the host. Apps spanning subdomains should pass
|
|
1997
|
+
* the parent domain (e.g. "muhkoo.dev") explicitly. */
|
|
1998
|
+
declare function defaultRpId(): string;
|
|
1999
|
+
/**
|
|
2000
|
+
* Can a passkey enrolled under `rpId` be used from the CURRENT origin?
|
|
1572
2001
|
*
|
|
1573
|
-
*
|
|
1574
|
-
*
|
|
1575
|
-
*
|
|
1576
|
-
*
|
|
1577
|
-
*
|
|
2002
|
+
* WebAuthn only accepts an RP id that is the origin's own host or a registrable
|
|
2003
|
+
* *parent* of it — so a passkey enrolled on `app.example.com` is unusable on
|
|
2004
|
+
* `app.example.net`, and one enrolled on a full host is unusable from a sibling
|
|
2005
|
+
* host. An app served on BOTH a custom domain and the platform host therefore
|
|
2006
|
+
* needs one passkey per origin.
|
|
1578
2007
|
*
|
|
1579
|
-
*
|
|
1580
|
-
*
|
|
1581
|
-
*
|
|
1582
|
-
*
|
|
1583
|
-
* DELETE /api/spaces/:spaceId/files/:fileId → { ok, existed }
|
|
2008
|
+
* Call this BEFORE `navigator.credentials.get()`: passing a foreign rpId throws
|
|
2009
|
+
* a raw "The requested RPID did not match the origin or related origins", which
|
|
2010
|
+
* is a dead end for the user. Checking first lets the caller fall back to normal
|
|
2011
|
+
* sign-in (and offer to enroll a passkey for this origin) instead.
|
|
1584
2012
|
*/
|
|
1585
|
-
|
|
1586
|
-
interface SharedSpaceClientOptions {
|
|
1587
|
-
/** Base URL of the accelerator (e.g. `https://api.muhkoo.dev`). */
|
|
1588
|
-
baseUrl: string;
|
|
1589
|
-
/**
|
|
1590
|
-
* Credential-stamping fetch — pass {@link HttpClient.fetch} so requests
|
|
1591
|
-
* carry the app key + session token. Defaults to `globalThis.fetch`
|
|
1592
|
-
* (unauthenticated; only useful in tests).
|
|
1593
|
-
*/
|
|
1594
|
-
fetch?: typeof fetch;
|
|
1595
|
-
}
|
|
1596
|
-
declare class SharedSpaceClient {
|
|
1597
|
-
private readonly baseUrl;
|
|
1598
|
-
private readonly fetchFn;
|
|
1599
|
-
constructor(opts: SharedSpaceClientOptions);
|
|
1600
|
-
/** Write a file manifest into the space. Requires space membership. */
|
|
1601
|
-
writeFileManifest(spaceId: string, manifest: FileManifest): Promise<void>;
|
|
1602
|
-
/** Read a file manifest by id. Requires space membership. */
|
|
1603
|
-
readFileManifest(spaceId: string, fileId: string): Promise<FileManifest>;
|
|
1604
|
-
/** Delete a file manifest by id. Requires space membership. */
|
|
1605
|
-
deleteFileManifest(spaceId: string, fileId: string): Promise<boolean>;
|
|
1606
|
-
/** List file summaries in the space. Requires space membership. */
|
|
1607
|
-
listFiles(spaceId: string): Promise<FileStat[]>;
|
|
1608
|
-
private filesBase;
|
|
1609
|
-
private jsonRequest;
|
|
1610
|
-
}
|
|
1611
|
-
|
|
2013
|
+
declare function rpIdUsableForOrigin(rpId: string | undefined | null, host?: string): boolean;
|
|
1612
2014
|
/**
|
|
1613
|
-
*
|
|
1614
|
-
*
|
|
2015
|
+
* Would {@link AuthNamespace.loginWithPasskey} accept a factor recorded with
|
|
2016
|
+
* this `rpId`, from `host`?
|
|
1615
2017
|
*
|
|
1616
|
-
*
|
|
1617
|
-
*
|
|
1618
|
-
*
|
|
1619
|
-
*
|
|
1620
|
-
* b. Encrypt the chunk → ciphertext (plaintext.length + 16 bytes tag).
|
|
1621
|
-
* c. Reed–Solomon encode the ciphertext into `dataShards + parityShards`
|
|
1622
|
-
* equal-length shards (`ReedSolomonCodec`).
|
|
1623
|
-
* d. PUT every shard to the content-addressed shard store, keyed by
|
|
1624
|
-
* SHA-256 of the shard bytes (`ShardClient`).
|
|
1625
|
-
* 3. Build a `FileManifest` listing every chunk's keys, IVs, sizes, and
|
|
1626
|
-
* the SHA-256 of every shard in order.
|
|
2018
|
+
* This is deliberately NOT the same as {@link rpIdUsableForOrigin}. That answers
|
|
2019
|
+
* a raw WebAuthn question and is `false` for a missing rpId. But a factor with
|
|
2020
|
+
* NO recorded rpId predates per-origin enrolment, and `loginWithPasskey`
|
|
2021
|
+
* resolves it as `params.rpId ?? host` — so it IS usable, from anywhere.
|
|
1627
2022
|
*
|
|
1628
|
-
*
|
|
1629
|
-
*
|
|
1630
|
-
*
|
|
1631
|
-
* it as a chat message). The chat application takes this path.
|
|
1632
|
-
* - {@link FileStorage.writeFile} additionally POSTs the manifest to a
|
|
1633
|
-
* gated multi-user space (`SharedSpaceClient`) so any participant of the
|
|
1634
|
-
* space can read the file by manifest id later.
|
|
2023
|
+
* Callers deciding "should I even try a passkey here?" must use this one.
|
|
2024
|
+
* Reaching for `rpIdUsableForOrigin` instead silently excludes every legacy
|
|
2025
|
+
* passkey holder, which has locked real users out of production twice.
|
|
1635
2026
|
*
|
|
1636
|
-
*
|
|
1637
|
-
*
|
|
1638
|
-
* fetches the manifest from the space.
|
|
2027
|
+
* The `?? host` fallback applies to nullish only, matching `loginWithPasskey`:
|
|
2028
|
+
* an empty-string rpId stays unusable rather than resolving to the host.
|
|
1639
2029
|
*/
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
* Optional — only required by {@link FileStorage.writeFile} /
|
|
1647
|
-
* {@link FileStorage.readFile} / {@link FileStorage.deleteFile} /
|
|
1648
|
-
* {@link FileStorage.listFiles} / {@link FileStorage.readManifest}.
|
|
1649
|
-
*
|
|
1650
|
-
* Callers that ship the manifest themselves (e.g. via an encrypted chat
|
|
1651
|
-
* message) can omit `space` and use the `*FromShards` / `*ToShards`
|
|
1652
|
-
* methods directly.
|
|
1653
|
-
*/
|
|
1654
|
-
space?: SharedSpaceClient;
|
|
1655
|
-
/**
|
|
1656
|
-
* Plaintext chunk size in bytes. After encryption each chunk gains 16
|
|
1657
|
-
* bytes (GCM tag); after RS encoding the size is rounded up to a multiple
|
|
1658
|
-
* of `dataShards` per chunk. Default 4 MiB — a balance between
|
|
1659
|
-
* round-trip cost and parallelism on the upload path.
|
|
1660
|
-
*/
|
|
1661
|
-
chunkSize?: number;
|
|
1662
|
-
/** RS data shards per chunk (default 4). */
|
|
1663
|
-
dataShards?: number;
|
|
1664
|
-
/** RS parity shards per chunk (default 2). */
|
|
1665
|
-
parityShards?: number;
|
|
1666
|
-
/**
|
|
1667
|
-
* Cap on concurrent shard uploads / downloads. Tune for the network. The
|
|
1668
|
-
* default of 8 saturates most home connections without overwhelming the
|
|
1669
|
-
* server's per-IP rate limiter.
|
|
1670
|
-
*/
|
|
1671
|
-
concurrency?: number;
|
|
1672
|
-
/**
|
|
1673
|
-
* Optional pre-compiled RS WebAssembly module. Defaults to the bundled
|
|
1674
|
-
* loader. Pass this in environments where the bundled loader path isn't
|
|
1675
|
-
* available (vitest, deploy-time-precompiled CF Workers).
|
|
1676
|
-
*/
|
|
1677
|
-
rsWasmModule?: WebAssembly.Module;
|
|
2030
|
+
declare function passkeyUsableFromOrigin(rpId: string | undefined | null, host?: string): boolean;
|
|
2031
|
+
/** Thrown when the account's passkey belongs to a different origin. Callers
|
|
2032
|
+
* should treat this as "no passkey here" and fall back to another factor. */
|
|
2033
|
+
declare class PasskeyOriginError extends Error {
|
|
2034
|
+
readonly code = "passkey_wrong_origin";
|
|
2035
|
+
constructor(message?: string);
|
|
1678
2036
|
}
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
interface WriteFileInput extends WriteToShardsInput {
|
|
1684
|
-
/** Space to write the manifest to — must allow the caller `write`. */
|
|
1685
|
-
spaceId: string;
|
|
2037
|
+
interface PasskeyEnrollment {
|
|
2038
|
+
credentialId: Uint8Array;
|
|
2039
|
+
prfSalt: Uint8Array;
|
|
2040
|
+
prfOutput: Uint8Array;
|
|
1686
2041
|
}
|
|
1687
2042
|
/**
|
|
1688
|
-
*
|
|
1689
|
-
*
|
|
2043
|
+
* Create a passkey with PRF and return its credential id, a fresh PRF salt, and
|
|
2044
|
+
* the PRF output for that salt. Throws if the authenticator can't do PRF.
|
|
1690
2045
|
*/
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
path?: string;
|
|
1699
|
-
lastModified?: number;
|
|
1700
|
-
};
|
|
1701
|
-
/**
|
|
1702
|
-
* Optional pre-allocated file id. Defaults to a fresh one. Useful for
|
|
1703
|
-
* resumable uploads: the same id always maps to the same manifest slot.
|
|
1704
|
-
*/
|
|
1705
|
-
fileId?: string;
|
|
1706
|
-
/**
|
|
1707
|
-
* Optional upload-progress callback, invoked after each chunk finishes
|
|
1708
|
-
* (`completed` of `total` chunks). Lets a UI show a progress bar. Called once
|
|
1709
|
-
* with `(0, total)` before the first chunk so a bar can render immediately.
|
|
1710
|
-
*/
|
|
1711
|
-
onProgress?: (completed: number, total: number) => void;
|
|
1712
|
-
}
|
|
1713
|
-
declare class FileStorage {
|
|
1714
|
-
private readonly space;
|
|
1715
|
-
private readonly shards;
|
|
1716
|
-
private readonly chunkSize;
|
|
1717
|
-
private readonly dataShards;
|
|
1718
|
-
private readonly parityShards;
|
|
1719
|
-
private readonly concurrency;
|
|
1720
|
-
private readonly cipher;
|
|
1721
|
-
private readonly codec;
|
|
1722
|
-
private readonly rsWasmModule?;
|
|
1723
|
-
constructor(opts: FileStorageOptions);
|
|
1724
|
-
/**
|
|
1725
|
-
* Chunk → encrypt → encode → upload-shards. Returns the resulting manifest
|
|
1726
|
-
* along with a `FileStat` summary. The caller is responsible for storing
|
|
1727
|
-
* or transmitting the manifest however it likes.
|
|
1728
|
-
*/
|
|
1729
|
-
writeFileToShards(input: WriteToShardsInput): Promise<{
|
|
1730
|
-
manifest: FileManifest;
|
|
1731
|
-
stat: FileStat;
|
|
1732
|
-
}>;
|
|
1733
|
-
/**
|
|
1734
|
-
* Inverse of {@link writeFileToShards}. Takes a manifest the caller
|
|
1735
|
-
* obtained somehow (chat message, share link, etc.) and reassembles the
|
|
1736
|
-
* plaintext. Throws if too many shards are missing or any chunk's auth
|
|
1737
|
-
* tag fails.
|
|
1738
|
-
*/
|
|
1739
|
-
readFileFromShards(manifest: FileManifest): Promise<{
|
|
1740
|
-
data: Uint8Array;
|
|
1741
|
-
stat: FileStat;
|
|
1742
|
-
}>;
|
|
1743
|
-
/**
|
|
1744
|
-
* Streaming inverse of {@link writeFileToShards}: yields each chunk's
|
|
1745
|
-
* decrypted plaintext in order WITHOUT concatenating. Peak memory stays at
|
|
1746
|
-
* ~one chunk, so multi-GB files (which overflow a single `Uint8Array`) can be
|
|
1747
|
-
* piped straight to a file/socket. Same shard sourcing (cache → peers →
|
|
1748
|
-
* origin) and per-chunk auth as {@link readFileFromShards}.
|
|
1749
|
-
*/
|
|
1750
|
-
readChunksFromShards(manifest: FileManifest): AsyncGenerator<Uint8Array>;
|
|
1751
|
-
/** As {@link writeFileToShards}, plus POSTs the manifest to `input.spaceId`. */
|
|
1752
|
-
writeFile(input: WriteFileInput): Promise<FileStat>;
|
|
1753
|
-
/** As {@link readFileFromShards}, but fetches the manifest from a space first. */
|
|
1754
|
-
readFile(spaceId: string, fileId: string): Promise<{
|
|
1755
|
-
data: Uint8Array;
|
|
1756
|
-
stat: FileStat;
|
|
1757
|
-
}>;
|
|
1758
|
-
/**
|
|
1759
|
-
* Delete the manifest from a space. **Does not GC the shards** — those
|
|
1760
|
-
* are content-addressed and may be referenced by other manifests.
|
|
1761
|
-
* Garbage collection is a separate server-side concern (Phase D / D-ish).
|
|
1762
|
-
*/
|
|
1763
|
-
deleteFile(spaceId: string, fileId: string): Promise<boolean>;
|
|
1764
|
-
/** Convenience pass-through. */
|
|
1765
|
-
listFiles(spaceId: string): Promise<FileStat[]>;
|
|
1766
|
-
/** Read a manifest verbatim — useful for "share file" / export flows. */
|
|
1767
|
-
readManifest(spaceId: string, fileId: string): Promise<FileManifest>;
|
|
1768
|
-
private requireSpace;
|
|
1769
|
-
/** Encrypt + encode + upload one chunk; return its manifest entry. */
|
|
1770
|
-
private writeChunk;
|
|
1771
|
-
/** Inverse of `writeChunk`. */
|
|
1772
|
-
private readChunk;
|
|
1773
|
-
/** Run `tasks` with at most `this.concurrency` in flight. */
|
|
1774
|
-
private runWithConcurrency;
|
|
1775
|
-
}
|
|
2046
|
+
declare function createPasskeyWithPrf(opts: {
|
|
2047
|
+
rpId: string;
|
|
2048
|
+
rpName: string;
|
|
2049
|
+
username: string;
|
|
2050
|
+
}): Promise<PasskeyEnrollment>;
|
|
2051
|
+
/** Evaluate a passkey's PRF for `salt` → 32 bytes. Used at recover/login. */
|
|
2052
|
+
declare function evaluatePasskeyPrf(rpId: string, credentialId: Uint8Array, salt: Uint8Array): Promise<Uint8Array>;
|
|
1776
2053
|
|
|
1777
2054
|
/**
|
|
1778
|
-
*
|
|
2055
|
+
* `PersonalSpaceClient` — the client-side counterpart to the accelerator's
|
|
2056
|
+
* `PersonalSpaceDO`. Wraps the `/api/personal/:commitment/*` HTTP protocol so
|
|
2057
|
+
* callers can `put` / `get` / `delete` / `list` per-user KV blobs without
|
|
2058
|
+
* thinking about challenges, nonces, or Groth16 proof generation.
|
|
2059
|
+
*
|
|
2060
|
+
* Every gated operation:
|
|
2061
|
+
* 1. Requests a one-shot challenge from the DO (`POST /challenge`).
|
|
2062
|
+
* 2. Converts the returned hex nonce into a BN254 field element.
|
|
2063
|
+
* 3. Generates a fresh Groth16 proof for the `preimagePoK` circuit using
|
|
2064
|
+
* snarkjs, with the user's secret/salt/ecdsaPub as private inputs and
|
|
2065
|
+
* `{commitment, nonce, ecdsaPubHash}` as public signals.
|
|
2066
|
+
* 4. POSTs the proof + publicSignals + (optional) value to the gated endpoint.
|
|
1779
2067
|
*
|
|
1780
|
-
*
|
|
1781
|
-
*
|
|
1782
|
-
*
|
|
2068
|
+
* snarkjs is loaded lazily (dynamic `import`) the first time a proof is
|
|
2069
|
+
* generated — it's an optional peer dependency that only the ZK-proof path
|
|
2070
|
+
* needs, so importing this module (or constructing a `Client`) never requires
|
|
2071
|
+
* it. Consumers using the proof path install `snarkjs`, or provide it via an
|
|
2072
|
+
* import map in the browser (see the README).
|
|
1783
2073
|
*
|
|
1784
|
-
*
|
|
1785
|
-
*
|
|
1786
|
-
*
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
*
|
|
2074
|
+
* Encryption of the stored values is the caller's responsibility — the DO
|
|
2075
|
+
* sees only opaque JSON. The companion `wrap.ts` helpers in this folder are
|
|
2076
|
+
* a convenient way to passphrase-encrypt a payload before putting it.
|
|
2077
|
+
*/
|
|
2078
|
+
|
|
2079
|
+
/**
|
|
2080
|
+
* Constructor options for {@link PersonalSpaceClient}.
|
|
1791
2081
|
*
|
|
1792
|
-
*
|
|
1793
|
-
*
|
|
1794
|
-
*
|
|
1795
|
-
* (the file manifest, in this codebase).
|
|
2082
|
+
* All of `secret`, `salt`, `ecdsaPub`, and `ecdsaPubHash` are decimal-encoded
|
|
2083
|
+
* BigInt strings (snarkjs convention). `commitment` is likewise decimal — it's
|
|
2084
|
+
* the Poseidon hash of `(secret, salt, ecdsaPubHash)`.
|
|
1796
2085
|
*/
|
|
1797
|
-
interface
|
|
1798
|
-
/**
|
|
1799
|
-
|
|
1800
|
-
/**
|
|
1801
|
-
|
|
1802
|
-
/**
|
|
1803
|
-
|
|
1804
|
-
/**
|
|
1805
|
-
|
|
1806
|
-
/**
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
2086
|
+
interface PersonalSpaceClientOptions {
|
|
2087
|
+
/** Base URL of the accelerator, e.g. `http://localhost:8787`. */
|
|
2088
|
+
baseUrl: string;
|
|
2089
|
+
/** The user's Poseidon commitment as a decimal string (~78 digits). */
|
|
2090
|
+
commitment: string;
|
|
2091
|
+
/** ZK identity material — private inputs to the proof. Decimal BigInt string. */
|
|
2092
|
+
secret: string;
|
|
2093
|
+
/** ZK identity salt. Decimal BigInt string. */
|
|
2094
|
+
salt: string;
|
|
2095
|
+
/** Raw ECDSA public-key field representation. Decimal BigInt string. */
|
|
2096
|
+
ecdsaPub: string;
|
|
2097
|
+
/** Poseidon hash of `ecdsaPub`, used as a public signal. Decimal BigInt string. */
|
|
2098
|
+
ecdsaPubHash: string;
|
|
1810
2099
|
/**
|
|
1811
|
-
*
|
|
1812
|
-
*
|
|
1813
|
-
*
|
|
1814
|
-
*
|
|
1815
|
-
*
|
|
1816
|
-
* base64-inlined wasm — works in Node, browsers, and Workers.
|
|
2100
|
+
* Where the runtime can fetch the compiled circuit assets, and the digests
|
|
2101
|
+
* to verify them against. Prefer `defaultCircuitUrls(baseUrl)`, which pins
|
|
2102
|
+
* them — this client passes `secret` and `salt` to the witness generator as
|
|
2103
|
+
* private witnesses, so an unverified one can exfiltrate them. See
|
|
2104
|
+
* `src/auth/circuitIntegrity.ts`.
|
|
1817
2105
|
*/
|
|
1818
|
-
|
|
2106
|
+
circuits: CircuitIntegrity & {
|
|
2107
|
+
/** URL of the `preimagePoK.wasm` witness generator. */
|
|
2108
|
+
wasmUrl: string;
|
|
2109
|
+
/** URL of the `preimagePoK_0001.zkey` proving key. */
|
|
2110
|
+
zkeyUrl: string;
|
|
2111
|
+
};
|
|
2112
|
+
}
|
|
2113
|
+
declare class PersonalSpaceClient {
|
|
2114
|
+
private readonly baseUrl;
|
|
2115
|
+
private readonly commitment;
|
|
2116
|
+
private readonly secret;
|
|
2117
|
+
private readonly salt;
|
|
2118
|
+
private readonly ecdsaPub;
|
|
2119
|
+
private readonly ecdsaPubHash;
|
|
2120
|
+
private readonly circuits;
|
|
2121
|
+
constructor(opts: PersonalSpaceClientOptions);
|
|
2122
|
+
/** Store `value` under `key`, replacing any prior value. */
|
|
2123
|
+
put(key: string, value: unknown): Promise<void>;
|
|
2124
|
+
/** Fetch the value stored under `key`. Returns `null` if absent. */
|
|
2125
|
+
get<T = unknown>(key: string): Promise<T | null>;
|
|
2126
|
+
/** Remove `key` from the user's space. Returns whether the key existed. */
|
|
2127
|
+
delete(key: string): Promise<boolean>;
|
|
2128
|
+
/** List every key currently stored in the user's space. */
|
|
2129
|
+
list(): Promise<string[]>;
|
|
1819
2130
|
/**
|
|
1820
|
-
*
|
|
1821
|
-
*
|
|
2131
|
+
* Run the full pre-flight: ask the DO for a challenge, reduce the nonce to
|
|
2132
|
+
* a field element, and generate a fresh Groth16 proof. Returns everything
|
|
2133
|
+
* the gated endpoints need in their body.
|
|
1822
2134
|
*/
|
|
1823
|
-
|
|
2135
|
+
private proveFreshChallenge;
|
|
2136
|
+
/** Hit `POST /challenge` and return the issued nonce + ID. */
|
|
2137
|
+
private requestChallenge;
|
|
1824
2138
|
/**
|
|
1825
|
-
*
|
|
1826
|
-
*
|
|
1827
|
-
*
|
|
1828
|
-
* `deadShardIndexes`.
|
|
1829
|
-
*
|
|
1830
|
-
* `originalSize` is required so the trailing zero-padding can be trimmed.
|
|
2139
|
+
* Thin wrapper over `globalThis.fetch` so all methods share a single set of
|
|
2140
|
+
* headers and body-encoding behaviour. `globalThis.fetch` is available in
|
|
2141
|
+
* modern browsers, Node 18+, Workers, and Bun.
|
|
1831
2142
|
*/
|
|
1832
|
-
|
|
2143
|
+
private fetch;
|
|
1833
2144
|
}
|
|
1834
2145
|
|
|
1835
2146
|
/**
|
|
1836
|
-
*
|
|
1837
|
-
* Reed–Solomon erasure-coding library compiled with `wasm-pack --target nodejs`).
|
|
1838
|
-
*
|
|
1839
|
-
* The upstream npm package ships `wasm_reed_solomon_erasure.js` which uses
|
|
1840
|
-
* `require('fs').readFileSync()` to load the WASM file at module load time —
|
|
1841
|
-
* that hard-codes Node and breaks browsers / CF Workers. This file is a port
|
|
1842
|
-
* of that glue with the Node-only pieces removed:
|
|
1843
|
-
*
|
|
1844
|
-
* - `TextDecoder` is taken from the global (available in Node 12+, every
|
|
1845
|
-
* modern browser, and Workers — no `require('util')`).
|
|
1846
|
-
* - The WASM module is loaded via the rollup-inlined `.wasm` import, the
|
|
1847
|
-
* same `targetEnv: 'auto-inline'` path the bn128 verifier uses. Works in
|
|
1848
|
-
* Node, browser, and Workers without runtime fs access.
|
|
1849
|
-
*
|
|
1850
|
-
* Only the two public functions consumers actually need (`encode` and
|
|
1851
|
-
* `reconstruct`) are exported. The rest is the wbindgen heap-table dance.
|
|
1852
|
-
*
|
|
1853
|
-
* Encoding:
|
|
1854
|
-
* `encode(dataShards: Uint8Array[], parityShard: number)` → array of
|
|
1855
|
-
* `dataShards.length + parityShard` Uint8Arrays. The first N are the input
|
|
1856
|
-
* shards unchanged; the trailing M are the freshly computed parity shards.
|
|
1857
|
-
* All shards are the same length — caller is responsible for padding the
|
|
1858
|
-
* data into equal-size data shards.
|
|
2147
|
+
* Wire types for the storage layer.
|
|
1859
2148
|
*
|
|
1860
|
-
*
|
|
1861
|
-
*
|
|
1862
|
-
*
|
|
1863
|
-
*
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
* Load and instantiate the WASM module. Idempotent — repeated calls await the
|
|
1867
|
-
* same one-shot promise.
|
|
2149
|
+
* The `FileManifest` is the single piece of metadata the system needs to
|
|
2150
|
+
* reassemble a file: per-chunk encryption material, RS parameters, and the
|
|
2151
|
+
* SHA-256 content hashes of every shard that belongs to the chunk. With the
|
|
2152
|
+
* manifest in hand, a reader can pull each shard from the open content-
|
|
2153
|
+
* addressed shard store, decode, decrypt, and concatenate to recover the
|
|
2154
|
+
* original bytes.
|
|
1868
2155
|
*
|
|
1869
|
-
*
|
|
1870
|
-
*
|
|
1871
|
-
*
|
|
1872
|
-
*
|
|
1873
|
-
* 2. `await initRsWasm(myWasmModule)` — accepts a pre-compiled
|
|
1874
|
-
* `WebAssembly.Module`. Useful when the bundled loader path isn't
|
|
1875
|
-
* available (vitest, CF Workers wanting deploy-time precompile, etc.).
|
|
2156
|
+
* The manifest itself lives inside a `SharedSpaceDO` (Phase B) which gates
|
|
2157
|
+
* access by ZK proof + ACL. Shards are stored in the open content-addressed
|
|
2158
|
+
* `ShardStoreDO` — anyone can put/get any shard, but without the manifest
|
|
2159
|
+
* (which has the keys) the ciphertext is just noise.
|
|
1876
2160
|
*/
|
|
1877
|
-
declare function initRsWasm(wasmModule?: WebAssembly.Module): Promise<void>;
|
|
1878
2161
|
|
|
1879
2162
|
/**
|
|
1880
|
-
*
|
|
1881
|
-
*
|
|
1882
|
-
* Owns the wire shapes for register / challenge / authenticate / verify so
|
|
1883
|
-
* the React SPA (and any future consumers) don't reimplement them. The ZK
|
|
1884
|
-
* identity derivation + Groth16 proof generation stay on the application
|
|
1885
|
-
* side — this client handles only the round-trips.
|
|
2163
|
+
* One chunk's metadata. A file is the concatenation of `chunks` in order.
|
|
1886
2164
|
*
|
|
1887
|
-
*
|
|
1888
|
-
*
|
|
1889
|
-
*
|
|
2165
|
+
* - `originalSize` is the chunk's plaintext length, before AES-GCM expanded
|
|
2166
|
+
* it by 16 bytes for the auth tag and before RS padded it for shard
|
|
2167
|
+
* alignment.
|
|
2168
|
+
* - `ciphertextSize` is the AES-GCM output length (plaintext + 16). This is
|
|
2169
|
+
* what the RS decoder trims back to.
|
|
2170
|
+
* - `shardHashes` lists every shard's SHA-256 in encoding order — the first
|
|
2171
|
+
* `dataShards` are data shards, the trailing `parityShards` are parity.
|
|
1890
2172
|
*/
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
/**
|
|
1895
|
-
|
|
1896
|
-
/**
|
|
1897
|
-
|
|
1898
|
-
/**
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
commitment: string;
|
|
1911
|
-
}
|
|
1912
|
-
interface ZkAuthenticateRequest {
|
|
1913
|
-
challengeId: string;
|
|
1914
|
-
proof: {
|
|
1915
|
-
commitment: string;
|
|
1916
|
-
/** Original hex nonce from the challenge (NOT the field-reduced version). */
|
|
1917
|
-
nonce: string;
|
|
1918
|
-
response: {
|
|
1919
|
-
proof: Groth16Proof;
|
|
1920
|
-
publicSignals: string[];
|
|
1921
|
-
};
|
|
1922
|
-
/** Base64 ECDSA-SHA256 signature over `JSON.stringify(response.proof)`. */
|
|
1923
|
-
signature: string;
|
|
1924
|
-
};
|
|
1925
|
-
rememberMe?: boolean;
|
|
2173
|
+
interface ChunkManifest {
|
|
2174
|
+
/** Stable per-chunk ID (uuid-like). Useful for client-side resume / dedup. */
|
|
2175
|
+
id: string;
|
|
2176
|
+
/** Position of this chunk in the file (0-indexed). */
|
|
2177
|
+
chunkIndex: number;
|
|
2178
|
+
/** Plaintext byte length of the chunk before encryption. */
|
|
2179
|
+
originalSize: number;
|
|
2180
|
+
/** AES-GCM ciphertext byte length (originalSize + 16 for the tag). */
|
|
2181
|
+
ciphertextSize: number;
|
|
2182
|
+
/** Number of RS data shards. */
|
|
2183
|
+
dataShards: number;
|
|
2184
|
+
/** Number of RS parity shards. */
|
|
2185
|
+
parityShards: number;
|
|
2186
|
+
/** Length in bytes of every shard (data and parity are equal). */
|
|
2187
|
+
shardSize: number;
|
|
2188
|
+
/** SHA-256 (hex) of each shard's ciphertext bytes, in shard order. */
|
|
2189
|
+
shardHashes: string[];
|
|
2190
|
+
/** Per-chunk AES-256-GCM key + IV. Plain in the manifest — gating happens at the SpaceDO. */
|
|
2191
|
+
cipher: ChunkKeyMaterial;
|
|
1926
2192
|
}
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
2193
|
+
/**
|
|
2194
|
+
* Top-level manifest for a single stored file. Persisted in the user's
|
|
2195
|
+
* SharedSpace by file id; readable / writable per the space's ACL.
|
|
2196
|
+
*/
|
|
2197
|
+
interface FileManifest {
|
|
2198
|
+
/** Stable file ID. Assigned by the SDK at write time; used to read or delete. */
|
|
2199
|
+
id: string;
|
|
2200
|
+
/** Human-readable filename (display only — not used by the shard store). */
|
|
2201
|
+
name: string;
|
|
2202
|
+
/** Total plaintext byte length. Sum of chunk `originalSize`s. */
|
|
2203
|
+
size: number;
|
|
2204
|
+
/** MIME type the application supplied at write time. Opaque to storage. */
|
|
2205
|
+
type: string;
|
|
2206
|
+
/** Optional virtual path / folder hint for the application. */
|
|
2207
|
+
path?: string;
|
|
2208
|
+
/** When the file was last modified per the application (ms epoch). */
|
|
2209
|
+
lastModified: number;
|
|
2210
|
+
/** When this manifest was created on the SDK side (ms epoch). */
|
|
2211
|
+
createdAt: number;
|
|
2212
|
+
/** Ordered list of chunks. Concatenate them in index order to rebuild the file. */
|
|
2213
|
+
chunks: ChunkManifest[];
|
|
2214
|
+
/** SDK schema version. Bump on breaking manifest-shape changes. */
|
|
2215
|
+
version: 1;
|
|
1930
2216
|
}
|
|
1931
|
-
/**
|
|
1932
|
-
|
|
2217
|
+
/**
|
|
2218
|
+
* Lightweight summary returned by `FileStorage.writeFile` and surfaced in
|
|
2219
|
+
* directory-style listings. The full manifest is only fetched when actually
|
|
2220
|
+
* reassembling the file.
|
|
2221
|
+
*/
|
|
2222
|
+
interface FileStat {
|
|
1933
2223
|
id: string;
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
createdAt
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
/** Passkey's origin (RP id) — list responses only. A passkey only unlocks
|
|
1943
|
-
* from this host or a subdomain of it, so apps on several hosts enroll one
|
|
1944
|
-
* per origin. */
|
|
1945
|
-
rpId?: string;
|
|
2224
|
+
name: string;
|
|
2225
|
+
size: number;
|
|
2226
|
+
type: string;
|
|
2227
|
+
path?: string;
|
|
2228
|
+
lastModified: number;
|
|
2229
|
+
createdAt: number;
|
|
2230
|
+
chunkCount: number;
|
|
2231
|
+
shardCount: number;
|
|
1946
2232
|
}
|
|
1947
|
-
|
|
1948
|
-
|
|
2233
|
+
/** Distil a `FileManifest` into a `FileStat`. */
|
|
2234
|
+
declare function manifestToStat(manifest: FileManifest): FileStat;
|
|
2235
|
+
|
|
2236
|
+
/**
|
|
2237
|
+
* HTTP client for the open content-addressed shard store.
|
|
2238
|
+
*
|
|
2239
|
+
* Each shard is named by the lowercase hex SHA-256 of its ciphertext bytes.
|
|
2240
|
+
* The server verifies the hash on PUT, so the address authenticates the
|
|
2241
|
+
* contents — there's no benefit to ACLing PUT/GET, and making them open
|
|
2242
|
+
* enables cross-user dedup down the line.
|
|
2243
|
+
*
|
|
2244
|
+
* Wire protocol:
|
|
2245
|
+
* PUT /api/shards/:hash body: shard bytes (application/octet-stream)
|
|
2246
|
+
* 200 { ok: true, hash, size, dedup: boolean }
|
|
2247
|
+
* 400 if hash doesn't match body
|
|
2248
|
+
*
|
|
2249
|
+
* GET /api/shards/:hash 200 with shard bytes (application/octet-stream)
|
|
2250
|
+
* 404 if absent
|
|
2251
|
+
*
|
|
2252
|
+
* HEAD /api/shards/:hash 200 if present (no body) — used by callers to
|
|
2253
|
+
* skip a PUT they'd otherwise dedup-rewrite. The
|
|
2254
|
+
* server can ignore the optimization if it wants.
|
|
2255
|
+
*/
|
|
2256
|
+
/**
|
|
2257
|
+
* A content-addressed byte cache for shards (the offline layer's `ShardCache`
|
|
2258
|
+
* satisfies this structurally). Declared here as a minimal interface so the
|
|
2259
|
+
* storage layer doesn't import the offline module — keeping the dependency
|
|
2260
|
+
* arrow one-way (offline → storage).
|
|
2261
|
+
*/
|
|
2262
|
+
interface ShardByteCache {
|
|
2263
|
+
get(hash: string): Promise<Uint8Array | null>;
|
|
2264
|
+
put(hash: string, bytes: Uint8Array): Promise<void>;
|
|
2265
|
+
}
|
|
2266
|
+
/**
|
|
2267
|
+
* A peer block source (the p2p layer's `PeerExchange` satisfies this
|
|
2268
|
+
* structurally). Consulted between the local cache and origin: a block fetched
|
|
2269
|
+
* from a peer is verified by hash before use. Declared here so the storage layer
|
|
2270
|
+
* doesn't import the p2p module (dependency arrow stays p2p → storage).
|
|
2271
|
+
*/
|
|
2272
|
+
interface PeerBlockSource {
|
|
2273
|
+
getBlock(hash: string, opts?: {
|
|
2274
|
+
timeoutMs?: number;
|
|
2275
|
+
}): Promise<Uint8Array | null>;
|
|
2276
|
+
announce(hash: string): void;
|
|
2277
|
+
}
|
|
2278
|
+
interface ShardClientOptions {
|
|
2279
|
+
/** Base URL of the accelerator (e.g. `https://muhkoo-accelerator.workers.dev`). */
|
|
1949
2280
|
baseUrl: string;
|
|
1950
|
-
/**
|
|
2281
|
+
/**
|
|
2282
|
+
* Path prefix for the shard endpoints, joined to `baseUrl` before the
|
|
2283
|
+
* hash. Defaults to `/api/shards`, which targets a global shard store
|
|
2284
|
+
* (e.g. a dedicated `ShardStoreDO`). Pass a room-scoped value like
|
|
2285
|
+
* `/api/spaces/{roomId}/shards` to keep shards bound to a specific
|
|
2286
|
+
* `SharedSpaceDO` — that's the path the chat application uses today.
|
|
2287
|
+
*
|
|
2288
|
+
* Leading slash optional; trailing slash stripped. The final URL is
|
|
2289
|
+
* always `${baseUrl}${normalizedPrefix}/${encodeURIComponent(hash)}`.
|
|
2290
|
+
*/
|
|
2291
|
+
pathPrefix?: string;
|
|
2292
|
+
/**
|
|
2293
|
+
* Optional custom fetch implementation. Defaults to `globalThis.fetch`.
|
|
2294
|
+
* Provided so callers can inject retries / auth / instrumentation.
|
|
2295
|
+
*/
|
|
1951
2296
|
fetch?: typeof fetch;
|
|
1952
|
-
}
|
|
1953
|
-
declare class AuthClient {
|
|
1954
|
-
private readonly baseUrl;
|
|
1955
|
-
private readonly fetchFn;
|
|
1956
|
-
constructor(opts: AuthClientOptions);
|
|
1957
|
-
/** Submit ZK registration. Caller has already derived the identity + commitment. */
|
|
1958
|
-
register(req: ZkRegisterRequest): Promise<ZkRegisterResponse>;
|
|
1959
|
-
/** Request an auth challenge for `username` (server issues a fresh nonce). */
|
|
1960
|
-
getChallenge(username: string): Promise<ZkChallengeResponse>;
|
|
1961
|
-
/** Trade a challenge + proof for a session token. */
|
|
1962
|
-
authenticate(req: ZkAuthenticateRequest): Promise<AuthSuccess>;
|
|
1963
2297
|
/**
|
|
1964
|
-
*
|
|
1965
|
-
* success
|
|
1966
|
-
*
|
|
2298
|
+
* Optional offline byte cache. When present, GETs are served cache-first and
|
|
2299
|
+
* populated on success, and PUTs write the cache before the network so a
|
|
2300
|
+
* just-uploaded file reads back offline. See {@link ShardByteCache}.
|
|
1967
2301
|
*/
|
|
1968
|
-
|
|
1969
|
-
username: string;
|
|
1970
|
-
}>;
|
|
1971
|
-
/** Blinded OPRF evaluation — the server-gated half of a password/recovery wrap key. */
|
|
1972
|
-
oprfEvaluate(username: string, blinded: string): Promise<{
|
|
1973
|
-
evaluated: string;
|
|
1974
|
-
}>;
|
|
2302
|
+
cache?: ShardByteCache;
|
|
1975
2303
|
/**
|
|
1976
|
-
*
|
|
1977
|
-
*
|
|
1978
|
-
*
|
|
2304
|
+
* Coalesce concurrent origin reads into `POST {pathPrefix}/batch`. On by
|
|
2305
|
+
* default: it is what keeps opening a project from costing one request per
|
|
2306
|
+
* shard. Set false to force one-at-a-time — useful against a server without
|
|
2307
|
+
* the route, though that is detected and handled automatically.
|
|
1979
2308
|
*/
|
|
1980
|
-
|
|
1981
|
-
evaluated: string;
|
|
1982
|
-
evaluated2: string;
|
|
1983
|
-
}>;
|
|
2309
|
+
batchShards?: boolean;
|
|
1984
2310
|
/**
|
|
1985
|
-
*
|
|
1986
|
-
*
|
|
1987
|
-
*
|
|
2311
|
+
* Called when a PUT can't reach the network (offline). Lets the caller queue
|
|
2312
|
+
* the upload for replay on reconnect; the bytes are already in {@link cache}
|
|
2313
|
+
* keyed by `hash`, so only the hash needs recording.
|
|
1988
2314
|
*/
|
|
1989
|
-
|
|
1990
|
-
token?: string;
|
|
1991
|
-
email?: string;
|
|
1992
|
-
username?: string;
|
|
1993
|
-
}): Promise<void>;
|
|
1994
|
-
/** Trade an emailed code for a single-use verifyToken (+ the proven address). */
|
|
1995
|
-
emailOtpVerify(opts: {
|
|
1996
|
-
code: string;
|
|
1997
|
-
token?: string;
|
|
1998
|
-
username?: string;
|
|
1999
|
-
}): Promise<{
|
|
2000
|
-
verifyToken: string;
|
|
2001
|
-
email: string;
|
|
2002
|
-
}>;
|
|
2315
|
+
deferUpload?: (hash: string) => Promise<void>;
|
|
2003
2316
|
/**
|
|
2004
|
-
*
|
|
2005
|
-
*
|
|
2006
|
-
*
|
|
2317
|
+
* Optional peer block source. When present, GETs try peers between the local
|
|
2318
|
+
* cache and origin (peer blocks are verified by hash); PUTs announce the new
|
|
2319
|
+
* block to peers. Best-effort — any failure falls through to origin.
|
|
2007
2320
|
*/
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2321
|
+
peers?: PeerBlockSource;
|
|
2322
|
+
/**
|
|
2323
|
+
* When true, {@link ShardClient.putShard} returns as soon as the shard is
|
|
2324
|
+
* written to {@link cache} and announced to {@link peers} — the origin PUT is
|
|
2325
|
+
* fired in the BACKGROUND rather than awaited. This makes a large write's
|
|
2326
|
+
* manifest available immediately (so a peer/worker can start pulling the
|
|
2327
|
+
* shards over P2P right away) while origin catches up as a durable fallback.
|
|
2328
|
+
* On a background-PUT failure the shard is handed to {@link deferUpload} (if
|
|
2329
|
+
* present) for replay. Requires a {@link cache} so the bytes survive locally
|
|
2330
|
+
* until the background upload lands. No-op without a cache.
|
|
2331
|
+
*/
|
|
2332
|
+
backgroundUpload?: boolean;
|
|
2333
|
+
}
|
|
2334
|
+
/**
|
|
2335
|
+
* Compute the lowercase hex SHA-256 of `bytes`. Available via WebCrypto in
|
|
2336
|
+
* every runtime this SDK targets.
|
|
2337
|
+
*/
|
|
2338
|
+
declare function shardHash(bytes: Uint8Array): Promise<string>;
|
|
2339
|
+
declare class ShardClient {
|
|
2340
|
+
private readonly baseUrl;
|
|
2341
|
+
private readonly pathPrefix;
|
|
2342
|
+
private readonly fetchFn;
|
|
2343
|
+
private readonly cache?;
|
|
2344
|
+
/** Hashes waiting for the next batch, each with everyone awaiting it. */
|
|
2345
|
+
private pendingShards;
|
|
2346
|
+
private batchTimer;
|
|
2347
|
+
/** False once the server is known not to have the batch route. */
|
|
2348
|
+
private batchSupported;
|
|
2349
|
+
private readonly deferUpload?;
|
|
2350
|
+
private readonly peers?;
|
|
2351
|
+
private readonly backgroundUpload;
|
|
2352
|
+
constructor(opts: ShardClientOptions);
|
|
2353
|
+
/** Build the URL for a given shard hash — exposed for callers that want to log or pre-resolve. */
|
|
2354
|
+
urlForHash(hash: string): string;
|
|
2355
|
+
/**
|
|
2356
|
+
* Upload a shard. The server verifies SHA-256(body) matches `:hash` and
|
|
2357
|
+
* rejects with 400 if not, so the caller is the source of truth on
|
|
2358
|
+
* naming. Re-uploading an existing shard is idempotent — the server
|
|
2359
|
+
* returns `dedup: true` in that case.
|
|
2360
|
+
*/
|
|
2361
|
+
putShard(hash: string, bytes: Uint8Array): Promise<{
|
|
2362
|
+
dedup: boolean;
|
|
2037
2363
|
}>;
|
|
2364
|
+
private bgActive;
|
|
2365
|
+
private readonly bgQueue;
|
|
2366
|
+
private static readonly BG_CONCURRENCY;
|
|
2367
|
+
/** Queue a shard's origin PUT to run in the background under a concurrency
|
|
2368
|
+
* cap. Bytes are re-read from {@link cache} at upload time (not held in the
|
|
2369
|
+
* queue) so a large file's backlog doesn't pin a second copy in memory. */
|
|
2370
|
+
private enqueueBackgroundUpload;
|
|
2371
|
+
/** PUT the shard bytes to origin, verifying the server accepted them.
|
|
2372
|
+
* Used by the background-upload path; throws on network/HTTP failure. */
|
|
2373
|
+
private uploadToOrigin;
|
|
2038
2374
|
/**
|
|
2039
|
-
*
|
|
2040
|
-
*
|
|
2375
|
+
* Fetch a shard by hash. Throws if the server returns a non-200; returns
|
|
2376
|
+
* `null` for 404 so callers can distinguish "missing" from "errored".
|
|
2377
|
+
*/
|
|
2378
|
+
getShard(hash: string): Promise<Uint8Array | null>;
|
|
2379
|
+
/**
|
|
2380
|
+
* Fetch from origin, COALESCING concurrent requests into one batch.
|
|
2381
|
+
*
|
|
2382
|
+
* A shard is not a file. A project is thousands of small source modules,
|
|
2383
|
+
* each split into shards, so reading one at a time made opening a project
|
|
2384
|
+
* cost thousands of requests — 2,086 for a 519-file project. Latency, not
|
|
2385
|
+
* bytes, is what that spends: Chromium took 18.4s over those requests and
|
|
2386
|
+
* Brave, which filters every one, took 37.5s for identical work. Batching
|
|
2387
|
+
* collapses whatever is in flight at the same moment into a single POST,
|
|
2388
|
+
* which is why it helps most exactly where it hurt most.
|
|
2389
|
+
*
|
|
2390
|
+
* Callers are unchanged: `getShard` still asks for one shard and gets one
|
|
2391
|
+
* shard. The window is a few milliseconds — long enough to gather the reads
|
|
2392
|
+
* a concurrent walk issues together, short enough not to be felt by a lone
|
|
2393
|
+
* read.
|
|
2394
|
+
*
|
|
2395
|
+
* Falls back permanently to one-at-a-time if the server has no batch route,
|
|
2396
|
+
* so a new SDK keeps working against an older deployment.
|
|
2041
2397
|
*/
|
|
2042
|
-
|
|
2043
|
-
/**
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
factorId?: string): Promise<{
|
|
2049
|
-
factor: VaultFactor | null;
|
|
2050
|
-
}>;
|
|
2398
|
+
private fetchFromOrigin;
|
|
2399
|
+
/** Send one batch of whatever has accumulated, and settle those callers. */
|
|
2400
|
+
private flushShardBatch;
|
|
2401
|
+
/** Fetch a shard from origin. Returns `null` for 404 (or offline when a
|
|
2402
|
+
* cache is present, so RS recovery can proceed); throws otherwise. */
|
|
2403
|
+
private fetchOneFromOrigin;
|
|
2051
2404
|
/**
|
|
2052
|
-
*
|
|
2053
|
-
*
|
|
2054
|
-
*
|
|
2405
|
+
* Check whether a shard exists. Lets the caller skip a PUT they'd otherwise
|
|
2406
|
+
* dedup-rewrite. The server can implement this as a HEAD or return a small
|
|
2407
|
+
* JSON body — we don't depend on either.
|
|
2055
2408
|
*/
|
|
2056
|
-
|
|
2057
|
-
ok: boolean;
|
|
2058
|
-
id: string;
|
|
2059
|
-
}>;
|
|
2060
|
-
/** List the user's factor metadata (no ciphertext). Session-authed. */
|
|
2061
|
-
vaultFactors(token: string): Promise<{
|
|
2062
|
-
factors: VaultFactor[];
|
|
2063
|
-
migrated: boolean;
|
|
2064
|
-
}>;
|
|
2065
|
-
/** Remove a factor by id (can't remove the last one). Session-authed. */
|
|
2066
|
-
vaultDeleteFactor(token: string, id: string): Promise<{
|
|
2067
|
-
deleted: boolean;
|
|
2068
|
-
}>;
|
|
2069
|
-
private json;
|
|
2070
|
-
private parse;
|
|
2409
|
+
hasShard(hash: string): Promise<boolean>;
|
|
2071
2410
|
}
|
|
2072
2411
|
|
|
2073
2412
|
/**
|
|
2074
|
-
*
|
|
2075
|
-
*
|
|
2076
|
-
* Same inputs on ANY device produce the same secret, salt, and P-256
|
|
2077
|
-
* ECDSA + ECDH keypairs — which is what makes federated login from a
|
|
2078
|
-
* fresh browser possible. There is no localStorage involvement: every
|
|
2079
|
-
* register and login call re-derives the identity on demand.
|
|
2413
|
+
* HTTP client for the gated file-manifest store on a SharedSpace
|
|
2414
|
+
* (`/api/spaces/:spaceId/files`).
|
|
2080
2415
|
*
|
|
2081
|
-
*
|
|
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)
|
|
2416
|
+
* A "file" in a space is its **manifest** (per-chunk keys + the SHA-256 of each
|
|
2417
|
+
* global shard). The shard bytes live in the open `/api/shards` store; the
|
|
2418
|
+
* manifest is the capability. This client only moves manifests in/out of the
|
|
2419
|
+
* space's gated store.
|
|
2087
2420
|
*
|
|
2088
|
-
*
|
|
2089
|
-
*
|
|
2090
|
-
*
|
|
2091
|
-
*
|
|
2421
|
+
* Authorization is **session-based**, consistent with the rest of the
|
|
2422
|
+
* SharedSpace (keyring, websocket): the SDK's credential-stamping `fetch`
|
|
2423
|
+
* (from {@link HttpClient}) attaches the app key + session token, the platform
|
|
2424
|
+
* validates the session and injects a trusted `X-Muhkoo-User-Context`, and the
|
|
2425
|
+
* DO authorizes on that + channel membership. There is no per-op Groth16 proof.
|
|
2092
2426
|
*
|
|
2093
|
-
*
|
|
2094
|
-
*
|
|
2095
|
-
*
|
|
2427
|
+
* Wire protocol (server: `SharedSpaceDO`):
|
|
2428
|
+
* POST /api/spaces/:spaceId/files body: { manifest } → { ok, fileId }
|
|
2429
|
+
* GET /api/spaces/:spaceId/files → { files: FileStat[] }
|
|
2430
|
+
* GET /api/spaces/:spaceId/files/:fileId → { manifest }
|
|
2431
|
+
* DELETE /api/spaces/:spaceId/files/:fileId → { ok, existed }
|
|
2096
2432
|
*/
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2433
|
+
|
|
2434
|
+
interface SharedSpaceClientOptions {
|
|
2435
|
+
/** Base URL of the accelerator (e.g. `https://api.muhkoo.dev`). */
|
|
2436
|
+
baseUrl: string;
|
|
2437
|
+
/**
|
|
2438
|
+
* Credential-stamping fetch — pass {@link HttpClient.fetch} so requests
|
|
2439
|
+
* carry the app key + session token. Defaults to `globalThis.fetch`
|
|
2440
|
+
* (unauthenticated; only useful in tests).
|
|
2441
|
+
*/
|
|
2442
|
+
fetch?: typeof fetch;
|
|
2443
|
+
}
|
|
2444
|
+
declare class SharedSpaceClient {
|
|
2445
|
+
private readonly baseUrl;
|
|
2446
|
+
private readonly fetchFn;
|
|
2447
|
+
constructor(opts: SharedSpaceClientOptions);
|
|
2448
|
+
/** Write a file manifest into the space. Requires space membership. */
|
|
2449
|
+
writeFileManifest(spaceId: string, manifest: FileManifest): Promise<void>;
|
|
2450
|
+
/** Read a file manifest by id. Requires space membership. */
|
|
2451
|
+
readFileManifest(spaceId: string, fileId: string): Promise<FileManifest>;
|
|
2452
|
+
/** Delete a file manifest by id. Requires space membership. */
|
|
2453
|
+
deleteFileManifest(spaceId: string, fileId: string): Promise<boolean>;
|
|
2454
|
+
/** List file summaries in the space. Requires space membership. */
|
|
2455
|
+
listFiles(spaceId: string): Promise<FileStat[]>;
|
|
2456
|
+
private filesBase;
|
|
2457
|
+
private jsonRequest;
|
|
2106
2458
|
}
|
|
2107
|
-
/**
|
|
2108
|
-
* Derive a deterministic ZK identity from `(username, password)`.
|
|
2109
|
-
*
|
|
2110
|
-
* Same inputs always produce the same identity, on any device — that's
|
|
2111
|
-
* what makes federated login work without dragging encrypted state across
|
|
2112
|
-
* devices.
|
|
2113
|
-
*/
|
|
2114
|
-
declare function deriveIdentity(username: string, password: string): Promise<ZkIdentity>;
|
|
2115
|
-
/**
|
|
2116
|
-
* Legacy master-seed derivation: `PBKDF2(password, "muhkoo-zk-v1:"||username)`.
|
|
2117
|
-
*
|
|
2118
|
-
* This was the *only* path pre-vault, where the identity descended directly from
|
|
2119
|
-
* the password. It's kept so existing accounts still re-derive their seed (and
|
|
2120
|
-
* the vault can adopt that exact seed on migration, preserving the commitment).
|
|
2121
|
-
* New accounts use a random seed (`vault.randomSeed`) instead, so their identity
|
|
2122
|
-
* is no longer a function of the password.
|
|
2123
|
-
*/
|
|
2124
|
-
declare function deriveMasterSeedFromPassword(username: string, password: string): Promise<Uint8Array>;
|
|
2125
|
-
/**
|
|
2126
|
-
* Derive the (unchanged) ZK identity from a 32-byte **master seed**. The seed is
|
|
2127
|
-
* HKDF-expanded into secret/salt + the P-256 ECDSA & ECDH keypairs exactly as
|
|
2128
|
-
* before — so the resulting commitment is byte-identical for a given seed. The
|
|
2129
|
-
* vault wraps THIS seed under each recovery factor.
|
|
2130
|
-
*/
|
|
2131
|
-
declare function deriveIdentityFromSeed(seed: Uint8Array): Promise<ZkIdentity>;
|
|
2132
2459
|
|
|
2133
2460
|
/**
|
|
2134
|
-
*
|
|
2135
|
-
*
|
|
2136
|
-
* Backed by `circomlibjs`'s `buildPoseidon`. Builder is lazy and cached
|
|
2137
|
-
* because the JS-side Babyjubjub field reps take ~100ms to construct, and
|
|
2138
|
-
* we'd rather not pay that on every page load.
|
|
2139
|
-
*
|
|
2140
|
-
* Returns hashes as decimal-string BigInts — same format snarkjs and the
|
|
2141
|
-
* `preimagePoK` circuit expect for inputs.
|
|
2461
|
+
* `FileStorage` — top-level entry point for chunked, encrypted, erasure-coded
|
|
2462
|
+
* file storage.
|
|
2142
2463
|
*
|
|
2143
|
-
*
|
|
2144
|
-
*
|
|
2145
|
-
*
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
*
|
|
2149
|
-
*
|
|
2150
|
-
*
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
/**
|
|
2155
|
-
* Groth16 proof generation for the `preimagePoK` circuit.
|
|
2464
|
+
* Pipeline for writing a file:
|
|
2465
|
+
* 1. Split the plaintext into fixed-size chunks (default 4 MiB).
|
|
2466
|
+
* 2. For each chunk:
|
|
2467
|
+
* a. Generate a fresh AES-256-GCM key + 12-byte IV (`ChunkCipher`).
|
|
2468
|
+
* b. Encrypt the chunk → ciphertext (plaintext.length + 16 bytes tag).
|
|
2469
|
+
* c. Reed–Solomon encode the ciphertext into `dataShards + parityShards`
|
|
2470
|
+
* equal-length shards (`ReedSolomonCodec`).
|
|
2471
|
+
* d. PUT every shard to the content-addressed shard store, keyed by
|
|
2472
|
+
* SHA-256 of the shard bytes (`ShardClient`).
|
|
2473
|
+
* 3. Build a `FileManifest` listing every chunk's keys, IVs, sizes, and
|
|
2474
|
+
* the SHA-256 of every shard in order.
|
|
2156
2475
|
*
|
|
2157
|
-
*
|
|
2158
|
-
*
|
|
2159
|
-
*
|
|
2476
|
+
* Two ways to use the manifest:
|
|
2477
|
+
* - {@link FileStorage.writeFileToShards} returns it to the caller, who is
|
|
2478
|
+
* then responsible for delivering it to readers (e.g. by E2E-encrypting
|
|
2479
|
+
* it as a chat message). The chat application takes this path.
|
|
2480
|
+
* - {@link FileStorage.writeFile} additionally POSTs the manifest to a
|
|
2481
|
+
* gated multi-user space (`SharedSpaceClient`) so any participant of the
|
|
2482
|
+
* space can read the file by manifest id later.
|
|
2160
2483
|
*
|
|
2161
|
-
*
|
|
2162
|
-
*
|
|
2163
|
-
*
|
|
2164
|
-
* `baseUrl` so the SPA can target either the local wrangler dev or a deployed
|
|
2165
|
-
* worker depending on `VITE_WORKER_URL`.
|
|
2484
|
+
* Reading is the inverse: {@link FileStorage.readFileFromShards} takes a
|
|
2485
|
+
* manifest and reassembles the plaintext; {@link FileStorage.readFile} first
|
|
2486
|
+
* fetches the manifest from the space.
|
|
2166
2487
|
*/
|
|
2167
2488
|
|
|
2168
|
-
interface
|
|
2169
|
-
/**
|
|
2170
|
-
|
|
2171
|
-
/**
|
|
2172
|
-
|
|
2173
|
-
}
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
declare function buildCommitment(secretHex: string, saltHex: string, ecdsaPubHex: string): Promise<string>;
|
|
2489
|
+
interface FileStorageOptions {
|
|
2490
|
+
/** Talks to the content-addressed shard store. Always required. */
|
|
2491
|
+
shards: ShardClient;
|
|
2492
|
+
/**
|
|
2493
|
+
* Talks to the gated multi-user space holding manifests + ACLs.
|
|
2494
|
+
* Optional — only required by {@link FileStorage.writeFile} /
|
|
2495
|
+
* {@link FileStorage.readFile} / {@link FileStorage.deleteFile} /
|
|
2496
|
+
* {@link FileStorage.listFiles} / {@link FileStorage.readManifest}.
|
|
2497
|
+
*
|
|
2498
|
+
* Callers that ship the manifest themselves (e.g. via an encrypted chat
|
|
2499
|
+
* message) can omit `space` and use the `*FromShards` / `*ToShards`
|
|
2500
|
+
* methods directly.
|
|
2501
|
+
*/
|
|
2502
|
+
space?: SharedSpaceClient;
|
|
2503
|
+
/**
|
|
2504
|
+
* Plaintext chunk size in bytes. After encryption each chunk gains 16
|
|
2505
|
+
* bytes (GCM tag); after RS encoding the size is rounded up to a multiple
|
|
2506
|
+
* of `dataShards` per chunk. Default 4 MiB — a balance between
|
|
2507
|
+
* round-trip cost and parallelism on the upload path.
|
|
2508
|
+
*/
|
|
2509
|
+
chunkSize?: number;
|
|
2510
|
+
/** RS data shards per chunk (default 4). */
|
|
2511
|
+
dataShards?: number;
|
|
2512
|
+
/** RS parity shards per chunk (default 2). */
|
|
2513
|
+
parityShards?: number;
|
|
2514
|
+
/**
|
|
2515
|
+
* Cap on concurrent shard uploads / downloads. Tune for the network. The
|
|
2516
|
+
* default of 8 saturates most home connections without overwhelming the
|
|
2517
|
+
* server's per-IP rate limiter.
|
|
2518
|
+
*/
|
|
2519
|
+
concurrency?: number;
|
|
2520
|
+
/**
|
|
2521
|
+
* Optional pre-compiled RS WebAssembly module. Defaults to the bundled
|
|
2522
|
+
* loader. Pass this in environments where the bundled loader path isn't
|
|
2523
|
+
* available (vitest, deploy-time-precompiled CF Workers).
|
|
2524
|
+
*/
|
|
2525
|
+
rsWasmModule?: WebAssembly.Module;
|
|
2526
|
+
}
|
|
2207
2527
|
/**
|
|
2208
|
-
*
|
|
2209
|
-
*
|
|
2210
|
-
* matches the stored one and the nonce matches the issued challenge nonce
|
|
2211
|
-
* reduced into the field.
|
|
2212
|
-
*
|
|
2213
|
-
* Throws if circuit URLs are unreachable or snarkjs decides the inputs are
|
|
2214
|
-
* malformed.
|
|
2528
|
+
* Input to {@link FileStorage.writeFile} (which writes the manifest to a
|
|
2529
|
+
* `SharedSpaceClient`). Carries the destination `spaceId`.
|
|
2215
2530
|
*/
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
nonceHex: string;
|
|
2221
|
-
/** Required — caller supplies URLs scoped to the worker they want to talk to. */
|
|
2222
|
-
circuits: CircuitUrls;
|
|
2223
|
-
}): Promise<{
|
|
2224
|
-
proof: Groth16Proof;
|
|
2225
|
-
publicSignals: string[];
|
|
2226
|
-
commitment: string;
|
|
2227
|
-
nonceField: string;
|
|
2228
|
-
ecdsaPubHash: string;
|
|
2229
|
-
}>;
|
|
2230
|
-
|
|
2531
|
+
interface WriteFileInput extends WriteToShardsInput {
|
|
2532
|
+
/** Space to write the manifest to — must allow the caller `write`. */
|
|
2533
|
+
spaceId: string;
|
|
2534
|
+
}
|
|
2231
2535
|
/**
|
|
2232
|
-
*
|
|
2233
|
-
*
|
|
2234
|
-
*
|
|
2235
|
-
* Two outputs we need from an ECDSA `CryptoKey`:
|
|
2236
|
-
* - **base64 raw** — passed to `/api/auth/zk-register` as
|
|
2237
|
-
* `ecdsaPublicKey` / `ecdhPublicKey`. (Name is historical;
|
|
2238
|
-
* the legacy client called it `exportPublicKeyToBase58`
|
|
2239
|
-
* but actually used base64 — kept as base64.)
|
|
2240
|
-
* - **hex raw** — fed to the proof as `ecdsaPub` input; the circuit
|
|
2241
|
-
* reduces it into a field element.
|
|
2242
|
-
*
|
|
2243
|
-
* Both go through `crypto.subtle.exportKey("raw", ...)` which yields the
|
|
2244
|
-
* uncompressed SEC1 form (`0x04 || X || Y`).
|
|
2536
|
+
* Input to {@link FileStorage.writeFileToShards} (which doesn't write a
|
|
2537
|
+
* manifest anywhere — that's the caller's job).
|
|
2245
2538
|
*/
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2539
|
+
interface WriteToShardsInput {
|
|
2540
|
+
/** Raw data, a Blob, or a File. */
|
|
2541
|
+
data: Uint8Array | Blob | File;
|
|
2542
|
+
/** Application-supplied display metadata. */
|
|
2543
|
+
metadata: {
|
|
2544
|
+
name: string;
|
|
2545
|
+
type: string;
|
|
2546
|
+
path?: string;
|
|
2547
|
+
lastModified?: number;
|
|
2548
|
+
};
|
|
2549
|
+
/**
|
|
2550
|
+
* Optional pre-allocated file id. Defaults to a fresh one. Useful for
|
|
2551
|
+
* resumable uploads: the same id always maps to the same manifest slot.
|
|
2552
|
+
*/
|
|
2553
|
+
fileId?: string;
|
|
2554
|
+
/**
|
|
2555
|
+
* Optional upload-progress callback, invoked after each chunk finishes
|
|
2556
|
+
* (`completed` of `total` chunks). Lets a UI show a progress bar. Called once
|
|
2557
|
+
* with `(0, total)` before the first chunk so a bar can render immediately.
|
|
2558
|
+
*/
|
|
2559
|
+
onProgress?: (completed: number, total: number) => void;
|
|
2560
|
+
}
|
|
2561
|
+
declare class FileStorage {
|
|
2562
|
+
private readonly space;
|
|
2563
|
+
private readonly shards;
|
|
2564
|
+
private readonly chunkSize;
|
|
2565
|
+
private readonly dataShards;
|
|
2566
|
+
private readonly parityShards;
|
|
2567
|
+
private readonly concurrency;
|
|
2568
|
+
private readonly cipher;
|
|
2569
|
+
private readonly codec;
|
|
2570
|
+
private readonly rsWasmModule?;
|
|
2571
|
+
constructor(opts: FileStorageOptions);
|
|
2572
|
+
/**
|
|
2573
|
+
* Chunk → encrypt → encode → upload-shards. Returns the resulting manifest
|
|
2574
|
+
* along with a `FileStat` summary. The caller is responsible for storing
|
|
2575
|
+
* or transmitting the manifest however it likes.
|
|
2576
|
+
*/
|
|
2577
|
+
writeFileToShards(input: WriteToShardsInput): Promise<{
|
|
2578
|
+
manifest: FileManifest;
|
|
2579
|
+
stat: FileStat;
|
|
2580
|
+
}>;
|
|
2581
|
+
/**
|
|
2582
|
+
* Inverse of {@link writeFileToShards}. Takes a manifest the caller
|
|
2583
|
+
* obtained somehow (chat message, share link, etc.) and reassembles the
|
|
2584
|
+
* plaintext. Throws if too many shards are missing or any chunk's auth
|
|
2585
|
+
* tag fails.
|
|
2586
|
+
*/
|
|
2587
|
+
readFileFromShards(manifest: FileManifest): Promise<{
|
|
2588
|
+
data: Uint8Array;
|
|
2589
|
+
stat: FileStat;
|
|
2590
|
+
}>;
|
|
2591
|
+
/**
|
|
2592
|
+
* Streaming inverse of {@link writeFileToShards}: yields each chunk's
|
|
2593
|
+
* decrypted plaintext in order WITHOUT concatenating. Peak memory stays at
|
|
2594
|
+
* ~one chunk, so multi-GB files (which overflow a single `Uint8Array`) can be
|
|
2595
|
+
* piped straight to a file/socket. Same shard sourcing (cache → peers →
|
|
2596
|
+
* origin) and per-chunk auth as {@link readFileFromShards}.
|
|
2597
|
+
*/
|
|
2598
|
+
readChunksFromShards(manifest: FileManifest): AsyncGenerator<Uint8Array>;
|
|
2599
|
+
/** As {@link writeFileToShards}, plus POSTs the manifest to `input.spaceId`. */
|
|
2600
|
+
writeFile(input: WriteFileInput): Promise<FileStat>;
|
|
2601
|
+
/** As {@link readFileFromShards}, but fetches the manifest from a space first. */
|
|
2602
|
+
readFile(spaceId: string, fileId: string): Promise<{
|
|
2603
|
+
data: Uint8Array;
|
|
2604
|
+
stat: FileStat;
|
|
2605
|
+
}>;
|
|
2606
|
+
/**
|
|
2607
|
+
* Delete the manifest from a space. **Does not GC the shards** — those
|
|
2608
|
+
* are content-addressed and may be referenced by other manifests.
|
|
2609
|
+
* Garbage collection is a separate server-side concern (Phase D / D-ish).
|
|
2610
|
+
*/
|
|
2611
|
+
deleteFile(spaceId: string, fileId: string): Promise<boolean>;
|
|
2612
|
+
/** Convenience pass-through. */
|
|
2613
|
+
listFiles(spaceId: string): Promise<FileStat[]>;
|
|
2614
|
+
/** Read a manifest verbatim — useful for "share file" / export flows. */
|
|
2615
|
+
readManifest(spaceId: string, fileId: string): Promise<FileManifest>;
|
|
2616
|
+
private requireSpace;
|
|
2617
|
+
/** Encrypt + encode + upload one chunk; return its manifest entry. */
|
|
2618
|
+
private writeChunk;
|
|
2619
|
+
/** Inverse of `writeChunk`. */
|
|
2620
|
+
private readChunk;
|
|
2621
|
+
/** Run `tasks` with at most `this.concurrency` in flight. */
|
|
2622
|
+
private runWithConcurrency;
|
|
2623
|
+
}
|
|
2252
2624
|
|
|
2253
2625
|
/**
|
|
2254
|
-
*
|
|
2255
|
-
*
|
|
2256
|
-
* The hosted page authenticates the user and ends up holding the master seed.
|
|
2257
|
-
* To pass it to the developer's app without it ever sitting server-readable or
|
|
2258
|
-
* landing in browser history:
|
|
2259
|
-
*
|
|
2260
|
-
* 1. {@link sealSeed} generates a one-time key K_t, AES-256-GCM-seals the seed
|
|
2261
|
-
* under it, and returns the ciphertext envelope (`sealedKeys`, stored in
|
|
2262
|
-
* the grant the server relays but can't read) plus K_t (`fragmentKey`,
|
|
2263
|
-
* placed ONLY in the redirect URL fragment — fragments never hit a server).
|
|
2264
|
-
* 2. The app exchanges the code for `sealedKeys`, reads K_t from the fragment,
|
|
2265
|
-
* and {@link unsealSeed} recovers the seed, then strips the fragment.
|
|
2266
|
-
*
|
|
2267
|
-
* Trust-equivalent to today's embedded SDK (the app still ends up with the
|
|
2268
|
-
* seed) while moving credential entry onto the Muhkoo origin. The envelope is
|
|
2269
|
-
* versioned so a future per-app-key handoff (v2) can swap in without a protocol
|
|
2270
|
-
* change.
|
|
2271
|
-
*
|
|
2272
|
-
* ## v2 — device pairing (TV / no-redirect handoff)
|
|
2626
|
+
* Reed–Solomon erasure-coding codec.
|
|
2273
2627
|
*
|
|
2274
|
-
*
|
|
2275
|
-
*
|
|
2276
|
-
*
|
|
2277
|
-
* server relays a sealed envelope it cannot read — and replaces only the key
|
|
2278
|
-
* agreement:
|
|
2628
|
+
* Wraps the WASM-backed `encode` / `reconstruct` primitives in `wasm/rs.ts`
|
|
2629
|
+
* with shard-padding bookkeeping so callers can hand it any-length input and
|
|
2630
|
+
* get back uniform-size shards.
|
|
2279
2631
|
*
|
|
2280
|
-
*
|
|
2281
|
-
*
|
|
2282
|
-
*
|
|
2283
|
-
*
|
|
2284
|
-
*
|
|
2285
|
-
*
|
|
2286
|
-
*
|
|
2287
|
-
* ({@link unsealSeedFromDevice}).
|
|
2632
|
+
* Usage:
|
|
2633
|
+
* const codec = new ReedSolomonCodec();
|
|
2634
|
+
* await codec.ready();
|
|
2635
|
+
* const { shards, shardSize, originalSize } = codec.encode(plaintext, 4, 2);
|
|
2636
|
+
* // 6 shards (4 data + 2 parity), each `shardSize` bytes
|
|
2637
|
+
* // …ship them, lose up to 2, then …
|
|
2638
|
+
* const recovered = codec.decode(shards, 2, deadIndexes, originalSize);
|
|
2288
2639
|
*
|
|
2289
|
-
*
|
|
2290
|
-
*
|
|
2291
|
-
*
|
|
2292
|
-
*
|
|
2293
|
-
* the only MITM defence in the flow (see the entropy note on that function).
|
|
2640
|
+
* Why the explicit `originalSize`: RS pads input to make every data shard the
|
|
2641
|
+
* same length. The decoder must trim that padding back off after recovery.
|
|
2642
|
+
* The caller is responsible for stashing `originalSize` somewhere durable
|
|
2643
|
+
* (the file manifest, in this codebase).
|
|
2294
2644
|
*/
|
|
2295
|
-
interface
|
|
2296
|
-
/**
|
|
2297
|
-
|
|
2298
|
-
/**
|
|
2299
|
-
|
|
2645
|
+
interface EncodeResult {
|
|
2646
|
+
/** All `dataShards + parityShards` shards. All have length `shardSize`. */
|
|
2647
|
+
shards: Uint8Array[];
|
|
2648
|
+
/** Length of every shard (data and parity). */
|
|
2649
|
+
shardSize: number;
|
|
2650
|
+
/** Length of the original input before padding. Needed to trim on decode. */
|
|
2651
|
+
originalSize: number;
|
|
2652
|
+
/** How many of the leading shards are data (the rest are parity). */
|
|
2653
|
+
dataShards: number;
|
|
2654
|
+
/** How many parity shards trail the data shards. */
|
|
2655
|
+
parityShards: number;
|
|
2300
2656
|
}
|
|
2301
|
-
|
|
2302
|
-
declare function sealSeed(seed: Uint8Array): Promise<SealedHandoff>;
|
|
2303
|
-
/** Recover the master seed from the sealed envelope + the fragment key. */
|
|
2304
|
-
declare function unsealSeed(sealedKeys: string, fragmentKey: string): Promise<Uint8Array>;
|
|
2305
|
-
/**
|
|
2306
|
-
* Alphabet for the pairing verification code: 30 symbols, every confusable pair
|
|
2307
|
-
* removed in BOTH directions — no `0`/`O`, no `1`/`I`/`L`, and no `U` (which is
|
|
2308
|
-
* read as `V` on the low-resolution, far-viewed type a TV renders). Because the
|
|
2309
|
-
* user compares this string across two screens by eye, unambiguity beats the
|
|
2310
|
-
* convenience of a power-of-two alphabet; the derivation below rejection-samples
|
|
2311
|
-
* to avoid the modulo bias that costs us.
|
|
2312
|
-
*/
|
|
2313
|
-
declare const PAIRING_CODE_ALPHABET = "ABCDEFGHJKMNPQRSTVWXYZ23456789";
|
|
2314
|
-
/** Length of the pairing verification code, in characters. 30^8 ≈ 39.3 bits. */
|
|
2315
|
-
declare const PAIRING_CODE_LENGTH = 8;
|
|
2316
|
-
/** An ephemeral ECDH keypair generated by the pairing device (the TV). */
|
|
2317
|
-
interface DevicePairingKeypair {
|
|
2318
|
-
/** base64 raw (SEC1 uncompressed, 65 bytes) public key — safe to publish. */
|
|
2319
|
-
publicKeyB64: string;
|
|
2657
|
+
declare class ReedSolomonCodec {
|
|
2320
2658
|
/**
|
|
2321
|
-
*
|
|
2322
|
-
*
|
|
2323
|
-
*
|
|
2659
|
+
* Resolves once the WASM module is loaded; safe to call repeatedly.
|
|
2660
|
+
*
|
|
2661
|
+
* Pass `wasmModule` to use a pre-compiled module (e.g. in tests where the
|
|
2662
|
+
* rollup-inlined loader path isn't available, or in CF Workers where you
|
|
2663
|
+
* want to load the module at deploy time). Default path uses the bundled
|
|
2664
|
+
* base64-inlined wasm — works in Node, browsers, and Workers.
|
|
2324
2665
|
*/
|
|
2325
|
-
|
|
2666
|
+
ready(wasmModule?: WebAssembly.Module): Promise<void>;
|
|
2667
|
+
/**
|
|
2668
|
+
* Split `data` into `dataShards` equal-size chunks (zero-padded to fit),
|
|
2669
|
+
* compute `parityShards` parity shards, and return all of them.
|
|
2670
|
+
*/
|
|
2671
|
+
encode(data: Uint8Array, dataShards: number, parityShards: number): EncodeResult;
|
|
2672
|
+
/**
|
|
2673
|
+
* Reconstruct the original `data` from `shards`. Up to `parityShards`
|
|
2674
|
+
* shards may be missing; for each missing shard, pass an empty
|
|
2675
|
+
* `Uint8Array` placeholder at its position and list the position in
|
|
2676
|
+
* `deadShardIndexes`.
|
|
2677
|
+
*
|
|
2678
|
+
* `originalSize` is required so the trailing zero-padding can be trimmed.
|
|
2679
|
+
*/
|
|
2680
|
+
decode(shards: Uint8Array[], parityShards: number, deadShardIndexes: number[], originalSize: number): Uint8Array;
|
|
2326
2681
|
}
|
|
2682
|
+
|
|
2327
2683
|
/**
|
|
2328
|
-
*
|
|
2329
|
-
*
|
|
2330
|
-
* reusing a keypair across attempts would let an old, observed ciphertext be
|
|
2331
|
-
* decrypted by whoever later obtains the key.
|
|
2332
|
-
*/
|
|
2333
|
-
declare function generateDevicePairingKeypair(): Promise<DevicePairingKeypair>;
|
|
2334
|
-
/**
|
|
2335
|
-
* (Hosted-page side) Seal the master seed *to a device's* pairing public key.
|
|
2684
|
+
* Universal wasm-bindgen glue for `wasm-reed-solomon-erasure` (v0.2.2 — Rust
|
|
2685
|
+
* Reed–Solomon erasure-coding library compiled with `wasm-pack --target nodejs`).
|
|
2336
2686
|
*
|
|
2337
|
-
*
|
|
2338
|
-
*
|
|
2339
|
-
*
|
|
2340
|
-
*
|
|
2687
|
+
* The upstream npm package ships `wasm_reed_solomon_erasure.js` which uses
|
|
2688
|
+
* `require('fs').readFileSync()` to load the WASM file at module load time —
|
|
2689
|
+
* that hard-codes Node and breaks browsers / CF Workers. This file is a port
|
|
2690
|
+
* of that glue with the Node-only pieces removed:
|
|
2341
2691
|
*
|
|
2342
|
-
*
|
|
2343
|
-
*
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
* (Device side) Recover the master seed from a v2 envelope using the ephemeral
|
|
2348
|
-
* private key from {@link generateDevicePairingKeypair}.
|
|
2692
|
+
* - `TextDecoder` is taken from the global (available in Node 12+, every
|
|
2693
|
+
* modern browser, and Workers — no `require('util')`).
|
|
2694
|
+
* - The WASM module is loaded via the rollup-inlined `.wasm` import, the
|
|
2695
|
+
* same `targetEnv: 'auto-inline'` path the bn128 verifier uses. Works in
|
|
2696
|
+
* Node, browser, and Workers without runtime fs access.
|
|
2349
2697
|
*
|
|
2350
|
-
*
|
|
2351
|
-
*
|
|
2352
|
-
* AES-GCM tag. There is no path that returns partially-authenticated bytes.
|
|
2698
|
+
* Only the two public functions consumers actually need (`encode` and
|
|
2699
|
+
* `reconstruct`) are exported. The rest is the wbindgen heap-table dance.
|
|
2353
2700
|
*
|
|
2354
|
-
*
|
|
2355
|
-
*
|
|
2356
|
-
*
|
|
2357
|
-
*
|
|
2701
|
+
* Encoding:
|
|
2702
|
+
* `encode(dataShards: Uint8Array[], parityShard: number)` → array of
|
|
2703
|
+
* `dataShards.length + parityShard` Uint8Arrays. The first N are the input
|
|
2704
|
+
* shards unchanged; the trailing M are the freshly computed parity shards.
|
|
2705
|
+
* All shards are the same length — caller is responsible for padding the
|
|
2706
|
+
* data into equal-size data shards.
|
|
2707
|
+
*
|
|
2708
|
+
* Reconstruction:
|
|
2709
|
+
* `reconstruct(corruptedShards, parityShard, deadIndexes)` → fully recovered
|
|
2710
|
+
* array. Pass placeholders (empty Uint8Arrays) at the dead indexes; the
|
|
2711
|
+
* reconstructor fills them in. Up to `parityShard` shards may be missing.
|
|
2358
2712
|
*/
|
|
2359
|
-
declare function unsealSeedFromDevice(sealedKeys: string, privateKey: CryptoKey, devicePublicKeyB64: string): Promise<Uint8Array>;
|
|
2360
2713
|
/**
|
|
2361
|
-
*
|
|
2362
|
-
*
|
|
2363
|
-
* they match before approving.
|
|
2364
|
-
*
|
|
2365
|
-
* ## What this is worth (be honest about it)
|
|
2366
|
-
*
|
|
2367
|
-
* ~39.3 bits (30^8), rendered in an unambiguous 30-symbol alphabet and grouped
|
|
2368
|
-
* as `XXXX-XXXX` for comparison. Alongside the recipient-key binding in
|
|
2369
|
-
* {@link deriveDeviceSealKey}, this is what stops a substituted device key: the
|
|
2370
|
-
* KDF rejects a *negated* key, and this code is what a human uses to reject a
|
|
2371
|
-
* wholly *attacker-generated* one, which the crypto cannot detect on its own —
|
|
2372
|
-
* ECDH proves nothing about WHOSE public key it is.
|
|
2373
|
-
*
|
|
2374
|
-
* Two attacks, and what each actually costs:
|
|
2375
|
-
*
|
|
2376
|
-
* 1. **Online grind.** Generate keypairs until one's code equals the honest
|
|
2377
|
-
* TV's. ~2^39 trials. Naively that reads as "keygen + hash", but a real
|
|
2378
|
-
* attacker never generates keys: walking `P_{i+1} = P_i + G` while tracking
|
|
2379
|
-
* `d_i = d_0 + i` makes each trial one point-add plus one derivation. With
|
|
2380
|
-
* a plain hash that is well under a core-day — which is why this uses
|
|
2381
|
-
* PBKDF2-SHA256 at {@link PAIRING_CODE_ITERATIONS} iterations instead. The
|
|
2382
|
-
* work factor applies per trial, pushing the same grind to the order of
|
|
2383
|
-
* 10^5 core-days. The target is also only learned when pairing starts, so
|
|
2384
|
-
* the grind is online against the pairing TTL (keep it minutes).
|
|
2385
|
-
* 2. **Precomputed table.** The reason this is not a plain hash. A table over
|
|
2386
|
-
* candidate keys is computed once and reused forever; with SHA-256 that is
|
|
2387
|
-
* cheap enough to be the preferred attack. The iteration count multiplies
|
|
2388
|
-
* the table's build cost by the same 2 * 10^5.
|
|
2389
|
-
*
|
|
2390
|
-
* Neither is a proof of security, and an adversary with ASIC-scale hardware
|
|
2391
|
-
* changes these numbers. If the margin is ever judged insufficient, raise
|
|
2392
|
-
* {@link PAIRING_CODE_LENGTH} — 10 characters buys ~49 bits for two more
|
|
2393
|
-
* characters of human comparison, and is display-only: it does not touch the
|
|
2394
|
-
* envelope or the wire format.
|
|
2714
|
+
* Load and instantiate the WASM module. Idempotent — repeated calls await the
|
|
2715
|
+
* same one-shot promise.
|
|
2395
2716
|
*
|
|
2396
|
-
*
|
|
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
|
|
@@ -6317,205 +6635,56 @@ declare class Client {
|
|
|
6317
6635
|
/** Per-user key/value storage — `client.kv.set(...)`, etc. */
|
|
6318
6636
|
readonly kv: KvNamespace;
|
|
6319
6637
|
/** App scalable database — `client.db.table('todos').query(...)`, etc. */
|
|
6320
|
-
readonly db: DbNamespace;
|
|
6321
|
-
/** File storage — `client.storage.writeFile(...)`, etc. */
|
|
6322
|
-
readonly storage: StorageNamespace;
|
|
6323
|
-
/** Filesystem over the personal space: directories, versions, real paths. */
|
|
6324
|
-
readonly vfs: VfsNamespace;
|
|
6325
|
-
/** Version control over the VFS: commits, branches, merges — per project. */
|
|
6326
|
-
readonly vcs: VcsNamespace;
|
|
6327
|
-
/** Realtime messaging — `client.message.subscribe(...)`, etc. */
|
|
6328
|
-
readonly message: MessageNamespace;
|
|
6329
|
-
/** Fan-out group spaces — `client.space.createSpace(...)`, etc. */
|
|
6330
|
-
readonly space: SpaceNamespace;
|
|
6331
|
-
/** Programmable Agents — `client.agents.create(appId, …)`, etc. */
|
|
6332
|
-
readonly agents: AgentsNamespace;
|
|
6333
|
-
/** Serverless functions — `client.functions.deploy(appId, …)`, etc. */
|
|
6334
|
-
readonly functions: FunctionsNamespace;
|
|
6335
|
-
/** Access tokens — `client.accessTokens.create(appId, …)`, etc. */
|
|
6336
|
-
readonly accessTokens: AccessTokensNamespace;
|
|
6337
|
-
/** Offline cache + sync — `client.offline.status`, `client.offline.snapshot`, etc. */
|
|
6338
|
-
readonly offline: OfflineManager;
|
|
6339
|
-
private readonly session;
|
|
6340
|
-
private readonly http;
|
|
6341
|
-
/** Subscribers notified when a session expires and can't be recovered. */
|
|
6342
|
-
private readonly sessionExpiredHandlers;
|
|
6343
|
-
/** Shared in-flight recovery so a burst of 401s triggers one re-auth. */
|
|
6344
|
-
private recoverInFlight;
|
|
6345
|
-
constructor(options?: ClientOptions);
|
|
6346
|
-
/** The currently signed-in user, or `null`. */
|
|
6347
|
-
get user(): AuthUser | null;
|
|
6348
|
-
/** Whether a token-bearing session is active. */
|
|
6349
|
-
get isAuthenticated(): boolean;
|
|
6350
|
-
/**
|
|
6351
|
-
* Subscribe to session expiry that couldn't be silently recovered (the
|
|
6352
|
-
* user's identity wasn't in memory to re-prove — typically after a reload
|
|
6353
|
-
* where only the token was persisted, and that token has since gone stale).
|
|
6354
|
-
* This is the signal to send the user back to the login screen.
|
|
6355
|
-
*
|
|
6356
|
-
* Returns an unsubscribe function.
|
|
6357
|
-
*
|
|
6358
|
-
* const off = client.onSessionExpired(() => router.push('/login'));
|
|
6359
|
-
*/
|
|
6360
|
-
onSessionExpired(handler: () => void): () => void;
|
|
6361
|
-
/**
|
|
6362
|
-
* Attempt a silent re-auth of the current user. Concurrent callers share a
|
|
6363
|
-
* single in-flight attempt (a stale token usually 401s several requests at
|
|
6364
|
-
* once — we re-auth once, not once per request). On failure the
|
|
6365
|
-
* session-expired event fires so the app can react. Exposed so apps can
|
|
6366
|
-
* also drive recovery proactively (e.g. on `visibilitychange`).
|
|
6367
|
-
*/
|
|
6368
|
-
recoverSession(): Promise<boolean>;
|
|
6369
|
-
}
|
|
6370
|
-
|
|
6371
|
-
/**
|
|
6372
|
-
* `deviceStore` — at-rest persistence for a paired device (the TV).
|
|
6373
|
-
*
|
|
6374
|
-
* A device that has completed {@link HostedAuth.startDevicePairing} holds the
|
|
6375
|
-
* account's **master seed**. Android WebView has no WebAuthn, so the passkey
|
|
6376
|
-
* wrapping used elsewhere isn't available, and re-pairing on every cold start
|
|
6377
|
-
* is not acceptable UX for a set-top box. So the seed is persisted, encrypted:
|
|
6378
|
-
*
|
|
6379
|
-
* | Item | Where | Form |
|
|
6380
|
-
* | ------------------------ | ------------------------------------ | ---- |
|
|
6381
|
-
* | wrapping key | IndexedDB (`muhkoo.device.v1`) | `CryptoKey`, AES-GCM-256, **non-extractable** |
|
|
6382
|
-
* | device identity keypair | IndexedDB (`muhkoo.device.v1`) | `CryptoKeyPair`, ECDSA P-256, private half **non-extractable** |
|
|
6383
|
-
* | sealed identity blob | localStorage `muhkoo.tv.identity.v1` | `{"v":1,"iv":…,"ct":…}` over `JSON{username, commitment, seed(b64), pairedAt}` |
|
|
6384
|
-
*
|
|
6385
|
-
* The split is deliberate — ciphertext in one backend, key in another, and the
|
|
6386
|
-
* key non-extractable so JS can *use* it but not export it.
|
|
6387
|
-
*
|
|
6388
|
-
* ## Be honest about what this is worth
|
|
6389
|
-
*
|
|
6390
|
-
* **This is obfuscation, not protection.** It raises the cost of a casual
|
|
6391
|
-
* `localStorage` dump to "nothing useful", and that is all:
|
|
6392
|
-
*
|
|
6393
|
-
* - **XSS on the device origin** cannot *exfiltrate* the wrapping key (it is
|
|
6394
|
-
* non-extractable) but can absolutely call `decrypt` and read the seed. The
|
|
6395
|
-
* only mitigation is that there is no third-party script in the shell —
|
|
6396
|
-
* enforce a strict CSP with no `unsafe-inline`, no CDN, no remote fonts.
|
|
6397
|
-
* - **ADB / root / a backup extraction / a forensic image recovers the seed.**
|
|
6398
|
-
* The IndexedDB LevelDB files hold the AES key bytes in the clear; WebCrypto
|
|
6399
|
-
* "non-extractable" is a JS-API boundary, not an OS one. **A stolen device is
|
|
6400
|
-
* a stolen identity.**
|
|
6401
|
-
*
|
|
6402
|
-
* That is not an implementation shortcut, it is the shape of the problem: any
|
|
6403
|
-
* wrapping key usable without user interaction lives somewhere equally readable
|
|
6404
|
-
* on a keyboard-less device with no hardware key store exposed to WebView.
|
|
6405
|
-
* Reduce the blast radius with `android:allowBackup="false"`,
|
|
6406
|
-
* `android:debuggable="false"`, a strict CSP, and by storing *only* the 32-byte
|
|
6407
|
-
* seed + username + commitment (never a session token, never a recovery phrase).
|
|
6408
|
-
*
|
|
6409
|
-
* ## Documented upgrade path — no protocol change
|
|
6410
|
-
*
|
|
6411
|
-
* If the native shell exposes a Keystore bridge:
|
|
6412
|
-
*
|
|
6413
|
-
* ```java
|
|
6414
|
-
* @JavascriptInterface public String wrap(String b64plain); // AES-GCM, StrongBox when present
|
|
6415
|
-
* @JavascriptInterface public String unwrap(String b64cipher);
|
|
6416
|
-
* ```
|
|
6417
|
-
*
|
|
6418
|
-
* this module prefers `globalThis.MuhkooKeystore` automatically and falls back
|
|
6419
|
-
* to the WebCrypto scheme otherwise. The Keystore key is hardware-backed and
|
|
6420
|
-
* non-exportable at the OS level, so a copied filesystem image alone becomes
|
|
6421
|
-
* useless. Nothing on the wire, in the endpoint contracts, or in the SDK surface
|
|
6422
|
-
* changes — only the two functions below do, internally.
|
|
6423
|
-
*
|
|
6424
|
-
* ## Degradation
|
|
6425
|
-
*
|
|
6426
|
-
* Nothing here throws at import time. On a runtime with neither backend (SSR,
|
|
6427
|
-
* Workers, Node) the module still loads: {@link loadDeviceIdentity} returns
|
|
6428
|
-
* `null`, {@link persistDeviceIdentity} throws a typed
|
|
6429
|
-
* {@link DeviceStoreUnavailableError} the caller can swallow, and
|
|
6430
|
-
* {@link deviceIdentityKey} falls back to an **in-memory** keypair for the
|
|
6431
|
-
* lifetime of the process (so pairing still works; the device is just never
|
|
6432
|
-
* recognised as "known" on a later run).
|
|
6433
|
-
*/
|
|
6434
|
-
/** localStorage key holding the AES-GCM-sealed identity blob. */
|
|
6435
|
-
declare const DEVICE_IDENTITY_STORAGE_KEY = "muhkoo.tv.identity.v1";
|
|
6436
|
-
/** IndexedDB database holding the non-extractable keys. */
|
|
6437
|
-
declare const DEVICE_KEY_DB_NAME = "muhkoo.device.v1";
|
|
6438
|
-
/** What a paired device persists. The seed **is** the identity — treat it as such. */
|
|
6439
|
-
interface PersistedDeviceIdentity {
|
|
6440
|
-
username: string;
|
|
6441
|
-
/** Decimal-string Poseidon commitment. Pinned: a later pairing that yields a
|
|
6442
|
-
* different commitment for this device must be refused, not adopted. */
|
|
6443
|
-
commitment: string;
|
|
6444
|
-
/** The 32-byte master seed. */
|
|
6445
|
-
seed: Uint8Array;
|
|
6446
|
-
pairedAt: number;
|
|
6447
|
-
}
|
|
6448
|
-
/** Thrown when persistence is impossible (no IndexedDB and/or no localStorage). */
|
|
6449
|
-
declare class DeviceStoreUnavailableError extends Error {
|
|
6450
|
-
constructor(what: string);
|
|
6451
|
-
}
|
|
6452
|
-
/** Key/value store for live `CryptoKey` objects (IndexedDB in the browser). */
|
|
6453
|
-
interface DeviceKeyVault {
|
|
6454
|
-
get(id: string): Promise<unknown | null>;
|
|
6455
|
-
put(id: string, value: unknown): Promise<void>;
|
|
6456
|
-
/** Delete the entire store — used by `clearDeviceIdentity`. */
|
|
6457
|
-
destroy(): Promise<void>;
|
|
6458
|
-
}
|
|
6459
|
-
/** String store for the sealed blob (localStorage in the browser). */
|
|
6460
|
-
interface DeviceBlobStore {
|
|
6461
|
-
get(key: string): string | null;
|
|
6462
|
-
set(key: string, value: string): void;
|
|
6463
|
-
remove(key: string): void;
|
|
6464
|
-
}
|
|
6465
|
-
/** The native Keystore bridge, when the shell provides one (see module docs). */
|
|
6466
|
-
interface MuhkooKeystoreBridge {
|
|
6467
|
-
wrap(b64plain: string): string | Promise<string>;
|
|
6468
|
-
unwrap(b64cipher: string): string | Promise<string>;
|
|
6638
|
+
readonly db: DbNamespace;
|
|
6639
|
+
/** File storage — `client.storage.writeFile(...)`, etc. */
|
|
6640
|
+
readonly storage: StorageNamespace;
|
|
6641
|
+
/** Filesystem over the personal space: directories, versions, real paths. */
|
|
6642
|
+
readonly vfs: VfsNamespace;
|
|
6643
|
+
/** Version control over the VFS: commits, branches, merges — per project. */
|
|
6644
|
+
readonly vcs: VcsNamespace;
|
|
6645
|
+
/** Realtime messaging — `client.message.subscribe(...)`, etc. */
|
|
6646
|
+
readonly message: MessageNamespace;
|
|
6647
|
+
/** Fan-out group spaces — `client.space.createSpace(...)`, etc. */
|
|
6648
|
+
readonly space: SpaceNamespace;
|
|
6649
|
+
/** Programmable Agents — `client.agents.create(appId, …)`, etc. */
|
|
6650
|
+
readonly agents: AgentsNamespace;
|
|
6651
|
+
/** Serverless functions — `client.functions.deploy(appId, …)`, etc. */
|
|
6652
|
+
readonly functions: FunctionsNamespace;
|
|
6653
|
+
/** Access tokens — `client.accessTokens.create(appId, …)`, etc. */
|
|
6654
|
+
readonly accessTokens: AccessTokensNamespace;
|
|
6655
|
+
/** Offline cache + sync — `client.offline.status`, `client.offline.snapshot`, etc. */
|
|
6656
|
+
readonly offline: OfflineManager;
|
|
6657
|
+
private readonly session;
|
|
6658
|
+
private readonly http;
|
|
6659
|
+
/** Subscribers notified when a session expires and can't be recovered. */
|
|
6660
|
+
private readonly sessionExpiredHandlers;
|
|
6661
|
+
/** Shared in-flight recovery so a burst of 401s triggers one re-auth. */
|
|
6662
|
+
private recoverInFlight;
|
|
6663
|
+
constructor(options?: ClientOptions);
|
|
6664
|
+
/** The currently signed-in user, or `null`. */
|
|
6665
|
+
get user(): AuthUser | null;
|
|
6666
|
+
/** Whether a token-bearing session is active. */
|
|
6667
|
+
get isAuthenticated(): boolean;
|
|
6668
|
+
/**
|
|
6669
|
+
* Subscribe to session expiry that couldn't be silently recovered (the
|
|
6670
|
+
* user's identity wasn't in memory to re-prove — typically after a reload
|
|
6671
|
+
* where only the token was persisted, and that token has since gone stale).
|
|
6672
|
+
* This is the signal to send the user back to the login screen.
|
|
6673
|
+
*
|
|
6674
|
+
* Returns an unsubscribe function.
|
|
6675
|
+
*
|
|
6676
|
+
* const off = client.onSessionExpired(() => router.push('/login'));
|
|
6677
|
+
*/
|
|
6678
|
+
onSessionExpired(handler: () => void): () => void;
|
|
6679
|
+
/**
|
|
6680
|
+
* Attempt a silent re-auth of the current user. Concurrent callers share a
|
|
6681
|
+
* single in-flight attempt (a stale token usually 401s several requests at
|
|
6682
|
+
* once — we re-auth once, not once per request). On failure the
|
|
6683
|
+
* session-expired event fires so the app can react. Exposed so apps can
|
|
6684
|
+
* also drive recovery proactively (e.g. on `visibilitychange`).
|
|
6685
|
+
*/
|
|
6686
|
+
recoverSession(): Promise<boolean>;
|
|
6469
6687
|
}
|
|
6470
|
-
/**
|
|
6471
|
-
* Point the store at explicit backends (or `null` to force "unavailable"), and
|
|
6472
|
-
* reset the memoized device keypair. Testing / embedding seam — pass `{}` to
|
|
6473
|
-
* restore the default browser backends.
|
|
6474
|
-
*/
|
|
6475
|
-
declare function configureDeviceStore(opts?: {
|
|
6476
|
-
keys?: DeviceKeyVault | null;
|
|
6477
|
-
blobs?: DeviceBlobStore | null;
|
|
6478
|
-
keystore?: MuhkooKeystoreBridge | null;
|
|
6479
|
-
}): void;
|
|
6480
|
-
/**
|
|
6481
|
-
* True when {@link deviceIdentityKey} had to fall back to an in-memory keypair
|
|
6482
|
-
* because no key vault was available. Such a device can pair, but will never be
|
|
6483
|
-
* recognised as "known" afterwards (so approval always needs a fresh factor).
|
|
6484
|
-
*/
|
|
6485
|
-
declare function deviceIdentityIsEphemeral(): boolean;
|
|
6486
|
-
/**
|
|
6487
|
-
* The device's **persistent** ECDSA P-256 identity keypair, created on first
|
|
6488
|
-
* call. This is not a browser fingerprint — it is a key, and every
|
|
6489
|
-
* `POST /api/auth/device/code` proves possession of it. Without that proof,
|
|
6490
|
-
* "known device" would be an unauthenticated string anyone could claim in order
|
|
6491
|
-
* to skip the approver's re-authentication.
|
|
6492
|
-
*
|
|
6493
|
-
* The private half is non-extractable and never leaves IndexedDB as bytes.
|
|
6494
|
-
*/
|
|
6495
|
-
declare function deviceIdentityKey(): Promise<CryptoKeyPair>;
|
|
6496
|
-
/** `base64url(SHA-256(raw SEC1 device identity public key))` — 43 chars. */
|
|
6497
|
-
declare function deviceFingerprint(): Promise<string>;
|
|
6498
|
-
/** Whether a sealed identity blob exists. Cheap: no crypto, no IndexedDB, no network. */
|
|
6499
|
-
declare function hasDeviceIdentity(): Promise<boolean>;
|
|
6500
|
-
/**
|
|
6501
|
-
* Seal and store the identity. Throws {@link DeviceStoreUnavailableError} when
|
|
6502
|
-
* there is nowhere to put it — callers should treat that as "this device won't
|
|
6503
|
-
* resume", not as a pairing failure.
|
|
6504
|
-
*/
|
|
6505
|
-
declare function persistDeviceIdentity(v: PersistedDeviceIdentity): Promise<void>;
|
|
6506
|
-
/**
|
|
6507
|
-
* Recover the persisted identity, or `null` when there is none / it can't be
|
|
6508
|
-
* opened (wiped IndexedDB, a different browser profile, a corrupted blob).
|
|
6509
|
-
* Never throws — a cold start with unreadable storage must fall back to a fresh
|
|
6510
|
-
* pairing, not to an error screen.
|
|
6511
|
-
*/
|
|
6512
|
-
declare function loadDeviceIdentity(): Promise<PersistedDeviceIdentity | null>;
|
|
6513
|
-
/**
|
|
6514
|
-
* Wipe everything: the sealed blob **and** the whole key database — so the
|
|
6515
|
-
* wrapping key never outlives the ciphertext it opens, and the device identity
|
|
6516
|
-
* key is regenerated (a re-pair is then correctly treated as a new device).
|
|
6517
|
-
*/
|
|
6518
|
-
declare function clearDeviceIdentity(): Promise<void>;
|
|
6519
6688
|
|
|
6520
6689
|
/**
|
|
6521
6690
|
* Three-way line merge.
|
|
@@ -6558,92 +6727,6 @@ declare function merge3Text(base: string, ours: string, theirs: string, labels?:
|
|
|
6558
6727
|
conflicted: boolean;
|
|
6559
6728
|
};
|
|
6560
6729
|
|
|
6561
|
-
/**
|
|
6562
|
-
* Passkey factor (M1.1) — WebAuthn with the **PRF extension**.
|
|
6563
|
-
*
|
|
6564
|
-
* A passkey becomes a device-bound, platform-synced source of a stable secret:
|
|
6565
|
-
* for a fixed salt, the authenticator's PRF returns the same 32 bytes, which we
|
|
6566
|
-
* HKDF into an AES key that wraps the master seed. The server never sees the
|
|
6567
|
-
* passkey — it's purely a local key source; the ZK proof remains the server auth.
|
|
6568
|
-
*
|
|
6569
|
-
* BROWSER ONLY (`navigator.credentials`). The PRF extension fields aren't in the
|
|
6570
|
-
* standard DOM types yet, so they're reached through narrow casts.
|
|
6571
|
-
*/
|
|
6572
|
-
/** Whether this environment can do WebAuthn at all. (PRF support is checked at enroll.) */
|
|
6573
|
-
declare function passkeySupported(): boolean;
|
|
6574
|
-
/**
|
|
6575
|
-
* Whether this browser's authenticator can do the **PRF extension** our passkey
|
|
6576
|
-
* factor needs. A browser can save passkeys (e.g. for password autofill) yet not
|
|
6577
|
-
* expose PRF — PRF is what derives the stable secret we wrap the seed under, so a
|
|
6578
|
-
* passkey without it is useless to us.
|
|
6579
|
-
*
|
|
6580
|
-
* Uses `PublicKeyCredential.getClientCapabilities()` (the `extension:prf` cap)
|
|
6581
|
-
* where available. Returns:
|
|
6582
|
-
* - `true` — PRF is supported,
|
|
6583
|
-
* - `false` — definitively NOT supported (hide the option),
|
|
6584
|
-
* - `null` — couldn't determine (older browser without the capabilities API);
|
|
6585
|
-
* the caller may optimistically allow and let enroll surface a real
|
|
6586
|
-
* error if it turns out PRF isn't there.
|
|
6587
|
-
*/
|
|
6588
|
-
declare function passkeyPrfCapable(): Promise<boolean | null>;
|
|
6589
|
-
/** Best-effort registrable RP id — the host. Apps spanning subdomains should pass
|
|
6590
|
-
* the parent domain (e.g. "muhkoo.dev") explicitly. */
|
|
6591
|
-
declare function defaultRpId(): string;
|
|
6592
|
-
/**
|
|
6593
|
-
* Can a passkey enrolled under `rpId` be used from the CURRENT origin?
|
|
6594
|
-
*
|
|
6595
|
-
* WebAuthn only accepts an RP id that is the origin's own host or a registrable
|
|
6596
|
-
* *parent* of it — so a passkey enrolled on `app.example.com` is unusable on
|
|
6597
|
-
* `app.example.net`, and one enrolled on a full host is unusable from a sibling
|
|
6598
|
-
* host. An app served on BOTH a custom domain and the platform host therefore
|
|
6599
|
-
* needs one passkey per origin.
|
|
6600
|
-
*
|
|
6601
|
-
* Call this BEFORE `navigator.credentials.get()`: passing a foreign rpId throws
|
|
6602
|
-
* a raw "The requested RPID did not match the origin or related origins", which
|
|
6603
|
-
* is a dead end for the user. Checking first lets the caller fall back to normal
|
|
6604
|
-
* sign-in (and offer to enroll a passkey for this origin) instead.
|
|
6605
|
-
*/
|
|
6606
|
-
declare function rpIdUsableForOrigin(rpId: string | undefined | null, host?: string): boolean;
|
|
6607
|
-
/**
|
|
6608
|
-
* Would {@link AuthNamespace.loginWithPasskey} accept a factor recorded with
|
|
6609
|
-
* this `rpId`, from `host`?
|
|
6610
|
-
*
|
|
6611
|
-
* This is deliberately NOT the same as {@link rpIdUsableForOrigin}. That answers
|
|
6612
|
-
* a raw WebAuthn question and is `false` for a missing rpId. But a factor with
|
|
6613
|
-
* NO recorded rpId predates per-origin enrolment, and `loginWithPasskey`
|
|
6614
|
-
* resolves it as `params.rpId ?? host` — so it IS usable, from anywhere.
|
|
6615
|
-
*
|
|
6616
|
-
* Callers deciding "should I even try a passkey here?" must use this one.
|
|
6617
|
-
* Reaching for `rpIdUsableForOrigin` instead silently excludes every legacy
|
|
6618
|
-
* passkey holder, which has locked real users out of production twice.
|
|
6619
|
-
*
|
|
6620
|
-
* The `?? host` fallback applies to nullish only, matching `loginWithPasskey`:
|
|
6621
|
-
* an empty-string rpId stays unusable rather than resolving to the host.
|
|
6622
|
-
*/
|
|
6623
|
-
declare function passkeyUsableFromOrigin(rpId: string | undefined | null, host?: string): boolean;
|
|
6624
|
-
/** Thrown when the account's passkey belongs to a different origin. Callers
|
|
6625
|
-
* should treat this as "no passkey here" and fall back to another factor. */
|
|
6626
|
-
declare class PasskeyOriginError extends Error {
|
|
6627
|
-
readonly code = "passkey_wrong_origin";
|
|
6628
|
-
constructor(message?: string);
|
|
6629
|
-
}
|
|
6630
|
-
interface PasskeyEnrollment {
|
|
6631
|
-
credentialId: Uint8Array;
|
|
6632
|
-
prfSalt: Uint8Array;
|
|
6633
|
-
prfOutput: Uint8Array;
|
|
6634
|
-
}
|
|
6635
|
-
/**
|
|
6636
|
-
* Create a passkey with PRF and return its credential id, a fresh PRF salt, and
|
|
6637
|
-
* the PRF output for that salt. Throws if the authenticator can't do PRF.
|
|
6638
|
-
*/
|
|
6639
|
-
declare function createPasskeyWithPrf(opts: {
|
|
6640
|
-
rpId: string;
|
|
6641
|
-
rpName: string;
|
|
6642
|
-
username: string;
|
|
6643
|
-
}): Promise<PasskeyEnrollment>;
|
|
6644
|
-
/** Evaluate a passkey's PRF for `salt` → 32 bytes. Used at recover/login. */
|
|
6645
|
-
declare function evaluatePasskeyPrf(rpId: string, credentialId: Uint8Array, salt: Uint8Array): Promise<Uint8Array>;
|
|
6646
|
-
|
|
6647
6730
|
/**
|
|
6648
6731
|
* Muhkoo app decorators — declare your app's agent-facing surface in code, then
|
|
6649
6732
|
* "eject" a system prompt you hand to a Programmable Agent.
|
|
@@ -7548,5 +7631,271 @@ declare class KeyringClient implements KeyringTransport {
|
|
|
7548
7631
|
fetchMetadata(): Promise<SpaceMetadata | null>;
|
|
7549
7632
|
}
|
|
7550
7633
|
|
|
7551
|
-
|
|
7552
|
-
|
|
7634
|
+
declare class Logger {
|
|
7635
|
+
private prefix;
|
|
7636
|
+
private logLevel;
|
|
7637
|
+
private console;
|
|
7638
|
+
private showCaller;
|
|
7639
|
+
level: string;
|
|
7640
|
+
constructor(prefix?: string, LOGLEVEL?: string, showCaller?: boolean);
|
|
7641
|
+
private shouldLog;
|
|
7642
|
+
/**
|
|
7643
|
+
* Extract caller information from stack trace
|
|
7644
|
+
*/
|
|
7645
|
+
private getCallerInfo;
|
|
7646
|
+
/**
|
|
7647
|
+
* Format caller info for display
|
|
7648
|
+
*/
|
|
7649
|
+
private formatCallerInfo;
|
|
7650
|
+
setLevel(level: string): void;
|
|
7651
|
+
setShowCaller(show: boolean): void;
|
|
7652
|
+
info(...args: any[]): void;
|
|
7653
|
+
log(...args: any[]): void;
|
|
7654
|
+
verbose(...args: any[]): void;
|
|
7655
|
+
success(...args: any[]): void;
|
|
7656
|
+
warn(...args: any[]): void;
|
|
7657
|
+
error(...args: any[]): void;
|
|
7658
|
+
debug(...args: any[]): void;
|
|
7659
|
+
assert(condition?: boolean, ...data: any[]): void;
|
|
7660
|
+
clear(): void;
|
|
7661
|
+
count(label?: string): void;
|
|
7662
|
+
countReset(label?: string): void;
|
|
7663
|
+
dir(item?: any, options?: any): void;
|
|
7664
|
+
dirxml(...data: any[]): void;
|
|
7665
|
+
group(...data: any[]): void;
|
|
7666
|
+
groupCollapsed(...data: any[]): void;
|
|
7667
|
+
groupEnd(): void;
|
|
7668
|
+
table(tabularData?: any, properties?: string[]): void;
|
|
7669
|
+
time(label?: string): void;
|
|
7670
|
+
timeEnd(label?: string): void;
|
|
7671
|
+
timeLog(label?: string, ...data: any[]): void;
|
|
7672
|
+
trace(...data: any[]): void;
|
|
7673
|
+
profile(label?: string): void;
|
|
7674
|
+
profileEnd(label?: string): void;
|
|
7675
|
+
timeStamp(label?: string): void;
|
|
7676
|
+
}
|
|
7677
|
+
|
|
7678
|
+
declare const log: Logger;
|
|
7679
|
+
|
|
7680
|
+
declare global {
|
|
7681
|
+
interface Console {
|
|
7682
|
+
success(...args: any[]): void;
|
|
7683
|
+
verbose(...args: any[]): void;
|
|
7684
|
+
}
|
|
7685
|
+
}
|
|
7686
|
+
|
|
7687
|
+
/**
|
|
7688
|
+
* Byte conversion helpers usable in both Node and the browser.
|
|
7689
|
+
*
|
|
7690
|
+
* Replaces Node's `Buffer` API for the small set of operations we actually
|
|
7691
|
+
* need in the crypto layer: hex/base64/base64url encode + decode, and array
|
|
7692
|
+
* concatenation. Lets `DoubleRatchet`, `KeyStore`, etc. compile against
|
|
7693
|
+
* platforms without `Buffer` (browsers, CF Workers).
|
|
7694
|
+
*/
|
|
7695
|
+
/** Encode a byte array as a lowercase hex string. */
|
|
7696
|
+
declare function toHex(bytes: Uint8Array): string;
|
|
7697
|
+
/** Decode a hex string into a byte array. Accepts upper/lowercase; rejects odd-length input. */
|
|
7698
|
+
declare function fromHex(hex: string): Uint8Array<ArrayBuffer>;
|
|
7699
|
+
/** Encode a byte array as a standard base64 string. */
|
|
7700
|
+
declare function toBase64(bytes: Uint8Array): string;
|
|
7701
|
+
/** Decode a standard base64 string to bytes. */
|
|
7702
|
+
declare function fromBase64(b64: string): Uint8Array<ArrayBuffer>;
|
|
7703
|
+
/** Encode a byte array as a base64url string (no padding). */
|
|
7704
|
+
declare function toBase64Url(bytes: Uint8Array): string;
|
|
7705
|
+
/** Decode a base64url string (with or without padding) to bytes. */
|
|
7706
|
+
declare function fromBase64Url(b64url: string): Uint8Array<ArrayBuffer>;
|
|
7707
|
+
/** Concatenate any number of byte arrays into a single Uint8Array. */
|
|
7708
|
+
declare function concatBytes(...arrays: (Uint8Array | ArrayBuffer)[]): Uint8Array<ArrayBuffer>;
|
|
7709
|
+
/** Encode a UTF-8 string as bytes. Thin wrapper around TextEncoder for symmetry. */
|
|
7710
|
+
declare function utf8Encode(s: string): Uint8Array<ArrayBuffer>;
|
|
7711
|
+
/** Decode UTF-8 bytes to a string. */
|
|
7712
|
+
declare function utf8Decode(bytes: Uint8Array): string;
|
|
7713
|
+
|
|
7714
|
+
/**
|
|
7715
|
+
* @public
|
|
7716
|
+
* @remarks Asserts the basic type of a variable
|
|
7717
|
+
* @param value - The value to check
|
|
7718
|
+
* @param type - The expected type
|
|
7719
|
+
*
|
|
7720
|
+
* @throws Error - If the type of the value does not match the expected type
|
|
7721
|
+
*
|
|
7722
|
+
*/
|
|
7723
|
+
declare function assertType<T>(value: unknown, typeOrConstructors: string | (new (...args: any[]) => any) | (new (...args: any[]) => any)[]): asserts value is T;
|
|
7724
|
+
/**
|
|
7725
|
+
* @public
|
|
7726
|
+
* @remarks Encodes a string to Base58
|
|
7727
|
+
* @param input - the data to encode, must be a cloneable object
|
|
7728
|
+
* @returns string
|
|
7729
|
+
*/
|
|
7730
|
+
declare function base58Encode(input: ArrayBuffer | string): string;
|
|
7731
|
+
/**
|
|
7732
|
+
* @public
|
|
7733
|
+
* @remarks Decodes a Base58 string to a Uint8Array
|
|
7734
|
+
* @param input - the Base58 string to decode
|
|
7735
|
+
* @returns Uint8Array
|
|
7736
|
+
*/
|
|
7737
|
+
declare function base58Decode(input: string): ArrayBuffer;
|
|
7738
|
+
/**
|
|
7739
|
+
* @public
|
|
7740
|
+
* @remarks Generates a checksum for a given input
|
|
7741
|
+
* @param input - The input to generate a checksum for as a string
|
|
7742
|
+
* @returns string
|
|
7743
|
+
*/
|
|
7744
|
+
declare function generateChecksum(input: string): string;
|
|
7745
|
+
/**
|
|
7746
|
+
* @public
|
|
7747
|
+
* @remarks Verifies a checksum for a given input
|
|
7748
|
+
* @param input - The input to verify as a string
|
|
7749
|
+
* @param checksum - The checksum to verify as a string
|
|
7750
|
+
* @returns boolean
|
|
7751
|
+
*/
|
|
7752
|
+
declare function verifyChecksum(input: string, checksum: string): boolean;
|
|
7753
|
+
/**
|
|
7754
|
+
* @internal
|
|
7755
|
+
* @remarks Generates a random UUID for a given packet
|
|
7756
|
+
* @returns string
|
|
7757
|
+
*/
|
|
7758
|
+
declare function _packetId(): string;
|
|
7759
|
+
/**
|
|
7760
|
+
* @internal
|
|
7761
|
+
* @remarks Generates a random UUID for a given object
|
|
7762
|
+
* @returns string
|
|
7763
|
+
*
|
|
7764
|
+
*/
|
|
7765
|
+
declare function _objectId(): string;
|
|
7766
|
+
/**
|
|
7767
|
+
* @internal
|
|
7768
|
+
* @remarks Generates a random UUID for a given user
|
|
7769
|
+
* @returns string
|
|
7770
|
+
*
|
|
7771
|
+
*/
|
|
7772
|
+
declare function _userId(): string;
|
|
7773
|
+
/**
|
|
7774
|
+
* @internal
|
|
7775
|
+
* @remarks Generates a random UUID for a given account
|
|
7776
|
+
* @returns string
|
|
7777
|
+
*
|
|
7778
|
+
*/
|
|
7779
|
+
declare function _accountId(): string;
|
|
7780
|
+
/**
|
|
7781
|
+
* @internal
|
|
7782
|
+
* @remarks Generates a random UUID for a socket
|
|
7783
|
+
* @returns string
|
|
7784
|
+
*
|
|
7785
|
+
*/
|
|
7786
|
+
declare function _socketId(): string;
|
|
7787
|
+
/**
|
|
7788
|
+
* @internal
|
|
7789
|
+
* @remarks Generates a random UUID for a message
|
|
7790
|
+
* @returns string
|
|
7791
|
+
*/
|
|
7792
|
+
declare function _messageId(): string;
|
|
7793
|
+
/**
|
|
7794
|
+
* @public
|
|
7795
|
+
* @remarks Generates a random UUID and base58 encodes it
|
|
7796
|
+
* @returns string
|
|
7797
|
+
*/
|
|
7798
|
+
declare function generateId(): string;
|
|
7799
|
+
/**
|
|
7800
|
+
* @public
|
|
7801
|
+
* @remarks Serializes data to a base58 encoded string
|
|
7802
|
+
* @param data - The data to serialize
|
|
7803
|
+
* @typeParam T - The type of the data to serialize
|
|
7804
|
+
* @returns base58 encoded string
|
|
7805
|
+
*/
|
|
7806
|
+
declare function serialize<T>(data: T): string;
|
|
7807
|
+
/**
|
|
7808
|
+
* @public
|
|
7809
|
+
* @remarks Deserializes a base58 encoded string to data
|
|
7810
|
+
* @param base58String - The base58 encoded string to deserialize
|
|
7811
|
+
* @returns Deserialized data
|
|
7812
|
+
*/
|
|
7813
|
+
declare function deserialize<T>(base58String: string): T;
|
|
7814
|
+
/**
|
|
7815
|
+
* @public
|
|
7816
|
+
* @remarks Retries a method a specified number of times with a delay between each attempt, this is a decorator
|
|
7817
|
+
* @param retries - The number of times to retry the method
|
|
7818
|
+
* @param delay - The delay in milliseconds between each attempt
|
|
7819
|
+
*/
|
|
7820
|
+
declare function Retry(retries?: number, delay?: number): MethodDecorator;
|
|
7821
|
+
/**
|
|
7822
|
+
* @public
|
|
7823
|
+
* @remarks Waits for a class property to be ready before executing a method, this is a decorator.
|
|
7824
|
+
* @param readyPropertyName - The class property to check for readiness
|
|
7825
|
+
*/
|
|
7826
|
+
declare function Ready(readyPropertyName?: string): MethodDecorator | PropertyDecorator;
|
|
7827
|
+
/**
|
|
7828
|
+
* Converts bytes into a more human-friendly format (e.g., KB, MB, GB).
|
|
7829
|
+
* @internal
|
|
7830
|
+
* @param bytes - The number of bytes to convert.
|
|
7831
|
+
* @param decimals - The number of decimals to display (default is 2).
|
|
7832
|
+
* @returns A formatted string representing the size in appropriate units.
|
|
7833
|
+
*/
|
|
7834
|
+
declare function _formatBytes(bytes: number, decimals?: number): string;
|
|
7835
|
+
/**
|
|
7836
|
+
* Retrieves the IP address(es) for a given hostname using Cloudflare's DNS-over-HTTPS service.
|
|
7837
|
+
*
|
|
7838
|
+
* This function queries the Cloudflare DNS API to resolve the provided hostname to its corresponding
|
|
7839
|
+
* IPv4 address(es) (A records). It returns an array of IP addresses or logs an error if the resolution fails.
|
|
7840
|
+
*
|
|
7841
|
+
* @param hostname - The hostname to resolve (e.g., "example.com").
|
|
7842
|
+
* @returns A promise that resolves to an array of IP addresses (strings) associated with the hostname.
|
|
7843
|
+
* If no IP addresses are found or an error occurs, an empty array or undefined may be returned.
|
|
7844
|
+
* @throws Error If the DNS query fails due to a network error or invalid response from the server.
|
|
7845
|
+
* @public
|
|
7846
|
+
* @example
|
|
7847
|
+
* ```typescript
|
|
7848
|
+
* import { log } from '@muhkoo/connect/utilities';
|
|
7849
|
+
* getIPAddress("example.com").then(ips => log.debug(ips));
|
|
7850
|
+
* // Output: ["93.184.216.34"]
|
|
7851
|
+
* ```
|
|
7852
|
+
*/
|
|
7853
|
+
declare function getIPAddress(hostname: string): Promise<any>;
|
|
7854
|
+
declare function isNumber(value: string | number): boolean;
|
|
7855
|
+
declare function getId(row: {
|
|
7856
|
+
insertId?: number;
|
|
7857
|
+
id?: number;
|
|
7858
|
+
}): number;
|
|
7859
|
+
|
|
7860
|
+
/**
|
|
7861
|
+
* SDK build version — stamped to the console when a {@link Client} is created.
|
|
7862
|
+
*
|
|
7863
|
+
* During active development, bump the `-alpha.N` suffix whenever we ship a build
|
|
7864
|
+
* so we can confirm in the browser console that the page is running the build we
|
|
7865
|
+
* expect (and isn't serving a stale cached bundle). Keep this in sync with
|
|
7866
|
+
* `package.json` `version`.
|
|
7867
|
+
*/
|
|
7868
|
+
declare const VERSION = "0.14.0-alpha.8";
|
|
7869
|
+
|
|
7870
|
+
/**
|
|
7871
|
+
* The SDK's process-global logger.
|
|
7872
|
+
*
|
|
7873
|
+
* Eight modules under `src/` read a bare `appLogger` off `globalThis` rather
|
|
7874
|
+
* than importing one (`src/crypto/{KeyStore,DoubleRatchet,DoubleRatchetManager,
|
|
7875
|
+
* Authenticator}.ts`, `src/core/Client.ts`,
|
|
7876
|
+
* `src/core/namespaces/HostedAuth.ts`, `src/events/EventCore.ts`), so the
|
|
7877
|
+
* binding has to exist before any of them runs, and the `declare global` below
|
|
7878
|
+
* is what makes that typecheck.
|
|
7879
|
+
*
|
|
7880
|
+
* This module is the single place that bootstrap happens. It previously lived
|
|
7881
|
+
* inline in three entry files — `src/browser/index.ts`, `src/workers/index.ts`,
|
|
7882
|
+
* and `src/core/index.ts` (which `src/server/index.ts` picked up as an import
|
|
7883
|
+
* side effect) — three copies that could drift.
|
|
7884
|
+
*
|
|
7885
|
+
* `??=` makes it idempotent, so importing it from several entries in one
|
|
7886
|
+
* process is safe and the first writer wins.
|
|
7887
|
+
*
|
|
7888
|
+
* KNOWN HAZARD: package.json declares `"sideEffects": false`, which entitles a
|
|
7889
|
+
* tree-shaker to drop a module imported purely for effect. The entries import
|
|
7890
|
+
* this one for its `export`s as well, so it survives today — but the durable
|
|
7891
|
+
* fix is to convert those eight bare reads into real imports of `appLogger`
|
|
7892
|
+
* from here, which removes the reliance on a global entirely.
|
|
7893
|
+
*/
|
|
7894
|
+
|
|
7895
|
+
declare global {
|
|
7896
|
+
var appLogger: InstanceType<typeof Logger>;
|
|
7897
|
+
}
|
|
7898
|
+
declare const appLogger: InstanceType<typeof Logger>;
|
|
7899
|
+
|
|
7900
|
+
export { ACCESS_TOKEN_SCOPES, AES_GCM_IV_BYTES, AES_GCM_KEY_BYTES, AES_GCM_TAG_BYTES, AccessTokensNamespace, AgentsNamespace, AuthClient, AuthNamespace, AuthPublicInput, Authenticator, BlockEngine, BroadcastChannel, BroadcastChannelEvents, ChannelExistsError, ChannelNotFoundError, ChunkCipher, CircuitIntegrityError, Client, ConnectivityManager, DEFAULT_AUTH_BASE_URL, DEFAULT_BASE_URL, DEFAULT_BRANCH, DEFAULT_FN_HOST_SUFFIX, DEVICE_IDENTITY_STORAGE_KEY, DEVICE_KEY_DB_NAME, DbCache, DbNamespace, DbTable, DevicePairingError, DeviceRevokedError, DeviceStoreUnavailableError, DoubleRatchet, DoubleRatchetCipher, DoubleRatchetManager, EncryptedSession, EventCore, EventCoreEvents, Field, Field as FieldElement, FileStorage, FunctionsNamespace, HLC, HashKnowledge, HostedAuth, HttpClient, HttpError, IndexedDbStore, KEEPER_MEMBER_ID, KeyStore, KeyringClient, KvCache, KvNamespace, LocalEngineHost, LocalStorageSessionStore, Logger, MemorySessionStore, Message, Message as MessageDefault, MessageNamespace, MuhkooAgent, MuhkooDB, MuhkooFunction, MuhkooSpace, NoopStore, OfflineLockedError, OfflineManager, OutboundQueue, P2P_SUBJECT, PAIRING_CODE_ALPHABET, PAIRING_CODE_LENGTH, PBKDF2_DEFAULT_ITERATIONS, PINNED_CIRCUIT_DIGESTS, PREIMAGE_POK_VERIFICATION_KEY, Packet, PasskeyOriginError, PeerExchange, PeerNetwork, PersonalSpaceClient, Poseidon, PreimagePoK, ROOT_ID, Ready, ReauthRequiredError, ReedSolomonCodec, Repo, Retry, Space as Room, SerializeMessage, SessionState, ShardCache, ShardClient, SharedSpaceClient, SnapshotNamespace, Space, SpaceCache, SpaceKeyring, SpaceNamespace, SpacePacketCipher, SpaceSignaler, StorageNamespace, SyncEngine, VERSION, VFS_ROOT_INFO, VaultUnavailableError, VcsError, VcsNamespace, VfsConflictError, VfsLockedError, VfsNamespace, VfsNotFoundError, WSTransport, WebRtcTransport, WorkerEngineHost, ZERO_HLC, ZkAuth, _accountId, _formatBytes, _messageId, _objectId, _packetId, _socketId, _userId, appLogger, assertType, base58Decode, base58Encode, buildCommitment, canonicalUserCode, clearDeviceIdentity, compareHlc, compilePrograms, concatBytes, configureDeviceStore, createPasskeyWithPrf, decodeFromHex, decorators, decryptAesGcm, defaultCircuitUrls, defaultRpId, defaultSessionStore, deleteBeatsWrite, deleteRegister, deriveAesKeyFromPassphrase, deriveBitsHkdf, deriveIdentity, deriveIdentityFromSeed, deriveMasterSeedFromPassword, deserialize, deviceFingerprint, deviceIdentityIsEphemeral, deviceIdentityKey, dirKey, ejectAgentPrompt, ejectAgentTools, emptyMap, emptyRegister, encodeToHex, encryptAesGcm, evaluatePasskeyPrf, exportPublicKeyBase64, exportPublicKeyHex, formatUserCode, fromBase64, fromBase64Url, fromHex, generateAuthProof, generateChecksum, generateDevicePairingKeypair, generateId, generatePkce, getIPAddress, getId, getMuhkooAppDescriptor, getSubtle, hasDeviceIdentity, headKey, historyKey, initBn128Wasm, initRsWasm, initializeCircuits, isNewer, isNumber, isOfflineCapable, isP2pCapable, isProvisional, loadDeviceIdentity, log, manifestToStat, materializeRow, merge3, merge3Text, mergeKey, mergeMap, mergeMessage, mergeRegister, messageKey, objectKey, pack, padHandle, pairingVerificationCode, passkeyPrfCapable, passkeySupported, passkeyUsableFromOrigin, persistDeviceIdentity, poseidonHash, protocol, provisionalHandle, quickVerify, randomBytes, randomState, refKey, refsIndexKey, rpIdUsableForOrigin, sealSeed, sealSeedToDevice, serialize, shardHash, signMessage, stableStringify, toBase64, toBase64Url, toField, toHex, tombstoneRow, unpack, unsealSeed, unsealSeedFromDevice, unwrapWithPassphrase, utf8Decode, utf8Encode, verify, verifyChecksum, verifyGroth16, verifyHashKnowledge, verifyPreimagePoK, wrapWithPassphrase, writeBeatsTombstone, writeColumns, writeRegister };
|
|
7901
|
+
export type { ACL, ACLEntry, AccessTokenInfo, AccessTokensNamespaceDeps, AgentConfig, AgentCreateInput, AgentDbToolMode, AgentProvisioned, AgentScopeOpts, AgentSkill, AgentToolsConfig, AgentTrigger, AgentTriggerType, AgentUpdateInput, AgentsNamespaceDeps, AttachedPeerNetwork, AuthClientOptions, AuthKeyPair, AuthSuccess, AuthToken, AuthType, AuthUser, BlockStore, Bn128WasmInstance, BroadcastChannelEvent, BroadcastChannelOptions, CachedSpaceMessage, Change, ChannelLike, ChunkKeyMaterial, ChunkManifest, CipherFrame, CipherMessage, CipherMessageHeader, CircuitBufferConfig, CircuitIntegrity, CircuitUrls, ClientOptions, Commit, Conflict, ConnectivityOptions, ConnectivityState, CreateAccessTokenInput, CreatedAccessToken, DbFilterOp, DbNamespaceDeps, DbQuery, DbQueryPage, DbQueryResult, DbWhereCondition, DehydratedKeypair, DehydratedKeys, DeviceBlobStore, DeviceKeyVault, DevicePairingErrorReason, DevicePairingKeypair, DevicePairingPoll, DevicePairingRequest, DevicePairingSession, DirEntry, DirNode, DoubleRatchetCipherDeps, ECDHCurve, EncodeResult, EngineHandle, EngineHost, Entry, EphemeralEvent, EpochKeyProvider, FileEntry, FileManifest, FileNamespaceDeps, FileStat, FileStorageOptions, FunctionCaps, FunctionConfig, FunctionDeployInput, FunctionInvokeOptions, FunctionScopeOpts, FunctionTrigger, FunctionTriggerType, FunctionTriggers, FunctionUpdateInput, FunctionsNamespaceDeps, Groth16Proof, Hasher, Head, HistoryRecord, HlcParts, HlcState, HostedAuthDeps, HttpClientOptions, IncomingFrame, KeyExchangeFrame, KeyPair, Keypair, KeyringClientDeps, KeyringTransport, KvEntry, KvNamespaceDeps, LogEntry, LoginOptions, LwwMap, LwwRegister, Merge3Result, MergeRegionConflict, MergeResult, MessageBody, MessageDeletedEvent, MessageEntry, MessageEvent, MessageHeaders, MessageNamespaceDeps, MessageOp, MessageOptions, MessageSubscription, MuhkooAgentMeta, MuhkooAgentToolsConfig, MuhkooAppDescriptor, MuhkooDBAccess, MuhkooDBMeta, MuhkooFunctionMeta, MuhkooKeystoreBridge, MuhkooMessageEvent, MuhkooSpaceMeta, OAuthProvider, OfflineEntry, OfflineManagerDeps, OfflineStore, OutSink, PacketCipher, PacketHeaders, PacketOptions, PairedDevice, Participant, PasskeyEnrollment, PeerBlockSource, PeerExchangeOptions, PeerId, PeerNetworkOptions, PeerTransport, Permission, PermissionCheckRequest, PermissionCheckResult, PersistedDeviceIdentity, PersonalSpaceClientOptions, Pkce, PublicKeyJWK, QueueEntry, RatchetState, ReceiveResult, RegisterParams, Replayer, Role, SpaceDeps as RoomDeps, SpaceFileMetadata as RoomFileMetadata, Scope, SealedHandoff, SealedHeaders, SealedRecord, Session, SessionOptions, SessionStore, SetOptions, ShardByteCache, ShardClientOptions, SharedSpaceClientOptions, SignalEnvelope, SignalKind, SignalingSpace, SnapshotNamespaceDeps, SnarkProof, SpaceChannelLike, SpaceCursor, SpaceDeps, SpaceFileMetadata, SpaceKeyCache, SpaceKeyringDeps, SpaceMessageEvent, SpaceNamespaceDeps, SpaceOfflineAdapter, StartDevicePairingOptions, StorageChangeEvent, StoreName, StoredSession, Subscription, Tree, TreeDir, TreeEntry, TreeFile, UserIdentity, VaultFactor, VcsNamespaceDeps, VcsNamespaceOptions, VerificationKey, VfsContentStore, VfsNamespaceDeps, VfsStat, VfsStore, WSMessage, WSMessageType, WSTransportOptions, WaitForDevicePairingOptions, WebRtcTransportOptions, WrappedPayload, WriteFileInput, WriteFileOptions, WriteToShardsInput, ZkAuthDeps, ZkAuthenticateRequest, ZkChallengeResponse, ZkCompiled, ZkIdentity, ZkRegisterRequest, ZkRegisterResponse };
|