@argentic/chest-sdk 0.3.0 → 0.4.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.
Files changed (51) hide show
  1. package/README.md +210 -44
  2. package/client/index.ts +4 -3
  3. package/client/src/api.ts +12 -0
  4. package/client/src/chest.ts +30 -6
  5. package/client/src/database.ts +6 -1
  6. package/client/src/events.ts +12 -104
  7. package/client/src/files.ts +9 -9
  8. package/client/src/members.ts +18 -13
  9. package/client/src/schedules.ts +87 -0
  10. package/client/src/signed.ts +166 -0
  11. package/client/src/testing.ts +130 -54
  12. package/dist/index.d.ts +1 -0
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +4 -3
  15. package/dist/index.js.map +1 -1
  16. package/dist/src/api.d.ts +1 -0
  17. package/dist/src/api.d.ts.map +1 -1
  18. package/dist/src/api.js +13 -0
  19. package/dist/src/api.js.map +1 -1
  20. package/dist/src/chest.d.ts +5 -0
  21. package/dist/src/chest.d.ts.map +1 -1
  22. package/dist/src/chest.js +13 -1
  23. package/dist/src/chest.js.map +1 -1
  24. package/dist/src/database.d.ts.map +1 -1
  25. package/dist/src/database.js +5 -1
  26. package/dist/src/database.js.map +1 -1
  27. package/dist/src/events.d.ts +3 -6
  28. package/dist/src/events.d.ts.map +1 -1
  29. package/dist/src/events.js +8 -102
  30. package/dist/src/events.js.map +1 -1
  31. package/dist/src/files.d.ts +1 -0
  32. package/dist/src/files.d.ts.map +1 -1
  33. package/dist/src/files.js +6 -7
  34. package/dist/src/files.js.map +1 -1
  35. package/dist/src/members.d.ts +1 -1
  36. package/dist/src/members.d.ts.map +1 -1
  37. package/dist/src/members.js +6 -5
  38. package/dist/src/members.js.map +1 -1
  39. package/dist/src/schedules.d.ts +15 -0
  40. package/dist/src/schedules.d.ts.map +1 -0
  41. package/dist/src/schedules.js +45 -0
  42. package/dist/src/schedules.js.map +1 -0
  43. package/dist/src/signed.d.ts +29 -0
  44. package/dist/src/signed.d.ts.map +1 -0
  45. package/dist/src/signed.js +139 -0
  46. package/dist/src/signed.js.map +1 -0
  47. package/dist/src/testing.d.ts +15 -5
  48. package/dist/src/testing.d.ts.map +1 -1
  49. package/dist/src/testing.js +122 -52
  50. package/dist/src/testing.js.map +1 -1
  51. package/package.json +14 -3
@@ -1,4 +1,4 @@
1
- import { createHash, createHmac, randomBytes } from "node:crypto";
1
+ import { createHash, randomBytes } from "node:crypto";
2
2
  import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
3
3
  import type { AddressInfo } from "node:net";
4
4
  import type { Alias, Provider } from "./ai.js";
@@ -6,21 +6,24 @@ import type { AiUnavailableReason } from "./errors.js";
6
6
  import type { ChestEvent } from "./events.js";
7
7
  import { groupIdPattern, memberIdPattern, type Member } from "./member.js";
8
8
  import { forget } from "./members.js";
9
+ import { eventChannel, scheduleChannel, sign, signClaims, type Channel } from "./signed.js";
9
10
 
10
11
  // For a tool's own tests, never imported by its production code: a member's
11
12
  // assertion signed as the Chest signs it, and a Chest's API in the test's
12
- // process that answers members, groups, files (stat, move, links; an upload
13
- // it authorises but does not receive), badges, notifications, AI (chat,
13
+ // process that answers members, groups, files (stat, move, links and
14
+ // uploads, which it serves and takes itself as the team host would, on its
15
+ // own origin), badges, notifications, AI (chat,
14
16
  // streamed or not, embeddings, models, usage: deterministic answers, no
15
17
  // provider) and the acknowledgment of an erasure with the Chest's bounds,
16
- // quotas and errors; and that delivers an event to the tool, signed as the
17
- // Chest signs it.
18
+ // quotas and errors; and that delivers an event or a run of a schedule to
19
+ // the tool, signed as the Chest signs them.
18
20
  //
19
21
  // import { fakeChest, withMember } from "@argentic/chest-sdk/testing";
20
22
  // const chest = await fakeChest({ members: [camille], capabilities: ["members", "files", "notifications"] });
21
23
  // const response = await app(withMember(new Request("http://tool/chest"), camille));
22
24
  // assert.equal(chest.notifications[0]?.member, camille.id);
23
25
  // assert.equal(await chest.emit({ type: "access.revoked", data: { id: camille.id } }, request => app(request)), 204);
26
+ // assert.equal(await chest.run("morning", request => app(request)), 204);
24
27
  // await chest.close();
25
28
 
26
29
  // A group as a fake Chest keeps it: its identifier, its name, and the
@@ -28,6 +31,10 @@ import { forget } from "./members.js";
28
31
  export type FakeGroup = { id: string; name: string; members: string[] };
29
32
  // A file as a fake Chest keeps it.
30
33
  export type FakeFile = { data: Uint8Array; type: string; updated: string };
34
+ // Someone the tool had who no longer has it: a member of the Chest without
35
+ // access to it, a member who left ("former", by default), or one whose data
36
+ // was erased (their name gone).
37
+ export type FakeFormer = { id: string; name?: string; status?: "no_access" | "former" | "erased" };
31
38
  // A notification as a fake Chest keeps it: the member it went to, its text
32
39
  // cleaned as the Chest cleans it, its path (/chest when not said) and its
33
40
  // key.
@@ -51,22 +58,24 @@ export type FakeAi = { models?: FakeAiModel[]; reply?: (request: Record<string,
51
58
  // sent (null for a GET).
52
59
  export type FakeAiCall = { path: string; body: unknown };
53
60
 
54
- // What a fake Chest is given: the members who have the tool, those who left
55
- // it (erased: their data was erased, the name gone), its groups, the
61
+ // What a fake Chest is given: the members who have the tool, those it had
62
+ // who no longer have it (FakeFormer), its groups, the
56
63
  // capabilities its version holds (a capability left out answers 403;
57
64
  // members, files, notifications and ai by default, members.email to read the
58
65
  // addresses), the events it receives (["member.*"] by default, [] to answer
59
66
  // an acknowledgment 403), the files it keeps, its AI, and what the Chest is
60
- // (the chest module: "Test organization", UTC and English by default).
67
+ // (the chest module: "Test organization", UTC, English and euros by default;
68
+ // the tool at https://<tool>-chest.chest.test, its public part at
69
+ // https://<tool>.chest.test).
61
70
  export type FakeChestOptions = {
62
71
  members?: Member[];
63
- former?: { id: string; name?: string; erased?: boolean }[];
72
+ former?: FakeFormer[];
64
73
  groups?: FakeGroup[];
65
74
  capabilities?: string[];
66
75
  receives?: string[];
67
76
  files?: Record<string, { data: Uint8Array | string; type?: string }>;
68
77
  ai?: FakeAi;
69
- chest?: { organization?: string; timeZone?: string; language?: string };
78
+ chest?: { organization?: string; timeZone?: string; language?: string; currency?: string; teamUrl?: string; publicUrl?: string | null };
70
79
  };
71
80
 
72
81
  // An event for emit: its type and data; its id (a new evt_… by default) and
@@ -74,13 +83,20 @@ export type FakeChestOptions = {
74
83
  // twice.
75
84
  export type FakeEvent = { [K in ChestEvent["type"]]: { type: K; data: Extract<ChestEvent, { type: K }>["data"]; id?: string; occurredAt?: string } }[ChestEvent["type"]];
76
85
 
77
- // A fake Chest in the test's process: its address, the token and the tool it
78
- // set in the environment, what it keeps (members, groups and files a test
86
+ // A run for run: its id (a new run_… by default: name it to deliver the same
87
+ // run twice), the time it stands for (now by default) and its attempt (1 by
88
+ // default).
89
+ export type FakeRun = { id?: string; scheduledAt?: string; attempt?: number };
90
+
91
+ // A fake Chest in the test's process: its address — its API, and the origin
92
+ // where it serves the links and takes the uploads it signs —, the token and
93
+ // the tool it set in the environment, what it keeps (members, groups and files a test
79
94
  // changes or reads; the notifications the tool sent, in the order sent, a
80
95
  // replaced one last; each member's badge; the erasures the tool
81
96
  // acknowledged; its calls to AI, in order), emit, which delivers an event to the tool — POST
82
97
  // /chest-events of its address, or a handler of Web Requests — and says the
83
- // status it answered, and close, which stops it and restores the
98
+ // status it answered, run, which delivers a run of a schedule the same way
99
+ // on /chest-schedules, and close, which stops it and restores the
84
100
  // environment. Its members are those who have the tool: the others are
85
101
  // skipped.
86
102
  export type FakeChest = {
@@ -95,11 +111,10 @@ export type FakeChest = {
95
111
  acknowledged: string[];
96
112
  ai: FakeAiCall[];
97
113
  emit(event: FakeEvent, to: string | ((request: Request) => Response | Promise<Response>)): Promise<number>;
114
+ run(name: string, to: string | ((request: Request) => Response | Promise<Response>), run?: FakeRun): Promise<number>;
98
115
  close(): Promise<void>;
99
116
  };
100
117
 
101
- const encode = (value: unknown): string => Buffer.from(JSON.stringify(value)).toString("base64url");
102
-
103
118
  // What an assertion is signed with and says besides the member: the token
104
119
  // (CHEST_TOKEN by default) and the tool (CHEST_TOOL by default) it is for,
105
120
  // and when it is issued (now by default). The member is signed as given: a
@@ -116,27 +131,16 @@ export function signAssertion(member: Member, options: AssertionOptions = {}): s
116
131
  if (!token || !tool) throw new Error("signAssertion needs a token and a tool: start a fakeChest, or name them");
117
132
  if (!memberIdPattern.test(member.id) || !member.groups.every(g => groupIdPattern.test(g))) throw new Error("signAssertion needs identifiers of the Chest's shape (mbr_…, grp_…)");
118
133
  const iat = Math.floor((options.now ?? new Date()).getTime() / 1000);
119
- const body = encode({ alg: "HS256", typ: "JWT" }) + "." + encode({
134
+ // Under the label of the assertion's shape, as the Chest signs it and
135
+ // member() reads it.
136
+ return signClaims("Chest-Member v2", {
120
137
  iss: `https://${tool}-chest.chest.test`, aud: tool, iat, exp: iat + 60, sub: member.id,
121
138
  given_name: member.firstName, family_name: member.lastName, name: member.name, picture: member.photo ?? "", role: member.role ?? "",
122
139
  admin: member.isAdmin, builder: member.isBuilder, groups: member.groups, time_zone: member.timeZone, ...(member.email === undefined ? {} : { email: member.email }),
123
140
  language: member.language,
124
- });
125
- // The key as the Chest derives it, and member() reads it: HMAC-SHA256 of
126
- // the label of the assertion's shape under the text of the token.
127
- const key = createHmac("sha256", Buffer.from(token, "utf8")).update("Chest-Member v2").digest();
128
- return body + "." + createHmac("sha256", key).update(body).digest("base64url");
141
+ }, token);
129
142
  }
130
143
 
131
- // signEvent is the Chest-Event value the Chest would send with that body:
132
- // HS256 under the key the token derives for events, for the tool, naming the
133
- // event and the digest of the body, valid 60 seconds.
134
- function signEvent(id: string, body: string, options: { token: string; tool: string }): string {
135
- const iat = Math.floor(Date.now() / 1000);
136
- const signed = encode({ alg: "HS256", typ: "JWT" }) + "." + encode({ aud: options.tool, iat, exp: iat + 60, jti: id, digest: createHash("sha256").update(body).digest("base64url") });
137
- const key = createHmac("sha256", Buffer.from(options.token, "utf8")).update("Chest-Event v1").digest();
138
- return signed + "." + createHmac("sha256", key).update(signed).digest("base64url");
139
- }
140
144
 
141
145
  // withMember is the request carrying that member's assertion, signed with
142
146
  // the options of signAssertion: a new Web Request, or the same Node request
@@ -156,6 +160,19 @@ export function withMember<R extends Request | IncomingMessage>(request: R, memb
156
160
  const maxLimit = 500, defaultLimit = 100, maxLookup = 200, callsPerMinute = 600;
157
161
  const maxObject = 32 << 20, maxObjects = 10000, maxTotal = 1 << 30;
158
162
  const namePattern = /^[A-Za-z0-9][A-Za-z0-9._-]{0,99}(\/[A-Za-z0-9][A-Za-z0-9._-]{0,99}){0,7}$/u;
163
+ // Its links and uploads: their paths on the team host, their lives in
164
+ // seconds, the images it makes thumbnails of, the first bytes an upload of
165
+ // a type must start with, the endings of the names it chooses (chest/toolfiles).
166
+ const linkPath = "/_chest/files/", uploadPath = "/_chest/files/upload/", linkLife = 900, uploadLife = 900;
167
+ const mediaPattern = /^[a-z0-9][a-z0-9!#$&^_.+-]{0,62}\/(\*|[a-z0-9][a-z0-9!#$&^_.+-]{0,62})$/u;
168
+ const thumbnailed = ["image/jpeg", "image/png", "image/gif", "image/webp"];
169
+ const starts = (...heads: string[]) => (data: Buffer): boolean => heads.some(head => data.subarray(0, head.length).equals(Buffer.from(head, "latin1")));
170
+ const signatures: Record<string, (data: Buffer) => boolean> = {
171
+ "image/jpeg": starts("\xff\xd8\xff"), "image/png": starts("\x89PNG\r\n\x1a\n"), "image/gif": starts("GIF87a", "GIF89a"),
172
+ "image/webp": data => data.subarray(0, 4).toString("latin1") === "RIFF" && data.subarray(8, 12).toString("latin1") === "WEBP", "application/pdf": starts("%PDF-"),
173
+ };
174
+ const extensions: Record<string, string> = { "image/jpeg": ".jpg", "image/png": ".png", "image/gif": ".gif", "image/webp": ".webp", "image/avif": ".avif", "image/heic": ".heic", "application/pdf": ".pdf", "text/plain": ".txt", "text/csv": ".csv", "application/json": ".json", "application/zip": ".zip", "video/mp4": ".mp4", "audio/mpeg": ".mp3" };
175
+ const newToken = (): string => randomBytes(18).toString("base64url") + "." + randomBytes(12).toString("base64url");
159
176
  // Its notifications: recipients and badges a call, text, quotas.
160
177
  const maxRecipients = 500, maxTitle = 80, maxText = 280, maxPath = 512, maxCount = 9999;
161
178
  const recipientsPerHour = 1000, itemsPerDay = 100, badgesPerMinute = 600;
@@ -216,12 +233,12 @@ export async function fakeChest(options: FakeChestOptions = {}): Promise<FakeChe
216
233
  const receives = options.receives ?? ["member.*"];
217
234
  // The erasures the tool was told of, by emit: those it may acknowledge.
218
235
  const erasures = new Set<string>();
219
- const chest: FakeChest = { api: "", token, tool, members: [...(options.members ?? [])], groups: [...(options.groups ?? [])], files, notifications: [], badges: new Map(), acknowledged: [], ai: [], emit: async () => 0, close: async () => {} };
236
+ const chest: FakeChest = { api: "", token, tool, members: [...(options.members ?? [])], groups: [...(options.groups ?? [])], files, notifications: [], badges: new Map(), acknowledged: [], ai: [], emit: async () => 0, run: async () => 0, close: async () => {} };
220
237
  const former = [...(options.former ?? [])];
221
238
  let window = 0, calls = 0;
222
239
  const shown = (m: Member) => ({ id: m.id, first_name: m.firstName, last_name: m.lastName, name: m.name, photo: m.photo, role: m.role, admin: m.isAdmin, builder: m.isBuilder, groups: m.groups, language: m.language, time_zone: m.timeZone, ...(email && m.email !== undefined ? { email: m.email } : {}) });
223
240
  const key = (m: Member) => fold(m.name) + "\u0000" + m.id;
224
- const described = (name: string, f: FakeFile) => ({ name, type: f.type, size: f.data.byteLength, updated: f.updated });
241
+ const described = (name: string, f: FakeFile) => ({ name, type: f.type, size: f.data.byteLength, sha256: createHash("sha256").update(f.data).digest("hex"), updated: f.updated });
225
242
 
226
243
  async function members(request: IncomingMessage, response: ServerResponse, url: URL): Promise<void> {
227
244
  if (!capabilities.has("members")) return send(response, 403, { error: "capability_not_granted" });
@@ -252,7 +269,8 @@ export async function fakeChest(options: FakeChestOptions = {}): Promise<FakeChe
252
269
  for (const id of new Set(ids as string[])) {
253
270
  const m = chest.members.find(x => x.id === id), f = former.find(x => x.id === id);
254
271
  if (m) answer.members.push(shown(m));
255
- else if (f?.erased) answer.former.push({ id, status: "erased" });
272
+ else if (f?.status === "erased") answer.former.push({ id, status: "erased" });
273
+ else if (f?.status === "no_access") answer.former.push({ id, name: f.name ?? "Test member", status: "no_access" });
256
274
  else if (f) answer.former.push({ id, ...(f.name ? { name: f.name } : {}), status: "former" });
257
275
  else answer.unknown.push(id);
258
276
  }
@@ -275,23 +293,30 @@ export async function fakeChest(options: FakeChestOptions = {}): Promise<FakeChe
275
293
  const names = [...files.keys()].filter(n => n.startsWith(prefix) && n > after).sort();
276
294
  return send(response, 200, { files: names.slice(0, 1000).map(n => described(n, files.get(n)!)), next: names.length > 1000 ? names[999] : null });
277
295
  }
278
- if (request.method === "POST" && url.pathname === "/files/url") {
279
- const raw = await body(request, 4096);
280
- let name: unknown;
281
- try { name = (JSON.parse(raw?.toString() ?? "") as { name?: unknown }).name; } catch { name = undefined; }
282
- if (typeof name !== "string") return send(response, 400, { error: "invalid_body" });
283
- if (!files.has(name)) return send(response, 404, { error: "not_found" });
284
- return send(response, 200, { url: `https://${tool}-chest.chest.test/_chest/files/${Buffer.from(name).toString("base64url")}.fake`, expires_in: 900 });
285
- }
286
- if (request.method === "POST" && (url.pathname === "/files/move" || url.pathname === "/files/upload-url")) {
296
+ if (request.method === "POST" && (url.pathname === "/files/url" || url.pathname === "/files/move" || url.pathname === "/files/upload-url")) {
287
297
  const raw = await body(request, 4096);
288
298
  let command: Record<string, unknown> = {};
289
299
  try { command = JSON.parse(raw?.toString() ?? "") as Record<string, unknown>; } catch { command = {}; }
300
+ if (url.pathname === "/files/url") {
301
+ const { name, thumbnail, download } = command;
302
+ if (typeof name !== "string" || !(thumbnail === undefined || thumbnail === 256 || thumbnail === 1024) || !(download === undefined || typeof download === "boolean")) return send(response, 400, { error: "invalid_body" });
303
+ const object = files.get(name);
304
+ if (!object) return send(response, 404, { error: "not_found" });
305
+ if (thumbnail !== undefined && !thumbnailed.includes(object.type)) return send(response, 400, { error: "no_thumbnail" });
306
+ const token = newToken();
307
+ links.set(token, { name, object, until: Date.now() + linkLife * 1000, download: download === true });
308
+ return send(response, 200, { url: `${chest.api}${linkPath}${token}`, expires_in: linkLife });
309
+ }
290
310
  if (url.pathname === "/files/upload-url") {
291
- // The upload itself goes from a member's browser to the team host,
292
- // which a fake Chest does not play: it only authorises it.
293
- if (typeof command["name"] !== "string") return send(response, 400, { error: "invalid_body" });
294
- return send(response, 200, { url: `https://${tool}-chest.chest.test/_chest/files/upload/${Buffer.from(command["name"]).toString("base64url")}.fake`, method: "PUT", expires_in: typeof command["expires_in"] === "number" ? command["expires_in"] : 900 });
311
+ const { name, max_size: maxSize, types, expires_in: expiresIn } = command;
312
+ const folder = typeof name === "string" && name.endsWith("/");
313
+ if (typeof name !== "string" || !namePattern.test(folder ? name.slice(0, -1) : name)) return send(response, 400, { error: "invalid_name" });
314
+ if (!(types === undefined || (Array.isArray(types) && types.length <= 8 && types.every(t => typeof t === "string" && mediaPattern.test(t))))) return send(response, 400, { error: "invalid_type" });
315
+ if (!(maxSize === undefined || (typeof maxSize === "number" && Number.isSafeInteger(maxSize) && maxSize > 0)) || !(expiresIn === undefined || (typeof expiresIn === "number" && Number.isInteger(expiresIn) && expiresIn > 0 && expiresIn <= uploadLife))) return send(response, 400, { error: "invalid_body" });
316
+ if ((maxSize ?? 0) > maxObject) return send(response, 413, { error: "too_large" });
317
+ const token = newToken(), life = (expiresIn as number | undefined) ?? uploadLife;
318
+ uploads.set(token, { name, maxSize: (maxSize as number | undefined) ?? maxObject, types: (types as string[] | undefined) ?? [], until: Date.now() + life * 1000 });
319
+ return send(response, 200, { url: `${chest.api}${uploadPath}${token}`, method: "PUT", expires_in: life });
295
320
  }
296
321
  const from = command["from"], to = command["to"];
297
322
  if (typeof from !== "string" || typeof to !== "string" || !namePattern.test(from) || !namePattern.test(to)) return send(response, 400, { error: "invalid_name" });
@@ -323,6 +348,39 @@ export async function fakeChest(options: FakeChestOptions = {}): Promise<FakeChe
323
348
  send(response, 404, { error: "not_found" });
324
349
  }
325
350
 
351
+ // The team host's part of the files, on the fake's own origin: a link it
352
+ // signed opens the content it was signed for, for its life, as long as
353
+ // the file is that content — a thumbnail is the image itself, a fake does
354
+ // not reduce it —; an upload token takes one file, once, within its life,
355
+ // of the types it names and up to its size, whose first bytes are those of
356
+ // its type, named by the Chest in a folder. Nobody's session is checked:
357
+ // the Chest's own front does that.
358
+ const links = new Map<string, { name: string; object: FakeFile; until: number; download: boolean }>();
359
+ const uploads = new Map<string, { name: string; maxSize: number; types: string[]; until: number }>();
360
+ async function front(request: IncomingMessage, response: ServerResponse, url: URL): Promise<void> {
361
+ if (request.method === "GET" && url.pathname.startsWith(linkPath) && !url.pathname.startsWith(uploadPath)) {
362
+ const link = links.get(url.pathname.slice(linkPath.length));
363
+ if (!link || link.until < Date.now() || files.get(link.name) !== link.object) return send(response, 404, { error: "not_found" });
364
+ const headers: Record<string, string> = { "Content-Type": link.object.type, "Content-Length": String(link.object.data.byteLength), "X-Content-Type-Options": "nosniff" };
365
+ if (link.download) headers["Content-Disposition"] = `attachment; filename="${link.name.split("/").pop()}"`;
366
+ return void response.writeHead(200, headers).end(link.object.data);
367
+ }
368
+ if (request.method !== "PUT" || !url.pathname.startsWith(uploadPath)) return send(response, 404, { error: "not_found" });
369
+ const token = url.pathname.slice(uploadPath.length), grant = uploads.get(token);
370
+ uploads.delete(token);
371
+ if (!grant || grant.until < Date.now()) return send(response, 403, { error: "invalid_token" });
372
+ const type = (request.headers["content-type"] ?? "").split(";")[0]!.trim().toLowerCase();
373
+ if (!mediaPattern.test(type) || type.endsWith("/*") || (grant.types.length > 0 && !grant.types.some(t => t === type || (t.endsWith("/*") && type.startsWith(t.slice(0, -1)))))) return send(response, 415, { error: "type_refused" });
374
+ const data = await body(request, grant.maxSize);
375
+ if (data === null) return send(response, 413, { error: "too_large" });
376
+ if (signatures[type] && !signatures[type]!(data)) return send(response, 400, { error: "type_mismatch" });
377
+ const name = grant.name.endsWith("/") ? grant.name + randomBytes(10).toString("hex") + (extensions[type] ?? "") : grant.name;
378
+ const total = [...files.entries()].reduce((sum, [n, f]) => n === name ? sum : sum + f.data.byteLength, 0);
379
+ if (total + data.length > maxTotal || (!files.has(name) && files.size >= maxObjects)) return send(response, 429, { error: "quota_exceeded" });
380
+ files.set(name, { data: new Uint8Array(data), type, updated: new Date().toISOString() });
381
+ send(response, 201, { name, type, size: data.length });
382
+ }
383
+
326
384
  // The windows of the notification quotas, each from the first call it
327
385
  // counts: the tool's recipients this hour, each member's items this day,
328
386
  // the tool's badge writes this minute.
@@ -529,7 +587,8 @@ export async function fakeChest(options: FakeChestOptions = {}): Promise<FakeChe
529
587
 
530
588
  const server = createServer((request, response) => {
531
589
  const url = new URL(request.url ?? "/", "http://127.0.0.1");
532
- const route = url.pathname.startsWith("/erasures/") ? erasuresRoute
590
+ const route = url.pathname.startsWith("/_chest/files/") ? front
591
+ : url.pathname.startsWith("/erasures/") ? erasuresRoute
533
592
  : url.pathname.startsWith("/ai/") ? aiRoute
534
593
  : url.pathname === "/members" || url.pathname.startsWith("/members/") || url.pathname === "/groups" ? members
535
594
  : url.pathname === "/files" || url.pathname.startsWith("/files/") ? filesRoute
@@ -538,19 +597,36 @@ export async function fakeChest(options: FakeChestOptions = {}): Promise<FakeChe
538
597
  route(request, response, url).catch(() => { if (!response.headersSent) send(response, 503, { error: "unavailable" }); else response.destroy(); });
539
598
  });
540
599
  await new Promise<void>(resolve => server.listen(0, "127.0.0.1", resolve));
541
- const saved = Object.fromEntries(["CHEST_API", "CHEST_TOKEN", "CHEST_TOOL", "CHEST_ORGANIZATION", "CHEST_TIME_ZONE", "CHEST_LANGUAGE"].map(name => [name, process.env[name]]));
600
+ const saved = Object.fromEntries(["CHEST_API", "CHEST_TOKEN", "CHEST_TOOL", "CHEST_ORGANIZATION", "CHEST_TIME_ZONE", "CHEST_LANGUAGE", "CHEST_CURRENCY", "CHEST_TEAM_URL", "CHEST_PUBLIC_URL"].map(name => [name, process.env[name]]));
542
601
  chest.api = "http://127.0.0.1:" + (server.address() as AddressInfo).port;
543
- Object.assign(process.env, { CHEST_API: chest.api, CHEST_TOKEN: token, CHEST_TOOL: tool, CHEST_ORGANIZATION: options.chest?.organization ?? "Test organization", CHEST_TIME_ZONE: options.chest?.timeZone ?? "UTC", CHEST_LANGUAGE: options.chest?.language ?? "en" });
602
+ // Its links are on its own origin, the CHEST_API it sets: the files
603
+ // module takes them there (api.ts, chestLink), in this process or in a
604
+ // tool it starts with this environment.
605
+ const publicUrl = options.chest?.publicUrl === undefined ? `https://${tool}.chest.test` : options.chest.publicUrl;
606
+ Object.assign(process.env, { CHEST_API: chest.api, CHEST_TOKEN: token, CHEST_TOOL: tool, CHEST_ORGANIZATION: options.chest?.organization ?? "Test organization", CHEST_TIME_ZONE: options.chest?.timeZone ?? "UTC", CHEST_LANGUAGE: options.chest?.language ?? "en", CHEST_CURRENCY: options.chest?.currency ?? "EUR", CHEST_TEAM_URL: options.chest?.teamUrl ?? `https://${tool}-chest.chest.test` });
607
+ if (publicUrl === null) delete process.env["CHEST_PUBLIC_URL"];
608
+ else process.env["CHEST_PUBLIC_URL"] = publicUrl;
544
609
  forget();
545
- chest.emit = async (event, to) => {
546
- const id = event.id ?? "evt_" + Array.from(randomBytes(26), b => "abcdefghijklmnopqrstuvwxyz234567"[b & 31]).join("");
547
- const body = JSON.stringify({ id, type: event.type, occurredAt: event.occurredAt ?? new Date().toISOString(), data: event.data });
548
- if (event.type === "member.erased") erasures.add(event.data.erasure);
549
- const request = new Request(typeof to === "string" ? to.replace(/\/$/u, "") + "/chest-events" : "http://tool.test/chest-events", { method: "POST", headers: { "Content-Type": "application/json", "Chest-Event": signEvent(id, body, { token, tool }) }, body });
610
+ // post delivers what the Chest posts to the tool on a channel — at its
611
+ // address, or to a handler — and says the status it answered.
612
+ const post = async (path: string, channel: Channel, id: string, body: string, to: string | ((request: Request) => Response | Promise<Response>)): Promise<number> => {
613
+ const request = new Request((typeof to === "string" ? to.replace(/\/$/u, "") : "http://tool.test") + path, { method: "POST", headers: { "Content-Type": "application/json", [channel.header]: sign(channel, id, body, { token, tool }) }, body });
550
614
  const answer = typeof to === "string" ? await fetch(request, { redirect: "manual" }) : await to(request);
551
615
  await answer.body?.cancel();
552
616
  return answer.status;
553
617
  };
618
+ const newId = (prefix: string): string => prefix + Array.from(randomBytes(26), b => "abcdefghijklmnopqrstuvwxyz234567"[b & 31]).join("");
619
+ chest.emit = async (event, to) => {
620
+ const id = event.id ?? newId("evt_");
621
+ const body = JSON.stringify({ id, type: event.type, occurredAt: event.occurredAt ?? new Date().toISOString(), data: event.data });
622
+ if (event.type === "member.erased") erasures.add(event.data.erasure);
623
+ return post("/chest-events", eventChannel, id, body, to);
624
+ };
625
+ chest.run = async (name, to, run = {}) => {
626
+ const id = run.id ?? newId("run_");
627
+ const body = JSON.stringify({ id, name, scheduledAt: run.scheduledAt ?? new Date().toISOString(), attempt: run.attempt ?? 1 });
628
+ return post("/chest-schedules", scheduleChannel, id, body, to);
629
+ };
554
630
  chest.close = async () => {
555
631
  server.closeAllConnections();
556
632
  await new Promise<void>(resolve => server.close(() => resolve()));
package/dist/index.d.ts CHANGED
@@ -6,5 +6,6 @@ export * as files from "./src/files.js";
6
6
  export * as members from "./src/members.js";
7
7
  export * as notifications from "./src/notifications.js";
8
8
  export * as events from "./src/events.js";
9
+ export * as schedules from "./src/schedules.js";
9
10
  export * as ai from "./src/ai.js";
10
11
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../client/index.ts"],"names":[],"mappings":"AAOA,cAAc,iBAAiB,CAAC;AAChC,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,mBAAmB,CAAC;AAClC,OAAO,KAAK,KAAK,MAAM,gBAAgB,CAAC;AACxC,OAAO,KAAK,OAAO,MAAM,kBAAkB,CAAC;AAC5C,OAAO,KAAK,aAAa,MAAM,wBAAwB,CAAC;AACxD,OAAO,KAAK,MAAM,MAAM,iBAAiB,CAAC;AAC1C,OAAO,KAAK,EAAE,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../client/index.ts"],"names":[],"mappings":"AAOA,cAAc,iBAAiB,CAAC;AAChC,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,mBAAmB,CAAC;AAClC,OAAO,KAAK,KAAK,MAAM,gBAAgB,CAAC;AACxC,OAAO,KAAK,OAAO,MAAM,kBAAkB,CAAC;AAC5C,OAAO,KAAK,aAAa,MAAM,wBAAwB,CAAC;AACxD,OAAO,KAAK,MAAM,MAAM,iBAAiB,CAAC;AAC1C,OAAO,KAAK,SAAS,MAAM,oBAAoB,CAAC;AAChD,OAAO,KAAK,EAAE,MAAM,aAAa,CAAC"}
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // The package root: every published module a tool's code uses (tool contract
2
- // v2). Each one is also its own subpath (@argentic/chest-sdk/member, /chest,
3
- // /database, /files, /members, /notifications, /events, /ai, /errors), which
4
- // pulls in nothing else. The files, members, notifications, events and ai
2
+ // 0.4). Each one is also its own subpath (@argentic/chest-sdk/member, /chest,
3
+ // /database, /files, /members, /notifications, /events, /schedules, /ai, /errors), which
4
+ // pulls in nothing else. The files, members, notifications, events, schedules and ai
5
5
  // APIs are namespaces here, as their names (get, list, stat, move, notify,
6
6
  // verify, chat…) are too plain to stand alone. @argentic/chest-sdk/testing is for a tool's tests only, and
7
7
  // is not here.
@@ -13,5 +13,6 @@ export * as files from "./src/files.js";
13
13
  export * as members from "./src/members.js";
14
14
  export * as notifications from "./src/notifications.js";
15
15
  export * as events from "./src/events.js";
16
+ export * as schedules from "./src/schedules.js";
16
17
  export * as ai from "./src/ai.js";
17
18
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../client/index.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,6EAA6E;AAC7E,6EAA6E;AAC7E,0EAA0E;AAC1E,2EAA2E;AAC3E,2GAA2G;AAC3G,eAAe;AACf,cAAc,iBAAiB,CAAC;AAChC,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,mBAAmB,CAAC;AAClC,OAAO,KAAK,KAAK,MAAM,gBAAgB,CAAC;AACxC,OAAO,KAAK,OAAO,MAAM,kBAAkB,CAAC;AAC5C,OAAO,KAAK,aAAa,MAAM,wBAAwB,CAAC;AACxD,OAAO,KAAK,MAAM,MAAM,iBAAiB,CAAC;AAC1C,OAAO,KAAK,EAAE,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../client/index.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,8EAA8E;AAC9E,yFAAyF;AACzF,qFAAqF;AACrF,2EAA2E;AAC3E,2GAA2G;AAC3G,eAAe;AACf,cAAc,iBAAiB,CAAC;AAChC,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,mBAAmB,CAAC;AAClC,OAAO,KAAK,KAAK,MAAM,gBAAgB,CAAC;AACxC,OAAO,KAAK,OAAO,MAAM,kBAAkB,CAAC;AAC5C,OAAO,KAAK,aAAa,MAAM,wBAAwB,CAAC;AACxD,OAAO,KAAK,MAAM,MAAM,iBAAiB,CAAC;AAC1C,OAAO,KAAK,SAAS,MAAM,oBAAoB,CAAC;AAChD,OAAO,KAAK,EAAE,MAAM,aAAa,CAAC"}
package/dist/src/api.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { ChestError } from "./errors.js";
2
+ export declare function chestLink(url: unknown, path: string): string | undefined;
2
3
  export declare function ask(capability: string, method: string, path: string, init?: {
3
4
  body?: Uint8Array<ArrayBuffer> | string;
4
5
  type?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../../client/src/api.ts"],"names":[],"mappings":"AAAA,OAAO,EAAwB,UAAU,EAAqD,MAAM,aAAa,CAAC;AAuBlH,wBAAsB,GAAG,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE;IAAE,IAAI,CAAC,EAAE,UAAU,CAAC,WAAW,CAAC,GAAG,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,CAAA;CAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAW7M;AAID,wBAAsB,IAAI,CAAC,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAqBjF;AAGD,wBAAsB,IAAI,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,CAM/D;AAGD,wBAAsB,OAAO,CAAC,QAAQ,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAazF"}
1
+ {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../../client/src/api.ts"],"names":[],"mappings":"AAAA,OAAO,EAAwB,UAAU,EAAqD,MAAM,aAAa,CAAC;AAgBlH,wBAAgB,SAAS,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAKxE;AAcD,wBAAsB,GAAG,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE;IAAE,IAAI,CAAC,EAAE,UAAU,CAAC,WAAW,CAAC,GAAG,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,CAAA;CAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAW7M;AAID,wBAAsB,IAAI,CAAC,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAqBjF;AAGD,wBAAsB,IAAI,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,CAM/D;AAGD,wBAAsB,OAAO,CAAC,QAAQ,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAazF"}
package/dist/src/api.js CHANGED
@@ -6,6 +6,19 @@ import { CapabilityNotGranted, ChestError, QuotaExceeded, RateLimited, TooLarge,
6
6
  // identity. Not a published module.
7
7
  const maxAnswer = 4 << 20;
8
8
  const deadline = 120000;
9
+ // chestLink reads a link to the team host the Chest answered, at path (its
10
+ // links, its uploads): the token it carries, or undefined for an address
11
+ // that is not one — https, or the origin of the Chest's API itself
12
+ // (CHEST_API), where only a fake Chest of a tool's tests serves its links:
13
+ // the address the tool already trusts for every call, never another.
14
+ export function chestLink(url, path) {
15
+ if (typeof url !== "string")
16
+ return undefined;
17
+ const found = /^(https:\/\/[A-Za-z0-9.-]{1,253}(?::[0-9]{1,5})?|http:\/\/127\.0\.0\.1:[0-9]{1,5})(\/_chest\/[a-z/]+\/)([A-Za-z0-9_-]+\.[A-Za-z0-9_-]+)$/u.exec(url);
18
+ if (!found || found[2] !== path || (found[1].startsWith("http:") && found[1] !== process.env["CHEST_API"]))
19
+ return undefined;
20
+ return found[3];
21
+ }
9
22
  // base is the Chest's API as the launcher gives it; without, the version holds
10
23
  // none of the capabilities that use it.
11
24
  function base(capability) {
@@ -1 +1 @@
1
- {"version":3,"file":"api.js","sourceRoot":"","sources":["../../client/src/api.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"api.js","sourceRoot":"","sources":["../../client/src/api.ts"],"names":[],"mappings":"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"}
@@ -4,6 +4,11 @@ export type Chest = {
4
4
  };
5
5
  readonly timeZone: string;
6
6
  readonly language: string;
7
+ readonly currency: string;
8
+ readonly tool: {
9
+ readonly teamUrl: string;
10
+ readonly publicUrl: string | null;
11
+ };
7
12
  today(at?: Date | number): string;
8
13
  };
9
14
  export declare const chest: Chest;
@@ -1 +1 @@
1
- {"version":3,"file":"chest.d.ts","sourceRoot":"","sources":["../../client/src/chest.ts"],"names":[],"mappings":"AA0BA,MAAM,MAAM,KAAK,GAAG;IAClB,QAAQ,CAAC,YAAY,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IACjD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,KAAK,CAAC,EAAE,CAAC,EAAE,IAAI,GAAG,MAAM,GAAG,MAAM,CAAC;CACnC,CAAC;AAiCF,eAAO,MAAM,KAAK,EAAE,KAenB,CAAC"}
1
+ {"version":3,"file":"chest.d.ts","sourceRoot":"","sources":["../../client/src/chest.ts"],"names":[],"mappings":"AAoCA,MAAM,MAAM,KAAK,GAAG;IAClB,QAAQ,CAAC,YAAY,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IACjD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE;QAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;IAC/E,KAAK,CAAC,EAAE,CAAC,EAAE,IAAI,GAAG,MAAM,GAAG,MAAM,CAAC;CACnC,CAAC;AAqCF,eAAO,MAAM,KAAK,EAAE,KAuBnB,CAAC"}
package/dist/src/chest.js CHANGED
@@ -2,8 +2,12 @@ import { ChestError } from "./errors.js";
2
2
  import { languagePattern, timeZonePattern } from "./member.js";
3
3
  // The shapes the Chest gives: the organization's (2 to 80 characters,
4
4
  // counted as code points, without control characters), a zone's
5
- // (timeZonePattern, and one this runtime knows), a language's.
5
+ // (timeZonePattern, and one this runtime knows), a language's, a currency's,
6
+ // an origin's.
6
7
  const organizationPattern = /^[^\u0000-\u001f\u007f-\u009f]{2,80}$/u;
8
+ // An ISO 4217 code; an https origin without a path, as the Chest gives them.
9
+ const currencyPattern = /^[A-Z]{3}$/u;
10
+ const originPattern = /^https:\/\/[a-z0-9]([a-z0-9.-]{0,251}[a-z0-9])?(:[0-9]{1,5})?$/u;
7
11
  function read(name, valid) {
8
12
  const value = process.env[name];
9
13
  if (typeof value !== "string" || !valid(value))
@@ -39,6 +43,14 @@ export const chest = {
39
43
  get language() {
40
44
  return read("CHEST_LANGUAGE", value => languagePattern.test(value));
41
45
  },
46
+ get currency() {
47
+ return read("CHEST_CURRENCY", value => currencyPattern.test(value));
48
+ },
49
+ get tool() {
50
+ const teamUrl = read("CHEST_TEAM_URL", value => originPattern.test(value));
51
+ const publicUrl = process.env["CHEST_PUBLIC_URL"] === undefined ? null : read("CHEST_PUBLIC_URL", value => originPattern.test(value));
52
+ return { teamUrl, publicUrl };
53
+ },
42
54
  today(at = Date.now()) {
43
55
  const instant = typeof at === "number" ? new Date(at) : at;
44
56
  if (Number.isNaN(instant.getTime()))
@@ -1 +1 @@
1
- {"version":3,"file":"chest.js","sourceRoot":"","sources":["../../client/src/chest.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAgC/D,sEAAsE;AACtE,gEAAgE;AAChE,+DAA+D;AAC/D,MAAM,mBAAmB,GAAG,wCAAwC,CAAC;AAErE,SAAS,IAAI,CAAC,IAAY,EAAE,KAAiC;IAC3D,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAChC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC;QAAE,MAAM,IAAI,UAAU,CAAC,cAAc,EAAE,GAAG,EAAE,2BAA2B,IAAI,wBAAwB,CAAC,CAAC;IACnJ,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,SAAS,CAAC,IAAY;IAC7B,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,KAAK,CAAC;IAC9C,IAAI,CAAC;QACH,IAAI,IAAI,CAAC,cAAc,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;QACrD,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,MAAM,IAAI,GAAG,GAAW,EAAE,CAAC,IAAI,CAAC,iBAAiB,EAAE,SAAS,CAAC,CAAC;AAE9D,+DAA+D;AAC/D,SAAS,MAAM,CAAC,EAAQ,EAAE,IAAY;IACpC,MAAM,KAAK,GAAG,MAAM,CAAC,WAAW,CAAC,IAAI,IAAI,CAAC,cAAc,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,SAAS,EAAE,CAAC,CAAC,aAAa,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACxL,OAAO,GAAG,KAAK,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC;AAC9D,CAAC;AAED,2EAA2E;AAC3E,sBAAsB;AACtB,MAAM,CAAC,MAAM,KAAK,GAAU;IAC1B,IAAI,YAAY;QACd,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,oBAAoB,EAAE,KAAK,CAAC,EAAE,CAAC,mBAAmB,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;IACxF,CAAC;IACD,IAAI,QAAQ;QACV,OAAO,IAAI,EAAE,CAAC;IAChB,CAAC;IACD,IAAI,QAAQ;QACV,OAAO,IAAI,CAAC,gBAAgB,EAAE,KAAK,CAAC,EAAE,CAAC,eAAe,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;IACtE,CAAC;IACD,KAAK,CAAC,EAAE,GAAkB,IAAI,CAAC,GAAG,EAAE;QAClC,MAAM,OAAO,GAAG,OAAO,EAAE,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAC3D,IAAI,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;YAAE,MAAM,IAAI,UAAU,CAAC,4BAA4B,CAAC,CAAC;QACxF,OAAO,MAAM,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;IACjC,CAAC;CACF,CAAC"}
1
+ {"version":3,"file":"chest.js","sourceRoot":"","sources":["../../client/src/chest.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../client/src/database.ts"],"names":[],"mappings":"AAaA,wBAAgB,WAAW,IAAI,MAAM,CAcpC"}
1
+ {"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../client/src/database.ts"],"names":[],"mappings":"AAkBA,wBAAgB,WAAW,IAAI,MAAM,CAcpC"}
@@ -7,9 +7,13 @@ import { CapabilityNotGranted } from "./errors.js";
7
7
  // postgres (porsager) or pg; the SDK carries none. PGHOST, PGPORT, PGUSER,
8
8
  // PGPASSWORD and PGDATABASE say the same for a client that reads them.
9
9
  //
10
+ // Its user is the tool's role (t_<tool>), or, in the preview of a draft
11
+ // Perseus Code builds, the draft's (pb_<project>: its own empty database).
12
+ //
10
13
  // Throws CapabilityNotGranted when the Chest gave no database: the version
11
14
  // does not declare it, or a DATABASE_URL of the tool's own is not the
12
15
  // Chest's. The value is a secret: never log it, never send it to a browser.
16
+ const role = /^(t_[a-z][a-z0-9_]{0,47}|pb_[a-z2-7]{26})$/u;
13
17
  export function databaseUrl() {
14
18
  const value = process.env["DATABASE_URL"];
15
19
  if (typeof value !== "string" || value.length > 1024)
@@ -22,7 +26,7 @@ export function databaseUrl() {
22
26
  throw new CapabilityNotGranted("database");
23
27
  }
24
28
  const port = Number(url.port);
25
- if (url.protocol !== "postgres:" || url.hostname !== "127.0.0.1" || !Number.isInteger(port) || port < 1 || port > 65535 || !/^t_[a-z][a-z0-9_]{0,47}$/u.test(url.username) || url.pathname !== "/" + url.username || url.password === "" || url.search !== "?sslmode=disable" || url.hash !== "") {
29
+ if (url.protocol !== "postgres:" || url.hostname !== "127.0.0.1" || !Number.isInteger(port) || port < 1 || port > 65535 || !role.test(url.username) || url.pathname !== "/" + url.username || url.password === "" || url.search !== "?sslmode=disable" || url.hash !== "") {
26
30
  throw new CapabilityNotGranted("database");
27
31
  }
28
32
  return value;
@@ -1 +1 @@
1
- {"version":3,"file":"database.js","sourceRoot":"","sources":["../../client/src/database.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAEnD,2EAA2E;AAC3E,2EAA2E;AAC3E,4EAA4E;AAC5E,6EAA6E;AAC7E,yEAAyE;AACzE,2EAA2E;AAC3E,uEAAuE;AACvE,EAAE;AACF,2EAA2E;AAC3E,sEAAsE;AACtE,4EAA4E;AAC5E,MAAM,UAAU,WAAW;IACzB,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAC1C,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,GAAG,IAAI;QAAE,MAAM,IAAI,oBAAoB,CAAC,UAAU,CAAC,CAAC;IACjG,IAAI,GAAQ,CAAC;IACb,IAAI,CAAC;QACH,GAAG,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC;IACvB,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,oBAAoB,CAAC,UAAU,CAAC,CAAC;IAC7C,CAAC;IACD,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC9B,IAAI,GAAG,CAAC,QAAQ,KAAK,WAAW,IAAI,GAAG,CAAC,QAAQ,KAAK,WAAW,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,IAAI,GAAG,CAAC,IAAI,IAAI,GAAG,KAAK,IAAI,CAAC,2BAA2B,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,GAAG,CAAC,QAAQ,KAAK,GAAG,GAAG,GAAG,CAAC,QAAQ,IAAI,GAAG,CAAC,QAAQ,KAAK,EAAE,IAAI,GAAG,CAAC,MAAM,KAAK,kBAAkB,IAAI,GAAG,CAAC,IAAI,KAAK,EAAE,EAAE,CAAC;QACjS,MAAM,IAAI,oBAAoB,CAAC,UAAU,CAAC,CAAC;IAC7C,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}
1
+ {"version":3,"file":"database.js","sourceRoot":"","sources":["../../client/src/database.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAEnD,2EAA2E;AAC3E,2EAA2E;AAC3E,4EAA4E;AAC5E,6EAA6E;AAC7E,yEAAyE;AACzE,2EAA2E;AAC3E,uEAAuE;AACvE,EAAE;AACF,wEAAwE;AACxE,2EAA2E;AAC3E,EAAE;AACF,2EAA2E;AAC3E,sEAAsE;AACtE,4EAA4E;AAC5E,MAAM,IAAI,GAAG,6CAA6C,CAAC;AAE3D,MAAM,UAAU,WAAW;IACzB,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAC1C,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,GAAG,IAAI;QAAE,MAAM,IAAI,oBAAoB,CAAC,UAAU,CAAC,CAAC;IACjG,IAAI,GAAQ,CAAC;IACb,IAAI,CAAC;QACH,GAAG,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC;IACvB,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,oBAAoB,CAAC,UAAU,CAAC,CAAC;IAC7C,CAAC;IACD,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC9B,IAAI,GAAG,CAAC,QAAQ,KAAK,WAAW,IAAI,GAAG,CAAC,QAAQ,KAAK,WAAW,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,IAAI,GAAG,CAAC,IAAI,IAAI,GAAG,KAAK,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,GAAG,CAAC,QAAQ,KAAK,GAAG,GAAG,GAAG,CAAC,QAAQ,IAAI,GAAG,CAAC,QAAQ,KAAK,EAAE,IAAI,GAAG,CAAC,MAAM,KAAK,kBAAkB,IAAI,GAAG,CAAC,IAAI,KAAK,EAAE,EAAE,CAAC;QAC1Q,MAAM,IAAI,oBAAoB,CAAC,UAAU,CAAC,CAAC;IAC7C,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}
@@ -1,5 +1,6 @@
1
1
  import type { IncomingMessage } from "node:http";
2
- export type MemberChange = "name" | "photo" | "role" | "groups" | "email";
2
+ import { type Seen } from "./signed.js";
3
+ export type MemberChange = "name" | "photo" | "role" | "groups" | "email" | "language" | "timeZone";
3
4
  export type MemberUpdated = {
4
5
  id: string;
5
6
  type: "member.updated";
@@ -42,11 +43,7 @@ export type Handlers = {
42
43
  type: K;
43
44
  }>) => void | Promise<void>;
44
45
  };
45
- export type Seen = {
46
- has(id: string): boolean | Promise<boolean>;
47
- add(id: string): void | Promise<void>;
48
- };
49
- export declare function memorySeen(limit?: number): Seen;
46
+ export { memorySeen, type Seen } from "./signed.js";
50
47
  export declare const erasureIdPattern: RegExp;
51
48
  export declare function verify(request: IncomingMessage | Request): Promise<ChestEvent | null>;
52
49
  export declare function handle(request: IncomingMessage | Request, handlers: Handlers, options?: {
@@ -1 +1 @@
1
- {"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../../client/src/events.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AA4BjD,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,QAAQ,GAAG,OAAO,CAAC;AAE1E,MAAM,MAAM,aAAa,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,gBAAgB,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,YAAY,EAAE,CAAA;KAAE,CAAA;CAAE,CAAC;AAEtI,MAAM,MAAM,aAAa,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,gBAAgB,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,CAAC;AAE7G,MAAM,MAAM,aAAa,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,gBAAgB,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,CAAC;AAG7G,MAAM,MAAM,YAAY,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,eAAe,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,CAAC;AAE9I,MAAM,MAAM,UAAU,GAAG,aAAa,GAAG,aAAa,GAAG,aAAa,GAAG,YAAY,CAAC;AACtF,MAAM,MAAM,cAAc,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC;AAGhD,MAAM,MAAM,QAAQ,GAAG;KAAG,CAAC,IAAI,cAAc,CAAC,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,UAAU,EAAE;QAAE,IAAI,EAAE,CAAC,CAAA;KAAE,CAAC,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;CAAE,CAAC;AAWrH,MAAM,MAAM,IAAI,GAAG;IAAE,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAAC,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAAE,CAAC;AAI1G,wBAAgB,UAAU,CAAC,KAAK,SAAQ,GAAG,IAAI,CAU9C;AAcD,eAAO,MAAM,gBAAgB,QAAwB,CAAC;AAoGtD,wBAAsB,MAAM,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAG3F;AAWD,wBAAsB,MAAM,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,OAAO,GAAE;IAAE,IAAI,CAAC,EAAE,IAAI,CAAA;CAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAanI;AAQD,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CASvE"}
1
+ {"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../../client/src/events.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAKjD,OAAO,EAA6D,KAAK,IAAI,EAAE,MAAM,aAAa,CAAC;AA0BnG,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,QAAQ,GAAG,OAAO,GAAG,UAAU,GAAG,UAAU,CAAC;AAEpG,MAAM,MAAM,aAAa,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,gBAAgB,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,YAAY,EAAE,CAAA;KAAE,CAAA;CAAE,CAAC;AAEtI,MAAM,MAAM,aAAa,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,gBAAgB,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,CAAC;AAE7G,MAAM,MAAM,aAAa,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,gBAAgB,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,CAAC;AAG7G,MAAM,MAAM,YAAY,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,eAAe,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,CAAC;AAE9I,MAAM,MAAM,UAAU,GAAG,aAAa,GAAG,aAAa,GAAG,aAAa,GAAG,YAAY,CAAC;AACtF,MAAM,MAAM,cAAc,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC;AAGhD,MAAM,MAAM,QAAQ,GAAG;KAAG,CAAC,IAAI,cAAc,CAAC,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,UAAU,EAAE;QAAE,IAAI,EAAE,CAAC,CAAA;KAAE,CAAC,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;CAAE,CAAC;AAIrH,OAAO,EAAE,UAAU,EAAE,KAAK,IAAI,EAAE,MAAM,aAAa,CAAC;AAGpD,eAAO,MAAM,gBAAgB,QAAwB,CAAC;AAsCtD,wBAAsB,MAAM,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAG3F;AAWD,wBAAsB,MAAM,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,OAAO,GAAE;IAAE,IAAI,CAAC,EAAE,IAAI,CAAA;CAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAanI;AAQD,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CASvE"}