@muhkoo/connect 0.11.0-alpha.0 → 0.12.1-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.
@@ -3235,7 +3235,7 @@ function base64UrlDecode(s) {
3235
3235
  * expect (and isn't serving a stale cached bundle). Keep this in sync with
3236
3236
  * `package.json` `version`.
3237
3237
  */
3238
- const VERSION = "0.11.0-alpha.0";
3238
+ const VERSION = "0.12.1-alpha.0";
3239
3239
 
3240
3240
  /**
3241
3241
  * `HttpClient` — the one place the unified {@link Client} talks to the
@@ -9025,7 +9025,12 @@ class Space {
9025
9025
  const hashes = manifest.chunks.flatMap((c) => c.shardHashes);
9026
9026
  if (hashes.length === 0)
9027
9027
  return;
9028
- const body = JSON.stringify({ hashes, bytes: manifest.size });
9028
+ // `manifestId` is what lets the platform tell two copies of the same
9029
+ // bytes apart. Without it the reference ledger keys on content, so the
9030
+ // second copy takes no reference and releasing either frees shards the
9031
+ // other still needs — silent data loss. Sent on BOTH commit and release,
9032
+ // because a release has to reproduce the key its commit took.
9033
+ const body = JSON.stringify({ hashes, bytes: manifest.size, manifestId: manifest.id });
9029
9034
  for (let attempt = 0; attempt < 3; attempt++) {
9030
9035
  try {
9031
9036
  const res = await this.deps.fetch(`${this.deps.httpBaseUrl}/api/storage/${action}`, {
@@ -11655,6 +11660,16 @@ function newId() {
11655
11660
  return toBase64(randomBytes(16)).replace(/[+/=]/g, "").slice(0, 22);
11656
11661
  }
11657
11662
  /** The root key, derived from the master seed. The only key not stored in a parent. */
11663
+ /**
11664
+ * The key a project's repository objects are sealed with.
11665
+ *
11666
+ * Per project, not one key for everything: commits, trees and refs describe a
11667
+ * single project's history, and deriving per slug keeps that blast radius small
11668
+ * if a key is ever shared to collaborate on one project.
11669
+ */
11670
+ async function deriveRepoKey(seed, slug) {
11671
+ return deriveBitsHkdf(seed, `${VFS_ROOT_INFO}:vcs:${slug}`, AES_KEY_BYTES);
11672
+ }
11658
11673
  async function deriveRootKey(seed) {
11659
11674
  return deriveBitsHkdf(seed, VFS_ROOT_INFO, AES_KEY_BYTES);
11660
11675
  }
@@ -11967,9 +11982,18 @@ class VfsNamespace {
11967
11982
  return;
11968
11983
  if (!frame.key.startsWith("vfs/"))
11969
11984
  return; // someone else's data on the shared feed
11970
- // Our own writes echo back. Dropping the cache is cheap and keeping a
11971
- // stale entry is not, so this does not try to filter them out.
11972
- this.cache.clear();
11985
+ // Drop only the record that changed.
11986
+ //
11987
+ // Clearing everything looked cheap and is not: our own writes echo
11988
+ // back, so a sync that writes a few hundred files emptied the cache
11989
+ // a few hundred times, and every path resolution in flight went back
11990
+ // to the network for the root and every directory below it. The key
11991
+ // names the record, so there is no need to guess.
11992
+ const id = frame.key.startsWith("vfs/d/") ? frame.key.slice("vfs/d/".length) : null;
11993
+ if (id)
11994
+ this.cache.delete(id);
11995
+ else
11996
+ this.cache.clear(); // a shape we do not recognise: be safe
11973
11997
  handler();
11974
11998
  });
11975
11999
  }
@@ -12017,6 +12041,68 @@ class VfsNamespace {
12017
12041
  async readText(path) {
12018
12042
  return new TextDecoder().decode(await this.readFile(path));
12019
12043
  }
12044
+ /**
12045
+ * A file's manifest — the handle itself, without reading the bytes.
12046
+ *
12047
+ * Version control records manifests rather than content, so a commit costs
12048
+ * a hash and a reference, not a copy.
12049
+ */
12050
+ async statManifest(path) {
12051
+ const entry = await this.expectFile(path);
12052
+ return { manifest: entry.manifest, size: entry.size };
12053
+ }
12054
+ /**
12055
+ * Read content by its manifest, wherever it came from.
12056
+ *
12057
+ * A merge needs the COMMON ANCESTOR's version of a file, which by definition
12058
+ * is not in the working tree any more. The manifest is the whole capability,
12059
+ * so no path lookup is involved.
12060
+ */
12061
+ async readByManifest(manifest) {
12062
+ return this.deps.content.read(manifest);
12063
+ }
12064
+ /**
12065
+ * Pin content that something other than the working tree depends on.
12066
+ *
12067
+ * A commit references a manifest, so it is an OWNER of that content — without
12068
+ * a reference of its own, deleting the file from the working tree would free
12069
+ * shards the history still needs, and checking out an older commit would find
12070
+ * the content gone. (It did, in test.)
12071
+ */
12072
+ async retainManifest(manifest) {
12073
+ await this.deps.content.retain(manifest);
12074
+ }
12075
+ /**
12076
+ * Point a path at content that already exists.
12077
+ *
12078
+ * This is how a checkout restores a file: the bytes are in the shard store
12079
+ * already, so nothing is uploaded or downloaded — only the directory record
12080
+ * changes. Takes a reference, because a second path now depends on those
12081
+ * shards.
12082
+ */
12083
+ async writeManifest(path, manifest, size) {
12084
+ const target = this.resolve(path);
12085
+ const name = basename(target);
12086
+ assertValidName(name);
12087
+ await this.mkdir(dirname(target), { recursive: true });
12088
+ const parent = await this.openDir(dirname(target));
12089
+ const prior = parent.node.entries[name];
12090
+ if (prior?.kind === "dir")
12091
+ throw new VfsConflictError(`${target} is a directory`);
12092
+ await this.deps.content.retain(manifest);
12093
+ const entry = {
12094
+ kind: "file",
12095
+ id: prior?.id ?? newId(),
12096
+ manifest,
12097
+ size,
12098
+ mtime: Date.now(),
12099
+ versions: prior?.versions ?? 0,
12100
+ };
12101
+ await this.mutate(parent, (node) => {
12102
+ node.entries[name] = entry;
12103
+ });
12104
+ return toStat(target, name, entry);
12105
+ }
12020
12106
  // ---- writes -------------------------------------------------------------
12021
12107
  /**
12022
12108
  * Create a directory. `recursive` creates missing parents and tolerates an
@@ -12079,7 +12165,14 @@ class VfsNamespace {
12079
12165
  });
12080
12166
  return toStat(target, name, entry);
12081
12167
  }
12082
- /** Delete a file, or a directory when `recursive`. */
12168
+ /**
12169
+ * Delete a file, or a directory when `recursive`.
12170
+ *
12171
+ * `keepContent` removes the entry WITHOUT releasing its shards. A checkout
12172
+ * needs that: the file is leaving the working tree, but commits still
12173
+ * reference the same manifest, and releasing would free content history
12174
+ * depends on.
12175
+ */
12083
12176
  async delete(path, opts = {}) {
12084
12177
  const target = this.resolve(path);
12085
12178
  if (target === "/")
@@ -12096,6 +12189,9 @@ class VfsNamespace {
12096
12189
  throw new VfsConflictError(`${target} is not empty`);
12097
12190
  await this.purge(child);
12098
12191
  }
12192
+ else if (opts.keepContent) {
12193
+ await this.deps.store.delete(historyKey(entry.id)).catch(() => { });
12194
+ }
12099
12195
  else {
12100
12196
  await this.releaseFile(parent, entry);
12101
12197
  }
@@ -12320,25 +12416,41 @@ class VfsNamespace {
12320
12416
  }
12321
12417
  return dir;
12322
12418
  }
12323
- async openDirById(id, key) {
12419
+ openDirById(id, key) {
12324
12420
  const hit = this.cache.get(id);
12325
12421
  if (hit)
12326
12422
  return hit;
12327
- const raw = await this.deps.store.get(dirKey(id));
12328
- // An absent record is an EMPTY directory, not an error: the root exists
12329
- // before anything has been written to it, and a directory is created by
12330
- // its parent naming it rather than by its own record appearing.
12331
- const node = (await unseal(key, raw)) ?? { v: 1, entries: {}, mtime: Date.now() };
12332
- const open = { id, key, node };
12333
- this.cache.set(id, open);
12334
- return open;
12423
+ // The PROMISE goes in the cache, not the resolved value.
12424
+ //
12425
+ // Reading a project means resolving hundreds of paths that share the
12426
+ // same handful of directories. Caching only the settled value leaves
12427
+ // every concurrent reader to miss and fire its own request for the same
12428
+ // record — which is how loading one project turned into thousands of
12429
+ // identical `vfs/d/<id>` fetches, most of them in flight at once.
12430
+ const pending = (async () => {
12431
+ const raw = await this.deps.store.get(dirKey(id));
12432
+ // An absent record is an EMPTY directory, not an error: the root
12433
+ // exists before anything has been written to it, and a directory is
12434
+ // created by its parent naming it rather than by its own record
12435
+ // appearing.
12436
+ const node = (await unseal(key, raw)) ?? { v: 1, entries: {}, mtime: Date.now() };
12437
+ return { id, key, node };
12438
+ })().catch((err) => {
12439
+ // A failed read must not be remembered, or one blip poisons the
12440
+ // directory for the life of the session.
12441
+ if (this.cache.get(id) === pending)
12442
+ this.cache.delete(id);
12443
+ throw err;
12444
+ });
12445
+ this.cache.set(id, pending);
12446
+ return pending;
12335
12447
  }
12336
12448
  /** Apply an edit to a directory record and persist it. */
12337
12449
  async mutate(dir, edit) {
12338
12450
  edit(dir.node);
12339
12451
  dir.node.mtime = Date.now();
12340
12452
  await this.deps.store.put(dirKey(dir.id), await seal(dir.key, dir.node));
12341
- this.cache.set(dir.id, dir);
12453
+ this.cache.set(dir.id, Promise.resolve(dir));
12342
12454
  }
12343
12455
  async createDir(parentPath, name) {
12344
12456
  assertValidName(name);
@@ -12381,7 +12493,7 @@ class VfsNamespace {
12381
12493
  }
12382
12494
  const open = { id: clone.id, key: fromBase64(clone.key), node };
12383
12495
  await this.deps.store.put(dirKey(open.id), await seal(open.key, node));
12384
- this.cache.set(open.id, open);
12496
+ this.cache.set(open.id, Promise.resolve(open));
12385
12497
  return clone;
12386
12498
  }
12387
12499
  /**
@@ -12433,6 +12545,812 @@ function byDirsThenName(a, b) {
12433
12545
  return a.name.localeCompare(b.name);
12434
12546
  }
12435
12547
 
12548
+ /**
12549
+ * Content addressing for repository objects.
12550
+ *
12551
+ * The hash must depend only on MEANING, never on serialisation accidents — two
12552
+ * clients that build the same tree have to agree on its hash, or history forks
12553
+ * for no reason. So keys are sorted and the encoding is fixed here rather than
12554
+ * left to `JSON.stringify`'s insertion order.
12555
+ */
12556
+ /** Canonical JSON: object keys sorted, recursively. */
12557
+ function canonical(value) {
12558
+ if (value === null || typeof value !== "object")
12559
+ return JSON.stringify(value) ?? "null";
12560
+ if (Array.isArray(value))
12561
+ return `[${value.map(canonical).join(",")}]`;
12562
+ const entries = Object.entries(value)
12563
+ .filter(([, v]) => v !== undefined)
12564
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
12565
+ return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${canonical(v)}`).join(",")}}`;
12566
+ }
12567
+ /** SHA-256 of an object's canonical form, hex. */
12568
+ async function hashObject(value) {
12569
+ const bytes = new TextEncoder().encode(canonical(value));
12570
+ const digest = await crypto.subtle.digest("SHA-256", bytes);
12571
+ return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join("");
12572
+ }
12573
+
12574
+ /**
12575
+ * Three-way line merge.
12576
+ *
12577
+ * Given a common ancestor and two descendants, produce the combined text — or
12578
+ * report the regions where both sides changed the same lines differently.
12579
+ *
12580
+ * The server cannot do any of this: it holds ciphertext and cannot compare
12581
+ * versions, so merging is entirely a client concern and has to be exact here.
12582
+ */
12583
+ /**
12584
+ * Longest common subsequence, as a list of matched index pairs.
12585
+ *
12586
+ * Classic dynamic programming. Fine for source files; a file with hundreds of
12587
+ * thousands of lines would want a smarter algorithm, and would also be a strange
12588
+ * thing to merge.
12589
+ */
12590
+ function lcs(a, b) {
12591
+ const n = a.length;
12592
+ const m = b.length;
12593
+ const table = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
12594
+ for (let i = n - 1; i >= 0; i--) {
12595
+ for (let j = m - 1; j >= 0; j--) {
12596
+ table[i][j] = a[i] === b[j] ? table[i + 1][j + 1] + 1 : Math.max(table[i + 1][j], table[i][j + 1]);
12597
+ }
12598
+ }
12599
+ const pairs = [];
12600
+ let i = 0;
12601
+ let j = 0;
12602
+ while (i < n && j < m) {
12603
+ if (a[i] === b[j]) {
12604
+ pairs.push([i, j]);
12605
+ i++;
12606
+ j++;
12607
+ }
12608
+ else if (table[i + 1][j] >= table[i][j + 1])
12609
+ i++;
12610
+ else
12611
+ j++;
12612
+ }
12613
+ return pairs;
12614
+ }
12615
+ /**
12616
+ * Lines of `base` that BOTH sides left untouched.
12617
+ *
12618
+ * These are the anchors the merge is built around: between two consecutive
12619
+ * anchors, each side has some (possibly empty) replacement, and comparing those
12620
+ * replacements is the entire decision.
12621
+ */
12622
+ function stableAnchors(base, ours, theirs) {
12623
+ const ourMatch = new Map(lcs(base, ours));
12624
+ const theirMatch = new Map(lcs(base, theirs));
12625
+ const anchors = [];
12626
+ for (let b = 0; b < base.length; b++) {
12627
+ const o = ourMatch.get(b);
12628
+ const t = theirMatch.get(b);
12629
+ if (o !== undefined && t !== undefined)
12630
+ anchors.push([b, o, t]);
12631
+ }
12632
+ return anchors;
12633
+ }
12634
+ const same = (a, b) => a.length === b.length && a.every((x, i) => x === b[i]);
12635
+ /**
12636
+ * Merge `ours` and `theirs` over their common ancestor `base`.
12637
+ *
12638
+ * A region where only one side changed takes that side. A region where both
12639
+ * made the SAME change takes it once — two people fixing the same typo is not a
12640
+ * conflict. Only genuinely divergent edits conflict.
12641
+ */
12642
+ function merge3(base, ours, theirs) {
12643
+ const anchors = stableAnchors(base, ours, theirs);
12644
+ const out = [];
12645
+ const conflicts = [];
12646
+ let b = 0;
12647
+ let o = 0;
12648
+ let t = 0;
12649
+ const settle = (ourSlice, theirSlice, baseSlice) => {
12650
+ if (same(ourSlice, theirSlice)) {
12651
+ out.push(...ourSlice); // identical edits, or no edit
12652
+ }
12653
+ else if (same(ourSlice, baseSlice)) {
12654
+ out.push(...theirSlice); // only they changed it
12655
+ }
12656
+ else if (same(theirSlice, baseSlice)) {
12657
+ out.push(...ourSlice); // only we changed it
12658
+ }
12659
+ else {
12660
+ conflicts.push({ line: out.length + 1, ours: ourSlice, theirs: theirSlice });
12661
+ out.push(...ourSlice); // markers are added by the caller
12662
+ }
12663
+ };
12664
+ for (const [ab, ao, at] of anchors) {
12665
+ settle(ours.slice(o, ao), theirs.slice(t, at), base.slice(b, ab));
12666
+ out.push(base[ab]);
12667
+ b = ab + 1;
12668
+ o = ao + 1;
12669
+ t = at + 1;
12670
+ }
12671
+ settle(ours.slice(o), theirs.slice(t), base.slice(b));
12672
+ return { lines: out, conflicts };
12673
+ }
12674
+ /**
12675
+ * Merge to text, writing conflict markers where the sides diverged.
12676
+ *
12677
+ * Markers rather than a bespoke format: they are what every developer already
12678
+ * knows how to resolve, and resolving becomes ordinary editing.
12679
+ */
12680
+ function merge3Text(base, ours, theirs, labels = { ours: "ours", theirs: "theirs" }) {
12681
+ const split = (s) => (s === "" ? [] : s.split("\n"));
12682
+ const anchors = stableAnchors(split(base), split(ours), split(theirs));
12683
+ const B = split(base);
12684
+ const O = split(ours);
12685
+ const T = split(theirs);
12686
+ const out = [];
12687
+ let conflicted = false;
12688
+ let b = 0;
12689
+ let o = 0;
12690
+ let t = 0;
12691
+ const settle = (ourSlice, theirSlice, baseSlice) => {
12692
+ if (same(ourSlice, theirSlice))
12693
+ out.push(...ourSlice);
12694
+ else if (same(ourSlice, baseSlice))
12695
+ out.push(...theirSlice);
12696
+ else if (same(theirSlice, baseSlice))
12697
+ out.push(...ourSlice);
12698
+ else {
12699
+ conflicted = true;
12700
+ out.push(`<<<<<<< ${labels.ours}`, ...ourSlice, "=======", ...theirSlice, `>>>>>>> ${labels.theirs}`);
12701
+ }
12702
+ };
12703
+ for (const [ab, ao, at] of anchors) {
12704
+ settle(O.slice(o, ao), T.slice(t, at), B.slice(b, ab));
12705
+ out.push(B[ab]);
12706
+ b = ab + 1;
12707
+ o = ao + 1;
12708
+ t = at + 1;
12709
+ }
12710
+ settle(O.slice(o), T.slice(t), B.slice(b));
12711
+ return { text: out.join("\n"), conflicted };
12712
+ }
12713
+
12714
+ /** Storage keys. Namespaced per project so repositories cannot collide. */
12715
+ const objectKey = (slug, hash) => `vcs/${slug}/obj/${hash}`;
12716
+ const refKey = (slug, name) => `vcs/${slug}/refs/${name}`;
12717
+ const headKey = (slug) => `vcs/${slug}/HEAD`;
12718
+ const refsIndexKey = (slug) => `vcs/${slug}/refs`;
12719
+ /** An in-progress merge: what to make the second parent when it is committed. */
12720
+ const mergeKey = (slug) => `vcs/${slug}/MERGE`;
12721
+ const DEFAULT_BRANCH = "main";
12722
+ class VcsError extends Error {
12723
+ constructor(message) {
12724
+ super(message);
12725
+ this.name = "VcsError";
12726
+ }
12727
+ }
12728
+
12729
+ /** A project's repository. One per `/apps/<slug>`. */
12730
+ /**
12731
+ * Types never worth attempting a three-way merge on.
12732
+ *
12733
+ * NOT application/octet-stream: that is the UNKNOWN case, which is what every
12734
+ * extensionless text file gets labelled, so it must reach the content sniff
12735
+ * rather than being rejected on the label.
12736
+ */
12737
+ const BINARY_TYPE = /^(image|audio|video|font)\/|^application\/(wasm|zip|pdf|x-tar|gzip)/;
12738
+ /**
12739
+ * Does this look like text a person could resolve conflict markers in?
12740
+ *
12741
+ * A NUL byte is the classic tell and the one git uses — no textual encoding we
12742
+ * care about produces one. Invalid UTF-8 is the other: decoding it would
12743
+ * corrupt the file on write-back even if the merge itself succeeded.
12744
+ */
12745
+ function looksTextual(bytes) {
12746
+ if (bytes.includes(0))
12747
+ return false;
12748
+ try {
12749
+ new TextDecoder("utf-8", { fatal: true }).decode(bytes);
12750
+ return true;
12751
+ }
12752
+ catch {
12753
+ return false;
12754
+ }
12755
+ }
12756
+ /** Compare two path → entry maps into a change list. */
12757
+ function comparePaths(before, after) {
12758
+ const changes = [];
12759
+ for (const [path, entry] of after) {
12760
+ const was = before.get(path);
12761
+ if (!was)
12762
+ changes.push({ path, kind: "added" });
12763
+ else if (was.manifest.id !== entry.manifest.id)
12764
+ changes.push({ path, kind: "modified" });
12765
+ }
12766
+ for (const path of before.keys())
12767
+ if (!after.has(path))
12768
+ changes.push({ path, kind: "removed" });
12769
+ return changes.sort((a, b) => (a.path < b.path ? -1 : 1));
12770
+ }
12771
+ class Repo {
12772
+ slug;
12773
+ deps;
12774
+ constructor(slug, deps) {
12775
+ this.slug = slug;
12776
+ this.deps = deps;
12777
+ }
12778
+ get root() {
12779
+ return `/apps/${this.slug}`;
12780
+ }
12781
+ // ---- objects ------------------------------------------------------------
12782
+ /** Store an object, returning its hash and whether it was new. */
12783
+ async put(value) {
12784
+ const hash = await hashObject(value);
12785
+ // Content-addressed, so re-writing an identical object is a no-op —
12786
+ // worth skipping because an unchanged directory is written on every
12787
+ // commit that touches any of its siblings.
12788
+ const existing = await this.deps.store.get(objectKey(this.slug, hash));
12789
+ if (existing)
12790
+ return { hash, created: false };
12791
+ await this.deps.store.put(objectKey(this.slug, hash), await seal(await this.deps.key(), value));
12792
+ return { hash, created: true };
12793
+ }
12794
+ async get(hash) {
12795
+ const raw = await this.deps.store.get(objectKey(this.slug, hash));
12796
+ const value = raw ? await unseal(await this.deps.key(), raw) : null;
12797
+ if (!value)
12798
+ throw new VcsError(`object ${hash.slice(0, 8)} is missing from this repository`);
12799
+ return value;
12800
+ }
12801
+ // ---- refs ---------------------------------------------------------------
12802
+ async head() {
12803
+ const raw = await this.deps.store.get(headKey(this.slug));
12804
+ const value = raw ? await unseal(await this.deps.key(), raw) : null;
12805
+ return value ?? { branch: DEFAULT_BRANCH };
12806
+ }
12807
+ async setHead(head) {
12808
+ await this.deps.store.put(headKey(this.slug), await seal(await this.deps.key(), head));
12809
+ }
12810
+ async readRef(name) {
12811
+ const raw = await this.deps.store.get(refKey(this.slug, name));
12812
+ return raw ? await unseal(await this.deps.key(), raw).then((v) => v?.commit ?? null) : null;
12813
+ }
12814
+ async writeRef(name, commit) {
12815
+ await this.deps.store.put(refKey(this.slug, name), await seal(await this.deps.key(), { commit }));
12816
+ // The personal space has no prefix listing, so branches are tracked in
12817
+ // an index. Kept append-only-ish: losing a name here loses the branch.
12818
+ const names = new Set(await this.branches());
12819
+ names.add(name);
12820
+ await this.deps.store.put(refsIndexKey(this.slug), await seal(await this.deps.key(), { names: [...names].sort() }));
12821
+ }
12822
+ /** Every branch name in this repository. */
12823
+ async branches() {
12824
+ const raw = await this.deps.store.get(refsIndexKey(this.slug));
12825
+ const value = raw ? await unseal(await this.deps.key(), raw) : null;
12826
+ return value?.names ?? [];
12827
+ }
12828
+ /** The commit HEAD resolves to, or null in an empty repository. */
12829
+ async current() {
12830
+ const head = await this.head();
12831
+ return "detached" in head ? head.detached : this.readRef(head.branch);
12832
+ }
12833
+ // ---- commit -------------------------------------------------------------
12834
+ /**
12835
+ * Freeze the working tree as a commit.
12836
+ *
12837
+ * Trees are written bottom-up so a directory's hash is known before its
12838
+ * parent references it. Nothing is written to the shard store: file entries
12839
+ * carry the manifest the VFS already holds.
12840
+ */
12841
+ async commit(message, parents) {
12842
+ if (!message.trim())
12843
+ throw new VcsError("a commit needs a message");
12844
+ const tree = await this.writeTree(this.root);
12845
+ const parent = await this.current();
12846
+ // Finish an interrupted merge: the recorded other side becomes the
12847
+ // second parent. Losing this would produce a single-parent commit, and
12848
+ // the merge would look as though it never happened — the branches would
12849
+ // still appear diverged.
12850
+ const pending = parents ? null : await this.pendingMerge();
12851
+ const resolved = parents ?? (pending ? [parent, pending.theirs] : parent ? [parent] : []);
12852
+ if (!parents && !pending && parent) {
12853
+ const previous = await this.get(parent);
12854
+ // An empty commit is almost always a mistake — the user believes
12855
+ // they saved something they did not.
12856
+ if (previous.tree === tree)
12857
+ throw new VcsError("nothing to commit — the project is unchanged");
12858
+ }
12859
+ const commit = {
12860
+ v: 1,
12861
+ tree,
12862
+ parents: resolved,
12863
+ message: message.trim(),
12864
+ author: this.deps.author(),
12865
+ at: Date.now(),
12866
+ };
12867
+ const { hash } = await this.put(commit);
12868
+ await this.advance(hash);
12869
+ if (pending)
12870
+ await this.deps.store.delete(mergeKey(this.slug)).catch(() => { });
12871
+ return hash;
12872
+ }
12873
+ /** Move whatever HEAD points at onto a new commit. */
12874
+ async advance(hash) {
12875
+ const head = await this.head();
12876
+ if ("detached" in head)
12877
+ await this.setHead({ detached: hash });
12878
+ else
12879
+ await this.writeRef(head.branch, hash);
12880
+ }
12881
+ /** The other side of a merge that stopped for conflicts, if any. */
12882
+ async pendingMerge() {
12883
+ const raw = await this.deps.store.get(mergeKey(this.slug));
12884
+ return raw ? await unseal(await this.deps.key(), raw) : null;
12885
+ }
12886
+ /** Write a directory and everything under it, returning the tree's hash. */
12887
+ async writeTree(path) {
12888
+ const entries = {};
12889
+ for (const entry of await this.deps.vfs.list(path)) {
12890
+ if (entry.kind === "dir") {
12891
+ entries[entry.name] = { kind: "tree", hash: await this.writeTree(entry.path) };
12892
+ }
12893
+ else {
12894
+ const stat = await this.deps.vfs.statManifest(entry.path);
12895
+ entries[entry.name] = { kind: "file", manifest: stat.manifest, size: entry.size };
12896
+ }
12897
+ }
12898
+ const { hash, created } = await this.put({ v: 1, entries });
12899
+ // A NEW tree means this directory's contents are newly recorded, so the
12900
+ // repository takes its own reference to their content. Without it, a
12901
+ // later working-tree delete frees shards the commit still needs and the
12902
+ // history becomes unreadable.
12903
+ //
12904
+ // Retaining per new tree rather than per changed FILE slightly
12905
+ // over-retains — an unchanged file in a directory whose sibling changed
12906
+ // gets a second reference. That errs towards keeping content, which is
12907
+ // the correct direction to err in.
12908
+ if (created) {
12909
+ for (const entry of Object.values(entries)) {
12910
+ if (entry.kind === "file")
12911
+ await this.deps.vfs.retainManifest(entry.manifest);
12912
+ }
12913
+ }
12914
+ return hash;
12915
+ }
12916
+ // ---- history ------------------------------------------------------------
12917
+ /** Commits reachable from HEAD, newest first. */
12918
+ async log(limit = 50) {
12919
+ const start = await this.current();
12920
+ if (!start)
12921
+ return [];
12922
+ const out = [];
12923
+ const seen = new Set();
12924
+ // Breadth-first over parents so a merge's two sides both appear, and
12925
+ // `seen` keeps a diamond from listing the shared history twice.
12926
+ const queue = [start];
12927
+ while (queue.length && out.length < limit) {
12928
+ const hash = queue.shift();
12929
+ if (seen.has(hash))
12930
+ continue;
12931
+ seen.add(hash);
12932
+ const commit = await this.get(hash);
12933
+ out.push({
12934
+ hash,
12935
+ message: commit.message,
12936
+ author: commit.author,
12937
+ at: commit.at,
12938
+ parents: commit.parents,
12939
+ });
12940
+ queue.push(...commit.parents);
12941
+ }
12942
+ return out.sort((a, b) => b.at - a.at);
12943
+ }
12944
+ // ---- diff ---------------------------------------------------------------
12945
+ /**
12946
+ * What changed between two commits.
12947
+ *
12948
+ * Identical subtree hashes prune the walk: an untouched directory is one
12949
+ * comparison regardless of how much is inside it. That is the payoff for
12950
+ * content-addressing the trees.
12951
+ */
12952
+ async diff(fromHash, toHash) {
12953
+ const from = fromHash ? (await this.get(fromHash)).tree : null;
12954
+ const to = (await this.get(toHash)).tree;
12955
+ const changes = [];
12956
+ await this.diffTrees(from, to, "", changes);
12957
+ return changes.sort((a, b) => (a.path < b.path ? -1 : 1));
12958
+ }
12959
+ async diffTrees(fromHash, toHash, prefix, out) {
12960
+ if (fromHash === toHash)
12961
+ return; // identical subtrees: nothing below can differ
12962
+ const from = fromHash ? await this.get(fromHash) : { entries: {} };
12963
+ const to = toHash ? await this.get(toHash) : { entries: {} };
12964
+ for (const name of new Set([...Object.keys(from.entries), ...Object.keys(to.entries)])) {
12965
+ const a = from.entries[name];
12966
+ const b = to.entries[name];
12967
+ const path = `${prefix}/${name}`;
12968
+ if (a && b && a.kind === "tree" && b.kind === "tree") {
12969
+ await this.diffTrees(a.hash, b.hash, path, out);
12970
+ }
12971
+ else if (a?.kind === "tree" || b?.kind === "tree") {
12972
+ // A directory replaced by a file (or the reverse) — recurse both
12973
+ // sides so every affected path is reported, not just the name.
12974
+ await this.diffTrees(a?.kind === "tree" ? a.hash : null, b?.kind === "tree" ? b.hash : null, path, out);
12975
+ if (a?.kind === "file")
12976
+ out.push({ path, kind: "removed" });
12977
+ if (b?.kind === "file")
12978
+ out.push({ path, kind: "added" });
12979
+ }
12980
+ else if (!a && b) {
12981
+ out.push({ path, kind: "added" });
12982
+ }
12983
+ else if (a && !b) {
12984
+ out.push({ path, kind: "removed" });
12985
+ }
12986
+ else if (a?.kind === "file" && b?.kind === "file" && a.manifest.id !== b.manifest.id) {
12987
+ out.push({ path, kind: "modified" });
12988
+ }
12989
+ }
12990
+ }
12991
+ // ---- checkout -----------------------------------------------------------
12992
+ /**
12993
+ * Replace the working tree with a commit's.
12994
+ *
12995
+ * Moves no file content: entries carry manifests the shard store already
12996
+ * holds, so this rewrites directory records only.
12997
+ */
12998
+ /**
12999
+ * What has changed since the last commit.
13000
+ *
13001
+ * Compares the WORKING TREE against HEAD without writing any objects: the
13002
+ * common case is answering "anything to commit?", and a `status` that
13003
+ * quietly stored a tree object every time would litter the repository with
13004
+ * trees for states nobody chose to keep.
13005
+ */
13006
+ async status() {
13007
+ const head = await this.current();
13008
+ const before = head ? await this.filesOf(head) : new Map();
13009
+ const after = await this.workingFiles();
13010
+ return comparePaths(before, after);
13011
+ }
13012
+ /** The working tree as a path → entry map, mirroring `filesOf`. */
13013
+ async workingFiles() {
13014
+ const out = new Map();
13015
+ for (const abs of await this.deps.vfs.walk(this.root)) {
13016
+ const stat = await this.deps.vfs.statManifest(abs);
13017
+ out.set(abs.slice(this.root.length), { kind: "file", manifest: stat.manifest, size: stat.size });
13018
+ }
13019
+ return out;
13020
+ }
13021
+ /**
13022
+ * Turn what a person typed into a commit hash.
13023
+ *
13024
+ * Accepts a branch name, `HEAD`, a full or abbreviated hash, and trailing
13025
+ * `^` / `~n` to walk back through parents — so `show abc123^` means "the
13026
+ * commit before that one" the way anyone who has used git expects.
13027
+ */
13028
+ async resolve(rev) {
13029
+ const m = /^(.*?)((?:\^|~\d+)*)$/.exec(rev.trim());
13030
+ if (!m)
13031
+ throw new VcsError(`cannot understand "${rev}"`);
13032
+ const [, base, steps] = m;
13033
+ let hash = null;
13034
+ if (!base || base === "HEAD")
13035
+ hash = await this.current();
13036
+ else if (await this.readRef(base))
13037
+ hash = await this.readRef(base);
13038
+ else
13039
+ hash = await this.expand(base);
13040
+ if (!hash)
13041
+ throw new VcsError(`no commit matches "${rev}"`);
13042
+ for (const step of steps.match(/\^|~\d+/g) ?? []) {
13043
+ const back = step === "^" ? 1 : Number(step.slice(1));
13044
+ for (let i = 0; i < back; i++) {
13045
+ const parents = (await this.get(hash)).parents;
13046
+ if (!parents.length)
13047
+ throw new VcsError(`"${rev}" reaches past the first commit`);
13048
+ hash = parents[0]; // first parent: the branch you were on
13049
+ }
13050
+ }
13051
+ return hash;
13052
+ }
13053
+ /** A hash prefix a person typed → the one commit it names. */
13054
+ async expand(prefix) {
13055
+ if (!/^[0-9a-f]{4,}$/.test(prefix))
13056
+ return null;
13057
+ const keys = await this.deps.store.list();
13058
+ const wanted = objectKey(this.slug, prefix);
13059
+ const hits = keys.filter((k) => k.startsWith(wanted));
13060
+ // Ambiguous is an error, not a coin toss — acting on the wrong commit
13061
+ // is exactly the kind of mistake version control exists to prevent.
13062
+ if (hits.length > 1)
13063
+ throw new VcsError(`"${prefix}" matches ${hits.length} objects — use more characters`);
13064
+ if (!hits.length)
13065
+ return null;
13066
+ const hash = hits[0].slice(objectKey(this.slug, "").length);
13067
+ // Objects include trees; only a commit is a valid revision.
13068
+ const object = await this.get(hash);
13069
+ return object && "tree" in object ? hash : null;
13070
+ }
13071
+ /**
13072
+ * Put one file back to the way it was in a commit.
13073
+ *
13074
+ * Scoped to a path on purpose — recovering one file you broke should not
13075
+ * mean rewinding everything else you have done since.
13076
+ */
13077
+ async restore(path, rev = "HEAD") {
13078
+ const hash = await this.resolve(rev);
13079
+ const files = await this.filesOf(hash);
13080
+ const key = path.startsWith(this.root) ? path.slice(this.root.length) : path.startsWith("/") ? path : `/${path}`;
13081
+ const entry = files.get(key);
13082
+ if (!entry)
13083
+ throw new VcsError(`${key} is not in ${rev}`);
13084
+ await this.deps.vfs.writeManifest(this.abs(key), entry.manifest, entry.size);
13085
+ }
13086
+ async checkout(commitHash, opts = {}) {
13087
+ await this.guardWorkingTree(opts.discardChanges);
13088
+ await this.materialise(commitHash);
13089
+ // DETACHED on purpose: committing from a historical state must not
13090
+ // silently rewrite whichever branch you happened to be on. `switchTo`
13091
+ // is how you re-attach.
13092
+ await this.setHead({ detached: commitHash });
13093
+ }
13094
+ /** Make the working tree match a commit, leaving HEAD alone. */
13095
+ async materialise(commitHash) {
13096
+ const commit = await this.get(commitHash);
13097
+ const wanted = new Map();
13098
+ await this.collect(commit.tree, "", wanted);
13099
+ // Remove what the commit does not have BEFORE writing what it does —
13100
+ // otherwise a file replaced by a directory of the same name collides.
13101
+ for (const abs of await this.deps.vfs.walk(this.root)) {
13102
+ const path = abs.slice(this.root.length);
13103
+ // keepContent: history still references these manifests.
13104
+ if (!wanted.has(path))
13105
+ await this.deps.vfs.delete(abs, { keepContent: true }).catch(() => { });
13106
+ }
13107
+ for (const [path, entry] of wanted) {
13108
+ await this.deps.vfs.writeManifest(this.abs(path), entry.manifest, entry.size);
13109
+ }
13110
+ }
13111
+ /** Project-relative path → absolute VFS path. */
13112
+ abs(path) {
13113
+ return `${this.root}${path}`;
13114
+ }
13115
+ async collect(treeHash, prefix, out) {
13116
+ const tree = await this.get(treeHash);
13117
+ for (const [name, entry] of Object.entries(tree.entries)) {
13118
+ const path = `${prefix}/${name}`;
13119
+ if (entry.kind === "tree")
13120
+ await this.collect(entry.hash, path, out);
13121
+ else
13122
+ out.set(path, entry);
13123
+ }
13124
+ }
13125
+ // ---- branches -----------------------------------------------------------
13126
+ /** Create a branch at a commit (HEAD by default). Does not switch to it. */
13127
+ async branch(name, at) {
13128
+ if (!/^[A-Za-z0-9][A-Za-z0-9._\/-]*$/.test(name))
13129
+ throw new VcsError(`"${name}" is not a usable branch name`);
13130
+ if (await this.readRef(name))
13131
+ throw new VcsError(`branch "${name}" already exists`);
13132
+ const commit = at ?? (await this.current());
13133
+ if (!commit)
13134
+ throw new VcsError("cannot branch an empty repository — commit something first");
13135
+ await this.writeRef(name, commit);
13136
+ return commit;
13137
+ }
13138
+ /**
13139
+ * Move onto a branch, bringing the working tree with it.
13140
+ *
13141
+ * Unlike {@link checkout}, this ATTACHES head: later commits advance the
13142
+ * branch. Checking out a bare commit deliberately does not, so that
13143
+ * committing from a historical state cannot silently rewrite a branch.
13144
+ */
13145
+ async switchTo(name, opts = {}) {
13146
+ const commit = await this.readRef(name);
13147
+ if (!commit)
13148
+ throw new VcsError(`no branch named "${name}"`);
13149
+ await this.guardWorkingTree(opts.discardChanges);
13150
+ await this.materialise(commit);
13151
+ await this.setHead({ branch: name });
13152
+ }
13153
+ /**
13154
+ * Refuse to overwrite work that was never recorded.
13155
+ *
13156
+ * Moving between commits REPLACES the working tree, so uncommitted edits
13157
+ * are gone — and unlike an ordinary overwrite, the person did not ask for
13158
+ * anything to be written. Naming the files is the point: "you have changes"
13159
+ * with no list leaves them guessing what they are about to lose.
13160
+ */
13161
+ async guardWorkingTree(discard) {
13162
+ if (discard)
13163
+ return;
13164
+ const pending = await this.status();
13165
+ if (!pending.length)
13166
+ return;
13167
+ const names = pending.slice(0, 5).map((c) => c.path).join(", ");
13168
+ const more = pending.length > 5 ? `, and ${pending.length - 5} more` : "";
13169
+ throw new VcsError(`${pending.length} uncommitted change(s) would be lost: ${names}${more}. ` +
13170
+ "Commit them first, or pass { discardChanges: true } to throw them away.");
13171
+ }
13172
+ /** The branch HEAD is on, or null when detached. */
13173
+ async currentBranch() {
13174
+ const head = await this.head();
13175
+ return "detached" in head ? null : head.branch;
13176
+ }
13177
+ // ---- merge --------------------------------------------------------------
13178
+ /**
13179
+ * Merge another branch into the current one.
13180
+ *
13181
+ * Every part of this is client-side by necessity: the server holds
13182
+ * ciphertext and cannot compare versions, so base, ours and theirs are all
13183
+ * fetched and decrypted here.
13184
+ */
13185
+ async merge(name, opts = {}) {
13186
+ const theirs = await this.readRef(name);
13187
+ if (!theirs)
13188
+ throw new VcsError(`no branch named "${name}"`);
13189
+ // A merge writes to the working tree as freely as a switch does.
13190
+ await this.guardWorkingTree(opts.discardChanges);
13191
+ const ours = await this.current();
13192
+ if (!ours)
13193
+ throw new VcsError("nothing to merge into — commit something first");
13194
+ if (ours === theirs)
13195
+ return { kind: "up-to-date", conflicts: [] };
13196
+ const base = await this.mergeBase(ours, theirs);
13197
+ if (base === theirs)
13198
+ return { kind: "up-to-date", conflicts: [] };
13199
+ if (base === ours) {
13200
+ // Fast-forward: our history is contained in theirs, so there is
13201
+ // nothing to combine — just move.
13202
+ await this.materialise(theirs);
13203
+ await this.advance(theirs);
13204
+ return { kind: "fast-forward", commit: theirs, conflicts: [] };
13205
+ }
13206
+ const [baseFiles, ourFiles, theirFiles] = await Promise.all([
13207
+ base ? this.filesOf(base) : new Map(),
13208
+ this.filesOf(ours),
13209
+ this.filesOf(theirs),
13210
+ ]);
13211
+ const conflicts = [];
13212
+ for (const path of new Set([...ourFiles.keys(), ...theirFiles.keys(), ...baseFiles.keys()])) {
13213
+ const b = baseFiles.get(path);
13214
+ const o = ourFiles.get(path);
13215
+ const t = theirFiles.get(path);
13216
+ const conflict = await this.settlePath(path, b, o, t);
13217
+ if (conflict)
13218
+ conflicts.push(conflict);
13219
+ }
13220
+ if (conflicts.length) {
13221
+ // Record what to merge with, so finishing is just: fix the files,
13222
+ // then commit. Losing this would silently produce a commit with one
13223
+ // parent, and the merge would appear never to have happened.
13224
+ await this.deps.store.put(mergeKey(this.slug), await seal(await this.deps.key(), { theirs, name }));
13225
+ return { kind: "conflicted", conflicts };
13226
+ }
13227
+ const commit = await this.commit(`Merge ${name}`, [ours, theirs]);
13228
+ return { kind: "merged", commit, conflicts: [] };
13229
+ }
13230
+ /** Decide one path. Returns a conflict when it cannot be settled. */
13231
+ async settlePath(path, b, o, t) {
13232
+ const id = (f) => f?.manifest.id;
13233
+ if (id(o) === id(t))
13234
+ return null; // already agree
13235
+ if (!o && !b && t)
13236
+ return this.take(path, t); // only they added it
13237
+ if (!t && !b && o)
13238
+ return null; // only we added it
13239
+ if (id(o) === id(b)) { // only they touched it
13240
+ if (!t) {
13241
+ await this.deps.vfs.delete(this.abs(path), { keepContent: true }).catch(() => { });
13242
+ return null;
13243
+ }
13244
+ return this.take(path, t);
13245
+ }
13246
+ if (id(t) === id(b))
13247
+ return null; // only we touched it
13248
+ // Both sides moved. A file one side deleted and the other edited is not
13249
+ // a merge decision a tool should make silently.
13250
+ if (!o || !t)
13251
+ return { path, reason: "modify-delete" };
13252
+ // Definitely-binary types are rejected WITHOUT reading them — merging
13253
+ // bytes line-by-line produces corruption, not a result, and these files
13254
+ // are exactly the large ones.
13255
+ if (BINARY_TYPE.test(o.manifest.type ?? "") || BINARY_TYPE.test(t.manifest.type ?? "")) {
13256
+ return { path, reason: "binary" };
13257
+ }
13258
+ const [baseBytes, ourBytes, theirBytes] = await Promise.all([
13259
+ b ? this.deps.vfs.readByManifest(b.manifest) : Promise.resolve(new Uint8Array()),
13260
+ this.deps.vfs.readByManifest(o.manifest),
13261
+ this.deps.vfs.readByManifest(t.manifest),
13262
+ ]);
13263
+ // Then judge by CONTENT, not by the declared type.
13264
+ //
13265
+ // The type is a hint we do not own: a content store may omit it, and
13266
+ // plenty of real text files carry no extension to derive one from
13267
+ // (Makefile, LICENSE, .gitignore, .env). Trusting it alone turned every
13268
+ // one of those into an unmergeable false conflict.
13269
+ if (![ourBytes, theirBytes, baseBytes].every(looksTextual))
13270
+ return { path, reason: "binary" };
13271
+ const decode = (bytes) => new TextDecoder().decode(bytes);
13272
+ const { text, conflicted } = merge3Text(decode(baseBytes), decode(ourBytes), decode(theirBytes));
13273
+ await this.deps.vfs.writeFile(this.abs(path), text);
13274
+ return conflicted ? { path, reason: "content" } : null;
13275
+ }
13276
+ async take(path, entry) {
13277
+ await this.deps.vfs.writeManifest(this.abs(path), entry.manifest, entry.size);
13278
+ return null;
13279
+ }
13280
+ /**
13281
+ * The most recent commit reachable from both.
13282
+ *
13283
+ * Walks our ancestry into a set, then theirs breadth-first until it hits it —
13284
+ * so the first match is the nearest, which is what a three-way merge needs.
13285
+ */
13286
+ async mergeBase(a, b) {
13287
+ const ancestors = new Set();
13288
+ const collect = async (start) => {
13289
+ const queue = [start];
13290
+ while (queue.length) {
13291
+ const hash = queue.shift();
13292
+ if (ancestors.has(hash))
13293
+ continue;
13294
+ ancestors.add(hash);
13295
+ queue.push(...(await this.get(hash)).parents);
13296
+ }
13297
+ };
13298
+ await collect(a);
13299
+ const queue = [b];
13300
+ const seen = new Set();
13301
+ while (queue.length) {
13302
+ const hash = queue.shift();
13303
+ if (seen.has(hash))
13304
+ continue;
13305
+ seen.add(hash);
13306
+ if (ancestors.has(hash))
13307
+ return hash;
13308
+ queue.push(...(await this.get(hash)).parents);
13309
+ }
13310
+ return null;
13311
+ }
13312
+ /** Every file in a commit, by path. */
13313
+ async filesOf(commitHash) {
13314
+ const commit = await this.get(commitHash);
13315
+ const out = new Map();
13316
+ await this.collect(commit.tree, "", out);
13317
+ return out;
13318
+ }
13319
+ }
13320
+ /**
13321
+ * Version control over the VFS.
13322
+ *
13323
+ * One repository per project, addressed by slug — the same slug the VFS stores
13324
+ * that project's working tree under. `client.vcs.open("my-app")` is the whole
13325
+ * entry point.
13326
+ */
13327
+ class VcsNamespace {
13328
+ deps;
13329
+ repos = new Map();
13330
+ constructor(deps) {
13331
+ this.deps = deps;
13332
+ }
13333
+ /** The repository for one project. Cheap and cached; creates nothing. */
13334
+ open(slug) {
13335
+ let repo = this.repos.get(slug);
13336
+ if (!repo) {
13337
+ repo = new Repo(slug, {
13338
+ vfs: this.deps.vfs,
13339
+ store: this.deps.store,
13340
+ key: async () => {
13341
+ const seed = this.deps.seed();
13342
+ if (!seed)
13343
+ throw new VcsError("client.vcs: no session — sign in first.");
13344
+ return deriveRepoKey(seed, slug);
13345
+ },
13346
+ author: this.deps.author,
13347
+ });
13348
+ this.repos.set(slug, repo);
13349
+ }
13350
+ return repo;
13351
+ }
13352
+ }
13353
+
12436
13354
  /**
12437
13355
  * `ChatKeyVault` — SDK-owned lifecycle for a member's long-lived ratchet/space
12438
13356
  * keypair (the connect `KeyStore` entry keyed by the member id / username).
@@ -13676,6 +14594,8 @@ class Client {
13676
14594
  storage;
13677
14595
  /** Filesystem over the personal space: directories, versions, real paths. */
13678
14596
  vfs;
14597
+ /** Version control over the VFS: commits, branches, merges — per project. */
14598
+ vcs;
13679
14599
  /** Realtime messaging — `client.message.subscribe(...)`, etc. */
13680
14600
  message;
13681
14601
  /** Fan-out group spaces — `client.space.createSpace(...)`, etc. */
@@ -13863,6 +14783,15 @@ class Client {
13863
14783
  },
13864
14784
  },
13865
14785
  });
14786
+ // Sealed into the SAME personal store as the VFS metadata, so a project
14787
+ // and its history live or die together — there is no way to end up with
14788
+ // commits whose working tree is gone, or the reverse.
14789
+ this.vcs = new VcsNamespace({
14790
+ vfs: this.vfs,
14791
+ store: personalStore,
14792
+ seed: () => this.session.seed,
14793
+ author: () => this.session.commitment ?? "unknown",
14794
+ });
13866
14795
  // Group keys are cached in the user's PersonalSpace (encrypted at rest
13867
14796
  // by client.kv), so a returning member hydrates without a fresh keyring
13868
14797
  // round-trip. The server only ever sees ciphertext.
@@ -14801,5 +15730,5 @@ var bundledLoader = /*#__PURE__*/Object.freeze({
14801
15730
  default: loadRsWasm
14802
15731
  });
14803
15732
 
14804
- 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_BASE_URL, DEFAULT_FN_HOST_SUFFIX, DEVICE_IDENTITY_STORAGE_KEY, DEVICE_KEY_DB_NAME, DbCache, DbNamespace, DbTable, 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, 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, PREIMAGE_POK_VERIFICATION_KEY, Packet, PasskeyOriginError, PeerExchange, PeerNetwork, PersonalSpaceClient, Poseidon, PreimagePoK, Ready, ReauthRequiredError, ReedSolomonCodec, Retry, Space as Room, SerializeMessage, SessionState, ShardCache, ShardClient, SharedSpaceClient, SnapshotNamespace, Space, SpaceCache, SpaceKeyring, SpaceNamespace, SpacePacketCipher, SpaceSignaler, StorageNamespace, SyncEngine, VERSION, VaultUnavailableError, WSTransport, WebRtcTransport, WorkerEngineHost, ZERO_HLC, ZkAuth, _accountId, _formatBytes, _messageId, _objectId, _packetId, _socketId, _userId, appLogger$1 as appLogger, assertType, base58Decode, base58Encode, buildCommitment, canonicalUserCode, clearDeviceIdentity, compareHlc, compilePrograms, concatBytes, configureDeviceStore, decodeFromHex, decorators, decryptAesGcm, defaultCircuitUrls, defaultSessionStore, deleteBeatsWrite, deleteRegister, deriveAesKeyFromPassphrase, deriveBitsHkdf, deriveIdentity, deriveIdentityFromSeed, deriveMasterSeedFromPassword, deserialize, deviceFingerprint, deviceIdentityIsEphemeral, deviceIdentityKey, ejectAgentPrompt, ejectAgentTools, emptyMap, emptyRegister, encodeToHex, encryptAesGcm, exportPublicKeyBase64, exportPublicKeyHex, formatUserCode, fromBase64$2 as fromBase64, fromBase64Url, fromHex, generateAuthProof, generateChecksum, generateDevicePairingKeypair, generateId, generatePkce, getIPAddress, getId, getMuhkooAppDescriptor, getSubtle, hasDeviceIdentity, initBn128Wasm, initRsWasm, initializeCircuits, isNewer, isNumber, isOfflineCapable, isP2pCapable, isProvisional, loadDeviceIdentity, log, manifestToStat, materializeRow, mergeMap, mergeMessage, mergeRegister, messageKey, pack, padHandle, pairingVerificationCode, passkeyUsableFromOrigin, persistDeviceIdentity, poseidonHash, protocol, provisionalHandle, quickVerify, randomBytes, randomState, rpIdUsableForOrigin, sealSeed, sealSeedToDevice, serialize, shardHash, signMessage, stableStringify, toBase64$2 as toBase64, toBase64Url, toField, toHex, tombstoneRow, unpack, unsealSeed, unsealSeedFromDevice, unwrapWithPassphrase, utf8Decode, utf8Encode, verify, verifyChecksum, verifyGroth16, verifyHashKnowledge, verifyPreimagePoK, wrapWithPassphrase, writeBeatsTombstone, writeColumns, writeRegister };
15733
+ 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_BASE_URL, DEFAULT_BRANCH, DEFAULT_FN_HOST_SUFFIX, DEVICE_IDENTITY_STORAGE_KEY, DEVICE_KEY_DB_NAME, DbCache, DbNamespace, DbTable, 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, 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, PREIMAGE_POK_VERIFICATION_KEY, Packet, PasskeyOriginError, PeerExchange, PeerNetwork, PersonalSpaceClient, Poseidon, PreimagePoK, Ready, ReauthRequiredError, ReedSolomonCodec, Repo, Retry, Space as Room, SerializeMessage, SessionState, ShardCache, ShardClient, SharedSpaceClient, SnapshotNamespace, Space, SpaceCache, SpaceKeyring, SpaceNamespace, SpacePacketCipher, SpaceSignaler, StorageNamespace, SyncEngine, VERSION, VaultUnavailableError, VcsError, VcsNamespace, VfsLockedError, VfsNamespace, WSTransport, WebRtcTransport, WorkerEngineHost, ZERO_HLC, ZkAuth, _accountId, _formatBytes, _messageId, _objectId, _packetId, _socketId, _userId, appLogger$1 as appLogger, assertType, base58Decode, base58Encode, buildCommitment, canonicalUserCode, clearDeviceIdentity, compareHlc, compilePrograms, concatBytes, configureDeviceStore, decodeFromHex, decorators, decryptAesGcm, defaultCircuitUrls, defaultSessionStore, deleteBeatsWrite, deleteRegister, deriveAesKeyFromPassphrase, deriveBitsHkdf, deriveIdentity, deriveIdentityFromSeed, deriveMasterSeedFromPassword, deserialize, deviceFingerprint, deviceIdentityIsEphemeral, deviceIdentityKey, ejectAgentPrompt, ejectAgentTools, emptyMap, emptyRegister, encodeToHex, encryptAesGcm, exportPublicKeyBase64, exportPublicKeyHex, formatUserCode, fromBase64$2 as fromBase64, fromBase64Url, fromHex, generateAuthProof, generateChecksum, generateDevicePairingKeypair, generateId, generatePkce, getIPAddress, getId, getMuhkooAppDescriptor, getSubtle, hasDeviceIdentity, initBn128Wasm, initRsWasm, initializeCircuits, isNewer, isNumber, isOfflineCapable, isP2pCapable, isProvisional, loadDeviceIdentity, log, manifestToStat, materializeRow, merge3, merge3Text, mergeMap, mergeMessage, mergeRegister, messageKey, pack, padHandle, pairingVerificationCode, passkeyUsableFromOrigin, persistDeviceIdentity, poseidonHash, protocol, provisionalHandle, quickVerify, randomBytes, randomState, rpIdUsableForOrigin, sealSeed, sealSeedToDevice, serialize, shardHash, signMessage, stableStringify, toBase64$2 as toBase64, toBase64Url, toField, toHex, tombstoneRow, unpack, unsealSeed, unsealSeedFromDevice, unwrapWithPassphrase, utf8Decode, utf8Encode, verify, verifyChecksum, verifyGroth16, verifyHashKnowledge, verifyPreimagePoK, wrapWithPassphrase, writeBeatsTombstone, writeColumns, writeRegister };
14805
15734
  //# sourceMappingURL=index.js.map