@muhkoo/connect 0.11.0-alpha.0 → 0.12.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.
@@ -5327,7 +5327,7 @@ function base64UrlDecode(s) {
5327
5327
  * expect (and isn't serving a stale cached bundle). Keep this in sync with
5328
5328
  * `package.json` `version`.
5329
5329
  */
5330
- const VERSION = "0.11.0-alpha.0";
5330
+ const VERSION = "0.12.0-alpha.0";
5331
5331
 
5332
5332
  /**
5333
5333
  * `HttpClient` — the one place the unified {@link Client} talks to the
@@ -11924,6 +11924,16 @@ function newId() {
11924
11924
  return toBase64(randomBytes(16)).replace(/[+/=]/g, "").slice(0, 22);
11925
11925
  }
11926
11926
  /** The root key, derived from the master seed. The only key not stored in a parent. */
11927
+ /**
11928
+ * The key a project's repository objects are sealed with.
11929
+ *
11930
+ * Per project, not one key for everything: commits, trees and refs describe a
11931
+ * single project's history, and deriving per slug keeps that blast radius small
11932
+ * if a key is ever shared to collaborate on one project.
11933
+ */
11934
+ async function deriveRepoKey(seed, slug) {
11935
+ return deriveBitsHkdf(seed, `${VFS_ROOT_INFO}:vcs:${slug}`, AES_KEY_BYTES);
11936
+ }
11927
11937
  async function deriveRootKey(seed) {
11928
11938
  return deriveBitsHkdf(seed, VFS_ROOT_INFO, AES_KEY_BYTES);
11929
11939
  }
@@ -12236,9 +12246,18 @@ class VfsNamespace {
12236
12246
  return;
12237
12247
  if (!frame.key.startsWith("vfs/"))
12238
12248
  return; // someone else's data on the shared feed
12239
- // Our own writes echo back. Dropping the cache is cheap and keeping a
12240
- // stale entry is not, so this does not try to filter them out.
12241
- this.cache.clear();
12249
+ // Drop only the record that changed.
12250
+ //
12251
+ // Clearing everything looked cheap and is not: our own writes echo
12252
+ // back, so a sync that writes a few hundred files emptied the cache
12253
+ // a few hundred times, and every path resolution in flight went back
12254
+ // to the network for the root and every directory below it. The key
12255
+ // names the record, so there is no need to guess.
12256
+ const id = frame.key.startsWith("vfs/d/") ? frame.key.slice("vfs/d/".length) : null;
12257
+ if (id)
12258
+ this.cache.delete(id);
12259
+ else
12260
+ this.cache.clear(); // a shape we do not recognise: be safe
12242
12261
  handler();
12243
12262
  });
12244
12263
  }
@@ -12286,6 +12305,68 @@ class VfsNamespace {
12286
12305
  async readText(path) {
12287
12306
  return new TextDecoder().decode(await this.readFile(path));
12288
12307
  }
12308
+ /**
12309
+ * A file's manifest — the handle itself, without reading the bytes.
12310
+ *
12311
+ * Version control records manifests rather than content, so a commit costs
12312
+ * a hash and a reference, not a copy.
12313
+ */
12314
+ async statManifest(path) {
12315
+ const entry = await this.expectFile(path);
12316
+ return { manifest: entry.manifest, size: entry.size };
12317
+ }
12318
+ /**
12319
+ * Read content by its manifest, wherever it came from.
12320
+ *
12321
+ * A merge needs the COMMON ANCESTOR's version of a file, which by definition
12322
+ * is not in the working tree any more. The manifest is the whole capability,
12323
+ * so no path lookup is involved.
12324
+ */
12325
+ async readByManifest(manifest) {
12326
+ return this.deps.content.read(manifest);
12327
+ }
12328
+ /**
12329
+ * Pin content that something other than the working tree depends on.
12330
+ *
12331
+ * A commit references a manifest, so it is an OWNER of that content — without
12332
+ * a reference of its own, deleting the file from the working tree would free
12333
+ * shards the history still needs, and checking out an older commit would find
12334
+ * the content gone. (It did, in test.)
12335
+ */
12336
+ async retainManifest(manifest) {
12337
+ await this.deps.content.retain(manifest);
12338
+ }
12339
+ /**
12340
+ * Point a path at content that already exists.
12341
+ *
12342
+ * This is how a checkout restores a file: the bytes are in the shard store
12343
+ * already, so nothing is uploaded or downloaded — only the directory record
12344
+ * changes. Takes a reference, because a second path now depends on those
12345
+ * shards.
12346
+ */
12347
+ async writeManifest(path, manifest, size) {
12348
+ const target = this.resolve(path);
12349
+ const name = basename(target);
12350
+ assertValidName(name);
12351
+ await this.mkdir(dirname(target), { recursive: true });
12352
+ const parent = await this.openDir(dirname(target));
12353
+ const prior = parent.node.entries[name];
12354
+ if (prior?.kind === "dir")
12355
+ throw new VfsConflictError(`${target} is a directory`);
12356
+ await this.deps.content.retain(manifest);
12357
+ const entry = {
12358
+ kind: "file",
12359
+ id: prior?.id ?? newId(),
12360
+ manifest,
12361
+ size,
12362
+ mtime: Date.now(),
12363
+ versions: prior?.versions ?? 0,
12364
+ };
12365
+ await this.mutate(parent, (node) => {
12366
+ node.entries[name] = entry;
12367
+ });
12368
+ return toStat(target, name, entry);
12369
+ }
12289
12370
  // ---- writes -------------------------------------------------------------
12290
12371
  /**
12291
12372
  * Create a directory. `recursive` creates missing parents and tolerates an
@@ -12348,7 +12429,14 @@ class VfsNamespace {
12348
12429
  });
12349
12430
  return toStat(target, name, entry);
12350
12431
  }
12351
- /** Delete a file, or a directory when `recursive`. */
12432
+ /**
12433
+ * Delete a file, or a directory when `recursive`.
12434
+ *
12435
+ * `keepContent` removes the entry WITHOUT releasing its shards. A checkout
12436
+ * needs that: the file is leaving the working tree, but commits still
12437
+ * reference the same manifest, and releasing would free content history
12438
+ * depends on.
12439
+ */
12352
12440
  async delete(path, opts = {}) {
12353
12441
  const target = this.resolve(path);
12354
12442
  if (target === "/")
@@ -12365,6 +12453,9 @@ class VfsNamespace {
12365
12453
  throw new VfsConflictError(`${target} is not empty`);
12366
12454
  await this.purge(child);
12367
12455
  }
12456
+ else if (opts.keepContent) {
12457
+ await this.deps.store.delete(historyKey(entry.id)).catch(() => { });
12458
+ }
12368
12459
  else {
12369
12460
  await this.releaseFile(parent, entry);
12370
12461
  }
@@ -12589,25 +12680,41 @@ class VfsNamespace {
12589
12680
  }
12590
12681
  return dir;
12591
12682
  }
12592
- async openDirById(id, key) {
12683
+ openDirById(id, key) {
12593
12684
  const hit = this.cache.get(id);
12594
12685
  if (hit)
12595
12686
  return hit;
12596
- const raw = await this.deps.store.get(dirKey(id));
12597
- // An absent record is an EMPTY directory, not an error: the root exists
12598
- // before anything has been written to it, and a directory is created by
12599
- // its parent naming it rather than by its own record appearing.
12600
- const node = (await unseal(key, raw)) ?? { v: 1, entries: {}, mtime: Date.now() };
12601
- const open = { id, key, node };
12602
- this.cache.set(id, open);
12603
- return open;
12687
+ // The PROMISE goes in the cache, not the resolved value.
12688
+ //
12689
+ // Reading a project means resolving hundreds of paths that share the
12690
+ // same handful of directories. Caching only the settled value leaves
12691
+ // every concurrent reader to miss and fire its own request for the same
12692
+ // record — which is how loading one project turned into thousands of
12693
+ // identical `vfs/d/<id>` fetches, most of them in flight at once.
12694
+ const pending = (async () => {
12695
+ const raw = await this.deps.store.get(dirKey(id));
12696
+ // An absent record is an EMPTY directory, not an error: the root
12697
+ // exists before anything has been written to it, and a directory is
12698
+ // created by its parent naming it rather than by its own record
12699
+ // appearing.
12700
+ const node = (await unseal(key, raw)) ?? { v: 1, entries: {}, mtime: Date.now() };
12701
+ return { id, key, node };
12702
+ })().catch((err) => {
12703
+ // A failed read must not be remembered, or one blip poisons the
12704
+ // directory for the life of the session.
12705
+ if (this.cache.get(id) === pending)
12706
+ this.cache.delete(id);
12707
+ throw err;
12708
+ });
12709
+ this.cache.set(id, pending);
12710
+ return pending;
12604
12711
  }
12605
12712
  /** Apply an edit to a directory record and persist it. */
12606
12713
  async mutate(dir, edit) {
12607
12714
  edit(dir.node);
12608
12715
  dir.node.mtime = Date.now();
12609
12716
  await this.deps.store.put(dirKey(dir.id), await seal(dir.key, dir.node));
12610
- this.cache.set(dir.id, dir);
12717
+ this.cache.set(dir.id, Promise.resolve(dir));
12611
12718
  }
12612
12719
  async createDir(parentPath, name) {
12613
12720
  assertValidName(name);
@@ -12650,7 +12757,7 @@ class VfsNamespace {
12650
12757
  }
12651
12758
  const open = { id: clone.id, key: fromBase64(clone.key), node };
12652
12759
  await this.deps.store.put(dirKey(open.id), await seal(open.key, node));
12653
- this.cache.set(open.id, open);
12760
+ this.cache.set(open.id, Promise.resolve(open));
12654
12761
  return clone;
12655
12762
  }
12656
12763
  /**
@@ -12702,6 +12809,812 @@ function byDirsThenName(a, b) {
12702
12809
  return a.name.localeCompare(b.name);
12703
12810
  }
12704
12811
 
12812
+ /**
12813
+ * Content addressing for repository objects.
12814
+ *
12815
+ * The hash must depend only on MEANING, never on serialisation accidents — two
12816
+ * clients that build the same tree have to agree on its hash, or history forks
12817
+ * for no reason. So keys are sorted and the encoding is fixed here rather than
12818
+ * left to `JSON.stringify`'s insertion order.
12819
+ */
12820
+ /** Canonical JSON: object keys sorted, recursively. */
12821
+ function canonical(value) {
12822
+ if (value === null || typeof value !== "object")
12823
+ return JSON.stringify(value) ?? "null";
12824
+ if (Array.isArray(value))
12825
+ return `[${value.map(canonical).join(",")}]`;
12826
+ const entries = Object.entries(value)
12827
+ .filter(([, v]) => v !== undefined)
12828
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
12829
+ return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${canonical(v)}`).join(",")}}`;
12830
+ }
12831
+ /** SHA-256 of an object's canonical form, hex. */
12832
+ async function hashObject(value) {
12833
+ const bytes = new TextEncoder().encode(canonical(value));
12834
+ const digest = await crypto.subtle.digest("SHA-256", bytes);
12835
+ return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join("");
12836
+ }
12837
+
12838
+ /**
12839
+ * Three-way line merge.
12840
+ *
12841
+ * Given a common ancestor and two descendants, produce the combined text — or
12842
+ * report the regions where both sides changed the same lines differently.
12843
+ *
12844
+ * The server cannot do any of this: it holds ciphertext and cannot compare
12845
+ * versions, so merging is entirely a client concern and has to be exact here.
12846
+ */
12847
+ /**
12848
+ * Longest common subsequence, as a list of matched index pairs.
12849
+ *
12850
+ * Classic dynamic programming. Fine for source files; a file with hundreds of
12851
+ * thousands of lines would want a smarter algorithm, and would also be a strange
12852
+ * thing to merge.
12853
+ */
12854
+ function lcs(a, b) {
12855
+ const n = a.length;
12856
+ const m = b.length;
12857
+ const table = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
12858
+ for (let i = n - 1; i >= 0; i--) {
12859
+ for (let j = m - 1; j >= 0; j--) {
12860
+ table[i][j] = a[i] === b[j] ? table[i + 1][j + 1] + 1 : Math.max(table[i + 1][j], table[i][j + 1]);
12861
+ }
12862
+ }
12863
+ const pairs = [];
12864
+ let i = 0;
12865
+ let j = 0;
12866
+ while (i < n && j < m) {
12867
+ if (a[i] === b[j]) {
12868
+ pairs.push([i, j]);
12869
+ i++;
12870
+ j++;
12871
+ }
12872
+ else if (table[i + 1][j] >= table[i][j + 1])
12873
+ i++;
12874
+ else
12875
+ j++;
12876
+ }
12877
+ return pairs;
12878
+ }
12879
+ /**
12880
+ * Lines of `base` that BOTH sides left untouched.
12881
+ *
12882
+ * These are the anchors the merge is built around: between two consecutive
12883
+ * anchors, each side has some (possibly empty) replacement, and comparing those
12884
+ * replacements is the entire decision.
12885
+ */
12886
+ function stableAnchors(base, ours, theirs) {
12887
+ const ourMatch = new Map(lcs(base, ours));
12888
+ const theirMatch = new Map(lcs(base, theirs));
12889
+ const anchors = [];
12890
+ for (let b = 0; b < base.length; b++) {
12891
+ const o = ourMatch.get(b);
12892
+ const t = theirMatch.get(b);
12893
+ if (o !== undefined && t !== undefined)
12894
+ anchors.push([b, o, t]);
12895
+ }
12896
+ return anchors;
12897
+ }
12898
+ const same = (a, b) => a.length === b.length && a.every((x, i) => x === b[i]);
12899
+ /**
12900
+ * Merge `ours` and `theirs` over their common ancestor `base`.
12901
+ *
12902
+ * A region where only one side changed takes that side. A region where both
12903
+ * made the SAME change takes it once — two people fixing the same typo is not a
12904
+ * conflict. Only genuinely divergent edits conflict.
12905
+ */
12906
+ function merge3(base, ours, theirs) {
12907
+ const anchors = stableAnchors(base, ours, theirs);
12908
+ const out = [];
12909
+ const conflicts = [];
12910
+ let b = 0;
12911
+ let o = 0;
12912
+ let t = 0;
12913
+ const settle = (ourSlice, theirSlice, baseSlice) => {
12914
+ if (same(ourSlice, theirSlice)) {
12915
+ out.push(...ourSlice); // identical edits, or no edit
12916
+ }
12917
+ else if (same(ourSlice, baseSlice)) {
12918
+ out.push(...theirSlice); // only they changed it
12919
+ }
12920
+ else if (same(theirSlice, baseSlice)) {
12921
+ out.push(...ourSlice); // only we changed it
12922
+ }
12923
+ else {
12924
+ conflicts.push({ line: out.length + 1, ours: ourSlice, theirs: theirSlice });
12925
+ out.push(...ourSlice); // markers are added by the caller
12926
+ }
12927
+ };
12928
+ for (const [ab, ao, at] of anchors) {
12929
+ settle(ours.slice(o, ao), theirs.slice(t, at), base.slice(b, ab));
12930
+ out.push(base[ab]);
12931
+ b = ab + 1;
12932
+ o = ao + 1;
12933
+ t = at + 1;
12934
+ }
12935
+ settle(ours.slice(o), theirs.slice(t), base.slice(b));
12936
+ return { lines: out, conflicts };
12937
+ }
12938
+ /**
12939
+ * Merge to text, writing conflict markers where the sides diverged.
12940
+ *
12941
+ * Markers rather than a bespoke format: they are what every developer already
12942
+ * knows how to resolve, and resolving becomes ordinary editing.
12943
+ */
12944
+ function merge3Text(base, ours, theirs, labels = { ours: "ours", theirs: "theirs" }) {
12945
+ const split = (s) => (s === "" ? [] : s.split("\n"));
12946
+ const anchors = stableAnchors(split(base), split(ours), split(theirs));
12947
+ const B = split(base);
12948
+ const O = split(ours);
12949
+ const T = split(theirs);
12950
+ const out = [];
12951
+ let conflicted = false;
12952
+ let b = 0;
12953
+ let o = 0;
12954
+ let t = 0;
12955
+ const settle = (ourSlice, theirSlice, baseSlice) => {
12956
+ if (same(ourSlice, theirSlice))
12957
+ out.push(...ourSlice);
12958
+ else if (same(ourSlice, baseSlice))
12959
+ out.push(...theirSlice);
12960
+ else if (same(theirSlice, baseSlice))
12961
+ out.push(...ourSlice);
12962
+ else {
12963
+ conflicted = true;
12964
+ out.push(`<<<<<<< ${labels.ours}`, ...ourSlice, "=======", ...theirSlice, `>>>>>>> ${labels.theirs}`);
12965
+ }
12966
+ };
12967
+ for (const [ab, ao, at] of anchors) {
12968
+ settle(O.slice(o, ao), T.slice(t, at), B.slice(b, ab));
12969
+ out.push(B[ab]);
12970
+ b = ab + 1;
12971
+ o = ao + 1;
12972
+ t = at + 1;
12973
+ }
12974
+ settle(O.slice(o), T.slice(t), B.slice(b));
12975
+ return { text: out.join("\n"), conflicted };
12976
+ }
12977
+
12978
+ /** Storage keys. Namespaced per project so repositories cannot collide. */
12979
+ const objectKey = (slug, hash) => `vcs/${slug}/obj/${hash}`;
12980
+ const refKey = (slug, name) => `vcs/${slug}/refs/${name}`;
12981
+ const headKey = (slug) => `vcs/${slug}/HEAD`;
12982
+ const refsIndexKey = (slug) => `vcs/${slug}/refs`;
12983
+ /** An in-progress merge: what to make the second parent when it is committed. */
12984
+ const mergeKey = (slug) => `vcs/${slug}/MERGE`;
12985
+ const DEFAULT_BRANCH = "main";
12986
+ class VcsError extends Error {
12987
+ constructor(message) {
12988
+ super(message);
12989
+ this.name = "VcsError";
12990
+ }
12991
+ }
12992
+
12993
+ /** A project's repository. One per `/apps/<slug>`. */
12994
+ /**
12995
+ * Types never worth attempting a three-way merge on.
12996
+ *
12997
+ * NOT application/octet-stream: that is the UNKNOWN case, which is what every
12998
+ * extensionless text file gets labelled, so it must reach the content sniff
12999
+ * rather than being rejected on the label.
13000
+ */
13001
+ const BINARY_TYPE = /^(image|audio|video|font)\/|^application\/(wasm|zip|pdf|x-tar|gzip)/;
13002
+ /**
13003
+ * Does this look like text a person could resolve conflict markers in?
13004
+ *
13005
+ * A NUL byte is the classic tell and the one git uses — no textual encoding we
13006
+ * care about produces one. Invalid UTF-8 is the other: decoding it would
13007
+ * corrupt the file on write-back even if the merge itself succeeded.
13008
+ */
13009
+ function looksTextual(bytes) {
13010
+ if (bytes.includes(0))
13011
+ return false;
13012
+ try {
13013
+ new TextDecoder("utf-8", { fatal: true }).decode(bytes);
13014
+ return true;
13015
+ }
13016
+ catch {
13017
+ return false;
13018
+ }
13019
+ }
13020
+ /** Compare two path → entry maps into a change list. */
13021
+ function comparePaths(before, after) {
13022
+ const changes = [];
13023
+ for (const [path, entry] of after) {
13024
+ const was = before.get(path);
13025
+ if (!was)
13026
+ changes.push({ path, kind: "added" });
13027
+ else if (was.manifest.id !== entry.manifest.id)
13028
+ changes.push({ path, kind: "modified" });
13029
+ }
13030
+ for (const path of before.keys())
13031
+ if (!after.has(path))
13032
+ changes.push({ path, kind: "removed" });
13033
+ return changes.sort((a, b) => (a.path < b.path ? -1 : 1));
13034
+ }
13035
+ class Repo {
13036
+ slug;
13037
+ deps;
13038
+ constructor(slug, deps) {
13039
+ this.slug = slug;
13040
+ this.deps = deps;
13041
+ }
13042
+ get root() {
13043
+ return `/apps/${this.slug}`;
13044
+ }
13045
+ // ---- objects ------------------------------------------------------------
13046
+ /** Store an object, returning its hash and whether it was new. */
13047
+ async put(value) {
13048
+ const hash = await hashObject(value);
13049
+ // Content-addressed, so re-writing an identical object is a no-op —
13050
+ // worth skipping because an unchanged directory is written on every
13051
+ // commit that touches any of its siblings.
13052
+ const existing = await this.deps.store.get(objectKey(this.slug, hash));
13053
+ if (existing)
13054
+ return { hash, created: false };
13055
+ await this.deps.store.put(objectKey(this.slug, hash), await seal(await this.deps.key(), value));
13056
+ return { hash, created: true };
13057
+ }
13058
+ async get(hash) {
13059
+ const raw = await this.deps.store.get(objectKey(this.slug, hash));
13060
+ const value = raw ? await unseal(await this.deps.key(), raw) : null;
13061
+ if (!value)
13062
+ throw new VcsError(`object ${hash.slice(0, 8)} is missing from this repository`);
13063
+ return value;
13064
+ }
13065
+ // ---- refs ---------------------------------------------------------------
13066
+ async head() {
13067
+ const raw = await this.deps.store.get(headKey(this.slug));
13068
+ const value = raw ? await unseal(await this.deps.key(), raw) : null;
13069
+ return value ?? { branch: DEFAULT_BRANCH };
13070
+ }
13071
+ async setHead(head) {
13072
+ await this.deps.store.put(headKey(this.slug), await seal(await this.deps.key(), head));
13073
+ }
13074
+ async readRef(name) {
13075
+ const raw = await this.deps.store.get(refKey(this.slug, name));
13076
+ return raw ? await unseal(await this.deps.key(), raw).then((v) => v?.commit ?? null) : null;
13077
+ }
13078
+ async writeRef(name, commit) {
13079
+ await this.deps.store.put(refKey(this.slug, name), await seal(await this.deps.key(), { commit }));
13080
+ // The personal space has no prefix listing, so branches are tracked in
13081
+ // an index. Kept append-only-ish: losing a name here loses the branch.
13082
+ const names = new Set(await this.branches());
13083
+ names.add(name);
13084
+ await this.deps.store.put(refsIndexKey(this.slug), await seal(await this.deps.key(), { names: [...names].sort() }));
13085
+ }
13086
+ /** Every branch name in this repository. */
13087
+ async branches() {
13088
+ const raw = await this.deps.store.get(refsIndexKey(this.slug));
13089
+ const value = raw ? await unseal(await this.deps.key(), raw) : null;
13090
+ return value?.names ?? [];
13091
+ }
13092
+ /** The commit HEAD resolves to, or null in an empty repository. */
13093
+ async current() {
13094
+ const head = await this.head();
13095
+ return "detached" in head ? head.detached : this.readRef(head.branch);
13096
+ }
13097
+ // ---- commit -------------------------------------------------------------
13098
+ /**
13099
+ * Freeze the working tree as a commit.
13100
+ *
13101
+ * Trees are written bottom-up so a directory's hash is known before its
13102
+ * parent references it. Nothing is written to the shard store: file entries
13103
+ * carry the manifest the VFS already holds.
13104
+ */
13105
+ async commit(message, parents) {
13106
+ if (!message.trim())
13107
+ throw new VcsError("a commit needs a message");
13108
+ const tree = await this.writeTree(this.root);
13109
+ const parent = await this.current();
13110
+ // Finish an interrupted merge: the recorded other side becomes the
13111
+ // second parent. Losing this would produce a single-parent commit, and
13112
+ // the merge would look as though it never happened — the branches would
13113
+ // still appear diverged.
13114
+ const pending = parents ? null : await this.pendingMerge();
13115
+ const resolved = parents ?? (pending ? [parent, pending.theirs] : parent ? [parent] : []);
13116
+ if (!parents && !pending && parent) {
13117
+ const previous = await this.get(parent);
13118
+ // An empty commit is almost always a mistake — the user believes
13119
+ // they saved something they did not.
13120
+ if (previous.tree === tree)
13121
+ throw new VcsError("nothing to commit — the project is unchanged");
13122
+ }
13123
+ const commit = {
13124
+ v: 1,
13125
+ tree,
13126
+ parents: resolved,
13127
+ message: message.trim(),
13128
+ author: this.deps.author(),
13129
+ at: Date.now(),
13130
+ };
13131
+ const { hash } = await this.put(commit);
13132
+ await this.advance(hash);
13133
+ if (pending)
13134
+ await this.deps.store.delete(mergeKey(this.slug)).catch(() => { });
13135
+ return hash;
13136
+ }
13137
+ /** Move whatever HEAD points at onto a new commit. */
13138
+ async advance(hash) {
13139
+ const head = await this.head();
13140
+ if ("detached" in head)
13141
+ await this.setHead({ detached: hash });
13142
+ else
13143
+ await this.writeRef(head.branch, hash);
13144
+ }
13145
+ /** The other side of a merge that stopped for conflicts, if any. */
13146
+ async pendingMerge() {
13147
+ const raw = await this.deps.store.get(mergeKey(this.slug));
13148
+ return raw ? await unseal(await this.deps.key(), raw) : null;
13149
+ }
13150
+ /** Write a directory and everything under it, returning the tree's hash. */
13151
+ async writeTree(path) {
13152
+ const entries = {};
13153
+ for (const entry of await this.deps.vfs.list(path)) {
13154
+ if (entry.kind === "dir") {
13155
+ entries[entry.name] = { kind: "tree", hash: await this.writeTree(entry.path) };
13156
+ }
13157
+ else {
13158
+ const stat = await this.deps.vfs.statManifest(entry.path);
13159
+ entries[entry.name] = { kind: "file", manifest: stat.manifest, size: entry.size };
13160
+ }
13161
+ }
13162
+ const { hash, created } = await this.put({ v: 1, entries });
13163
+ // A NEW tree means this directory's contents are newly recorded, so the
13164
+ // repository takes its own reference to their content. Without it, a
13165
+ // later working-tree delete frees shards the commit still needs and the
13166
+ // history becomes unreadable.
13167
+ //
13168
+ // Retaining per new tree rather than per changed FILE slightly
13169
+ // over-retains — an unchanged file in a directory whose sibling changed
13170
+ // gets a second reference. That errs towards keeping content, which is
13171
+ // the correct direction to err in.
13172
+ if (created) {
13173
+ for (const entry of Object.values(entries)) {
13174
+ if (entry.kind === "file")
13175
+ await this.deps.vfs.retainManifest(entry.manifest);
13176
+ }
13177
+ }
13178
+ return hash;
13179
+ }
13180
+ // ---- history ------------------------------------------------------------
13181
+ /** Commits reachable from HEAD, newest first. */
13182
+ async log(limit = 50) {
13183
+ const start = await this.current();
13184
+ if (!start)
13185
+ return [];
13186
+ const out = [];
13187
+ const seen = new Set();
13188
+ // Breadth-first over parents so a merge's two sides both appear, and
13189
+ // `seen` keeps a diamond from listing the shared history twice.
13190
+ const queue = [start];
13191
+ while (queue.length && out.length < limit) {
13192
+ const hash = queue.shift();
13193
+ if (seen.has(hash))
13194
+ continue;
13195
+ seen.add(hash);
13196
+ const commit = await this.get(hash);
13197
+ out.push({
13198
+ hash,
13199
+ message: commit.message,
13200
+ author: commit.author,
13201
+ at: commit.at,
13202
+ parents: commit.parents,
13203
+ });
13204
+ queue.push(...commit.parents);
13205
+ }
13206
+ return out.sort((a, b) => b.at - a.at);
13207
+ }
13208
+ // ---- diff ---------------------------------------------------------------
13209
+ /**
13210
+ * What changed between two commits.
13211
+ *
13212
+ * Identical subtree hashes prune the walk: an untouched directory is one
13213
+ * comparison regardless of how much is inside it. That is the payoff for
13214
+ * content-addressing the trees.
13215
+ */
13216
+ async diff(fromHash, toHash) {
13217
+ const from = fromHash ? (await this.get(fromHash)).tree : null;
13218
+ const to = (await this.get(toHash)).tree;
13219
+ const changes = [];
13220
+ await this.diffTrees(from, to, "", changes);
13221
+ return changes.sort((a, b) => (a.path < b.path ? -1 : 1));
13222
+ }
13223
+ async diffTrees(fromHash, toHash, prefix, out) {
13224
+ if (fromHash === toHash)
13225
+ return; // identical subtrees: nothing below can differ
13226
+ const from = fromHash ? await this.get(fromHash) : { entries: {} };
13227
+ const to = toHash ? await this.get(toHash) : { entries: {} };
13228
+ for (const name of new Set([...Object.keys(from.entries), ...Object.keys(to.entries)])) {
13229
+ const a = from.entries[name];
13230
+ const b = to.entries[name];
13231
+ const path = `${prefix}/${name}`;
13232
+ if (a && b && a.kind === "tree" && b.kind === "tree") {
13233
+ await this.diffTrees(a.hash, b.hash, path, out);
13234
+ }
13235
+ else if (a?.kind === "tree" || b?.kind === "tree") {
13236
+ // A directory replaced by a file (or the reverse) — recurse both
13237
+ // sides so every affected path is reported, not just the name.
13238
+ await this.diffTrees(a?.kind === "tree" ? a.hash : null, b?.kind === "tree" ? b.hash : null, path, out);
13239
+ if (a?.kind === "file")
13240
+ out.push({ path, kind: "removed" });
13241
+ if (b?.kind === "file")
13242
+ out.push({ path, kind: "added" });
13243
+ }
13244
+ else if (!a && b) {
13245
+ out.push({ path, kind: "added" });
13246
+ }
13247
+ else if (a && !b) {
13248
+ out.push({ path, kind: "removed" });
13249
+ }
13250
+ else if (a?.kind === "file" && b?.kind === "file" && a.manifest.id !== b.manifest.id) {
13251
+ out.push({ path, kind: "modified" });
13252
+ }
13253
+ }
13254
+ }
13255
+ // ---- checkout -----------------------------------------------------------
13256
+ /**
13257
+ * Replace the working tree with a commit's.
13258
+ *
13259
+ * Moves no file content: entries carry manifests the shard store already
13260
+ * holds, so this rewrites directory records only.
13261
+ */
13262
+ /**
13263
+ * What has changed since the last commit.
13264
+ *
13265
+ * Compares the WORKING TREE against HEAD without writing any objects: the
13266
+ * common case is answering "anything to commit?", and a `status` that
13267
+ * quietly stored a tree object every time would litter the repository with
13268
+ * trees for states nobody chose to keep.
13269
+ */
13270
+ async status() {
13271
+ const head = await this.current();
13272
+ const before = head ? await this.filesOf(head) : new Map();
13273
+ const after = await this.workingFiles();
13274
+ return comparePaths(before, after);
13275
+ }
13276
+ /** The working tree as a path → entry map, mirroring `filesOf`. */
13277
+ async workingFiles() {
13278
+ const out = new Map();
13279
+ for (const abs of await this.deps.vfs.walk(this.root)) {
13280
+ const stat = await this.deps.vfs.statManifest(abs);
13281
+ out.set(abs.slice(this.root.length), { kind: "file", manifest: stat.manifest, size: stat.size });
13282
+ }
13283
+ return out;
13284
+ }
13285
+ /**
13286
+ * Turn what a person typed into a commit hash.
13287
+ *
13288
+ * Accepts a branch name, `HEAD`, a full or abbreviated hash, and trailing
13289
+ * `^` / `~n` to walk back through parents — so `show abc123^` means "the
13290
+ * commit before that one" the way anyone who has used git expects.
13291
+ */
13292
+ async resolve(rev) {
13293
+ const m = /^(.*?)((?:\^|~\d+)*)$/.exec(rev.trim());
13294
+ if (!m)
13295
+ throw new VcsError(`cannot understand "${rev}"`);
13296
+ const [, base, steps] = m;
13297
+ let hash = null;
13298
+ if (!base || base === "HEAD")
13299
+ hash = await this.current();
13300
+ else if (await this.readRef(base))
13301
+ hash = await this.readRef(base);
13302
+ else
13303
+ hash = await this.expand(base);
13304
+ if (!hash)
13305
+ throw new VcsError(`no commit matches "${rev}"`);
13306
+ for (const step of steps.match(/\^|~\d+/g) ?? []) {
13307
+ const back = step === "^" ? 1 : Number(step.slice(1));
13308
+ for (let i = 0; i < back; i++) {
13309
+ const parents = (await this.get(hash)).parents;
13310
+ if (!parents.length)
13311
+ throw new VcsError(`"${rev}" reaches past the first commit`);
13312
+ hash = parents[0]; // first parent: the branch you were on
13313
+ }
13314
+ }
13315
+ return hash;
13316
+ }
13317
+ /** A hash prefix a person typed → the one commit it names. */
13318
+ async expand(prefix) {
13319
+ if (!/^[0-9a-f]{4,}$/.test(prefix))
13320
+ return null;
13321
+ const keys = await this.deps.store.list();
13322
+ const wanted = objectKey(this.slug, prefix);
13323
+ const hits = keys.filter((k) => k.startsWith(wanted));
13324
+ // Ambiguous is an error, not a coin toss — acting on the wrong commit
13325
+ // is exactly the kind of mistake version control exists to prevent.
13326
+ if (hits.length > 1)
13327
+ throw new VcsError(`"${prefix}" matches ${hits.length} objects — use more characters`);
13328
+ if (!hits.length)
13329
+ return null;
13330
+ const hash = hits[0].slice(objectKey(this.slug, "").length);
13331
+ // Objects include trees; only a commit is a valid revision.
13332
+ const object = await this.get(hash);
13333
+ return object && "tree" in object ? hash : null;
13334
+ }
13335
+ /**
13336
+ * Put one file back to the way it was in a commit.
13337
+ *
13338
+ * Scoped to a path on purpose — recovering one file you broke should not
13339
+ * mean rewinding everything else you have done since.
13340
+ */
13341
+ async restore(path, rev = "HEAD") {
13342
+ const hash = await this.resolve(rev);
13343
+ const files = await this.filesOf(hash);
13344
+ const key = path.startsWith(this.root) ? path.slice(this.root.length) : path.startsWith("/") ? path : `/${path}`;
13345
+ const entry = files.get(key);
13346
+ if (!entry)
13347
+ throw new VcsError(`${key} is not in ${rev}`);
13348
+ await this.deps.vfs.writeManifest(this.abs(key), entry.manifest, entry.size);
13349
+ }
13350
+ async checkout(commitHash, opts = {}) {
13351
+ await this.guardWorkingTree(opts.discardChanges);
13352
+ await this.materialise(commitHash);
13353
+ // DETACHED on purpose: committing from a historical state must not
13354
+ // silently rewrite whichever branch you happened to be on. `switchTo`
13355
+ // is how you re-attach.
13356
+ await this.setHead({ detached: commitHash });
13357
+ }
13358
+ /** Make the working tree match a commit, leaving HEAD alone. */
13359
+ async materialise(commitHash) {
13360
+ const commit = await this.get(commitHash);
13361
+ const wanted = new Map();
13362
+ await this.collect(commit.tree, "", wanted);
13363
+ // Remove what the commit does not have BEFORE writing what it does —
13364
+ // otherwise a file replaced by a directory of the same name collides.
13365
+ for (const abs of await this.deps.vfs.walk(this.root)) {
13366
+ const path = abs.slice(this.root.length);
13367
+ // keepContent: history still references these manifests.
13368
+ if (!wanted.has(path))
13369
+ await this.deps.vfs.delete(abs, { keepContent: true }).catch(() => { });
13370
+ }
13371
+ for (const [path, entry] of wanted) {
13372
+ await this.deps.vfs.writeManifest(this.abs(path), entry.manifest, entry.size);
13373
+ }
13374
+ }
13375
+ /** Project-relative path → absolute VFS path. */
13376
+ abs(path) {
13377
+ return `${this.root}${path}`;
13378
+ }
13379
+ async collect(treeHash, prefix, out) {
13380
+ const tree = await this.get(treeHash);
13381
+ for (const [name, entry] of Object.entries(tree.entries)) {
13382
+ const path = `${prefix}/${name}`;
13383
+ if (entry.kind === "tree")
13384
+ await this.collect(entry.hash, path, out);
13385
+ else
13386
+ out.set(path, entry);
13387
+ }
13388
+ }
13389
+ // ---- branches -----------------------------------------------------------
13390
+ /** Create a branch at a commit (HEAD by default). Does not switch to it. */
13391
+ async branch(name, at) {
13392
+ if (!/^[A-Za-z0-9][A-Za-z0-9._\/-]*$/.test(name))
13393
+ throw new VcsError(`"${name}" is not a usable branch name`);
13394
+ if (await this.readRef(name))
13395
+ throw new VcsError(`branch "${name}" already exists`);
13396
+ const commit = at ?? (await this.current());
13397
+ if (!commit)
13398
+ throw new VcsError("cannot branch an empty repository — commit something first");
13399
+ await this.writeRef(name, commit);
13400
+ return commit;
13401
+ }
13402
+ /**
13403
+ * Move onto a branch, bringing the working tree with it.
13404
+ *
13405
+ * Unlike {@link checkout}, this ATTACHES head: later commits advance the
13406
+ * branch. Checking out a bare commit deliberately does not, so that
13407
+ * committing from a historical state cannot silently rewrite a branch.
13408
+ */
13409
+ async switchTo(name, opts = {}) {
13410
+ const commit = await this.readRef(name);
13411
+ if (!commit)
13412
+ throw new VcsError(`no branch named "${name}"`);
13413
+ await this.guardWorkingTree(opts.discardChanges);
13414
+ await this.materialise(commit);
13415
+ await this.setHead({ branch: name });
13416
+ }
13417
+ /**
13418
+ * Refuse to overwrite work that was never recorded.
13419
+ *
13420
+ * Moving between commits REPLACES the working tree, so uncommitted edits
13421
+ * are gone — and unlike an ordinary overwrite, the person did not ask for
13422
+ * anything to be written. Naming the files is the point: "you have changes"
13423
+ * with no list leaves them guessing what they are about to lose.
13424
+ */
13425
+ async guardWorkingTree(discard) {
13426
+ if (discard)
13427
+ return;
13428
+ const pending = await this.status();
13429
+ if (!pending.length)
13430
+ return;
13431
+ const names = pending.slice(0, 5).map((c) => c.path).join(", ");
13432
+ const more = pending.length > 5 ? `, and ${pending.length - 5} more` : "";
13433
+ throw new VcsError(`${pending.length} uncommitted change(s) would be lost: ${names}${more}. ` +
13434
+ "Commit them first, or pass { discardChanges: true } to throw them away.");
13435
+ }
13436
+ /** The branch HEAD is on, or null when detached. */
13437
+ async currentBranch() {
13438
+ const head = await this.head();
13439
+ return "detached" in head ? null : head.branch;
13440
+ }
13441
+ // ---- merge --------------------------------------------------------------
13442
+ /**
13443
+ * Merge another branch into the current one.
13444
+ *
13445
+ * Every part of this is client-side by necessity: the server holds
13446
+ * ciphertext and cannot compare versions, so base, ours and theirs are all
13447
+ * fetched and decrypted here.
13448
+ */
13449
+ async merge(name, opts = {}) {
13450
+ const theirs = await this.readRef(name);
13451
+ if (!theirs)
13452
+ throw new VcsError(`no branch named "${name}"`);
13453
+ // A merge writes to the working tree as freely as a switch does.
13454
+ await this.guardWorkingTree(opts.discardChanges);
13455
+ const ours = await this.current();
13456
+ if (!ours)
13457
+ throw new VcsError("nothing to merge into — commit something first");
13458
+ if (ours === theirs)
13459
+ return { kind: "up-to-date", conflicts: [] };
13460
+ const base = await this.mergeBase(ours, theirs);
13461
+ if (base === theirs)
13462
+ return { kind: "up-to-date", conflicts: [] };
13463
+ if (base === ours) {
13464
+ // Fast-forward: our history is contained in theirs, so there is
13465
+ // nothing to combine — just move.
13466
+ await this.materialise(theirs);
13467
+ await this.advance(theirs);
13468
+ return { kind: "fast-forward", commit: theirs, conflicts: [] };
13469
+ }
13470
+ const [baseFiles, ourFiles, theirFiles] = await Promise.all([
13471
+ base ? this.filesOf(base) : new Map(),
13472
+ this.filesOf(ours),
13473
+ this.filesOf(theirs),
13474
+ ]);
13475
+ const conflicts = [];
13476
+ for (const path of new Set([...ourFiles.keys(), ...theirFiles.keys(), ...baseFiles.keys()])) {
13477
+ const b = baseFiles.get(path);
13478
+ const o = ourFiles.get(path);
13479
+ const t = theirFiles.get(path);
13480
+ const conflict = await this.settlePath(path, b, o, t);
13481
+ if (conflict)
13482
+ conflicts.push(conflict);
13483
+ }
13484
+ if (conflicts.length) {
13485
+ // Record what to merge with, so finishing is just: fix the files,
13486
+ // then commit. Losing this would silently produce a commit with one
13487
+ // parent, and the merge would appear never to have happened.
13488
+ await this.deps.store.put(mergeKey(this.slug), await seal(await this.deps.key(), { theirs, name }));
13489
+ return { kind: "conflicted", conflicts };
13490
+ }
13491
+ const commit = await this.commit(`Merge ${name}`, [ours, theirs]);
13492
+ return { kind: "merged", commit, conflicts: [] };
13493
+ }
13494
+ /** Decide one path. Returns a conflict when it cannot be settled. */
13495
+ async settlePath(path, b, o, t) {
13496
+ const id = (f) => f?.manifest.id;
13497
+ if (id(o) === id(t))
13498
+ return null; // already agree
13499
+ if (!o && !b && t)
13500
+ return this.take(path, t); // only they added it
13501
+ if (!t && !b && o)
13502
+ return null; // only we added it
13503
+ if (id(o) === id(b)) { // only they touched it
13504
+ if (!t) {
13505
+ await this.deps.vfs.delete(this.abs(path), { keepContent: true }).catch(() => { });
13506
+ return null;
13507
+ }
13508
+ return this.take(path, t);
13509
+ }
13510
+ if (id(t) === id(b))
13511
+ return null; // only we touched it
13512
+ // Both sides moved. A file one side deleted and the other edited is not
13513
+ // a merge decision a tool should make silently.
13514
+ if (!o || !t)
13515
+ return { path, reason: "modify-delete" };
13516
+ // Definitely-binary types are rejected WITHOUT reading them — merging
13517
+ // bytes line-by-line produces corruption, not a result, and these files
13518
+ // are exactly the large ones.
13519
+ if (BINARY_TYPE.test(o.manifest.type ?? "") || BINARY_TYPE.test(t.manifest.type ?? "")) {
13520
+ return { path, reason: "binary" };
13521
+ }
13522
+ const [baseBytes, ourBytes, theirBytes] = await Promise.all([
13523
+ b ? this.deps.vfs.readByManifest(b.manifest) : Promise.resolve(new Uint8Array()),
13524
+ this.deps.vfs.readByManifest(o.manifest),
13525
+ this.deps.vfs.readByManifest(t.manifest),
13526
+ ]);
13527
+ // Then judge by CONTENT, not by the declared type.
13528
+ //
13529
+ // The type is a hint we do not own: a content store may omit it, and
13530
+ // plenty of real text files carry no extension to derive one from
13531
+ // (Makefile, LICENSE, .gitignore, .env). Trusting it alone turned every
13532
+ // one of those into an unmergeable false conflict.
13533
+ if (![ourBytes, theirBytes, baseBytes].every(looksTextual))
13534
+ return { path, reason: "binary" };
13535
+ const decode = (bytes) => new TextDecoder().decode(bytes);
13536
+ const { text, conflicted } = merge3Text(decode(baseBytes), decode(ourBytes), decode(theirBytes));
13537
+ await this.deps.vfs.writeFile(this.abs(path), text);
13538
+ return conflicted ? { path, reason: "content" } : null;
13539
+ }
13540
+ async take(path, entry) {
13541
+ await this.deps.vfs.writeManifest(this.abs(path), entry.manifest, entry.size);
13542
+ return null;
13543
+ }
13544
+ /**
13545
+ * The most recent commit reachable from both.
13546
+ *
13547
+ * Walks our ancestry into a set, then theirs breadth-first until it hits it —
13548
+ * so the first match is the nearest, which is what a three-way merge needs.
13549
+ */
13550
+ async mergeBase(a, b) {
13551
+ const ancestors = new Set();
13552
+ const collect = async (start) => {
13553
+ const queue = [start];
13554
+ while (queue.length) {
13555
+ const hash = queue.shift();
13556
+ if (ancestors.has(hash))
13557
+ continue;
13558
+ ancestors.add(hash);
13559
+ queue.push(...(await this.get(hash)).parents);
13560
+ }
13561
+ };
13562
+ await collect(a);
13563
+ const queue = [b];
13564
+ const seen = new Set();
13565
+ while (queue.length) {
13566
+ const hash = queue.shift();
13567
+ if (seen.has(hash))
13568
+ continue;
13569
+ seen.add(hash);
13570
+ if (ancestors.has(hash))
13571
+ return hash;
13572
+ queue.push(...(await this.get(hash)).parents);
13573
+ }
13574
+ return null;
13575
+ }
13576
+ /** Every file in a commit, by path. */
13577
+ async filesOf(commitHash) {
13578
+ const commit = await this.get(commitHash);
13579
+ const out = new Map();
13580
+ await this.collect(commit.tree, "", out);
13581
+ return out;
13582
+ }
13583
+ }
13584
+ /**
13585
+ * Version control over the VFS.
13586
+ *
13587
+ * One repository per project, addressed by slug — the same slug the VFS stores
13588
+ * that project's working tree under. `client.vcs.open("my-app")` is the whole
13589
+ * entry point.
13590
+ */
13591
+ class VcsNamespace {
13592
+ deps;
13593
+ repos = new Map();
13594
+ constructor(deps) {
13595
+ this.deps = deps;
13596
+ }
13597
+ /** The repository for one project. Cheap and cached; creates nothing. */
13598
+ open(slug) {
13599
+ let repo = this.repos.get(slug);
13600
+ if (!repo) {
13601
+ repo = new Repo(slug, {
13602
+ vfs: this.deps.vfs,
13603
+ store: this.deps.store,
13604
+ key: async () => {
13605
+ const seed = this.deps.seed();
13606
+ if (!seed)
13607
+ throw new VcsError("client.vcs: no session — sign in first.");
13608
+ return deriveRepoKey(seed, slug);
13609
+ },
13610
+ author: this.deps.author,
13611
+ });
13612
+ this.repos.set(slug, repo);
13613
+ }
13614
+ return repo;
13615
+ }
13616
+ }
13617
+
12705
13618
  /**
12706
13619
  * `ChatKeyVault` — SDK-owned lifecycle for a member's long-lived ratchet/space
12707
13620
  * keypair (the connect `KeyStore` entry keyed by the member id / username).
@@ -13945,6 +14858,8 @@ class Client {
13945
14858
  storage;
13946
14859
  /** Filesystem over the personal space: directories, versions, real paths. */
13947
14860
  vfs;
14861
+ /** Version control over the VFS: commits, branches, merges — per project. */
14862
+ vcs;
13948
14863
  /** Realtime messaging — `client.message.subscribe(...)`, etc. */
13949
14864
  message;
13950
14865
  /** Fan-out group spaces — `client.space.createSpace(...)`, etc. */
@@ -14132,6 +15047,15 @@ class Client {
14132
15047
  },
14133
15048
  },
14134
15049
  });
15050
+ // Sealed into the SAME personal store as the VFS metadata, so a project
15051
+ // and its history live or die together — there is no way to end up with
15052
+ // commits whose working tree is gone, or the reverse.
15053
+ this.vcs = new VcsNamespace({
15054
+ vfs: this.vfs,
15055
+ store: personalStore,
15056
+ seed: () => this.session.seed,
15057
+ author: () => this.session.commitment ?? "unknown",
15058
+ });
14135
15059
  // Group keys are cached in the user's PersonalSpace (encrypted at rest
14136
15060
  // by client.kv), so a returning member hydrates without a fresh keyring
14137
15061
  // round-trip. The server only ever sees ciphertext.
@@ -14600,5 +15524,5 @@ var bundledLoader = /*#__PURE__*/Object.freeze({
14600
15524
  default: loadRsWasm
14601
15525
  });
14602
15526
 
14603
- export { ACCESS_TOKEN_SCOPES, AES_GCM_IV_BYTES$1 as AES_GCM_IV_BYTES, AES_GCM_KEY_BYTES, AES_GCM_TAG_BYTES, AccessTokensNamespace, AgentsNamespace, AuthClient, AuthNamespace, AuthPublicInput, Authenticator, BlockEngine, BroadcastChannel, BroadcastChannelEvents, ChannelExistsError, ChannelNotFoundError, ChunkCipher, Client, ConnectivityManager, DEFAULT_AUTH_BASE_URL, DEFAULT_BASE_URL, DEFAULT_FN_HOST_SUFFIX, DEVICE_IDENTITY_STORAGE_KEY, DEVICE_KEY_DB_NAME, DbCache, DevicePairingError, 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, 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, PREIMAGE_POK_VERIFICATION_KEY, Packet, PasskeyOriginError, PeerExchange, PeerNetwork, PersonalSpaceClient, Poseidon, PreimagePoK, ReauthRequiredError, ReedSolomonCodec, Space as Room, SerializeMessage, SessionState, ShardCache, ShardClient, SharedSpaceClient, SnapshotNamespace, Space, SpaceCache, SpaceKeyring, SpaceNamespace, SpacePacketCipher, SpaceSignaler, StorageNamespace, SyncEngine, VaultUnavailableError, WSTransport, WebRtcTransport, WorkerEngineHost, ZERO_HLC, ZkAuth, buildCommitment, canonicalUserCode, clearDeviceIdentity, compareHlc, compilePrograms, configureDeviceStore, createPasskeyWithPrf, decodeFromHex, decorators, decryptAesGcm, defaultCircuitUrls, defaultRpId, defaultSessionStore, deleteBeatsWrite, deleteRegister, deriveAesKeyFromPassphrase, deriveBitsHkdf, deriveIdentity, deriveIdentityFromSeed, deriveMasterSeedFromPassword, deviceFingerprint, deviceIdentityIsEphemeral, deviceIdentityKey, ejectAgentPrompt, ejectAgentTools, emptyMap, emptyRegister, encodeToHex, encryptAesGcm, evaluatePasskeyPrf, exportPublicKeyBase64, exportPublicKeyHex, formatUserCode, generateAuthProof, generateDevicePairingKeypair, generatePkce, getMuhkooAppDescriptor, getSubtle, hasDeviceIdentity, initBn128Wasm, initRsWasm, initializeCircuits, isNewer, isOfflineCapable, isP2pCapable, isProvisional, loadDeviceIdentity, manifestToStat, materializeRow, mergeMap, mergeMessage, mergeRegister, messageKey, pack, padHandle, pairingVerificationCode, passkeyPrfCapable, passkeySupported, passkeyUsableFromOrigin, persistDeviceIdentity, poseidonHash, protocol, provisionalHandle, quickVerify, randomBytes, randomState, rpIdUsableForOrigin, sealSeed, sealSeedToDevice, shardHash, signMessage, stableStringify, toField, tombstoneRow, unpack, unsealSeed, unsealSeedFromDevice, unwrapWithPassphrase, verify, verifyGroth16, verifyHashKnowledge, verifyPreimagePoK, wrapWithPassphrase, writeBeatsTombstone, writeColumns, writeRegister };
15527
+ export { ACCESS_TOKEN_SCOPES, AES_GCM_IV_BYTES$1 as AES_GCM_IV_BYTES, AES_GCM_KEY_BYTES, AES_GCM_TAG_BYTES, AccessTokensNamespace, AgentsNamespace, AuthClient, AuthNamespace, AuthPublicInput, Authenticator, BlockEngine, BroadcastChannel, BroadcastChannelEvents, ChannelExistsError, ChannelNotFoundError, ChunkCipher, Client, ConnectivityManager, DEFAULT_AUTH_BASE_URL, DEFAULT_BASE_URL, DEFAULT_BRANCH, DEFAULT_FN_HOST_SUFFIX, DEVICE_IDENTITY_STORAGE_KEY, DEVICE_KEY_DB_NAME, DbCache, DevicePairingError, 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, 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, PREIMAGE_POK_VERIFICATION_KEY, Packet, PasskeyOriginError, PeerExchange, PeerNetwork, PersonalSpaceClient, Poseidon, PreimagePoK, ROOT_ID, ReauthRequiredError, ReedSolomonCodec, Repo, Space as Room, SerializeMessage, SessionState, ShardCache, ShardClient, SharedSpaceClient, SnapshotNamespace, Space, SpaceCache, SpaceKeyring, SpaceNamespace, SpacePacketCipher, SpaceSignaler, StorageNamespace, SyncEngine, VFS_ROOT_INFO, VaultUnavailableError, VcsError, VcsNamespace, VfsConflictError, VfsLockedError, VfsNamespace, VfsNotFoundError, WSTransport, WebRtcTransport, WorkerEngineHost, ZERO_HLC, ZkAuth, buildCommitment, canonicalUserCode, clearDeviceIdentity, compareHlc, compilePrograms, configureDeviceStore, createPasskeyWithPrf, decodeFromHex, decorators, decryptAesGcm, defaultCircuitUrls, defaultRpId, defaultSessionStore, deleteBeatsWrite, deleteRegister, deriveAesKeyFromPassphrase, deriveBitsHkdf, deriveIdentity, deriveIdentityFromSeed, deriveMasterSeedFromPassword, deviceFingerprint, deviceIdentityIsEphemeral, deviceIdentityKey, dirKey, ejectAgentPrompt, ejectAgentTools, emptyMap, emptyRegister, encodeToHex, encryptAesGcm, evaluatePasskeyPrf, exportPublicKeyBase64, exportPublicKeyHex, formatUserCode, generateAuthProof, generateDevicePairingKeypair, generatePkce, getMuhkooAppDescriptor, getSubtle, hasDeviceIdentity, headKey, historyKey, initBn128Wasm, initRsWasm, initializeCircuits, isNewer, isOfflineCapable, isP2pCapable, isProvisional, loadDeviceIdentity, 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, shardHash, signMessage, stableStringify, toField, tombstoneRow, unpack, unsealSeed, unsealSeedFromDevice, unwrapWithPassphrase, verify, verifyGroth16, verifyHashKnowledge, verifyPreimagePoK, wrapWithPassphrase, writeBeatsTombstone, writeColumns, writeRegister };
14604
15528
  //# sourceMappingURL=index.js.map