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/CHANGELOG.md +64 -0
- package/README.md +124 -8
- package/SKILL.md +11 -0
- package/dist/{chunk-6CWBHSJ7.js → chunk-ATJDMCRV.js} +118 -35
- package/dist/{chunk-RWMBGIAS.js → chunk-DRACF66Y.js} +1 -1
- package/dist/{chunk-3LISWG7H.js → chunk-L7GASKBQ.js} +1 -1
- package/dist/cli.js +44 -14
- package/dist/client.d.ts +173 -0
- package/dist/client.js +240 -0
- package/dist/{drive-DKOs1FyM.d.ts → drive-Cn3mpuDW.d.ts} +16 -7
- package/dist/index.d.ts +26 -16
- package/dist/index.js +19 -3
- package/dist/mcp.d.ts +1 -1
- package/dist/mcp.js +2 -2
- package/dist/worker.d.ts +8 -7
- package/dist/worker.js +6 -10
- package/llms.txt +2 -1
- package/package.json +21 -4
- package/ui-dist/_headers +4 -0
- package/ui-dist/assets/index-e_cefh0u.js +49 -0
- package/ui-dist/assets/index-xUEd8lQS.css +1 -0
- package/ui-dist/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/ui-dist/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/ui-dist/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- package/ui-dist/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/ui-dist/index.html +14 -0
- package/ui-dist/theme-init.js +1 -0
package/dist/cli.js
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
VERSION
|
|
4
|
-
} from "./chunk-
|
|
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-
|
|
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
|
-
--
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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;
|
package/dist/client.d.ts
ADDED
|
@@ -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 =
|
|
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" | "
|
|
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
|
|
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
|
|
2
|
-
export {
|
|
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
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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,
|
|
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 };
|