agentic-drive 0.9.1 → 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 CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.10.0
4
+
5
+ ### Added
6
+
7
+ - **Account plug-in.** `driveAccount` (from `agentic-drive/account`, also exported from the root) has `exportAccount` and `eraseAccount`, so a host runs a GDPR export and erase without reading the drive storage layout. The export has `items`, `authored`, `agents`, `grants`, `invites`, `audit`, and `activity`. The erase returns counts, and a second erase counts zero.
8
+
9
+ ### Changed
10
+
11
+ - Person exports include held grants and sent invites for drive resources only. They no longer include shares for other apps. This also affects the export manifest in the SDK, CLI, MCP, and HTTP API.
12
+ - Reading or exporting an empty drive no longer creates its root. When no root or items exist, a delegated caller needs the `drive:read` scope.
13
+
3
14
  ## 0.9.1
4
15
 
5
16
  No change to the SDK, CLI, MCP or HTTP shape. Two breaking changes for hosts that pin `agentic-design` or `agentic-identity`, see Breaking.
package/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  Store, sort, and share files and folders.
9
9
 
10
- > Status: 0.9.1, early. The library, CLI, MCP server, and HTTP API work, with sharing in the SDK, the HTTP API, the CLI, and the MCP server.
10
+ > Status: 0.10.0, early. The library, CLI, MCP server, and HTTP API work, with sharing in the SDK, the HTTP API, the CLI, and the MCP server.
11
11
  > A small web UI works. See [UI](#ui).
12
12
 
13
13
  ## Live demo
@@ -163,6 +163,12 @@ The root export, `agentic-drive`, also exports these names. The package typings
163
163
  The local functions trust their caller. Local data is the trust boundary: anyone who can write the folder
164
164
  can act as any principal in it.
165
165
 
166
+ ## Account export and erase
167
+
168
+ A host that runs account export and erase across many packages calls `driveAccount` (from `agentic-drive/account`). It has `exportAccount(subject, { drive, ctx })` and `eraseAccount(subject, { drive, ctx })`, so the host does not read the storage layout. `subject` is an id and a kind (`human`, or `agent` with its own identity), and `ctx` is the drive context of that subject. A context for someone else throws `FORBIDDEN`. A bad subject, or a missing `drive` or `ctx`, throws a `TypeError`.
169
+
170
+ The checks of an export write access-log rows to `audit`, as every read does. That log is the record of access to personal data. The export writes no other data of the person. The export is plain JSON with `items` (your drive, trashed items flagged, file bytes as base64 in `data`), `authored` (items you made in other drives), `agents`, `grants` (shares you hold on drive items), `invites` (invites you sent on drive items), `audit`, and `activity` (the entries of your changes to items, with your kind). Only an agent with its own identity can be a subject: it gets its own `items` and empty collections. An agent that acts for a person is refused with `FORBIDDEN`. The erase removes your drive with its bytes, the shares you hold on drive items, and the invites you sent on drive items. It returns counts of `items`, `grants`, `invites`, and `anonymized`. `invites` counts only the invites sent on other drives: the invites on your own items go with your drive and are not counted. What you made in other drives stays, with your id set to `erased` and the kind kept. Only the person, not an agent, may erase: an agent subject gets `FORBIDDEN`. A second erase counts zero. The shape is provisional until `agentic-identity` adds its `AccountPackage` contract.
171
+
166
172
  ## UI
167
173
 
168
174
  The Worker serves a small web UI. It uses the [agentic-design](https://www.npmjs.com/package/agentic-design) package and the typed client of this package (`agentic-drive/client`). It has a folder tree, breadcrumbs, a file list and grid, upload by button or by drag and drop, new folder, rename, move, trash and restore, and a share dialog with roles and a link. An item that an agent made shows a "Created by agent" label while a person is its last editor. "Edited by" names the last editor, a person or an agent. It works with the keyboard. An automated axe check for WCAG 2.2 AA passes on each view in the light and dark themes.
@@ -0,0 +1,28 @@
1
+ import { b as Actor, D as Drive, C as Ctx } from './drive-BhMTeQ-s.js';
2
+ import 'agentic-identity';
3
+
4
+ interface DriveAccountDeps {
5
+ drive: Drive;
6
+ /** The subject's own context. The package checks that it names the subject, in person. */
7
+ ctx: Ctx;
8
+ }
9
+ type JsonRecord = Record<string, unknown>;
10
+ interface DriveAccount {
11
+ id: "drive";
12
+ /**
13
+ * Collections: `items` (the subject's own drive, trashed ones flagged, a file with its bytes as base64 in `data`),
14
+ * `authored` (ids of items the subject made or changed in other drives), `agents` (the drives of agents
15
+ * that belong to the subject, with no bytes), `grants` (shares the subject holds on drive items), `invites` (invites the subject sent on drive items),
16
+ * `audit` (what the subject did), and `activity` (the entries of the subject's changes to items). Only an agent with its own identity can be a subject: it gets its own `items` and empty collections, and the export does not say that these are partial. An agent that acts for a person has a context for that person, and is refused.
17
+ */
18
+ exportAccount(subject: Actor, deps: DriveAccountDeps): Promise<Record<"items" | "authored" | "agents" | "grants" | "invites" | "audit" | "activity", JsonRecord[]>>;
19
+ /**
20
+ * Erases the subject's drive with all its bytes, the shares they hold, and the invites they sent, on drive items only.
21
+ * What the subject made in other drives stays, with the id set to "erased" and the kind kept. Counts by `items`, `grants`,
22
+ * `invites`, and `anonymized`. `invites` counts only the invites on items of other drives: the ones on the subject's own items go with the drive, and are not counted. A repeat counts what is left, so it is zero after a full run. Only the person, not an agent, may erase: an agent subject is refused.
23
+ */
24
+ eraseAccount(subject: Actor, deps: DriveAccountDeps): Promise<Record<"items" | "grants" | "invites" | "anonymized", number>>;
25
+ }
26
+ declare const driveAccount: DriveAccount;
27
+
28
+ export { type DriveAccount, type DriveAccountDeps, driveAccount };
@@ -0,0 +1,7 @@
1
+ import {
2
+ driveAccount
3
+ } from "./chunk-ECVUDAHL.js";
4
+ import "./chunk-DB4F5EWD.js";
5
+ export {
6
+ driveAccount
7
+ };
@@ -1,5 +1,5 @@
1
1
  // src/drive.ts
2
- import { EraseRefusedError, isMachineMade, 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";
@@ -477,6 +477,16 @@ function createDrive(deps) {
477
477
  if (!await can(ctx, "read", rootNode(ctx.owner, ctx.actor))) throw new DriveError("FORBIDDEN", "not allowed to read this drive");
478
478
  return rawOwnerNodes(ctx.owner, withMarked);
479
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
+ }
480
490
  function pathIn(byId, node, liveOnly) {
481
491
  const names = [];
482
492
  for (let n = node, depth = 0; depth < 1e3; depth++) {
@@ -886,7 +896,8 @@ function createDrive(deps) {
886
896
  },
887
897
  /** Every item of the owner, trashed or not, with its original path and bytes. For export. */
888
898
  async exportData(ctx) {
889
- const all = await ownerNodes(ctx);
899
+ await readOwnerRoot(ctx);
900
+ const all = await rawOwnerNodes(ctx.owner, false);
890
901
  const byId = new Map(all.map((n) => [n.id, n]));
891
902
  const out = [];
892
903
  for (const node of all.sort((a, b) => a.id < b.id ? -1 : 1)) {
@@ -1030,11 +1041,11 @@ function createDrive(deps) {
1030
1041
  * What drive holds about the person behind a call, besides their own drive (`exportData` has that). It is
1031
1042
  * only for the person in person: an agent gets `partial: true` and nothing else. It holds the ids of the
1032
1043
  * items they or their agents made or changed in other drives (no names, and nothing of the people who own
1033
- * them), the shares they hold, the invites they sent, and the audit entries of what they and their agents
1034
- * 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.
1035
1046
  */
1036
1047
  async exportPerson(ctx) {
1037
- await need(ctx, "read", rootNode(ctx.owner, ctx.actor));
1048
+ await readOwnerRoot(ctx);
1038
1049
  const subject = subjectOf(ctx);
1039
1050
  if (!isInPerson(ctx)) return { subject, partial: true, authored: [], agents: [], shares: { held: [], invitesCreated: [] }, audit: [], activity: [] };
1040
1051
  const { actors, ownedAgents } = await actorsOf(store, subject.id);
@@ -1047,8 +1058,9 @@ function createDrive(deps) {
1047
1058
  updatedAt: actors.includes(n.updatedById) ? n.updatedAt : null,
1048
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))
1049
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";
1050
1062
  const held = [];
1051
- 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 });
1052
1064
  const agents = [];
1053
1065
  for (const agent of ownedAgents) {
1054
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 }));
@@ -1058,6 +1070,7 @@ function createDrive(deps) {
1058
1070
  const audit = [];
1059
1071
  for (const actor of actors) {
1060
1072
  for (const i of await allWhere(store, "invites", { createdBy: actor })) {
1073
+ if (!await onDrive(i.data.resourceId)) continue;
1061
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 });
1062
1075
  }
1063
1076
  for (const e of await allWhere(store, "audit", { principalId: actor }, { field: "at" })) {
@@ -1223,9 +1236,10 @@ function createDrive(deps) {
1223
1236
  throw error;
1224
1237
  }
1225
1238
  }
1226
- async function prepare(ctx) {
1239
+ async function prepare(ctx, create = true) {
1227
1240
  if (!sharing || ctx.principal === void 0) return;
1228
1241
  if (ctx.principal.chain[0]?.id !== ctx.owner) throw new DriveError("FORBIDDEN", "the caller does not own this drive");
1242
+ if (!create) return;
1229
1243
  const id = await rootResource(ctx.owner);
1230
1244
  if ((await store.get("resources", id))?.type === "drive") return;
1231
1245
  try {
@@ -1233,203 +1247,13 @@ function createDrive(deps) {
1233
1247
  } catch {
1234
1248
  }
1235
1249
  }
1236
- const guarded = (call) => async (ctx, ...args) => {
1237
- await prepare(ctx);
1250
+ const guarded = (call, create = true) => async (ctx, ...args) => {
1251
+ await prepare(ctx, create);
1238
1252
  return call(ctx, ...args);
1239
1253
  };
1240
1254
  const open = /* @__PURE__ */ new Set(["openShared", "listShared", "readShared"]);
1241
- return Object.fromEntries(Object.entries(api).map(([name, call]) => [name, open.has(name) ? call : guarded(call)]));
1242
- }
1243
-
1244
- // src/upload.ts
1245
- var DEFAULT_MAX_BYTES = 25 * 1024 * 1024;
1246
- var SIGNATURES = [
1247
- [[137, 80, 78, 71, 13, 10, 26, 10], "image/png"],
1248
- [[255, 216, 255], "image/jpeg"],
1249
- [[71, 73, 70, 56], "image/gif"],
1250
- [[37, 80, 68, 70, 45], "application/pdf"],
1251
- [[80, 75, 3, 4], "application/zip"],
1252
- [[31, 139], "application/gzip"]
1253
- ];
1254
- function sniffMime(head) {
1255
- const hit = SIGNATURES.find(([sig]) => sig.every((byte, i) => head[i] === byte));
1256
- if (hit) return hit[1];
1257
- const isWebp = [..."RIFF"].every((c, i) => head[i] === c.charCodeAt(0)) && [..."WEBP"].every((c, i) => head[8 + i] === c.charCodeAt(0));
1258
- return isWebp ? "image/webp" : null;
1259
- }
1260
- function looksLikeText(head) {
1261
- if (head.includes(0)) return false;
1262
- for (let cut = 0; cut < 4 && cut <= head.length; cut++) {
1263
- try {
1264
- new TextDecoder("utf-8", { fatal: true }).decode(head.subarray(0, head.length - cut));
1265
- return true;
1266
- } catch {
1267
- continue;
1268
- }
1269
- }
1270
- return false;
1271
- }
1272
- async function* iterate(source) {
1273
- if (Symbol.asyncIterator in source) {
1274
- yield* source;
1275
- return;
1276
- }
1277
- const reader = source.getReader();
1278
- try {
1279
- for (; ; ) {
1280
- const { done, value } = await reader.read();
1281
- if (done) return;
1282
- yield value;
1283
- }
1284
- } finally {
1285
- reader.releaseLock();
1286
- }
1287
- }
1288
- async function upload(drive, ctx, path, source, opts = {}) {
1289
- const max = opts.maxBytes ?? DEFAULT_MAX_BYTES;
1290
- const parts = [];
1291
- let size = 0;
1292
- for await (const chunk of iterate(source)) {
1293
- size += chunk.byteLength;
1294
- if (size > max) throw new DriveError("TOO_LARGE", `upload is over the limit of ${max} bytes`);
1295
- parts.push(chunk);
1296
- }
1297
- const data = new Uint8Array(size);
1298
- for (let at = 0, i = 0; i < parts.length; at += parts[i].byteLength, i++) data.set(parts[i], at);
1299
- const head = data.subarray(0, 512);
1300
- const mime = sniffMime(head) ?? (opts.mime !== void 0 ? safeMime(opts.mime) : looksLikeText(head) ? "text/plain" : "application/octet-stream");
1301
- return drive.putFile(ctx, path, data, { mime, parents: opts.parents, onConflict: opts.onConflict });
1302
- }
1303
-
1304
- // src/zip.ts
1305
- var CRC_TABLE = Array.from({ length: 256 }, (_, n) => {
1306
- let c = n;
1307
- for (let k = 0; k < 8; k++) c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
1308
- return c >>> 0;
1309
- });
1310
- function crc32(data) {
1311
- let crc = 4294967295;
1312
- for (const byte of data) crc = CRC_TABLE[(crc ^ byte) & 255] ^ crc >>> 8;
1313
- return (crc ^ 4294967295) >>> 0;
1314
- }
1315
- function safeEntryName(name) {
1316
- if (name === "" || /[\\\u0000]/.test(name) || name.startsWith("/") || /^[a-zA-Z]:/.test(name)) return false;
1317
- return name.split("/").every((part) => part !== "" && part !== "." && part !== "..");
1318
- }
1319
- function writeZip(entries, at) {
1320
- for (const { name } of entries) if (!safeEntryName(name)) throw new Error(`zip: unsafe entry name ${JSON.stringify(name)}`);
1321
- const d = new Date(at);
1322
- const time = d.getUTCHours() << 11 | d.getUTCMinutes() << 5 | d.getUTCSeconds() >> 1;
1323
- const date = Math.max(d.getUTCFullYear(), 1980) - 1980 << 9 | d.getUTCMonth() + 1 << 5 | d.getUTCDate();
1324
- const parts = [];
1325
- const central = [];
1326
- let offset = 0;
1327
- const u16 = (v, o, n) => v.setUint16(o, n, true);
1328
- const u32 = (v, o, n) => v.setUint32(o, n, true);
1329
- for (const entry of entries) {
1330
- const name = new TextEncoder().encode(entry.name);
1331
- const crc = crc32(entry.data);
1332
- if (entry.data.byteLength >= 4294967295 || offset >= 4294967295 || entries.length >= 65535) throw new Error("zip: export is too large for a plain zip");
1333
- const local = new DataView(new ArrayBuffer(30));
1334
- u32(local, 0, 67324752);
1335
- u16(local, 4, 20);
1336
- u16(local, 6, 2048);
1337
- u16(local, 8, 0);
1338
- u16(local, 10, time);
1339
- u16(local, 12, date);
1340
- u32(local, 14, crc);
1341
- u32(local, 18, entry.data.byteLength);
1342
- u32(local, 22, entry.data.byteLength);
1343
- u16(local, 26, name.length);
1344
- u16(local, 28, 0);
1345
- const head = new DataView(new ArrayBuffer(46));
1346
- u32(head, 0, 33639248);
1347
- u16(head, 4, 20);
1348
- u16(head, 6, 20);
1349
- u16(head, 8, 2048);
1350
- u16(head, 10, 0);
1351
- u16(head, 12, time);
1352
- u16(head, 14, date);
1353
- u32(head, 16, crc);
1354
- u32(head, 20, entry.data.byteLength);
1355
- u32(head, 24, entry.data.byteLength);
1356
- u16(head, 28, name.length);
1357
- u32(head, 42, offset);
1358
- parts.push(new Uint8Array(local.buffer), name, entry.data);
1359
- central.push(new Uint8Array(head.buffer), name);
1360
- offset += 30 + name.length + entry.data.byteLength;
1361
- }
1362
- const size = central.reduce((n, p) => n + p.byteLength, 0);
1363
- const end = new DataView(new ArrayBuffer(22));
1364
- u32(end, 0, 101010256);
1365
- u16(end, 8, entries.length);
1366
- u16(end, 10, entries.length);
1367
- u32(end, 12, size);
1368
- u32(end, 16, offset);
1369
- const all = [...parts, ...central, new Uint8Array(end.buffer)];
1370
- const out = new Uint8Array(all.reduce((n, p) => n + p.byteLength, 0));
1371
- let at2 = 0;
1372
- for (const p of all) {
1373
- out.set(p, at2);
1374
- at2 += p.byteLength;
1375
- }
1376
- return out;
1377
- }
1378
- function readZip(bytes) {
1379
- const v = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
1380
- let end = bytes.byteLength - 22;
1381
- while (end >= 0 && v.getUint32(end, true) !== 101010256) end--;
1382
- if (end < 0) throw new Error("zip: no end signature");
1383
- const count = v.getUint16(end + 10, true);
1384
- const out = /* @__PURE__ */ new Map();
1385
- for (let i = 0, at = v.getUint32(end + 16, true); i < count; i++) {
1386
- if (v.getUint32(at, true) !== 33639248) throw new Error("zip: bad central signature");
1387
- if (v.getUint16(at + 10, true) !== 0) throw new Error("zip: compressed entries are not supported");
1388
- const crc = v.getUint32(at + 16, true);
1389
- const size = v.getUint32(at + 24, true);
1390
- const nameLen = v.getUint16(at + 28, true);
1391
- const local = v.getUint32(at + 42, true);
1392
- const name = new TextDecoder().decode(bytes.subarray(at + 46, at + 46 + nameLen));
1393
- if (v.getUint32(local, true) !== 67324752) throw new Error("zip: bad local signature");
1394
- const start = local + 30 + v.getUint16(local + 26, true) + v.getUint16(local + 28, true);
1395
- const data = bytes.slice(start, start + size);
1396
- if (crc32(data) !== crc) throw new Error(`zip: crc mismatch in ${name}`);
1397
- out.set(name, data);
1398
- at += 46 + nameLen + v.getUint16(at + 30, true) + v.getUint16(at + 32, true);
1399
- }
1400
- return out;
1401
- }
1402
-
1403
- // src/export.ts
1404
- async function buildExport(drive, ctx, now = Date.now) {
1405
- const entries = [];
1406
- const items = (await drive.exportData(ctx)).map(({ node, path, data, missing }) => {
1407
- const file = data ? `files/${node.id}` : null;
1408
- if (data && file) entries.push({ name: file, data });
1409
- return {
1410
- id: node.id,
1411
- owner: node.owner,
1412
- path,
1413
- parentId: node.parentId,
1414
- kind: node.kind,
1415
- name: node.name,
1416
- size: node.size,
1417
- mime: node.mime,
1418
- hash: node.hash,
1419
- ref: node.ref,
1420
- createdAt: node.createdAt,
1421
- updatedAt: node.updatedAt,
1422
- createdBy: node.createdBy,
1423
- updatedBy: node.updatedBy,
1424
- trashed: node.trashed,
1425
- trashedAt: node.trashedAt,
1426
- file,
1427
- ...missing ? { missing: true } : {}
1428
- };
1429
- });
1430
- const person = await drive.exportPerson(ctx);
1431
- 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, activity: person.activity };
1432
- 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))]));
1433
1257
  }
1434
1258
 
1435
1259
  // src/auth.ts
@@ -1589,12 +1413,6 @@ export {
1589
1413
  subjectKindOf,
1590
1414
  parseSubject,
1591
1415
  createDrive,
1592
- DEFAULT_MAX_BYTES,
1593
- sniffMime,
1594
- upload,
1595
- writeZip,
1596
- readZip,
1597
- buildExport,
1598
1416
  authenticate,
1599
1417
  inviteIdentity,
1600
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
+ };
@@ -0,0 +1,204 @@
1
+ import {
2
+ DriveError,
3
+ safeMime
4
+ } from "./chunk-DB4F5EWD.js";
5
+
6
+ // src/upload.ts
7
+ var DEFAULT_MAX_BYTES = 25 * 1024 * 1024;
8
+ var SIGNATURES = [
9
+ [[137, 80, 78, 71, 13, 10, 26, 10], "image/png"],
10
+ [[255, 216, 255], "image/jpeg"],
11
+ [[71, 73, 70, 56], "image/gif"],
12
+ [[37, 80, 68, 70, 45], "application/pdf"],
13
+ [[80, 75, 3, 4], "application/zip"],
14
+ [[31, 139], "application/gzip"]
15
+ ];
16
+ function sniffMime(head) {
17
+ const hit = SIGNATURES.find(([sig]) => sig.every((byte, i) => head[i] === byte));
18
+ if (hit) return hit[1];
19
+ const isWebp = [..."RIFF"].every((c, i) => head[i] === c.charCodeAt(0)) && [..."WEBP"].every((c, i) => head[8 + i] === c.charCodeAt(0));
20
+ return isWebp ? "image/webp" : null;
21
+ }
22
+ function looksLikeText(head) {
23
+ if (head.includes(0)) return false;
24
+ for (let cut = 0; cut < 4 && cut <= head.length; cut++) {
25
+ try {
26
+ new TextDecoder("utf-8", { fatal: true }).decode(head.subarray(0, head.length - cut));
27
+ return true;
28
+ } catch {
29
+ continue;
30
+ }
31
+ }
32
+ return false;
33
+ }
34
+ async function* iterate(source) {
35
+ if (Symbol.asyncIterator in source) {
36
+ yield* source;
37
+ return;
38
+ }
39
+ const reader = source.getReader();
40
+ try {
41
+ for (; ; ) {
42
+ const { done, value } = await reader.read();
43
+ if (done) return;
44
+ yield value;
45
+ }
46
+ } finally {
47
+ reader.releaseLock();
48
+ }
49
+ }
50
+ async function upload(drive, ctx, path, source, opts = {}) {
51
+ const max = opts.maxBytes ?? DEFAULT_MAX_BYTES;
52
+ const parts = [];
53
+ let size = 0;
54
+ for await (const chunk of iterate(source)) {
55
+ size += chunk.byteLength;
56
+ if (size > max) throw new DriveError("TOO_LARGE", `upload is over the limit of ${max} bytes`);
57
+ parts.push(chunk);
58
+ }
59
+ const data = new Uint8Array(size);
60
+ for (let at = 0, i = 0; i < parts.length; at += parts[i].byteLength, i++) data.set(parts[i], at);
61
+ const head = data.subarray(0, 512);
62
+ const mime = sniffMime(head) ?? (opts.mime !== void 0 ? safeMime(opts.mime) : looksLikeText(head) ? "text/plain" : "application/octet-stream");
63
+ return drive.putFile(ctx, path, data, { mime, parents: opts.parents, onConflict: opts.onConflict });
64
+ }
65
+
66
+ // src/zip.ts
67
+ var CRC_TABLE = Array.from({ length: 256 }, (_, n) => {
68
+ let c = n;
69
+ for (let k = 0; k < 8; k++) c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
70
+ return c >>> 0;
71
+ });
72
+ function crc32(data) {
73
+ let crc = 4294967295;
74
+ for (const byte of data) crc = CRC_TABLE[(crc ^ byte) & 255] ^ crc >>> 8;
75
+ return (crc ^ 4294967295) >>> 0;
76
+ }
77
+ function safeEntryName(name) {
78
+ if (name === "" || /[\\\u0000]/.test(name) || name.startsWith("/") || /^[a-zA-Z]:/.test(name)) return false;
79
+ return name.split("/").every((part) => part !== "" && part !== "." && part !== "..");
80
+ }
81
+ function writeZip(entries, at) {
82
+ for (const { name } of entries) if (!safeEntryName(name)) throw new Error(`zip: unsafe entry name ${JSON.stringify(name)}`);
83
+ const d = new Date(at);
84
+ const time = d.getUTCHours() << 11 | d.getUTCMinutes() << 5 | d.getUTCSeconds() >> 1;
85
+ const date = Math.max(d.getUTCFullYear(), 1980) - 1980 << 9 | d.getUTCMonth() + 1 << 5 | d.getUTCDate();
86
+ const parts = [];
87
+ const central = [];
88
+ let offset = 0;
89
+ const u16 = (v, o, n) => v.setUint16(o, n, true);
90
+ const u32 = (v, o, n) => v.setUint32(o, n, true);
91
+ for (const entry of entries) {
92
+ const name = new TextEncoder().encode(entry.name);
93
+ const crc = crc32(entry.data);
94
+ if (entry.data.byteLength >= 4294967295 || offset >= 4294967295 || entries.length >= 65535) throw new Error("zip: export is too large for a plain zip");
95
+ const local = new DataView(new ArrayBuffer(30));
96
+ u32(local, 0, 67324752);
97
+ u16(local, 4, 20);
98
+ u16(local, 6, 2048);
99
+ u16(local, 8, 0);
100
+ u16(local, 10, time);
101
+ u16(local, 12, date);
102
+ u32(local, 14, crc);
103
+ u32(local, 18, entry.data.byteLength);
104
+ u32(local, 22, entry.data.byteLength);
105
+ u16(local, 26, name.length);
106
+ u16(local, 28, 0);
107
+ const head = new DataView(new ArrayBuffer(46));
108
+ u32(head, 0, 33639248);
109
+ u16(head, 4, 20);
110
+ u16(head, 6, 20);
111
+ u16(head, 8, 2048);
112
+ u16(head, 10, 0);
113
+ u16(head, 12, time);
114
+ u16(head, 14, date);
115
+ u32(head, 16, crc);
116
+ u32(head, 20, entry.data.byteLength);
117
+ u32(head, 24, entry.data.byteLength);
118
+ u16(head, 28, name.length);
119
+ u32(head, 42, offset);
120
+ parts.push(new Uint8Array(local.buffer), name, entry.data);
121
+ central.push(new Uint8Array(head.buffer), name);
122
+ offset += 30 + name.length + entry.data.byteLength;
123
+ }
124
+ const size = central.reduce((n, p) => n + p.byteLength, 0);
125
+ const end = new DataView(new ArrayBuffer(22));
126
+ u32(end, 0, 101010256);
127
+ u16(end, 8, entries.length);
128
+ u16(end, 10, entries.length);
129
+ u32(end, 12, size);
130
+ u32(end, 16, offset);
131
+ const all = [...parts, ...central, new Uint8Array(end.buffer)];
132
+ const out = new Uint8Array(all.reduce((n, p) => n + p.byteLength, 0));
133
+ let at2 = 0;
134
+ for (const p of all) {
135
+ out.set(p, at2);
136
+ at2 += p.byteLength;
137
+ }
138
+ return out;
139
+ }
140
+ function readZip(bytes) {
141
+ const v = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
142
+ let end = bytes.byteLength - 22;
143
+ while (end >= 0 && v.getUint32(end, true) !== 101010256) end--;
144
+ if (end < 0) throw new Error("zip: no end signature");
145
+ const count = v.getUint16(end + 10, true);
146
+ const out = /* @__PURE__ */ new Map();
147
+ for (let i = 0, at = v.getUint32(end + 16, true); i < count; i++) {
148
+ if (v.getUint32(at, true) !== 33639248) throw new Error("zip: bad central signature");
149
+ if (v.getUint16(at + 10, true) !== 0) throw new Error("zip: compressed entries are not supported");
150
+ const crc = v.getUint32(at + 16, true);
151
+ const size = v.getUint32(at + 24, true);
152
+ const nameLen = v.getUint16(at + 28, true);
153
+ const local = v.getUint32(at + 42, true);
154
+ const name = new TextDecoder().decode(bytes.subarray(at + 46, at + 46 + nameLen));
155
+ if (v.getUint32(local, true) !== 67324752) throw new Error("zip: bad local signature");
156
+ const start = local + 30 + v.getUint16(local + 26, true) + v.getUint16(local + 28, true);
157
+ const data = bytes.slice(start, start + size);
158
+ if (crc32(data) !== crc) throw new Error(`zip: crc mismatch in ${name}`);
159
+ out.set(name, data);
160
+ at += 46 + nameLen + v.getUint16(at + 30, true) + v.getUint16(at + 32, true);
161
+ }
162
+ return out;
163
+ }
164
+
165
+ // src/export.ts
166
+ async function buildExport(drive, ctx, now = Date.now) {
167
+ const entries = [];
168
+ const items = (await drive.exportData(ctx)).map(({ node, path, data, missing }) => {
169
+ const file = data ? `files/${node.id}` : null;
170
+ if (data && file) entries.push({ name: file, data });
171
+ return {
172
+ id: node.id,
173
+ owner: node.owner,
174
+ path,
175
+ parentId: node.parentId,
176
+ kind: node.kind,
177
+ name: node.name,
178
+ size: node.size,
179
+ mime: node.mime,
180
+ hash: node.hash,
181
+ ref: node.ref,
182
+ createdAt: node.createdAt,
183
+ updatedAt: node.updatedAt,
184
+ createdBy: node.createdBy,
185
+ updatedBy: node.updatedBy,
186
+ trashed: node.trashed,
187
+ trashedAt: node.trashedAt,
188
+ file,
189
+ ...missing ? { missing: true } : {}
190
+ };
191
+ });
192
+ const person = await drive.exportPerson(ctx);
193
+ 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, activity: person.activity };
194
+ return writeZip([{ name: "manifest.json", data: new TextEncoder().encode(JSON.stringify(manifest, null, 2)) }, ...entries], now());
195
+ }
196
+
197
+ export {
198
+ DEFAULT_MAX_BYTES,
199
+ sniffMime,
200
+ upload,
201
+ writeZip,
202
+ readZip,
203
+ buildExport
204
+ };
@@ -1,6 +1,6 @@
1
1
  // src/version.ts
2
2
  import { createRequire } from "module";
3
- var VERSION = true ? "0.9.1" : readPackageVersion();
3
+ var VERSION = true ? "0.10.0" : readPackageVersion();
4
4
 
5
5
  export {
6
6
  VERSION
package/dist/cli.js CHANGED
@@ -4,13 +4,16 @@ import {
4
4
  } from "./chunk-AB2RTTHJ.js";
5
5
  import {
6
6
  VERSION
7
- } from "./chunk-D6KW6NVM.js";
7
+ } from "./chunk-NHHBNW64.js";
8
+ import {
9
+ buildExport,
10
+ upload
11
+ } from "./chunk-HJXRMOUY.js";
8
12
  import {
9
13
  AuthError,
10
14
  DriveError,
11
15
  NO_TOKEN,
12
16
  addPrincipal,
13
- buildExport,
14
17
  ensureSharing,
15
18
  inviteIdentity,
16
19
  issueKey,
@@ -18,9 +21,8 @@ import {
18
21
  publicNode,
19
22
  resolveToken,
20
23
  revokeKey,
21
- revokePrincipal,
22
- upload
23
- } from "./chunk-622NAJ6K.js";
24
+ revokePrincipal
25
+ } from "./chunk-DB4F5EWD.js";
24
26
 
25
27
  // src/cli.ts
26
28
  import { createReadStream, realpathSync, writeFileSync } from "fs";
package/dist/client.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { E as ErrorCode, N as Node, A as ActivityEntry, S as SearchQuery } from './drive-BRg1Uj_3.js';
2
- export { a as ActivityAction } from './drive-BRg1Uj_3.js';
1
+ import { E as ErrorCode, N as Node, A as ActivityEntry, S as SearchQuery } from './drive-BhMTeQ-s.js';
2
+ export { a as ActivityAction } from './drive-BhMTeQ-s.js';
3
3
  import 'agentic-identity';
4
4
 
5
5
  type Role = "viewer" | "commenter" | "editor" | "owner";
@@ -1,5 +1,5 @@
1
1
  import * as agentic_identity from 'agentic-identity';
2
- import { PrincipalKind, PrincipalContext, Store, Blobs, createSharing, Subject, Role, SubjectKind } from 'agentic-identity';
2
+ import { PrincipalKind, Store, Blobs, PrincipalContext, createSharing, Subject, Role, SubjectKind } from 'agentic-identity';
3
3
 
4
4
  declare const ROOT_ID = "root";
5
5
  declare const ACTOR_KINDS: readonly ["human", "agent_of_human", "agent_of_agent", "agent", "user_token"];
@@ -242,8 +242,8 @@ declare function createDrive(deps: DriveDeps): {
242
242
  * What drive holds about the person behind a call, besides their own drive (`exportData` has that). It is
243
243
  * only for the person in person: an agent gets `partial: true` and nothing else. It holds the ids of the
244
244
  * items they or their agents made or changed in other drives (no names, and nothing of the people who own
245
- * them), the shares they hold, the invites they sent, and the audit entries of what they and their agents
246
- * did. Invitee emails and link tokens are not in it.
245
+ * them), the shares they hold and the invites they sent on drive items, and the audit entries of what they and
246
+ * their agents did. Invitee emails and link tokens are not in it.
247
247
  */
248
248
  exportPerson(ctx: Ctx): Promise<{
249
249
  subject: {
package/dist/index.d.ts CHANGED
@@ -1,7 +1,8 @@
1
- import { O as OnConflict, D as Drive, C as Ctx, N as Node, b as Actor } from './drive-BRg1Uj_3.js';
2
- export { c as ACTOR_KINDS, d as Action, a as ActivityAction, A as ActivityEntry, e as ActorKind, f as Can, g as DriveDeps, h as DriveError, i as ERROR_STATUS, E as ErrorCode, R as ROOT_ID, S as SearchQuery, j as createDrive, p as parseName, k as parseSubject, s as subjectKindOf } from './drive-BRg1Uj_3.js';
1
+ import { O as OnConflict, D as Drive, C as Ctx, N as Node, b as Actor } from './drive-BhMTeQ-s.js';
2
+ export { c as ACTOR_KINDS, d as Action, a as ActivityAction, A as ActivityEntry, e as ActorKind, f as Can, g as DriveDeps, h as DriveError, i as ERROR_STATUS, E as ErrorCode, R as ROOT_ID, S as SearchQuery, j as createDrive, p as parseName, k as parseSubject, s as subjectKindOf } from './drive-BhMTeQ-s.js';
3
3
  import { Store, Blobs, Doc } from 'agentic-identity';
4
4
  export { B as BackfillOptions, a as BackfillResult, b as backfillAuthorIds, c as backfillResources } from './migrate-B79gRPMu.js';
5
+ export { DriveAccount, DriveAccountDeps, driveAccount } from './account.js';
5
6
 
6
7
  declare const VERSION: string;
7
8
 
package/dist/index.js CHANGED
@@ -1,35 +1,40 @@
1
+ import {
2
+ driveAccount
3
+ } from "./chunk-ECVUDAHL.js";
1
4
  import {
2
5
  importLegacyLocal
3
6
  } from "./chunk-AB2RTTHJ.js";
4
7
  import {
5
8
  VERSION
6
- } from "./chunk-D6KW6NVM.js";
9
+ } from "./chunk-NHHBNW64.js";
10
+ import {
11
+ DEFAULT_MAX_BYTES,
12
+ buildExport,
13
+ readZip,
14
+ sniffMime,
15
+ upload,
16
+ writeZip
17
+ } from "./chunk-HJXRMOUY.js";
7
18
  import {
8
19
  ACTOR_KINDS,
9
20
  AuthError,
10
- DEFAULT_MAX_BYTES,
11
21
  DriveError,
12
22
  ERROR_STATUS,
13
23
  ROOT_ID,
14
24
  addPrincipal,
15
25
  backfillAuthorIds,
16
26
  backfillResources,
17
- buildExport,
18
27
  createDrive,
19
28
  issueKey,
20
29
  openLocal,
21
30
  parseName,
22
31
  parseSubject,
23
32
  publicNode,
24
- readZip,
25
33
  resolveToken,
26
34
  revokeKey,
27
35
  revokePrincipal,
28
- sniffMime,
29
- subjectKindOf,
30
- upload,
31
- writeZip
32
- } from "./chunk-622NAJ6K.js";
36
+ subjectKindOf
37
+ } from "./chunk-DB4F5EWD.js";
33
38
  export {
34
39
  ACTOR_KINDS,
35
40
  AuthError,
@@ -43,6 +48,7 @@ export {
43
48
  backfillResources,
44
49
  buildExport,
45
50
  createDrive,
51
+ driveAccount,
46
52
  importLegacyLocal,
47
53
  issueKey,
48
54
  openLocal,
package/dist/mcp.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- import { D as Drive, C as Ctx } from './drive-BRg1Uj_3.js';
2
+ import { D as Drive, C as Ctx } from './drive-BhMTeQ-s.js';
3
3
  import { Store } from 'agentic-identity';
4
4
 
5
5
  declare function createMcpServer(drive: Drive, ctx: Ctx, exportDir: string, store: Store): McpServer;
package/dist/mcp.js CHANGED
@@ -1,14 +1,16 @@
1
1
  import {
2
2
  VERSION
3
- } from "./chunk-D6KW6NVM.js";
3
+ } from "./chunk-NHHBNW64.js";
4
+ import {
5
+ buildExport,
6
+ upload
7
+ } from "./chunk-HJXRMOUY.js";
4
8
  import {
5
9
  ACTOR_KINDS,
6
10
  DriveError,
7
- buildExport,
8
11
  inviteIdentity,
9
- publicNode,
10
- upload
11
- } from "./chunk-622NAJ6K.js";
12
+ publicNode
13
+ } from "./chunk-DB4F5EWD.js";
12
14
 
13
15
  // src/mcp.ts
14
16
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
package/dist/worker.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { Hono } from 'hono';
2
2
  import { D1Like, R2Like } from 'agentic-identity/cloudflare';
3
3
  import { Store, Blobs } from 'agentic-identity';
4
- import { C as Ctx, D as Drive } from './drive-BRg1Uj_3.js';
5
- export { i as ERROR_STATUS, E as ErrorCode, N as Node } from './drive-BRg1Uj_3.js';
4
+ import { C as Ctx, D as Drive } from './drive-BhMTeQ-s.js';
5
+ export { i as ERROR_STATUS, E as ErrorCode, N as Node } from './drive-BhMTeQ-s.js';
6
6
  export { B as BackfillOptions, a as BackfillResult, b as backfillAuthorIds, c as backfillResources } from './migrate-B79gRPMu.js';
7
7
 
8
8
  /**
package/dist/worker.js CHANGED
@@ -1,18 +1,20 @@
1
1
  import {
2
2
  DEFAULT_MAX_BYTES,
3
+ buildExport,
4
+ upload
5
+ } from "./chunk-HJXRMOUY.js";
6
+ import {
3
7
  DEFAULT_URL_SECONDS,
4
8
  DriveError,
5
9
  ERROR_STATUS,
6
10
  authenticate,
7
11
  backfillAuthorIds,
8
12
  backfillResources,
9
- buildExport,
10
13
  createDrive,
11
14
  inviteIdentity,
12
15
  publicNode,
13
- safeMime,
14
- upload
15
- } from "./chunk-622NAJ6K.js";
16
+ safeMime
17
+ } from "./chunk-DB4F5EWD.js";
16
18
 
17
19
  // src/worker.ts
18
20
  import { Hono } from "hono";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-drive",
3
- "version": "0.9.1",
3
+ "version": "0.10.0",
4
4
  "description": "Store, sort, and share files and folders.",
5
5
  "keywords": [
6
6
  "drive",
@@ -45,6 +45,10 @@
45
45
  "types": "./dist/index.d.ts",
46
46
  "import": "./dist/index.js"
47
47
  },
48
+ "./account": {
49
+ "types": "./dist/account.d.ts",
50
+ "import": "./dist/account.js"
51
+ },
48
52
  "./client": {
49
53
  "types": "./dist/client.d.ts",
50
54
  "import": "./dist/client.js"