@muhkoo/connect 0.14.0-alpha.8 → 0.15.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/connect.d.ts CHANGED
@@ -160,14 +160,14 @@ type Permission = 'read' | 'write' | 'delete' | 'admin' | 'share' | 'invite';
160
160
  /**
161
161
  * Role types
162
162
  */
163
- type Role = 'owner' | 'admin' | 'editor' | 'viewer' | 'member';
163
+ type Role$1 = 'owner' | 'admin' | 'editor' | 'viewer' | 'member';
164
164
  /**
165
165
  * ACL Entry - grants permissions to a user (identified by public key)
166
166
  */
167
167
  interface ACLEntry {
168
168
  subject: string;
169
169
  permissions: Permission[];
170
- role?: Role;
170
+ role?: Role$1;
171
171
  expiresAt?: number;
172
172
  conditions?: {
173
173
  ipAllowlist?: string[];
@@ -194,7 +194,7 @@ interface ACL {
194
194
  */
195
195
  interface Participant {
196
196
  publicKey: string;
197
- role: Role;
197
+ role: Role$1;
198
198
  addedAt: number;
199
199
  addedBy: string;
200
200
  customPermissions?: Permission[];
@@ -434,20 +434,51 @@ interface DehydratedKeys {
434
434
  ecdsaPub: string;
435
435
  ecdsaPriv: string;
436
436
  }
437
+ /** A member's own ECDH and ECDSA pairs, held outside the store until {@link KeyStore.replaceOwnKeyPair}. */
438
+ interface OwnKeyPair {
439
+ ecdh: KeyPair;
440
+ ecdsa: AuthKeyPair;
441
+ }
437
442
  declare class KeyStore {
438
443
  private static instance;
439
444
  private _keys;
440
445
  private _authKeys;
446
+ /** The exact pairs marked vault-backed, so a pair written over them later is never taken for them. */
447
+ private _vaultBacked;
441
448
  private constructor();
442
449
  static getInstance(): KeyStore;
443
450
  get keys(): Map<string, KeyPair>;
444
451
  get authKeys(): Map<string, AuthKeyPair>;
445
452
  generateOwnKeyPair(id: string): Promise<KeyPair>;
446
453
  storeRemotePublicKeys(id: string, ecdhPublicKey: CryptoKey, ecdsaPublicKey: CryptoKey): Promise<void>;
454
+ /** Generate an own pair without touching the store; {@link replaceOwnKeyPair} puts it in place. */
455
+ generateKeyPair(): Promise<OwnKeyPair>;
447
456
  hydrateKeyPair(id: string, dehydrated: DehydratedKeys): Promise<void>;
457
+ /** Import a dehydrated pair without touching the store; {@link replaceOwnKeyPair} puts it in place. */
458
+ importKeyPair(dehydrated: DehydratedKeys): Promise<OwnKeyPair>;
448
459
  getKeyPair(id: string): KeyPair | null;
449
460
  getAuthKeyPair(id: string): AuthKeyPair | null;
461
+ /** Record that the pair `id` holds now came from the seed-wrapped vault (ChatKeyVault). */
462
+ markVaultBacked(id: string): void;
463
+ /**
464
+ * Put `pair` in place as `id`'s own pair and mark it vault-backed, in one
465
+ * synchronous step: whatever `id` held before is dropped, so a pair minted
466
+ * while the vault pair was being fetched or generated is never kept and
467
+ * marked in its place (design §3.3).
468
+ */
469
+ replaceOwnKeyPair(id: string, pair: OwnKeyPair): void;
470
+ /**
471
+ * Whether `id`'s own pair was loaded from, or persisted to, the vault in this
472
+ * tab, and is still the very pair that was marked. A Space identity must be
473
+ * vault-backed (design §3.3): any other pair, such as one minted by a locked
474
+ * DM subscription, never takes part, even one that lands over the marked pair.
475
+ */
476
+ isVaultBacked(id: string): boolean;
477
+ /** Drop every key held for `id` (SpecA §6.4 `KeyStore.forget`). */
478
+ forget(id: string): void;
450
479
  dehydrateKeyPair(id: string): Promise<DehydratedKeys>;
480
+ /** Dehydrate a pair that is not (yet) in the store, in the same format as {@link dehydrateKeyPair}. */
481
+ exportKeyPair(pair: OwnKeyPair): Promise<DehydratedKeys>;
451
482
  getRawEcdsaPublicKey(id: string): Promise<Uint8Array | null>;
452
483
  packDehydratedKeys(id: string): Promise<string>;
453
484
  hydrateFromPacked(id: string, masterKey: string): Promise<void>;
@@ -522,7 +553,6 @@ declare class DoubleRatchet {
522
553
  private sessionType;
523
554
  private maxSkip;
524
555
  private overlapPeriod;
525
- private windowSize;
526
556
  constructor(senderId: string, recipientId: string, sessionType: 'global' | 'specific', isClient?: boolean);
527
557
  private hkdf;
528
558
  initializeSession(isClient?: boolean): Promise<void>;
@@ -885,6 +915,8 @@ interface WSTransportOptions {
885
915
  * auto-response. Default `"pong"`.
886
916
  */
887
917
  pongFrame?: string;
918
+ /** A close with one of these codes is terminal: no reconnect, `DISCONNECTED` carries `{code, reason}`; `resume()` restarts. */
919
+ terminalCloseCodes?: number[];
888
920
  }
889
921
  declare class WSTransport extends EventCore {
890
922
  /**
@@ -917,11 +949,14 @@ declare class WSTransport extends EventCore {
917
949
  private pongFrame;
918
950
  private heartbeatTimer;
919
951
  private pongTimer;
952
+ private readonly terminalCloseCodes;
920
953
  constructor(opts: WSTransportOptions);
921
954
  /** Open the socket. Resolves when CONNECTED fires. Rejects on first error. */
922
955
  connect(): Promise<void>;
923
956
  /** Close the socket and stop reconnecting. */
924
957
  disconnect(): void;
958
+ /** Restart reconnecting after a terminal close or `disconnect()`; the next URL comes from `urlProvider`. */
959
+ resume(): void;
925
960
  /**
926
961
  * Send a raw frame. If currently disconnected, the frame is queued and sent
927
962
  * after the next successful reconnect (up to `maxQueueSize`).
@@ -1035,6 +1070,8 @@ declare class BroadcastChannel {
1035
1070
  connect(): Promise<void>;
1036
1071
  /** Close the WebSocket and stop reconnecting. */
1037
1072
  disconnect(): void;
1073
+ /** Reconnect after a terminal close (see `terminalCloseCodes`) or `disconnect()`, on a fresh `urlProvider` URL. */
1074
+ resume(): void;
1038
1075
  /**
1039
1076
  * Send our `keyExchange` frame to the room. Idempotent — only sends on the
1040
1077
  * first call. Call this once your higher-level handshake (e.g. sending a
@@ -1214,6 +1251,68 @@ declare class DoubleRatchetManager {
1214
1251
  private loadState;
1215
1252
  }
1216
1253
 
1254
+ /**
1255
+ * OPRF (Oblivious PRF) — RFC 9497, ristretto255-SHA512, mode 0x00.
1256
+ *
1257
+ * Used to gate the password/recovery factors of the identity vault
1258
+ * ({@link ./vault}). The wrap key for a password factor is
1259
+ * `HKDF(OPRF(serverKey, scrypt(password)))`, so:
1260
+ * - the client cannot derive the wrap key offline (it needs the server's
1261
+ * secret-keyed evaluation), and
1262
+ * - the server learns nothing — it only ever sees a *blinded* group element,
1263
+ * never the password or the resulting key.
1264
+ *
1265
+ * Net effect: a stolen vault blob is NOT offline-crackable; every guess costs a
1266
+ * round-trip to a rate-limited server endpoint. The same OPRF backs the M2
1267
+ * Google/email factors.
1268
+ *
1269
+ * Thin wrapper over `@noble/curves`' vetted `ristretto255_oprf` so the protocol
1270
+ * (blind / blindEvaluate / finalize) lives in one named place. CLIENT uses
1271
+ * {@link oprfBlind} + {@link oprfFinalize}.
1272
+ *
1273
+ * The `oprfDeriveKey` / `oprfBlindEvaluate` helpers here are the SERVER half,
1274
+ * but note they are used only by this repo's tests. The accelerator does NOT
1275
+ * import them: it has an independent implementation in its own
1276
+ * `src/services/oprf.ts`, also built directly on `@noble/curves`. (An earlier
1277
+ * version of this comment claimed the accelerator's VaultDO imported them; it
1278
+ * does not — it imports `@muhkoo/connect` in exactly two files, both for
1279
+ * Groth16 verification.) Any protocol change therefore has to land in BOTH
1280
+ * repos in lockstep.
1281
+ *
1282
+ * KNOWN WEAKNESS — mode 0x00, tracked as audit finding C4 and NOT fixed here.
1283
+ * This uses RFC 9497 base mode: the server returns a bare evaluation with no
1284
+ * DLEQ proof, and {@link oprfFinalize} verifies nothing. A hostile server can
1285
+ * answer `{evaluated: blinded}` (i.e. k=1); the unblinded point is then just
1286
+ * `H(input)` and the wrap key collapses to a pure function of public strings,
1287
+ * destroying the offline-uncrackability guarantee stated above.
1288
+ *
1289
+ * The fix is `ristretto255_oprf.voprf` (mode 0x01) with the server's public key
1290
+ * pinned as a build-time constant and the proof verified on every finalize.
1291
+ * noble's `voprf` variant is already present and unused. It cannot be done
1292
+ * client-side alone: the wire response (`{evaluated}` / `{evaluated, evaluated2}`)
1293
+ * carries no proof field, and the pinned public keys must correspond to the
1294
+ * accelerator's actual K1/K2 secrets. Changing only this file would break login
1295
+ * against the live server.
1296
+ */
1297
+ /**
1298
+ * The RFC 9497 mode this client speaks, declared on every evaluation request.
1299
+ *
1300
+ * Declaring it is what lets the server refuse loudly instead of answering in a
1301
+ * mode we did not ask for — the silent substitution that IS audit finding C4.
1302
+ * A server too old to know the field drops it and answers in base mode, which is
1303
+ * what we asked for anyway, so sending it is safe in both directions and imposes
1304
+ * no deploy ordering.
1305
+ *
1306
+ * NOT to be confused with the vault's KEY VERSION, a different axis that also
1307
+ * uses small integers: there, `2` means "per-account key". Here `2` means POPRF.
1308
+ * The server half of these constants is `accelerator/src/services/oprf.ts` and
1309
+ * MUST hold the same numbers.
1310
+ */
1311
+ declare const OPRF_PROTOCOL_BASE = 0;
1312
+ /** RFC 9497 mode 0x02 — the fix for C4. Reserved; no deployment serves it yet. */
1313
+ declare const OPRF_PROTOCOL_POPRF = 2;
1314
+ type OprfProtocol = typeof OPRF_PROTOCOL_BASE | typeof OPRF_PROTOCOL_POPRF;
1315
+
1217
1316
  /**
1218
1317
  * HTTP client for the accelerator's `/api/auth/*` endpoints.
1219
1318
  *
@@ -1227,6 +1326,21 @@ declare class DoubleRatchetManager {
1227
1326
  * (with the worker's message inlined where available).
1228
1327
  */
1229
1328
 
1329
+ /**
1330
+ * A non-2xx response from `/api/auth/*`, carrying the HTTP status and the
1331
+ * worker's machine-readable `code` alongside its human `error` string.
1332
+ *
1333
+ * Exists because the status used to be discarded: every failure arrived as a
1334
+ * bare `Error` whose only distinguishing feature was its message, so classifying
1335
+ * one meant regex-matching a sentence the server is free to reword. See
1336
+ * `VaultUnavailableError`, which still does exactly that for rate limits.
1337
+ */
1338
+ declare class AuthHttpError extends Error {
1339
+ readonly label: string;
1340
+ readonly status: number;
1341
+ readonly code: string | undefined;
1342
+ constructor(label: string, status: number, code: string | undefined, detail: string);
1343
+ }
1230
1344
  interface ZkRegisterRequest {
1231
1345
  username: string;
1232
1346
  /** Decimal-string Poseidon commitment. */
@@ -1306,16 +1420,24 @@ declare class AuthClient {
1306
1420
  verify(token: string): Promise<{
1307
1421
  username: string;
1308
1422
  }>;
1309
- /** Blinded OPRF evaluation — the server-gated half of a password/recovery wrap key. */
1310
- oprfEvaluate(username: string, blinded: string): Promise<{
1423
+ /**
1424
+ * Blinded OPRF evaluation — the server-gated half of a password/recovery wrap key.
1425
+ *
1426
+ * `oprfProtocol` declares the RFC 9497 mode this client speaks so the server
1427
+ * can refuse rather than answer in a different one. A server too old to know
1428
+ * the field drops it and answers in base mode, so this is safe against any
1429
+ * deployment.
1430
+ */
1431
+ oprfEvaluate(username: string, blinded: string, oprfProtocol?: OprfProtocol): Promise<{
1311
1432
  evaluated: string;
1312
1433
  }>;
1313
1434
  /**
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.
1435
+ * Gated OPRF evaluation (M2 — email/google factors). Requires a live
1436
+ * `verifyToken` from {@link emailOtpVerify}; returns BOTH keys'
1437
+ * evaluations (K1 and K2, both operated by Muhkoo). The recover token is
1438
+ * consumed by this call.
1317
1439
  */
1318
- oprfEvaluateGated(username: string, blinded: string, factorType: "email" | "google", purpose: "enroll" | "recover", verifyToken: string): Promise<{
1440
+ oprfEvaluateGated(username: string, blinded: string, factorType: "email" | "google", purpose: "enroll" | "recover", verifyToken: string, oprfProtocol?: OprfProtocol): Promise<{
1319
1441
  evaluated: string;
1320
1442
  evaluated2: string;
1321
1443
  }>;
@@ -2896,6 +3018,11 @@ interface HostedAuthDeps {
2896
3018
  /** Sleep used by the poll loop. Injectable so tests don't burn wall-clock. */
2897
3019
  sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
2898
3020
  }
3021
+ /**
3022
+ * A same-origin path the app may be sent back to after sign-in. It starts with
3023
+ * "/", not "//", and has no backslash or control characters (design §4.4.9).
3024
+ */
3025
+ declare function isSameOriginPath(p: unknown): p is string;
2899
3026
  /** One in-flight pairing attempt. Hold it; pass it back into poll/wait/cancel. */
2900
3027
  interface DevicePairingSession {
2901
3028
  /** Opaque; carry it back into poll/wait/cancel. Never render it. */
@@ -3048,13 +3175,15 @@ declare class HostedAuth {
3048
3175
  * (App side) Begin hosted login: stash a PKCE verifier + state, then send
3049
3176
  * the browser to the hosted authorize page. Returns the URL it redirects to
3050
3177
  * (also useful for popup mode / tests). `redirectUri` must be registered for
3051
- * the app in the portal.
3178
+ * the app in the portal. `returnTo`, a same-origin path, rides the stash and
3179
+ * comes back from {@link handleCallback}; anything else is refused here.
3052
3180
  */
3053
3181
  login(opts: {
3054
3182
  appId: string;
3055
3183
  redirectUri: string;
3056
3184
  redirect?: boolean;
3057
3185
  prompt?: "login";
3186
+ returnTo?: string;
3058
3187
  }): Promise<string>;
3059
3188
  /**
3060
3189
  * (App side) Send the user to centralized account & security management on
@@ -3071,9 +3200,12 @@ declare class HostedAuth {
3071
3200
  /**
3072
3201
  * (App side) Complete login on the callback URL: verify state, exchange the
3073
3202
  * code (+ PKCE verifier) for the session, unseal the seed from the fragment,
3074
- * establish the session, and scrub the URL. Returns the signed-in user.
3203
+ * establish the session, and scrub the URL. Returns the signed-in user, with
3204
+ * the `returnTo` passed to {@link login} when it is still a same-origin path.
3075
3205
  */
3076
- handleCallback(): Promise<AuthUser>;
3206
+ handleCallback(): Promise<AuthUser & {
3207
+ returnTo?: string;
3208
+ }>;
3077
3209
  /**
3078
3210
  * (Hosted-page side) Finish an /authorize request after the user has
3079
3211
  * authenticated here: seal the in-memory seed, mint the grant, and return
@@ -3299,6 +3431,24 @@ declare function formatUserCode(canonical: string): string;
3299
3431
  declare class VaultUnavailableError extends Error {
3300
3432
  constructor(cause?: unknown);
3301
3433
  }
3434
+ /**
3435
+ * The vault refused the OPRF protocol this client speaks.
3436
+ *
3437
+ * Distinct from {@link VaultUnavailableError} because the remedy is different and
3438
+ * the two used to be conflated: "unavailable" means retry, and a version mismatch
3439
+ * never resolves by retrying — it resolves by updating the app (or the server).
3440
+ *
3441
+ * Critically, this must NOT be caught into the
3442
+ * `vaultSeed ?? deriveMasterSeedFromPassword(...)` fallback in `login`/`unlock`.
3443
+ * That fallback is correct for an account with no password factor, and wrong for
3444
+ * a refusal: it derives a different seed, and on a legacy-migrated account —
3445
+ * where the fallback seed happens to be the right one — it would let
3446
+ * `migrateLegacyPasswordFactor` rewrite the factor as a side effect of a failure
3447
+ * the user was never shown.
3448
+ */
3449
+ declare class VaultProtocolMismatchError extends Error {
3450
+ constructor(cause?: unknown);
3451
+ }
3302
3452
  /**
3303
3453
  * A stored device pairing that the account has since revoked.
3304
3454
  *
@@ -3428,15 +3578,16 @@ declare class ZkAuth {
3428
3578
  recoverWithPhrase(username: string, mnemonic: string, opts?: LoginOptions): Promise<AuthUser>;
3429
3579
  /**
3430
3580
  * Add an **email** as a recovery + login factor (M2 verification-gated:
3431
- * the server only releases the split-key OPRF eval after the address is
3581
+ * the server only releases the gated OPRF eval after the address is
3432
3582
  * OTP-verified). Sends the code, then returns a `confirm` continuation:
3433
3583
  *
3434
3584
  * const { confirm } = await client.auth.zk.enrollEmailFactor("me@x.com");
3435
3585
  * await confirm(codeFromInbox);
3436
3586
  *
3437
3587
  * Requires being signed in with the seed held (like the other enrolls).
3438
- * Custody note: gated factors trade some custody for recoverability — the
3439
- * trade-off should be disclosed in the enroll UI.
3588
+ * Custody note: Muhkoo holds the keys behind gated factors, so it can
3589
+ * recover the seed of any account with an email or Google factor. The
3590
+ * hosted sign-in UI discloses this; a custom enroll UI must too.
3440
3591
  */
3441
3592
  enrollEmailFactor(email: string): Promise<{
3442
3593
  email: string;
@@ -3445,7 +3596,7 @@ declare class ZkAuth {
3445
3596
  /**
3446
3597
  * Recover an account via its enrolled **email** factor. Sends a code to the
3447
3598
  * enrolled address (the response never reveals whether one exists), then
3448
- * `confirm(code)` completes: gated read + split-key eval → unwrap seed →
3599
+ * `confirm(code)` completes: gated read + gated OPRF eval → unwrap seed →
3449
3600
  * normal ZK login. After this, call {@link changePassword} to set a new
3450
3601
  * password.
3451
3602
  *
@@ -3458,11 +3609,14 @@ declare class ZkAuth {
3458
3609
  /**
3459
3610
  * Link a Google account as a recovery + login factor. Requires being signed
3460
3611
  * in with the seed held. `idToken` is a fresh Google ID token (GIS).
3612
+ * Custody note: as with {@link enrollEmailFactor}, Muhkoo holds the keys
3613
+ * behind this factor and can recover the seed of an account that has it.
3614
+ * The hosted sign-in UI discloses this; a custom enroll UI must too.
3461
3615
  */
3462
3616
  enrollGoogleFactor(idToken: string): Promise<void>;
3463
3617
  /**
3464
3618
  * Sign in with Google — verify the ID token, unlock the seed via the gated
3465
- * split-key eval, and establish a session. Same machinery serves "Sign in
3619
+ * OPRF eval, and establish a session. Same machinery serves "Sign in
3466
3620
  * with Google" and "recover with Google"; the account must have linked the
3467
3621
  * factor first.
3468
3622
  */
@@ -3471,7 +3625,8 @@ declare class ZkAuth {
3471
3625
  * Register a brand-new, **passwordless** account whose only factor is Google.
3472
3626
  * A random master seed is generated and wrapped under the Google-gated key;
3473
3627
  * the user can add a password / passkey / phrase later. The ZK registration
3474
- * is identical to a password signup.
3628
+ * is identical to a password signup. Custody note: see
3629
+ * {@link enrollGoogleFactor} — Muhkoo can recover this account's seed.
3475
3630
  */
3476
3631
  registerWithGoogle(username: string, idToken: string): Promise<AuthUser>;
3477
3632
  /**
@@ -3726,9 +3881,10 @@ interface HttpClientOptions {
3726
3881
  * token) or `false` if it couldn't (the `401` propagates as an
3727
3882
  * {@link HttpError}). Wired by {@link Client} to `auth.zk.recover()`.
3728
3883
  *
3729
- * Only consulted by the JSON helpers (`get`/`post`/`del`); the raw
3730
- * {@link fetch} passes 401s straight through so the auth wire calls that
3731
- * power recovery itself can't recurse.
3884
+ * Only consulted by the JSON helpers (`get`/`post`/`del`) and
3885
+ * {@link HttpClient.fetchWithRecovery}; the raw {@link fetch} passes 401s
3886
+ * straight through so the auth wire calls that power recovery itself
3887
+ * can't recurse.
3732
3888
  */
3733
3889
  onUnauthorized?: () => Promise<boolean>;
3734
3890
  }
@@ -3755,6 +3911,15 @@ declare class HttpClient {
3755
3911
  * up credentials without knowing about them.
3756
3912
  */
3757
3913
  readonly fetch: typeof fetch;
3914
+ /**
3915
+ * {@link fetch} with the JSON helpers' stale-session recovery: a 401 on a
3916
+ * request made while a session token is held runs `onUnauthorized` once
3917
+ * and, if that recovered, replays the request once with the fresh token.
3918
+ * The final response is returned whatever its status, never thrown. The
3919
+ * replay re-sends `init` as given, so its body must be re-sendable (a
3920
+ * string or bytes, not a stream).
3921
+ */
3922
+ readonly fetchWithRecovery: typeof fetch;
3758
3923
  /** True when `input` resolves to {@link baseOrigin} (relative URLs always do). */
3759
3924
  private isSameOrigin;
3760
3925
  get<T = unknown>(path: string): Promise<T>;
@@ -3919,8 +4084,9 @@ interface OfflineStore {
3919
4084
  * mutation by re-invoking the real namespace method (with an `isReplay` flag so
3920
4085
  * it doesn't re-enqueue). `drain()` replays entries in HLC order:
3921
4086
  * - success → remove the entry;
3922
- * - permanent failure (4xx) → drop it and surface a `SYNC_ERROR` (a write the
3923
- * server will never accept must not wedge the queue forever);
4087
+ * - permanent failure (4xx, or a Space whose member list refuses this user)
4088
+ * → drop it and surface a `SYNC_ERROR` (a write the server will never
4089
+ * accept must not wedge the queue forever);
3924
4090
  * - transient/network failure → stop draining and leave the rest for the next
3925
4091
  * reconnect.
3926
4092
  *
@@ -4500,6 +4666,140 @@ declare class StorageNamespace {
4500
4666
  private space;
4501
4667
  }
4502
4668
 
4669
+ /**
4670
+ * Wire types of the e2e-channels trust layer (spec 2026-09-27 §3.2, §3.5,
4671
+ * §4.2.4, §5.3.1). These shapes are signed and stored: never rename a field.
4672
+ *
4673
+ * Every string field holds the exact string that was transmitted; the
4674
+ * canonical encoders in `canonical.ts` sign those strings as they are.
4675
+ *
4676
+ * Ported from the accelerator's `src/trust/types.ts` (the server-only app
4677
+ * roster types are left out); the shared shapes must stay identical.
4678
+ */
4679
+ type Visibility = "private" | "public";
4680
+ type HistoryPolicy$1 = "static" | "rotate";
4681
+ type MemberRole = "viewer" | "editor";
4682
+ type SvcMode = "addressed" | "all";
4683
+ type MlogOp = "genesis" | "add" | "role" | "remove" | "leave" | "join" | "link" | "unlink" | "redeem" | "svc" | "retire";
4684
+ /** `legacyCommits` is `{}` for a new Space; values are b64url of 32 bytes, keys canonical decimal epochs. */
4685
+ interface GenesisBody {
4686
+ visibility: Visibility;
4687
+ policy: HistoryPolicy$1;
4688
+ appId: string;
4689
+ baseEpoch: number;
4690
+ legacyCommits: Record<string, string>;
4691
+ }
4692
+ interface AddBody {
4693
+ member: string;
4694
+ memberIk: string;
4695
+ role: MemberRole;
4696
+ }
4697
+ interface RoleBody {
4698
+ member: string;
4699
+ role: MemberRole;
4700
+ }
4701
+ interface RemoveBody {
4702
+ member: string;
4703
+ }
4704
+ type LeaveBody = Record<string, never>;
4705
+ /** `via` is a linkId, or "" for no link. */
4706
+ interface JoinBody {
4707
+ via: string;
4708
+ }
4709
+ interface LinkBody {
4710
+ linkId: string;
4711
+ role: MemberRole;
4712
+ standing: boolean;
4713
+ expiresAt: number;
4714
+ maxUses: number;
4715
+ linkEcdhPub: string;
4716
+ linkSignPub: string;
4717
+ keyCommits: Record<string, string>;
4718
+ }
4719
+ interface UnlinkBody {
4720
+ linkId: string;
4721
+ }
4722
+ interface RedeemBody {
4723
+ linkId: string;
4724
+ }
4725
+ interface SvcBody {
4726
+ member: string;
4727
+ action: "add" | "remove";
4728
+ ecdhPub: string;
4729
+ ecdsaPub: string;
4730
+ mode: SvcMode;
4731
+ rosterSeq: number;
4732
+ }
4733
+ interface RetireBody {
4734
+ member: string;
4735
+ reason: string;
4736
+ }
4737
+ interface MlogBodies {
4738
+ genesis: GenesisBody;
4739
+ add: AddBody;
4740
+ role: RoleBody;
4741
+ remove: RemoveBody;
4742
+ leave: LeaveBody;
4743
+ join: JoinBody;
4744
+ link: LinkBody;
4745
+ unlink: UnlinkBody;
4746
+ redeem: RedeemBody;
4747
+ svc: SvcBody;
4748
+ retire: RetireBody;
4749
+ }
4750
+ interface EntryBase {
4751
+ v: 1;
4752
+ spaceId: string;
4753
+ /** 0 = genesis; −1 only in an enable intent (§5.3.3), never stored. */
4754
+ seq: number;
4755
+ /** hash of entry seq−1; "" at genesis and in an intent. */
4756
+ prev: string;
4757
+ /** Signer member id; "" in a server-written `retire`. */
4758
+ by: string;
4759
+ /** Signer IK (b64 or hex raw P-256), exactly as sent; "" in a `retire`. */
4760
+ ik: string;
4761
+ /** Signer clock, informational. */
4762
+ at: number;
4763
+ /** b64 P1363 ECDSA-P256-SHA256 by `ik` over entryBytes; "" only in a `retire`. */
4764
+ sig: string;
4765
+ /** Link-sign signature over entryBytes (`link`, `redeem`, `join` with `via`). */
4766
+ lsig?: string;
4767
+ }
4768
+ type MembershipEntry<O extends MlogOp = MlogOp> = {
4769
+ [K in O]: EntryBase & {
4770
+ op: K;
4771
+ body: MlogBodies[K];
4772
+ };
4773
+ }[O];
4774
+ type UnsignedEntry<O extends MlogOp = MlogOp> = {
4775
+ [K in O]: Omit<MembershipEntry<K>, "sig" | "lsig">;
4776
+ }[O];
4777
+ interface ChatKeyCert {
4778
+ v: 1;
4779
+ ik: string;
4780
+ ecdh: string;
4781
+ ecdsa: string;
4782
+ sig: string;
4783
+ }
4784
+ interface EpochCommit {
4785
+ v: 1;
4786
+ spaceId: string;
4787
+ epoch: number;
4788
+ prevKeyed: number;
4789
+ keyCommit: string;
4790
+ headSeq: number;
4791
+ headHash: string;
4792
+ sole: boolean;
4793
+ by: string;
4794
+ ik: string;
4795
+ at: number;
4796
+ sig: string;
4797
+ }
4798
+ interface MlogHead {
4799
+ seq: number;
4800
+ hash: string;
4801
+ }
4802
+
4503
4803
  /**
4504
4804
  * Shared types for the Space fan-out group-encryption layer.
4505
4805
  *
@@ -4509,6 +4809,7 @@ declare class StorageNamespace {
4509
4809
  * which is what lets the server persist + replay messages as history while
4510
4810
  * staying blind to their contents.
4511
4811
  */
4812
+
4512
4813
  /**
4513
4814
  * How a space treats history across membership changes.
4514
4815
  * - `static`: one group key for all time (epoch 0). New members can read the
@@ -4556,6 +4857,8 @@ interface JoinRequest {
4556
4857
  /** Epoch the newcomer wants a key for (defaults to current). */
4557
4858
  desiredEpoch?: number;
4558
4859
  ts?: number;
4860
+ /** The account IK's certificate over the two identity strings above (design §3.2). Optional: a join never requires one. */
4861
+ keyCert?: ChatKeyCert;
4559
4862
  }
4560
4863
  /** A space member's published identity keys (the keyring directory). */
4561
4864
  interface RosterMember {
@@ -4710,6 +5013,18 @@ interface KeyringTransport {
4710
5013
  /** Add `username` to a private channel's membership allowlist. */
4711
5014
  invite(username: string): Promise<void>;
4712
5015
  fetchMetadata(): Promise<SpaceMetadata | null>;
5016
+ /** GET /keyring/log (design §3.6): entries with seq > since, ascending, ≤ limit; head null = no log. */
5017
+ fetchLog?(since?: number, limit?: number): Promise<{
5018
+ entries: MembershipEntry[];
5019
+ head: MlogHead | null;
5020
+ }>;
5021
+ /** POST /keyring/log {entry, ec?}. */
5022
+ postLogEntry?(entry: MembershipEntry, ec?: EpochCommit): Promise<{
5023
+ ok: true;
5024
+ head: MlogHead;
5025
+ }>;
5026
+ /** Status of GET /keyring/state, or "network" when the request fails. */
5027
+ probeState?(): Promise<number | "network">;
4713
5028
  }
4714
5029
  /** PersonalSpace-backed cache of this member's group keys (epoch → base64 key). */
4715
5030
  interface SpaceKeyCache {
@@ -4729,6 +5044,10 @@ interface SpaceKeyringDeps {
4729
5044
  transport: KeyringTransport;
4730
5045
  cache?: SpaceKeyCache;
4731
5046
  historyPolicy?: HistoryPolicy;
5047
+ /** Mints the IK certificate for this member's chat keys (design §3.2). Absent, or resolving undefined, means none. */
5048
+ keyCert?: () => Promise<ChatKeyCert | undefined>;
5049
+ /** Whether a key may be wrapped to `memberId`; absent means anyone. On a signed Space: log members and the keeper only. */
5050
+ canWrapTo?: (memberId: string) => boolean;
4732
5051
  }
4733
5052
  declare class SpaceKeyring implements EpochKeyProvider {
4734
5053
  private readonly deps;
@@ -4745,7 +5064,11 @@ declare class SpaceKeyring implements EpochKeyProvider {
4745
5064
  * this window won't re-fetch (the roster hasn't changed that fast); genuine
4746
5065
  * membership changes use `{ force: true }` (join requests / history loads). */
4747
5066
  private static readonly DIR_COOLDOWN_MS;
5067
+ private readonly keyListeners;
4748
5068
  constructor(deps: SpaceKeyringDeps);
5069
+ /** Notified whenever this keyring gains a key (bootstrap, cache hit, pull). Returns an unsubscribe. */
5070
+ onKeysChanged(h: () => void): () => void;
5071
+ private notifyKeys;
4749
5072
  /** The cached ECDSA verify key for a member, if known. */
4750
5073
  ecdsaKeyFor(memberId: string): CryptoKey | undefined;
4751
5074
  /**
@@ -4763,6 +5086,10 @@ declare class SpaceKeyring implements EpochKeyProvider {
4763
5086
  currentEpoch(): number;
4764
5087
  keyForEpoch(epoch: number): Uint8Array | undefined;
4765
5088
  hasAnyKey(): boolean;
5089
+ /** The raw 32-byte group key for `epoch`, for its key commitment (design §4.2.4). */
5090
+ rawKey(epoch: number): Uint8Array | undefined;
5091
+ /** Whether a key may be wrapped to `memberId` (design §4.5.1: on a signed Space, log members and the keeper only). */
5092
+ canWrapTo(memberId: string): boolean;
4766
5093
  /** Hydrate keys from the PersonalSpace cache, if available. Best-effort —
4767
5094
  * a cache miss/failure (e.g. not unlocked) must not block joining. */
4768
5095
  loadFromCache(): Promise<boolean>;
@@ -4772,7 +5099,8 @@ declare class SpaceKeyring implements EpochKeyProvider {
4772
5099
  /**
4773
5100
  * Post a join request so an online key-holder can wrap a key for us. Also
4774
5101
  * publishes our identity keys in the member directory (so others can verify
4775
- * our message signatures, and the keeper can wrap to us).
5102
+ * our message signatures, and the keeper can wrap to us). With a `keyCert`
5103
+ * dep it also carries the IK certificate over exactly those two strings.
4776
5104
  */
4777
5105
  requestKey(desiredEpoch?: number): Promise<void>;
4778
5106
  /**
@@ -4810,6 +5138,124 @@ declare class SpaceKeyring implements EpochKeyProvider {
4810
5138
  invite(username: string): Promise<void>;
4811
5139
  }
4812
5140
 
5141
+ type Role = "owner" | "editor" | "viewer";
5142
+ interface LogMember {
5143
+ id: string;
5144
+ ik: string;
5145
+ role: Role;
5146
+ since: number;
5147
+ }
5148
+
5149
+ /** What LogSync needs from core (kept structural so spaces/ never imports core/). */
5150
+ interface LogSyncDeps {
5151
+ spaceId: string;
5152
+ me: () => string;
5153
+ appId: () => Promise<string>;
5154
+ commitment: () => string | null;
5155
+ transport: {
5156
+ fetchLog(since?: number, limit?: number): Promise<{
5157
+ entries: MembershipEntry[];
5158
+ head: MlogHead | null;
5159
+ }>;
5160
+ postLogEntry(entry: MembershipEntry, ec?: EpochCommit): Promise<{
5161
+ head: MlogHead;
5162
+ }>;
5163
+ };
5164
+ signer: {
5165
+ sign(b: Uint8Array): Promise<string>;
5166
+ identityKeyRaw(): Promise<string>;
5167
+ };
5168
+ directory: {
5169
+ lookup(username: string): Promise<string>;
5170
+ };
5171
+ pins: {
5172
+ get(doc: "identity" | "spaces", key: string): Promise<string | undefined>;
5173
+ pin(doc: "identity" | "spaces", entries: Record<string, string>): Promise<{
5174
+ conflicts: string[];
5175
+ }>;
5176
+ };
5177
+ store: () => Promise<{
5178
+ get<T>(k: string): Promise<T | undefined>;
5179
+ put(k: string, v: unknown): Promise<void>;
5180
+ }>;
5181
+ }
5182
+ interface MemberView {
5183
+ id: string;
5184
+ role: Role;
5185
+ ik: string;
5186
+ fingerprint: string;
5187
+ identityChanged: boolean;
5188
+ }
5189
+ type IntegrityState = {
5190
+ broken: false;
5191
+ } | {
5192
+ broken: true;
5193
+ reason: string;
5194
+ };
5195
+ /**
5196
+ * One Space's membership log, as this client has verified it (design §3.6–§3.7).
5197
+ * Syncs the whole log from −1 on each call (slice logs are tiny, so no entry
5198
+ * cache), checks the head against the high-water mark kept in `muhkoo.trust`,
5199
+ * checks the genesis against the pins (TOFU against the directory the first
5200
+ * time), and pins every IK a verified `add` names (design §3.4).
5201
+ */
5202
+ declare class LogSync {
5203
+ private readonly deps;
5204
+ private log;
5205
+ private enabled;
5206
+ private integrityState;
5207
+ private changed;
5208
+ private fps;
5209
+ private listeners;
5210
+ private timer;
5211
+ private running;
5212
+ private followUp;
5213
+ constructor(deps: LogSyncDeps);
5214
+ get logEnabled(): boolean;
5215
+ get integrity(): IntegrityState;
5216
+ get signer(): LogSyncDeps["signer"];
5217
+ get directory(): LogSyncDeps["directory"];
5218
+ member(id: string): LogMember | undefined;
5219
+ isMember(id: string): boolean;
5220
+ members(): MemberView[];
5221
+ onChange(h: () => void): () => void;
5222
+ /**
5223
+ * An `mlog` frame marks the log stale; sync once a burst of frames settles.
5224
+ * A failure is logged with its code and reported through `onChange`, never
5225
+ * thrown; integrity is left as it was, and the next frame or open retries.
5226
+ */
5227
+ scheduleSync(): void;
5228
+ private hwKey;
5229
+ /** Whether this client holds a high-water mark for this Space, i.e. has verified its log before (design §3.7 item 3). */
5230
+ hasHighWater(): Promise<boolean>;
5231
+ /**
5232
+ * Fetch and verify the whole log. Single-flight: a call made while a sync
5233
+ * runs does not start a second one alongside it. It waits for exactly one
5234
+ * follow-up run, which starts once the running one ends, so it sees whatever
5235
+ * prompted the call; every call made in the meantime shares that follow-up.
5236
+ */
5237
+ sync(): Promise<void>;
5238
+ private startSync;
5239
+ private runSync;
5240
+ /** Genesis trust (design §3.7 item 2): the pinned genesis hash, else TOFU with the owner's IK checked against the directory. */
5241
+ private checkGenesis;
5242
+ /** "s:" + b64url(SHA-256(utf8(spaceId))), the spaces-v1 key; computed exactly as PinStore.spacePinKey, which this layer may not import. */
5243
+ private spacePinKey;
5244
+ /** Pin every IK a verified add names (design §3.4 row 2); a different pinned IK flags the member (identity-changed). */
5245
+ private pinMembers;
5246
+ /**
5247
+ * Sign and append one entry. On `409 Log moved`: re-sync, re-sign against the
5248
+ * new head and retry, at most 3 times with jitter (design §9.2 I9). Nothing is
5249
+ * signed while integrity is broken, including after a re-sync that breaks it.
5250
+ * Any other refusal → SpaceTrustError("log-refused") carrying the server's code and reason.
5251
+ * Once the server has stored the entry, its `{seq}` is returned even if the sync confirming it fails.
5252
+ */
5253
+ append(op: MembershipEntry["op"], body: Record<string, unknown>, ec?: (hash: string, entry: UnsignedEntry) => Promise<EpochCommit>): Promise<{
5254
+ seq: number;
5255
+ }>;
5256
+ private emit;
5257
+ }
5258
+
4813
5259
  /**
4814
5260
  * `Space` — a client handle for one shared space.
4815
5261
  *
@@ -4927,6 +5373,11 @@ interface EphemeralEvent {
4927
5373
  interface MessageDeletedEvent {
4928
5374
  handle: number;
4929
5375
  }
5376
+ type AdmissionState = "locked" | "requesting" | "keyed" | "not-member" | "identity-mismatch" | "integrity";
5377
+ /** WebSocket close code for a refused private-Space socket (design §3.6; accelerator NOT_MEMBER_CLOSE_CODE). */
5378
+ declare const NOT_MEMBER_CLOSE_CODE = 4403;
5379
+ /** At most one resume after a 4403 per Space per this interval (design §6.7). */
5380
+ declare const NOT_MEMBER_RECHECK_MS = 60000;
4930
5381
  interface SpaceDeps {
4931
5382
  /** Transport room id. For fan-out spaces this is the encoded space pubkey. */
4932
5383
  name: string;
@@ -4961,6 +5412,21 @@ interface SpaceDeps {
4961
5412
  deferShardUpload?: (hash: string) => Promise<void>;
4962
5413
  /** Offline message cache + send queue. Undefined ⇒ no offline behavior. */
4963
5414
  offline?: SpaceOfflineAdapter;
5415
+ /** Locked session (design §3.3): no keyring, never connects; the app re-opens after sign-in. */
5416
+ locked?: boolean;
5417
+ /** The Space's membership log (a signed private Space), when this client has trust wired. */
5418
+ logSync?: LogSync;
5419
+ /** Visibility the Space was created with; only a private Space gets a genesis (design §3.8). */
5420
+ visibility?: "private" | "public";
5421
+ /** This app's id, which a genesis names (design §3.5). */
5422
+ appId?: () => Promise<string>;
5423
+ /**
5424
+ * Status of GET /keyring/state after the client's silent 401 recovery, or "network": asked after a
5425
+ * 4403 close before believing it (design §6.9).
5426
+ */
5427
+ probeState?: () => Promise<number | "network">;
5428
+ /** True while the session holds a token, so a reconnect must never go ticketless. */
5429
+ hasSession?: () => boolean;
4964
5430
  }
4965
5431
  /** The subset of {@link BroadcastChannel} a {@link Space} drives. */
4966
5432
  interface SpaceChannelLike {
@@ -4973,13 +5439,22 @@ interface SpaceChannelLike {
4973
5439
  sendRaw(frame: unknown): void;
4974
5440
  peers?(): string[];
4975
5441
  isConnected?(): boolean;
5442
+ /** Reconnect after a terminal close, on a fresh URL. */
5443
+ resume?(): void;
4976
5444
  }
4977
5445
  declare class Space {
4978
5446
  private readonly deps;
4979
5447
  readonly name: string;
4980
5448
  readonly historyPolicy: HistoryPolicy;
4981
5449
  private channel;
4982
- private readonly pending;
5450
+ /**
5451
+ * The log turned this user away and closed `channel` (design §3.7). Its
5452
+ * single-use ticket is spent, so it is never dialled again: the next
5453
+ * `connect()` checks the log and opens a new channel on a fresh ticket.
5454
+ */
5455
+ private channelSpent;
5456
+ /** Every raw channel listener (`on`), attached to each channel this Space opens. */
5457
+ private readonly listeners;
4983
5458
  private readonly cipher;
4984
5459
  /** Per-instance event bus for fan-out events (isolated across spaces). */
4985
5460
  private readonly events;
@@ -4987,12 +5462,52 @@ declare class Space {
4987
5462
  private catchUpUnsub;
4988
5463
  /** Optional P2P swarm for this Space (block exchange among members). */
4989
5464
  private peerNet;
5465
+ private _admission;
5466
+ /** Whether admission follows the log's changes yet (subscribed once, on the first open the log admits). */
5467
+ private admissionWired;
5468
+ /** When the last resume after a 4403 ran (design §6.7: at most one per {@link NOT_MEMBER_RECHECK_MS}). */
5469
+ private lastResumeAt;
5470
+ private resumeTimer;
5471
+ /** Bumped by `disconnect()`: a /keyring/state answer that arrives after it is dropped. */
5472
+ private teardowns;
4990
5473
  constructor(deps: SpaceDeps);
4991
5474
  /** The encoded space id (same as the transport name for fan-out spaces). */
4992
5475
  get id(): string;
4993
5476
  /** The keyring backing this space's group keys, if any. */
4994
5477
  get keyring(): SpaceKeyring | undefined;
4995
- /** Open the space websocket (resolves the ticket + identity lazily). */
5478
+ /** Where this member stands in this Space (design §4.5.12, as the slice ships it). */
5479
+ get admission(): AdmissionState;
5480
+ /** Subscribe to admission changes. Returns an unsubscribe. */
5481
+ onAdmission(h: (s: AdmissionState) => void): () => void;
5482
+ protected setAdmission(s: AdmissionState): void;
5483
+ /** The log turned this user away; nothing reconnects until it admits them again. */
5484
+ private denied;
5485
+ /**
5486
+ * Sync the membership log and derive admission from it, before any ticket
5487
+ * (design §3.4, §3.7, §4.5.12). A 403 from the log route is not-member and a
5488
+ * 401 is locked. Any other failure is fatal only where this client has
5489
+ * verified the log before (it holds a high-water mark). Otherwise the Space
5490
+ * opens as unlogged this time, and the next open or `mlog` frame syncs again.
5491
+ */
5492
+ private admitByLog;
5493
+ /** Re-derive admission from the verified log (design §3.4 rows, §3.7, §4.5.12). */
5494
+ private evaluateAdmission;
5495
+ /**
5496
+ * A 4403 alone never shows not-member: ask /keyring/state first (design §6.9, AV12).
5497
+ * 403 is not-member and 401, which `probeState` returns only once a silent re-auth has failed, is
5498
+ * locked; either closes the channel for good, like a log refusal. Any
5499
+ * other answer (404 while step I has no state route, 2xx, 5xx, a network error) resumes, at most
5500
+ * once per {@link NOT_MEMBER_RECHECK_MS}.
5501
+ */
5502
+ private onNotMemberClose;
5503
+ /** Close the socket of a user the log turned away, and never dial that channel again (design §3.7). */
5504
+ private dropChannel;
5505
+ /**
5506
+ * Open the space websocket (resolves the ticket + identity lazily). On a
5507
+ * Space with a membership log, the log is checked first: a user it does not
5508
+ * list, or lists under another identity key, is refused with
5509
+ * {@link AdmissionDeniedError} before any ticket is fetched (design §4.5.12).
5510
+ */
4996
5511
  connect(): Promise<void>;
4997
5512
  disconnect(): void;
4998
5513
  /** Attach a P2P swarm so this Space's file reads/writes can use peers. */
@@ -5008,7 +5523,7 @@ declare class Space {
5008
5523
  gossipToPeers(data: Uint8Array): void;
5009
5524
  /** Subscribe to peer gossip ({@link gossipToPeers}). Returns an unsubscribe fn. */
5010
5525
  onPeerGossip(handler: (from: string, data: Uint8Array) => void): () => void;
5011
- /** Subscribe to a raw channel event. Buffered until `connect()` if early. */
5526
+ /** Subscribe to a raw channel event. Buffered until `connect()` if early, and kept across a new channel. */
5012
5527
  on(event: string, handler: Listener): void;
5013
5528
  off(event: string, handler: Listener): void;
5014
5529
  /** Advertise our keyExchange to the room (idempotent per connection). */
@@ -5092,7 +5607,7 @@ declare class Space {
5092
5607
  timeoutMs?: number;
5093
5608
  pollMs?: number;
5094
5609
  }): Promise<boolean>;
5095
- /** Creator path: mint the initial group key, then connect. */
5610
+ /** Creator path: mint epoch 0; for a private Space, sign the genesis + ec(0) BEFORE connecting (design §4.5.10). */
5096
5611
  create(): Promise<void>;
5097
5612
  /** Wrap the group key for a newcomer and post it (key-holder action). */
5098
5613
  admit(memberId: string, identityEcdhPub: string): Promise<void>;
@@ -5103,6 +5618,29 @@ declare class Space {
5103
5618
  memberId: string;
5104
5619
  identityEcdhPub: string;
5105
5620
  }>): Promise<number>;
5621
+ /** Whether this Space has a verified membership log. */
5622
+ get logEnabled(): boolean;
5623
+ /** The verified member list, or null when this Space has no log. */
5624
+ membership(): MemberView[] | null;
5625
+ /** Called with the member list (null when unlogged) each time the log is re-synced. Returns an unsubscribe. */
5626
+ onMembership(h: (m: MemberView[] | null) => void): () => void;
5627
+ private requireLog;
5628
+ /** Add by exact username (design §3.8; the owner, or an editor for viewers). Looks the IK up in the directory. */
5629
+ add(username: string, opts?: {
5630
+ role?: "viewer" | "editor";
5631
+ }): Promise<{
5632
+ seq: number;
5633
+ }>;
5634
+ /** Remove a member. In this build it cuts them off at the relay; it does not rotate the key. */
5635
+ remove(memberId: string): Promise<{
5636
+ seq: number;
5637
+ }>;
5638
+ setRole(memberId: string, role: "viewer" | "editor"): Promise<{
5639
+ seq: number;
5640
+ }>;
5641
+ leave(): Promise<{
5642
+ seq: number;
5643
+ }>;
5106
5644
  putFile(file: File | Blob | Uint8Array, metadata: SpaceFileMetadata, opts?: {
5107
5645
  onProgress?: (completed: number, total: number) => void;
5108
5646
  /**
@@ -5295,19 +5833,27 @@ declare class ChatKeyVault {
5295
5833
  private readonly store;
5296
5834
  constructor(store: ChatKeyStore);
5297
5835
  /**
5298
- * Register-time: ensure a keypair exists for `memberId`, seed-wrap the
5299
- * dehydrated form, and persist it to the personal space. Idempotent — reuses
5300
- * an existing in-tab keypair rather than minting a second one.
5836
+ * Register-time: ensure a vault-backed keypair exists for `memberId`,
5837
+ * seed-wrap it and persist it. Idempotent for a pair that is already
5838
+ * vault-backed. Otherwise a fresh pair is generated, persisted, and only
5839
+ * then swapped in, replacing whatever the tab held: a throwaway key is never
5840
+ * persisted as the stable one, and a failed persist leaves the tab's keys
5841
+ * as they were, with nothing marked.
5301
5842
  */
5302
5843
  provision(memberId: string, seedSecret: string): Promise<void>;
5303
5844
  /**
5304
- * Sign-in / unlock time: fetch the wrapped blob, unwrap with the seed, and
5305
- * hydrate the `KeyStore` — restoring the SAME keypair the member had before.
5306
- * Returns `true` when a keypair is now in hand (already present or hydrated),
5307
- * `false` when there's no blob yet (a brand-new member → caller provisions).
5308
- * Throws only on a genuine unwrap failure (wrong secret / tampered blob).
5845
+ * Sign-in / unlock time: fetch the wrapped blob, unwrap it with the seed, and
5846
+ * hydrate the `KeyStore`, restoring the SAME keypair the member had before.
5847
+ * Returns `true` when a vault-backed keypair is now in hand, and `false` when
5848
+ * there is no blob yet (a brand-new member; the caller provisions). A store
5849
+ * failure propagates as thrown; a blob that does not unwrap, parse or import
5850
+ * throws `ChatKeysUnreadableError`. Nothing in the `KeyStore` changes until
5851
+ * the saved pair is imported; it then replaces whatever pair the tab held,
5852
+ * in one step, so a pair minted meanwhile is never kept in its place.
5309
5853
  */
5310
5854
  rehydrate(memberId: string, seedSecret: string): Promise<boolean>;
5855
+ /** Unwrap, parse and import a saved pair, outside the `KeyStore`. */
5856
+ private unwrapPair;
5311
5857
  /**
5312
5858
  * Ensure a stable keypair is in hand: rehydrate from the vault if a blob
5313
5859
  * exists, otherwise mint one and persist it (so it's stable next session).
@@ -5316,6 +5862,101 @@ declare class ChatKeyVault {
5316
5862
  ensure(memberId: string, seedSecret: string): Promise<void>;
5317
5863
  }
5318
5864
 
5865
+ /**
5866
+ * POST /api/auth/identity (SpecA §4.1 as amended by CS-7). An unknown name
5867
+ * answers a deterministic decoy, so a lookup never says whether an account
5868
+ * exists. Responses are cached for the life of the Client. Any failure,
5869
+ * including a 429, fails closed (SpecA §4.3).
5870
+ */
5871
+ declare class IdentityDirectory {
5872
+ private readonly http;
5873
+ private readonly cache;
5874
+ constructor(http: Pick<HttpClient, "post">);
5875
+ lookup(username: string): Promise<string>;
5876
+ lookupMany(usernames: string[]): Promise<Record<string, string>>;
5877
+ }
5878
+
5879
+ /** Signs with the account identity key (IK). Never exposes the seed (SpecA §6.1). */
5880
+ interface IdentitySigner {
5881
+ /** b64 IEEE-P1363 ECDSA-P256-SHA256 over `bytes`. */
5882
+ sign(bytes: Uint8Array): Promise<string>;
5883
+ /** The IK as standard padded base64 of the raw 65-byte point. */
5884
+ identityKeyRaw(): Promise<string>;
5885
+ }
5886
+
5887
+ /**
5888
+ * IndexedDB `muhkoo.trust` (design §6.6): pin high-water marks and per-Space log
5889
+ * heads. When IndexedDB is absent, throws on open, or has not opened within 2 s,
5890
+ * it is an in-memory store for the life of the Client, and `persistent` is
5891
+ * false (KI1).
5892
+ */
5893
+ interface TrustStore {
5894
+ readonly persistent: boolean;
5895
+ get<T>(key: string): Promise<T | undefined>;
5896
+ put(key: string, value: unknown): Promise<void>;
5897
+ del(key: string): Promise<void>;
5898
+ }
5899
+
5900
+ type PinDocName = "identity" | "spaces";
5901
+ interface PinDoc {
5902
+ v: 1;
5903
+ ownerCommitment: string;
5904
+ counter: number;
5905
+ entries: Record<string, string>;
5906
+ sig: string;
5907
+ }
5908
+ interface PinStoreDeps {
5909
+ http: Pick<HttpClient, "post">;
5910
+ commitment: () => string | null;
5911
+ signer: IdentitySigner;
5912
+ store: () => Promise<TrustStore>;
5913
+ }
5914
+ declare class PinStore {
5915
+ private readonly deps;
5916
+ constructor(deps: PinStoreDeps);
5917
+ private commitment;
5918
+ private markKey;
5919
+ private kvPath;
5920
+ private fetchDoc;
5921
+ private verify;
5922
+ /** The current verified document; an empty bootstrap at counter 0 when none exists and no mark does either. */
5923
+ read(doc: PinDocName): Promise<PinDoc>;
5924
+ get(doc: PinDocName, key: string): Promise<string | undefined>;
5925
+ /** Pin new keys. A key already pinned to a different value is reported, never overwritten (design §3.4). */
5926
+ pin(doc: PinDocName, entries: Record<string, string>): Promise<{
5927
+ conflicts: string[];
5928
+ }>;
5929
+ unpin(doc: PinDocName, keys: string[]): Promise<void>;
5930
+ /** User-confirmed recovery (design §9.1 "Your saved trust records are missing."): forget the high-water mark. */
5931
+ resetMark(doc: PinDocName): Promise<void>;
5932
+ private mutate;
5933
+ }
5934
+
5935
+ /** One per Client: shared by every Space (and, in sub-project 4, DMs). */
5936
+ declare class TrustContext {
5937
+ private readonly deps;
5938
+ readonly directory: IdentityDirectory;
5939
+ readonly signer: IdentitySigner;
5940
+ readonly pins: PinStore;
5941
+ private storeP;
5942
+ private opened;
5943
+ private appIdP;
5944
+ constructor(deps: {
5945
+ http: HttpClient;
5946
+ session: SessionState;
5947
+ openStore?: () => Promise<TrustStore>;
5948
+ });
5949
+ /** Whether muhkoo.trust survives a restart (KI1): false until the open settles, and false for an in-memory store. */
5950
+ get persistent(): boolean;
5951
+ store(): Promise<TrustStore>;
5952
+ /** This app's id, from GET /api/app/trust (a genesis must name it, D4). Cached; a failure is retried next call. */
5953
+ appId(): Promise<string>;
5954
+ /** User-confirmed "Reset trust for this space" (design §9.1): drop its genesis pin and log high-water. */
5955
+ resetSpacePins(spaceId: string): Promise<void>;
5956
+ /** `client.trust.unpin(username)`: forget a member's pinned identity after comparing safety numbers (design §3.4). */
5957
+ unpinUser(username: string): Promise<void>;
5958
+ }
5959
+
5319
5960
  /**
5320
5961
  * `client.space` — the fan-out group-encryption surface.
5321
5962
  *
@@ -5354,6 +5995,9 @@ interface SpaceNamespaceDeps {
5354
5995
  /** SDK-owned ratchet-keypair vault — gives a STABLE member keypair across
5355
5996
  * reloads (rehydrate-or-provision) instead of a fresh one each session. */
5356
5997
  chatKeys?: ChatKeyVault;
5998
+ /** The Client's trust context. Present, every join request carries the IK
5999
+ * certificate over this member's chat keys (design §3.2). */
6000
+ trust?: TrustContext;
5357
6001
  /** P2P swarm config — enables peer block exchange among Space members. */
5358
6002
  p2p?: {
5359
6003
  enabled?: boolean;
@@ -5369,6 +6013,10 @@ declare class SpaceNamespace {
5369
6013
  private spaceCache?;
5370
6014
  /** Spaces built this session, by id — used to route offline send replays. */
5371
6015
  private readonly openSpaces;
6016
+ /** In-flight identity loads, by member id; each entry is deleted once it settles. */
6017
+ private readonly identityInFlight;
6018
+ /** Chat-key certificates, by member and the exact key strings; a failed mint is dropped so the next join retries. */
6019
+ private readonly certs;
5372
6020
  /** Unique id for THIS device/session — the P2P peer identity. A user's own
5373
6021
  * devices share a member id, so the mesh MUST distinguish them by this or a
5374
6022
  * device treats its siblings' signals as its own echo and never connects. */
@@ -5397,8 +6045,14 @@ declare class SpaceNamespace {
5397
6045
  joinSpace(spaceId: string, opts?: {
5398
6046
  timeoutMs?: number;
5399
6047
  }): Promise<Space>;
5400
- /** A handle to a space without auto-connecting (uses cached keys on demand). */
5401
- get(spaceId: string, historyPolicy?: HistoryPolicy): Promise<Space>;
6048
+ /**
6049
+ * A handle to a space without auto-connecting (uses cached keys on demand).
6050
+ * `messaging: false` opens a files-only space, as {@link createSpace} makes:
6051
+ * no keyring and no identity, so it opens whether or not the session is locked.
6052
+ */
6053
+ get(spaceId: string, historyPolicy?: HistoryPolicy, opts?: {
6054
+ messaging?: boolean;
6055
+ }): Promise<Space>;
5402
6056
  /** List this app's channels (name → space-id) for the key's environment. */
5403
6057
  listChannels(): Promise<Array<{
5404
6058
  name: string;
@@ -5457,8 +6111,22 @@ declare class SpaceNamespace {
5457
6111
  }>>;
5458
6112
  private keyringFor;
5459
6113
  private build;
5460
- /** Ensure the member has an identity keypair; return its public ECDH + ECDSA keys. */
6114
+ /**
6115
+ * The member's stable chat identity (design §3.3). A locked session (no
6116
+ * seed) throws `IdentityLockedError`. A vault failure throws
6117
+ * `IdentityUnavailableError`: `retryable` when the vault could not be
6118
+ * reached (§9.2 I8), and not when the saved keys do not unwrap under this
6119
+ * seed. A throwaway pair is never minted: doing so is what hid a reloaded
6120
+ * member's earlier messages from everyone else.
6121
+ *
6122
+ * Concurrent calls for one member share a single load, so two first opens
6123
+ * can never interleave their forget/generate steps and persist a second or
6124
+ * mixed pair.
6125
+ */
5461
6126
  private ensureIdentity;
6127
+ private loadIdentity;
6128
+ /** One certificate per (member, chat keys) for the life of the Client (design §3.2: global, not per Space). */
6129
+ private certFor;
5462
6130
  private fetchPolicy;
5463
6131
  private fetchTicket;
5464
6132
  private myId;
@@ -5920,6 +6588,8 @@ interface FileEntry {
5920
6588
  * would defeat the point of keeping history out of the directory record.
5921
6589
  */
5922
6590
  versions?: number;
6591
+ /** See `DirEntry.moving`. */
6592
+ moving?: true;
5923
6593
  }
5924
6594
  interface DirEntry {
5925
6595
  kind: "dir";
@@ -5928,6 +6598,25 @@ interface DirEntry {
5928
6598
  /** Base64 AES-256 key for that record. See the capability note above. */
5929
6599
  key: string;
5930
6600
  mtime: number;
6601
+ /**
6602
+ * Set on both copies while a move between two directories is under way.
6603
+ *
6604
+ * A move writes two records, attaching before it detaches, and one that
6605
+ * is interrupted leaves the same entry - the same directory record, or
6606
+ * the same file id, content and history - named in both. This is how a
6607
+ * delete of either copy knows to look for the other before letting go of
6608
+ * anything. Cleared by the move's last write, and from the one copy left
6609
+ * when a delete removes the other. Left in place, it costs a delete of
6610
+ * that entry one read of the tree, and a file keeps every version that
6611
+ * falls off its history.
6612
+ *
6613
+ * Not cleared when that last write is the one that fails: the move has
6614
+ * otherwise finished, only one copy is named anywhere, and nothing comes
6615
+ * back to unmark it. A retried move of the same entry carries the mark
6616
+ * over. That copy then keeps what falls off its history for good - a
6617
+ * storage cost, never a loss.
6618
+ */
6619
+ moving?: true;
5931
6620
  }
5932
6621
  type Entry = FileEntry | DirEntry;
5933
6622
  /** One directory record, as stored (before encryption). */
@@ -5983,6 +6672,57 @@ declare class VfsNotFoundError extends Error {
5983
6672
  declare class VfsConflictError extends Error {
5984
6673
  constructor(message: string);
5985
6674
  }
6675
+ /**
6676
+ * Raised when a directory record cannot be read: it does not decrypt or parse
6677
+ * with the key its parent holds, or it does but is not a shape this SDK can
6678
+ * read - not an object, a version other than 1, `entries` missing or not a map.
6679
+ *
6680
+ * Thrown, never read as an empty directory. The next write into an "empty"
6681
+ * directory would seal an empty record over the real one, and a record this
6682
+ * version cannot read may simply be one a NEWER version wrote. Nor is the
6683
+ * directory skipped: a sync that cannot see a subtree reads its absence as a
6684
+ * deletion. The record is left exactly as stored; a recursive delete
6685
+ * (`delete(path, { recursive: true })`) detaches it when the user wants it
6686
+ * gone.
6687
+ */
6688
+ declare class VfsCorruptRecordError extends Error {
6689
+ /** The record's id - `dirKey(id)` is where it is stored. */
6690
+ readonly id: string;
6691
+ /** The path it was reached through, when known. A moved subtree can have two. */
6692
+ readonly path?: string;
6693
+ constructor(id: string, path: string | undefined, reason: string);
6694
+ }
6695
+ /**
6696
+ * Raised when a walk or a copy would go past a size or depth ceiling, or a
6697
+ * write would put a directory deeper than a walk reads.
6698
+ *
6699
+ * Thrown rather than returning what was gathered so far, because every caller
6700
+ * of a walk treats a path that is missing from the result as a path that does
6701
+ * not exist - and a sync then plans a deletion for it. A tree this large or
6702
+ * deep is almost always one built to be expensive to read: directory records
6703
+ * can point at the same directory many times over, so a handful of tiny
6704
+ * records can describe more paths than there are atoms.
6705
+ */
6706
+ declare class VfsLimitError extends Error {
6707
+ /**
6708
+ * Which ceiling was hit: total entries, total characters of path text,
6709
+ * nesting depth, or - for a copy - how many places one directory may
6710
+ * appear. Entries alone do not bound memory - a name may be 255 bytes, so
6711
+ * one path can be ~16KB - which is why path text has its own.
6712
+ */
6713
+ readonly limit: "entries" | "pathChars" | "depth" | "shared";
6714
+ /** The ceiling itself. */
6715
+ readonly max: number;
6716
+ /** Where the operation started, or for a write what it was asked to write. */
6717
+ readonly path: string;
6718
+ /**
6719
+ * Where the ceiling was reached: the directory being listed, or for depth
6720
+ * the entry one level too deep. Walking the tree to find the culprit would
6721
+ * hit the same ceiling, so this is the only pointer the caller gets.
6722
+ */
6723
+ readonly at: string;
6724
+ constructor(limit: VfsLimitError["limit"], max: number, path: string, at: string, doing?: string);
6725
+ }
5986
6726
 
5987
6727
  /**
5988
6728
  * `client.vfs` — a real filesystem over Muhkoo primitives.
@@ -6053,6 +6793,18 @@ interface VfsNamespaceDeps {
6053
6793
  seed: () => Uint8Array | null;
6054
6794
  /** Versions retained per file. */
6055
6795
  historyLimit?: number;
6796
+ /**
6797
+ * Ceilings for the operations that visit a whole subtree - `walk` (and so
6798
+ * `glob`) and `copy`. `entries` is the most entries one call may visit;
6799
+ * `pathChars` the most characters of path text, summed over every path it
6800
+ * visits. The defaults are far past any real project; tests lower them so
6801
+ * a hostile tree hits them in milliseconds. `Client` does not pass these,
6802
+ * so `client.vfs` always has the defaults.
6803
+ */
6804
+ limits?: {
6805
+ entries?: number;
6806
+ pathChars?: number;
6807
+ };
6056
6808
  }
6057
6809
  declare class VfsNamespace {
6058
6810
  private readonly deps;
@@ -6066,6 +6818,8 @@ declare class VfsNamespace {
6066
6818
  */
6067
6819
  private cache;
6068
6820
  private readonly historyLimit;
6821
+ private readonly maxEntries;
6822
+ private readonly maxPathChars;
6069
6823
  constructor(deps: VfsNamespaceDeps);
6070
6824
  /**
6071
6825
  * The working directory relative paths resolve against.
@@ -6117,14 +6871,46 @@ declare class VfsNamespace {
6117
6871
  stat(path: string): Promise<VfsStat>;
6118
6872
  /** Immediate children of a directory. One record fetch. */
6119
6873
  list(path?: string): Promise<VfsStat[]>;
6874
+ /**
6875
+ * A directory's entries in listing order: directories first, then by name.
6876
+ * Shared by `list` and `walk`, so a walk's output order is exactly the order
6877
+ * of the listings it is made of.
6878
+ */
6879
+ private children;
6120
6880
  private warn;
6121
6881
  /**
6122
6882
  * Every file path under a prefix, depth-first.
6123
6883
  *
6124
6884
  * Deliberately separate from `list`: this walks the whole subtree, which is
6125
6885
  * one fetch per directory. Fine for a project, wrong as a default.
6886
+ *
6887
+ * Throws VfsLimitError past the entry budget or the depth ceiling, and
6888
+ * VfsCorruptRecordError for a directory it cannot read, rather than
6889
+ * returning part of the tree. Skips (with a warning) a directory that is
6890
+ * one of its own ancestors.
6126
6891
  */
6127
6892
  walk(path?: string): Promise<string[]>;
6893
+ /**
6894
+ * Every entry under a prefix - directories as well as files - depth-first,
6895
+ * each directory just before its contents. `walk` is this with the
6896
+ * directories filtered out, so the two can never disagree about what is in
6897
+ * a tree; version control uses this one because a commit records empty
6898
+ * directories too.
6899
+ */
6900
+ walkEntries(path?: string): Promise<VfsStat[]>;
6901
+ /**
6902
+ * `walkEntries` without the list: `visit` is called with each entry in
6903
+ * the same order, and for a file with its manifest - the handle
6904
+ * `statManifest` gives, without resolving the path all over again, which
6905
+ * on a deep tree cost far more than the walk itself. Null for a directory.
6906
+ *
6907
+ * For a caller that does not need every entry at once. Version control
6908
+ * compares the working tree with a commit as it goes, where holding the
6909
+ * whole walk beside the whole commit, and a map of one beside a map of
6910
+ * the other, ran a 512MB heap out of memory inside the default budgets.
6911
+ * The same guards and budgets as `walk`, which is built on this.
6912
+ */
6913
+ eachEntry(path: string, visit: (stat: VfsStat, manifest: FileManifest | null) => void): Promise<void>;
6128
6914
  readFile(path: string): Promise<Uint8Array>;
6129
6915
  readText(path: string): Promise<string>;
6130
6916
  /**
@@ -6170,6 +6956,16 @@ declare class VfsNamespace {
6170
6956
  mkdir(path: string, opts?: {
6171
6957
  recursive?: boolean;
6172
6958
  }): Promise<void>;
6959
+ /**
6960
+ * Refuse now what creating these directories with `mkdir -p` would refuse
6961
+ * part way: one deeper than MAX_DEPTH, or a name on the way held by an
6962
+ * entry this version cannot read. Writes nothing.
6963
+ *
6964
+ * For an operation that removes things before it writes - a checkout -
6965
+ * so that a refusal comes before anything is gone. A FILE in the way is
6966
+ * not refused: such an operation removes those first.
6967
+ */
6968
+ assertCreatable(dirs: Iterable<string>): Promise<void>;
6173
6969
  /**
6174
6970
  * Write a file, creating parent directories as needed.
6175
6971
  *
@@ -6183,10 +6979,10 @@ declare class VfsNamespace {
6183
6979
  /**
6184
6980
  * Delete a file, or a directory when `recursive`.
6185
6981
  *
6186
- * `keepContent` removes the entry WITHOUT releasing its shards. A checkout
6187
- * needs that: the file is leaving the working tree, but commits still
6188
- * reference the same manifest, and releasing would free content history
6189
- * depends on.
6982
+ * `keepContent` removes the entry WITHOUT releasing its shards - or, for
6983
+ * a directory, the shards of anything in it. A checkout needs that: the
6984
+ * file is leaving the working tree, but commits still reference the same
6985
+ * manifest, and releasing would free content history depends on.
6190
6986
  */
6191
6987
  delete(path: string, opts?: {
6192
6988
  recursive?: boolean;
@@ -6198,9 +6994,21 @@ declare class VfsNamespace {
6198
6994
  *
6199
6995
  * A directory move is O(1) regardless of how much is inside it — the
6200
6996
  * subtree records are not touched at all, because nothing in them records
6201
- * its own path.
6997
+ * its own path. Except a move to a DEEPER place, which reads the subtree
6998
+ * first: what is below must still be inside MAX_DEPTH once it is moved.
6202
6999
  */
6203
7000
  rename(from: string, to: string): Promise<void>;
7001
+ /**
7002
+ * Refuse to move the directory at `src` to `dst` when something in it
7003
+ * would then be deeper than MAX_DEPTH from the root. Writes nothing.
7004
+ *
7005
+ * Measured only moving deeper, because it reads the whole subtree: one
7006
+ * move of a 40-deep tree under a 30-deep directory was accepted when only
7007
+ * the moved directory was measured, and every walk of the root threw from
7008
+ * then on. A part that cannot be read cannot be measured, and does not
7009
+ * stop the move - that record stops every walk above it already.
7010
+ */
7011
+ private assertFitsAt;
6204
7012
  /**
6205
7013
  * Copy a file or a directory tree.
6206
7014
  *
@@ -6262,25 +7070,158 @@ declare class VfsNamespace {
6262
7070
  /** Open a directory by path, creating nothing. Throws if it is missing. */
6263
7071
  private openDir;
6264
7072
  private openDirOrNull;
7073
+ /** Open a directory by path, along with the id of every directory on the way to it. */
7074
+ private openChain;
7075
+ /**
7076
+ * `path` is only for error messages. The cache is keyed by id, so when two
7077
+ * paths reach one record - a move interrupted between attach and detach -
7078
+ * the error names whichever asked first.
7079
+ */
6265
7080
  private openDirById;
7081
+ /**
7082
+ * Turn a decrypted record into an OpenDir, or throw.
7083
+ *
7084
+ * The record is THROWN on, never read as empty, when its shape is wrong -
7085
+ * see VfsCorruptRecordError. An individual entry of the wrong shape is set
7086
+ * aside in `hidden` instead: out of every read, but kept for the write.
7087
+ */
7088
+ private readRecord;
6266
7089
  /** Apply an edit to a directory record and persist it. */
6267
7090
  private mutate;
6268
7091
  private createDir;
6269
- /** Recursively remove a subtree's records. Best-effort: unreachable ≠ fatal. */
7092
+ /**
7093
+ * Every directory record in a subtree, each once, opened - and nothing
7094
+ * changed. The first half of a recursive delete; see `delete`.
7095
+ *
7096
+ * Once per id, so a record that references another inside the subtree
7097
+ * cannot recurse forever, and a subtree reachable twice is not released
7098
+ * twice. And never a directory in `above` - the root and everything else
7099
+ * on the way down to the target - nor the root by any route. Deduplicating
7100
+ * alone let an entry naming the root pull the whole filesystem into the
7101
+ * delete, the root record included; a walk shows the same pointer as a
7102
+ * cycle and skips it, so this skips it too.
7103
+ *
7104
+ * `moving` says whether anything in it is marked as half of an
7105
+ * interrupted move, which is what makes a delete look for the other half.
7106
+ */
7107
+ private subtree;
7108
+ /**
7109
+ * Delete these records and let go of every file in them. The second half
7110
+ * of a recursive delete, run only once the subtree is detached. Cannot
7111
+ * throw: every step is best-effort, because unreachable is not fatal.
7112
+ *
7113
+ * With `elsewhere` - what the rest of the tree still names, read because
7114
+ * something here is half of an interrupted move - a record still named
7115
+ * elsewhere is kept, and so is a file.
7116
+ */
6270
7117
  private purge;
7118
+ /**
7119
+ * Unmark what is left of an interrupted move once a delete has removed
7120
+ * the other copy. Marked, a file keeps every version that falls off its
7121
+ * history, because the other copy might hold it - for good, once there
7122
+ * is no other copy - and every delete of the entry, or of a directory
7123
+ * above it, reads the whole tree. Only when exactly one marked copy is
7124
+ * left: more, and they are still shared. Best-effort, like everything
7125
+ * after a detach; failing, it stays marked, which costs only that.
7126
+ */
7127
+ private settle;
7128
+ /**
7129
+ * Let go of files that are leaving the tree: each one's content, every
7130
+ * version its history holds, and the history record. Best-effort, like
7131
+ * everything after a detach.
7132
+ *
7133
+ * A file id still named elsewhere is one file with two names, left by an
7134
+ * interrupted move: its content and history belong to the name that is
7135
+ * staying, and all that changes hands is the key its history opens with.
7136
+ * `keepContent` keeps every file's content and drops only its history
7137
+ * record, as a checkout needs.
7138
+ */
7139
+ private letGo;
7140
+ /**
7141
+ * What the tree still names with one entry left out: every directory
7142
+ * record, and every file id with a directory that holds it. Null when
7143
+ * part of the tree cannot be read or followed, so the answer is unknown.
7144
+ */
7145
+ private namedElsewhere;
7146
+ /**
7147
+ * Everything reachable from the root: every directory record's id and
7148
+ * key, and every file id with a directory that names it - following the
7149
+ * entries hidden only for their name, which still reference what they
7150
+ * point at. `skip` leaves one entry out: one about to be detached.
7151
+ *
7152
+ * Fetches up to WALK_CONCURRENCY records at once, each record once
7153
+ * however often it is named. Throws VfsCorruptRecordError for a record a
7154
+ * path reaches that cannot be read, unless `lenient`. `unknown` counts
7155
+ * what could not be followed at all: an entry this version cannot read,
7156
+ * and a record behind an unusable name that does not open - which no path
7157
+ * can reach, and so no recursive delete can remove - or, `lenient`, any
7158
+ * record that does not open.
7159
+ */
7160
+ private references;
6271
7161
  /**
6272
7162
  * Deep-copy an entry. Files reuse their manifest (no bytes move); each
6273
7163
  * directory gets a fresh id and key, so the copy is independent of the
6274
7164
  * original — writing into one must never appear in the other.
7165
+ *
7166
+ * `at` is the entry's SOURCE path, and `depth` how many directories deep
7167
+ * from the root the copy of it will be. With `run.write` false this is the
7168
+ * dry run: every check, no reference taken and no record stored.
6275
7169
  */
6276
7170
  private cloneEntry;
6277
7171
  /**
6278
- * Let go of a file's bytes: the current manifest and every retained
6279
- * version, then the history record itself.
7172
+ * Let go of one file's bytes: its content, every version its history
7173
+ * retains, and the history record itself.
7174
+ *
7175
+ * Each distinct manifest once. `copies` is every entry naming the file -
7176
+ * two after an interrupted move, which may by now hold different
7177
+ * versions - and `restore` puts a version back without taking a second
7178
+ * reference, so its manifest is in the entry and in the history both.
7179
+ * Releasing per mention released one reference twice, and so freed bytes
7180
+ * a copy elsewhere, holding its own reference, still needed.
6280
7181
  */
6281
7182
  private releaseFile;
7183
+ /**
7184
+ * A file's history record, opened with the first of `keys` that opens
7185
+ * it, and that key. Null when there is none, or none of them does.
7186
+ */
7187
+ private openHistory;
7188
+ /**
7189
+ * Seal a file's history with the key of the directory that holds it now.
7190
+ *
7191
+ * A history record is sealed with its directory's key, so a file moved
7192
+ * to another directory carried a history its new directory could not
7193
+ * open: every read of it, and every write to the file, failed from then
7194
+ * on. Nothing to do when there is no history, when it opens with `to`
7195
+ * already, or when none of the keys opens it.
7196
+ */
7197
+ private rekeyHistory;
7198
+ /**
7199
+ * The history of the file `entry` names as `name` in `parent`, and the
7200
+ * key to seal it with from now on.
7201
+ *
7202
+ * A history is sealed with its directory's key - usually. The two halves
7203
+ * of an interrupted move share one history, sealed with ONE of their
7204
+ * directories' keys, and a file moved between directories by a version
7205
+ * before `rename` re-sealed it still has its old directory's. So when the
7206
+ * directory's own key does not open it, every key in the tree is tried,
7207
+ * the other half's first: a read of the whole tree, and only then. It
7208
+ * need not be read in full, or understood - an entry this version cannot
7209
+ * follow may hide a key, but a key that opens the record is proof
7210
+ * enough. Found, it is re-sealed with this directory's key, so the search
7211
+ * is made once - except for half of a move, which keeps the key that
7212
+ * opened it and so keeps working for both halves.
7213
+ *
7214
+ * No record, or a value that is not a sealed record at all, is an empty
7215
+ * history, as it always was. One that no key opens is thrown on, with a
7216
+ * message rather than a bare decryption error: writing an empty history
7217
+ * over it would lose every version it holds.
7218
+ */
6282
7219
  private readHistory;
6283
- /** Push `entry` onto its history record. Returns the new version count. */
7220
+ /**
7221
+ * Push `entry` onto its history record. Returns the new version count.
7222
+ * `next` is the manifest the entry will hold once the caller has written
7223
+ * it.
7224
+ */
6284
7225
  private pushHistory;
6285
7226
  }
6286
7227
 
@@ -6386,21 +7327,88 @@ interface VcsNamespaceDeps {
6386
7327
  key: () => Promise<Uint8Array>;
6387
7328
  /** Who to record as the author. */
6388
7329
  author: () => string;
7330
+ /**
7331
+ * `entries` is the most tree entries one read of history may visit - one
7332
+ * `status`, `diff` or `checkout` - and `pathChars` the most characters of
7333
+ * path those entries may add up to. The defaults are far past any real
7334
+ * project; tests lower them so a hostile tree hits them in milliseconds.
7335
+ * `Client` does not pass these, so `client.vcs` always has the defaults.
7336
+ */
7337
+ limits?: {
7338
+ entries?: number;
7339
+ pathChars?: number;
7340
+ };
6389
7341
  }
6390
7342
  declare class Repo {
6391
7343
  private readonly slug;
6392
7344
  private readonly deps;
7345
+ private readonly maxEntries;
7346
+ private readonly maxPathChars;
7347
+ /**
7348
+ * How many directories deep below the project a tree may nest. The
7349
+ * working tree may be written MAX_DEPTH directories below the root, and
7350
+ * the project itself is some of those: a deeper tree would check out into
7351
+ * something no walk will read, and `status` - and every checkout after,
7352
+ * which walks the tree first - would throw from then on.
7353
+ */
7354
+ private readonly maxTreeDepth;
6393
7355
  constructor(slug: string, deps: VcsNamespaceDeps);
6394
7356
  private get root();
6395
7357
  /** Store an object, returning its hash and whether it was new. */
6396
7358
  private put;
7359
+ /** Does this stored record open to an object with this hash? */
7360
+ private holds;
7361
+ /**
7362
+ * Fetch an object, and prove it is the one that was asked for.
7363
+ *
7364
+ * Content addressing is only a guarantee if the address is checked. Without
7365
+ * this a stored object could claim any hash - including its own, so a tree
7366
+ * could contain itself and `status` would follow it for ever. Every object
7367
+ * this repository has ever written verifies: `put` hashes exactly the value
7368
+ * it seals, `canonical` is independent of key order and drops `undefined`
7369
+ * exactly as JSON does, and hashing has never been versioned.
7370
+ */
6397
7371
  private get;
7372
+ /** A commit, checked to be one. See `commitProblem`. */
7373
+ private readCommit;
7374
+ /**
7375
+ * Start a traversal of history: a budget shared by every branch of it, and
7376
+ * a cache of the tree objects it has fetched.
7377
+ *
7378
+ * The cache matters for the same reason the budget does. Identical
7379
+ * subtrees are one object, so a tree of honest-but-hostile shape is a
7380
+ * handful of fetches and a vast number of paths; caching keeps it to the
7381
+ * handful, and the budget stops the paths. It caches OBJECTS, never output
7382
+ * - each name a shared subtree appears at is still listed.
7383
+ */
7384
+ private treeWalk;
7385
+ /**
7386
+ * A tree object's entries, checked and ready to walk.
7387
+ *
7388
+ * Returned in a null-prototype map, so a name like `constructor` is looked
7389
+ * up as an entry and not found as an inherited property.
7390
+ */
7391
+ private readTree;
7392
+ /**
7393
+ * A record that is not an object - HEAD, a branch ref, the branch list,
7394
+ * MERGE - opened. Null when there is none; `problem` when there is one
7395
+ * that does not open. What is inside is the caller's to check.
7396
+ */
7397
+ private openRecord;
6398
7398
  private head;
6399
7399
  private setHead;
6400
7400
  private readRef;
7401
+ /** A branch's commit, null when there is no such branch, or "unreadable". */
7402
+ private refState;
6401
7403
  private writeRef;
6402
7404
  /** Every branch name in this repository. */
6403
7405
  branches(): Promise<string[]>;
7406
+ /**
7407
+ * The branch list, less any name a branch could not have been created
7408
+ * with - left out with a warning, like an unreadable entry in a tree, so
7409
+ * one planted name does not hide every real one.
7410
+ */
7411
+ private readIndex;
6404
7412
  /** The commit HEAD resolves to, or null in an empty repository. */
6405
7413
  current(): Promise<string | null>;
6406
7414
  /**
@@ -6418,7 +7426,27 @@ declare class Repo {
6418
7426
  theirs: string;
6419
7427
  name: string;
6420
7428
  } | null>;
6421
- /** Write a directory and everything under it, returning the tree's hash. */
7429
+ /**
7430
+ * Give up on a merge that stopped for conflicts: the next commit is an
7431
+ * ordinary one, with one parent. The working tree is left as it is,
7432
+ * conflict markers and all. To put it back, switch to the current branch
7433
+ * with `discardChanges` — not `checkout`, which always leaves HEAD
7434
+ * detached, so later commits would stop moving the branch.
7435
+ *
7436
+ * Also the way out when the record of the merge cannot be read, which
7437
+ * would otherwise stop every commit.
7438
+ */
7439
+ abortMerge(): Promise<void>;
7440
+ /**
7441
+ * Write a directory and everything under it, returning the tree's hash.
7442
+ *
7443
+ * Reads the working tree through ONE `walkEntries`, rather than listing
7444
+ * each directory by path and recursing. The recursion had no guard at all:
7445
+ * a directory record that names itself - which anything holding the seed
7446
+ * can plant - sent `commit` down /loop/loop/loop/... for ever. The walk has
7447
+ * the cycle guard, the budget and the depth ceiling, so a commit records
7448
+ * exactly the tree `status` compares it against.
7449
+ */
6422
7450
  private writeTree;
6423
7451
  /** Commits reachable from HEAD, newest first. */
6424
7452
  log(limit?: number): Promise<LogEntry[]>;
@@ -6446,8 +7474,6 @@ declare class Repo {
6446
7474
  * trees for states nobody chose to keep.
6447
7475
  */
6448
7476
  status(): Promise<Change[]>;
6449
- /** The working tree as a path → entry map, mirroring `filesOf`. */
6450
- private workingFiles;
6451
7477
  /**
6452
7478
  * Turn what a person typed into a commit hash.
6453
7479
  *
@@ -6539,6 +7565,11 @@ interface VcsNamespaceOptions {
6539
7565
  /** The in-memory master seed, or null when locked. Read per call. */
6540
7566
  seed: () => Uint8Array | null;
6541
7567
  author: () => string;
7568
+ /** See `VcsNamespaceDeps.limits`. */
7569
+ limits?: {
7570
+ entries?: number;
7571
+ pathChars?: number;
7572
+ };
6542
7573
  }
6543
7574
 
6544
7575
  /** The hosted Muhkoo Accelerator — the default {@link ClientOptions.baseUrl}. */
@@ -6646,6 +7677,12 @@ declare class Client {
6646
7677
  readonly message: MessageNamespace;
6647
7678
  /** Fan-out group spaces — `client.space.createSpace(...)`, etc. */
6648
7679
  readonly space: SpaceNamespace;
7680
+ /** Trust records shared by every Space: pins and per-Space log heads (design §3.4, §6.9). */
7681
+ readonly trust: {
7682
+ readonly persistent: boolean;
7683
+ unpin(username: string): Promise<void>;
7684
+ resetSpacePins(spaceId: string): Promise<void>;
7685
+ };
6649
7686
  /** Programmable Agents — `client.agents.create(appId, …)`, etc. */
6650
7687
  readonly agents: AgentsNamespace;
6651
7688
  /** Serverless functions — `client.functions.deploy(appId, …)`, etc. */
@@ -7582,14 +8619,29 @@ declare namespace protocol {
7582
8619
  * GET /api/spaces/:id/keyring/pending -> JoinRequest[]
7583
8620
  * GET /api/spaces/:id/keyring/roster -> { memberId, identityEcdhPub }[]
7584
8621
  * POST /api/spaces/:id/keyring/rotate { nextEpoch } -> { epoch }
8622
+ * GET /api/spaces/:id/keyring/log ?since&limit -> { entries, head }
8623
+ * POST /api/spaces/:id/keyring/log { entry, ec? } -> { ok, head }
8624
+ * GET /api/spaces/:id/keyring/state (status only)
7585
8625
  * GET /api/spaces/:id/metadata -> SpaceMetadata
7586
8626
  */
7587
8627
 
8628
+ /** A keyring route answered non-2xx; status and JSON body preserved (the existing methods keep their plain Errors). */
8629
+ declare class KeyringHttpError extends Error {
8630
+ readonly status: number;
8631
+ readonly body: Record<string, unknown> | null;
8632
+ constructor(status: number, body: Record<string, unknown> | null, route: string);
8633
+ }
7588
8634
  interface KeyringClientDeps {
7589
8635
  spaceId: string;
7590
8636
  httpBaseUrl: string;
7591
8637
  /** Header-injecting fetch from the client's HttpClient. */
7592
8638
  fetch: typeof fetch;
8639
+ /**
8640
+ * The same fetch with the HttpClient's silent 401 recovery (`HttpClient.fetchWithRecovery`).
8641
+ * `probeState` and the log routes use it, so a 401 reads as locked only once a re-auth has failed (design §6.9).
8642
+ * Defaults to {@link fetch}.
8643
+ */
8644
+ fetchWithRecovery?: typeof fetch;
7593
8645
  }
7594
8646
  declare class KeyringClient implements KeyringTransport {
7595
8647
  private readonly deps;
@@ -7597,6 +8649,27 @@ declare class KeyringClient implements KeyringTransport {
7597
8649
  private url;
7598
8650
  private post;
7599
8651
  private get;
8652
+ /**
8653
+ * The log routes. Sent through `fetchWithRecovery` when given, as `probeState` is, so a stale session is
8654
+ * re-established before a 401 reads as locked (design §6.9): a GET replays safely, and a POST body is a string.
8655
+ */
8656
+ private request;
8657
+ /** GET /keyring/log (design §3.6): entries with seq > since, ascending, ≤ limit; head null = no log. */
8658
+ fetchLog(since?: number, limit?: number): Promise<{
8659
+ entries: MembershipEntry[];
8660
+ head: MlogHead | null;
8661
+ }>;
8662
+ /** POST /keyring/log {entry, ec?} → {ok, head}; `409 {error:"Log moved", head}` (no code) on a compare-and-set miss. */
8663
+ postLogEntry(entry: MembershipEntry, ec?: EpochCommit): Promise<{
8664
+ ok: true;
8665
+ head: MlogHead;
8666
+ }>;
8667
+ /**
8668
+ * Status of GET /keyring/state, or "network" when the request fails: 403 is not-member, 401 is locked,
8669
+ * anything else resumes (design §6.9). Sent through `fetchWithRecovery` when given, so a stale session
8670
+ * is re-established before a 401 is believed.
8671
+ */
8672
+ probeState(): Promise<number | "network">;
7600
8673
  postJoinRequest(req: JoinRequest): Promise<void>;
7601
8674
  fetchBlobs(memberId: string): Promise<WrappedKey[]>;
7602
8675
  postWrappedKey(targetMemberId: string, wrapped: WrappedKey, fromMemberId: string): Promise<void>;
@@ -7631,6 +8704,43 @@ declare class KeyringClient implements KeyringTransport {
7631
8704
  fetchMetadata(): Promise<SpaceMetadata | null>;
7632
8705
  }
7633
8706
 
8707
+ /**
8708
+ * Trust-layer errors shared by Spaces and, later, DMs (design §3.3, §6.9;
8709
+ * SpecA §6.6). Class names are public API and are frozen at their first
8710
+ * release (design Q16).
8711
+ */
8712
+ /** The session holds no seed, so it has no stable identity and may not take part (design §3.3, T7). */
8713
+ declare class IdentityLockedError extends Error {
8714
+ readonly code: "identity-locked";
8715
+ constructor(message?: string);
8716
+ }
8717
+ /**
8718
+ * The stable chat keys could not be loaded (design §3.3). `retryable` says
8719
+ * whether the same call can succeed later. It is true when the vault could not
8720
+ * be reached or written (a 5xx or a network error on `chat-keys`), which the
8721
+ * next open retries (§9.2 I8). It is false when the saved keys were read but do
8722
+ * not unwrap under this session's seed, because they are damaged or were saved
8723
+ * under another seed, and when the client has no vault: retrying fails the same way.
8724
+ */
8725
+ declare class IdentityUnavailableError extends Error {
8726
+ readonly reason?: unknown | undefined;
8727
+ readonly retryable: boolean;
8728
+ readonly code: "identity-unavailable";
8729
+ constructor(message?: string, reason?: unknown | undefined, retryable?: boolean);
8730
+ }
8731
+ type SpaceTrustCode = "integrity" | "log-conflict" | "log-refused" | "lookup-failed" | "pins-missing" | "pins-rollback" | "pins-stale" | "pins-invalid" | "pins-write-failed";
8732
+ /** A membership-log or pin failure. `detail` carries server fields (`reason`, `head`, ...). */
8733
+ declare class SpaceTrustError extends Error {
8734
+ readonly code: SpaceTrustCode;
8735
+ readonly detail?: Record<string, unknown> | undefined;
8736
+ constructor(code: SpaceTrustCode, message?: string, detail?: Record<string, unknown> | undefined);
8737
+ }
8738
+ /** This user may not open this Space (design §4.5.12, V25). */
8739
+ declare class AdmissionDeniedError extends Error {
8740
+ readonly code: "not-member" | "identity-mismatch";
8741
+ constructor(code: "not-member" | "identity-mismatch", message?: string);
8742
+ }
8743
+
7634
8744
  declare class Logger {
7635
8745
  private prefix;
7636
8746
  private logLevel;
@@ -7865,7 +8975,7 @@ declare function getId(row: {
7865
8975
  * expect (and isn't serving a stale cached bundle). Keep this in sync with
7866
8976
  * `package.json` `version`.
7867
8977
  */
7868
- declare const VERSION = "0.14.0-alpha.8";
8978
+ declare const VERSION = "0.15.0-alpha.0";
7869
8979
 
7870
8980
  /**
7871
8981
  * The SDK's process-global logger.
@@ -7897,5 +9007,5 @@ declare global {
7897
9007
  }
7898
9008
  declare const appLogger: InstanceType<typeof Logger>;
7899
9009
 
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 };
9010
+ export { ACCESS_TOKEN_SCOPES, AES_GCM_IV_BYTES, AES_GCM_KEY_BYTES, AES_GCM_TAG_BYTES, AccessTokensNamespace, AdmissionDeniedError, AgentsNamespace, AuthClient, AuthHttpError, 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, IdentityLockedError, IdentityUnavailableError, IndexedDbStore, KEEPER_MEMBER_ID, KeyStore, KeyringClient, KeyringHttpError, KvCache, KvNamespace, LocalEngineHost, LocalStorageSessionStore, Logger, MemorySessionStore, Message, Message as MessageDefault, MessageNamespace, MuhkooAgent, MuhkooDB, MuhkooFunction, MuhkooSpace, NOT_MEMBER_CLOSE_CODE, NOT_MEMBER_RECHECK_MS, 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, SpaceTrustError, StorageNamespace, SyncEngine, VERSION, VFS_ROOT_INFO, VaultProtocolMismatchError, VaultUnavailableError, VcsError, VcsNamespace, VfsConflictError, VfsCorruptRecordError, VfsLimitError, VfsLockedError, VfsNamespace, VfsNotFoundError, WSTransport, WebRtcTransport, WorkerEngineHost, ZERO_HLC, ZkAuth, _accountId, _formatBytes, _messageId, _objectId, _packetId, _socketId, _userId, appLogger, assertType, base58Decode, base58Encode, buildCommitment, canonicalUserCode, clearDeviceIdentity, compareHlc, compilePrograms, concatBytes, configureDeviceStore, createPasskeyWithPrf, decodeFromHex, decorators, decryptAesGcm, defaultCircuitUrls, defaultRpId, defaultSessionStore, deleteBeatsWrite, deleteRegister, deriveAesKeyFromPassphrase, deriveBitsHkdf, deriveIdentity, deriveIdentityFromSeed, deriveMasterSeedFromPassword, deserialize, deviceFingerprint, deviceIdentityIsEphemeral, deviceIdentityKey, dirKey, ejectAgentPrompt, ejectAgentTools, emptyMap, emptyRegister, encodeToHex, encryptAesGcm, evaluatePasskeyPrf, exportPublicKeyBase64, exportPublicKeyHex, formatUserCode, fromBase64, fromBase64Url, fromHex, generateAuthProof, generateChecksum, generateDevicePairingKeypair, generateId, generatePkce, getIPAddress, getId, getMuhkooAppDescriptor, getSubtle, hasDeviceIdentity, headKey, historyKey, initBn128Wasm, initRsWasm, initializeCircuits, isNewer, isNumber, isOfflineCapable, isP2pCapable, isProvisional, isSameOriginPath, 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 };
9011
+ export type { ACL, ACLEntry, AccessTokenInfo, AccessTokensNamespaceDeps, AdmissionState, 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, ChatKeyCert, 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, MemberView, MembershipEntry, 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$1 as 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, SpaceTrustCode, 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 };