agentic-drive 0.5.0 → 0.6.1

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,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.1
4
+
5
+ ### Changed
6
+
7
+ - **Bearer tokens are checked by the agentic-identity HTTP helper.** The Worker and the local key check now call `requirePrincipal` from `agentic-identity/http` (agentic-identity 0.6). The package no longer has its own copy of the bearer-token check in `src/auth.ts` and `src/local.ts`. A request without an `authorization` header still gets no caller, so a session cookie does not sign in. A bad key still gives 401, and `resolveToken` still throws `AuthError` with the code `UNAUTHENTICATED`.
8
+ - **The dependency is agentic-identity 0.6.** The package now needs `agentic-identity` `^0.6.0`.
9
+
10
+ ## 0.6.0
11
+
12
+ ### Added
13
+
14
+ - **`createApp` takes an injected store, blobs, and auth.** `createApp({ store, blobs, auth })` from `agentic-drive/worker` lets a host serve the drive routes with its own `Store`, `Blobs`, and sign-in callback. Each part may be a function of the Worker env. With no options, the Worker works as before. A Worker with no `DB` or `FILES` binding and no injected store or blobs now answers 503 `NOT_CONFIGURED`. The package also exports the `AppOptions` type.
15
+ - **Mountable UI at `agentic-drive/ui`.** `mount(element, { baseUrl, getToken })` renders the drive UI in any page and returns `{ update, unmount }`. `getToken` runs before each request. The styles are in `agentic-drive/ui.css`. The embed keeps its folder in memory, so it does not change the page address. `react` and `react-dom` 19 are optional peer dependencies, and `agentic-design` is now a dependency.
16
+
3
17
  ## 0.5.0
4
18
 
5
19
  Read the breaking changes before you upgrade or deploy.
package/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  Store, sort, and share files and folders.
9
9
 
10
- > Status: 0.5.0, early. The library, CLI, MCP server, and HTTP API work, with sharing in the SDK and the HTTP API.
10
+ > Status: 0.6.1, early. The library, CLI, MCP server, and HTTP API work, with sharing in the SDK and the HTTP API.
11
11
  > A small web UI works. See [UI](#ui).
12
12
 
13
13
  ## Live demo
@@ -47,18 +47,7 @@ await drive.trash(me, "/reports/2026");
47
47
 
48
48
  ## CLI and MCP
49
49
 
50
- ```sh
51
- npx agentic-drive mkdir -p /reports
52
- npx agentic-drive upload ./q1.pdf /reports/
53
- npx agentic-drive search --name q1
54
- npx agentic-drive export ./everything.zip
55
- npx agentic-drive mcp
56
- ```
57
-
58
- Every command prints JSON. A failure prints `{"error":{"code","message"}}` on stderr and exits 1.
59
- Exit code 2 means bad usage. Exit code 3 means no principal, or a key that is not valid.
60
-
61
- Every command except `help`, `version`, `principal`, and `key` needs a principal. Pass an API key with
50
+ Every command except `help`, `--version`, `principal`, and `key` needs a principal. Pass an API key with
62
51
  `--token <key>` or `AGENTIC_DRIVE_TOKEN`. Prefer the variable: a flag shows in `ps`.
63
52
  `agentic-identity` resolves the key. The drive belongs to the person at the top of the
64
53
  principal's chain. An agent key marks each item it makes as made by a machine. A revoked,
@@ -73,10 +62,21 @@ npx agentic-drive key revoke <key>
73
62
  npx agentic-drive principal revoke helper # stops its keys, and its agents
74
63
  ```
75
64
 
76
- `principal` and `key` need no key: anyone who can read the `--dir` folder can run them.
65
+ `principal` and `key` need no key: anyone who can write to the `--dir` folder can run them.
77
66
  Set `--dir` (or `AGENTIC_DRIVE_DIR`) to choose the data folder.
78
67
  The local drive checks the owner only. It does not apply the scopes of an agent key.
79
68
 
69
+ ```sh
70
+ npx agentic-drive mkdir -p /reports
71
+ npx agentic-drive upload ./q1.pdf /reports/
72
+ npx agentic-drive search --name q1
73
+ npx agentic-drive export ./everything.zip
74
+ npx agentic-drive mcp
75
+ ```
76
+
77
+ Every command prints JSON, except `help`, `--version`, `mcp`, and `download` to `-`. A failure prints `{"error":{"code","message"}}` on stderr and exits 1.
78
+ Exit code 2 means bad usage. Exit code 3 means no principal, or a key that is not valid.
79
+
80
80
  ## HTTP API
81
81
 
82
82
  A Cloudflare Worker (Hono) serves `/api/v1/drive` and mirrors the CLI. It uses D1 and R2
@@ -133,6 +133,35 @@ try {
133
133
  - A browser page needs the Worker on its own origin for now: the Worker sends no CORS headers.
134
134
  - Share with a subject: `share("/pub", { kind: "human", id: "u2" }, "viewer")`.
135
135
 
136
+ ### Other exports
137
+
138
+ The root export, `agentic-drive`, also exports these names. The package typings give the type of each one.
139
+
140
+ - `VERSION` is the package version. `ROOT_ID` is the id of a drive's root folder, `"root"`. `ACTOR_KINDS` lists
141
+ the five principal kinds that `agentic-identity` names.
142
+ - `DEFAULT_MAX_BYTES` is the upload cap: 25 MiB (26,214,400 bytes). `sniffMime(head)` returns the type that
143
+ the first bytes name: PNG, JPEG, GIF, PDF, ZIP, gzip, or WebP. It returns `null` for any other bytes.
144
+ - `parseName(raw)` returns `{ name, key }`. It normalizes the name to NFC, and `key` is the lowercase name.
145
+ It throws a `DriveError` with the code `INVALID_NAME` for a blank name, `.`, `..`, a lone surrogate, a `/`,
146
+ a control character (U+0000 to U+001F, or U+007F), or a name over 255 bytes in UTF-8. `parseSubject(input)` returns `{ kind, id }` for a valid subject, and throws
147
+ `INVALID_SHARE` for any other input. `subjectKindOf(kind)` returns `human` or `agent`, and throws
148
+ `INVALID_SHARE` for any other kind.
149
+ - A `DriveError` has a `code` from `ErrorCode`, such as `NOT_FOUND` or `NAME_TAKEN`.
150
+ - `readZip(bytes)` reads a zip that `writeZip` made, with stored (uncompressed) entries only. It returns a `Map`
151
+ from each entry name to its bytes. It throws for a compressed entry. `writeZip(entries, at)` writes a zip with
152
+ one `{ name, data }` entry for each file, and `at` sets the time in milliseconds. It throws for an unsafe
153
+ name, such as one with `..` or a leading `/`.
154
+ - The local drive has the functions behind the CLI. `openLocal(dir)` returns `{ drive, store }` for a folder.
155
+ `addPrincipal(store, id, { parent?, scopes? })` makes a principal. `issueKey(store, principalId, label?)`
156
+ returns the key. Keep it: the store keeps no plain copy. `resolveToken(store, token)` returns the caller, or throws `AuthError`
157
+ with the code `UNAUTHENTICATED`. `revokeKey(store, token)` stops one key. `revokePrincipal(store, id)` stops a
158
+ principal, its keys, and every agent below it.
159
+ `publicNode(node, path?)` returns an item without its storage keys and its `purging` flag, and adds `machineMade`. When `path` is
160
+ given, it also adds `path`.
161
+
162
+ The local functions trust their caller. Local data is the trust boundary: anyone who can write the folder
163
+ can act as any principal in it.
164
+
136
165
  ## UI
137
166
 
138
167
  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.
@@ -155,6 +184,26 @@ Run `npm run build:ui` before `npm run test:e2e` or `wrangler deploy`, because t
155
184
 
156
185
  Set `DRIVE_PUBLIC_URL` to the origin that serves the UI. A download link on another origin is refused.
157
186
 
187
+ ### Mount the UI in your app
188
+
189
+ `agentic-drive/ui` renders the same UI inside your own page. It needs no framework in your app. You give it an element, the address of a drive server, and a function that returns the bearer token. `mount` returns a handle.
190
+
191
+ ```ts
192
+ import "agentic-design/styles.css"; // tokens, fonts, and base styles for body, headings, and links
193
+ import "agentic-drive/ui.css"; // the drive styles
194
+ import { mount } from "agentic-drive/ui";
195
+
196
+ const drive = mount(document.getElementById("drive")!, {
197
+ baseUrl: "https://drive.example.com",
198
+ getToken: () => session.token, // runs before every request, so it can refresh
199
+ });
200
+
201
+ drive.update({ baseUrl, getToken }); // use another server or callback
202
+ drive.unmount(); // remove the UI
203
+ ```
204
+
205
+ `getToken` returns 1 to 512 letters, digits, `-` or `_`, or `undefined` to send no `authorization` header. A bad `baseUrl` or a `getToken` that is not a function makes `mount` throw a `TypeError`. The server still checks every request against the token. The embed shows files and trash with upload, rename, move, share, and the "Created by agent" label. It has no sign-in form, because your app owns sign-in. It does not change the page address, and it has no theme switch. `agentic-design/styles.css` sets base styles for the whole page, and it follows the dark mode of the system unless your page sets `data-theme`. Add `agentic-design` to your own dependencies, because your page imports its files. The Worker sends no CORS headers, so the server must be on the origin of your page. Set `baseUrl` to the same address as `DRIVE_PUBLIC_URL`, or the UI refuses its download links. `unmount` and `update` abort the requests that run and stop an upload that has more files to send. The UI keeps the list or grid choice in `localStorage` under `agentic-drive-view`, and it has its own heading for each view. Dialogs, menus, and messages render in `document.body`, outside your element. A click with Ctrl, Cmd, Shift, or the middle button on a folder link does not open the folder. The class names start with `drive-`. `DriveBrowser` is the same UI as a React component. It closes its requests when it unmounts. Wrap it in `TooltipProvider` and `ToastProvider` from `agentic-design/react`. `react` and `react-dom` (version 19 or later) are optional peer dependencies. Install them in your project when you use `agentic-drive/ui`. The web UI that the Worker serves uses the same views.
206
+
158
207
  ### Serve the UI from the package
159
208
 
160
209
  The npm package ships the built UI in `ui-dist`. The package does not ship a `wrangler.jsonc`, so a project that deploys its own Worker adds the assets block to its config. Its Worker re-exports the package Worker. In a workspace, the package can sit in the root `node_modules`. Then point `assets.directory` at that folder, such as `../../node_modules/agentic-drive/ui-dist`:
@@ -190,6 +239,30 @@ The [seed script](https://github.com/pooriaarab/agentic-drive/blob/main/e2e/serv
190
239
 
191
240
  The UI does not make snapshot links yet. The address of a share link shows once, when you create it.
192
241
 
242
+ ### Inject a store, blobs, and sign-in
243
+
244
+ A host that has its own Worker can serve the drive routes from it. `createApp` from `agentic-drive/worker` takes the parts to use. Each part is optional. A part may be a function of the Worker env, which runs on each request.
245
+
246
+ ```ts
247
+ import { createApp } from "agentic-drive/worker";
248
+
249
+ const app = createApp({
250
+ store: (env) => myStore(env), // a Store of agentic-identity
251
+ blobs: (env) => myBlobs(env), // a Blobs of agentic-identity
252
+ // Return a Ctx for the caller, or null for a 401.
253
+ auth: async (request, store, env) => {
254
+ const principal = await mySignIn(request, env);
255
+ return principal && { owner: principal.chain[0].id, actor: { id: principal.principalId, kind: principal.kind }, principal };
256
+ },
257
+ });
258
+ export default { fetch: app.fetch };
259
+ ```
260
+
261
+ - With `store`, the Worker reads no `DB` binding. It does not create the table either, so the host owns the schema.
262
+ - With `blobs`, the Worker reads no `FILES` binding and needs no `DRIVE_PUBLIC_URL`. The blobs must sign download URLs that point to `/api/v1/drive/blob` on this Worker. That route checks the signature with `DRIVE_URL_SECRET` before it reads the store or the blobs, so set that secret.
263
+ - With `auth`, the Worker does not read bearer tokens from the store. The callback decides who the caller is. Return a whole `Ctx`: a `principal`, and an `owner` and `actor` that match it. Any other value gets 401. Make sure `principal.chain` and `principal.scopes` are real, because the drive checks them. A callback that reads a session cookie must check the request origin itself. Every write route is open to a cross-site form until it does.
264
+ - With no options, `createApp()` works as before: D1, R2, and bearer tokens from the store. `export { default } from "agentic-drive/worker"` is the same.
265
+
193
266
  ## Features
194
267
 
195
268
  - Folders, files, and typed references to documents in other apps (`docs:<id>`).
@@ -217,7 +290,7 @@ The UI does not make snapshot links yet. The address of a share link shows once,
217
290
  - `registerDocument` makes one item per document in a drive. Two calls at the same moment can make two.
218
291
 
219
292
  - Drive reads the `store_docs` table in D1 and `store.jsonl` in the CLI folder. It does not read older layouts. There is no import and no claim backfill.
220
- - Uploads buffer the stream in memory up to the cap. The default cap is 25 MiB.
293
+ - Uploads buffer the stream in memory up to the cap. The default cap is 25 MiB (`DEFAULT_MAX_BYTES`).
221
294
  - Local file blobs return a `file:` URL that nothing serves or checks.
222
295
  - Drive writes each new blob only when its key is free (`ifNoneMatch`). A write that finds the key taken fails with `BLOB_EXISTS`.
223
296
  - Only `upload()` enforces the size cap. A caller of `putFile()` must check size first.
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  ROOT_ID,
3
3
  rootResource
4
- } from "./chunk-T6C5Y2WA.js";
4
+ } from "./chunk-EAKQK7ST.js";
5
5
 
6
6
  // src/migrate.ts
7
7
  var PAGE = 10;
@@ -94,7 +94,8 @@ function parseSubject(input) {
94
94
  var SHARE_ERROR = /^(invalid|give an|expiresAt|expiresInMs|resource exists|the last owner)/;
95
95
  function createDrive(deps) {
96
96
  const { store, blobs } = deps;
97
- const now = deps.now ?? Date.now;
97
+ let last = 0;
98
+ const now = deps.now ?? (() => last = Math.max(Date.now(), last + 1));
98
99
  const newId = deps.newId ?? (() => globalThis.crypto.randomUUID());
99
100
  const maxDepth = deps.maxDepth ?? DEFAULT_MAX_DEPTH;
100
101
  const onOrphan = deps.onOrphan ?? ((key, error) => console.error(`orphan blob ${key}: ${error instanceof Error ? error.name : "delete failed"}`));
@@ -314,9 +315,9 @@ function createDrive(deps) {
314
315
  }
315
316
  async function parentAndName(ops, w, ctx, path, parents) {
316
317
  const { start, segs } = await startOf(ops, ctx, path);
317
- const last = segs.pop();
318
- 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");
319
- return { parent: await folderAt(ops, w, ctx, start, segs, parents), name: last.name };
318
+ const last2 = segs.pop();
319
+ if (!last2) throw start.id === ROOT_ID ? new DriveError("IS_ROOT", "the root already exists") : new DriveError("INVALID_PATH", "name an item inside the folder");
320
+ return { parent: await folderAt(ops, w, ctx, start, segs, parents), name: last2.name };
320
321
  }
321
322
  async function relocate(ops, w, ctx, node, destLoc, opts) {
322
323
  if (node.id === ROOT_ID) throw new DriveError("IS_ROOT", "cannot change the root");
@@ -991,137 +992,6 @@ function createDrive(deps) {
991
992
  return Object.fromEntries(Object.entries(api).map(([name, call]) => [name, open.has(name) ? call : guarded(call)]));
992
993
  }
993
994
 
994
- // src/zip.ts
995
- var CRC_TABLE = Array.from({ length: 256 }, (_, n) => {
996
- let c = n;
997
- for (let k = 0; k < 8; k++) c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
998
- return c >>> 0;
999
- });
1000
- function crc32(data) {
1001
- let crc = 4294967295;
1002
- for (const byte of data) crc = CRC_TABLE[(crc ^ byte) & 255] ^ crc >>> 8;
1003
- return (crc ^ 4294967295) >>> 0;
1004
- }
1005
- function safeEntryName(name) {
1006
- if (name === "" || /[\\\u0000]/.test(name) || name.startsWith("/") || /^[a-zA-Z]:/.test(name)) return false;
1007
- return name.split("/").every((part) => part !== "" && part !== "." && part !== "..");
1008
- }
1009
- function writeZip(entries, at) {
1010
- for (const { name } of entries) if (!safeEntryName(name)) throw new Error(`zip: unsafe entry name ${JSON.stringify(name)}`);
1011
- const d = new Date(at);
1012
- const time = d.getUTCHours() << 11 | d.getUTCMinutes() << 5 | d.getUTCSeconds() >> 1;
1013
- const date = Math.max(d.getUTCFullYear(), 1980) - 1980 << 9 | d.getUTCMonth() + 1 << 5 | d.getUTCDate();
1014
- const parts = [];
1015
- const central = [];
1016
- let offset = 0;
1017
- const u16 = (v, o, n) => v.setUint16(o, n, true);
1018
- const u32 = (v, o, n) => v.setUint32(o, n, true);
1019
- for (const entry of entries) {
1020
- const name = new TextEncoder().encode(entry.name);
1021
- const crc = crc32(entry.data);
1022
- if (entry.data.byteLength >= 4294967295 || offset >= 4294967295 || entries.length >= 65535) throw new Error("zip: export is too large for a plain zip");
1023
- const local = new DataView(new ArrayBuffer(30));
1024
- u32(local, 0, 67324752);
1025
- u16(local, 4, 20);
1026
- u16(local, 6, 2048);
1027
- u16(local, 8, 0);
1028
- u16(local, 10, time);
1029
- u16(local, 12, date);
1030
- u32(local, 14, crc);
1031
- u32(local, 18, entry.data.byteLength);
1032
- u32(local, 22, entry.data.byteLength);
1033
- u16(local, 26, name.length);
1034
- u16(local, 28, 0);
1035
- const head = new DataView(new ArrayBuffer(46));
1036
- u32(head, 0, 33639248);
1037
- u16(head, 4, 20);
1038
- u16(head, 6, 20);
1039
- u16(head, 8, 2048);
1040
- u16(head, 10, 0);
1041
- u16(head, 12, time);
1042
- u16(head, 14, date);
1043
- u32(head, 16, crc);
1044
- u32(head, 20, entry.data.byteLength);
1045
- u32(head, 24, entry.data.byteLength);
1046
- u16(head, 28, name.length);
1047
- u32(head, 42, offset);
1048
- parts.push(new Uint8Array(local.buffer), name, entry.data);
1049
- central.push(new Uint8Array(head.buffer), name);
1050
- offset += 30 + name.length + entry.data.byteLength;
1051
- }
1052
- const size = central.reduce((n, p) => n + p.byteLength, 0);
1053
- const end = new DataView(new ArrayBuffer(22));
1054
- u32(end, 0, 101010256);
1055
- u16(end, 8, entries.length);
1056
- u16(end, 10, entries.length);
1057
- u32(end, 12, size);
1058
- u32(end, 16, offset);
1059
- const all = [...parts, ...central, new Uint8Array(end.buffer)];
1060
- const out = new Uint8Array(all.reduce((n, p) => n + p.byteLength, 0));
1061
- let at2 = 0;
1062
- for (const p of all) {
1063
- out.set(p, at2);
1064
- at2 += p.byteLength;
1065
- }
1066
- return out;
1067
- }
1068
- function readZip(bytes) {
1069
- const v = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
1070
- let end = bytes.byteLength - 22;
1071
- while (end >= 0 && v.getUint32(end, true) !== 101010256) end--;
1072
- if (end < 0) throw new Error("zip: no end signature");
1073
- const count = v.getUint16(end + 10, true);
1074
- const out = /* @__PURE__ */ new Map();
1075
- for (let i = 0, at = v.getUint32(end + 16, true); i < count; i++) {
1076
- if (v.getUint32(at, true) !== 33639248) throw new Error("zip: bad central signature");
1077
- if (v.getUint16(at + 10, true) !== 0) throw new Error("zip: compressed entries are not supported");
1078
- const crc = v.getUint32(at + 16, true);
1079
- const size = v.getUint32(at + 24, true);
1080
- const nameLen = v.getUint16(at + 28, true);
1081
- const local = v.getUint32(at + 42, true);
1082
- const name = new TextDecoder().decode(bytes.subarray(at + 46, at + 46 + nameLen));
1083
- if (v.getUint32(local, true) !== 67324752) throw new Error("zip: bad local signature");
1084
- const start = local + 30 + v.getUint16(local + 26, true) + v.getUint16(local + 28, true);
1085
- const data = bytes.slice(start, start + size);
1086
- if (crc32(data) !== crc) throw new Error(`zip: crc mismatch in ${name}`);
1087
- out.set(name, data);
1088
- at += 46 + nameLen + v.getUint16(at + 30, true) + v.getUint16(at + 32, true);
1089
- }
1090
- return out;
1091
- }
1092
-
1093
- // src/export.ts
1094
- async function buildExport(drive, ctx, now = Date.now) {
1095
- const entries = [];
1096
- const items = (await drive.exportData(ctx)).map(({ node, path, data, missing }) => {
1097
- const file = data ? `files/${node.id}` : null;
1098
- if (data && file) entries.push({ name: file, data });
1099
- return {
1100
- id: node.id,
1101
- owner: node.owner,
1102
- path,
1103
- parentId: node.parentId,
1104
- kind: node.kind,
1105
- name: node.name,
1106
- size: node.size,
1107
- mime: node.mime,
1108
- hash: node.hash,
1109
- ref: node.ref,
1110
- createdAt: node.createdAt,
1111
- updatedAt: node.updatedAt,
1112
- createdBy: node.createdBy,
1113
- updatedBy: node.updatedBy,
1114
- trashed: node.trashed,
1115
- trashedAt: node.trashedAt,
1116
- file,
1117
- ...missing ? { missing: true } : {}
1118
- };
1119
- });
1120
- const person = await drive.exportPerson(ctx);
1121
- 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 };
1122
- return writeZip([{ name: "manifest.json", data: new TextEncoder().encode(JSON.stringify(manifest, null, 2)) }, ...entries], now());
1123
- }
1124
-
1125
995
  // src/adapters/fs.ts
1126
996
  import { createHash } from "crypto";
1127
997
  import { appendFileSync, closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, truncateSync, writeFileSync, writeSync } from "fs";
@@ -1283,66 +1153,6 @@ function createFileBlobs(dir, options = {}) {
1283
1153
  };
1284
1154
  }
1285
1155
 
1286
- // src/auth.ts
1287
- import { createStoreAuth } from "agentic-identity";
1288
- async function authenticate(request, store, now) {
1289
- if (!/^Bearer [A-Za-z0-9_-]{1,512}$/.test(request.headers.get("authorization") ?? "")) return null;
1290
- const principal = await createStoreAuth(store, now ? { now } : {}).authenticate(request);
1291
- return principal ? { owner: principal.chain[0].id, actor: { id: principal.principalId, kind: principal.kind }, principal } : null;
1292
- }
1293
-
1294
- // src/local.ts
1295
- import { createHash as createHash2 } from "crypto";
1296
- import { join as join2 } from "path";
1297
- import { isMachineMade, isScope, issueCredential, revokeCredential } from "agentic-identity";
1298
- function openLocal(dir) {
1299
- const store = createFileStore(join2(dir, "db"));
1300
- return { store, drive: createDrive({ store, blobs: createFileBlobs(join2(dir, "blobs")) }) };
1301
- }
1302
- var AuthError = class extends Error {
1303
- code = "UNAUTHENTICATED";
1304
- };
1305
- var NO_TOKEN = "no principal: pass --token <key> or set AGENTIC_DRIVE_TOKEN to an API key";
1306
- var BAD_TOKEN = "the key is not valid: it is unknown, revoked, or expired, or its principal is revoked";
1307
- async function resolveToken(store, token) {
1308
- if (!token) throw new AuthError(NO_TOKEN);
1309
- if (!/^[A-Za-z0-9_-]{1,512}$/.test(token)) throw new AuthError(BAD_TOKEN);
1310
- const ctx = await authenticate(new Request("http://local", { headers: { authorization: `Bearer ${token}` } }), store);
1311
- if (!ctx) throw new AuthError(BAD_TOKEN);
1312
- return ctx;
1313
- }
1314
- async function addPrincipal(store, id, opts) {
1315
- if (!/^[A-Za-z0-9_.@-]{1,128}$/.test(id)) throw new Error(`invalid principal id "${id}"`);
1316
- const scopes = opts.scopes?.length ? opts.scopes : [opts.parent ? "drive:write" : "drive:admin"];
1317
- const bad = scopes.find((s) => !isScope(s));
1318
- if (bad !== void 0) throw new Error(`invalid scope "${bad}"`);
1319
- if (await store.get("principals", id)) throw new Error(`principal "${id}" exists`);
1320
- if (opts.parent && !await store.get("principals", opts.parent)) throw new Error(`parent "${opts.parent}" does not exist`);
1321
- const parent = opts.parent ? await store.get("principals", opts.parent) : null;
1322
- if (parent && parent.kind !== "human") throw new Error(`parent "${opts.parent}" is not a person`);
1323
- const doc = opts.parent ? { id, kind: "agent_of_human", parentId: opts.parent, scopes } : { id, kind: "human", scopes };
1324
- await store.put("principals", id, doc);
1325
- return doc;
1326
- }
1327
- async function revokePrincipal(store, id) {
1328
- const doc = await store.get("principals", id);
1329
- if (!doc) throw new DriveError("NOT_FOUND", `principal "${id}" does not exist`);
1330
- await store.put("principals", id, { ...doc, revokedAt: Date.now() });
1331
- }
1332
- async function issueKey(store, principalId, label) {
1333
- if (!await store.get("principals", principalId)) throw new Error(`principal "${principalId}" does not exist`);
1334
- return issueCredential(store, { principalId, kind: "api_key", ...label ? { label } : {} });
1335
- }
1336
- async function revokeKey(store, token) {
1337
- if (!await store.get("credentials", createHash2("sha256").update(token).digest("hex"))) throw new DriveError("NOT_FOUND", "no such key");
1338
- await revokeCredential(store, token, Date.now());
1339
- }
1340
- function publicNode(node, path) {
1341
- const { nameKey: _nameKey, blobKey: _blobKey, purging: _purging, ...rest } = node;
1342
- const shown = { ...rest, machineMade: isMachineMade(node.createdBy.kind) };
1343
- return path === void 0 ? shown : { path, ...shown };
1344
- }
1345
-
1346
1156
  // src/upload.ts
1347
1157
  var DEFAULT_MAX_BYTES = 25 * 1024 * 1024;
1348
1158
  var SIGNATURES = [
@@ -1403,6 +1213,204 @@ async function upload(drive, ctx, path, source, opts = {}) {
1403
1213
  return drive.putFile(ctx, path, data, { mime, parents: opts.parents, onConflict: opts.onConflict });
1404
1214
  }
1405
1215
 
1216
+ // src/zip.ts
1217
+ var CRC_TABLE = Array.from({ length: 256 }, (_, n) => {
1218
+ let c = n;
1219
+ for (let k = 0; k < 8; k++) c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
1220
+ return c >>> 0;
1221
+ });
1222
+ function crc32(data) {
1223
+ let crc = 4294967295;
1224
+ for (const byte of data) crc = CRC_TABLE[(crc ^ byte) & 255] ^ crc >>> 8;
1225
+ return (crc ^ 4294967295) >>> 0;
1226
+ }
1227
+ function safeEntryName(name) {
1228
+ if (name === "" || /[\\\u0000]/.test(name) || name.startsWith("/") || /^[a-zA-Z]:/.test(name)) return false;
1229
+ return name.split("/").every((part) => part !== "" && part !== "." && part !== "..");
1230
+ }
1231
+ function writeZip(entries, at) {
1232
+ for (const { name } of entries) if (!safeEntryName(name)) throw new Error(`zip: unsafe entry name ${JSON.stringify(name)}`);
1233
+ const d = new Date(at);
1234
+ const time = d.getUTCHours() << 11 | d.getUTCMinutes() << 5 | d.getUTCSeconds() >> 1;
1235
+ const date = Math.max(d.getUTCFullYear(), 1980) - 1980 << 9 | d.getUTCMonth() + 1 << 5 | d.getUTCDate();
1236
+ const parts = [];
1237
+ const central = [];
1238
+ let offset = 0;
1239
+ const u16 = (v, o, n) => v.setUint16(o, n, true);
1240
+ const u32 = (v, o, n) => v.setUint32(o, n, true);
1241
+ for (const entry of entries) {
1242
+ const name = new TextEncoder().encode(entry.name);
1243
+ const crc = crc32(entry.data);
1244
+ if (entry.data.byteLength >= 4294967295 || offset >= 4294967295 || entries.length >= 65535) throw new Error("zip: export is too large for a plain zip");
1245
+ const local = new DataView(new ArrayBuffer(30));
1246
+ u32(local, 0, 67324752);
1247
+ u16(local, 4, 20);
1248
+ u16(local, 6, 2048);
1249
+ u16(local, 8, 0);
1250
+ u16(local, 10, time);
1251
+ u16(local, 12, date);
1252
+ u32(local, 14, crc);
1253
+ u32(local, 18, entry.data.byteLength);
1254
+ u32(local, 22, entry.data.byteLength);
1255
+ u16(local, 26, name.length);
1256
+ u16(local, 28, 0);
1257
+ const head = new DataView(new ArrayBuffer(46));
1258
+ u32(head, 0, 33639248);
1259
+ u16(head, 4, 20);
1260
+ u16(head, 6, 20);
1261
+ u16(head, 8, 2048);
1262
+ u16(head, 10, 0);
1263
+ u16(head, 12, time);
1264
+ u16(head, 14, date);
1265
+ u32(head, 16, crc);
1266
+ u32(head, 20, entry.data.byteLength);
1267
+ u32(head, 24, entry.data.byteLength);
1268
+ u16(head, 28, name.length);
1269
+ u32(head, 42, offset);
1270
+ parts.push(new Uint8Array(local.buffer), name, entry.data);
1271
+ central.push(new Uint8Array(head.buffer), name);
1272
+ offset += 30 + name.length + entry.data.byteLength;
1273
+ }
1274
+ const size = central.reduce((n, p) => n + p.byteLength, 0);
1275
+ const end = new DataView(new ArrayBuffer(22));
1276
+ u32(end, 0, 101010256);
1277
+ u16(end, 8, entries.length);
1278
+ u16(end, 10, entries.length);
1279
+ u32(end, 12, size);
1280
+ u32(end, 16, offset);
1281
+ const all = [...parts, ...central, new Uint8Array(end.buffer)];
1282
+ const out = new Uint8Array(all.reduce((n, p) => n + p.byteLength, 0));
1283
+ let at2 = 0;
1284
+ for (const p of all) {
1285
+ out.set(p, at2);
1286
+ at2 += p.byteLength;
1287
+ }
1288
+ return out;
1289
+ }
1290
+ function readZip(bytes) {
1291
+ const v = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
1292
+ let end = bytes.byteLength - 22;
1293
+ while (end >= 0 && v.getUint32(end, true) !== 101010256) end--;
1294
+ if (end < 0) throw new Error("zip: no end signature");
1295
+ const count = v.getUint16(end + 10, true);
1296
+ const out = /* @__PURE__ */ new Map();
1297
+ for (let i = 0, at = v.getUint32(end + 16, true); i < count; i++) {
1298
+ if (v.getUint32(at, true) !== 33639248) throw new Error("zip: bad central signature");
1299
+ if (v.getUint16(at + 10, true) !== 0) throw new Error("zip: compressed entries are not supported");
1300
+ const crc = v.getUint32(at + 16, true);
1301
+ const size = v.getUint32(at + 24, true);
1302
+ const nameLen = v.getUint16(at + 28, true);
1303
+ const local = v.getUint32(at + 42, true);
1304
+ const name = new TextDecoder().decode(bytes.subarray(at + 46, at + 46 + nameLen));
1305
+ if (v.getUint32(local, true) !== 67324752) throw new Error("zip: bad local signature");
1306
+ const start = local + 30 + v.getUint16(local + 26, true) + v.getUint16(local + 28, true);
1307
+ const data = bytes.slice(start, start + size);
1308
+ if (crc32(data) !== crc) throw new Error(`zip: crc mismatch in ${name}`);
1309
+ out.set(name, data);
1310
+ at += 46 + nameLen + v.getUint16(at + 30, true) + v.getUint16(at + 32, true);
1311
+ }
1312
+ return out;
1313
+ }
1314
+
1315
+ // src/export.ts
1316
+ async function buildExport(drive, ctx, now = Date.now) {
1317
+ const entries = [];
1318
+ const items = (await drive.exportData(ctx)).map(({ node, path, data, missing }) => {
1319
+ const file = data ? `files/${node.id}` : null;
1320
+ if (data && file) entries.push({ name: file, data });
1321
+ return {
1322
+ id: node.id,
1323
+ owner: node.owner,
1324
+ path,
1325
+ parentId: node.parentId,
1326
+ kind: node.kind,
1327
+ name: node.name,
1328
+ size: node.size,
1329
+ mime: node.mime,
1330
+ hash: node.hash,
1331
+ ref: node.ref,
1332
+ createdAt: node.createdAt,
1333
+ updatedAt: node.updatedAt,
1334
+ createdBy: node.createdBy,
1335
+ updatedBy: node.updatedBy,
1336
+ trashed: node.trashed,
1337
+ trashedAt: node.trashedAt,
1338
+ file,
1339
+ ...missing ? { missing: true } : {}
1340
+ };
1341
+ });
1342
+ const person = await drive.exportPerson(ctx);
1343
+ 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 };
1344
+ return writeZip([{ name: "manifest.json", data: new TextEncoder().encode(JSON.stringify(manifest, null, 2)) }, ...entries], now());
1345
+ }
1346
+
1347
+ // src/auth.ts
1348
+ import { createStoreAuth } from "agentic-identity";
1349
+ import { requirePrincipal } from "agentic-identity/http";
1350
+ async function authenticate(request, store, now) {
1351
+ if (!request.headers.has("authorization")) return null;
1352
+ const principal = await requirePrincipal(request, createStoreAuth(store, now ? { now } : {}));
1353
+ if (principal instanceof Response) return null;
1354
+ return { owner: principal.chain[0].id, actor: { id: principal.principalId, kind: principal.kind }, principal };
1355
+ }
1356
+
1357
+ // src/local.ts
1358
+ import { createHash as createHash2 } from "crypto";
1359
+ import { join as join2 } from "path";
1360
+ import { isMachineMade, isScope, issueCredential, revokeCredential } from "agentic-identity";
1361
+ function openLocal(dir) {
1362
+ const store = createFileStore(join2(dir, "db"));
1363
+ return { store, drive: createDrive({ store, blobs: createFileBlobs(join2(dir, "blobs")) }) };
1364
+ }
1365
+ var AuthError = class extends Error {
1366
+ code = "UNAUTHENTICATED";
1367
+ };
1368
+ var NO_TOKEN = "no principal: pass --token <key> or set AGENTIC_DRIVE_TOKEN to an API key";
1369
+ var BAD_TOKEN = "the key is not valid: it is unknown, revoked, or expired, or its principal is revoked";
1370
+ async function resolveToken(store, token) {
1371
+ if (!token) throw new AuthError(NO_TOKEN);
1372
+ let request;
1373
+ try {
1374
+ request = new Request("http://local", { headers: { authorization: `Bearer ${token}` } });
1375
+ } catch {
1376
+ throw new AuthError(BAD_TOKEN);
1377
+ }
1378
+ const ctx = await authenticate(request, store);
1379
+ if (!ctx) throw new AuthError(BAD_TOKEN);
1380
+ return ctx;
1381
+ }
1382
+ async function addPrincipal(store, id, opts) {
1383
+ if (!/^[A-Za-z0-9_.@-]{1,128}$/.test(id)) throw new Error(`invalid principal id "${id}"`);
1384
+ const scopes = opts.scopes?.length ? opts.scopes : [opts.parent ? "drive:write" : "drive:admin"];
1385
+ const bad = scopes.find((s) => !isScope(s));
1386
+ if (bad !== void 0) throw new Error(`invalid scope "${bad}"`);
1387
+ if (await store.get("principals", id)) throw new Error(`principal "${id}" exists`);
1388
+ if (opts.parent && !await store.get("principals", opts.parent)) throw new Error(`parent "${opts.parent}" does not exist`);
1389
+ const parent = opts.parent ? await store.get("principals", opts.parent) : null;
1390
+ if (parent && parent.kind !== "human") throw new Error(`parent "${opts.parent}" is not a person`);
1391
+ const doc = opts.parent ? { id, kind: "agent_of_human", parentId: opts.parent, scopes } : { id, kind: "human", scopes };
1392
+ await store.put("principals", id, doc);
1393
+ return doc;
1394
+ }
1395
+ async function revokePrincipal(store, id) {
1396
+ const doc = await store.get("principals", id);
1397
+ if (!doc) throw new DriveError("NOT_FOUND", `principal "${id}" does not exist`);
1398
+ await store.put("principals", id, { ...doc, revokedAt: Date.now() });
1399
+ }
1400
+ async function issueKey(store, principalId, label) {
1401
+ if (!await store.get("principals", principalId)) throw new Error(`principal "${principalId}" does not exist`);
1402
+ return issueCredential(store, { principalId, kind: "api_key", ...label ? { label } : {} });
1403
+ }
1404
+ async function revokeKey(store, token) {
1405
+ if (!await store.get("credentials", createHash2("sha256").update(token).digest("hex"))) throw new DriveError("NOT_FOUND", "no such key");
1406
+ await revokeCredential(store, token, Date.now());
1407
+ }
1408
+ function publicNode(node, path) {
1409
+ const { nameKey: _nameKey, blobKey: _blobKey, purging: _purging, ...rest } = node;
1410
+ const shown = { ...rest, machineMade: isMachineMade(node.createdBy.kind) };
1411
+ return path === void 0 ? shown : { path, ...shown };
1412
+ }
1413
+
1406
1414
  export {
1407
1415
  ROOT_ID,
1408
1416
  ACTOR_KINDS,
@@ -1414,11 +1422,14 @@ export {
1414
1422
  subjectKindOf,
1415
1423
  parseSubject,
1416
1424
  createDrive,
1425
+ createFileStore,
1426
+ createFileBlobs,
1427
+ DEFAULT_MAX_BYTES,
1428
+ sniffMime,
1429
+ upload,
1417
1430
  writeZip,
1418
1431
  readZip,
1419
1432
  buildExport,
1420
- createFileStore,
1421
- createFileBlobs,
1422
1433
  authenticate,
1423
1434
  openLocal,
1424
1435
  AuthError,
@@ -1428,8 +1439,5 @@ export {
1428
1439
  revokePrincipal,
1429
1440
  issueKey,
1430
1441
  revokeKey,
1431
- publicNode,
1432
- DEFAULT_MAX_BYTES,
1433
- sniffMime,
1434
- upload
1442
+ publicNode
1435
1443
  };
@@ -1,6 +1,6 @@
1
1
  // src/version.ts
2
2
  import { createRequire } from "module";
3
- var VERSION = true ? "0.5.0" : readPackageVersion();
3
+ var VERSION = true ? "0.6.1" : readPackageVersion();
4
4
 
5
5
  export {
6
6
  VERSION