agentic-drive 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.0
4
+
5
+ ### Added
6
+
7
+ - **`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.
8
+ - **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.
9
+
3
10
  ## 0.5.0
4
11
 
5
12
  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.0, 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-WJZNXLTR.js";
5
5
 
6
6
  // src/migrate.ts
7
7
  var PAGE = 10;
@@ -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.0" : readPackageVersion();
4
4
 
5
5
  export {
6
6
  VERSION
@@ -0,0 +1,241 @@
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: true, agent_of_human: true, agent_of_agent: true, agent: true, user_token: true };
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(Object.keys(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) && typeof v.machineMade === "boolean";
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
+
238
+ export {
239
+ DriveApiError,
240
+ createClient
241
+ };