agentic-drive 0.9.0 → 0.10.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 +33 -0
- package/README.md +26 -4
- package/dist/account.d.ts +28 -0
- package/dist/account.js +7 -0
- package/dist/{chunk-R2DUVOJT.js → chunk-DB4F5EWD.js} +146 -216
- package/dist/chunk-ECVUDAHL.js +49 -0
- package/dist/chunk-HJXRMOUY.js +204 -0
- package/dist/{chunk-IMWV5QEH.js → chunk-NHHBNW64.js} +1 -1
- package/dist/{chunk-LELTSSZF.js → chunk-SEHT5Q5L.js} +4 -0
- package/dist/cli.js +17 -6
- package/dist/client.d.ts +7 -2
- package/dist/client.js +1 -1
- package/dist/{drive-Dxy0gL0_.d.ts → drive-BhMTeQ-s.d.ts} +28 -4
- package/dist/embed.css +15 -0
- package/dist/embed.js +150 -99
- package/dist/index.d.ts +4 -3
- package/dist/index.js +15 -9
- package/dist/mcp.d.ts +1 -1
- package/dist/mcp.js +8 -5
- package/dist/worker.d.ts +2 -2
- package/dist/worker.js +12 -4
- package/package.json +10 -4
- package/ui-dist/assets/{index-Dg9jF2oJ.js → index-BiYPMlrf.js} +16 -16
- package/ui-dist/assets/index-oybSrFGW.css +1 -0
- package/ui-dist/index.html +2 -2
- package/ui-dist/assets/index-Cb1lY2Sf.css +0 -1
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// src/drive.ts
|
|
2
|
-
import { EraseRefusedError, StoreConflictError, MAX_FOLDER_DEPTH, PRINCIPAL_KINDS, ROLES, SUBJECT_KINDS } from "agentic-identity";
|
|
2
|
+
import { EraseRefusedError, isMachineMade, StoreConflictError, MAX_FOLDER_DEPTH, PRINCIPAL_KINDS, ROLES, SUBJECT_KINDS, hasScope } from "agentic-identity";
|
|
3
3
|
var ROOT_ID = "root";
|
|
4
4
|
var NODES = "drive_nodes";
|
|
5
5
|
var NAMES = "drive_names";
|
|
6
|
+
var ACTIVITY = "drive_activity";
|
|
6
7
|
var ACTOR_KINDS = PRINCIPAL_KINDS;
|
|
7
8
|
var DECIDED = /* @__PURE__ */ Symbol("decided");
|
|
8
9
|
var ERROR_STATUS = Object.freeze({
|
|
@@ -131,6 +132,17 @@ function parseSubject(input) {
|
|
|
131
132
|
return { kind: s.kind, id: s.id };
|
|
132
133
|
}
|
|
133
134
|
var SHARE_ERROR = /^(invalid|give an|expiresAt|expiresInMs|resource exists|the last owner)/;
|
|
135
|
+
var ACTIVITY_ACTIONS = ["create", "rename", "move", "update", "share", "unshare", "delete", "restore"];
|
|
136
|
+
var publicActivity = ({ owner: _owner, actorId: _actorId, subjectId: _subjectId, subjectKind: _subjectKind, ...entry }) => ({
|
|
137
|
+
...entry,
|
|
138
|
+
actor: { id: entry.actor.id, kind: entry.principalKind === "human" ? "human" : "agent" }
|
|
139
|
+
});
|
|
140
|
+
var keepVia = (entry) => {
|
|
141
|
+
const via = entry.detail?.via;
|
|
142
|
+
if (via === void 0) delete entry.detail;
|
|
143
|
+
else entry.detail = { via };
|
|
144
|
+
};
|
|
145
|
+
var shown = (doc) => typeof doc.id === "string" && typeof doc.itemId === "string" && typeof doc.at === "number" && Number.isFinite(doc.at) && ACTIVITY_ACTIONS.includes(doc.action) && typeof doc.actor === "object" && doc.actor !== null && typeof doc.actor.id === "string" && PRINCIPAL_KINDS.includes(doc.principalKind);
|
|
134
146
|
function createDrive(deps) {
|
|
135
147
|
const { store, blobs } = deps;
|
|
136
148
|
let last = 0;
|
|
@@ -183,6 +195,29 @@ function createDrive(deps) {
|
|
|
183
195
|
const who = (ctx) => ({ id: ctx.actor.id, kind: ctx.actor.kind });
|
|
184
196
|
const stamp = (ctx, node) => ({ createdById: node.createdById ?? node.createdBy.id, updatedBy: who(ctx), updatedById: ctx.actor.id });
|
|
185
197
|
const resourceDoc = async (node, parent) => ({ id: node.id, type: "drive", parentId: await resourceOf(parent) });
|
|
198
|
+
const activityDoc = (ctx, node, action, detail) => {
|
|
199
|
+
if (!PRINCIPAL_KINDS.includes(ctx.actor.kind)) throw new DriveError("FORBIDDEN", `an actor needs a kind: ${PRINCIPAL_KINDS.join(", ")}`);
|
|
200
|
+
return {
|
|
201
|
+
id: newId(),
|
|
202
|
+
itemId: node.id,
|
|
203
|
+
owner: node.owner,
|
|
204
|
+
action,
|
|
205
|
+
actor: { id: ctx.actor.id, kind: isMachineMade(ctx.actor.kind) ? "agent" : "human" },
|
|
206
|
+
actorId: ctx.actor.id,
|
|
207
|
+
principalKind: ctx.actor.kind,
|
|
208
|
+
at: now(),
|
|
209
|
+
...detail ? { detail } : {},
|
|
210
|
+
...typeof detail?.subjectId === "string" ? { subjectId: detail.subjectId, subjectKind: detail.subjectKind } : {}
|
|
211
|
+
};
|
|
212
|
+
};
|
|
213
|
+
const logDoc = (ctx, node, action, detail) => {
|
|
214
|
+
const doc = activityDoc(ctx, node, action, detail);
|
|
215
|
+
return [doc.id, doc];
|
|
216
|
+
};
|
|
217
|
+
const logIn = (w, ctx, node, action, detail) => {
|
|
218
|
+
const doc = activityDoc(ctx, node, action, detail);
|
|
219
|
+
w.put(ACTIVITY, doc.id, doc);
|
|
220
|
+
};
|
|
186
221
|
async function rawNode(ops, id, owner, by, withMarked = false) {
|
|
187
222
|
const node = id === ROOT_ID ? rootNode(owner, by) : await ops.get(NODES, id);
|
|
188
223
|
return node && (withMarked || node.purging !== true) ? node : null;
|
|
@@ -328,6 +363,7 @@ function createDrive(deps) {
|
|
|
328
363
|
if (onConflict === "replace" && slot.node?.kind === "file" && fields.kind === "file") {
|
|
329
364
|
const node2 = { ...slot.node, ...fields, updatedAt: now(), ...stamp(ctx, slot.node) };
|
|
330
365
|
w.put(NODES, node2.id, node2);
|
|
366
|
+
logIn(w, ctx, node2, "update", { size: node2.size, hash: node2.hash });
|
|
331
367
|
return { node: node2, replacedBlob: slot.node.blobKey };
|
|
332
368
|
}
|
|
333
369
|
if (onConflict !== "rename") throw new DriveError("NAME_TAKEN", `"${name}" already exists`);
|
|
@@ -365,6 +401,7 @@ function createDrive(deps) {
|
|
|
365
401
|
...fields
|
|
366
402
|
};
|
|
367
403
|
w.put(NODES, node.id, node);
|
|
404
|
+
logIn(w, ctx, node, "create", { name: node.name, kind: node.kind });
|
|
368
405
|
w.put(NAMES, await claimId(parent.owner, parent.id, key), { nodeId: node.id, owner: parent.owner, parentId: parent.id });
|
|
369
406
|
if (sharing) w.put("resources", node.id, await resourceDoc(node, parent));
|
|
370
407
|
return { node, replacedBlob: null };
|
|
@@ -402,6 +439,8 @@ function createDrive(deps) {
|
|
|
402
439
|
}
|
|
403
440
|
const moved = { ...node, parentId: dest.id, name, nameKey: key, updatedAt: now(), ...stamp(ctx, node) };
|
|
404
441
|
w.put(NODES, moved.id, moved);
|
|
442
|
+
if (moved.name !== node.name) logIn(w, ctx, moved, "rename", { from: node.name, to: moved.name });
|
|
443
|
+
if (moved.parentId !== node.parentId) logIn(w, ctx, moved, "move", { from: node.parentId, to: moved.parentId });
|
|
405
444
|
const [from, to] = [await claimId(node.owner, node.parentId, node.nameKey), await claimId(dest.owner, dest.id, key)];
|
|
406
445
|
if (from !== to) {
|
|
407
446
|
if ((await ops.get(NAMES, from))?.nodeId === node.id) w.remove(NAMES, from);
|
|
@@ -438,6 +477,16 @@ function createDrive(deps) {
|
|
|
438
477
|
if (!await can(ctx, "read", rootNode(ctx.owner, ctx.actor))) throw new DriveError("FORBIDDEN", "not allowed to read this drive");
|
|
439
478
|
return rawOwnerNodes(ctx.owner, withMarked);
|
|
440
479
|
}
|
|
480
|
+
async function readOwnerRoot(ctx) {
|
|
481
|
+
if (sharing && ctx.principal !== void 0 && await store.get("resources", await rootResource(ctx.owner)) === null) {
|
|
482
|
+
if ((await rawOwnerNodes(ctx.owner, true)).length === 0) {
|
|
483
|
+
if (hasScope(ctx.principal.scopes, "drive:read")) return;
|
|
484
|
+
throw new DriveError("FORBIDDEN", "not allowed to read this drive");
|
|
485
|
+
}
|
|
486
|
+
await prepare(ctx);
|
|
487
|
+
}
|
|
488
|
+
if (!await can(ctx, "read", rootNode(ctx.owner, ctx.actor))) throw new DriveError("FORBIDDEN", "not allowed to read this drive");
|
|
489
|
+
}
|
|
441
490
|
function pathIn(byId, node, liveOnly) {
|
|
442
491
|
const names = [];
|
|
443
492
|
for (let n = node, depth = 0; depth < 1e3; depth++) {
|
|
@@ -514,6 +563,7 @@ function createDrive(deps) {
|
|
|
514
563
|
const all = await rawOwnerNodes(owner, true);
|
|
515
564
|
if (all.length === 0) {
|
|
516
565
|
await finishResource(principal, await rootResource(owner));
|
|
566
|
+
for (const entry of await allWhere(store, ACTIVITY, { itemId: ROOT_ID, owner })) await store.delete(ACTIVITY, entry.id);
|
|
517
567
|
return removed;
|
|
518
568
|
}
|
|
519
569
|
removed += await removeNodes(all, void 0, principal);
|
|
@@ -592,6 +642,7 @@ function createDrive(deps) {
|
|
|
592
642
|
const chunk = rows.slice(i, i + size);
|
|
593
643
|
for (const node of chunk) if (node.blobKey) await blobs.delete(node.blobKey);
|
|
594
644
|
for (const node of chunk) await finishResource(principal, node.id);
|
|
645
|
+
for (const node of chunk) for (const entry of await allWhere(store, ACTIVITY, { itemId: node.id, owner: node.owner })) await store.delete(ACTIVITY, entry.id);
|
|
595
646
|
await transact(async (tx) => {
|
|
596
647
|
const held = [];
|
|
597
648
|
for (const node of chunk) {
|
|
@@ -619,6 +670,30 @@ function createDrive(deps) {
|
|
|
619
670
|
async stat(ctx, loc) {
|
|
620
671
|
return resolve(store, ctx, loc);
|
|
621
672
|
},
|
|
673
|
+
/**
|
|
674
|
+
* The changes made to one item, newest first: who (a person or an agent), what, and when. Anyone who may read
|
|
675
|
+
* the item may read its log. A trashed item can be named by id, so its log can be read before a restore.
|
|
676
|
+
*/
|
|
677
|
+
async listActivity(ctx, loc, opts = {}) {
|
|
678
|
+
const limit = opts.limit ?? 100;
|
|
679
|
+
if (!Number.isInteger(limit) || limit < 1) throw new RangeError("limit must be an integer of 1 or more");
|
|
680
|
+
const node = loc.includes("/") ? await resolve(store, ctx, loc) : await fetch(store, ctx, loc);
|
|
681
|
+
if (!node) throw new DriveError("NOT_FOUND", `no item ${loc}`);
|
|
682
|
+
const owner = node.owner === ctx.owner;
|
|
683
|
+
const out = [];
|
|
684
|
+
for (let cursor; out.length < limit; ) {
|
|
685
|
+
const page = await store.query(ACTIVITY, { where: { itemId: node.id, owner: node.owner }, orderBy: { field: "at", direction: "desc" }, limit: Math.min(limit - out.length, 100), cursor });
|
|
686
|
+
for (const e of page.items) {
|
|
687
|
+
if (!shown(e.data)) continue;
|
|
688
|
+
const entry = publicActivity(e.data);
|
|
689
|
+
if (!owner && (entry.action === "share" || entry.action === "unshare")) keepVia(entry);
|
|
690
|
+
out.push(entry);
|
|
691
|
+
}
|
|
692
|
+
if (!page.nextCursor) break;
|
|
693
|
+
cursor = page.nextCursor;
|
|
694
|
+
}
|
|
695
|
+
return out;
|
|
696
|
+
},
|
|
622
697
|
/** Live children, sorted by name. */
|
|
623
698
|
async list(ctx, loc = "/") {
|
|
624
699
|
const folder = await resolve(store, ctx, loc);
|
|
@@ -735,6 +810,8 @@ function createDrive(deps) {
|
|
|
735
810
|
const t = now();
|
|
736
811
|
const trashed = { ...node, trashed: true, trashedAt: t, updatedAt: t, ...stamp(ctx2, node) };
|
|
737
812
|
await tx.put(NODES, node.id, trashed);
|
|
813
|
+
const entry = activityDoc(ctx2, node, "delete");
|
|
814
|
+
await tx.put(ACTIVITY, entry.id, entry);
|
|
738
815
|
if (held?.nodeId === node.id) await tx.delete(NAMES, claim);
|
|
739
816
|
return trashed;
|
|
740
817
|
});
|
|
@@ -788,6 +865,8 @@ function createDrive(deps) {
|
|
|
788
865
|
}
|
|
789
866
|
const restored = { ...node, parentId: parent.id, name, nameKey: key, trashed: false, trashedAt: null, updatedAt: now(), ...stamp(ctx2, node) };
|
|
790
867
|
await tx.put(NODES, restored.id, restored);
|
|
868
|
+
const entry = activityDoc(ctx2, restored, "restore");
|
|
869
|
+
await tx.put(ACTIVITY, entry.id, entry);
|
|
791
870
|
await tx.put(NAMES, await claimId(parent.owner, parent.id, key), { nodeId: restored.id, owner: parent.owner, parentId: parent.id });
|
|
792
871
|
if (sharing && parent.id !== node.parentId) await tx.put("resources", restored.id, await resourceDoc(restored, parent));
|
|
793
872
|
return restored;
|
|
@@ -817,7 +896,8 @@ function createDrive(deps) {
|
|
|
817
896
|
},
|
|
818
897
|
/** Every item of the owner, trashed or not, with its original path and bytes. For export. */
|
|
819
898
|
async exportData(ctx) {
|
|
820
|
-
|
|
899
|
+
await readOwnerRoot(ctx);
|
|
900
|
+
const all = await rawOwnerNodes(ctx.owner, false);
|
|
821
901
|
const byId = new Map(all.map((n) => [n.id, n]));
|
|
822
902
|
const out = [];
|
|
823
903
|
for (const node of all.sort((a, b) => a.id < b.id ? -1 : 1)) {
|
|
@@ -938,6 +1018,7 @@ function createDrive(deps) {
|
|
|
938
1018
|
if (removed === 0 && !gone) console.error(`claimRef could not remove ${node.id} after a failed share. Purge it.`);
|
|
939
1019
|
throw error;
|
|
940
1020
|
}
|
|
1021
|
+
await store.put(ACTIVITY, ...logDoc(ctx, node, "share", { subjectKind: subject.kind, subjectId: subject.id, role: claim.role }));
|
|
941
1022
|
return node;
|
|
942
1023
|
},
|
|
943
1024
|
/** An app deleted a document. Its references in this drive go to the trash. Returns how many. */
|
|
@@ -960,13 +1041,13 @@ function createDrive(deps) {
|
|
|
960
1041
|
* What drive holds about the person behind a call, besides their own drive (`exportData` has that). It is
|
|
961
1042
|
* only for the person in person: an agent gets `partial: true` and nothing else. It holds the ids of the
|
|
962
1043
|
* items they or their agents made or changed in other drives (no names, and nothing of the people who own
|
|
963
|
-
* them), the shares they hold
|
|
964
|
-
* did. Invitee emails and link tokens are not in it.
|
|
1044
|
+
* them), the shares they hold and the invites they sent on drive items, and the audit entries of what they and
|
|
1045
|
+
* their agents did. Invitee emails and link tokens are not in it.
|
|
965
1046
|
*/
|
|
966
1047
|
async exportPerson(ctx) {
|
|
967
|
-
await
|
|
1048
|
+
await readOwnerRoot(ctx);
|
|
968
1049
|
const subject = subjectOf(ctx);
|
|
969
|
-
if (!isInPerson(ctx)) return { subject, partial: true, authored: [], agents: [], shares: { held: [], invitesCreated: [] }, audit: [] };
|
|
1050
|
+
if (!isInPerson(ctx)) return { subject, partial: true, authored: [], agents: [], shares: { held: [], invitesCreated: [] }, audit: [], activity: [] };
|
|
970
1051
|
const { actors, ownedAgents } = await actorsOf(store, subject.id);
|
|
971
1052
|
const authored = (await madeBy(store, actors)).filter((n) => n.owner !== subject.id && n.purging !== true).map((n) => ({
|
|
972
1053
|
id: n.id,
|
|
@@ -977,8 +1058,9 @@ function createDrive(deps) {
|
|
|
977
1058
|
updatedAt: actors.includes(n.updatedById) ? n.updatedAt : null,
|
|
978
1059
|
by: [...new Set([actors.includes(n.createdById) ? n.createdBy : null, actors.includes(n.updatedById) ? n.updatedBy : null].filter((a) => a !== null).map((a) => JSON.stringify(a)))].map((a) => JSON.parse(a))
|
|
979
1060
|
})).sort((x, y) => x.id < y.id ? -1 : 1);
|
|
1061
|
+
const onDrive = async (resourceId) => typeof resourceId === "string" && (await store.get("resources", resourceId))?.type === "drive";
|
|
980
1062
|
const held = [];
|
|
981
|
-
for (const [actor, kind] of heldBy(subject, ownedAgents)) for (const g of await allWhere(store, "grants", { subjectId: actor, subjectKind: kind })) held.push({ subjectKind: g.data.subjectKind, subjectId: g.data.subjectId, resourceId: g.data.resourceId, role: g.data.role });
|
|
1063
|
+
for (const [actor, kind] of heldBy(subject, ownedAgents)) for (const g of await allWhere(store, "grants", { subjectId: actor, subjectKind: kind })) if (await onDrive(g.data.resourceId)) held.push({ subjectKind: g.data.subjectKind, subjectId: g.data.subjectId, resourceId: g.data.resourceId, role: g.data.role });
|
|
982
1064
|
const agents = [];
|
|
983
1065
|
for (const agent of ownedAgents) {
|
|
984
1066
|
const items = (await rawOwnerNodes(agent, false)).map((n) => ({ id: n.id, kind: n.kind, name: n.name, size: n.size, mime: n.mime, createdAt: n.createdAt, updatedAt: n.updatedAt, trashed: n.trashed }));
|
|
@@ -988,6 +1070,7 @@ function createDrive(deps) {
|
|
|
988
1070
|
const audit = [];
|
|
989
1071
|
for (const actor of actors) {
|
|
990
1072
|
for (const i of await allWhere(store, "invites", { createdBy: actor })) {
|
|
1073
|
+
if (!await onDrive(i.data.resourceId)) continue;
|
|
991
1074
|
invitesCreated.push({ id: i.data.id, resourceId: i.data.resourceId, role: i.data.role, expiresAt: i.data.expiresAt ?? null, accepted: i.data.acceptedAt !== void 0 });
|
|
992
1075
|
}
|
|
993
1076
|
for (const e of await allWhere(store, "audit", { principalId: actor }, { field: "at" })) {
|
|
@@ -995,7 +1078,18 @@ function createDrive(deps) {
|
|
|
995
1078
|
}
|
|
996
1079
|
}
|
|
997
1080
|
audit.sort((x, y) => x.at - y.at);
|
|
998
|
-
|
|
1081
|
+
const activity = [];
|
|
1082
|
+
for (const actor of actors) {
|
|
1083
|
+
for (const e of await allWhere(store, ACTIVITY, { actorId: actor })) {
|
|
1084
|
+
if (!shown(e.data)) continue;
|
|
1085
|
+
const doc = e.data;
|
|
1086
|
+
const entry = publicActivity(doc);
|
|
1087
|
+
if (doc.owner !== subject.id && !ownedAgents.includes(doc.owner)) keepVia(entry);
|
|
1088
|
+
activity.push(entry);
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
activity.sort((x, y) => x.at - y.at || (x.id < y.id ? -1 : 1));
|
|
1092
|
+
return { subject, partial: false, authored, agents, shares: { held, invitesCreated }, audit, activity };
|
|
999
1093
|
},
|
|
1000
1094
|
/**
|
|
1001
1095
|
* Erases a person for good. Only the person in person may. Their drive and its shares go. So do the shares
|
|
@@ -1043,16 +1137,38 @@ function createDrive(deps) {
|
|
|
1043
1137
|
}
|
|
1044
1138
|
});
|
|
1045
1139
|
}
|
|
1140
|
+
for (const actor of actors) {
|
|
1141
|
+
const mine = await allWhere(store, ACTIVITY, { actorId: actor });
|
|
1142
|
+
for (let i = 0; i < mine.length; i += CHUNK) {
|
|
1143
|
+
await transact(async (tx) => {
|
|
1144
|
+
for (const e of mine.slice(i, i + CHUNK)) await tx.put(ACTIVITY, e.id, { ...e.data, actorId: ERASED, actor: { ...e.data.actor, id: ERASED } });
|
|
1145
|
+
});
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
for (const [actor, kind] of heldBy(subjectOf(ctx), ownedAgents)) {
|
|
1149
|
+
for (const e of await allWhere(store, ACTIVITY, { subjectId: actor, subjectKind: kind })) {
|
|
1150
|
+
await store.put(ACTIVITY, e.id, { ...e.data, subjectId: ERASED, detail: { ...e.data.detail, subjectId: ERASED } });
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1046
1153
|
return { removed, grants, invites, anonymized: todo.length };
|
|
1047
1154
|
},
|
|
1048
1155
|
/** Gives a person or an agent a role on an item and everything under it. Only an owner may. */
|
|
1049
1156
|
async shareWith(ctx, loc, subject, role) {
|
|
1050
|
-
const
|
|
1051
|
-
|
|
1157
|
+
const node = await resolve(store, ctx, loc);
|
|
1158
|
+
const resource = await resourceOf(node);
|
|
1159
|
+
const target = parseSubject(subject);
|
|
1160
|
+
const entry = logDoc(ctx, node, "share", { subjectKind: target.kind, subjectId: target.id, role });
|
|
1161
|
+
await viaSharing(ctx, (s, p) => s.grant(p, resource, target, role));
|
|
1162
|
+
await store.put(ACTIVITY, ...entry);
|
|
1052
1163
|
},
|
|
1053
1164
|
async unshare(ctx, loc, subject) {
|
|
1054
|
-
const
|
|
1055
|
-
|
|
1165
|
+
const node = await resolve(store, ctx, loc);
|
|
1166
|
+
const resource = await resourceOf(node);
|
|
1167
|
+
const target = parseSubject(subject);
|
|
1168
|
+
const entry = logDoc(ctx, node, "unshare", { subjectKind: target.kind, subjectId: target.id });
|
|
1169
|
+
const held = (await allWhere(store, "grants", { resourceId: resource, subjectId: target.id, subjectKind: target.kind })).length > 0;
|
|
1170
|
+
await viaSharing(ctx, (s, p) => s.revokeGrant(p, resource, target));
|
|
1171
|
+
if (held) await store.put(ACTIVITY, ...entry);
|
|
1056
1172
|
},
|
|
1057
1173
|
/** Invite by `email` (a person) or `handle` (an agent). The invite lasts `expiresInMs`, if given. It sends no message. */
|
|
1058
1174
|
async invite(ctx, loc, input) {
|
|
@@ -1061,7 +1177,16 @@ function createDrive(deps) {
|
|
|
1061
1177
|
},
|
|
1062
1178
|
/** `identity` is the verified email of a person, or the handle of an agent. */
|
|
1063
1179
|
async acceptInvite(ctx, inviteId, identity) {
|
|
1180
|
+
const invite = await store.get("invites", inviteId);
|
|
1181
|
+
const node = typeof invite?.resourceId === "string" ? await store.get(NODES, invite.resourceId) : null;
|
|
1182
|
+
const subjectKind = node && (ctx.actor.kind === "human" || ctx.actor.kind === "agent") ? subjectKindOf(ctx.actor.kind) : null;
|
|
1183
|
+
const roleOf = async () => node && subjectKind ? (await allWhere(store, "grants", { resourceId: node.id, subjectId: ctx.actor.id, subjectKind }))[0]?.data.role ?? null : null;
|
|
1184
|
+
const before = await roleOf();
|
|
1064
1185
|
await viaSharing(ctx, (s, p) => s.acceptInvite(p, inviteId, identity));
|
|
1186
|
+
const after = await roleOf();
|
|
1187
|
+
if (node && subjectKind && after !== null && after !== before) {
|
|
1188
|
+
await store.put(ACTIVITY, ...logDoc(ctx, node, "share", { subjectKind, subjectId: ctx.actor.id, role: after, via: "invite" }));
|
|
1189
|
+
}
|
|
1065
1190
|
},
|
|
1066
1191
|
/** The token comes back once. Only its hash is stored. */
|
|
1067
1192
|
async createLink(ctx, loc, input) {
|
|
@@ -1111,9 +1236,10 @@ function createDrive(deps) {
|
|
|
1111
1236
|
throw error;
|
|
1112
1237
|
}
|
|
1113
1238
|
}
|
|
1114
|
-
async function prepare(ctx) {
|
|
1239
|
+
async function prepare(ctx, create = true) {
|
|
1115
1240
|
if (!sharing || ctx.principal === void 0) return;
|
|
1116
1241
|
if (ctx.principal.chain[0]?.id !== ctx.owner) throw new DriveError("FORBIDDEN", "the caller does not own this drive");
|
|
1242
|
+
if (!create) return;
|
|
1117
1243
|
const id = await rootResource(ctx.owner);
|
|
1118
1244
|
if ((await store.get("resources", id))?.type === "drive") return;
|
|
1119
1245
|
try {
|
|
@@ -1121,203 +1247,13 @@ function createDrive(deps) {
|
|
|
1121
1247
|
} catch {
|
|
1122
1248
|
}
|
|
1123
1249
|
}
|
|
1124
|
-
const guarded = (call) => async (ctx, ...args) => {
|
|
1125
|
-
await prepare(ctx);
|
|
1250
|
+
const guarded = (call, create = true) => async (ctx, ...args) => {
|
|
1251
|
+
await prepare(ctx, create);
|
|
1126
1252
|
return call(ctx, ...args);
|
|
1127
1253
|
};
|
|
1128
1254
|
const open = /* @__PURE__ */ new Set(["openShared", "listShared", "readShared"]);
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
// src/upload.ts
|
|
1133
|
-
var DEFAULT_MAX_BYTES = 25 * 1024 * 1024;
|
|
1134
|
-
var SIGNATURES = [
|
|
1135
|
-
[[137, 80, 78, 71, 13, 10, 26, 10], "image/png"],
|
|
1136
|
-
[[255, 216, 255], "image/jpeg"],
|
|
1137
|
-
[[71, 73, 70, 56], "image/gif"],
|
|
1138
|
-
[[37, 80, 68, 70, 45], "application/pdf"],
|
|
1139
|
-
[[80, 75, 3, 4], "application/zip"],
|
|
1140
|
-
[[31, 139], "application/gzip"]
|
|
1141
|
-
];
|
|
1142
|
-
function sniffMime(head) {
|
|
1143
|
-
const hit = SIGNATURES.find(([sig]) => sig.every((byte, i) => head[i] === byte));
|
|
1144
|
-
if (hit) return hit[1];
|
|
1145
|
-
const isWebp = [..."RIFF"].every((c, i) => head[i] === c.charCodeAt(0)) && [..."WEBP"].every((c, i) => head[8 + i] === c.charCodeAt(0));
|
|
1146
|
-
return isWebp ? "image/webp" : null;
|
|
1147
|
-
}
|
|
1148
|
-
function looksLikeText(head) {
|
|
1149
|
-
if (head.includes(0)) return false;
|
|
1150
|
-
for (let cut = 0; cut < 4 && cut <= head.length; cut++) {
|
|
1151
|
-
try {
|
|
1152
|
-
new TextDecoder("utf-8", { fatal: true }).decode(head.subarray(0, head.length - cut));
|
|
1153
|
-
return true;
|
|
1154
|
-
} catch {
|
|
1155
|
-
continue;
|
|
1156
|
-
}
|
|
1157
|
-
}
|
|
1158
|
-
return false;
|
|
1159
|
-
}
|
|
1160
|
-
async function* iterate(source) {
|
|
1161
|
-
if (Symbol.asyncIterator in source) {
|
|
1162
|
-
yield* source;
|
|
1163
|
-
return;
|
|
1164
|
-
}
|
|
1165
|
-
const reader = source.getReader();
|
|
1166
|
-
try {
|
|
1167
|
-
for (; ; ) {
|
|
1168
|
-
const { done, value } = await reader.read();
|
|
1169
|
-
if (done) return;
|
|
1170
|
-
yield value;
|
|
1171
|
-
}
|
|
1172
|
-
} finally {
|
|
1173
|
-
reader.releaseLock();
|
|
1174
|
-
}
|
|
1175
|
-
}
|
|
1176
|
-
async function upload(drive, ctx, path, source, opts = {}) {
|
|
1177
|
-
const max = opts.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
1178
|
-
const parts = [];
|
|
1179
|
-
let size = 0;
|
|
1180
|
-
for await (const chunk of iterate(source)) {
|
|
1181
|
-
size += chunk.byteLength;
|
|
1182
|
-
if (size > max) throw new DriveError("TOO_LARGE", `upload is over the limit of ${max} bytes`);
|
|
1183
|
-
parts.push(chunk);
|
|
1184
|
-
}
|
|
1185
|
-
const data = new Uint8Array(size);
|
|
1186
|
-
for (let at = 0, i = 0; i < parts.length; at += parts[i].byteLength, i++) data.set(parts[i], at);
|
|
1187
|
-
const head = data.subarray(0, 512);
|
|
1188
|
-
const mime = sniffMime(head) ?? (opts.mime !== void 0 ? safeMime(opts.mime) : looksLikeText(head) ? "text/plain" : "application/octet-stream");
|
|
1189
|
-
return drive.putFile(ctx, path, data, { mime, parents: opts.parents, onConflict: opts.onConflict });
|
|
1190
|
-
}
|
|
1191
|
-
|
|
1192
|
-
// src/zip.ts
|
|
1193
|
-
var CRC_TABLE = Array.from({ length: 256 }, (_, n) => {
|
|
1194
|
-
let c = n;
|
|
1195
|
-
for (let k = 0; k < 8; k++) c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
|
|
1196
|
-
return c >>> 0;
|
|
1197
|
-
});
|
|
1198
|
-
function crc32(data) {
|
|
1199
|
-
let crc = 4294967295;
|
|
1200
|
-
for (const byte of data) crc = CRC_TABLE[(crc ^ byte) & 255] ^ crc >>> 8;
|
|
1201
|
-
return (crc ^ 4294967295) >>> 0;
|
|
1202
|
-
}
|
|
1203
|
-
function safeEntryName(name) {
|
|
1204
|
-
if (name === "" || /[\\\u0000]/.test(name) || name.startsWith("/") || /^[a-zA-Z]:/.test(name)) return false;
|
|
1205
|
-
return name.split("/").every((part) => part !== "" && part !== "." && part !== "..");
|
|
1206
|
-
}
|
|
1207
|
-
function writeZip(entries, at) {
|
|
1208
|
-
for (const { name } of entries) if (!safeEntryName(name)) throw new Error(`zip: unsafe entry name ${JSON.stringify(name)}`);
|
|
1209
|
-
const d = new Date(at);
|
|
1210
|
-
const time = d.getUTCHours() << 11 | d.getUTCMinutes() << 5 | d.getUTCSeconds() >> 1;
|
|
1211
|
-
const date = Math.max(d.getUTCFullYear(), 1980) - 1980 << 9 | d.getUTCMonth() + 1 << 5 | d.getUTCDate();
|
|
1212
|
-
const parts = [];
|
|
1213
|
-
const central = [];
|
|
1214
|
-
let offset = 0;
|
|
1215
|
-
const u16 = (v, o, n) => v.setUint16(o, n, true);
|
|
1216
|
-
const u32 = (v, o, n) => v.setUint32(o, n, true);
|
|
1217
|
-
for (const entry of entries) {
|
|
1218
|
-
const name = new TextEncoder().encode(entry.name);
|
|
1219
|
-
const crc = crc32(entry.data);
|
|
1220
|
-
if (entry.data.byteLength >= 4294967295 || offset >= 4294967295 || entries.length >= 65535) throw new Error("zip: export is too large for a plain zip");
|
|
1221
|
-
const local = new DataView(new ArrayBuffer(30));
|
|
1222
|
-
u32(local, 0, 67324752);
|
|
1223
|
-
u16(local, 4, 20);
|
|
1224
|
-
u16(local, 6, 2048);
|
|
1225
|
-
u16(local, 8, 0);
|
|
1226
|
-
u16(local, 10, time);
|
|
1227
|
-
u16(local, 12, date);
|
|
1228
|
-
u32(local, 14, crc);
|
|
1229
|
-
u32(local, 18, entry.data.byteLength);
|
|
1230
|
-
u32(local, 22, entry.data.byteLength);
|
|
1231
|
-
u16(local, 26, name.length);
|
|
1232
|
-
u16(local, 28, 0);
|
|
1233
|
-
const head = new DataView(new ArrayBuffer(46));
|
|
1234
|
-
u32(head, 0, 33639248);
|
|
1235
|
-
u16(head, 4, 20);
|
|
1236
|
-
u16(head, 6, 20);
|
|
1237
|
-
u16(head, 8, 2048);
|
|
1238
|
-
u16(head, 10, 0);
|
|
1239
|
-
u16(head, 12, time);
|
|
1240
|
-
u16(head, 14, date);
|
|
1241
|
-
u32(head, 16, crc);
|
|
1242
|
-
u32(head, 20, entry.data.byteLength);
|
|
1243
|
-
u32(head, 24, entry.data.byteLength);
|
|
1244
|
-
u16(head, 28, name.length);
|
|
1245
|
-
u32(head, 42, offset);
|
|
1246
|
-
parts.push(new Uint8Array(local.buffer), name, entry.data);
|
|
1247
|
-
central.push(new Uint8Array(head.buffer), name);
|
|
1248
|
-
offset += 30 + name.length + entry.data.byteLength;
|
|
1249
|
-
}
|
|
1250
|
-
const size = central.reduce((n, p) => n + p.byteLength, 0);
|
|
1251
|
-
const end = new DataView(new ArrayBuffer(22));
|
|
1252
|
-
u32(end, 0, 101010256);
|
|
1253
|
-
u16(end, 8, entries.length);
|
|
1254
|
-
u16(end, 10, entries.length);
|
|
1255
|
-
u32(end, 12, size);
|
|
1256
|
-
u32(end, 16, offset);
|
|
1257
|
-
const all = [...parts, ...central, new Uint8Array(end.buffer)];
|
|
1258
|
-
const out = new Uint8Array(all.reduce((n, p) => n + p.byteLength, 0));
|
|
1259
|
-
let at2 = 0;
|
|
1260
|
-
for (const p of all) {
|
|
1261
|
-
out.set(p, at2);
|
|
1262
|
-
at2 += p.byteLength;
|
|
1263
|
-
}
|
|
1264
|
-
return out;
|
|
1265
|
-
}
|
|
1266
|
-
function readZip(bytes) {
|
|
1267
|
-
const v = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
1268
|
-
let end = bytes.byteLength - 22;
|
|
1269
|
-
while (end >= 0 && v.getUint32(end, true) !== 101010256) end--;
|
|
1270
|
-
if (end < 0) throw new Error("zip: no end signature");
|
|
1271
|
-
const count = v.getUint16(end + 10, true);
|
|
1272
|
-
const out = /* @__PURE__ */ new Map();
|
|
1273
|
-
for (let i = 0, at = v.getUint32(end + 16, true); i < count; i++) {
|
|
1274
|
-
if (v.getUint32(at, true) !== 33639248) throw new Error("zip: bad central signature");
|
|
1275
|
-
if (v.getUint16(at + 10, true) !== 0) throw new Error("zip: compressed entries are not supported");
|
|
1276
|
-
const crc = v.getUint32(at + 16, true);
|
|
1277
|
-
const size = v.getUint32(at + 24, true);
|
|
1278
|
-
const nameLen = v.getUint16(at + 28, true);
|
|
1279
|
-
const local = v.getUint32(at + 42, true);
|
|
1280
|
-
const name = new TextDecoder().decode(bytes.subarray(at + 46, at + 46 + nameLen));
|
|
1281
|
-
if (v.getUint32(local, true) !== 67324752) throw new Error("zip: bad local signature");
|
|
1282
|
-
const start = local + 30 + v.getUint16(local + 26, true) + v.getUint16(local + 28, true);
|
|
1283
|
-
const data = bytes.slice(start, start + size);
|
|
1284
|
-
if (crc32(data) !== crc) throw new Error(`zip: crc mismatch in ${name}`);
|
|
1285
|
-
out.set(name, data);
|
|
1286
|
-
at += 46 + nameLen + v.getUint16(at + 30, true) + v.getUint16(at + 32, true);
|
|
1287
|
-
}
|
|
1288
|
-
return out;
|
|
1289
|
-
}
|
|
1290
|
-
|
|
1291
|
-
// src/export.ts
|
|
1292
|
-
async function buildExport(drive, ctx, now = Date.now) {
|
|
1293
|
-
const entries = [];
|
|
1294
|
-
const items = (await drive.exportData(ctx)).map(({ node, path, data, missing }) => {
|
|
1295
|
-
const file = data ? `files/${node.id}` : null;
|
|
1296
|
-
if (data && file) entries.push({ name: file, data });
|
|
1297
|
-
return {
|
|
1298
|
-
id: node.id,
|
|
1299
|
-
owner: node.owner,
|
|
1300
|
-
path,
|
|
1301
|
-
parentId: node.parentId,
|
|
1302
|
-
kind: node.kind,
|
|
1303
|
-
name: node.name,
|
|
1304
|
-
size: node.size,
|
|
1305
|
-
mime: node.mime,
|
|
1306
|
-
hash: node.hash,
|
|
1307
|
-
ref: node.ref,
|
|
1308
|
-
createdAt: node.createdAt,
|
|
1309
|
-
updatedAt: node.updatedAt,
|
|
1310
|
-
createdBy: node.createdBy,
|
|
1311
|
-
updatedBy: node.updatedBy,
|
|
1312
|
-
trashed: node.trashed,
|
|
1313
|
-
trashedAt: node.trashedAt,
|
|
1314
|
-
file,
|
|
1315
|
-
...missing ? { missing: true } : {}
|
|
1316
|
-
};
|
|
1317
|
-
});
|
|
1318
|
-
const person = await drive.exportPerson(ctx);
|
|
1319
|
-
const manifest = { format: "agentic-drive-export", version: 2, exportedAt: now(), owner: ctx.owner, subject: person.subject, partial: person.partial, items, authored: person.authored, agents: person.agents, shares: person.shares, audit: person.audit };
|
|
1320
|
-
return writeZip([{ name: "manifest.json", data: new TextEncoder().encode(JSON.stringify(manifest, null, 2)) }, ...entries], now());
|
|
1255
|
+
const reads = /* @__PURE__ */ new Set(["exportData", "exportPerson"]);
|
|
1256
|
+
return Object.fromEntries(Object.entries(api).map(([name, call]) => [name, open.has(name) ? call : guarded(call, !reads.has(name))]));
|
|
1321
1257
|
}
|
|
1322
1258
|
|
|
1323
1259
|
// src/auth.ts
|
|
@@ -1398,7 +1334,7 @@ function backfillResources(store, options = {}) {
|
|
|
1398
1334
|
import { createHash } from "crypto";
|
|
1399
1335
|
import { existsSync } from "fs";
|
|
1400
1336
|
import { join } from "path";
|
|
1401
|
-
import { createSharing, isMachineMade, isScope, issueCredential, revokeCredential } from "agentic-identity";
|
|
1337
|
+
import { createSharing, isMachineMade as isMachineMade2, isScope, issueCredential, revokeCredential } from "agentic-identity";
|
|
1402
1338
|
import { createFileBlobs, createFileStore } from "agentic-identity/file";
|
|
1403
1339
|
function openLocal(dir, opts = {}) {
|
|
1404
1340
|
if (!opts.beforeImport && existsSync(join(dir, "db", "store.jsonl")) && !existsSync(join(dir, "store.json"))) {
|
|
@@ -1462,8 +1398,8 @@ async function revokeKey(store, token) {
|
|
|
1462
1398
|
}
|
|
1463
1399
|
function publicNode(node, path) {
|
|
1464
1400
|
const { nameKey: _nameKey, blobKey: _blobKey, purging: _purging, ...rest } = node;
|
|
1465
|
-
const
|
|
1466
|
-
return path === void 0 ?
|
|
1401
|
+
const shown2 = { ...rest, machineMade: isMachineMade2(node.createdBy.kind) };
|
|
1402
|
+
return path === void 0 ? shown2 : { path, ...shown2 };
|
|
1467
1403
|
}
|
|
1468
1404
|
|
|
1469
1405
|
export {
|
|
@@ -1477,12 +1413,6 @@ export {
|
|
|
1477
1413
|
subjectKindOf,
|
|
1478
1414
|
parseSubject,
|
|
1479
1415
|
createDrive,
|
|
1480
|
-
DEFAULT_MAX_BYTES,
|
|
1481
|
-
sniffMime,
|
|
1482
|
-
upload,
|
|
1483
|
-
writeZip,
|
|
1484
|
-
readZip,
|
|
1485
|
-
buildExport,
|
|
1486
1416
|
authenticate,
|
|
1487
1417
|
inviteIdentity,
|
|
1488
1418
|
backfillAuthorIds,
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DriveError,
|
|
3
|
+
publicNode
|
|
4
|
+
} from "./chunk-DB4F5EWD.js";
|
|
5
|
+
|
|
6
|
+
// src/account.ts
|
|
7
|
+
var plain = (value) => JSON.parse(JSON.stringify(value));
|
|
8
|
+
function check(subject, deps) {
|
|
9
|
+
const { drive, ctx } = deps ?? {};
|
|
10
|
+
if (typeof drive?.exportData !== "function" || typeof drive.exportPerson !== "function" || typeof drive.erasePerson !== "function" || typeof ctx !== "object" || ctx === null) throw new TypeError("driveAccount needs deps.drive and deps.ctx");
|
|
11
|
+
const known = subject?.kind === "human" || subject?.kind === "agent";
|
|
12
|
+
if (typeof subject?.id !== "string" || subject.id.trim() === "" || !known) throw new TypeError("subject needs an id and a kind of human or agent");
|
|
13
|
+
if (ctx.owner !== subject.id || ctx.actor?.id !== subject.id || ctx.actor.kind !== subject.kind) throw new DriveError("FORBIDDEN", "context does not match the subject");
|
|
14
|
+
const p = ctx.principal;
|
|
15
|
+
if (p !== void 0 && (p?.principalId !== subject.id || p.kind !== subject.kind || p.chain?.length !== 1 || p.chain[0]?.id !== subject.id || p.chain[0]?.kind !== subject.kind)) throw new DriveError("FORBIDDEN", "context does not match the subject");
|
|
16
|
+
}
|
|
17
|
+
var b64 = (data) => {
|
|
18
|
+
let bin = "";
|
|
19
|
+
for (let i = 0; i < data.length; i += 32768) bin += String.fromCharCode(...data.subarray(i, i + 32768));
|
|
20
|
+
return btoa(bin);
|
|
21
|
+
};
|
|
22
|
+
var driveAccount = {
|
|
23
|
+
id: "drive",
|
|
24
|
+
async exportAccount(subject, deps) {
|
|
25
|
+
check(subject, deps);
|
|
26
|
+
const { drive, ctx } = deps;
|
|
27
|
+
const person = await drive.exportPerson(ctx);
|
|
28
|
+
if (person.partial && subject.kind === "human") throw new DriveError("FORBIDDEN", "the person's export is partial");
|
|
29
|
+
const items = (await drive.exportData(ctx)).map(({ node, path, data, missing }) => ({ ...publicNode(node, path), data: data ? b64(data) : null, ...missing ? { missing: true } : {} }));
|
|
30
|
+
return {
|
|
31
|
+
items: plain(items),
|
|
32
|
+
authored: plain(person.authored),
|
|
33
|
+
agents: plain(person.agents),
|
|
34
|
+
grants: plain(person.shares.held),
|
|
35
|
+
invites: plain(person.shares.invitesCreated),
|
|
36
|
+
audit: plain(person.audit),
|
|
37
|
+
activity: plain(person.activity)
|
|
38
|
+
};
|
|
39
|
+
},
|
|
40
|
+
async eraseAccount(subject, deps) {
|
|
41
|
+
check(subject, deps);
|
|
42
|
+
const { removed, grants, invites, anonymized } = await deps.drive.erasePerson(deps.ctx);
|
|
43
|
+
return { items: removed, grants, invites, anonymized };
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export {
|
|
48
|
+
driveAccount
|
|
49
|
+
};
|