agentic-drive 0.2.0 → 0.4.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/cli.js CHANGED
@@ -1,14 +1,21 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  VERSION
4
- } from "./chunk-RWMBGIAS.js";
4
+ } from "./chunk-DRACF66Y.js";
5
5
  import {
6
+ AuthError,
6
7
  DriveError,
8
+ NO_TOKEN,
9
+ addPrincipal,
7
10
  buildExport,
11
+ issueKey,
8
12
  openLocal,
9
13
  publicNode,
14
+ resolveToken,
15
+ revokeKey,
16
+ revokePrincipal,
10
17
  upload
11
- } from "./chunk-6CWBHSJ7.js";
18
+ } from "./chunk-ATJDMCRV.js";
12
19
 
13
20
  // src/cli.ts
14
21
  import { createReadStream, realpathSync, writeFileSync } from "fs";
@@ -37,23 +44,33 @@ Commands:
37
44
  register <ref> <name> Put an app document in the drive. Flag: --parent <folder>
38
45
  forget <ref> Move the references to an app document to the trash.
39
46
  delete-all --yes Erase you for good: your drive, and what you shared or held.
47
+ principal add <id> Make a person, or an agent with --of <person>. Flag: --scope <s> (repeat). No key needed.
48
+ principal revoke <id> Stop a principal, and every agent below it. No key needed.
49
+ key issue <principal> Print a new API key for a principal. It shows once. Flag: --label. No key needed.
50
+ key revoke <key> Stop a key from working. No key needed.
40
51
  mcp Start the MCP server on stdio.
41
52
 
42
53
  Flags:
43
54
  --dir <path> Data folder. Env AGENTIC_DRIVE_DIR. Default ./.agentic-drive
44
- --owner <id> Drive owner. Env AGENTIC_DRIVE_OWNER. Default "me"
45
- --actor <id> Who acts. Env AGENTIC_DRIVE_ACTOR. Default the owner
46
- --actor-kind <k> Actor kind. Env AGENTIC_DRIVE_KIND. Default human
55
+ --token <key> Who you are: an API key. Env AGENTIC_DRIVE_TOKEN. Needed by every command
56
+ except help, version, principal, and key. Prefer the env var: a flag shows in ps.
57
+ The key names a principal, found through agentic-identity. The drive belongs to
58
+ the person at the top of that principal's chain. An agent key marks items as
59
+ made by a machine. If the key is missing or not valid, the command exits with 3.
47
60
  --on-conflict <m> fail, rename, or replace
48
61
  -p, --parents Create missing parent folders
62
+
63
+ Exit codes: 0 done, 1 error, 2 bad usage, 3 no principal or an invalid key.
49
64
  `;
65
+ var COMMANDS = ["ls", "stat", "mkdir", "upload", "download", "mv", "rm", "trash", "restore", "purge", "search", "export", "home", "register", "forget", "delete-all", "mcp", "principal", "key"];
50
66
  var UsageError = class extends Error {
51
67
  };
52
68
  var options = {
53
69
  dir: { type: "string" },
54
- owner: { type: "string" },
55
- actor: { type: "string" },
56
- "actor-kind": { type: "string" },
70
+ token: { type: "string" },
71
+ of: { type: "string" },
72
+ scope: { type: "string", multiple: true },
73
+ label: { type: "string" },
57
74
  "on-conflict": { type: "string" },
58
75
  parents: { type: "boolean", short: "p" },
59
76
  mime: { type: "string" },
@@ -87,12 +104,21 @@ async function runCli(argv, io) {
87
104
  };
88
105
  const env = io.env;
89
106
  const dir = resolve(values.dir ?? env.AGENTIC_DRIVE_DIR ?? ".agentic-drive");
90
- const { drive, ctx } = openLocal({
91
- dir,
92
- owner: values.owner ?? env.AGENTIC_DRIVE_OWNER ?? "me",
93
- actor: values.actor ?? env.AGENTIC_DRIVE_ACTOR,
94
- kind: values["actor-kind"] ?? env.AGENTIC_DRIVE_KIND
95
- });
107
+ if (!COMMANDS.includes(command)) throw new UsageError(`unknown command "${command}"`);
108
+ const admin = command === "principal" || command === "key";
109
+ const token = values.token ?? env.AGENTIC_DRIVE_TOKEN;
110
+ if (!admin && !token) throw new AuthError(NO_TOKEN);
111
+ const { drive, store } = openLocal(dir);
112
+ if (admin || command === "key") {
113
+ const [sub, target] = args;
114
+ const out = (value) => io.out(JSON.stringify(value));
115
+ if (command === "principal" && sub === "add" && target) return out(await addPrincipal(store, target, { parent: values.of, scopes: values.scope })), 0;
116
+ if (command === "principal" && sub === "revoke" && target) return await revokePrincipal(store, target), out({ id: target, revoked: true }), 0;
117
+ if (command === "key" && sub === "issue" && target) return out({ principal: target, token: await issueKey(store, target, values.label) }), 0;
118
+ if (command === "key" && sub === "revoke" && target) return await revokeKey(store, target), out({ revoked: true }), 0;
119
+ throw new UsageError("usage: agentic-drive principal add <id> [--of <person>] [--scope <s>] | principal revoke <id> | key issue <principal> | key revoke <key>");
120
+ }
121
+ const ctx = await resolveToken(store, token);
96
122
  const onConflict = values["on-conflict"];
97
123
  if (onConflict && !["fail", "rename", "replace"].includes(onConflict)) throw new UsageError("--on-conflict needs fail, rename, or replace");
98
124
  const print = (value) => io.out(JSON.stringify(value));
@@ -199,6 +225,10 @@ async function runCli(argv, io) {
199
225
  io.err(JSON.stringify({ error: { code: "USAGE", message: error.message } }));
200
226
  return 2;
201
227
  }
228
+ if (error instanceof AuthError) {
229
+ io.err(JSON.stringify({ error: { code: error.code, message: error.message } }));
230
+ return 3;
231
+ }
202
232
  if (error instanceof DriveError) {
203
233
  io.err(JSON.stringify({ error: { code: error.code, message: error.message } }));
204
234
  return 1;
@@ -0,0 +1,173 @@
1
+ import { E as ErrorCode, N as Node, S as SearchQuery } from './drive-Cn3mpuDW.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: 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);
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,9 +1,9 @@
1
1
  import * as agentic_identity from 'agentic-identity';
2
- import { PrincipalContext, Store, Blobs, createSharing, Role } from 'agentic-identity';
2
+ import { PrincipalKind, 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"];
6
- type ActorKind = (typeof ACTOR_KINDS)[number];
6
+ type ActorKind = PrincipalKind;
7
7
  type Actor = {
8
8
  id: string;
9
9
  kind: ActorKind;
@@ -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" | "agent_of_human" | "agent_of_agent" | "agent" | "user_token" | null;
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
- * trail stays: it holds ids and kinds only. A failed call can run again.
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, subjectId: string, role: Role): Promise<void>;
260
- unshare(ctx: Ctx, loc: string, subjectId: string): Promise<void>;
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 Actor 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, ACTOR_KINDS as a, type Action as b, type ActorKind 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,6 +1,6 @@
1
- import { O as OnConflict, D as Drive, C as Ctx, N as Node, A as ActorKind, a as Actor } from './drive-DKOs1FyM.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 } from './drive-DKOs1FyM.js';
3
- import { AdapterOptions, Blobs, Store } from 'agentic-identity';
1
+ import { O as OnConflict, D as Drive, C as Ctx, N as Node, A as Actor } from './drive-Cn3mpuDW.js';
2
+ export { a as ACTOR_KINDS, b as Action, c as ActorKind, 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-Cn3mpuDW.js';
3
+ import { AdapterOptions, Blobs, Store, Doc } from 'agentic-identity';
4
4
  export { B as BackfillOptions, a as BackfillResult, b as backfillAuthorIds, c as backfillResources } from './migrate-B79gRPMu.js';
5
5
 
6
6
  declare const VERSION: string;
@@ -51,20 +51,30 @@ declare function writeZip(entries: ZipEntry[], at: number): Uint8Array;
51
51
  /** Reads a zip made by `writeZip`. It checks every CRC. */
52
52
  declare function readZip(bytes: Uint8Array): Map<string, Uint8Array>;
53
53
 
54
- interface LocalOptions {
55
- /** Where the drive keeps its data. */
56
- dir: string;
57
- /** Whose drive this is. */
58
- owner: string;
59
- /** Who acts. Defaults to the owner. */
60
- actor?: string;
61
- /** What kind of actor. Defaults to `human`. Set an agent kind when an agent runs the command. */
62
- kind?: ActorKind;
63
- }
64
- declare function openLocal(options: LocalOptions): {
54
+ /** The drive and the store behind it, both kept in `dir`. The store holds the principals and keys too. */
55
+ declare function openLocal(dir: string): {
65
56
  drive: Drive;
66
- ctx: Ctx;
57
+ store: Store;
67
58
  };
59
+ /** Why no caller could be named. The CLI turns it into exit code 3. */
60
+ declare class AuthError extends Error {
61
+ readonly code = "UNAUTHENTICATED";
62
+ }
63
+ /**
64
+ * The caller behind `token`, resolved through agentic-identity like the Worker does. The owner is the root
65
+ * of the delegation chain, and the actor is the principal itself, so an agent key shows as a machine.
66
+ */
67
+ declare function resolveToken(store: Store, token: string | undefined): Promise<Ctx>;
68
+ /** Make a principal. One with a parent is an agent of that human. One without is a human. Local data is the trust boundary. */
69
+ declare function addPrincipal(store: Store, id: string, opts: {
70
+ parent?: string;
71
+ scopes?: string[];
72
+ }): Promise<Doc>;
73
+ /** Stop a principal. Every agent below it stops too, because its chain is no longer live. */
74
+ declare function revokePrincipal(store: Store, id: string): Promise<void>;
75
+ /** Issue an API key for a principal. The token is shown once. */
76
+ declare function issueKey(store: Store, principalId: string, label?: string): Promise<string>;
77
+ declare function revokeKey(store: Store, token: string): Promise<void>;
68
78
  /** A node as the CLI and MCP print it: no storage keys. */
69
79
  declare function publicNode(node: Node, path?: string): {
70
80
  id: string;
@@ -105,4 +115,4 @@ declare function publicNode(node: Node, path?: string): {
105
115
  path: string;
106
116
  };
107
117
 
108
- export { Actor, ActorKind, Ctx, DEFAULT_MAX_BYTES, Drive, Node, OnConflict, type UploadOptions, VERSION, buildExport, createFileBlobs, createFileStore, openLocal, publicNode, readZip, sniffMime, upload, writeZip };
118
+ export { Actor, AuthError, Ctx, DEFAULT_MAX_BYTES, Drive, Node, OnConflict, type UploadOptions, VERSION, addPrincipal, buildExport, createFileBlobs, createFileStore, issueKey, openLocal, publicNode, readZip, resolveToken, revokeKey, revokePrincipal, sniffMime, upload, writeZip };