@muhkoo/connect 0.14.0-alpha.5 → 0.14.0-alpha.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  Client SDK for the Muhkoo Accelerator. The headline surface is a single
4
4
  **`Client`** that hangs everything off its namespaces —
5
- `client.auth` / `client.kv` / `client.db` / `client.storage` /
6
- `client.message` / `client.space` / `client.agents` / `client.functions` —
7
- backed by
5
+ `client.auth` / `client.kv` / `client.db` / `client.storage` / `client.vfs` /
6
+ `client.vcs` / `client.message` / `client.space` / `client.agents` /
7
+ `client.functions` / `client.accessTokens` / `client.offline` — backed by
8
8
  end-to-end-encrypted messaging (fan-out group channels with history, plus Double
9
9
  Ratchet + ECDH P-384 for direct messages), ZK identity + personal/shared spaces,
10
10
  server-side Programmable Agents, developer-authored serverless functions, and an
@@ -21,10 +21,12 @@ See [`CHANGELOG.md`](./CHANGELOG.md) for what's new.
21
21
  - **`Client`** — the unified entry point. `new Client({ apiKey, baseUrl })`
22
22
  exposes, over one shared session: `auth` (ZK register/login), `kv` (per-user
23
23
  encrypted key/value), `db` (the app's scalable database), `storage` (encrypted
24
- files), `message` (pub/sub + E2E direct messages), `space` (fan-out group
25
- channels with history), `agents` (server-side Programmable Agents), and
26
- `functions` (developer-authored serverless functions). This is the supported
27
- surface; everything below is a building block it composes.
24
+ files), `vfs` + `vcs` (the encrypted filesystem and its history), `message`
25
+ (pub/sub + E2E direct messages), `space` (fan-out group channels with
26
+ history), `agents` (server-side Programmable Agents), `functions`
27
+ (developer-authored serverless functions), `accessTokens` (scoped machine
28
+ credentials), and `offline` (CRDT sync). This is the supported surface;
29
+ everything below is a building block it composes.
28
30
 
29
31
  - **App-describing decorators** — `@MuhkooAgent` / `@MuhkooSpace` / `@MuhkooDB` /
30
32
  `@MuhkooFunction` annotate a plain class with your app's agent-facing surface;
@@ -46,11 +48,26 @@ See [`CHANGELOG.md`](./CHANGELOG.md) for what's new.
46
48
  - **`PersonalSpaceClient`** — HTTP client for the accelerator's
47
49
  `/api/personal/:commitment/*` ZK-gated KV API. Generates a fresh Groth16
48
50
  proof per request using snarkjs and the `preimagePoK` circuit.
51
+ - **`FileStorage` + `ShardClient`** — the chunked AES-256-GCM + Reed–Solomon
52
+ layer under `client.storage`. A read fetches only the **data** shards and
53
+ reaches for parity solely when one is actually missing: RS needs `dataShards`
54
+ of `dataShards + parityShards`, so pulling all of them and discarding the
55
+ parity cost six round trips where four would do. Concurrent shard reads then
56
+ **coalesce** into a single `POST /api/shards/batch`, which is what stops a
57
+ project costing one request per shard — a shard is not a file, and a project is
58
+ thousands of small modules. Callers are unchanged (`getShard` still asks for
59
+ one shard); `batchShards: false` forces one-at-a-time, and a deployment without
60
+ the route is detected and permanently degraded to it. Read concurrency was
61
+ raised to match, because batching with too few reads in flight trades
62
+ parallelism for round trips and is worse than not batching at all. Opening the
63
+ same 519-file project in the Muhkoo IDE went from 2,086 requests to 22: 18.4s →
64
+ 8.8s in Chromium, 37.5s → 14.9s in Brave, which filters every request and so
65
+ paid the round-trip cost hardest.
49
66
  - **`wrapWithPassphrase` / `unwrapWithPassphrase`** — PBKDF2-SHA256 (200k
50
67
  iterations) + AES-256-GCM passphrase wrap. Pairs naturally with
51
68
  `PersonalSpaceClient` for client-side payload encryption.
52
69
  - **`verifyGroth16` / `initBn128Wasm`** — universal Groth16 verifier driven by
53
- bn128.wasm. Runs anywhere WebAssembly does (Node, browsers, CF Workers).
70
+ bn128.wasm. Runs anywhere WebAssembly does (Node, browsers, edge runtimes).
54
71
  Used by the accelerator's `verifyZkAuthProof`.
55
72
  - ZK building blocks re-exported from `crypto/ZeroKnowledge` (`Field`,
56
73
  `Poseidon`, `PreimagePoK`, `HashKnowledge`, `AuthPublicInput`,
@@ -72,7 +89,7 @@ the `exports` field in `package.json`:
72
89
 
73
90
  The `workers` build deliberately excludes anything that pulls in
74
91
  `snarkjs` / `ffjavascript` transitive deps (which need `URL.createObjectURL`
75
- and other APIs workerd doesn't expose):
92
+ and other APIs edge runtimes don't expose):
76
93
 
77
94
  - `ZeroKnowledge.ts` — `Field`, `Poseidon`, `PreimagePoK`, `verifyPreimagePoK`,
78
95
  etc.
@@ -88,8 +105,8 @@ The `workers` build keeps `KeyStore`, `DoubleRatchet`, `EncryptedSession`,
88
105
  Types in `dist/connect.d.ts` (bundled from the flat `src/browser/index.ts`
89
106
  entry) include everything from all three runtimes so consumers can reference
90
107
  types even when the runtime impl is absent for their target. Calling
91
- `verifyGroth16` / `initBn128Wasm` works under workerd; importing
92
- `PersonalSpaceClient` and calling it in a worker will fail at runtime.
108
+ `verifyGroth16` / `initBn128Wasm` works in the `workers` build; importing
109
+ `PersonalSpaceClient` and calling it there will fail at runtime.
93
110
 
94
111
  ### Bare specifiers and snarkjs
95
112
 
@@ -101,10 +118,17 @@ chat app provides `snarkjs` via an esm.sh import map; Node consumers add
101
118
  ## Installation
102
119
 
103
120
  ```bash
104
- npm install @muhkoo/connect
105
- # or: yarn add @muhkoo/connect / pnpm add @muhkoo/connect
121
+ npm install @muhkoo/connect@alpha
122
+ # or: yarn add @muhkoo/connect@alpha / pnpm add @muhkoo/connect@alpha
106
123
  ```
107
124
 
125
+ Ask for the **`alpha`** tag explicitly. `latest` still points at the older
126
+ `0.13.2-alpha.0` line, so a bare `npm install @muhkoo/connect` installs a build
127
+ without the surface documented here. Two published builds are deprecated and
128
+ should not be pinned: `0.14.0-alpha.0` (reports the wrong version at runtime)
129
+ and `0.14.0-alpha.3` (shipped with no type declarations, so every SDK type
130
+ resolves to `any`).
131
+
108
132
  ESM-only, with bundled type declarations. `snarkjs` is an **optional peer
109
133
  dependency** — install it only if you use the ZK-proof features
110
134
  (`PersonalSpaceClient`, shared-space writes, client-side proof generation):
@@ -117,8 +141,8 @@ npm install snarkjs
117
141
 
118
142
  ### The unified client (recommended)
119
143
 
120
- `Client` is the supported entry point. One object, one app key, three
121
- namespaces — `auth`, `storage`, `message` — all sharing a single session.
144
+ `Client` is the supported entry point. One object, one app key, and every
145
+ namespace sharing a single session.
122
146
 
123
147
  ```typescript
124
148
  import { Client } from "@muhkoo/connect";
@@ -146,8 +170,8 @@ const user = await client.auth.zk.login("alice", "correct horse battery staple")
146
170
  // the server stores only ciphertext.
147
171
  await client.kv.set("todos", "t1", { title: "Buy groceries", completed: false });
148
172
  const todo = await client.kv.get("todos", "t1");
149
- const ids = await client.storage.list("todos");
150
- await client.storage.delete("todos", "t1");
173
+ const ids = await client.kv.list("todos");
174
+ await client.kv.delete("todos", "t1");
151
175
 
152
176
  // Message — realtime pub/sub + end-to-end-encrypted direct messages.
153
177
  const sub = client.message.subscribe("todos", (e) => console.log("update:", e.data));
@@ -170,7 +194,7 @@ online).
170
194
  message once with a shared group key so the server can persist + replay it.
171
195
  `createChannel(name)` mints + registers a channel (you become its first
172
196
  key-holder); `joinChannel(name)` resolves an existing one and is admitted by the
173
- app's **keeper** — a server-side process (the app's Durable Object) that holds
197
+ app's **keeper** — a server-side process the platform runs per app, which holds
174
198
  the channel's group key and re-issues it to newcomers — so a channel is joinable
175
199
  even when no human member is online. The message **relay** stays blind (it only
176
200
  ever sees opaque, ECIES-wrapped key blobs); the **keeper** does hold group keys,
@@ -281,6 +305,97 @@ call. Persist the token across reloads with a custom `sessionStore` (e.g. a
281
305
  `localStorage`-backed one); identity material is never persisted — `unlock()`
282
306
  re-derives it from the password when you need to decrypt or message.
283
307
 
308
+ ### Staying signed in — the `device` factor
309
+
310
+ The master seed is memory-only by design, which is what makes a forgotten
311
+ password unrecoverable and a stolen disk uninteresting. It also means a page
312
+ reload leaves the client holding a valid session and nothing to decrypt with,
313
+ and it means a CLI runs a full ZK login — circuit download and proof — on every
314
+ invocation. Persisting the seed would fix both and give away the whole property.
315
+
316
+ A `device` factor is the middle path. A random key is generated locally, the
317
+ seed is wrapped under it, and **only the ciphertext goes to the vault**. The key
318
+ never leaves the machine that made it, and the server holds a blob it cannot
319
+ open. Revoking the factor makes that local key inert immediately — which a
320
+ stored seed can never be, because nothing can reach out and un-store it.
321
+
322
+ ```typescript
323
+ // Pair once, while signed in. Store `deviceKey` with the tightest permissions
324
+ // available: for as long as the factor exists it is equivalent to the seed.
325
+ const { factorId, deviceKey } = await client.auth.zk.enrollDevice("Matt's laptop");
326
+
327
+ // Later, with no password typed: recovers the seed AND a session.
328
+ const user = await client.auth.zk.loginWithDevice(username, factorId, deviceKey);
329
+ // └ { username, commitment }
330
+
331
+ // Seed only — for a host that already holds a session token on disk.
332
+ await client.auth.zk.unlockWithDevice(username, factorId, deviceKey);
333
+
334
+ await client.auth.zk.listDevices(); // [{ id, label, createdAt }], newest first
335
+ await client.auth.zk.revokeDevice(factorId); // access withdrawn immediately
336
+ ```
337
+
338
+ The two recovery methods are not interchangeable. `unlockWithDevice` establishes
339
+ the **encryption identity only**, which suits a CLI that already has a session
340
+ token on disk. A browser generally does not: its session expires while the
341
+ pairing is still perfectly valid, leaving it able to decrypt everything and
342
+ unable to make an authenticated call. `loginWithDevice` derives the ZK identity
343
+ from the recovered seed and proves it, exactly as `recoverWithPhrase` does — the
344
+ seed has always been sufficient to authenticate, and withholding that only meant
345
+ re-typing a password to prove something already known.
346
+
347
+ A revoked pairing throws **`DeviceRevokedError`**, exported from the package
348
+ root. It is typed because the correct response is specific and the wrong one is
349
+ harmful: discard the stored key and fall back to a password. Retrying cannot
350
+ succeed — the factor is gone from the vault — and silently re-pairing would undo
351
+ a revocation someone performed on purpose. A *wrong* key is distinguishable: it
352
+ fails to decrypt rather than failing to resolve.
353
+
354
+ ```typescript
355
+ import { DeviceRevokedError } from "@muhkoo/connect";
356
+
357
+ try {
358
+ await client.auth.zk.loginWithDevice(username, factorId, deviceKey);
359
+ } catch (err) {
360
+ if (err instanceof DeviceRevokedError) {
361
+ forgetStoredKey(); // never re-pair on the user's behalf
362
+ showPasswordForm();
363
+ } else throw err;
364
+ }
365
+ ```
366
+
367
+ ### Hosted sign-in (`client.auth.hosted`)
368
+
369
+ `client.auth.hosted` sends the browser to Muhkoo's own sign-in page (PKCE
370
+ authorization code in, session + seed out), so an app never handles a password
371
+ and gets passkeys, Google and recovery for free. `redirectUri` must be
372
+ registered for the app in the portal.
373
+
374
+ ```typescript
375
+ await client.auth.hosted.login({ appId, redirectUri: "https://yourapp.com/callback" });
376
+ // …on the callback route:
377
+ if (client.auth.hosted.isCallback()) {
378
+ const user = await client.auth.hosted.handleCallback();
379
+ }
380
+ ```
381
+
382
+ A viewer who ticked *"Keep me signed in on this browser"* has a `device` factor
383
+ on the auth origin, so the hosted page can complete the flow without asking for
384
+ anything. That is the behaviour an app must be able to switch off at exactly one
385
+ moment — its own sign-out. Without `prompt`, clicking "sign in" straight after
386
+ signing out would resume the remembered pairing and look like the sign-out did
387
+ nothing:
388
+
389
+ ```typescript
390
+ await client.auth.hosted.login({ appId, redirectUri, prompt: "login" });
391
+ // forces the credential screen even on a remembered browser
392
+ ```
393
+
394
+ The name is borrowed from the OIDC parameter that solves the same problem. Note
395
+ that the remembered pairing lives on the auth origin only: `localStorage` is
396
+ per-origin, so sharing a registrable domain buys an app nothing here, and the
397
+ key that opens the seed is never handed to a developer app.
398
+
284
399
  > The sections below document the lower-level building blocks the client is
285
400
  > composed from (`BroadcastChannel`, `EncryptedSession`, `PersonalSpaceClient`,
286
401
  > the Groth16 verifier, …). Reach for them directly only when you need control
@@ -392,7 +507,7 @@ const ok = await verifyGroth16(
392
507
  );
393
508
  ```
394
509
 
395
- Initialize once at boot. For CF Workers, pass a pre-compiled
510
+ Initialize once at boot. On an edge runtime, pass a pre-compiled
396
511
  `WebAssembly.Module` to skip runtime compilation:
397
512
  `await initBn128Wasm(myWasmModule)`.
398
513
 
@@ -424,15 +539,20 @@ src/
424
539
  browser/ rollup entrypoint for the browser build
425
540
  server/ rollup entrypoint for the server / default build
426
541
  workers/ rollup entrypoint for the workers build (+ groth16-verifier + bundled bn128.wasm)
542
+ core/ the unified Client: Client, HttpClient, Session, namespaces/
543
+ auth/ ZK identity derivation, AuthClient, device/passkey stores
544
+ spaces/ Space, SpaceCipher, SpaceKeyring (fan-out group encryption)
427
545
  crypto/ KeyStore, DoubleRatchet, DoubleRatchetManager, Authenticator, ZeroKnowledge
428
546
  sessions/ EncryptedSession, BroadcastChannel
429
547
  transport/ WSTransport
430
548
  personal/ PersonalSpaceClient + wrap.ts (excluded from workers build)
549
+ storage/ FileStorage, ShardClient, SharedSpaceClient, Reed-Solomon codec
550
+ vfs/ vcs/ the encrypted filesystem and its history (client.vfs / client.vcs)
551
+ offline/ p2p/ CRDT offline sync; WebRTC peer shard exchange
431
552
  events/ EventCore + EventCoreEvents
432
553
  messaging/ Message, Packet, decorators
433
554
  types/ shared type defs (incl. zk.ts with PREIMAGE_POK_VERIFICATION_KEY)
434
555
  network/ legacy Network class (not exported from any build)
435
- storage/ Reed-Solomon Storage (not exported from any build)
436
556
  utilities/ Logger, base58, ID generation, decorators
437
557
  ```
438
558
 
@@ -458,10 +578,10 @@ yarn lint # eslint ./src
458
578
  - Node >= 20
459
579
  - All three builds rely on globalThis-level WebCrypto (`crypto.subtle`,
460
580
  `crypto.getRandomValues`), available natively in modern browsers, Node 16+,
461
- Bun, Deno, and CF Workers.
581
+ Bun, Deno, and edge runtimes.
462
582
  - The bn128.wasm verifier needs runtime `WebAssembly.compile()` available
463
- (true for all of the above; on CF Workers, pre-compile via wrangler's
464
- `.wasm` import if you want to skip startup cost).
583
+ (true for all of the above; on an edge runtime, hand in a module compiled at
584
+ build time if you want to skip the startup cost).
465
585
 
466
586
  ## License
467
587
 
@@ -4242,11 +4242,21 @@ class FileStorage {
4242
4242
  this.chunkSize = opts.chunkSize ?? 4 * 1024 * 1024;
4243
4243
  this.dataShards = opts.dataShards ?? 4;
4244
4244
  this.parityShards = opts.parityShards ?? 2;
4245
- // Shard reads coalesce into batches, so this is no longer a cap on
4246
- // requests — it is how many hashes are available to batch together. Too
4247
- // low and each batch is small and serial, which is strictly worse than
4248
- // not batching at all.
4249
- this.concurrency = Math.max(1, opts.concurrency ?? 16);
4245
+ // Shard reads coalesce into batches, so this is not a cap on requests —
4246
+ // it is how many hashes are available to batch together.
4247
+ //
4248
+ // 8, not 16, and that is measured rather than reasoned. Raising it to 16
4249
+ // (with the walk at 24) made a 519-file project load SLOWER: median 18.4s
4250
+ // against 14.9s over three runs each. More hashes pending fills batches
4251
+ // closer to the 256 cap, and a bigger batch is one large payload that has
4252
+ // to arrive and decode completely before ANY of its files resolve — so
4253
+ // the extra concurrency buys chunkier round trips with less overlap
4254
+ // rather than more parallelism.
4255
+ //
4256
+ // There is a floor as well as a ceiling: too low and each batch is small
4257
+ // and serial, which is worse than not batching at all. 8 sits between the
4258
+ // two, and moving it in either direction should be measured, not argued.
4259
+ this.concurrency = Math.max(1, opts.concurrency ?? 8);
4250
4260
  this.rsWasmModule = opts.rsWasmModule;
4251
4261
  if (this.chunkSize < 1)
4252
4262
  throw new Error("FileStorage: chunkSize must be >= 1 byte");
@@ -5500,7 +5510,7 @@ function base64UrlDecode(s) {
5500
5510
  * expect (and isn't serving a stale cached bundle). Keep this in sync with
5501
5511
  * `package.json` `version`.
5502
5512
  */
5503
- const VERSION = "0.14.0-alpha.5";
5513
+ const VERSION = "0.14.0-alpha.6";
5504
5514
 
5505
5515
  /**
5506
5516
  * `HttpClient` — the one place the unified {@link Client} talks to the
@@ -12421,10 +12431,12 @@ const MAX_DEPTH = 64;
12421
12431
  * project does not open a request per directory. Browsers cap concurrency per
12422
12432
  * origin anyway, so a larger number mostly moves the queue.
12423
12433
  */
12424
- /** Directory records fetched in parallel while walking. Raised alongside the
12425
- * shard read concurrency: the walk is what discovers the files whose shards
12426
- * then batch together, so a narrow walk throttles everything downstream. */
12427
- const WALK_CONCURRENCY = 24;
12434
+ /** Directory records fetched in parallel while walking.
12435
+ *
12436
+ * Back to 8 with the shard read concurrency: raising both together measured
12437
+ * SLOWER on a 519-file project (median 18.4s against 14.9s), because fuller
12438
+ * batches are chunkier round trips rather than more parallelism. */
12439
+ const WALK_CONCURRENCY = 8;
12428
12440
  /**
12429
12441
  * `Promise.all` with a ceiling, preserving input order.
12430
12442
  *