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.
package/dist/worker.js CHANGED
@@ -1,5 +1,8 @@
1
1
  import {
2
- ACTOR_KINDS,
2
+ backfillAuthorIds,
3
+ backfillResources
4
+ } from "./chunk-3LISWG7H.js";
5
+ import {
3
6
  DEFAULT_MAX_BYTES,
4
7
  DEFAULT_URL_SECONDS,
5
8
  DriveError,
@@ -8,14 +11,16 @@ import {
8
11
  publicNode,
9
12
  safeMime,
10
13
  upload
11
- } from "./chunk-QRGKXPLX.js";
14
+ } from "./chunk-6CWBHSJ7.js";
12
15
 
13
16
  // src/worker.ts
14
17
  import { Hono } from "hono";
15
- import { parseRegion } from "agentic-identity";
16
18
  import { D1_SCHEMA, createD1Store, createR2Blobs, verifySignedBlobUrl } from "agentic-identity/cloudflare";
17
- var MIN_TOKEN_LENGTH = 32;
19
+ import { createSharing, createStoreAuth, parseRegion } from "agentic-identity";
18
20
  var STATUS = {
21
+ CROSS_DRIVE: 409,
22
+ INVALID_SHARE: 400,
23
+ TOO_MANY_ITEMS: 409,
19
24
  TOO_DEEP: 400,
20
25
  INVALID_EXPIRY: 400,
21
26
  BUSY: 409,
@@ -37,35 +42,10 @@ var STATUS = {
37
42
  TOO_LARGE: 413
38
43
  };
39
44
  var fail = (code, message, status) => new Response(JSON.stringify({ error: { code, message } }), { status, headers: { "content-type": "application/json" } });
40
- function sameText(a, b) {
41
- const x = new TextEncoder().encode(a);
42
- const y = new TextEncoder().encode(b);
43
- let diff = x.length ^ y.length;
44
- for (let i = 0; i < Math.max(x.length, y.length); i++) diff |= (x[i] ?? 0) ^ (y[i] ?? 0);
45
- return diff === 0;
46
- }
47
- var isText = (v) => typeof v === "string" && v !== "";
48
- function principalOf(grant) {
49
- if (typeof grant !== "object" || grant === null) return null;
50
- const { owner, actor, kind } = grant;
51
- if (!isText(owner) || !isText(actor) || !ACTOR_KINDS.includes(kind)) return null;
52
- return { owner, actor: { id: actor, kind } };
53
- }
54
- function authenticate(request, env) {
55
- const token = /^Bearer (.+)$/.exec(request.headers.get("authorization") ?? "")?.[1];
56
- if (!token || !env.DRIVE_API_TOKENS) return null;
57
- let grants;
58
- try {
59
- grants = JSON.parse(env.DRIVE_API_TOKENS);
60
- } catch {
61
- return null;
62
- }
63
- if (typeof grants !== "object" || grants === null) return null;
64
- let found;
65
- for (const [candidate, grant] of Object.entries(grants)) {
66
- if (sameText(candidate, token) && candidate.length >= MIN_TOKEN_LENGTH) found = grant;
67
- }
68
- return principalOf(found);
45
+ async function authenticate(request, store, now) {
46
+ if (!/^Bearer [A-Za-z0-9_-]{1,512}$/.test(request.headers.get("authorization") ?? "")) return null;
47
+ const principal = await createStoreAuth(store, now ? { now } : {}).authenticate(request);
48
+ return principal ? { owner: principal.chain[0].id, actor: { id: principal.principalId, kind: principal.kind }, principal } : null;
69
49
  }
70
50
  var schemas = /* @__PURE__ */ new WeakMap();
71
51
  function settings(env) {
@@ -92,7 +72,8 @@ async function driveFor(env, config) {
92
72
  schemas.set(env.DB, ready);
93
73
  }
94
74
  await ready;
95
- return createDrive({ store: createD1Store(env.DB, { region: regionOf(env) }), blobs: blobsFor(env, config) });
75
+ const store = createD1Store(env.DB, { region: regionOf(env) });
76
+ return { store, drive: createDrive({ store, blobs: blobsFor(env, config), sharing: createSharing(store) }) };
96
77
  }
97
78
  var number = (v, name) => {
98
79
  if (v === void 0) return void 0;
@@ -108,10 +89,12 @@ function createApp() {
108
89
  api.use("*", async (c, next) => {
109
90
  const config = settings(c.env);
110
91
  if (!config) return fail("NOT_CONFIGURED", "set DRIVE_PUBLIC_URL and DRIVE_URL_SECRET (32 bytes or more)", 503);
111
- const ctx = authenticate(c.req.raw, c.env);
92
+ const { drive, store } = await driveFor(c.env, config);
93
+ const ctx = await authenticate(c.req.raw, store);
112
94
  if (!ctx) return fail("UNAUTHORIZED", "send a valid bearer token", 401);
113
95
  c.set("ctx", ctx);
114
- c.set("drive", await driveFor(c.env, config));
96
+ c.set("drive", drive);
97
+ c.set("store", store);
115
98
  await next();
116
99
  });
117
100
  const q = (c, k) => c.req.query(k);
@@ -187,15 +170,25 @@ function createApp() {
187
170
  minSize: number(q(c, "minSize"), "minSize"),
188
171
  maxSize: number(q(c, "maxSize"), "maxSize"),
189
172
  createdByKind: q(c, "createdByKind"),
173
+ app: q(c, "app"),
190
174
  limit: number(q(c, "limit"), "limit")
191
175
  };
192
176
  return c.json((await c.var.drive.search(c.var.ctx, query)).map((r) => publicNode(r.node, r.path)));
193
177
  });
194
178
  api.get("/export", async (c) => new Response(await buildExport(c.var.drive, c.var.ctx), { headers: { "content-type": "application/zip", "content-disposition": 'attachment; filename="drive-export.zip"' } }));
195
179
  api.delete("/account", async (c) => {
196
- if (q(c, "confirm") !== "yes") throw new DriveError("INVALID_PATH", "add ?confirm=yes to delete everything you own for good");
197
- return c.json(await c.var.drive.deleteAll(c.var.ctx));
180
+ if (q(c, "confirm") !== "yes") throw new DriveError("INVALID_PATH", "add ?confirm=yes to erase you for good: your drive, and what you shared or held");
181
+ return c.json(await c.var.drive.erasePerson(c.var.ctx));
198
182
  });
183
+ api.get("/home", async (c) => {
184
+ const home = await c.var.drive.home(c.var.ctx, { app: q(c, "app"), limit: number(q(c, "limit"), "limit") });
185
+ return c.json({ apps: home.apps, files: home.files, items: home.items.map((i) => ({ ...publicNode(i.node, i.path), app: i.app })) });
186
+ });
187
+ api.post("/documents", async (c) => {
188
+ const b = await body(c);
189
+ return c.json(publicNode(await c.var.drive.registerDocument(c.var.ctx, { ref: str(b.ref, "ref"), name: str(b.name, "name"), folder: opt(b.folder, "folder") })), 201);
190
+ });
191
+ api.delete("/documents", async (c) => c.json(await c.var.drive.forgetDocument(c.var.ctx, need(q(c, "ref"), "ref"))));
199
192
  app2.get("/api/v1/drive/blob", async (c) => {
200
193
  const config = settings(c.env);
201
194
  if (!config) return fail("NOT_CONFIGURED", "set DRIVE_PUBLIC_URL and DRIVE_URL_SECRET (32 bytes or more)", 503);
@@ -212,6 +205,88 @@ function createApp() {
212
205
  }
213
206
  });
214
207
  });
208
+ const roleOf = (v) => v;
209
+ const lifetime = (v, name) => {
210
+ if (v === void 0) return void 0;
211
+ if (typeof v !== "number") throw new DriveError("INVALID_SHARE", `${name} must be a number of milliseconds`);
212
+ return v;
213
+ };
214
+ api.get("/shares", async (c) => c.json(await c.var.drive.shares(c.var.ctx, need(q(c, "path"), "path"))));
215
+ api.post("/shares", async (c) => {
216
+ const b = await body(c);
217
+ await c.var.drive.shareWith(c.var.ctx, str(b.path, "path"), str(b.subjectId, "subjectId"), roleOf(b.role));
218
+ return c.json({ ok: true });
219
+ });
220
+ api.delete("/shares", async (c) => {
221
+ await c.var.drive.unshare(c.var.ctx, need(q(c, "path"), "path"), need(q(c, "subjectId"), "subjectId"));
222
+ return c.json({ ok: true });
223
+ });
224
+ api.post("/invites", async (c) => {
225
+ const b = await body(c);
226
+ const expiresInMs = lifetime(b.expiresInMs, "expiresInMs");
227
+ const invite = await c.var.drive.invite(c.var.ctx, str(b.path, "path"), {
228
+ role: roleOf(b.role),
229
+ ...b.email === void 0 ? {} : { email: str(b.email, "email") },
230
+ ...b.handle === void 0 ? {} : { handle: str(b.handle, "handle") },
231
+ ...expiresInMs === void 0 ? {} : { expiresInMs }
232
+ });
233
+ return c.json(invite, 201);
234
+ });
235
+ api.post("/invites/accept", async (c) => {
236
+ const id = str((await body(c)).id, "id");
237
+ const user = await c.var.store.get("users", c.var.ctx.actor.id);
238
+ const agent = await c.var.store.get("principals", c.var.ctx.actor.id);
239
+ await c.var.drive.acceptInvite(c.var.ctx, id, {
240
+ ...user?.emailVerified === true && typeof user.email === "string" ? { email: user.email } : {},
241
+ ...typeof agent?.handle === "string" ? { handle: agent.handle } : {}
242
+ });
243
+ return c.json({ ok: true });
244
+ });
245
+ api.post("/links", async (c) => {
246
+ const b = await body(c);
247
+ const expiresInMs = lifetime(b.expiresInMs, "expiresInMs");
248
+ const link = await c.var.drive.createLink(c.var.ctx, str(b.path, "path"), {
249
+ role: roleOf(b.role),
250
+ signedInOnly: b.signedInOnly === true,
251
+ ...expiresInMs === void 0 ? {} : { expiresAt: Date.now() + expiresInMs }
252
+ });
253
+ return c.json(link, 201);
254
+ });
255
+ api.delete("/links", async (c) => {
256
+ await c.var.drive.revokeLink(c.var.ctx, need(q(c, "id"), "id"));
257
+ return c.json({ ok: true });
258
+ });
259
+ const shared = new Hono();
260
+ const visit = async (c) => {
261
+ const config = settings(c.env);
262
+ if (!config) return { stop: fail("NOT_CONFIGURED", "set DRIVE_PUBLIC_URL and DRIVE_URL_SECRET (32 bytes or more)", 503) };
263
+ const { drive, store } = await driveFor(c.env, config);
264
+ const ctx = await authenticate(c.req.raw, store);
265
+ if (!ctx && c.req.header("authorization")) return { stop: fail("UNAUTHORIZED", "send a valid bearer token", 401) };
266
+ return { drive, principal: ctx?.principal ?? null, token: c.req.header("x-link-token") };
267
+ };
268
+ shared.get("/:id", async (c) => {
269
+ const v = await visit(c);
270
+ return "stop" in v ? v.stop : c.json(publicNode(await v.drive.openShared(v.principal, c.req.param("id"), v.token)));
271
+ });
272
+ shared.get("/:id/items", async (c) => {
273
+ const v = await visit(c);
274
+ return "stop" in v ? v.stop : c.json((await v.drive.listShared(v.principal, c.req.param("id"), v.token)).map((n) => publicNode(n)));
275
+ });
276
+ shared.get("/:id/content", async (c) => {
277
+ const v = await visit(c);
278
+ if ("stop" in v) return v.stop;
279
+ const { node, data } = await v.drive.readShared(v.principal, c.req.param("id"), v.token);
280
+ return new Response(data, {
281
+ headers: {
282
+ "content-type": safeMime(node.mime),
283
+ "content-disposition": `attachment; filename*=UTF-8''${encodeURIComponent(node.name)}`,
284
+ "x-content-type-options": "nosniff",
285
+ "cache-control": "private, no-store"
286
+ }
287
+ });
288
+ });
289
+ app2.route("/api/v1/drive/shared", shared);
215
290
  app2.route("/api/v1/drive", api);
216
291
  app2.notFound(() => fail("NOT_FOUND", "no such route", 404));
217
292
  app2.onError((error) => {
@@ -225,6 +300,8 @@ var app = createApp();
225
300
  var worker_default = { fetch: (request, env, ctx) => app.fetch(request, env, ctx) };
226
301
  export {
227
302
  authenticate,
303
+ backfillAuthorIds,
304
+ backfillResources,
228
305
  createApp,
229
306
  worker_default as default
230
307
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-drive",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Store, sort, and share files and folders.",
5
5
  "keywords": [
6
6
  "drive",
@@ -28,6 +28,7 @@
28
28
  "files": [
29
29
  "dist",
30
30
  "README.md",
31
+ "CHANGELOG.md",
31
32
  "LICENSE",
32
33
  "llms.txt",
33
34
  "SKILL.md"
@@ -53,11 +54,12 @@
53
54
  "typecheck": "tsc --noEmit",
54
55
  "test": "vitest run",
55
56
  "clean": "rm -rf dist",
57
+ "prepack": "npm run build --silent >&2",
56
58
  "ci:local": "npm run build && npm run typecheck && npm test"
57
59
  },
58
60
  "dependencies": {
59
61
  "@modelcontextprotocol/sdk": "^1.12.0",
60
- "agentic-identity": "^0.2.0",
62
+ "agentic-identity": "^0.3.0",
61
63
  "hono": "^4.13.14",
62
64
  "zod": "^3.25.0"
63
65
  },
@@ -1,144 +0,0 @@
1
- import { Store, Blobs } from 'agentic-identity';
2
-
3
- declare const ROOT_ID = "root";
4
- declare const ACTOR_KINDS: readonly ["human", "agent_of_human", "agent_of_agent", "agent", "user_token"];
5
- type ActorKind = (typeof ACTOR_KINDS)[number];
6
- type Actor = {
7
- id: string;
8
- kind: ActorKind;
9
- };
10
- /** `owner` is the drive's owner. `actor` is who acts for them. */
11
- type Ctx = {
12
- owner: string;
13
- actor: Actor;
14
- };
15
- type Action = "read" | "write" | "delete";
16
- /** The seam for sharing. A later release swaps this for a role check. */
17
- type Can = (ctx: Ctx, action: Action, node: Node) => boolean | Promise<boolean>;
18
- type Node = {
19
- id: string;
20
- owner: string;
21
- parentId: string;
22
- kind: "folder" | "file" | "ref";
23
- name: string;
24
- nameKey: string;
25
- size: number;
26
- mime: string | null;
27
- hash: string | null;
28
- /** A typed reference such as `docs:<id>`. Set when kind is "ref". */
29
- ref: string | null;
30
- blobKey: string | null;
31
- createdAt: number;
32
- updatedAt: number;
33
- createdBy: Actor;
34
- updatedBy: Actor;
35
- trashed: boolean;
36
- trashedAt: number | null;
37
- /** Set while a purge deletes the item. Nothing can see or change it then. */
38
- purging: boolean;
39
- };
40
- type ErrorCode = "NOT_FOUND" | "NAME_TAKEN" | "BLOB_EXISTS" | "INVALID_NAME" | "INVALID_PATH" | "INVALID_REF" | "NOT_A_FOLDER" | "NOT_A_FILE" | "MOVE_INTO_SELF" | "TOO_DEEP" | "PARENT_GONE" | "TRASHED" | "NOT_TRASHED" | "IS_ROOT" | "FORBIDDEN" | "BLOB_MISSING" | "TOO_LARGE" | "INVALID_EXPIRY" | "BUSY";
41
- declare class DriveError extends Error {
42
- readonly code: ErrorCode;
43
- constructor(code: ErrorCode, message: string);
44
- }
45
- interface SearchQuery {
46
- /** Part of the name. Case and Unicode form do not matter. */
47
- name?: string;
48
- /** An exact type, or a family such as `image/*`. */
49
- mime?: string;
50
- kind?: Node["kind"];
51
- minSize?: number;
52
- maxSize?: number;
53
- /** Items first made by this kind of actor, such as `agent_of_human`. */
54
- createdByKind?: ActorKind;
55
- /** At most this many results. Defaults to 100. */
56
- limit?: number;
57
- }
58
- type OnConflict = "fail" | "rename" | "replace";
59
- interface DriveDeps {
60
- store: Store;
61
- blobs: Blobs;
62
- now?: () => number;
63
- newId?: () => string;
64
- can?: Can;
65
- /** Most parent steps any ancestor walk takes. A walk that hits it fails closed. */
66
- maxDepth?: number;
67
- /** Called with a blob key that no node points at and that could not be deleted. */
68
- onOrphan?: (key: string, error: unknown) => void;
69
- }
70
- type Drive = ReturnType<typeof createDrive>;
71
- /** Validates a name. Names are NFC-normalized. Two names clash when their keys match. */
72
- declare function parseName(raw: string): {
73
- name: string;
74
- key: string;
75
- };
76
- declare function createDrive(deps: DriveDeps): {
77
- stat(ctx: Ctx, loc: string): Promise<Node>;
78
- /** Live children, sorted by name. */
79
- list(ctx: Ctx, loc?: string): Promise<Node[]>;
80
- mkdir(ctx: Ctx, path: string, opts?: {
81
- parents?: boolean;
82
- }): Promise<Node>;
83
- /**
84
- * Stores one blob and one node. The blob is written first. The new node commits before
85
- * any old blob goes. A failed old-blob delete leaves an orphan and never undoes the commit.
86
- */
87
- putFile(ctx: Ctx, path: string, data: Uint8Array, opts?: {
88
- mime?: string;
89
- parents?: boolean;
90
- onConflict?: OnConflict;
91
- }): Promise<Node>;
92
- /** A reference points at a document in another app, such as `docs:<id>`. */
93
- putRef(ctx: Ctx, path: string, ref: string, opts?: {
94
- parents?: boolean;
95
- onConflict?: OnConflict;
96
- }): Promise<Node>;
97
- read(ctx: Ctx, loc: string): Promise<{
98
- node: Node;
99
- data: Uint8Array;
100
- contentType?: string;
101
- }>;
102
- /**
103
- * A time-limited URL to fetch the bytes. The URL signs an attachment header and the stored
104
- * type, so the link holder cannot change how the bytes are served. Trashing an item does
105
- * not revoke a link already issued.
106
- */
107
- downloadUrl(ctx: Ctx, loc: string, expiresInSeconds?: number): Promise<string>;
108
- move(ctx: Ctx, src: string, destLoc: string, opts?: {
109
- name?: string;
110
- onConflict?: OnConflict;
111
- }): Promise<Node>;
112
- rename(ctx: Ctx, loc: string, name: string): Promise<Node>;
113
- /** Hides an item and, for a folder, everything under it. Nothing else changes. */
114
- trash(ctx: Ctx, loc: string): Promise<Node>;
115
- /** Items trashed on their own, newest first. */
116
- listTrash(ctx: Ctx): Promise<Node[]>;
117
- /** Puts a trashed item back. It needs a live parent folder. */
118
- restore(ctx: Ctx, id: string, opts?: {
119
- parentId?: string;
120
- onConflict?: OnConflict;
121
- }): Promise<Node>;
122
- /** Live items that match every field given, sorted by path. */
123
- search(ctx: Ctx, q?: SearchQuery): Promise<{
124
- node: Node;
125
- path: string;
126
- }[]>;
127
- /** Every item of the owner, trashed or not, with its original path and bytes. For export. */
128
- exportData(ctx: Ctx): Promise<{
129
- node: Node;
130
- path: string;
131
- data: Uint8Array | null;
132
- missing: boolean;
133
- }[]>;
134
- /** 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. */
135
- deleteAll(ctx: Ctx): Promise<{
136
- removed: number;
137
- }>;
138
- /** Deletes a trashed item for good, with everything under it and all their bytes. */
139
- purge(ctx: Ctx, id: string): Promise<{
140
- removed: number;
141
- }>;
142
- };
143
-
144
- export { type ActorKind as A, type Ctx as C, type Drive as D, type ErrorCode as E, type Node as N, type OnConflict as O, ROOT_ID as R, type SearchQuery as S, type Actor as a, ACTOR_KINDS as b, type Action as c, type Can as d, type DriveDeps as e, DriveError as f, createDrive as g, parseName as p };