agentic-drive 0.2.0 → 0.3.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 +31 -0
- package/README.md +30 -3
- package/SKILL.md +11 -0
- package/dist/{chunk-3LISWG7H.js → chunk-5KEISBS4.js} +1 -1
- package/dist/{chunk-RWMBGIAS.js → chunk-GMA5I5L6.js} +1 -1
- package/dist/{chunk-6CWBHSJ7.js → chunk-VSVIJBMJ.js} +58 -27
- package/dist/cli.js +2 -2
- package/dist/client.d.ts +173 -0
- package/dist/client.js +240 -0
- package/dist/{drive-DKOs1FyM.d.ts → drive-Dlzwukjq.d.ts} +15 -6
- package/dist/index.d.ts +2 -2
- package/dist/index.js +7 -3
- package/dist/mcp.d.ts +1 -1
- package/dist/mcp.js +2 -2
- package/dist/worker.d.ts +1 -1
- package/dist/worker.js +4 -4
- package/llms.txt +1 -0
- package/package.json +6 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
Read the breaking changes before you upgrade. The migration steps follow them.
|
|
6
|
+
|
|
7
|
+
### Breaking changes
|
|
8
|
+
|
|
9
|
+
- **agentic-identity 0.4.0.** A grant has a kind and an id, and a person and an agent with the same id are two subjects. The range is now `^0.4.0`. Run identity's `migrateGrants` once on a store with grants from 0.3.0. See the migration steps.
|
|
10
|
+
- **`shareWith` and `unshare` take a subject.** The subject is `{ kind: "human" | "agent", id }`, not a bare id. A subject with no kind, or with another kind, fails with `INVALID_SHARE`. The HTTP routes take `subjectKind` next to `subjectId`: in the body of `POST /shares`, and in the query of `DELETE /shares`. `GET /shares` lists `subjectKind` on each grant.
|
|
11
|
+
- **Erase and export read grants by kind.** A person's erase and export touch the grants of that person and of their agents with their own identity. A grant of an agent with the same id as a person stays.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **`agentic-drive/client`.** A typed client for the HTTP API: `createClient({ baseUrl, token, fetch?, retries? })`. It has one method for each bearer route and each shared route, a typed `DriveApiError` with the API `code` and the HTTP `status`, and `AbortSignal` support. It sends a write once. It uses only `fetch`.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- A purge, `deleteAll`, and `erasePerson` erase each item and the drive root through identity's `deleteResource`. The grants, links, invites, and audit entries of the item go with it, so none survives the item. Before, the audit entries stayed. The shares go while the item is marked for erase, before its row goes, so a failed call leaves a marked item and the same call runs again. A caller who does not own the item, and an agent drive that `erasePerson` removes, get the same result from a sweep. A purge costs more calls for each item. Only the owner of the drive, in person, can finish a purge that failed after the shares were erased. An agent gets `NOT_FOUND` for that item.
|
|
20
|
+
- New exports `subjectKindOf` and `parseSubject`. `subjectKindOf` accepts the kind of the root of a chain, `human` or `agent`, and throws on any other kind, a delegated one too.
|
|
21
|
+
- An export lists `subjectKind` on each grant a person holds.
|
|
22
|
+
|
|
23
|
+
### Migration
|
|
24
|
+
|
|
25
|
+
Run the steps in this order. The old Worker keeps writing grants in the old format until step 3, so the migration in step 4 must come after the deploy.
|
|
26
|
+
|
|
27
|
+
1. Install the new versions: `npm install agentic-drive@0.3.0 agentic-identity@^0.4.0`.
|
|
28
|
+
2. Change every call of `shareWith` and `unshare` in your code to pass `{ kind, id }`. The build fails on a bare id, and a bare id in JavaScript fails with `INVALID_SHARE`.
|
|
29
|
+
3. Deploy the 0.3.0 Worker. **From now until step 4, every owner loses access to their own drive.** Owner grants from before 0.3.0 are in the old format too, and identity 0.4.0 does not honor them. Everybody gets `NOT_FOUND`, and `erasePerson` and the export skip those grants. Run step 4 right after the deploy, or plan a maintenance window.
|
|
30
|
+
4. Run `migrateGrants` in a one-off Worker set up as in the 0.2.0 steps. Add it to the `steps` of that Worker, for example `grants: (store) => migrateGrants(store)`, with `import { migrateGrants } from "agentic-identity"`. It rewrites grants that have no kind. If the call fails or runs out of time, call it again until `migrated` is 0. It skips what is done. Delete the one-off Worker after it.
|
|
31
|
+
5. Delete from the `grants` collection each doc id that `migrateGrants` lists in `unresolved`. Its subject is gone, and identity never honors it. The share list leaves such a grant out.
|
|
32
|
+
6. Change every HTTP client of `POST /shares` and `DELETE /shares` to send `subjectKind`. The new client does this.
|
|
33
|
+
|
|
3
34
|
## 0.2.0
|
|
4
35
|
|
|
5
36
|
Read the breaking changes before you upgrade. The migration steps follow them.
|
package/README.md
CHANGED
|
@@ -66,7 +66,7 @@ every request. A missing, expired, or revoked credential, or a revoked or expire
|
|
|
66
66
|
A session cookie is refused: only a bearer token counts. An agent acts in its owner's drive with the narrower scopes of its grant.
|
|
67
67
|
Scopes are `drive:read`, `drive:write`, and `drive:admin`.
|
|
68
68
|
|
|
69
|
-
Sharing routes: `GET /shares?path=`, `POST /shares`, `DELETE /shares?path=&subjectId=`, `POST /invites`,
|
|
69
|
+
Sharing routes: `GET /shares?path=`, `POST /shares`, `DELETE /shares?path=&subjectId=&subjectKind=`, `POST /invites`,
|
|
70
70
|
`POST /invites/accept`, `POST /links`, `DELETE /links?id=`. A link visitor reads with the header
|
|
71
71
|
`x-link-token` on `GET /shared/:id`, `GET /shared/:id/items`, and `GET /shared/:id/content`.
|
|
72
72
|
Name a shared item by its id. A path can start at a folder id: `<folder id>/notes/a.txt`.
|
|
@@ -74,6 +74,33 @@ Without `DRIVE_URL_SECRET` or `DRIVE_PUBLIC_URL`, every request gets 503.
|
|
|
74
74
|
Create the R2 bucket with `--jurisdiction eu` and the D1 database with `--location weur`.
|
|
75
75
|
The Worker declares region `eu` and refuses a D1 query that a non-EU region served. `DRIVE_REGION=unknown` turns this off for local runs.
|
|
76
76
|
|
|
77
|
+
## SDK
|
|
78
|
+
|
|
79
|
+
`agentic-drive/client` is a typed client for the HTTP API. It uses only `fetch`, so it runs in browsers, Workers, and Node 22.
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
import { createClient, DriveApiError } from "agentic-drive/client";
|
|
83
|
+
|
|
84
|
+
const drive = createClient({ baseUrl: "https://drive.example.com", token: process.env.DRIVE_TOKEN });
|
|
85
|
+
await drive.mkdir("/docs", { parents: true });
|
|
86
|
+
const file = await drive.upload("/docs/a.txt", "hello", { mime: "text/plain" });
|
|
87
|
+
const { data } = await drive.download(file.id);
|
|
88
|
+
try {
|
|
89
|
+
await drive.stat("/missing");
|
|
90
|
+
} catch (error) {
|
|
91
|
+
if (error instanceof DriveApiError) console.log(error.code, error.status); // NOT_FOUND 404
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
- One method for each bearer route and each shared route: `list`, `stat`, `mkdir`, `upload`, `download`, `url`, `move`, `trash`, `listTrash`, `restore`, `purge`, `search`, `export`, `deleteAccount`, `home`, `registerDocument`, `forgetDocument`, `shares`, `share`, `unshare`, `invite`, `acceptInvite`, `createLink`, `revokeLink`, `openShared`, `listShared`, and `readShared`.
|
|
96
|
+
- Each method has typed input and output. The client checks the shape of every answer, and `download` checks the bytes against their hash (it needs WebCrypto). `readShared` does not: the Worker sends no hash there. An answer that fails a check throws `BAD_RESPONSE`. `url` gives a signed link that you fetch yourself. The last argument takes `{ signal }` to stop the call with an `AbortSignal`.
|
|
97
|
+
- A failed call throws `DriveApiError`. It has the `code` of the API and the HTTP `status`. A call that gets no answer has the code `NETWORK_ERROR` and status 0. An abort stays the abort error of `fetch`. A code that the client does not know gives `BAD_RESPONSE`.
|
|
98
|
+
- The client never sends a write again. `retries` (default 0) sends a read (GET) again after a network error or a 502, 503, or 504, with a short wait. A redirect is an error (`BAD_RESPONSE`, with the status of the redirect, or 0 in a browser): set `baseUrl` to the final URL.
|
|
99
|
+
- `fetch` is an option, for tests and for runtimes with their own. `openShared`, `listShared`, and `readShared` work without a token, with a `linkToken`.
|
|
100
|
+
- A stream body for `upload` is refused by some browsers. The error says so, and nothing is sent. Items from before 0.2.0 need `backfillAuthorIds`, and grants from before 0.3.0 need identity's `migrateGrants`. Without them, an answer that holds one fails with `BAD_RESPONSE`.
|
|
101
|
+
- A browser page needs the Worker on its own origin for now: the Worker sends no CORS headers.
|
|
102
|
+
- Share with a subject: `share("/pub", { kind: "human", id: "u2" }, "viewer")`.
|
|
103
|
+
|
|
77
104
|
## Features
|
|
78
105
|
|
|
79
106
|
- Folders, files, and typed references to documents in other apps (`docs:<id>`).
|
|
@@ -83,14 +110,14 @@ The Worker declares region `eu` and refuses a D1 query that a non-EU region serv
|
|
|
83
110
|
- Each item records its size, type, owner, and who made and last changed it, with the actor kind.
|
|
84
111
|
- Storage sits behind the `Store` and `Blobs` ports of `agentic-identity`. Tests use its memory adapters. The CLI uses a local file adapter that passes the same contract suites. The Worker uses its D1 and R2 adapters.
|
|
85
112
|
- Roles: viewer, commenter, editor, owner. A role on a folder reaches everything under it, and follows an item that moves. Identity decides every request through `can()`, which also writes the audit entry. A caller who cannot read an item gets `NOT_FOUND`. A caller who can read it but not change it gets `FORBIDDEN`. Editors write and trash. An item takes the roles of its new folder, so moving an item into a folder where the caller is owner needs owner on the item. Only owners share, purge, and read the share list.
|
|
86
|
-
- Share by
|
|
113
|
+
- Share by a subject (`{ kind: "human" | "agent", id }`), by invite (an email for a person, a handle for an agent), or by link with a role, an expiry, and an optional sign-in. The SDK calls are `shareWith`, `unshare`, `invite`, `acceptInvite`, `createLink`, `revokeLink`, `shares`, `openShared`, `listShared`, and `readShared`.
|
|
87
114
|
- Two live items never share a name in one folder. A claim document holds each name. It is written in the same transaction as the item, and it is deleted when the item goes to the trash.
|
|
88
115
|
- `upload()` reads a stream with a size cap, finds the type from the first bytes, and stores a SHA-256 hash.
|
|
89
116
|
- `downloadUrl()` returns a signed URL. The URL signs an attachment header with the file name and the stored type. The Worker verifies it and serves the bytes with those headers, so the link holder cannot change how the bytes are served. A link lasts 15 minutes by default and 1 hour at most. Trashing a file does not revoke a link already issued.
|
|
90
117
|
- `search()` finds live items by name, type, kind, size, and who made them.
|
|
91
118
|
- App documents: an app calls `registerDocument({ ref: "docs:<id>", name, folder })` to put a document in the drive, and `forgetDocument(ref)` when it deletes the document. `home({ app })` lists files and every app's documents together, newest change first, with a count per app. `search({ app })` filters the same way. The CLI has `home`, `register`, and `forget`. The MCP server has `drive_home`, `drive_register_document`, and `drive_forget_document`.
|
|
92
119
|
- `buildExport()` writes everything one person has to a zip with a `manifest.json` (format version 2). It names the person by id and kind. It lists the person's own items (trashed ones are flagged, and a file whose bytes are gone is marked `missing`). For the person in person it also lists the ids of what they or their agents made or changed in other drives (no names, and nothing of the people who own them), the shares they hold, the invites they sent, and the audit entries of what they and their agents did. An agent gets `partial: true` and none of that. Invitee emails and link tokens are never in it. Entries are named by item id.
|
|
93
|
-
- `erasePerson()` erases a person for good, and only the person in person can call it (the CLI `delete-all` and `DELETE /account` call it). It deletes their drive with all its shares, the shares they hold, and the invites they or their agents sent, on drive items only: the grants of other apps stay for those apps and for identity. What they or their agents made in other drives stays, because it belongs to those drives: the id becomes `erased`, and the kind stays, so machine-made work keeps its label. Agents are found through the `principals` collection, as identity finds them: an agent with its own identity names the person as `ownerId`, and a delegated agent names its parent. An agent with its own identity belongs to the person, and so does its drive: erase deletes it, and the export lists its items (no bytes). A person with more than 1000 agents cannot be erased here: the call fails and deletes nothing more. The audit
|
|
120
|
+
- `erasePerson()` erases a person for good, and only the person in person can call it (the CLI `delete-all` and `DELETE /account` call it). It deletes their drive with all its shares, the shares they hold, and the invites they or their agents sent, on drive items only: the grants of other apps stay for those apps and for identity. What they or their agents made in other drives stays, because it belongs to those drives: the id becomes `erased`, and the kind stays, so machine-made work keeps its label. Agents are found through the `principals` collection, as identity finds them: an agent with its own identity names the person as `ownerId`, and a delegated agent names its parent. An agent with its own identity belongs to the person, and so does its drive: erase deletes it, and the export lists its items (no bytes). A person with more than 1000 agents cannot be erased here: the call fails and deletes nothing more. The audit entries of the erased items go with them, through identity `deleteResource`. The entries about what the person did in other drives stay: they hold ids and kinds only. A failed call can run again. `deleteAll()` is the drive part alone.
|
|
94
121
|
- A `can()` hook decides who may read, write, or delete. Without `sharing`, the default allows the owner only.
|
|
95
122
|
|
|
96
123
|
## Limits
|
package/SKILL.md
CHANGED
|
@@ -18,6 +18,17 @@ const me = { owner: "u1", actor: { id: "u1", kind: "human" } };
|
|
|
18
18
|
await drive.mkdir(me, "/notes");
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
+
To call a deployed Worker, use the remote client. It runs in browsers, Workers, and Node 22:
|
|
22
|
+
|
|
23
|
+
```js
|
|
24
|
+
import { createClient } from "agentic-drive/client";
|
|
25
|
+
|
|
26
|
+
const remote = createClient({ baseUrl: "https://drive.example.com", token });
|
|
27
|
+
await remote.mkdir("/notes");
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
A failed call throws `DriveApiError`. Read its `code` and `status`.
|
|
31
|
+
|
|
21
32
|
## Rules
|
|
22
33
|
|
|
23
34
|
- A locator with a `/` is a path: `/x` from the root, or `<folder id>/x` from a folder. Any other locator is an id.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/drive.ts
|
|
2
|
-
import { MAX_FOLDER_DEPTH } from "agentic-identity";
|
|
2
|
+
import { EraseRefusedError, MAX_FOLDER_DEPTH, SUBJECT_KINDS } from "agentic-identity";
|
|
3
3
|
var ROOT_ID = "root";
|
|
4
4
|
var NODES = "drive_nodes";
|
|
5
5
|
var NAMES = "drive_names";
|
|
@@ -80,6 +80,17 @@ var MAX_ITEM_DEPTH = MAX_FOLDER_DEPTH - 1;
|
|
|
80
80
|
var VISITOR = { id: "", kind: "human" };
|
|
81
81
|
var appOf = (node) => node.kind === "ref" && node.ref ? node.ref.split(":")[0] : null;
|
|
82
82
|
var ERASED = "erased";
|
|
83
|
+
function subjectKindOf(kind) {
|
|
84
|
+
if (kind === "human" || kind === "agent") return kind;
|
|
85
|
+
throw new DriveError("INVALID_SHARE", `a ${typeof kind === "string" ? kind : "missing"} principal cannot hold a role. Only the root of a chain can: human or agent`);
|
|
86
|
+
}
|
|
87
|
+
function parseSubject(input) {
|
|
88
|
+
const s = input;
|
|
89
|
+
if (!s || typeof s.id !== "string" || s.id === "" || !SUBJECT_KINDS.includes(s.kind)) {
|
|
90
|
+
throw new DriveError("INVALID_SHARE", `a subject needs an id and a kind (${SUBJECT_KINDS.join(" or ")})`);
|
|
91
|
+
}
|
|
92
|
+
return { kind: s.kind, id: s.id };
|
|
93
|
+
}
|
|
83
94
|
var SHARE_ERROR = /^(invalid|give an|expiresAt|expiresInMs|resource exists|the last owner)/;
|
|
84
95
|
function createDrive(deps) {
|
|
85
96
|
const { store, blobs } = deps;
|
|
@@ -393,6 +404,7 @@ function createDrive(deps) {
|
|
|
393
404
|
if (!isInPerson(ctx) || ctx.actor.kind !== "human") throw new DriveError("FORBIDDEN", `only the owner in person can ${what}`);
|
|
394
405
|
}
|
|
395
406
|
const subjectOf = (ctx) => ({ id: ctx.owner, kind: ctx.principal?.chain[0]?.kind ?? (isInPerson(ctx) ? ctx.actor.kind : null) });
|
|
407
|
+
const heldBy = (who2, ownedAgents) => [[who2.id, subjectKindOf(who2.kind)], ...ownedAgents.map((a) => [a, subjectKindOf("agent")])];
|
|
396
408
|
async function allWhere(ops, collection, where, orderBy) {
|
|
397
409
|
const out = [];
|
|
398
410
|
for (let cursor; ; ) {
|
|
@@ -419,23 +431,39 @@ function createDrive(deps) {
|
|
|
419
431
|
for (const actor of actors) for (const field of ["createdById", "updatedById"]) for (const { data } of await allWhere(ops, NODES, { [field]: actor })) made.set(data.id, data);
|
|
420
432
|
return [...made.values()];
|
|
421
433
|
}
|
|
422
|
-
async function eraseDrive(owner) {
|
|
434
|
+
async function eraseDrive(owner, principal) {
|
|
423
435
|
let removed = 0;
|
|
424
436
|
for (let pass = 0; pass < 5; pass++) {
|
|
425
437
|
const all = await rawOwnerNodes(owner, true);
|
|
426
438
|
if (all.length === 0) {
|
|
427
|
-
|
|
428
|
-
await transact(async (tx) => {
|
|
429
|
-
const shares = sharing ? await sharesOf(tx, rootId) : [];
|
|
430
|
-
for (const share of shares) await tx.delete(share.collection, share.id);
|
|
431
|
-
if (sharing) await tx.delete("resources", rootId);
|
|
432
|
-
});
|
|
439
|
+
await finishResource(principal, await rootResource(owner));
|
|
433
440
|
return removed;
|
|
434
441
|
}
|
|
435
|
-
removed += await removeNodes(all);
|
|
442
|
+
removed += await removeNodes(all, void 0, principal);
|
|
436
443
|
}
|
|
437
444
|
throw new DriveError("BUSY", "the drive kept changing while it was deleted. Run it again.");
|
|
438
445
|
}
|
|
446
|
+
async function finishResource(principal, resourceId) {
|
|
447
|
+
if (!sharing) return;
|
|
448
|
+
if (principal) {
|
|
449
|
+
try {
|
|
450
|
+
await sharing.deleteResource(principal, resourceId);
|
|
451
|
+
return;
|
|
452
|
+
} catch (error) {
|
|
453
|
+
if (!(error instanceof EraseRefusedError) && !(error instanceof Error && error.message === "forbidden")) throw error;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
await transact(async (tx) => {
|
|
457
|
+
for (const share of await sharesOf(tx, resourceId)) await tx.delete(share.collection, share.id);
|
|
458
|
+
await tx.delete("resources", resourceId);
|
|
459
|
+
});
|
|
460
|
+
for (let cursor; ; ) {
|
|
461
|
+
const page = await store.query("audit", { where: { resourceId }, limit: 100, cursor });
|
|
462
|
+
for (const entry of page.items) await store.delete("audit", entry.id);
|
|
463
|
+
if (!page.nextCursor) break;
|
|
464
|
+
cursor = page.nextCursor;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
439
467
|
async function sharesOf(ops, resourceId) {
|
|
440
468
|
const out = [];
|
|
441
469
|
for (const collection of ["grants", "links", "invites"]) {
|
|
@@ -448,7 +476,7 @@ function createDrive(deps) {
|
|
|
448
476
|
}
|
|
449
477
|
return out;
|
|
450
478
|
}
|
|
451
|
-
async function removeNodes(nodes, top) {
|
|
479
|
+
async function removeNodes(nodes, top, principal) {
|
|
452
480
|
const unchanged = (now2, seen) => now2 !== null && now2.trashed === seen.trashed && now2.parentId === seen.parentId && now2.updatedAt === seen.updatedAt;
|
|
453
481
|
const size = sharing ? SHARED_CHUNK : CHUNK;
|
|
454
482
|
const marked = [];
|
|
@@ -486,18 +514,16 @@ function createDrive(deps) {
|
|
|
486
514
|
for (let i = 0; i < rows.length; i += size) {
|
|
487
515
|
const chunk = rows.slice(i, i + size);
|
|
488
516
|
for (const node of chunk) if (node.blobKey) await blobs.delete(node.blobKey);
|
|
517
|
+
for (const node of chunk) await finishResource(principal, node.id);
|
|
489
518
|
await transact(async (tx) => {
|
|
490
519
|
const held = [];
|
|
491
520
|
for (const node of chunk) {
|
|
492
521
|
const claim = node.trashed ? null : await claimId(node.owner, node.parentId, node.nameKey);
|
|
493
|
-
|
|
494
|
-
held.push({ node, claim, owns: claim !== null && (await tx.get(NAMES, claim))?.nodeId === node.id, shares });
|
|
522
|
+
held.push({ node, claim, owns: claim !== null && (await tx.get(NAMES, claim))?.nodeId === node.id });
|
|
495
523
|
}
|
|
496
|
-
for (const { node, claim, owns
|
|
524
|
+
for (const { node, claim, owns } of held) {
|
|
497
525
|
await tx.delete(NODES, node.id);
|
|
498
526
|
if (claim && owns) await tx.delete(NAMES, claim);
|
|
499
|
-
if (sharing) await tx.delete("resources", node.id);
|
|
500
|
-
for (const share of shares) await tx.delete(share.collection, share.id);
|
|
501
527
|
}
|
|
502
528
|
});
|
|
503
529
|
}
|
|
@@ -718,21 +744,23 @@ function createDrive(deps) {
|
|
|
718
744
|
async deleteAll(ctx) {
|
|
719
745
|
assertInPerson(ctx, "delete the whole drive");
|
|
720
746
|
await need(ctx, "delete", rootNode(ctx.owner, ctx.actor));
|
|
721
|
-
return { removed: await eraseDrive(ctx.owner) };
|
|
747
|
+
return { removed: await eraseDrive(ctx.owner, ctx.principal) };
|
|
722
748
|
},
|
|
723
749
|
/** Deletes a trashed item for good, with everything under it and all their bytes. */
|
|
724
750
|
async purge(ctx, id) {
|
|
725
|
-
const
|
|
751
|
+
const marked = await rawNode(store, id, ctx.owner, ctx.actor, true);
|
|
752
|
+
const resumed = marked?.purging === true && sharing !== void 0 && isInPerson(ctx) && marked.owner === ctx.owner && await store.get("resources", marked.id) === null;
|
|
753
|
+
const top = resumed ? marked : await fetch(store, ctx, id, true);
|
|
726
754
|
if (!top || top.id === ROOT_ID) throw new DriveError("NOT_FOUND", `no item ${id}`);
|
|
727
755
|
if (!top.trashed) throw new DriveError("NOT_TRASHED", `${top.name} is not in the trash`);
|
|
728
|
-
await need(ctx, "delete", top);
|
|
756
|
+
if (!resumed) await need(ctx, "delete", top);
|
|
729
757
|
const nodes = [];
|
|
730
758
|
for (const queue = [top]; queue.length; ) {
|
|
731
759
|
const node = queue.pop();
|
|
732
760
|
nodes.push(node);
|
|
733
761
|
if (node.kind === "folder") queue.push(...await children(store, node.owner, node.id));
|
|
734
762
|
}
|
|
735
|
-
return { removed: await removeNodes(nodes, top) };
|
|
763
|
+
return { removed: await removeNodes(nodes, top, ctx.principal) };
|
|
736
764
|
},
|
|
737
765
|
/**
|
|
738
766
|
* What the home view shows: every file and every reference to an app's document, newest change first.
|
|
@@ -812,7 +840,7 @@ function createDrive(deps) {
|
|
|
812
840
|
by: [...new Set([actors.includes(n.createdById) ? n.createdBy : null, actors.includes(n.updatedById) ? n.updatedBy : null].filter((a) => a !== null).map((a) => JSON.stringify(a)))].map((a) => JSON.parse(a))
|
|
813
841
|
})).sort((x, y) => x.id < y.id ? -1 : 1);
|
|
814
842
|
const held = [];
|
|
815
|
-
for (const actor of
|
|
843
|
+
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 });
|
|
816
844
|
const agents = [];
|
|
817
845
|
for (const agent of ownedAgents) {
|
|
818
846
|
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 }));
|
|
@@ -836,7 +864,7 @@ function createDrive(deps) {
|
|
|
836
864
|
* they hold and the invites they or their agents sent, on drive items only: the grants of other apps stay for
|
|
837
865
|
* those apps and for identity. What they or their agents made in other drives stays, because it belongs to
|
|
838
866
|
* those drives: the id becomes "erased", and the kind stays, so machine-made work keeps its label. The audit
|
|
839
|
-
*
|
|
867
|
+
* entries of the erased items go with them. The ones about what the person did in other drives stay: they hold ids and kinds only. A failed call can run again.
|
|
840
868
|
*/
|
|
841
869
|
async erasePerson(ctx) {
|
|
842
870
|
assertInPerson(ctx, "erase a person");
|
|
@@ -859,7 +887,7 @@ function createDrive(deps) {
|
|
|
859
887
|
};
|
|
860
888
|
let grants = 0;
|
|
861
889
|
let invites = 0;
|
|
862
|
-
for (const actor of
|
|
890
|
+
for (const [actor, kind] of heldBy(subjectOf(ctx), ownedAgents)) grants += await sweep(await ofDrive(await allWhere(store, "grants", { subjectId: actor, subjectKind: kind })), "grants");
|
|
863
891
|
for (const actor of actors) invites += await sweep(await ofDrive(await allWhere(store, "invites", { createdBy: actor })), "invites");
|
|
864
892
|
const todo = await madeBy(store, actors);
|
|
865
893
|
for (let i = 0; i < todo.length; i += CHUNK) {
|
|
@@ -880,13 +908,13 @@ function createDrive(deps) {
|
|
|
880
908
|
return { removed, grants, invites, anonymized: todo.length };
|
|
881
909
|
},
|
|
882
910
|
/** Gives a person or an agent a role on an item and everything under it. Only an owner may. */
|
|
883
|
-
async shareWith(ctx, loc,
|
|
911
|
+
async shareWith(ctx, loc, subject, role) {
|
|
884
912
|
const resource = await resourceOf(await resolve(store, ctx, loc));
|
|
885
|
-
await viaSharing(ctx, (s, p) => s.grant(p, resource,
|
|
913
|
+
await viaSharing(ctx, (s, p) => s.grant(p, resource, parseSubject(subject), role));
|
|
886
914
|
},
|
|
887
|
-
async unshare(ctx, loc,
|
|
915
|
+
async unshare(ctx, loc, subject) {
|
|
888
916
|
const resource = await resourceOf(await resolve(store, ctx, loc));
|
|
889
|
-
await viaSharing(ctx, (s, p) => s.revokeGrant(p, resource,
|
|
917
|
+
await viaSharing(ctx, (s, p) => s.revokeGrant(p, resource, parseSubject(subject)));
|
|
890
918
|
},
|
|
891
919
|
/** Invite by `email` (a person) or `handle` (an agent). The invite lasts `expiresInMs`, if given. It sends no message. */
|
|
892
920
|
async invite(ctx, loc, input) {
|
|
@@ -912,7 +940,8 @@ function createDrive(deps) {
|
|
|
912
940
|
const where = { resourceId: await resourceOf(node) };
|
|
913
941
|
const rows = async (collection) => (await store.query(collection, { where, limit: 100 })).items.map((item) => item.data);
|
|
914
942
|
return {
|
|
915
|
-
|
|
943
|
+
// A grant with no valid kind is from before 0.4.0 and not migrated. Identity does not honor it, so the list leaves it out.
|
|
944
|
+
grants: (await rows("grants")).filter((g) => SUBJECT_KINDS.includes(g.subjectKind)).map((g) => ({ subjectKind: g.subjectKind, subjectId: g.subjectId, role: g.role })),
|
|
916
945
|
links: (await rows("links")).map((l) => ({ id: l.id, role: l.role, signedInOnly: l.signedInOnly === true, expiresAt: l.expiresAt ?? null, revoked: l.revokedAt != null })),
|
|
917
946
|
invites: (await rows("invites")).map((i) => ({ id: i.id, role: i.role, email: i.email ?? null, handle: i.handle ?? null, expiresAt: i.expiresAt ?? null, accepted: i.acceptedAt !== void 0 }))
|
|
918
947
|
};
|
|
@@ -1337,6 +1366,8 @@ export {
|
|
|
1337
1366
|
safeMime,
|
|
1338
1367
|
parseName,
|
|
1339
1368
|
rootResource,
|
|
1369
|
+
subjectKindOf,
|
|
1370
|
+
parseSubject,
|
|
1340
1371
|
createDrive,
|
|
1341
1372
|
writeZip,
|
|
1342
1373
|
readZip,
|
package/dist/cli.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
VERSION
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-GMA5I5L6.js";
|
|
5
5
|
import {
|
|
6
6
|
DriveError,
|
|
7
7
|
buildExport,
|
|
8
8
|
openLocal,
|
|
9
9
|
publicNode,
|
|
10
10
|
upload
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-VSVIJBMJ.js";
|
|
12
12
|
|
|
13
13
|
// src/cli.ts
|
|
14
14
|
import { createReadStream, realpathSync, writeFileSync } from "fs";
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { E as ErrorCode, N as Node, S as SearchQuery } from './drive-Dlzwukjq.js';
|
|
2
|
+
import 'agentic-identity';
|
|
3
|
+
|
|
4
|
+
type Role = "viewer" | "commenter" | "editor" | "owner";
|
|
5
|
+
type OnConflict = "fail" | "rename" | "replace";
|
|
6
|
+
type SubjectKind = "human" | "agent";
|
|
7
|
+
type Subject = {
|
|
8
|
+
kind: SubjectKind;
|
|
9
|
+
id: string;
|
|
10
|
+
};
|
|
11
|
+
/** An item as the API prints it: no storage keys. */
|
|
12
|
+
type Item = Omit<Node, "nameKey" | "blobKey" | "purging">;
|
|
13
|
+
/** A search hit, or a home item: an item with its path. */
|
|
14
|
+
type PathItem = Item & {
|
|
15
|
+
path: string;
|
|
16
|
+
};
|
|
17
|
+
/** The codes of the drive, and the ones that only the Worker raises. */
|
|
18
|
+
type ApiErrorCode = ErrorCode | "UNAUTHORIZED" | "NOT_CONFIGURED" | "INVALID_LINK" | "INTERNAL";
|
|
19
|
+
/** `NETWORK_ERROR` means no answer came (status 0). `BAD_RESPONSE` means an answer that is not the API's. */
|
|
20
|
+
type ClientErrorCode = ApiErrorCode | "NETWORK_ERROR" | "BAD_RESPONSE";
|
|
21
|
+
/** The error of every failed call. `code` is the code of the API, and `status` is the HTTP status. */
|
|
22
|
+
declare class DriveApiError extends Error {
|
|
23
|
+
readonly code: ClientErrorCode;
|
|
24
|
+
readonly status: number;
|
|
25
|
+
constructor(code: ClientErrorCode, status: number, message: string);
|
|
26
|
+
}
|
|
27
|
+
interface ClientOptions {
|
|
28
|
+
/** The origin of the Worker, such as `https://drive.example.com`. */
|
|
29
|
+
baseUrl: string;
|
|
30
|
+
/** A bearer token. It is optional only for the link routes, `openShared`, `listShared`, and `readShared`. */
|
|
31
|
+
token?: string;
|
|
32
|
+
/** Defaults to the global `fetch`. */
|
|
33
|
+
fetch?: typeof fetch;
|
|
34
|
+
/** How often a read (GET) call tries again after a network error or a 502, 503, or 504. The default is 0. A write is never tried again. */
|
|
35
|
+
retries?: number;
|
|
36
|
+
}
|
|
37
|
+
interface CallOptions {
|
|
38
|
+
signal?: AbortSignal;
|
|
39
|
+
}
|
|
40
|
+
type UploadBody = string | Uint8Array | ArrayBuffer | Blob | ReadableStream<Uint8Array>;
|
|
41
|
+
interface Downloaded {
|
|
42
|
+
data: Uint8Array;
|
|
43
|
+
contentType: string;
|
|
44
|
+
sha256: string;
|
|
45
|
+
}
|
|
46
|
+
declare function createClient(options: ClientOptions): {
|
|
47
|
+
/** The live items of a folder. */
|
|
48
|
+
list: (path?: string, o?: CallOptions) => Promise<Item[]>;
|
|
49
|
+
stat: (path: string, o?: CallOptions) => Promise<Item>;
|
|
50
|
+
mkdir: (path: string, opts?: {
|
|
51
|
+
parents?: boolean;
|
|
52
|
+
}, o?: CallOptions) => Promise<Item>;
|
|
53
|
+
/** Writes a file. A `ReadableStream` body streams in runtimes that allow it. */
|
|
54
|
+
upload: (path: string, body: UploadBody, opts?: {
|
|
55
|
+
mime?: string;
|
|
56
|
+
parents?: boolean;
|
|
57
|
+
onConflict?: OnConflict;
|
|
58
|
+
}, o?: CallOptions) => Promise<Item>;
|
|
59
|
+
/** The bytes. They are checked against the hash that the server sends. */
|
|
60
|
+
download: (path: string, o?: CallOptions) => Promise<Downloaded>;
|
|
61
|
+
/** A signed link that works without a token, for `expires` seconds. You fetch the link yourself. */
|
|
62
|
+
url: (path: string, expires?: number, o?: CallOptions) => Promise<{
|
|
63
|
+
url: string;
|
|
64
|
+
expiresInSeconds: number;
|
|
65
|
+
}>;
|
|
66
|
+
move: (src: string, folder: string, opts?: {
|
|
67
|
+
name?: string;
|
|
68
|
+
onConflict?: OnConflict;
|
|
69
|
+
}, o?: CallOptions) => Promise<Item>;
|
|
70
|
+
trash: (path: string, o?: CallOptions) => Promise<Item>;
|
|
71
|
+
listTrash: (o?: CallOptions) => Promise<Item[]>;
|
|
72
|
+
restore: (itemId: string, opts?: {
|
|
73
|
+
parentId?: string;
|
|
74
|
+
onConflict?: OnConflict;
|
|
75
|
+
}, o?: CallOptions) => Promise<Item>;
|
|
76
|
+
/** Deletes a trashed item for good. */
|
|
77
|
+
purge: (itemId: string, o?: CallOptions) => Promise<{
|
|
78
|
+
removed: number;
|
|
79
|
+
}>;
|
|
80
|
+
search: (q?: Pick<SearchQuery, "name" | "mime" | "kind" | "minSize" | "maxSize" | "createdByKind" | "app" | "limit">, o?: CallOptions) => Promise<PathItem[]>;
|
|
81
|
+
/** Everything the caller has, as a zip. */
|
|
82
|
+
export: (o?: CallOptions) => Promise<Uint8Array<ArrayBufferLike>>;
|
|
83
|
+
/** Erases the caller for good. The client sends `confirm=yes`. */
|
|
84
|
+
deleteAccount: (o?: CallOptions) => Promise<{
|
|
85
|
+
removed: number;
|
|
86
|
+
grants: number;
|
|
87
|
+
invites: number;
|
|
88
|
+
anonymized: number;
|
|
89
|
+
}>;
|
|
90
|
+
home: (q?: {
|
|
91
|
+
app?: string;
|
|
92
|
+
limit?: number;
|
|
93
|
+
}, o?: CallOptions) => Promise<{
|
|
94
|
+
apps: Record<string, number>;
|
|
95
|
+
files: number;
|
|
96
|
+
items: (PathItem & {
|
|
97
|
+
app: string | null;
|
|
98
|
+
})[];
|
|
99
|
+
}>;
|
|
100
|
+
registerDocument: (input: {
|
|
101
|
+
ref: string;
|
|
102
|
+
name: string;
|
|
103
|
+
folder?: string;
|
|
104
|
+
}, o?: CallOptions) => Promise<Item>;
|
|
105
|
+
forgetDocument: (ref: string, o?: CallOptions) => Promise<{
|
|
106
|
+
trashed: number;
|
|
107
|
+
}>;
|
|
108
|
+
shares: (path: string, o?: CallOptions) => Promise<{
|
|
109
|
+
grants: {
|
|
110
|
+
subjectKind: SubjectKind;
|
|
111
|
+
subjectId: string;
|
|
112
|
+
role: Role;
|
|
113
|
+
}[];
|
|
114
|
+
links: {
|
|
115
|
+
id: string;
|
|
116
|
+
role: Role;
|
|
117
|
+
signedInOnly: boolean;
|
|
118
|
+
expiresAt: number | null;
|
|
119
|
+
revoked: boolean;
|
|
120
|
+
}[];
|
|
121
|
+
invites: {
|
|
122
|
+
id: string;
|
|
123
|
+
role: Role;
|
|
124
|
+
email: string | null;
|
|
125
|
+
handle: string | null;
|
|
126
|
+
expiresAt: number | null;
|
|
127
|
+
accepted: boolean;
|
|
128
|
+
}[];
|
|
129
|
+
}>;
|
|
130
|
+
share: (path: string, subject: Subject, role: Role, o?: CallOptions) => Promise<{
|
|
131
|
+
ok: true;
|
|
132
|
+
}>;
|
|
133
|
+
unshare: (path: string, subject: Subject, o?: CallOptions) => Promise<{
|
|
134
|
+
ok: true;
|
|
135
|
+
}>;
|
|
136
|
+
/** Invite by `email` (a person) or `handle` (an agent). It sends no message. */
|
|
137
|
+
invite: (path: string, input: {
|
|
138
|
+
role: Role;
|
|
139
|
+
email?: string;
|
|
140
|
+
handle?: string;
|
|
141
|
+
expiresInMs?: number;
|
|
142
|
+
}, o?: CallOptions) => Promise<{
|
|
143
|
+
id: string;
|
|
144
|
+
}>;
|
|
145
|
+
acceptInvite: (inviteId: string, o?: CallOptions) => Promise<{
|
|
146
|
+
ok: true;
|
|
147
|
+
}>;
|
|
148
|
+
/** The token of a link comes back once. */
|
|
149
|
+
createLink: (path: string, input: {
|
|
150
|
+
role: Role;
|
|
151
|
+
expiresInMs?: number;
|
|
152
|
+
signedInOnly?: boolean;
|
|
153
|
+
}, o?: CallOptions) => Promise<{
|
|
154
|
+
id: string;
|
|
155
|
+
token: string;
|
|
156
|
+
}>;
|
|
157
|
+
revokeLink: (linkId: string, o?: CallOptions) => Promise<{
|
|
158
|
+
ok: true;
|
|
159
|
+
}>;
|
|
160
|
+
/** The next three read a shared item by id. Pass a link token, a bearer token, or both. */
|
|
161
|
+
openShared: (itemId: string, opts?: {
|
|
162
|
+
linkToken?: string;
|
|
163
|
+
} & CallOptions) => Promise<Item>;
|
|
164
|
+
listShared: (itemId: string, opts?: {
|
|
165
|
+
linkToken?: string;
|
|
166
|
+
} & CallOptions) => Promise<Item[]>;
|
|
167
|
+
readShared: (itemId: string, opts?: {
|
|
168
|
+
linkToken?: string;
|
|
169
|
+
} & CallOptions) => Promise<Omit<Downloaded, "sha256">>;
|
|
170
|
+
};
|
|
171
|
+
type DriveClient = ReturnType<typeof createClient>;
|
|
172
|
+
|
|
173
|
+
export { type ApiErrorCode, type CallOptions, type ClientErrorCode, type ClientOptions, type Downloaded, DriveApiError, type DriveClient, type Item, type OnConflict, type PathItem, type Role, type Subject, type SubjectKind, type UploadBody, createClient };
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
// src/client.ts
|
|
2
|
+
var DriveApiError = class extends Error {
|
|
3
|
+
constructor(code, status, message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.code = code;
|
|
6
|
+
this.status = status;
|
|
7
|
+
this.name = "DriveApiError";
|
|
8
|
+
}
|
|
9
|
+
code;
|
|
10
|
+
status;
|
|
11
|
+
};
|
|
12
|
+
var BASE = "/api/v1/drive";
|
|
13
|
+
var CODES = {
|
|
14
|
+
NOT_FOUND: true,
|
|
15
|
+
NAME_TAKEN: true,
|
|
16
|
+
BLOB_EXISTS: true,
|
|
17
|
+
INVALID_NAME: true,
|
|
18
|
+
INVALID_PATH: true,
|
|
19
|
+
INVALID_REF: true,
|
|
20
|
+
NOT_A_FOLDER: true,
|
|
21
|
+
NOT_A_FILE: true,
|
|
22
|
+
MOVE_INTO_SELF: true,
|
|
23
|
+
TOO_DEEP: true,
|
|
24
|
+
PARENT_GONE: true,
|
|
25
|
+
TRASHED: true,
|
|
26
|
+
NOT_TRASHED: true,
|
|
27
|
+
IS_ROOT: true,
|
|
28
|
+
FORBIDDEN: true,
|
|
29
|
+
BLOB_MISSING: true,
|
|
30
|
+
TOO_LARGE: true,
|
|
31
|
+
INVALID_EXPIRY: true,
|
|
32
|
+
BUSY: true,
|
|
33
|
+
CROSS_DRIVE: true,
|
|
34
|
+
INVALID_SHARE: true,
|
|
35
|
+
TOO_MANY_ITEMS: true,
|
|
36
|
+
UNAUTHORIZED: true,
|
|
37
|
+
NOT_CONFIGURED: true,
|
|
38
|
+
INVALID_LINK: true,
|
|
39
|
+
INTERNAL: true
|
|
40
|
+
};
|
|
41
|
+
var KNOWN_CODES = new Set(Object.keys(CODES));
|
|
42
|
+
var record = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
43
|
+
var text = (v) => typeof v === "string";
|
|
44
|
+
var orNull = (test) => (v) => v === null || test(v);
|
|
45
|
+
var num = (v) => typeof v === "number";
|
|
46
|
+
var ACTOR_KINDS = ["human", "agent_of_human", "agent_of_agent", "agent", "user_token"];
|
|
47
|
+
var ROLES = ["viewer", "commenter", "editor", "owner"];
|
|
48
|
+
var oneOf = (list) => (v) => typeof v === "string" && list.includes(v);
|
|
49
|
+
var actor = (v) => record(v) && text(v.id) && oneOf(ACTOR_KINDS)(v.kind);
|
|
50
|
+
var isItem = (v) => record(v) && text(v.id) && text(v.owner) && text(v.parentId) && text(v.name) && (v.kind === "folder" || v.kind === "file" || v.kind === "ref") && num(v.size) && orNull(text)(v.mime) && orNull(text)(v.hash) && orNull(text)(v.ref) && num(v.createdAt) && num(v.updatedAt) && actor(v.createdBy) && actor(v.updatedBy) && text(v.createdById) && text(v.updatedById) && typeof v.trashed === "boolean" && orNull(num)(v.trashedAt);
|
|
51
|
+
var isPathItem = (v) => isItem(v) && typeof v.path === "string";
|
|
52
|
+
var listOf = (check) => (v) => Array.isArray(v) && v.every(check);
|
|
53
|
+
var shape = (test) => (v) => record(v) && test(v);
|
|
54
|
+
var numbers = (...keys) => shape((v) => keys.every((k) => typeof v[k] === "number"));
|
|
55
|
+
var isOk = shape((v) => v.ok === true);
|
|
56
|
+
var RETRY_STATUS = /* @__PURE__ */ new Set([502, 503, 504]);
|
|
57
|
+
function createClient(options) {
|
|
58
|
+
let origin;
|
|
59
|
+
try {
|
|
60
|
+
const url = new URL(options.baseUrl);
|
|
61
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error("not http");
|
|
62
|
+
origin = url.origin;
|
|
63
|
+
} catch {
|
|
64
|
+
throw new TypeError("baseUrl must be an http or https URL");
|
|
65
|
+
}
|
|
66
|
+
const { token } = options;
|
|
67
|
+
if (token !== void 0 && (typeof token !== "string" || !/^[A-Za-z0-9_-]{1,512}$/.test(token))) throw new TypeError("token must be 1 to 512 letters, digits, - or _");
|
|
68
|
+
const retries = options.retries ?? 0;
|
|
69
|
+
if (!Number.isInteger(retries) || retries < 0 || retries > 5) throw new RangeError("retries must be an integer from 0 to 5");
|
|
70
|
+
const doFetch = options.fetch ?? ((input, init) => globalThis.fetch(input, init));
|
|
71
|
+
const query = (params) => {
|
|
72
|
+
const search = new URLSearchParams();
|
|
73
|
+
for (const [key, value] of Object.entries(params)) if (value !== void 0) search.set(key, String(value));
|
|
74
|
+
const text2 = search.toString();
|
|
75
|
+
return text2 === "" ? "" : `?${text2}`;
|
|
76
|
+
};
|
|
77
|
+
async function send(method, path, init) {
|
|
78
|
+
if (init.auth !== false && token === void 0) throw new TypeError("this call needs a token");
|
|
79
|
+
const headers = { ...token === void 0 ? {} : { authorization: `Bearer ${token}` }, ...init.headers };
|
|
80
|
+
let body = init.body;
|
|
81
|
+
if (init.json !== void 0) {
|
|
82
|
+
headers["content-type"] = "application/json";
|
|
83
|
+
body = JSON.stringify(init.json);
|
|
84
|
+
}
|
|
85
|
+
const attempts = method === "GET" ? retries + 1 : 1;
|
|
86
|
+
for (let attempt = 1; ; attempt++) {
|
|
87
|
+
let response;
|
|
88
|
+
try {
|
|
89
|
+
response = await doFetch(`${origin}${BASE}${path}`, { method, headers, body, signal: init.signal, redirect: "manual", ...body instanceof ReadableStream ? { duplex: "half" } : {} });
|
|
90
|
+
} catch (error) {
|
|
91
|
+
if (init.signal?.aborted || error instanceof Error && error.name === "AbortError") throw error;
|
|
92
|
+
if (attempt < attempts) {
|
|
93
|
+
await pause(attempt, init.signal);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const refused = body instanceof ReadableStream ? " (a stream body may be refused by this runtime: send bytes)" : "";
|
|
97
|
+
throw new DriveApiError("NETWORK_ERROR", 0, `${error instanceof Error ? `no answer: ${error.message}` : "no answer"}${refused}`);
|
|
98
|
+
}
|
|
99
|
+
if (attempt < attempts && RETRY_STATUS.has(response.status)) {
|
|
100
|
+
await response.body?.cancel();
|
|
101
|
+
await pause(attempt, init.signal);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (response.type === "opaqueredirect" || response.status >= 300 && response.status < 400) {
|
|
105
|
+
await response.body?.cancel();
|
|
106
|
+
throw new DriveApiError("BAD_RESPONSE", response.status, "the server redirected. Set baseUrl to the final URL.");
|
|
107
|
+
}
|
|
108
|
+
if (response.ok) return response;
|
|
109
|
+
throw await errorOf(response, init.signal);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
async function pause(attempt, signal) {
|
|
113
|
+
signal?.throwIfAborted();
|
|
114
|
+
await new Promise((resolve, reject) => {
|
|
115
|
+
const timer = setTimeout(() => {
|
|
116
|
+
signal?.removeEventListener("abort", stop);
|
|
117
|
+
resolve();
|
|
118
|
+
}, Math.min(100 * 2 ** (attempt - 1), 1e3));
|
|
119
|
+
const stop = () => {
|
|
120
|
+
clearTimeout(timer);
|
|
121
|
+
reject(signal?.reason);
|
|
122
|
+
};
|
|
123
|
+
signal?.addEventListener("abort", stop, { once: true });
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
async function errorOf(response, signal) {
|
|
127
|
+
let code = "BAD_RESPONSE";
|
|
128
|
+
let message = `the server answered ${response.status}`;
|
|
129
|
+
try {
|
|
130
|
+
const parsed = await response.json();
|
|
131
|
+
if (typeof parsed.error?.code === "string" && KNOWN_CODES.has(parsed.error.code)) code = parsed.error.code;
|
|
132
|
+
else if (typeof parsed.error?.code === "string") message = `the server answered ${response.status} with an unknown code, ${JSON.stringify(parsed.error.code.slice(0, 40))}`;
|
|
133
|
+
if (code !== "BAD_RESPONSE" && typeof parsed.error?.message === "string") message = parsed.error.message;
|
|
134
|
+
} catch (error) {
|
|
135
|
+
if (signal?.aborted) throw error;
|
|
136
|
+
}
|
|
137
|
+
return new DriveApiError(code, response.status, message);
|
|
138
|
+
}
|
|
139
|
+
async function json(method, path, init, check) {
|
|
140
|
+
const response = await send(method, path, init);
|
|
141
|
+
let value;
|
|
142
|
+
try {
|
|
143
|
+
value = await response.json();
|
|
144
|
+
} catch (error) {
|
|
145
|
+
if (init.signal?.aborted) throw error;
|
|
146
|
+
if (!(error instanceof SyntaxError)) throw new DriveApiError("NETWORK_ERROR", 0, "the connection dropped while the body came in");
|
|
147
|
+
throw new DriveApiError("BAD_RESPONSE", response.status, "the answer is not JSON");
|
|
148
|
+
}
|
|
149
|
+
if (!check(value)) throw new DriveApiError("BAD_RESPONSE", response.status, "the answer is not the shape that the API sends");
|
|
150
|
+
return value;
|
|
151
|
+
}
|
|
152
|
+
async function bytes(path, init, verify = false) {
|
|
153
|
+
const response = await send("GET", path, init);
|
|
154
|
+
let data;
|
|
155
|
+
try {
|
|
156
|
+
data = new Uint8Array(await response.arrayBuffer());
|
|
157
|
+
} catch (error) {
|
|
158
|
+
if (init.signal?.aborted) throw error;
|
|
159
|
+
throw new DriveApiError("NETWORK_ERROR", 0, "the connection dropped while the body came in");
|
|
160
|
+
}
|
|
161
|
+
const sha256 = response.headers.get("x-content-sha256") ?? "";
|
|
162
|
+
if (verify) {
|
|
163
|
+
if (!/^[0-9a-f]{64}$/.test(sha256)) throw new DriveApiError("BAD_RESPONSE", response.status, "the answer has no valid x-content-sha256");
|
|
164
|
+
if (typeof crypto === "undefined" || !crypto.subtle) throw new DriveApiError("BAD_RESPONSE", response.status, "this runtime has no WebCrypto, so the bytes cannot be checked. Use https.");
|
|
165
|
+
const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", data));
|
|
166
|
+
if (Array.from(digest, (b) => b.toString(16).padStart(2, "0")).join("") !== sha256) throw new DriveApiError("BAD_RESPONSE", response.status, "the bytes do not match their hash");
|
|
167
|
+
}
|
|
168
|
+
return { data, contentType: response.headers.get("content-type") ?? "application/octet-stream", sha256 };
|
|
169
|
+
}
|
|
170
|
+
const link = (token2) => {
|
|
171
|
+
if (token2 === void 0) return void 0;
|
|
172
|
+
if (!/^[A-Za-z0-9_-]{1,512}$/.test(token2)) throw new TypeError("a link token has 1 to 512 letters, digits, - or _");
|
|
173
|
+
return { "x-link-token": token2 };
|
|
174
|
+
};
|
|
175
|
+
const id = (value) => {
|
|
176
|
+
if (typeof value !== "string" || !/^[A-Za-z0-9_-][A-Za-z0-9_.-]*$/.test(value) || value === "." || value === "..") throw new TypeError("an item id has letters, digits, -, _ and . (not first), and is not . or ..");
|
|
177
|
+
return value;
|
|
178
|
+
};
|
|
179
|
+
const isUrl = shape((v) => typeof v.url === "string" && /^https?:\/\//.test(v.url) && URL.canParse(v.url) && typeof v.expiresInSeconds === "number");
|
|
180
|
+
const isShares = shape(
|
|
181
|
+
(v) => listOf(shape((g) => (g.subjectKind === "human" || g.subjectKind === "agent") && text(g.subjectId) && oneOf(ROLES)(g.role)))(v.grants) && listOf(shape((l) => text(l.id) && oneOf(ROLES)(l.role) && typeof l.signedInOnly === "boolean" && orNull(num)(l.expiresAt) && typeof l.revoked === "boolean"))(v.links) && listOf(shape((i) => text(i.id) && oneOf(ROLES)(i.role) && orNull(text)(i.email) && orNull(text)(i.handle) && orNull(num)(i.expiresAt) && typeof i.accepted === "boolean"))(v.invites)
|
|
182
|
+
);
|
|
183
|
+
const isHome = shape((v) => record(v.apps) && Object.values(v.apps).every(num) && num(v.files) && listOf(shape((i) => isPathItem(i) && orNull(text)(i.app)))(v.items));
|
|
184
|
+
const isId = shape((v) => typeof v.id === "string");
|
|
185
|
+
const isLink = shape((v) => typeof v.id === "string" && typeof v.token === "string");
|
|
186
|
+
return {
|
|
187
|
+
/** The live items of a folder. */
|
|
188
|
+
list: (path = "/", o = {}) => json("GET", `/items${query({ path })}`, o, listOf(isItem)),
|
|
189
|
+
stat: (path, o = {}) => json("GET", `/stat${query({ path })}`, o, isItem),
|
|
190
|
+
mkdir: (path, opts = {}, o = {}) => json("POST", "/folders", { json: { path, parents: opts.parents === true }, ...o }, isItem),
|
|
191
|
+
/** Writes a file. A `ReadableStream` body streams in runtimes that allow it. */
|
|
192
|
+
upload: (path, body, opts = {}, o = {}) => json("PUT", `/files${query({ path, mime: opts.mime, parents: opts.parents === true ? true : void 0, onConflict: opts.onConflict })}`, {
|
|
193
|
+
body: typeof body === "string" ? new TextEncoder().encode(body) : body,
|
|
194
|
+
headers: { "content-type": "application/octet-stream" },
|
|
195
|
+
...o
|
|
196
|
+
}, isItem),
|
|
197
|
+
/** The bytes. They are checked against the hash that the server sends. */
|
|
198
|
+
download: (path, o = {}) => bytes(`/files${query({ path })}`, o, true),
|
|
199
|
+
/** A signed link that works without a token, for `expires` seconds. You fetch the link yourself. */
|
|
200
|
+
url: (path, expires, o = {}) => json("GET", `/url${query({ path, expires })}`, o, isUrl),
|
|
201
|
+
move: (src, folder, opts = {}, o = {}) => json("POST", "/move", { json: { src, folder, ...opts }, ...o }, isItem),
|
|
202
|
+
trash: (path, o = {}) => json("POST", "/trash", { json: { path }, ...o }, isItem),
|
|
203
|
+
listTrash: (o = {}) => json("GET", "/trash", o, listOf(isItem)),
|
|
204
|
+
restore: (itemId, opts = {}, o = {}) => json("POST", "/restore", { json: { id: itemId, ...opts }, ...o }, isItem),
|
|
205
|
+
/** Deletes a trashed item for good. */
|
|
206
|
+
purge: (itemId, o = {}) => json("POST", "/purge", { json: { id: itemId }, ...o }, numbers("removed")),
|
|
207
|
+
search: (q = {}, o = {}) => json("GET", `/search${query(q)}`, o, listOf(isPathItem)),
|
|
208
|
+
/** Everything the caller has, as a zip. */
|
|
209
|
+
export: async (o = {}) => {
|
|
210
|
+
const { data } = await bytes("/export", o);
|
|
211
|
+
if (data[0] !== 80 || data[1] !== 75) throw new DriveApiError("BAD_RESPONSE", 200, "the answer is not a zip");
|
|
212
|
+
return data;
|
|
213
|
+
},
|
|
214
|
+
/** Erases the caller for good. The client sends `confirm=yes`. */
|
|
215
|
+
deleteAccount: (o = {}) => json("DELETE", "/account?confirm=yes", o, numbers("removed", "grants", "invites", "anonymized")),
|
|
216
|
+
home: (q = {}, o = {}) => json("GET", `/home${query(q)}`, o, isHome),
|
|
217
|
+
registerDocument: (input, o = {}) => json("POST", "/documents", { json: input, ...o }, isItem),
|
|
218
|
+
forgetDocument: (ref, o = {}) => json("DELETE", `/documents${query({ ref })}`, o, numbers("trashed")),
|
|
219
|
+
shares: (path, o = {}) => json("GET", `/shares${query({ path })}`, o, isShares),
|
|
220
|
+
share: (path, subject, role, o = {}) => json("POST", "/shares", { json: { path, subjectId: subject.id, subjectKind: subject.kind, role }, ...o }, isOk),
|
|
221
|
+
unshare: (path, subject, o = {}) => json("DELETE", `/shares${query({ path, subjectId: subject.id, subjectKind: subject.kind })}`, o, isOk),
|
|
222
|
+
/** Invite by `email` (a person) or `handle` (an agent). It sends no message. */
|
|
223
|
+
invite: (path, input, o = {}) => json("POST", "/invites", { json: { path, ...input }, ...o }, isId),
|
|
224
|
+
acceptInvite: (inviteId, o = {}) => json("POST", "/invites/accept", { json: { id: inviteId }, ...o }, isOk),
|
|
225
|
+
/** The token of a link comes back once. */
|
|
226
|
+
createLink: (path, input, o = {}) => json("POST", "/links", { json: { path, ...input }, ...o }, isLink),
|
|
227
|
+
revokeLink: (linkId, o = {}) => json("DELETE", `/links${query({ id: linkId })}`, o, isOk),
|
|
228
|
+
/** The next three read a shared item by id. Pass a link token, a bearer token, or both. */
|
|
229
|
+
openShared: async (itemId, opts = {}) => json("GET", `/shared/${id(itemId)}`, { auth: false, headers: link(opts.linkToken), signal: opts.signal }, isItem),
|
|
230
|
+
listShared: async (itemId, opts = {}) => json("GET", `/shared/${id(itemId)}/items`, { auth: false, headers: link(opts.linkToken), signal: opts.signal }, listOf(isItem)),
|
|
231
|
+
readShared: async (itemId, opts = {}) => {
|
|
232
|
+
const { data, contentType } = await bytes(`/shared/${id(itemId)}/content`, { auth: false, headers: link(opts.linkToken), signal: opts.signal });
|
|
233
|
+
return { data, contentType };
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
export {
|
|
238
|
+
DriveApiError,
|
|
239
|
+
createClient
|
|
240
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as agentic_identity from 'agentic-identity';
|
|
2
|
-
import { PrincipalContext, Store, Blobs, createSharing, Role } from 'agentic-identity';
|
|
2
|
+
import { PrincipalContext, Store, Blobs, 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"];
|
|
@@ -87,6 +87,13 @@ declare function parseName(raw: string): {
|
|
|
87
87
|
name: string;
|
|
88
88
|
key: string;
|
|
89
89
|
};
|
|
90
|
+
/**
|
|
91
|
+
* The kind that holds a role, for the kind of a principal at the root of a chain. Only the root of a chain holds a
|
|
92
|
+
* role, so only `human` and `agent` map. Any other kind, a delegated one too, is an error: it never defaults to a person.
|
|
93
|
+
*/
|
|
94
|
+
declare function subjectKindOf(kind: unknown): SubjectKind;
|
|
95
|
+
/** A subject from input: both parts are checked. */
|
|
96
|
+
declare function parseSubject(input: unknown): Subject;
|
|
90
97
|
declare function createDrive(deps: DriveDeps): {
|
|
91
98
|
stat(ctx: Ctx, loc: string): Promise<Node>;
|
|
92
99
|
/** Live children, sorted by name. */
|
|
@@ -193,7 +200,7 @@ declare function createDrive(deps: DriveDeps): {
|
|
|
193
200
|
exportPerson(ctx: Ctx): Promise<{
|
|
194
201
|
subject: {
|
|
195
202
|
id: string;
|
|
196
|
-
kind: "human" | "
|
|
203
|
+
kind: "human" | "agent" | "agent_of_human" | "agent_of_agent" | "user_token" | null;
|
|
197
204
|
};
|
|
198
205
|
partial: boolean;
|
|
199
206
|
authored: {
|
|
@@ -220,6 +227,7 @@ declare function createDrive(deps: DriveDeps): {
|
|
|
220
227
|
}[];
|
|
221
228
|
shares: {
|
|
222
229
|
held: {
|
|
230
|
+
subjectKind: unknown;
|
|
223
231
|
subjectId: unknown;
|
|
224
232
|
resourceId: unknown;
|
|
225
233
|
role: unknown;
|
|
@@ -247,7 +255,7 @@ declare function createDrive(deps: DriveDeps): {
|
|
|
247
255
|
* they hold and the invites they or their agents sent, on drive items only: the grants of other apps stay for
|
|
248
256
|
* those apps and for identity. What they or their agents made in other drives stays, because it belongs to
|
|
249
257
|
* those drives: the id becomes "erased", and the kind stays, so machine-made work keeps its label. The audit
|
|
250
|
-
*
|
|
258
|
+
* entries of the erased items go with them. The ones about what the person did in other drives stay: they hold ids and kinds only. A failed call can run again.
|
|
251
259
|
*/
|
|
252
260
|
erasePerson(ctx: Ctx): Promise<{
|
|
253
261
|
removed: number;
|
|
@@ -256,8 +264,8 @@ declare function createDrive(deps: DriveDeps): {
|
|
|
256
264
|
anonymized: number;
|
|
257
265
|
}>;
|
|
258
266
|
/** Gives a person or an agent a role on an item and everything under it. Only an owner may. */
|
|
259
|
-
shareWith(ctx: Ctx, loc: string,
|
|
260
|
-
unshare(ctx: Ctx, loc: string,
|
|
267
|
+
shareWith(ctx: Ctx, loc: string, subject: Subject, role: Role): Promise<void>;
|
|
268
|
+
unshare(ctx: Ctx, loc: string, subject: Subject): Promise<void>;
|
|
261
269
|
/** Invite by `email` (a person) or `handle` (an agent). The invite lasts `expiresInMs`, if given. It sends no message. */
|
|
262
270
|
invite(ctx: Ctx, loc: string, input: {
|
|
263
271
|
role: Role;
|
|
@@ -285,6 +293,7 @@ declare function createDrive(deps: DriveDeps): {
|
|
|
285
293
|
/** Who can reach an item, and by which links and invites. Only an owner may look. Link tokens are never shown. */
|
|
286
294
|
shares(ctx: Ctx, loc: string): Promise<{
|
|
287
295
|
grants: {
|
|
296
|
+
subjectKind: agentic_identity.Json;
|
|
288
297
|
subjectId: agentic_identity.Json;
|
|
289
298
|
role: agentic_identity.Json;
|
|
290
299
|
}[];
|
|
@@ -322,4 +331,4 @@ declare function createDrive(deps: DriveDeps): {
|
|
|
322
331
|
}>;
|
|
323
332
|
};
|
|
324
333
|
|
|
325
|
-
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 };
|
|
334
|
+
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, parseSubject as h, parseName as p, subjectKindOf as s };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { O as OnConflict, D as Drive, C as Ctx, N as Node, A as ActorKind, a as Actor } from './drive-
|
|
2
|
-
export { b as ACTOR_KINDS, c as Action, d as Can, e as DriveDeps, f as DriveError, E as ErrorCode, R as ROOT_ID, S as SearchQuery, g as createDrive, p as parseName } from './drive-
|
|
1
|
+
import { O as OnConflict, D as Drive, C as Ctx, N as Node, A as ActorKind, a as Actor } from './drive-Dlzwukjq.js';
|
|
2
|
+
export { b as ACTOR_KINDS, c as Action, d as Can, e as DriveDeps, f as DriveError, E as ErrorCode, R as ROOT_ID, S as SearchQuery, g as createDrive, p as parseName, h as parseSubject, s as subjectKindOf } from './drive-Dlzwukjq.js';
|
|
3
3
|
import { AdapterOptions, Blobs, Store } from 'agentic-identity';
|
|
4
4
|
export { B as BackfillOptions, a as BackfillResult, b as backfillAuthorIds, c as backfillResources } from './migrate-B79gRPMu.js';
|
|
5
5
|
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
VERSION
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-GMA5I5L6.js";
|
|
4
4
|
import {
|
|
5
5
|
backfillAuthorIds,
|
|
6
6
|
backfillResources
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-5KEISBS4.js";
|
|
8
8
|
import {
|
|
9
9
|
ACTOR_KINDS,
|
|
10
10
|
DEFAULT_MAX_BYTES,
|
|
@@ -16,12 +16,14 @@ import {
|
|
|
16
16
|
createFileStore,
|
|
17
17
|
openLocal,
|
|
18
18
|
parseName,
|
|
19
|
+
parseSubject,
|
|
19
20
|
publicNode,
|
|
20
21
|
readZip,
|
|
21
22
|
sniffMime,
|
|
23
|
+
subjectKindOf,
|
|
22
24
|
upload,
|
|
23
25
|
writeZip
|
|
24
|
-
} from "./chunk-
|
|
26
|
+
} from "./chunk-VSVIJBMJ.js";
|
|
25
27
|
export {
|
|
26
28
|
ACTOR_KINDS,
|
|
27
29
|
DEFAULT_MAX_BYTES,
|
|
@@ -36,9 +38,11 @@ export {
|
|
|
36
38
|
createFileStore,
|
|
37
39
|
openLocal,
|
|
38
40
|
parseName,
|
|
41
|
+
parseSubject,
|
|
39
42
|
publicNode,
|
|
40
43
|
readZip,
|
|
41
44
|
sniffMime,
|
|
45
|
+
subjectKindOf,
|
|
42
46
|
upload,
|
|
43
47
|
writeZip
|
|
44
48
|
};
|
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-
|
|
2
|
+
import { D as Drive, C as Ctx } from './drive-Dlzwukjq.js';
|
|
3
3
|
import 'agentic-identity';
|
|
4
4
|
|
|
5
5
|
declare function createMcpServer(drive: Drive, ctx: Ctx, exportDir: string): McpServer;
|
package/dist/mcp.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
VERSION
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-GMA5I5L6.js";
|
|
4
4
|
import {
|
|
5
5
|
ACTOR_KINDS,
|
|
6
6
|
DriveError,
|
|
7
7
|
buildExport,
|
|
8
8
|
publicNode,
|
|
9
9
|
upload
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-VSVIJBMJ.js";
|
|
11
11
|
|
|
12
12
|
// src/mcp.ts
|
|
13
13
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
package/dist/worker.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Hono } from 'hono';
|
|
2
2
|
import { D1Like, R2Like } from 'agentic-identity/cloudflare';
|
|
3
3
|
import { Store } from 'agentic-identity';
|
|
4
|
-
import { C as Ctx, D as Drive } from './drive-
|
|
4
|
+
import { C as Ctx, D as Drive } from './drive-Dlzwukjq.js';
|
|
5
5
|
export { B as BackfillOptions, a as BackfillResult, b as backfillAuthorIds, c as backfillResources } from './migrate-B79gRPMu.js';
|
|
6
6
|
|
|
7
7
|
interface WorkerEnv {
|
package/dist/worker.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
backfillAuthorIds,
|
|
3
3
|
backfillResources
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-5KEISBS4.js";
|
|
5
5
|
import {
|
|
6
6
|
DEFAULT_MAX_BYTES,
|
|
7
7
|
DEFAULT_URL_SECONDS,
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
publicNode,
|
|
12
12
|
safeMime,
|
|
13
13
|
upload
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-VSVIJBMJ.js";
|
|
15
15
|
|
|
16
16
|
// src/worker.ts
|
|
17
17
|
import { Hono } from "hono";
|
|
@@ -214,11 +214,11 @@ function createApp() {
|
|
|
214
214
|
api.get("/shares", async (c) => c.json(await c.var.drive.shares(c.var.ctx, need(q(c, "path"), "path"))));
|
|
215
215
|
api.post("/shares", async (c) => {
|
|
216
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));
|
|
217
|
+
await c.var.drive.shareWith(c.var.ctx, str(b.path, "path"), { kind: b.subjectKind, id: str(b.subjectId, "subjectId") }, roleOf(b.role));
|
|
218
218
|
return c.json({ ok: true });
|
|
219
219
|
});
|
|
220
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"));
|
|
221
|
+
await c.var.drive.unshare(c.var.ctx, need(q(c, "path"), "path"), { kind: q(c, "subjectKind"), id: need(q(c, "subjectId"), "subjectId") });
|
|
222
222
|
return c.json({ ok: true });
|
|
223
223
|
});
|
|
224
224
|
api.post("/invites", async (c) => {
|
package/llms.txt
CHANGED
|
@@ -5,6 +5,7 @@ Store, sort, and share files and folders.
|
|
|
5
5
|
Keywords: drive,files,folders,storage,upload,sharing,agents,mcp,cli,r2.
|
|
6
6
|
|
|
7
7
|
- SDK: import { createDrive } from "agentic-drive".
|
|
8
|
+
- Remote SDK: import { createClient } from "agentic-drive/client". createClient({ baseUrl, token, fetch?, retries? }) has one method for each bearer route and each shared route (list, stat, mkdir, upload, download, move, trash, restore, purge, search, share, createLink, and more). A failed call throws DriveApiError with code and status. It runs in browsers, Workers, and Node 22.
|
|
8
9
|
- CLI: agentic-drive ls /.
|
|
9
10
|
- MCP: run agentic-drive mcp, then call drive_ls.
|
|
10
11
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentic-drive",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Store, sort, and share files and folders.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"drive",
|
|
@@ -44,6 +44,10 @@
|
|
|
44
44
|
"types": "./dist/index.d.ts",
|
|
45
45
|
"import": "./dist/index.js"
|
|
46
46
|
},
|
|
47
|
+
"./client": {
|
|
48
|
+
"types": "./dist/client.d.ts",
|
|
49
|
+
"import": "./dist/client.js"
|
|
50
|
+
},
|
|
47
51
|
"./worker": {
|
|
48
52
|
"types": "./dist/worker.d.ts",
|
|
49
53
|
"import": "./dist/worker.js"
|
|
@@ -59,7 +63,7 @@
|
|
|
59
63
|
},
|
|
60
64
|
"dependencies": {
|
|
61
65
|
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
62
|
-
"agentic-identity": "^0.
|
|
66
|
+
"agentic-identity": "^0.4.0",
|
|
63
67
|
"hono": "^4.13.14",
|
|
64
68
|
"zod": "^3.25.0"
|
|
65
69
|
},
|