agentic-drive 0.7.0 → 0.8.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.
- package/CHANGELOG.md +18 -0
- package/README.md +12 -10
- package/dist/{chunk-EFO5PSJL.js → chunk-6UQ6CDLJ.js} +1 -1
- package/dist/chunk-AB2RTTHJ.js +59 -0
- package/dist/{chunk-S4AGCKXW.js → chunk-LELTSSZF.js} +6 -0
- package/dist/{chunk-PJQ3CRVL.js → chunk-R2DUVOJT.js} +182 -201
- package/dist/cli.js +24 -6
- package/dist/client.d.ts +15 -2
- package/dist/client.js +1 -1
- package/dist/{drive-BtRyexUP.d.ts → drive-Dxy0gL0_.d.ts} +26 -1
- package/dist/embed.css +42 -42
- package/dist/embed.js +3 -3
- package/dist/index.d.ts +16 -19
- package/dist/index.js +8 -6
- package/dist/mcp.d.ts +1 -1
- package/dist/mcp.js +10 -3
- package/dist/worker.d.ts +2 -1
- package/dist/worker.js +17 -26
- package/package.json +8 -3
- package/ui-dist/assets/index-GxwjdcYH.css +1 -0
- package/ui-dist/assets/index-mHs8RJgV.js +49 -0
- package/ui-dist/index.html +2 -2
- package/ui-dist/assets/index-B9nizMSQ.css +0 -1
- package/ui-dist/assets/index-DQzn7NQP.js +0 -49
|
@@ -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
|
|
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
|
|
629
|
+
return transactAs(ctx, async (tx, ctx2) => {
|
|
554
630
|
const w = batch();
|
|
555
|
-
const { parent, name } = await parentAndName(tx, w,
|
|
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(
|
|
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,
|
|
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
|
|
654
|
+
committed = await transactAs(ctx, async (tx, ctx2) => {
|
|
579
655
|
const w = batch();
|
|
580
|
-
const { parent, name } = await parentAndName(tx, w,
|
|
581
|
-
const result = await insert(tx, w,
|
|
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
|
|
671
|
+
return transactAs(ctx, async (tx, ctx2) => {
|
|
596
672
|
const w = batch();
|
|
597
|
-
const { parent, name } = await parentAndName(tx, w,
|
|
598
|
-
const { node } = await insert(tx, w,
|
|
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
|
|
708
|
+
return transactAs(ctx, async (tx, ctx2) => {
|
|
633
709
|
const w = batch();
|
|
634
|
-
const moved = await relocate(tx, w,
|
|
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
|
|
716
|
+
return transactAs(ctx, async (tx, ctx2) => {
|
|
641
717
|
const w = batch();
|
|
642
|
-
const node = await resolve(tx,
|
|
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,
|
|
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,
|
|
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
|
|
654
|
-
const node = await resolve(tx,
|
|
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(
|
|
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(
|
|
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
|
|
683
|
-
const node = await fetch(tx,
|
|
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(
|
|
687
|
-
const parent = opts.parentId === void 0 ? await rawNode(tx, node.parentId, node.owner,
|
|
688
|
-
const visible = parent !== null && (opts.parentId === void 0 || await can(
|
|
689
|
-
if (!parent || !visible || parent.kind !== "folder" || !await isLive(tx,
|
|
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(
|
|
695
|
-
await noClimb(
|
|
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(
|
|
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
|
|
1423
|
-
import {
|
|
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
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
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",
|
|
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-
|
|
7
|
+
} from "./chunk-6UQ6CDLJ.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-
|
|
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
|
|
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-
|
|
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 };
|