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