agentic-drive 0.1.0 → 0.2.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,4 +1,5 @@
1
1
  // src/drive.ts
2
+ import { MAX_FOLDER_DEPTH } from "agentic-identity";
2
3
  var ROOT_ID = "root";
3
4
  var NODES = "drive_nodes";
4
5
  var NAMES = "drive_names";
@@ -14,6 +15,8 @@ var DriveError = class extends Error {
14
15
  var MAX_PATH_SEGMENTS = 64;
15
16
  var DEFAULT_MAX_DEPTH = 256;
16
17
  var CHUNK = 25;
18
+ var SHARED_CHUNK = 10;
19
+ var HEIGHT_BUDGET = 200;
17
20
  var DEFAULT_URL_SECONDS = 900;
18
21
  var MAX_URL_SECONDS = 3600;
19
22
  var OPAQUE = "application/octet-stream";
@@ -59,8 +62,10 @@ function splitPath(path) {
59
62
  function batch() {
60
63
  const writes = [];
61
64
  const fresh = /* @__PURE__ */ new Set();
65
+ const depths = /* @__PURE__ */ new Map();
62
66
  return {
63
67
  fresh,
68
+ depths,
64
69
  put: (collection, id, doc) => void writes.push({ collection, id, doc }),
65
70
  remove: (collection, id) => void writes.push({ collection, id, doc: null }),
66
71
  async apply(ops) {
@@ -69,16 +74,25 @@ function batch() {
69
74
  };
70
75
  }
71
76
  var claimId = (owner, parentId, key) => sha256Hex(new TextEncoder().encode(`${owner}\0${parentId}\0${key}`));
77
+ var rootResource = async (owner) => `drive-root-${(await sha256Hex(new TextEncoder().encode(owner))).slice(0, 32)}`;
78
+ var ACTION_TO_IDENTITY = { read: "read", write: "write", delete: "admin" };
79
+ var MAX_ITEM_DEPTH = MAX_FOLDER_DEPTH - 1;
80
+ var VISITOR = { id: "", kind: "human" };
81
+ var appOf = (node) => node.kind === "ref" && node.ref ? node.ref.split(":")[0] : null;
82
+ var ERASED = "erased";
83
+ var SHARE_ERROR = /^(invalid|give an|expiresAt|expiresInMs|resource exists|the last owner)/;
72
84
  function createDrive(deps) {
73
85
  const { store, blobs } = deps;
74
86
  const now = deps.now ?? Date.now;
75
87
  const newId = deps.newId ?? (() => globalThis.crypto.randomUUID());
76
88
  const maxDepth = deps.maxDepth ?? DEFAULT_MAX_DEPTH;
77
89
  const onOrphan = deps.onOrphan ?? ((key, error) => console.error(`orphan blob ${key}: ${error instanceof Error ? error.name : "delete failed"}`));
78
- const can = deps.can ?? ((ctx, _action, node) => node.owner === ctx.owner);
79
- const rootNode = (ctx) => ({
90
+ const sharing = deps.sharing;
91
+ const resourceOf = async (node) => node.id === ROOT_ID ? rootResource(node.owner) : node.id;
92
+ const can = deps.can ?? (sharing ? async (ctx, action, node) => ctx.principal !== void 0 && (await sharing.can(ctx.principal, ACTION_TO_IDENTITY[action], await resourceOf(node))).allowed : (ctx, _action, node) => node.owner === ctx.owner);
93
+ const rootNode = (owner, by) => ({
80
94
  id: ROOT_ID,
81
- owner: ctx.owner,
95
+ owner,
82
96
  parentId: "",
83
97
  kind: "folder",
84
98
  name: "",
@@ -90,16 +104,24 @@ function createDrive(deps) {
90
104
  blobKey: null,
91
105
  createdAt: 0,
92
106
  updatedAt: 0,
93
- createdBy: ctx.actor,
94
- updatedBy: ctx.actor,
107
+ createdBy: by,
108
+ updatedBy: by,
109
+ createdById: by.id,
110
+ updatedById: by.id,
95
111
  trashed: false,
96
112
  trashedAt: null,
97
113
  purging: false
98
114
  });
99
115
  const who = (ctx) => ({ id: ctx.actor.id, kind: ctx.actor.kind });
116
+ const stamp = (ctx, node) => ({ createdById: node.createdById ?? node.createdBy.id, updatedBy: who(ctx), updatedById: ctx.actor.id });
117
+ const resourceDoc = async (node, parent) => ({ id: node.id, type: "drive", parentId: await resourceOf(parent) });
118
+ async function rawNode(ops, id, owner, by, withMarked = false) {
119
+ const node = id === ROOT_ID ? rootNode(owner, by) : await ops.get(NODES, id);
120
+ return node && (withMarked || node.purging !== true) ? node : null;
121
+ }
100
122
  async function fetch(ops, ctx, id, purgeAgain = false) {
101
- const node = id === ROOT_ID ? rootNode(ctx) : await ops.get(NODES, id);
102
- return node && (purgeAgain || node.purging !== true) && await can(ctx, "read", node) ? node : null;
123
+ const node = await rawNode(ops, id, ctx.owner, ctx.actor, purgeAgain);
124
+ return node && await can(ctx, "read", node) ? node : null;
103
125
  }
104
126
  async function need(ctx, action, node) {
105
127
  if (!await can(ctx, action, node)) throw new DriveError("FORBIDDEN", `not allowed to ${action} ${node.id}`);
@@ -119,45 +141,106 @@ function createDrive(deps) {
119
141
  const node = found && !found.trashed && found.purging !== true && found.parentId === parentId && found.nameKey === key ? found : null;
120
142
  return { taken: claim !== null, node };
121
143
  }
122
- const liveChild = async (ops, ctx, parentId, key) => (await claimed(ops, ctx.owner, parentId, key)).node;
144
+ const liveChild = async (ops, folder, key) => (await claimed(ops, folder.owner, folder.id, key)).node;
123
145
  async function isLive(ops, ctx, node) {
124
146
  for (let n = node, depth = 0; n && depth < maxDepth; depth++) {
125
147
  if (n.trashed) return false;
126
148
  if (n.id === ROOT_ID) return true;
127
- n = await fetch(ops, ctx, n.parentId);
149
+ n = await rawNode(ops, n.parentId, n.owner, ctx.actor);
128
150
  }
129
151
  return false;
130
152
  }
153
+ async function depthOf(ops, w, node) {
154
+ let depth = 0;
155
+ for (let n = node; n && n.id !== ROOT_ID; ) {
156
+ const known = w.depths.get(n.id);
157
+ if (known !== void 0) return depth + known;
158
+ if (++depth > MAX_ITEM_DEPTH) return depth;
159
+ n = await rawNode(ops, n.parentId, n.owner, node.createdBy);
160
+ if (n === null) return MAX_ITEM_DEPTH + 1;
161
+ }
162
+ return depth;
163
+ }
164
+ async function heightOf(ops, folder) {
165
+ let level = [folder];
166
+ let queries = 0;
167
+ for (let height = 1; height <= MAX_ITEM_DEPTH; height++) {
168
+ const next = [];
169
+ for (const f of level) {
170
+ for (let cursor; ; ) {
171
+ if (++queries > HEIGHT_BUDGET) throw new DriveError("TOO_MANY_ITEMS", `${folder.name} has too many folders to count. Move its parts one by one.`);
172
+ const page = await ops.query(NODES, { where: { owner: f.owner, parentId: f.id, kind: "folder" }, limit: 100, cursor });
173
+ next.push(...page.items.map((item) => item.data));
174
+ if (!page.nextCursor) break;
175
+ cursor = page.nextCursor;
176
+ }
177
+ }
178
+ if (next.length === 0) return height;
179
+ level = next;
180
+ }
181
+ return MAX_ITEM_DEPTH + 1;
182
+ }
183
+ async function heightFor(ctx, node, dest) {
184
+ if (!sharing || node.kind !== "folder") return 0;
185
+ const target = typeof dest === "string" ? await resolve(store, ctx, dest) : dest;
186
+ if (target.id === node.parentId) return 0;
187
+ const w = batch();
188
+ if (await depthOf(store, w, target) + 1 <= await depthOf(store, w, node)) return 0;
189
+ return heightOf(store, node);
190
+ }
191
+ async function noClimb(ctx, node, dest) {
192
+ const person = ctx.principal ? { ...ctx, principal: { ...ctx.principal, scopes: ["drive:admin"] } } : ctx;
193
+ if (await can(person, "delete", dest) && !await can(person, "delete", node)) {
194
+ throw new DriveError("FORBIDDEN", "moving into a folder you own needs owner on the item");
195
+ }
196
+ }
197
+ async function fitsUnder(ops, w, dest, height) {
198
+ if (sharing && await depthOf(ops, w, dest) + 1 + height > MAX_ITEM_DEPTH) {
199
+ throw new DriveError("TOO_DEEP", `an item can sit at most ${MAX_ITEM_DEPTH} folders deep`);
200
+ }
201
+ }
202
+ async function startOf(ops, ctx, loc) {
203
+ if (loc.startsWith("/")) {
204
+ const start = rootNode(ctx.owner, ctx.actor);
205
+ if (!await can(ctx, "read", start)) throw new DriveError("NOT_FOUND", `no item ${loc}`);
206
+ return { start, segs: splitPath(loc) };
207
+ }
208
+ const slash = loc.indexOf("/");
209
+ const base = await resolve(ops, ctx, loc.slice(0, slash));
210
+ if (base.kind !== "folder") throw new DriveError("NOT_A_FOLDER", `${base.name} is not a folder`);
211
+ return { start: base, segs: splitPath(`/${loc.slice(slash + 1)}`) };
212
+ }
131
213
  async function resolve(ops, ctx, loc) {
132
- if (!loc.startsWith("/")) {
214
+ if (!loc.includes("/")) {
133
215
  const node2 = await fetch(ops, ctx, loc);
134
216
  if (!node2) throw new DriveError("NOT_FOUND", `no item ${loc}`);
135
217
  if (!await isLive(ops, ctx, node2)) throw new DriveError("TRASHED", `${loc} is in the trash`);
136
218
  return node2;
137
219
  }
138
- let node = rootNode(ctx);
139
- for (const seg of splitPath(loc)) {
220
+ const { start, segs } = await startOf(ops, ctx, loc);
221
+ let node = start;
222
+ for (const seg of segs) {
140
223
  if (node.kind !== "folder") throw new DriveError("NOT_A_FOLDER", `${node.name} is not a folder`);
141
- const next = await liveChild(ops, ctx, node.id, seg.key);
224
+ const next = await liveChild(ops, node, seg.key);
142
225
  if (!next) throw new DriveError("NOT_FOUND", `no item ${loc}`);
143
226
  node = next;
144
227
  }
145
228
  if (!await can(ctx, "read", node)) throw new DriveError("NOT_FOUND", `no item ${loc}`);
146
229
  return node;
147
230
  }
148
- async function freeName(ops, ctx, parentId, name) {
231
+ async function freeName(ops, owner, parentId, name) {
149
232
  const dot = name.lastIndexOf(".");
150
233
  const [base, ext] = dot > 0 ? [name.slice(0, dot), name.slice(dot)] : [name, ""];
151
234
  for (let n = 2; ; n++) {
152
235
  const candidate = `${base} (${n})${ext}`;
153
- if (!(await claimed(ops, ctx.owner, parentId, parseName(candidate).key)).taken) return candidate;
236
+ if (!(await claimed(ops, owner, parentId, parseName(candidate).key)).taken) return candidate;
154
237
  }
155
238
  }
156
- async function folderAt(ops, w, ctx, segs, create) {
157
- let node = rootNode(ctx);
239
+ async function folderAt(ops, w, ctx, start, segs, create) {
240
+ let node = start;
158
241
  for (const seg of segs) {
159
242
  if (node.kind !== "folder") throw new DriveError("NOT_A_FOLDER", `${node.name} is not a folder`);
160
- let next = w.fresh.has(node.id) ? null : await liveChild(ops, ctx, node.id, seg.key);
243
+ let next = w.fresh.has(node.id) ? null : await liveChild(ops, node, seg.key);
161
244
  if (!next) {
162
245
  if (!create) throw new DriveError("NOT_FOUND", `no folder ${seg.name}`);
163
246
  next = (await insert(ops, w, ctx, node, seg.name, { kind: "folder" }, "fail")).node;
@@ -172,21 +255,27 @@ function createDrive(deps) {
172
255
  let { name, key } = parseName(rawName);
173
256
  const empty = w.fresh.has(parent.id);
174
257
  if (!empty) await need(ctx, "write", parent);
175
- const slot = empty ? null : await claimed(ops, ctx.owner, parent.id, key);
258
+ const slot = empty ? null : await claimed(ops, parent.owner, parent.id, key);
176
259
  if (slot?.taken) {
177
260
  if (onConflict === "replace" && slot.node?.kind === "file" && fields.kind === "file") {
178
- const node2 = { ...slot.node, ...fields, updatedAt: now(), updatedBy: who(ctx) };
261
+ const node2 = { ...slot.node, ...fields, updatedAt: now(), ...stamp(ctx, slot.node) };
179
262
  w.put(NODES, node2.id, node2);
180
263
  return { node: node2, replacedBlob: slot.node.blobKey };
181
264
  }
182
265
  if (onConflict !== "rename") throw new DriveError("NAME_TAKEN", `"${name}" already exists`);
183
- name = await freeName(ops, ctx, parent.id, name);
266
+ name = await freeName(ops, parent.owner, parent.id, name);
184
267
  key = parseName(name).key;
185
268
  }
269
+ const id = newId();
270
+ if (sharing) {
271
+ const depth = await depthOf(ops, w, parent) + 1;
272
+ if (depth > MAX_ITEM_DEPTH) throw new DriveError("TOO_DEEP", `an item can sit at most ${MAX_ITEM_DEPTH} folders deep`);
273
+ w.depths.set(id, depth);
274
+ }
186
275
  const t = now();
187
276
  const node = {
188
- id: newId(),
189
- owner: ctx.owner,
277
+ id,
278
+ owner: parent.owner,
190
279
  parentId: parent.id,
191
280
  kind: "file",
192
281
  name,
@@ -200,53 +289,62 @@ function createDrive(deps) {
200
289
  updatedAt: t,
201
290
  createdBy: who(ctx),
202
291
  updatedBy: who(ctx),
292
+ createdById: ctx.actor.id,
293
+ updatedById: ctx.actor.id,
203
294
  trashed: false,
204
295
  trashedAt: null,
205
296
  purging: false,
206
297
  ...fields
207
298
  };
208
299
  w.put(NODES, node.id, node);
209
- w.put(NAMES, await claimId(ctx.owner, parent.id, key), { nodeId: node.id, owner: ctx.owner, parentId: parent.id });
300
+ w.put(NAMES, await claimId(parent.owner, parent.id, key), { nodeId: node.id, owner: parent.owner, parentId: parent.id });
301
+ if (sharing) w.put("resources", node.id, await resourceDoc(node, parent));
210
302
  return { node, replacedBlob: null };
211
303
  }
212
304
  async function parentAndName(ops, w, ctx, path, parents) {
213
- const segs = splitPath(path);
305
+ const { start, segs } = await startOf(ops, ctx, path);
214
306
  const last = segs.pop();
215
- if (!last) throw new DriveError("IS_ROOT", "the root already exists");
216
- return { parent: await folderAt(ops, w, ctx, segs, parents), name: last.name };
307
+ if (!last) throw start.id === ROOT_ID ? new DriveError("IS_ROOT", "the root already exists") : new DriveError("INVALID_PATH", "name an item inside the folder");
308
+ return { parent: await folderAt(ops, w, ctx, start, segs, parents), name: last.name };
217
309
  }
218
310
  async function relocate(ops, w, ctx, node, destLoc, opts) {
219
311
  if (node.id === ROOT_ID) throw new DriveError("IS_ROOT", "cannot change the root");
220
- const dest = await resolve(ops, ctx, destLoc);
312
+ const dest = typeof destLoc === "string" ? await resolve(ops, ctx, destLoc) : destLoc;
221
313
  if (dest.kind !== "folder") throw new DriveError("NOT_A_FOLDER", `${dest.name} is not a folder`);
314
+ if (dest.owner !== node.owner) throw new DriveError("CROSS_DRIVE", "an item cannot move into another person's drive");
222
315
  for (let n = dest, depth = 0; n.id !== ROOT_ID; depth++) {
223
316
  if (n.id === node.id) throw new DriveError("MOVE_INTO_SELF", `cannot move ${node.name} into itself`);
224
317
  if (depth >= maxDepth) throw new DriveError("TOO_DEEP", `the destination is deeper than ${maxDepth} folders`);
225
- const parent = await fetch(ops, ctx, n.parentId);
318
+ const parent = await rawNode(ops, n.parentId, n.owner, ctx.actor);
226
319
  if (!parent) throw new DriveError("NOT_FOUND", `a parent folder of ${dest.name} is missing`);
227
320
  n = parent;
228
321
  }
229
322
  await need(ctx, "write", node);
230
- await need(ctx, "write", dest);
323
+ if (dest.id !== node.parentId) {
324
+ await need(ctx, "write", dest);
325
+ await noClimb(ctx, node, dest);
326
+ await fitsUnder(ops, w, dest, opts.height ?? 0);
327
+ }
231
328
  let { name, key } = parseName(opts.name ?? node.name);
232
- const slot = await claimed(ops, ctx.owner, dest.id, key);
329
+ const slot = await claimed(ops, dest.owner, dest.id, key);
233
330
  if (slot.taken && slot.node?.id !== node.id) {
234
331
  if (opts.onConflict !== "rename") throw new DriveError("NAME_TAKEN", `"${name}" already exists`);
235
- name = await freeName(ops, ctx, dest.id, name);
332
+ name = await freeName(ops, dest.owner, dest.id, name);
236
333
  key = parseName(name).key;
237
334
  }
238
- const moved = { ...node, parentId: dest.id, name, nameKey: key, updatedAt: now(), updatedBy: who(ctx) };
335
+ const moved = { ...node, parentId: dest.id, name, nameKey: key, updatedAt: now(), ...stamp(ctx, node) };
239
336
  w.put(NODES, moved.id, moved);
240
- const [from, to] = [await claimId(ctx.owner, node.parentId, node.nameKey), await claimId(ctx.owner, dest.id, key)];
337
+ const [from, to] = [await claimId(node.owner, node.parentId, node.nameKey), await claimId(dest.owner, dest.id, key)];
241
338
  if (from !== to) {
242
339
  if ((await ops.get(NAMES, from))?.nodeId === node.id) w.remove(NAMES, from);
243
- w.put(NAMES, to, { nodeId: moved.id, owner: ctx.owner, parentId: dest.id });
340
+ w.put(NAMES, to, { nodeId: moved.id, owner: dest.owner, parentId: dest.id });
244
341
  }
342
+ if (sharing && dest.id !== node.parentId) w.put("resources", moved.id, await resourceDoc(moved, dest));
245
343
  return moved;
246
344
  }
247
- async function isUnreferenced(ctx, blobKey) {
345
+ async function isUnreferenced(blobKey) {
248
346
  try {
249
- return (await store.query(NODES, { where: { owner: ctx.owner, blobKey }, limit: 1 })).items.length === 0;
347
+ return (await store.query(NODES, { where: { blobKey }, limit: 1 })).items.length === 0;
250
348
  } catch (error) {
251
349
  onOrphan(blobKey, error);
252
350
  return false;
@@ -259,17 +357,19 @@ function createDrive(deps) {
259
357
  onOrphan(key, error);
260
358
  }
261
359
  }
262
- async function ownerNodes(ctx, withMarked = false) {
263
- const root = rootNode(ctx);
264
- if (!await can(ctx, "read", root)) throw new DriveError("FORBIDDEN", "not allowed to read this drive");
360
+ async function rawOwnerNodes(owner, withMarked) {
265
361
  const out = [];
266
362
  for (let cursor; ; ) {
267
- const page = await store.query(NODES, { where: { owner: ctx.owner }, limit: 100, cursor });
363
+ const page = await store.query(NODES, { where: { owner }, limit: 100, cursor });
268
364
  out.push(...page.items.map((item) => item.data).filter((node) => withMarked || node.purging !== true));
269
365
  if (!page.nextCursor) return out;
270
366
  cursor = page.nextCursor;
271
367
  }
272
368
  }
369
+ async function ownerNodes(ctx, withMarked = false) {
370
+ if (!await can(ctx, "read", rootNode(ctx.owner, ctx.actor))) throw new DriveError("FORBIDDEN", "not allowed to read this drive");
371
+ return rawOwnerNodes(ctx.owner, withMarked);
372
+ }
273
373
  function pathIn(byId, node, liveOnly) {
274
374
  const names = [];
275
375
  for (let n = node, depth = 0; depth < 1e3; depth++) {
@@ -289,12 +389,72 @@ function createDrive(deps) {
289
389
  throw error;
290
390
  }
291
391
  }
392
+ function assertInPerson(ctx, what) {
393
+ if (!isInPerson(ctx) || ctx.actor.kind !== "human") throw new DriveError("FORBIDDEN", `only the owner in person can ${what}`);
394
+ }
395
+ const subjectOf = (ctx) => ({ id: ctx.owner, kind: ctx.principal?.chain[0]?.kind ?? (isInPerson(ctx) ? ctx.actor.kind : null) });
396
+ async function allWhere(ops, collection, where, orderBy) {
397
+ const out = [];
398
+ for (let cursor; ; ) {
399
+ const page = await ops.query(collection, { where, ...orderBy ? { orderBy } : {}, limit: 100, cursor });
400
+ out.push(...page.items);
401
+ if (!page.nextCursor) return out;
402
+ cursor = page.nextCursor;
403
+ }
404
+ }
405
+ const isInPerson = (ctx) => ctx.principal === void 0 ? ctx.actor.kind === "human" && ctx.actor.id === ctx.owner : ctx.principal.kind === "human" && ctx.principal.chain.length === 1 && ctx.actor.id === ctx.owner;
406
+ const MAX_ACTORS = 1e3;
407
+ async function actorsOf(ops, id) {
408
+ const ownedAgents = (await allWhere(ops, "principals", { ownerId: id })).map((p) => p.id);
409
+ const actors = [id, ...ownedAgents];
410
+ for (let depth = 0, from = 0; from < actors.length && depth < 8; depth++) {
411
+ const next = actors.length;
412
+ for (; from < next; from++) for (const child of await allWhere(ops, "principals", { parentId: actors[from] })) if (!actors.includes(child.id)) actors.push(child.id);
413
+ if (actors.length > MAX_ACTORS) throw new DriveError("TOO_MANY_ITEMS", `a person can have at most ${MAX_ACTORS} agents here`);
414
+ }
415
+ return { actors, ownedAgents };
416
+ }
417
+ async function madeBy(ops, actors) {
418
+ const made = /* @__PURE__ */ new Map();
419
+ for (const actor of actors) for (const field of ["createdById", "updatedById"]) for (const { data } of await allWhere(ops, NODES, { [field]: actor })) made.set(data.id, data);
420
+ return [...made.values()];
421
+ }
422
+ async function eraseDrive(owner) {
423
+ let removed = 0;
424
+ for (let pass = 0; pass < 5; pass++) {
425
+ const all = await rawOwnerNodes(owner, true);
426
+ if (all.length === 0) {
427
+ const rootId = await rootResource(owner);
428
+ await transact(async (tx) => {
429
+ const shares = sharing ? await sharesOf(tx, rootId) : [];
430
+ for (const share of shares) await tx.delete(share.collection, share.id);
431
+ if (sharing) await tx.delete("resources", rootId);
432
+ });
433
+ return removed;
434
+ }
435
+ removed += await removeNodes(all);
436
+ }
437
+ throw new DriveError("BUSY", "the drive kept changing while it was deleted. Run it again.");
438
+ }
439
+ async function sharesOf(ops, resourceId) {
440
+ const out = [];
441
+ for (const collection of ["grants", "links", "invites"]) {
442
+ for (let cursor; ; ) {
443
+ const page = await ops.query(collection, { where: { resourceId }, limit: 100, cursor });
444
+ out.push(...page.items.map((item) => ({ collection, id: item.id })));
445
+ if (!page.nextCursor) break;
446
+ cursor = page.nextCursor;
447
+ }
448
+ }
449
+ return out;
450
+ }
292
451
  async function removeNodes(nodes, top) {
293
452
  const unchanged = (now2, seen) => now2 !== null && now2.trashed === seen.trashed && now2.parentId === seen.parentId && now2.updatedAt === seen.updatedAt;
453
+ const size = sharing ? SHARED_CHUNK : CHUNK;
294
454
  const marked = [];
295
455
  const markedIds = /* @__PURE__ */ new Set();
296
- for (let i = 0; i < nodes.length; i += CHUNK) {
297
- const slice = nodes.slice(i, i + CHUNK);
456
+ for (let i = 0; i < nodes.length; i += size) {
457
+ const slice = nodes.slice(i, i + size);
298
458
  for (const node of slice) {
299
459
  if (node.purging === true) {
300
460
  marked.push(node);
@@ -323,23 +483,35 @@ function createDrive(deps) {
323
483
  if (top && i === 0 && !markedIds.has(top.id)) throw new DriveError("BUSY", `${top.name} changed during the purge. Nothing was deleted.`);
324
484
  }
325
485
  const rows = marked.reverse();
326
- for (let i = 0; i < rows.length; i += CHUNK) {
327
- const chunk = rows.slice(i, i + CHUNK);
486
+ for (let i = 0; i < rows.length; i += size) {
487
+ const chunk = rows.slice(i, i + size);
328
488
  for (const node of chunk) if (node.blobKey) await blobs.delete(node.blobKey);
329
489
  await transact(async (tx) => {
330
490
  const held = [];
331
491
  for (const node of chunk) {
332
492
  const claim = node.trashed ? null : await claimId(node.owner, node.parentId, node.nameKey);
333
- held.push({ node, claim, owns: claim !== null && (await tx.get(NAMES, claim))?.nodeId === node.id });
493
+ const shares = sharing ? await sharesOf(tx, node.id) : [];
494
+ held.push({ node, claim, owns: claim !== null && (await tx.get(NAMES, claim))?.nodeId === node.id, shares });
334
495
  }
335
- for (const { node, claim, owns } of held) {
496
+ for (const { node, claim, owns, shares } of held) {
336
497
  await tx.delete(NODES, node.id);
337
498
  if (claim && owns) await tx.delete(NAMES, claim);
499
+ if (sharing) await tx.delete("resources", node.id);
500
+ for (const share of shares) await tx.delete(share.collection, share.id);
338
501
  }
339
502
  });
340
503
  }
341
504
  return marked.length;
342
505
  }
506
+ async function openShared(principal, id, linkToken) {
507
+ if (!sharing) throw new DriveError("INVALID_SHARE", "sharing is not set up");
508
+ const node = await rawNode(store, id, "", VISITOR);
509
+ if (!node || node.id === ROOT_ID) throw new DriveError("NOT_FOUND", `no item ${id}`);
510
+ const decision = await sharing.can(principal, "read", id, linkToken === void 0 ? {} : { linkToken });
511
+ if (!decision.allowed) throw new DriveError("NOT_FOUND", `no item ${id}`);
512
+ if (!await isLive(store, { owner: node.owner, actor: node.createdBy }, node)) throw new DriveError("TRASHED", `${id} is in the trash`);
513
+ return node;
514
+ }
343
515
  const api = {
344
516
  async stat(ctx, loc) {
345
517
  return resolve(store, ctx, loc);
@@ -354,8 +526,11 @@ function createDrive(deps) {
354
526
  return transact(async (tx) => {
355
527
  const w = batch();
356
528
  const { parent, name } = await parentAndName(tx, w, ctx, path, opts.parents ?? false);
357
- const existing = w.fresh.has(parent.id) ? null : await liveChild(tx, ctx, parent.id, parseName(name).key);
358
- if (opts.parents && existing?.kind === "folder") return existing;
529
+ const existing = w.fresh.has(parent.id) ? null : await liveChild(tx, parent, parseName(name).key);
530
+ if (opts.parents && existing?.kind === "folder") {
531
+ if (!await can(ctx, "read", existing)) throw new DriveError("NOT_FOUND", `no item ${path}`);
532
+ return existing;
533
+ }
359
534
  const { node } = await insert(tx, w, ctx, parent, name, { kind: "folder" }, "fail");
360
535
  await w.apply(tx);
361
536
  return node;
@@ -368,7 +543,7 @@ function createDrive(deps) {
368
543
  async putFile(ctx, path, data, opts = {}) {
369
544
  const mime = opts.mime === void 0 ? null : safeMime(opts.mime);
370
545
  const hash = await sha256Hex(data);
371
- const blobKey = `${ctx.owner}/${newId()}`;
546
+ const blobKey = `blobs/${newId()}`;
372
547
  const stored = await blobs.put(blobKey, data, { ifNoneMatch: "*", ...mime ? { contentType: mime } : {} });
373
548
  if (!stored) throw new DriveError("BLOB_EXISTS", "a blob with this key already exists");
374
549
  let committed;
@@ -381,7 +556,7 @@ function createDrive(deps) {
381
556
  return result;
382
557
  });
383
558
  } catch (error) {
384
- if (await isUnreferenced(ctx, blobKey)) await dropBlob(blobKey);
559
+ if (await isUnreferenced(blobKey)) await dropBlob(blobKey);
385
560
  throw error;
386
561
  }
387
562
  if (committed.replacedBlob) await dropBlob(committed.replacedBlob);
@@ -425,9 +600,11 @@ function createDrive(deps) {
425
600
  });
426
601
  },
427
602
  async move(ctx, src, destLoc, opts = {}) {
603
+ const probe = sharing ? await resolve(store, ctx, src) : null;
604
+ const height = probe ? await heightFor(ctx, probe, destLoc) : 0;
428
605
  return transact(async (tx) => {
429
606
  const w = batch();
430
- const moved = await relocate(tx, w, ctx, await resolve(tx, ctx, src), destLoc, opts);
607
+ const moved = await relocate(tx, w, ctx, await resolve(tx, ctx, src), destLoc, { ...opts, height });
431
608
  await w.apply(tx);
432
609
  return moved;
433
610
  });
@@ -436,7 +613,10 @@ function createDrive(deps) {
436
613
  return transact(async (tx) => {
437
614
  const w = batch();
438
615
  const node = await resolve(tx, ctx, loc);
439
- const renamed = await relocate(tx, w, ctx, node, node.parentId, { name });
616
+ if (node.id === ROOT_ID) throw new DriveError("IS_ROOT", "cannot change the root");
617
+ const parent = await rawNode(tx, node.parentId, node.owner, ctx.actor);
618
+ if (!parent) throw new DriveError("NOT_FOUND", `the folder of ${node.name} is missing`);
619
+ const renamed = await relocate(tx, w, ctx, node, parent, { name });
440
620
  await w.apply(tx);
441
621
  return renamed;
442
622
  });
@@ -446,11 +626,11 @@ function createDrive(deps) {
446
626
  return transact(async (tx) => {
447
627
  const node = await resolve(tx, ctx, loc);
448
628
  if (node.id === ROOT_ID) throw new DriveError("IS_ROOT", "cannot trash the root");
449
- await need(ctx, "delete", node);
629
+ await need(ctx, "write", node);
450
630
  const claim = await claimId(node.owner, node.parentId, node.nameKey);
451
631
  const held = await tx.get(NAMES, claim);
452
632
  const t = now();
453
- const trashed = { ...node, trashed: true, trashedAt: t, updatedAt: t, updatedBy: who(ctx) };
633
+ const trashed = { ...node, trashed: true, trashedAt: t, updatedAt: t, ...stamp(ctx, node) };
454
634
  await tx.put(NODES, node.id, trashed);
455
635
  if (held?.nodeId === node.id) await tx.delete(NAMES, claim);
456
636
  return trashed;
@@ -458,31 +638,46 @@ function createDrive(deps) {
458
638
  },
459
639
  /** Items trashed on their own, newest first. */
460
640
  async listTrash(ctx) {
641
+ await need(ctx, "read", rootNode(ctx.owner, ctx.actor));
461
642
  const page = await store.query(NODES, { where: { owner: ctx.owner, trashed: true }, limit: 1e3 });
462
643
  const out = [];
463
- for (const { data } of page.items) if (data.purging !== true && await can(ctx, "read", data)) out.push(data);
644
+ for (const { data } of page.items) if (data.purging !== true) out.push(data);
464
645
  return out.sort((a, b) => (b.trashedAt ?? 0) - (a.trashedAt ?? 0));
465
646
  },
466
647
  /** Puts a trashed item back. It needs a live parent folder. */
467
648
  async restore(ctx, id, opts = {}) {
649
+ let height = 0;
650
+ if (sharing && opts.parentId !== void 0) {
651
+ const probe = await fetch(store, ctx, id);
652
+ const target = await rawNode(store, opts.parentId, ctx.owner, ctx.actor);
653
+ if (probe?.trashed && target && await can(ctx, "read", target)) height = await heightFor(ctx, probe, target);
654
+ }
468
655
  return transact(async (tx) => {
469
656
  const node = await fetch(tx, ctx, id);
470
657
  if (!node || node.id === ROOT_ID) throw new DriveError("NOT_FOUND", `no item ${id}`);
471
658
  if (!node.trashed) throw new DriveError("NOT_TRASHED", `${node.name} is not in the trash`);
472
659
  await need(ctx, "write", node);
473
- const parent = await fetch(tx, ctx, opts.parentId ?? node.parentId);
474
- if (!parent || parent.kind !== "folder" || !await isLive(tx, ctx, parent)) {
660
+ const parent = opts.parentId === void 0 ? await rawNode(tx, node.parentId, node.owner, ctx.actor) : await rawNode(tx, opts.parentId, ctx.owner, ctx.actor);
661
+ const visible = parent !== null && (opts.parentId === void 0 || await can(ctx, "read", parent));
662
+ if (!parent || !visible || parent.kind !== "folder" || !await isLive(tx, ctx, parent)) {
475
663
  throw new DriveError("PARENT_GONE", `the folder of ${node.name} is gone or in the trash`);
476
664
  }
665
+ if (parent.owner !== node.owner) throw new DriveError("CROSS_DRIVE", "an item cannot move into another person's drive");
666
+ if (parent.id !== node.parentId) {
667
+ await need(ctx, "write", parent);
668
+ await noClimb(ctx, node, parent);
669
+ await fitsUnder(tx, batch(), parent, height);
670
+ }
477
671
  let { name, key } = parseName(node.name);
478
- if ((await claimed(tx, ctx.owner, parent.id, key)).taken) {
672
+ if ((await claimed(tx, parent.owner, parent.id, key)).taken) {
479
673
  if (opts.onConflict !== "rename") throw new DriveError("NAME_TAKEN", `"${name}" already exists`);
480
- name = await freeName(tx, ctx, parent.id, name);
674
+ name = await freeName(tx, parent.owner, parent.id, name);
481
675
  key = parseName(name).key;
482
676
  }
483
- const restored = { ...node, parentId: parent.id, name, nameKey: key, trashed: false, trashedAt: null, updatedAt: now(), updatedBy: who(ctx) };
677
+ const restored = { ...node, parentId: parent.id, name, nameKey: key, trashed: false, trashedAt: null, updatedAt: now(), ...stamp(ctx, node) };
484
678
  await tx.put(NODES, restored.id, restored);
485
- await tx.put(NAMES, await claimId(ctx.owner, parent.id, key), { nodeId: restored.id, owner: ctx.owner, parentId: parent.id });
679
+ await tx.put(NAMES, await claimId(parent.owner, parent.id, key), { nodeId: restored.id, owner: parent.owner, parentId: parent.id });
680
+ if (sharing && parent.id !== node.parentId) await tx.put("resources", restored.id, await resourceDoc(restored, parent));
486
681
  return restored;
487
682
  });
488
683
  },
@@ -503,6 +698,7 @@ function createDrive(deps) {
503
698
  if (q.minSize !== void 0 && node.size < q.minSize) continue;
504
699
  if (q.maxSize !== void 0 && node.size > q.maxSize) continue;
505
700
  if (q.createdByKind && node.createdBy.kind !== q.createdByKind) continue;
701
+ if (q.app !== void 0 && appOf(node) !== q.app) continue;
506
702
  hits.push({ node, path });
507
703
  }
508
704
  return hits.sort((a, b) => a.path < b.path ? -1 : 1).slice(0, limit);
@@ -520,15 +716,9 @@ function createDrive(deps) {
520
716
  },
521
717
  /** Deletes every item and blob of the owner for good. Only the owner may do this, never an agent for them. A failed call can run again. */
522
718
  async deleteAll(ctx) {
523
- if (ctx.actor.kind !== "human" || ctx.actor.id !== ctx.owner) throw new DriveError("FORBIDDEN", "only the owner in person can delete the whole drive");
524
- await need(ctx, "delete", rootNode(ctx));
525
- let removed = 0;
526
- for (let pass = 0; pass < 5; pass++) {
527
- const all = await ownerNodes(ctx, true);
528
- if (all.length === 0) return { removed };
529
- removed += await removeNodes(all);
530
- }
531
- throw new DriveError("BUSY", "the drive kept changing while it was deleted. Run it again.");
719
+ assertInPerson(ctx, "delete the whole drive");
720
+ await need(ctx, "delete", rootNode(ctx.owner, ctx.actor));
721
+ return { removed: await eraseDrive(ctx.owner) };
532
722
  },
533
723
  /** Deletes a trashed item for good, with everything under it and all their bytes. */
534
724
  async purge(ctx, id) {
@@ -543,9 +733,233 @@ function createDrive(deps) {
543
733
  if (node.kind === "folder") queue.push(...await children(store, node.owner, node.id));
544
734
  }
545
735
  return { removed: await removeNodes(nodes, top) };
736
+ },
737
+ /**
738
+ * What the home view shows: every file and every reference to an app's document, newest change first.
739
+ * `apps` counts references per app, and `files` counts plain files, before `app` and `limit` apply.
740
+ */
741
+ async home(ctx, q = {}) {
742
+ const limit = q.limit ?? 100;
743
+ if (!Number.isInteger(limit) || limit < 1) throw new RangeError("limit must be an integer of 1 or more");
744
+ const all = await ownerNodes(ctx);
745
+ const byId = new Map(all.map((n) => [n.id, n]));
746
+ const apps = {};
747
+ let files = 0;
748
+ const items = [];
749
+ for (const node of all) {
750
+ const path = pathIn(byId, node, true);
751
+ if (path === null || node.kind === "folder") continue;
752
+ const app = appOf(node);
753
+ if (app === null) files++;
754
+ else apps[app] = (apps[app] ?? 0) + 1;
755
+ if (q.app === void 0 || app === q.app) items.push({ node, path, app });
756
+ }
757
+ items.sort((x, y) => y.node.updatedAt - x.node.updatedAt || (x.path < y.path ? -1 : 1));
758
+ return { items: items.slice(0, limit), apps, files };
759
+ },
760
+ /**
761
+ * An app puts one of its documents in this drive, as a reference such as `docs:<id>`. A document that is
762
+ * already in the drive comes back as it is, so an app can register again. The reference sits in `folder`
763
+ * (a path or a folder id, "/" by default) under `name`, and gets a free name when that one is taken.
764
+ */
765
+ async registerDocument(ctx, input) {
766
+ if (!REF.test(input.ref)) throw new DriveError("INVALID_REF", `a reference looks like "docs:<id>", got ${JSON.stringify(input.ref)}`);
767
+ parseName(input.name);
768
+ await need(ctx, "read", rootNode(ctx.owner, ctx.actor));
769
+ const dir = input.folder ?? "/";
770
+ const owner = dir.startsWith("/") ? ctx.owner : (await resolve(store, ctx, dir.split("/")[0])).owner;
771
+ for (const { data } of await allWhere(store, NODES, { owner, ref: input.ref })) {
772
+ const node = data;
773
+ if (node.kind === "ref" && !node.trashed && node.purging !== true && await isLive(store, ctx, node) && await can(ctx, "read", node)) return node;
774
+ }
775
+ return api.putRef(ctx, dir.endsWith("/") ? `${dir}${input.name}` : `${dir}/${input.name}`, input.ref, { parents: true, onConflict: "rename" });
776
+ },
777
+ /** An app deleted a document. Its references in this drive go to the trash. Returns how many. */
778
+ async forgetDocument(ctx, ref) {
779
+ if (!REF.test(ref)) throw new DriveError("INVALID_REF", `a reference looks like "docs:<id>", got ${JSON.stringify(ref)}`);
780
+ await need(ctx, "read", rootNode(ctx.owner, ctx.actor));
781
+ let trashed = 0;
782
+ for (const { data } of await allWhere(store, NODES, { owner: ctx.owner, ref })) {
783
+ if (data.kind !== "ref" || data.trashed || data.purging === true) continue;
784
+ try {
785
+ await api.trash(ctx, data.id);
786
+ trashed++;
787
+ } catch (error) {
788
+ if (!(error instanceof DriveError && error.code === "TRASHED")) throw error;
789
+ }
790
+ }
791
+ return { trashed };
792
+ },
793
+ /**
794
+ * What drive holds about the person behind a call, besides their own drive (`exportData` has that). It is
795
+ * only for the person in person: an agent gets `partial: true` and nothing else. It holds the ids of the
796
+ * items they or their agents made or changed in other drives (no names, and nothing of the people who own
797
+ * them), the shares they hold, the invites they sent, and the audit entries of what they and their agents
798
+ * did. Invitee emails and link tokens are not in it.
799
+ */
800
+ async exportPerson(ctx) {
801
+ await need(ctx, "read", rootNode(ctx.owner, ctx.actor));
802
+ const subject = subjectOf(ctx);
803
+ if (!isInPerson(ctx)) return { subject, partial: true, authored: [], agents: [], shares: { held: [], invitesCreated: [] }, audit: [] };
804
+ const { actors, ownedAgents } = await actorsOf(store, subject.id);
805
+ const authored = (await madeBy(store, actors)).filter((n) => n.owner !== subject.id && n.purging !== true).map((n) => ({
806
+ id: n.id,
807
+ kind: n.kind,
808
+ created: actors.includes(n.createdById),
809
+ updated: actors.includes(n.updatedById),
810
+ createdAt: actors.includes(n.createdById) ? n.createdAt : null,
811
+ updatedAt: actors.includes(n.updatedById) ? n.updatedAt : null,
812
+ 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))
813
+ })).sort((x, y) => x.id < y.id ? -1 : 1);
814
+ const held = [];
815
+ for (const actor of actors) for (const g of await allWhere(store, "grants", { subjectId: actor })) held.push({ subjectId: g.data.subjectId, resourceId: g.data.resourceId, role: g.data.role });
816
+ const agents = [];
817
+ for (const agent of ownedAgents) {
818
+ 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 }));
819
+ agents.push({ id: agent, items });
820
+ }
821
+ const invitesCreated = [];
822
+ const audit = [];
823
+ for (const actor of actors) {
824
+ for (const i of await allWhere(store, "invites", { createdBy: actor })) {
825
+ 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 });
826
+ }
827
+ for (const e of await allWhere(store, "audit", { principalId: actor }, { field: "at" })) {
828
+ audit.push({ at: e.data.at, action: e.data.action, resourceId: e.data.resourceId ?? null, outcome: e.data.outcome, principalId: e.data.principalId, principalKind: e.data.principalKind, machineMade: e.data.machineMade });
829
+ }
830
+ }
831
+ audit.sort((x, y) => x.at - y.at);
832
+ return { subject, partial: false, authored, agents, shares: { held, invitesCreated }, audit };
833
+ },
834
+ /**
835
+ * Erases a person for good. Only the person in person may. Their drive and its shares go. So do the shares
836
+ * they hold and the invites they or their agents sent, on drive items only: the grants of other apps stay for
837
+ * those apps and for identity. What they or their agents made in other drives stays, because it belongs to
838
+ * those drives: the id becomes "erased", and the kind stays, so machine-made work keeps its label. The audit
839
+ * trail stays: it holds ids and kinds only. A failed call can run again.
840
+ */
841
+ async erasePerson(ctx) {
842
+ assertInPerson(ctx, "erase a person");
843
+ const { id } = subjectOf(ctx);
844
+ const { actors, ownedAgents } = await actorsOf(store, id);
845
+ let { removed } = await api.deleteAll(ctx);
846
+ for (const agent of ownedAgents) removed += await eraseDrive(agent);
847
+ const ofDrive = async (docs) => {
848
+ const out = [];
849
+ for (const doc of docs) if ((await store.get("resources", doc.data.resourceId))?.type === "drive") out.push({ id: doc.id });
850
+ return out;
851
+ };
852
+ const sweep = async (docs, collection) => {
853
+ for (let i = 0; i < docs.length; i += 100) {
854
+ await transact(async (tx) => {
855
+ for (const doc of docs.slice(i, i + 100)) await tx.delete(collection, doc.id);
856
+ });
857
+ }
858
+ return docs.length;
859
+ };
860
+ let grants = 0;
861
+ let invites = 0;
862
+ for (const actor of actors) grants += await sweep(await ofDrive(await allWhere(store, "grants", { subjectId: actor })), "grants");
863
+ for (const actor of actors) invites += await sweep(await ofDrive(await allWhere(store, "invites", { createdBy: actor })), "invites");
864
+ const todo = await madeBy(store, actors);
865
+ for (let i = 0; i < todo.length; i += CHUNK) {
866
+ await transact(async (tx) => {
867
+ const current = [];
868
+ for (const node of todo.slice(i, i + CHUNK)) {
869
+ const found = await tx.get(NODES, node.id);
870
+ if (found) current.push(found);
871
+ }
872
+ for (const node of current) {
873
+ const next = { ...node };
874
+ if (actors.includes(node.createdById ?? node.createdBy.id)) Object.assign(next, { createdBy: { id: ERASED, kind: node.createdBy.kind }, createdById: ERASED });
875
+ if (actors.includes(node.updatedById ?? node.updatedBy.id)) Object.assign(next, { updatedBy: { id: ERASED, kind: node.updatedBy.kind }, updatedById: ERASED });
876
+ await tx.put(NODES, node.id, next);
877
+ }
878
+ });
879
+ }
880
+ return { removed, grants, invites, anonymized: todo.length };
881
+ },
882
+ /** Gives a person or an agent a role on an item and everything under it. Only an owner may. */
883
+ async shareWith(ctx, loc, subjectId, role) {
884
+ const resource = await resourceOf(await resolve(store, ctx, loc));
885
+ await viaSharing(ctx, (s, p) => s.grant(p, resource, subjectId, role));
886
+ },
887
+ async unshare(ctx, loc, subjectId) {
888
+ const resource = await resourceOf(await resolve(store, ctx, loc));
889
+ await viaSharing(ctx, (s, p) => s.revokeGrant(p, resource, subjectId));
890
+ },
891
+ /** Invite by `email` (a person) or `handle` (an agent). The invite lasts `expiresInMs`, if given. It sends no message. */
892
+ async invite(ctx, loc, input) {
893
+ const resource = await resourceOf(await resolve(store, ctx, loc));
894
+ return viaSharing(ctx, (s, p) => s.createInvite(p, resource, input));
895
+ },
896
+ /** `identity` is the verified email of a person, or the handle of an agent. */
897
+ async acceptInvite(ctx, inviteId, identity) {
898
+ await viaSharing(ctx, (s, p) => s.acceptInvite(p, inviteId, identity));
899
+ },
900
+ /** The token comes back once. Only its hash is stored. */
901
+ async createLink(ctx, loc, input) {
902
+ const resource = await resourceOf(await resolve(store, ctx, loc));
903
+ return viaSharing(ctx, (s, p) => s.createLink(p, resource, input));
904
+ },
905
+ async revokeLink(ctx, linkId) {
906
+ await viaSharing(ctx, (s, p) => s.revokeLink(p, linkId));
907
+ },
908
+ /** Who can reach an item, and by which links and invites. Only an owner may look. Link tokens are never shown. */
909
+ async shares(ctx, loc) {
910
+ const node = await resolve(store, ctx, loc);
911
+ await need(ctx, "delete", node);
912
+ const where = { resourceId: await resourceOf(node) };
913
+ const rows = async (collection) => (await store.query(collection, { where, limit: 100 })).items.map((item) => item.data);
914
+ return {
915
+ grants: (await rows("grants")).map((g) => ({ subjectId: g.subjectId, role: g.role })),
916
+ links: (await rows("links")).map((l) => ({ id: l.id, role: l.role, signedInOnly: l.signedInOnly === true, expiresAt: l.expiresAt ?? null, revoked: l.revokedAt != null })),
917
+ invites: (await rows("invites")).map((i) => ({ id: i.id, role: i.role, email: i.email ?? null, handle: i.handle ?? null, expiresAt: i.expiresAt ?? null, accepted: i.acceptedAt !== void 0 }))
918
+ };
919
+ },
920
+ openShared,
921
+ /** Live children of a folder that a link visitor or a signed-in person may read, sorted by name. */
922
+ async listShared(principal, id, linkToken) {
923
+ const folder = await openShared(principal, id, linkToken);
924
+ if (folder.kind !== "folder") throw new DriveError("NOT_A_FOLDER", `${folder.name} is not a folder`);
925
+ return (await children(store, folder.owner, folder.id)).filter((n) => !n.trashed && n.purging !== true).sort((x, y) => x.nameKey < y.nameKey ? -1 : x.nameKey > y.nameKey ? 1 : x.name < y.name ? -1 : 1);
926
+ },
927
+ async readShared(principal, id, linkToken) {
928
+ const node = await openShared(principal, id, linkToken);
929
+ if (node.kind !== "file" || !node.blobKey) throw new DriveError("NOT_A_FILE", `${node.name} is not a file`);
930
+ const blob = await blobs.get(node.blobKey);
931
+ if (!blob) throw new DriveError("BLOB_MISSING", `the bytes of ${node.name} are missing`);
932
+ return { node, ...blob };
546
933
  }
547
934
  };
548
- return api;
935
+ async function viaSharing(ctx, fn) {
936
+ if (!sharing) throw new DriveError("INVALID_SHARE", "sharing is not set up");
937
+ if (!ctx.principal) throw new DriveError("FORBIDDEN", "sign in to share");
938
+ try {
939
+ return await fn(sharing, ctx.principal);
940
+ } catch (error) {
941
+ const message = error instanceof Error ? error.message : "";
942
+ if (message === "forbidden") throw new DriveError("FORBIDDEN", "not allowed to share this item");
943
+ if (SHARE_ERROR.test(message)) throw new DriveError("INVALID_SHARE", message);
944
+ throw error;
945
+ }
946
+ }
947
+ async function prepare(ctx) {
948
+ if (!sharing || ctx.principal === void 0) return;
949
+ if (ctx.principal.chain[0]?.id !== ctx.owner) throw new DriveError("FORBIDDEN", "the caller does not own this drive");
950
+ const id = await rootResource(ctx.owner);
951
+ if ((await store.get("resources", id))?.type === "drive") return;
952
+ try {
953
+ await sharing.createResource(ctx.principal, { id, type: "drive" });
954
+ } catch {
955
+ }
956
+ }
957
+ const guarded = (call) => async (ctx, ...args) => {
958
+ await prepare(ctx);
959
+ return call(ctx, ...args);
960
+ };
961
+ const open = /* @__PURE__ */ new Set(["openShared", "listShared", "readShared"]);
962
+ return Object.fromEntries(Object.entries(api).map(([name, call]) => [name, open.has(name) ? call : guarded(call)]));
549
963
  }
550
964
 
551
965
  // src/zip.ts
@@ -674,7 +1088,8 @@ async function buildExport(drive, ctx, now = Date.now) {
674
1088
  ...missing ? { missing: true } : {}
675
1089
  };
676
1090
  });
677
- const manifest = { format: "agentic-drive-export", version: 1, exportedAt: now(), owner: ctx.owner, items };
1091
+ const person = await drive.exportPerson(ctx);
1092
+ 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 };
678
1093
  return writeZip([{ name: "manifest.json", data: new TextEncoder().encode(JSON.stringify(manifest, null, 2)) }, ...entries], now());
679
1094
  }
680
1095
 
@@ -921,6 +1336,7 @@ export {
921
1336
  DEFAULT_URL_SECONDS,
922
1337
  safeMime,
923
1338
  parseName,
1339
+ rootResource,
924
1340
  createDrive,
925
1341
  writeZip,
926
1342
  readZip,