@muhkoo/connect 0.14.0-alpha.5 → 0.14.0-alpha.7
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 +143 -23
- package/dist/browser/index.js +26 -10
- package/dist/browser/index.js.map +1 -1
- package/dist/server/index.js +26 -10
- package/dist/server/index.js.map +1 -1
- package/package.json +3 -2
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.
|
|
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), `
|
|
25
|
-
|
|
26
|
-
`
|
|
27
|
-
|
|
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,
|
|
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
|
|
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
|
|
92
|
-
`PersonalSpaceClient` and calling it
|
|
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,
|
|
121
|
-
|
|
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.
|
|
150
|
-
await client.
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
464
|
-
|
|
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
|
|
package/dist/browser/index.js
CHANGED
|
@@ -4242,11 +4242,24 @@ 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
|
|
4246
|
-
//
|
|
4247
|
-
//
|
|
4248
|
-
//
|
|
4249
|
-
|
|
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 because nothing measurable argues for more.
|
|
4249
|
+
//
|
|
4250
|
+
// Raising it to 16 (walk at 24) looked 23% slower over three runs each,
|
|
4251
|
+
// and reverting looked like it restored the win — until a third set of
|
|
4252
|
+
// three landed on the same numbers as the RAISED setting. Pooled, the
|
|
4253
|
+
// configurations are indistinguishable: the run-to-run spread on a
|
|
4254
|
+
// 519-file project is 13.9-20.9s, far wider than any difference between
|
|
4255
|
+
// them. Three samples could not have resolved it and should not have
|
|
4256
|
+
// been trusted to.
|
|
4257
|
+
//
|
|
4258
|
+
// What IS established is the floor: too low and each batch is small and
|
|
4259
|
+
// serial, which is worse than not batching at all. Above that the setting
|
|
4260
|
+
// stops mattering at this scale, so it stays at the simpler value. Moving
|
|
4261
|
+
// it needs a sample large enough to see past that spread.
|
|
4262
|
+
this.concurrency = Math.max(1, opts.concurrency ?? 8);
|
|
4250
4263
|
this.rsWasmModule = opts.rsWasmModule;
|
|
4251
4264
|
if (this.chunkSize < 1)
|
|
4252
4265
|
throw new Error("FileStorage: chunkSize must be >= 1 byte");
|
|
@@ -5500,7 +5513,7 @@ function base64UrlDecode(s) {
|
|
|
5500
5513
|
* expect (and isn't serving a stale cached bundle). Keep this in sync with
|
|
5501
5514
|
* `package.json` `version`.
|
|
5502
5515
|
*/
|
|
5503
|
-
const VERSION = "0.14.0-alpha.
|
|
5516
|
+
const VERSION = "0.14.0-alpha.7";
|
|
5504
5517
|
|
|
5505
5518
|
/**
|
|
5506
5519
|
* `HttpClient` — the one place the unified {@link Client} talks to the
|
|
@@ -12421,10 +12434,13 @@ const MAX_DEPTH = 64;
|
|
|
12421
12434
|
* project does not open a request per directory. Browsers cap concurrency per
|
|
12422
12435
|
* origin anyway, so a larger number mostly moves the queue.
|
|
12423
12436
|
*/
|
|
12424
|
-
/** Directory records fetched in parallel while walking.
|
|
12425
|
-
*
|
|
12426
|
-
*
|
|
12427
|
-
|
|
12437
|
+
/** Directory records fetched in parallel while walking.
|
|
12438
|
+
*
|
|
12439
|
+
* Kept at 8 alongside the shard read concurrency. Raising both to 24/16 was
|
|
12440
|
+
* neither better nor worse once enough samples were taken — the spread between
|
|
12441
|
+
* runs is wider than the difference between the settings — so this stays at the
|
|
12442
|
+
* simpler value. See FileStorage's note. */
|
|
12443
|
+
const WALK_CONCURRENCY = 8;
|
|
12428
12444
|
/**
|
|
12429
12445
|
* `Promise.all` with a ceiling, preserving input order.
|
|
12430
12446
|
*
|