agentic-drive 0.7.0 → 0.9.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.
@@ -1,9 +1,34 @@
1
1
  // src/drive.ts
2
- import { EraseRefusedError, MAX_FOLDER_DEPTH, PRINCIPAL_KINDS, SUBJECT_KINDS } from "agentic-identity";
2
+ import { EraseRefusedError, StoreConflictError, MAX_FOLDER_DEPTH, PRINCIPAL_KINDS, ROLES, SUBJECT_KINDS } from "agentic-identity";
3
3
  var ROOT_ID = "root";
4
4
  var NODES = "drive_nodes";
5
5
  var NAMES = "drive_names";
6
6
  var ACTOR_KINDS = PRINCIPAL_KINDS;
7
+ var DECIDED = /* @__PURE__ */ Symbol("decided");
8
+ var ERROR_STATUS = Object.freeze({
9
+ CROSS_DRIVE: 409,
10
+ INVALID_SHARE: 400,
11
+ TOO_MANY_ITEMS: 409,
12
+ TOO_DEEP: 400,
13
+ INVALID_EXPIRY: 400,
14
+ BUSY: 409,
15
+ BLOB_EXISTS: 500,
16
+ NOT_FOUND: 404,
17
+ NAME_TAKEN: 409,
18
+ INVALID_NAME: 400,
19
+ INVALID_PATH: 400,
20
+ INVALID_REF: 400,
21
+ NOT_A_FOLDER: 400,
22
+ NOT_A_FILE: 400,
23
+ MOVE_INTO_SELF: 409,
24
+ PARENT_GONE: 409,
25
+ TRASHED: 409,
26
+ NOT_TRASHED: 409,
27
+ IS_ROOT: 400,
28
+ FORBIDDEN: 403,
29
+ BLOB_MISSING: 500,
30
+ TOO_LARGE: 413
31
+ });
7
32
  var DriveError = class extends Error {
8
33
  constructor(code, message) {
9
34
  super(message);
@@ -59,6 +84,20 @@ function splitPath(path) {
59
84
  if (parts.length > MAX_PATH_SEGMENTS) throw new DriveError("TOO_DEEP", `a path has at most ${MAX_PATH_SEGMENTS} segments`);
60
85
  return parts.map(parseName);
61
86
  }
87
+ var Ask = class extends Error {
88
+ constructor(key, ctx, action, node) {
89
+ super("access answer needed");
90
+ this.key = key;
91
+ this.ctx = ctx;
92
+ this.action = action;
93
+ this.node = node;
94
+ }
95
+ key;
96
+ ctx;
97
+ action;
98
+ node;
99
+ };
100
+ var MAX_ASKS = 100;
62
101
  function batch() {
63
102
  const writes = [];
64
103
  const fresh = /* @__PURE__ */ new Set();
@@ -101,7 +140,24 @@ function createDrive(deps) {
101
140
  const onOrphan = deps.onOrphan ?? ((key, error) => console.error(`orphan blob ${key}: ${error instanceof Error ? error.name : "delete failed"}`));
102
141
  const sharing = deps.sharing;
103
142
  const resourceOf = async (node) => node.id === ROOT_ID ? rootResource(node.owner) : node.id;
104
- const can = deps.can ?? (sharing ? async (ctx, action, node) => ctx.principal !== void 0 && (await sharing.can(ctx.principal, ACTION_TO_IDENTITY[action], await resourceOf(node))).allowed : (ctx, _action, node) => node.owner === ctx.owner);
143
+ const askCan = deps.can ?? (sharing ? async (ctx, action, node) => ctx.principal !== void 0 && (await sharing.can(ctx.principal, ACTION_TO_IDENTITY[action], await resourceOf(node))).allowed : (ctx, _action, node) => node.owner === ctx.owner);
144
+ const can = async (ctx, action, node) => {
145
+ const held = ctx[DECIDED];
146
+ if (!held) return askCan(ctx, action, node);
147
+ const chain = [];
148
+ if (sharing) {
149
+ for (let id = await resourceOf(node), i = 0; id && i <= MAX_ITEM_DEPTH + 1; i++) {
150
+ const doc = await held.tx.get("resources", id);
151
+ chain.push(doc?.parentId ?? null);
152
+ id = typeof doc?.parentId === "string" ? doc.parentId : void 0;
153
+ }
154
+ }
155
+ const key = JSON.stringify([action, node.id, node.owner, node.parentId, node.updatedAt, node.trashed, chain, ctx.owner, ctx.actor, ctx.principal ?? null]);
156
+ const known = held.answers.get(key);
157
+ if (known !== void 0) return known;
158
+ const { [DECIDED]: _held, ...plain } = ctx;
159
+ throw new Ask(key, plain, action, node);
160
+ };
105
161
  const rootNode = (owner, by) => ({
106
162
  id: ROOT_ID,
107
163
  owner,
@@ -397,10 +453,30 @@ function createDrive(deps) {
397
453
  try {
398
454
  return await store.transaction(fn);
399
455
  } catch (error) {
400
- if (error instanceof Error && /transaction kept conflicting/.test(error.message)) throw new DriveError("BUSY", "the drive changed at the same time. Try again.");
456
+ if (error instanceof StoreConflictError || error instanceof Error && /transaction kept conflicting/.test(error.message)) throw new DriveError("BUSY", "the drive changed at the same time. Try again.");
401
457
  throw error;
402
458
  }
403
459
  }
460
+ async function transactAs(ctx, fn) {
461
+ const answers = /* @__PURE__ */ new Map();
462
+ let runs = 0;
463
+ let asked = false;
464
+ for (; ; ) {
465
+ try {
466
+ return await transact((tx) => {
467
+ if (++runs > 2 * MAX_ASKS) throw new DriveError("BUSY", "the drive changed at the same time. Try again.");
468
+ if (runs > 1 && !asked) answers.clear();
469
+ asked = false;
470
+ return fn(tx, { ...ctx, [DECIDED]: { answers, tx } });
471
+ });
472
+ } catch (error) {
473
+ if (!(error instanceof Ask)) throw error;
474
+ asked = true;
475
+ if (answers.size >= MAX_ASKS) throw new DriveError("BUSY", "the drive needed too many access checks. Try again.");
476
+ answers.set(error.key, await askCan(error.ctx, error.action, error.node));
477
+ }
478
+ }
479
+ }
404
480
  function assertInPerson(ctx, what) {
405
481
  if (!isInPerson(ctx) || ctx.actor.kind !== "human") throw new DriveError("FORBIDDEN", `only the owner in person can ${what}`);
406
482
  }
@@ -550,15 +626,15 @@ function createDrive(deps) {
550
626
  return (await children(store, folder.owner, folder.id)).filter((n) => !n.trashed && n.purging !== true).sort((a, b) => a.nameKey < b.nameKey ? -1 : a.nameKey > b.nameKey ? 1 : a.name < b.name ? -1 : 1);
551
627
  },
552
628
  async mkdir(ctx, path, opts = {}) {
553
- return transact(async (tx) => {
629
+ return transactAs(ctx, async (tx, ctx2) => {
554
630
  const w = batch();
555
- const { parent, name } = await parentAndName(tx, w, ctx, path, opts.parents ?? false);
631
+ const { parent, name } = await parentAndName(tx, w, ctx2, path, opts.parents ?? false);
556
632
  const existing = w.fresh.has(parent.id) ? null : await liveChild(tx, parent, parseName(name).key);
557
633
  if (opts.parents && existing?.kind === "folder") {
558
- if (!await can(ctx, "read", existing)) throw new DriveError("NOT_FOUND", `no item ${path}`);
634
+ if (!await can(ctx2, "read", existing)) throw new DriveError("NOT_FOUND", `no item ${path}`);
559
635
  return existing;
560
636
  }
561
- const { node } = await insert(tx, w, ctx, parent, name, { kind: "folder" }, "fail");
637
+ const { node } = await insert(tx, w, ctx2, parent, name, { kind: "folder" }, "fail");
562
638
  await w.apply(tx);
563
639
  return node;
564
640
  });
@@ -575,10 +651,10 @@ function createDrive(deps) {
575
651
  if (!stored) throw new DriveError("BLOB_EXISTS", "a blob with this key already exists");
576
652
  let committed;
577
653
  try {
578
- committed = await transact(async (tx) => {
654
+ committed = await transactAs(ctx, async (tx, ctx2) => {
579
655
  const w = batch();
580
- const { parent, name } = await parentAndName(tx, w, ctx, path, opts.parents ?? false);
581
- const result = await insert(tx, w, ctx, parent, name, { kind: "file", size: data.byteLength, mime, hash, blobKey }, opts.onConflict ?? "fail");
656
+ const { parent, name } = await parentAndName(tx, w, ctx2, path, opts.parents ?? false);
657
+ const result = await insert(tx, w, ctx2, parent, name, { kind: "file", size: data.byteLength, mime, hash, blobKey }, opts.onConflict ?? "fail");
582
658
  await w.apply(tx);
583
659
  return result;
584
660
  });
@@ -592,10 +668,10 @@ function createDrive(deps) {
592
668
  /** A reference points at a document in another app, such as `docs:<id>`. */
593
669
  async putRef(ctx, path, ref, opts = {}) {
594
670
  if (!REF.test(ref)) throw new DriveError("INVALID_REF", `a reference looks like "docs:<id>", got ${JSON.stringify(ref)}`);
595
- return transact(async (tx) => {
671
+ return transactAs(ctx, async (tx, ctx2) => {
596
672
  const w = batch();
597
- const { parent, name } = await parentAndName(tx, w, ctx, path, opts.parents ?? false);
598
- const { node } = await insert(tx, w, ctx, parent, name, { kind: "ref", ref }, opts.onConflict === "replace" ? "fail" : opts.onConflict ?? "fail");
673
+ const { parent, name } = await parentAndName(tx, w, ctx2, path, opts.parents ?? false);
674
+ const { node } = await insert(tx, w, ctx2, parent, name, { kind: "ref", ref }, opts.onConflict === "replace" ? "fail" : opts.onConflict ?? "fail");
599
675
  await w.apply(tx);
600
676
  return node;
601
677
  });
@@ -629,35 +705,35 @@ function createDrive(deps) {
629
705
  async move(ctx, src, destLoc, opts = {}) {
630
706
  const probe = sharing ? await resolve(store, ctx, src) : null;
631
707
  const height = probe ? await heightFor(ctx, probe, destLoc) : 0;
632
- return transact(async (tx) => {
708
+ return transactAs(ctx, async (tx, ctx2) => {
633
709
  const w = batch();
634
- const moved = await relocate(tx, w, ctx, await resolve(tx, ctx, src), destLoc, { ...opts, height });
710
+ const moved = await relocate(tx, w, ctx2, await resolve(tx, ctx2, src), destLoc, { ...opts, height });
635
711
  await w.apply(tx);
636
712
  return moved;
637
713
  });
638
714
  },
639
715
  async rename(ctx, loc, name) {
640
- return transact(async (tx) => {
716
+ return transactAs(ctx, async (tx, ctx2) => {
641
717
  const w = batch();
642
- const node = await resolve(tx, ctx, loc);
718
+ const node = await resolve(tx, ctx2, loc);
643
719
  if (node.id === ROOT_ID) throw new DriveError("IS_ROOT", "cannot change the root");
644
- const parent = await rawNode(tx, node.parentId, node.owner, ctx.actor);
720
+ const parent = await rawNode(tx, node.parentId, node.owner, ctx2.actor);
645
721
  if (!parent) throw new DriveError("NOT_FOUND", `the folder of ${node.name} is missing`);
646
- const renamed = await relocate(tx, w, ctx, node, parent, { name });
722
+ const renamed = await relocate(tx, w, ctx2, node, parent, { name });
647
723
  await w.apply(tx);
648
724
  return renamed;
649
725
  });
650
726
  },
651
727
  /** Hides an item and, for a folder, everything under it. Nothing else changes. */
652
728
  async trash(ctx, loc) {
653
- return transact(async (tx) => {
654
- const node = await resolve(tx, ctx, loc);
729
+ return transactAs(ctx, async (tx, ctx2) => {
730
+ const node = await resolve(tx, ctx2, loc);
655
731
  if (node.id === ROOT_ID) throw new DriveError("IS_ROOT", "cannot trash the root");
656
- await need(ctx, "write", node);
732
+ await need(ctx2, "write", node);
657
733
  const claim = await claimId(node.owner, node.parentId, node.nameKey);
658
734
  const held = await tx.get(NAMES, claim);
659
735
  const t = now();
660
- const trashed = { ...node, trashed: true, trashedAt: t, updatedAt: t, ...stamp(ctx, node) };
736
+ const trashed = { ...node, trashed: true, trashedAt: t, updatedAt: t, ...stamp(ctx2, node) };
661
737
  await tx.put(NODES, node.id, trashed);
662
738
  if (held?.nodeId === node.id) await tx.delete(NAMES, claim);
663
739
  return trashed;
@@ -671,6 +747,15 @@ function createDrive(deps) {
671
747
  for (const { data } of page.items) if (data.purging !== true) out.push(data);
672
748
  return out.sort((a, b) => (b.trashedAt ?? 0) - (a.trashedAt ?? 0));
673
749
  },
750
+ /** Everything that is in the trash, newest first, each with the path it had before. It reads the whole drive, so a big drive costs more than `listTrash`. */
751
+ async listTrashWithPaths(ctx) {
752
+ const all = await ownerNodes(ctx);
753
+ const byId = new Map(all.map((n) => [n.id, n]));
754
+ return all.filter((n) => n.trashed).sort((a, b) => (b.trashedAt ?? 0) - (a.trashedAt ?? 0)).flatMap((node) => {
755
+ const path = pathIn(byId, node, false);
756
+ return path === null ? [] : [{ node, path }];
757
+ });
758
+ },
674
759
  /** Puts a trashed item back. It needs a live parent folder. */
675
760
  async restore(ctx, id, opts = {}) {
676
761
  let height = 0;
@@ -679,20 +764,20 @@ function createDrive(deps) {
679
764
  const target = await rawNode(store, opts.parentId, ctx.owner, ctx.actor);
680
765
  if (probe?.trashed && target && await can(ctx, "read", target)) height = await heightFor(ctx, probe, target);
681
766
  }
682
- return transact(async (tx) => {
683
- const node = await fetch(tx, ctx, id);
767
+ return transactAs(ctx, async (tx, ctx2) => {
768
+ const node = await fetch(tx, ctx2, id);
684
769
  if (!node || node.id === ROOT_ID) throw new DriveError("NOT_FOUND", `no item ${id}`);
685
770
  if (!node.trashed) throw new DriveError("NOT_TRASHED", `${node.name} is not in the trash`);
686
- await need(ctx, "write", node);
687
- const parent = opts.parentId === void 0 ? await rawNode(tx, node.parentId, node.owner, ctx.actor) : await rawNode(tx, opts.parentId, ctx.owner, ctx.actor);
688
- const visible = parent !== null && (opts.parentId === void 0 || await can(ctx, "read", parent));
689
- if (!parent || !visible || parent.kind !== "folder" || !await isLive(tx, ctx, parent)) {
771
+ await need(ctx2, "write", node);
772
+ const parent = opts.parentId === void 0 ? await rawNode(tx, node.parentId, node.owner, ctx2.actor) : await rawNode(tx, opts.parentId, ctx2.owner, ctx2.actor);
773
+ const visible = parent !== null && (opts.parentId === void 0 || await can(ctx2, "read", parent));
774
+ if (!parent || !visible || parent.kind !== "folder" || !await isLive(tx, ctx2, parent)) {
690
775
  throw new DriveError("PARENT_GONE", `the folder of ${node.name} is gone or in the trash`);
691
776
  }
692
777
  if (parent.owner !== node.owner) throw new DriveError("CROSS_DRIVE", "an item cannot move into another person's drive");
693
778
  if (parent.id !== node.parentId) {
694
- await need(ctx, "write", parent);
695
- await noClimb(ctx, node, parent);
779
+ await need(ctx2, "write", parent);
780
+ await noClimb(ctx2, node, parent);
696
781
  await fitsUnder(tx, batch(), parent, height);
697
782
  }
698
783
  let { name, key } = parseName(node.name);
@@ -701,7 +786,7 @@ function createDrive(deps) {
701
786
  name = await freeName(tx, parent.owner, parent.id, name);
702
787
  key = parseName(name).key;
703
788
  }
704
- const restored = { ...node, parentId: parent.id, name, nameKey: key, trashed: false, trashedAt: null, updatedAt: now(), ...stamp(ctx, node) };
789
+ const restored = { ...node, parentId: parent.id, name, nameKey: key, trashed: false, trashedAt: null, updatedAt: now(), ...stamp(ctx2, node) };
705
790
  await tx.put(NODES, restored.id, restored);
706
791
  await tx.put(NAMES, await claimId(parent.owner, parent.id, key), { nodeId: restored.id, owner: parent.owner, parentId: parent.id });
707
792
  if (sharing && parent.id !== node.parentId) await tx.put("resources", restored.id, await resourceDoc(restored, parent));
@@ -803,6 +888,58 @@ function createDrive(deps) {
803
888
  }
804
889
  return api.putRef(ctx, dir.endsWith("/") ? `${dir}${input.name}` : `${dir}/${input.name}`, input.ref, { parents: true, onConflict: "rename" });
805
890
  },
891
+ /** The live references to `ref` in the caller's own drive that the caller can read, with their paths, sorted by path. */
892
+ async findByRef(ctx, ref) {
893
+ if (!REF.test(ref)) throw new DriveError("INVALID_REF", `a reference looks like "docs:<id>", got ${JSON.stringify(ref)}`);
894
+ await need(ctx, "read", rootNode(ctx.owner, ctx.actor));
895
+ const hits = [];
896
+ const seen = /* @__PURE__ */ new Map();
897
+ const folder = async (id) => {
898
+ if (!seen.has(id)) seen.set(id, await rawNode(store, id, ctx.owner, ctx.actor));
899
+ return seen.get(id) ?? null;
900
+ };
901
+ for (const { data } of await allWhere(store, NODES, { owner: ctx.owner, ref })) {
902
+ const node = data;
903
+ if (node.kind !== "ref" || node.trashed || node.purging === true) continue;
904
+ const names = [node.name];
905
+ let up = node;
906
+ for (let depth = 0; up && up.parentId !== ROOT_ID && depth < maxDepth; depth++) {
907
+ up = await folder(up.parentId);
908
+ if (up === null || up.trashed) break;
909
+ names.push(up.name);
910
+ }
911
+ if (up === null || up.trashed || up.parentId !== ROOT_ID) continue;
912
+ if (await can(ctx, "read", node)) hits.push({ node, path: `/${names.reverse().join("/")}` });
913
+ }
914
+ return hits.sort((a, b) => a.path < b.path ? -1 : 1);
915
+ },
916
+ /**
917
+ * Puts a reference at `path` and gives `claim.subject` the role `claim.role` on it, as one step. If the
918
+ * grant fails (the caller is not an owner of the item, or the subject is invalid), the reference is
919
+ * deleted again and the error is thrown, so no half-made item stays. Needs `sharing`. Folders that
920
+ * `parents` made stay, `onConflict: "replace"` acts as `"fail"` (a rollback could not bring back a replaced item), and an invalid role or subject is refused before anything is written.
921
+ */
922
+ async claimRef(ctx, path, ref, claim, opts = {}) {
923
+ if (!sharing) throw new DriveError("INVALID_SHARE", "sharing is not set up");
924
+ if (!ctx.principal) throw new DriveError("FORBIDDEN", "sign in to share");
925
+ const subject = parseSubject(claim.subject);
926
+ if (!ROLES.includes(claim.role)) throw new DriveError("INVALID_SHARE", `a role is one of ${ROLES.join(", ")}`);
927
+ const node = await api.putRef(ctx, path, ref, opts);
928
+ try {
929
+ await viaSharing(ctx, (s, p) => s.grant(p, node.id, subject, claim.role));
930
+ } catch (error) {
931
+ let removed = 0;
932
+ let gone = false;
933
+ for (let i = 0; i < 3 && removed === 0 && !gone; i++) {
934
+ const current = await rawNode(store, node.id, node.owner, ctx.actor, true);
935
+ if (current === null) gone = true;
936
+ else removed = await removeNodes([current], void 0, ctx.principal).catch(() => 0);
937
+ }
938
+ if (removed === 0 && !gone) console.error(`claimRef could not remove ${node.id} after a failed share. Purge it.`);
939
+ throw error;
940
+ }
941
+ return node;
942
+ },
806
943
  /** An app deleted a document. Its references in this drive go to the trash. Returns how many. */
807
944
  async forgetDocument(ctx, ref) {
808
945
  if (!REF.test(ref)) throw new DriveError("INVALID_REF", `a reference looks like "docs:<id>", got ${JSON.stringify(ref)}`);
@@ -992,167 +1129,6 @@ function createDrive(deps) {
992
1129
  return Object.fromEntries(Object.entries(api).map(([name, call]) => [name, open.has(name) ? call : guarded(call)]));
993
1130
  }
994
1131
 
995
- // src/adapters/fs.ts
996
- import { createHash } from "crypto";
997
- import { appendFileSync, closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, truncateSync, writeFileSync, writeSync } from "fs";
998
- import { join } from "path";
999
- import { assertBlobPutOptions, assertResponseHeaders, createMemoryStore, parseRegion } from "agentic-identity";
1000
- function createFileStore(dir, options = {}) {
1001
- mkdirSync(dir, { recursive: true });
1002
- const file = join(dir, "store.jsonl");
1003
- const inner = createMemoryStore(options);
1004
- const ready = (async () => {
1005
- if (!existsSync(file)) return;
1006
- const text = readFileSync(file, "utf8");
1007
- const lines = text.split("\n");
1008
- const tail = lines.pop();
1009
- const apply = async (changes2) => {
1010
- for (const [collection, id, doc] of changes2) await (doc ? inner.put(collection, id, doc) : inner.delete(collection, id));
1011
- };
1012
- for (const [i, line] of lines.entries()) {
1013
- if (line === "") continue;
1014
- try {
1015
- await apply(JSON.parse(line));
1016
- } catch {
1017
- throw new Error(`the journal ${file} has a bad line ${i + 1}`);
1018
- }
1019
- }
1020
- if (tail === "") return;
1021
- let changes;
1022
- try {
1023
- changes = JSON.parse(tail);
1024
- } catch {
1025
- truncateSync(file, Buffer.byteLength(text) - Buffer.byteLength(tail));
1026
- return;
1027
- }
1028
- try {
1029
- await apply(changes);
1030
- } catch {
1031
- throw new Error(`the journal ${file} has a bad last line`);
1032
- }
1033
- appendFileSync(file, "\n");
1034
- })();
1035
- let failed;
1036
- const keep = (changes) => {
1037
- if (changes.length === 0) return;
1038
- try {
1039
- const fd = openSync(file, "a");
1040
- try {
1041
- const line = Buffer.from(`${JSON.stringify(changes)}
1042
- `);
1043
- for (let done = 0; done < line.length; ) {
1044
- const wrote = writeSync(fd, line, done);
1045
- if (wrote <= 0) throw new Error("the journal write stopped short");
1046
- done += wrote;
1047
- }
1048
- fsyncSync(fd);
1049
- } finally {
1050
- closeSync(fd);
1051
- }
1052
- } catch (error) {
1053
- failed = error;
1054
- throw error;
1055
- }
1056
- };
1057
- const open = async () => {
1058
- await ready;
1059
- if (failed !== void 0) throw new Error("the journal could not be written, so the store is closed. Open it again.");
1060
- };
1061
- const recording = (ops, changes) => ({
1062
- ...ops,
1063
- put: async (collection, id, doc) => {
1064
- await ops.put(collection, id, doc);
1065
- changes.push([collection, id, doc]);
1066
- },
1067
- delete: async (collection, id) => {
1068
- await ops.delete(collection, id);
1069
- changes.push([collection, id, null]);
1070
- }
1071
- });
1072
- return {
1073
- region: inner.region,
1074
- get: async (c, id) => (await open(), inner.get(c, id)),
1075
- query: async (c, q) => (await open(), inner.query(c, q)),
1076
- async put(collection, id, doc) {
1077
- await open();
1078
- await inner.put(collection, id, doc);
1079
- keep([[collection, id, doc]]);
1080
- },
1081
- async delete(collection, id) {
1082
- await open();
1083
- await inner.delete(collection, id);
1084
- keep([[collection, id, null]]);
1085
- },
1086
- async transaction(fn) {
1087
- await open();
1088
- let changes = [];
1089
- const result = await inner.transaction((tx) => {
1090
- changes = [];
1091
- return fn(recording(tx, changes));
1092
- });
1093
- keep(changes);
1094
- return result;
1095
- }
1096
- };
1097
- }
1098
- var versionOf = (data) => createHash("sha256").update(data).digest("hex");
1099
- function createFileBlobs(dir, options = {}) {
1100
- mkdirSync(dir, { recursive: true });
1101
- const path = (key) => join(dir, createHash("sha256").update(key).digest("hex"));
1102
- const need = (key) => {
1103
- if (key === "") throw new Error("blob key must not be empty");
1104
- };
1105
- const now = options.now ?? Date.now;
1106
- return {
1107
- region: options.region === void 0 ? "unknown" : parseRegion(options.region),
1108
- async put(key, data, opts) {
1109
- need(key);
1110
- assertBlobPutOptions(opts);
1111
- const current = existsSync(path(key)) ? versionOf(readFileSync(path(key))) : void 0;
1112
- if (opts?.ifNoneMatch === "*" && current !== void 0 || opts?.ifMatch !== void 0 && current !== opts.ifMatch) return null;
1113
- const tmp = `${path(key)}.tmp`;
1114
- writeFileSync(tmp, data);
1115
- if (opts?.contentType) writeFileSync(`${path(key)}.type`, opts.contentType);
1116
- else rmSync(`${path(key)}.type`, { force: true });
1117
- renameSync(tmp, path(key));
1118
- return { version: versionOf(data) };
1119
- },
1120
- async get(key) {
1121
- need(key);
1122
- if (!existsSync(path(key))) return null;
1123
- const type = existsSync(`${path(key)}.type`) ? readFileSync(`${path(key)}.type`, "utf8") : void 0;
1124
- const data = new Uint8Array(readFileSync(path(key)));
1125
- return { data, version: versionOf(data), ...type ? { contentType: type } : {} };
1126
- },
1127
- async head(key) {
1128
- need(key);
1129
- if (!existsSync(path(key))) return null;
1130
- const type = existsSync(`${path(key)}.type`) ? readFileSync(`${path(key)}.type`, "utf8") : void 0;
1131
- const data = readFileSync(path(key));
1132
- return { size: data.length, version: versionOf(data), ...type ? { contentType: type } : {} };
1133
- },
1134
- async delete(key) {
1135
- need(key);
1136
- rmSync(path(key), { force: true });
1137
- rmSync(`${path(key)}.type`, { force: true });
1138
- },
1139
- /** A local path with its expiry. Nothing serves or checks it: it exists so the port is whole. */
1140
- async signedUrl(key, opts) {
1141
- need(key);
1142
- const { method, expiresInSeconds } = opts;
1143
- assertResponseHeaders(opts);
1144
- if (!Number.isInteger(expiresInSeconds) || expiresInSeconds < 1 || expiresInSeconds > 604800) {
1145
- throw new RangeError("expiresInSeconds must be an integer from 1 to 604800");
1146
- }
1147
- const extra = [
1148
- opts.responseContentDisposition === void 0 ? "" : `&disposition=${encodeURIComponent(opts.responseContentDisposition)}`,
1149
- opts.responseContentType === void 0 ? "" : `&type=${encodeURIComponent(opts.responseContentType)}`
1150
- ].join("");
1151
- return `file://${path(key)}?method=${method}&expires=${now() + expiresInSeconds * 1e3}${extra}`;
1152
- }
1153
- };
1154
- }
1155
-
1156
1132
  // src/upload.ts
1157
1133
  var DEFAULT_MAX_BYTES = 25 * 1024 * 1024;
1158
1134
  var SIGNATURES = [
@@ -1419,12 +1395,18 @@ function backfillResources(store, options = {}) {
1419
1395
  }
1420
1396
 
1421
1397
  // src/local.ts
1422
- import { createHash as createHash2 } from "crypto";
1423
- import { join as join2 } from "path";
1398
+ import { createHash } from "crypto";
1399
+ import { existsSync } from "fs";
1400
+ import { join } from "path";
1424
1401
  import { createSharing, isMachineMade, isScope, issueCredential, revokeCredential } from "agentic-identity";
1425
- function openLocal(dir) {
1426
- const store = createFileStore(join2(dir, "db"));
1427
- return { store, drive: createDrive({ store, blobs: createFileBlobs(join2(dir, "blobs")), sharing: createSharing(store) }) };
1402
+ import { createFileBlobs, createFileStore } from "agentic-identity/file";
1403
+ function openLocal(dir, opts = {}) {
1404
+ if (!opts.beforeImport && existsSync(join(dir, "db", "store.jsonl")) && !existsSync(join(dir, "store.json"))) {
1405
+ throw new Error(`${dir} holds data from an older version. Run importLegacyLocal(dir, store, blobs) first: open it with openLocal(dir, { beforeImport: true }).`);
1406
+ }
1407
+ const store = createFileStore({ path: join(dir, "store.json") });
1408
+ const blobs = createFileBlobs({ dir: join(dir, "blobs") });
1409
+ return { store, blobs, drive: createDrive({ store, blobs, sharing: createSharing(store) }) };
1428
1410
  }
1429
1411
  async function ensureSharing(store) {
1430
1412
  if (await store.get("drive_meta", "sharing")) return;
@@ -1475,7 +1457,7 @@ async function issueKey(store, principalId, label) {
1475
1457
  return issueCredential(store, { principalId, kind: "api_key", ...label ? { label } : {} });
1476
1458
  }
1477
1459
  async function revokeKey(store, token) {
1478
- if (!await store.get("credentials", createHash2("sha256").update(token).digest("hex"))) throw new DriveError("NOT_FOUND", "no such key");
1460
+ if (!await store.get("credentials", createHash("sha256").update(token).digest("hex"))) throw new DriveError("NOT_FOUND", "no such key");
1479
1461
  await revokeCredential(store, token, Date.now());
1480
1462
  }
1481
1463
  function publicNode(node, path) {
@@ -1487,6 +1469,7 @@ function publicNode(node, path) {
1487
1469
  export {
1488
1470
  ROOT_ID,
1489
1471
  ACTOR_KINDS,
1472
+ ERROR_STATUS,
1490
1473
  DriveError,
1491
1474
  DEFAULT_URL_SECONDS,
1492
1475
  safeMime,
@@ -1494,8 +1477,6 @@ export {
1494
1477
  subjectKindOf,
1495
1478
  parseSubject,
1496
1479
  createDrive,
1497
- createFileStore,
1498
- createFileBlobs,
1499
1480
  DEFAULT_MAX_BYTES,
1500
1481
  sniffMime,
1501
1482
  upload,
package/dist/cli.js CHANGED
@@ -1,7 +1,10 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ importLegacyLocal
4
+ } from "./chunk-AB2RTTHJ.js";
2
5
  import {
3
6
  VERSION
4
- } from "./chunk-EFO5PSJL.js";
7
+ } from "./chunk-IMWV5QEH.js";
5
8
  import {
6
9
  AuthError,
7
10
  DriveError,
@@ -17,7 +20,7 @@ import {
17
20
  revokeKey,
18
21
  revokePrincipal,
19
22
  upload
20
- } from "./chunk-PJQ3CRVL.js";
23
+ } from "./chunk-R2DUVOJT.js";
21
24
 
22
25
  // src/cli.ts
23
26
  import { createReadStream, realpathSync, writeFileSync } from "fs";
@@ -38,13 +41,15 @@ Commands:
38
41
  download <remote> [out] Save a file. Use "-" for stdout.
39
42
  mv <src> <folder> Move an item. Use --name to rename it.
40
43
  rm <path|id> Move an item to the trash.
41
- trash List the trash.
44
+ trash [--paths] List the trash. With --paths, each item shows the path it had.
42
45
  restore <id> Put a trashed item back.
43
46
  purge <id> Delete a trashed item for good.
44
47
  search Find items. Flags: --name --mime --kind --min-size --max-size --created-by-kind --limit
45
48
  export <out.zip> Write everything you own to a zip with a JSON manifest.
46
49
  home [--app <name>] List files and app documents, newest change first.
47
50
  register <ref> <name> Put an app document in the drive. Flag: --parent <folder>
51
+ find-ref <ref> List the live items that point at an app document, with paths.
52
+ claim-ref <ref> <path> <subject> Put a reference and give a role on it, or do neither. Flags: --role <r> --subject-kind <human|agent>
48
53
  forget <ref> Move the references to an app document to the trash.
49
54
  shares <path|id> Show who can reach an item, and by which links and invites. Owner only.
50
55
  share <path|id> <subject> Give a role to a person or agent. Flags: --role <r> --subject-kind <human|agent>
@@ -73,7 +78,7 @@ Flags:
73
78
 
74
79
  Exit codes: 0 done, 1 error, 2 bad usage, 3 no principal or an invalid key.
75
80
  `;
76
- var COMMANDS = ["ls", "stat", "mkdir", "upload", "download", "mv", "rm", "trash", "restore", "purge", "search", "export", "home", "register", "forget", "shares", "share", "unshare", "invite", "accept-invite", "link-create", "link-revoke", "delete-all", "mcp", "principal", "key"];
81
+ var COMMANDS = ["ls", "stat", "mkdir", "upload", "download", "mv", "rm", "trash", "restore", "purge", "search", "export", "home", "register", "find-ref", "claim-ref", "forget", "shares", "share", "unshare", "invite", "accept-invite", "link-create", "link-revoke", "delete-all", "mcp", "principal", "key"];
77
82
  var UsageError = class extends Error {
78
83
  };
79
84
  var options = {
@@ -83,6 +88,7 @@ var options = {
83
88
  scope: { type: "string", multiple: true },
84
89
  label: { type: "string" },
85
90
  "on-conflict": { type: "string" },
91
+ paths: { type: "boolean" },
86
92
  parents: { type: "boolean", short: "p" },
87
93
  mime: { type: "string" },
88
94
  "max-bytes": { type: "string" },
@@ -136,7 +142,8 @@ async function runCli(argv, io) {
136
142
  const admin = command === "principal" || command === "key";
137
143
  const token = values.token ?? env.AGENTIC_DRIVE_TOKEN;
138
144
  if (!admin && !token) throw new AuthError(NO_TOKEN);
139
- const { drive, store } = openLocal(dir);
145
+ const { drive, store, blobs } = openLocal(dir, { beforeImport: true });
146
+ await importLegacyLocal(dir, store, blobs);
140
147
  if (admin || command === "key") {
141
148
  const [sub, target] = args;
142
149
  const out = (value) => io.out(JSON.stringify(value));
@@ -195,7 +202,7 @@ async function runCli(argv, io) {
195
202
  print(publicNode(await drive.trash(ctx, args[0])));
196
203
  break;
197
204
  case "trash":
198
- print((await drive.listTrash(ctx)).map((n) => publicNode(n)));
205
+ print(values.paths ? (await drive.listTrashWithPaths(ctx)).map((r) => publicNode(r.node, r.path)) : (await drive.listTrash(ctx)).map((n) => publicNode(n)));
199
206
  break;
200
207
  case "restore":
201
208
  need(1, "restore <id>");
@@ -232,6 +239,17 @@ async function runCli(argv, io) {
232
239
  need(2, "register <ref> <name>");
233
240
  print(publicNode(await drive.registerDocument(ctx, { ref: args[0], name: args[1], folder: values.parent })));
234
241
  break;
242
+ case "find-ref":
243
+ need(1, "find-ref <ref>");
244
+ print((await drive.findByRef(ctx, args[0])).map((r) => publicNode(r.node, r.path)));
245
+ break;
246
+ case "claim-ref":
247
+ need(3, "claim-ref <ref> <path> <subject> --role <r> --subject-kind <human|agent>");
248
+ print(publicNode(await drive.claimRef(ctx, args[1], args[0], {
249
+ subject: { kind: pick(values["subject-kind"], SUBJECT_KINDS, "subject-kind"), id: args[2] },
250
+ role: pick(values.role, ROLES, "role")
251
+ }, { parents: values.parents, onConflict })));
252
+ break;
235
253
  case "forget":
236
254
  need(1, "forget <ref>");
237
255
  print(await drive.forgetDocument(ctx, args[0]));
package/dist/client.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { E as ErrorCode, N as Node, S as SearchQuery } from './drive-BtRyexUP.js';
1
+ import { E as ErrorCode, N as Node, S as SearchQuery } from './drive-Dxy0gL0_.js';
2
2
  import 'agentic-identity';
3
3
 
4
4
  type Role = "viewer" | "commenter" | "editor" | "owner";
@@ -100,6 +100,19 @@ declare function createClient(options: ClientOptions): {
100
100
  app: string | null;
101
101
  })[];
102
102
  }>;
103
+ /** The live references to `ref`, with their paths. */
104
+ findByRef: (ref: string, o?: CallOptions) => Promise<PathItem[]>;
105
+ /** Items in the trash, newest first, each with the path it had before. */
106
+ listTrashWithPaths: (o?: CallOptions) => Promise<PathItem[]>;
107
+ /** Puts a reference and gives the subject a role on it, or does neither. Needs sharing. */
108
+ claimRef: (input: {
109
+ path: string;
110
+ ref: string;
111
+ subject: Subject;
112
+ role: Role;
113
+ parents?: boolean;
114
+ onConflict?: OnConflict;
115
+ }, o?: CallOptions) => Promise<Item>;
103
116
  registerDocument: (input: {
104
117
  ref: string;
105
118
  name: string;
@@ -173,4 +186,4 @@ declare function createClient(options: ClientOptions): {
173
186
  };
174
187
  type DriveClient = ReturnType<typeof createClient>;
175
188
 
176
- export { type ApiErrorCode, type CallOptions, type ClientErrorCode, type ClientOptions, type Downloaded, DriveApiError, type DriveClient, type Item, type OnConflict, type PathItem, type Role, type Subject, type SubjectKind, type UploadBody, createClient };
189
+ export { type ApiErrorCode, type CallOptions, type ClientErrorCode, type ClientOptions, type Downloaded, DriveApiError, type DriveClient, ErrorCode, type Item, Node, type OnConflict, type PathItem, type Role, type Subject, type SubjectKind, type UploadBody, createClient };
package/dist/client.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  DriveApiError,
3
3
  createClient
4
- } from "./chunk-S4AGCKXW.js";
4
+ } from "./chunk-LELTSSZF.js";
5
5
  export {
6
6
  DriveApiError,
7
7
  createClient