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.
@@ -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
- const all = await ownerNodes(ctx);
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, the invites they sent, and the audit entries of what they and their agents
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 need(ctx, "read", rootNode(ctx.owner, ctx.actor));
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
- return { subject, partial: false, authored, agents, shares: { held, invitesCreated }, audit };
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 resource = await resourceOf(await resolve(store, ctx, loc));
1051
- await viaSharing(ctx, (s, p) => s.grant(p, resource, parseSubject(subject), role));
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 resource = await resourceOf(await resolve(store, ctx, loc));
1055
- await viaSharing(ctx, (s, p) => s.revokeGrant(p, resource, parseSubject(subject)));
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
- return Object.fromEntries(Object.entries(api).map(([name, call]) => [name, open.has(name) ? call : guarded(call)]));
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 shown = { ...rest, machineMade: isMachineMade(node.createdBy.kind) };
1466
- return path === void 0 ? shown : { path, ...shown };
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
+ };