agentic-drive 0.5.0 → 0.6.1
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 +14 -0
- package/README.md +88 -15
- package/dist/{chunk-B7E2Q3RB.js → chunk-5Z3SFBUK.js} +1 -1
- package/dist/{chunk-T6C5Y2WA.js → chunk-EAKQK7ST.js} +209 -201
- package/dist/{chunk-WQHJNHAR.js → chunk-H6JK62SP.js} +1 -1
- package/dist/chunk-S4AGCKXW.js +241 -0
- package/dist/cli.js +2 -2
- package/dist/client.d.ts +1 -1
- package/dist/client.js +4 -236
- package/dist/{drive-Cn3mpuDW.d.ts → drive-BtRyexUP.d.ts} +1 -1
- package/dist/embed.css +154 -0
- package/dist/embed.d.ts +21 -0
- package/dist/embed.js +412 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +3 -3
- package/dist/mcp.d.ts +1 -1
- package/dist/mcp.js +2 -2
- package/dist/worker.d.ts +23 -4
- package/dist/worker.js +48 -23
- package/llms.txt +5 -0
- package/package.json +24 -4
- package/ui-dist/assets/{index-xUEd8lQS.css → index-B9nizMSQ.css} +1 -1
- package/ui-dist/assets/index-DQzn7NQP.js +49 -0
- package/ui-dist/index.html +2 -2
- package/ui-dist/assets/index-BiU4TGuq.js +0 -49
|
@@ -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
|
+
};
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
VERSION
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-H6JK62SP.js";
|
|
5
5
|
import {
|
|
6
6
|
AuthError,
|
|
7
7
|
DriveError,
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
revokeKey,
|
|
16
16
|
revokePrincipal,
|
|
17
17
|
upload
|
|
18
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-EAKQK7ST.js";
|
|
19
19
|
|
|
20
20
|
// src/cli.ts
|
|
21
21
|
import { createReadStream, realpathSync, writeFileSync } from "fs";
|
package/dist/client.d.ts
CHANGED
package/dist/client.js
CHANGED
|
@@ -1,239 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
DriveApiError,
|
|
3
|
+
createClient
|
|
4
|
+
} from "./chunk-S4AGCKXW.js";
|
|
237
5
|
export {
|
|
238
6
|
DriveApiError,
|
|
239
7
|
createClient
|
|
@@ -200,7 +200,7 @@ declare function createDrive(deps: DriveDeps): {
|
|
|
200
200
|
exportPerson(ctx: Ctx): Promise<{
|
|
201
201
|
subject: {
|
|
202
202
|
id: string;
|
|
203
|
-
kind: "human" | "
|
|
203
|
+
kind: "human" | "agent_of_human" | "agent_of_agent" | "agent" | "user_token" | null;
|
|
204
204
|
};
|
|
205
205
|
partial: boolean;
|
|
206
206
|
authored: {
|