@argentic/chest-sdk 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Argentic
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,206 @@
1
+ # Chest SDK
2
+
3
+ `@argentic/chest-sdk` is what a server tool (tool contract v2) embeds to talk
4
+ with its Chest: the member the Chest asserts on a request, the address of the
5
+ tool's own database and its private files. The SDK has no dependency: it only
6
+ imports `node:*`.
7
+
8
+ ```sh
9
+ npm install @argentic/chest-sdk
10
+ ```
11
+
12
+ Node 22 or later. ESM only, compiled JavaScript with its type declarations.
13
+
14
+ ## Imports
15
+
16
+ Each module is its own subpath and pulls in nothing else; the root gives them
17
+ all, with the files API as the namespace `files`.
18
+
19
+ | Import | Gives |
20
+ |---|---|
21
+ | `@argentic/chest-sdk/member` | `member(request)`, type `Member`: the member of a request on the team host of a server tool, read from the `Chest-Member` assertion and verified; `null` without a valid assertion |
22
+ | `@argentic/chest-sdk/database` | `databaseUrl()`: the address of the tool's own PostgreSQL database (capability `database`) |
23
+ | `@argentic/chest-sdk/files` | `put`, `get`, `list`, `delete`, `url`, types `FileObject`, `FileData`, `FilePage`: the tool's private files (capability `files`), kept by the Chest, and a 15-minute signed link to one |
24
+ | `@argentic/chest-sdk/errors` | `ChestError` (`code`, `status`), `CapabilityNotGranted` (403), `TooLarge` (413), `QuotaExceeded` (429), `Unavailable` (503): what the SDK throws when the Chest does not give what a tool asks |
25
+ | `@argentic/chest-sdk` | all of the above; `files` as a namespace |
26
+
27
+ ```ts
28
+ import { member } from "@argentic/chest-sdk/member";
29
+ import { databaseUrl } from "@argentic/chest-sdk/database";
30
+ import * as files from "@argentic/chest-sdk/files";
31
+ import { CapabilityNotGranted } from "@argentic/chest-sdk/errors";
32
+ // or: import { member, databaseUrl, files } from "@argentic/chest-sdk";
33
+ ```
34
+
35
+ Types refer to `node:http` (`IncomingMessage`): a TypeScript project needs
36
+ `@types/node`, as any Node project does. Both `moduleResolution` `bundler` and
37
+ `nodenext` work.
38
+
39
+ ### Next.js
40
+
41
+ The SDK runs on the server only — it reads the tool's environment
42
+ (`CHEST_TOKEN`, `DATABASE_URL`, `CHEST_API`) and uses Node built-ins. Import it
43
+ in route handlers, server components or server actions, never in a
44
+ `"use client"` module. Webpack and Turbopack resolve the
45
+ compiled package with no configuration (no `transpilePackages`):
46
+
47
+ ```ts
48
+ // app/chest/api/me/route.ts
49
+ import { member } from "@argentic/chest-sdk/member";
50
+
51
+ export function GET(request: Request) {
52
+ const who = member(request);
53
+ return who ? Response.json(who) : new Response(null, { status: 401 });
54
+ }
55
+ ```
56
+
57
+ ## The contract, in short
58
+
59
+ A v2 tool is an ordinary web server in a container without network, run by
60
+ its Chest. The Chest's front is the only one to reach it; the tool reaches only
61
+ what its launcher gives it on `127.0.0.1` (its database, the Chest's API for
62
+ its files). Rights come from the Chest — the signed member, the capabilities
63
+ approved for the version — and the Chest enforces them even outside the SDK:
64
+ the SDK makes the calls easier, it is not a security boundary. The full
65
+ contract (manifest `chest.json`, capabilities, build from source, catalogue)
66
+ is described in the Chest repository, `docs/architecture.md`.
67
+
68
+ ## `member(request)` — server tool (contract v2)
69
+
70
+ A v2 tool is an ordinary web server; on its team host, the Chest relays
71
+ `/chest` and everything below it with the `Chest-Member` header of the
72
+ signed-in member. `member(request)` accepts a Node request (`IncomingMessage`)
73
+ or a Web `Request` and returns:
74
+
75
+ ```ts
76
+ type Member = { id: string; firstName: string; lastName: string; name: string; email: string; photo?: string; role?: string; isAdmin: boolean; isBuilder: boolean };
77
+ ```
78
+
79
+ or `null`: without the header, on the public host (the Chest never sends an
80
+ assertion there and strips a client's), or for any assertion that is not
81
+ exactly its own. Checks: compact JWS, header exactly
82
+ `{"alg":"HS256","typ":"JWT"}`, HMAC-SHA256 signature compared in constant time
83
+ under the key HMAC-SHA256("Chest-Member v1") of the text of `CHEST_TOKEN` —
84
+ the Chest's derivation —, `aud` equal to `CHEST_TOOL`, `iat` and `exp` within
85
+ 5 s, the shape of each claim (an unknown claim is ignored). Without
86
+ `CHEST_TOKEN` or `CHEST_TOOL`, nobody is a member. The function never throws
87
+ for what a request carries.
88
+
89
+ ```ts
90
+ import { member } from "@argentic/chest-sdk/member";
91
+ const who = member(request);
92
+ if (!who) { response.writeHead(401).end(); return; }
93
+ ```
94
+
95
+ `photo` is the address of the photo on the team host, `role` the role the
96
+ Chest gives the member among those the manifest declares. Only the Chest's
97
+ front reaches the container: the signature is a second defence; business rules
98
+ (who writes what) remain the tool's.
99
+
100
+ ## `databaseUrl()` — database of a server tool
101
+
102
+ A v2 tool that declares `"capabilities": ["database"]` in its `chest.json`
103
+ gets a PostgreSQL database of its own (the capability is shown and approved
104
+ like a permission, « Base de données »). The container has no network: its
105
+ launcher listens on `127.0.0.1` and relays each connection to the Chest. The
106
+ launcher sets `DATABASE_URL` —
107
+ `postgres://<user>:<password>@127.0.0.1:<port>/<database>?sslmode=disable`,
108
+ the user and the database both named `t_<tool>` — and `PGHOST`, `PGPORT`,
109
+ `PGUSER`, `PGPASSWORD`, `PGDATABASE`, which take precedence over a variable of
110
+ the tool with the same name. `databaseUrl()` returns `DATABASE_URL` when it has
111
+ exactly this shape, and throws `CapabilityNotGranted` otherwise (a version
112
+ without the capability, or a `DATABASE_URL` of the tool's own). The value is a
113
+ secret: never log it, never send it to a browser.
114
+
115
+ The SDK carries no PostgreSQL client: the tool picks its own, for example
116
+ [`postgres`](https://github.com/porsager/postgres) (porsager, no dependency) or
117
+ [`pg`](https://node-postgres.com):
118
+
119
+ ```ts
120
+ import postgres from "postgres";
121
+ import { databaseUrl } from "@argentic/chest-sdk/database";
122
+ const sql = postgres(databaseUrl(), { max: 5 });
123
+ const notes = await sql`SELECT id, text FROM notes ORDER BY id`;
124
+ ```
125
+
126
+ Ten connections at most per instance; a query longer than 30 s, a transaction
127
+ idle longer than 60 s are interrupted by the Chest. **Migrations**: the
128
+ repository's `migrations/NNNN_name.sql` files
129
+ (`^[0-9]{4}_[a-z0-9_-]{1,64}\.sql$`, 256 at most, 1 MiB each) are run by the
130
+ Chest, in order, each in its own transaction, at install and at every update,
131
+ before the new version receives traffic; a failing file keeps the version in
132
+ service. The Chest keeps the list of files run (table `chest_migrations`): a
133
+ version that loses one or changes one is refused. A migration must leave the
134
+ previous version working — going back to the previous version undoes nothing.
135
+
136
+ ## `files` — files of a server tool
137
+
138
+ A v2 tool that declares `"capabilities": ["files"]` (« Fichiers » at approval)
139
+ keeps private files **through its Chest**, never on its disk (the container's
140
+ root is read-only): 1 GiB and 10,000 objects per tool, 32 MiB per object. The
141
+ launcher gives the tool `CHEST_API=http://127.0.0.1:<port>` — its own port,
142
+ relayed to the Chest; the container has no network — and the instance is the
143
+ identity: the tool reaches its own files only.
144
+
145
+ ```ts
146
+ import * as files from "@argentic/chest-sdk/files";
147
+ await files.put("photos/cat.png", bytes, "image/png"); // Uint8Array or text
148
+ const file = await files.get("photos/cat.png"); // {data, type, size} or null
149
+ const { files: page, next } = await files.list({ prefix: "photos/" }); // 1000 per page
150
+ await files.delete("photos/cat.png"); // true, or false if it did not exist
151
+ const { url, expiresIn } = await files.url("photos/cat.png");
152
+ ```
153
+
154
+ A name: up to 8 segments of 1 to 100 letters, digits, `.`, `_` or `-`,
155
+ separated by `/`, none starting with `.` or `-`; refused before anything is
156
+ sent otherwise (`ChestError`, `invalid_name`). `url` signs a link to the file
157
+ as it is, on the tool's **team host** (`/_chest/files/…`): whoever has it opens
158
+ it without signing in for 15 minutes, or until the file changes or goes; the
159
+ Chest serves it in a sandbox, displayed for an image, a PDF or plain text,
160
+ downloaded otherwise. Give it to a member's browser, never to a public page.
161
+ Errors: `CapabilityNotGranted` (a version without the capability, or no
162
+ `CHEST_API`), `TooLarge` (413), `QuotaExceeded` (429), `Unavailable` (the
163
+ Chest not reached, or an answer that is not its own: a write may or may not
164
+ have happened), `ChestError` for the rest (`invalid_type`, `not_found` for
165
+ `url`…). Removing the tool removes its files; a new version keeps them.
166
+
167
+ ## Retired v1 contract
168
+
169
+ Tool contract v1 (a worker talking over stdout/stdin: invocations, a record)
170
+ is retired and **not part of the package**. Its modules,
171
+ `client/src/{channel,record,requests,worker}.ts` and
172
+ `client/test/worker.test.ts`, stay in this repository only because the Chest
173
+ repository vendors `client/src`, `client/test` and `template/` by exact file
174
+ list (`scripts/sync-sdk.mjs`, copies under `tests/sdk/chest-client` and
175
+ `tests/creator`, each with a `VENDORED.md` naming the commit); they go once the
176
+ Chest repository drops them. They are not built into `dist/`, not exported and
177
+ not published. `template/` (the v1 starter project) is not published either.
178
+
179
+ ## Version
180
+
181
+ The package version is `version` in `package.json` (semver), published by a
182
+ tag `vX.Y.Z` (see `PUBLISHING.md`).
183
+
184
+ ## What this repository is not
185
+
186
+ This repository is public and **is not a tool**: it has no `chest.json`, and a
187
+ Chest's catalogue — which only lists the organisation's public repositories
188
+ that carry a manifest — never offers it.
189
+
190
+ ## Develop
191
+
192
+ ```sh
193
+ npm ci
194
+ npm test # build dist/, compile all tests (v1 included) into build/, run them
195
+ npm run check:package # npm pack, install into a temp project, import every subpath
196
+ # from Node and through esbuild, type-check a TS consumer
197
+ ```
198
+
199
+ `client/src` holds the modules, `client/index.ts` the package root,
200
+ `client/test` the tests. `npm run build` compiles `client/index.ts` and the
201
+ four published modules (`errors`, `member`, `database`, `files`; TypeScript
202
+ strict, ES2022, NodeNext) into `dist/`: ESM `.js`, `.d.ts` and their maps. Read `AGENTS.md` before changing anything.
203
+
204
+ ## Licence
205
+
206
+ MIT (`LICENSE`), © 2026 Argentic.
@@ -0,0 +1,11 @@
1
+ // The package root: every published module of the SDK (tool contract v2).
2
+ // Each one is also its own subpath (@argentic/chest-sdk/member, /database,
3
+ // /files, /errors), which pulls in nothing else. The files API is a namespace
4
+ // here, as its names (get, put, list, delete, url) are too plain to stand
5
+ // alone. channel, record, requests and worker in client/src are the retired
6
+ // v1 contract: kept for the Chest repository's vendored copy, never built
7
+ // into nor published with the package.
8
+ export * from "./src/errors.js";
9
+ export * from "./src/member.js";
10
+ export * from "./src/database.js";
11
+ export * as files from "./src/files.js";
@@ -0,0 +1,28 @@
1
+ import { CapabilityNotGranted } from "./errors.js";
2
+
3
+ // The address of the tool's own PostgreSQL database, as the Chest gives it
4
+ // to a server tool whose chest.json declares "capabilities": ["database"]:
5
+ // postgres://<user>:<password>@127.0.0.1:<port>/<database>?sslmode=disable,
6
+ // where the port is the tool's launcher, which relays each connection to the
7
+ // Chest (the container has no network). Hand it to a PostgreSQL client —
8
+ // postgres (porsager) or pg; the SDK carries none. PGHOST, PGPORT, PGUSER,
9
+ // PGPASSWORD and PGDATABASE say the same for a client that reads them.
10
+ //
11
+ // Throws CapabilityNotGranted when the Chest gave no database: the version
12
+ // does not declare it, or a DATABASE_URL of the tool's own is not the
13
+ // Chest's. The value is a secret: never log it, never send it to a browser.
14
+ export function databaseUrl(): string {
15
+ const value = process.env["DATABASE_URL"];
16
+ if (typeof value !== "string" || value.length > 1024) throw new CapabilityNotGranted("database");
17
+ let url: URL;
18
+ try {
19
+ url = new URL(value);
20
+ } catch {
21
+ throw new CapabilityNotGranted("database");
22
+ }
23
+ const port = Number(url.port);
24
+ 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 !== "") {
25
+ throw new CapabilityNotGranted("database");
26
+ }
27
+ return value;
28
+ }
@@ -0,0 +1,44 @@
1
+ // What the SDK throws when the Chest does not give what a tool asks: a code
2
+ // the tool can test, and the HTTP status the Chest answers for it.
3
+ export class ChestError extends Error {
4
+ readonly code: string;
5
+ readonly status: number;
6
+ constructor(code: string, status: number, message: string) {
7
+ super(message);
8
+ this.name = new.target.name;
9
+ this.code = code;
10
+ this.status = status;
11
+ }
12
+ }
13
+
14
+ // The version of the tool in service does not declare the capability in its
15
+ // chest.json ("capabilities"), or it was not approved: the Chest gives it
16
+ // nothing.
17
+ export class CapabilityNotGranted extends ChestError {
18
+ constructor(capability: string) {
19
+ super("capability_not_granted", 403, `the Chest did not grant the capability "${capability}" to this version of the tool`);
20
+ }
21
+ }
22
+
23
+ // What the tool keeps would go beyond what its Chest gives it: its total
24
+ // (1 GiB of files) or its count (10,000 objects).
25
+ export class QuotaExceeded extends ChestError {
26
+ constructor() {
27
+ super("quota_exceeded", 429, "the Chest refused: the tool's quota would be exceeded");
28
+ }
29
+ }
30
+
31
+ // One object is beyond the bound of one (32 MiB for a file).
32
+ export class TooLarge extends ChestError {
33
+ constructor() {
34
+ super("too_large", 413, "the Chest refused: the object is too large");
35
+ }
36
+ }
37
+
38
+ // The Chest did not answer, or not as it does: nothing is known of what was
39
+ // asked — a write may or may not have happened.
40
+ export class Unavailable extends ChestError {
41
+ constructor() {
42
+ super("unavailable", 503, "the Chest is unavailable");
43
+ }
44
+ }
@@ -0,0 +1,181 @@
1
+ import { CapabilityNotGranted, ChestError, QuotaExceeded, TooLarge, Unavailable } from "./errors.js";
2
+
3
+ // The private files of a server tool whose chest.json declares
4
+ // "capabilities": ["files"]: kept by its Chest (1 GiB, 10,000 objects, 32 MiB
5
+ // each), never on the tool's own disk. The container has no network: the
6
+ // Chest's API is at CHEST_API (http://127.0.0.1:<port>, the tool's launcher,
7
+ // which relays each request to the Chest). A call reaches the tool's files
8
+ // only: its instance is its identity.
9
+ //
10
+ // import * as files from "../../packages/chest-client/src/files.js";
11
+ // await files.put("photos/cat.png", bytes, "image/png");
12
+ // const { url } = await files.url("photos/cat.png"); // 15 min, team host
13
+ //
14
+ // Names: up to 8 segments of 1–100 letters, digits, '.', '_' or '-',
15
+ // separated by '/', none starting with '.' or '-'. Errors: CapabilityNotGranted
16
+ // (403), TooLarge (413), QuotaExceeded (429), Unavailable (503, or the Chest
17
+ // not reached), ChestError otherwise (invalid_name, invalid_type 400…).
18
+
19
+ export type FileObject = { name: string; type: string; size: number; updated: string };
20
+ export type FileData = { data: Uint8Array; type: string; size: number };
21
+ export type FilePage = { files: FileObject[]; next: string | null };
22
+
23
+ const maxObject = 32 << 20;
24
+ const maxAnswer = 4 << 20;
25
+ const deadline = 120000;
26
+ 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;
27
+
28
+ // base is the Chest's API as the launcher gives it; without, the version
29
+ // does not keep files.
30
+ function base(): string {
31
+ const value = process.env["CHEST_API"];
32
+ if (typeof value !== "string" || !/^http:\/\/127\.0\.0\.1:[1-9][0-9]{0,4}$/u.test(value) || Number(value.slice(17)) > 65535) throw new CapabilityNotGranted("files");
33
+ return value;
34
+ }
35
+
36
+ function checkName(name: unknown): string {
37
+ if (typeof name !== "string" || !namePattern.test(name)) throw new ChestError("invalid_name", 400, "invalid file name");
38
+ return name;
39
+ }
40
+
41
+ // ask sends one request to the Chest; a failure to reach it is Unavailable.
42
+ async function ask(method: string, path: string, init: { body?: Uint8Array<ArrayBuffer> | string; type?: string } = {}): Promise<Response> {
43
+ const headers: Record<string, string> = {};
44
+ if (init.type !== undefined) headers["Content-Type"] = init.type;
45
+ try {
46
+ return await fetch(base() + path, { method, headers, ...(init.body !== undefined ? { body: init.body } : {}), redirect: "error", signal: AbortSignal.timeout(deadline) });
47
+ } catch (error) {
48
+ if (error instanceof ChestError) throw error;
49
+ throw new Unavailable();
50
+ }
51
+ }
52
+
53
+ // read takes a body of limit bytes at most; beyond, or cut, the answer is not
54
+ // the Chest's.
55
+ async function read(response: Response, limit: number): Promise<Uint8Array> {
56
+ const declared = Number(response.headers.get("content-length") ?? "0");
57
+ if (declared > limit) throw new Unavailable();
58
+ const chunks: Uint8Array[] = [];
59
+ let size = 0;
60
+ try {
61
+ for await (const chunk of response.body ?? []) {
62
+ size += chunk.byteLength;
63
+ if (size > limit) throw new Unavailable();
64
+ chunks.push(chunk);
65
+ }
66
+ } catch {
67
+ throw new Unavailable();
68
+ }
69
+ const all = new Uint8Array(size);
70
+ let at = 0;
71
+ for (const chunk of chunks) {
72
+ all.set(chunk, at);
73
+ at += chunk.byteLength;
74
+ }
75
+ return all;
76
+ }
77
+
78
+ async function json(response: Response): Promise<unknown> {
79
+ try {
80
+ return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(await read(response, maxAnswer)));
81
+ } catch (error) {
82
+ if (error instanceof ChestError) throw error;
83
+ throw new Unavailable();
84
+ }
85
+ }
86
+
87
+ // refusal turns an answer that is not a success into what the tool tests.
88
+ async function refusal(response: Response): Promise<ChestError> {
89
+ if (response.status === 403) return new CapabilityNotGranted("files");
90
+ if (response.status === 413) return new TooLarge();
91
+ if (response.status === 429) return new QuotaExceeded();
92
+ if (response.status >= 500) return new Unavailable();
93
+ let code = "refused";
94
+ try {
95
+ const body = await json(response);
96
+ const given = (body as { error?: unknown } | null)?.error;
97
+ if (typeof given === "string" && /^[a-z_]{1,40}$/u.test(given)) code = given;
98
+ } catch {
99
+ // The code stays « refused ».
100
+ }
101
+ return new ChestError(code, response.status, `the Chest refused: ${code}`);
102
+ }
103
+
104
+ function isObject(value: unknown): value is FileObject {
105
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
106
+ const o = value as Record<string, unknown>;
107
+ return typeof o["name"] === "string" && namePattern.test(o["name"]) && typeof o["type"] === "string" && o["type"].length <= 200 && typeof o["size"] === "number" && Number.isSafeInteger(o["size"]) && o["size"] >= 0 && typeof o["updated"] === "string";
108
+ }
109
+ function object(value: unknown): FileObject {
110
+ if (!isObject(value)) throw new Unavailable();
111
+ return { name: value.name, type: value.type, size: value.size, updated: value.updated };
112
+ }
113
+
114
+ // put keeps data as the file name, of type type (application/octet-stream
115
+ // when none), replacing the one of that name.
116
+ export async function put(name: string, data: Uint8Array | string, type?: string): Promise<FileObject> {
117
+ checkName(name);
118
+ if (!(data instanceof Uint8Array) && typeof data !== "string") throw new TypeError("data must be bytes or text");
119
+ const body = typeof data === "string" ? new TextEncoder().encode(data) : new Uint8Array(data);
120
+ if (body.byteLength > maxObject) throw new TooLarge();
121
+ const response = await ask("PUT", "/files/" + name, { body, type: type ?? (typeof data === "string" ? "text/plain; charset=utf-8" : "application/octet-stream") });
122
+ if (response.status !== 201) throw await refusal(response);
123
+ return object(await json(response));
124
+ }
125
+
126
+ // get reads a file; null when the tool has none of that name.
127
+ export async function get(name: string): Promise<FileData | null> {
128
+ checkName(name);
129
+ const response = await ask("GET", "/files/" + name);
130
+ if (response.status === 404) {
131
+ await response.body?.cancel();
132
+ return null;
133
+ }
134
+ if (response.status !== 200) throw await refusal(response);
135
+ const data = await read(response, maxObject);
136
+ return { data, type: response.headers.get("content-type") ?? "application/octet-stream", size: data.byteLength };
137
+ }
138
+
139
+ // list says the files whose name starts with prefix, in the order of their
140
+ // names, after the name after, 1000 at most; next is the name to ask after
141
+ // for the following page, null once all were said.
142
+ export async function list(options: { prefix?: string; after?: string } = {}): Promise<FilePage> {
143
+ const query = new URLSearchParams();
144
+ if (options.prefix !== undefined && options.prefix !== "") {
145
+ if (typeof options.prefix !== "string" || options.prefix.length > 807 || !/^[A-Za-z0-9._\/-]*$/u.test(options.prefix)) throw new ChestError("invalid_name", 400, "invalid prefix");
146
+ query.set("prefix", options.prefix);
147
+ }
148
+ if (options.after !== undefined && options.after !== "") query.set("after", checkName(options.after));
149
+ const response = await ask("GET", "/files" + (query.size ? "?" + query.toString() : ""));
150
+ if (response.status !== 200) throw await refusal(response);
151
+ const body = await json(response);
152
+ const page = body as { files?: unknown; next?: unknown } | null;
153
+ if (!page || !Array.isArray(page.files) || page.files.length > 1000 || !(page.next === null || (typeof page.next === "string" && namePattern.test(page.next)))) throw new Unavailable();
154
+ return { files: page.files.map(object), next: page.next };
155
+ }
156
+
157
+ // remove deletes a file; false when the tool had none of that name.
158
+ async function remove(name: string): Promise<boolean> {
159
+ checkName(name);
160
+ const response = await ask("DELETE", "/files/" + name);
161
+ if (response.status === 204) return true;
162
+ if (response.status === 404) {
163
+ await response.body?.cancel();
164
+ return false;
165
+ }
166
+ throw await refusal(response);
167
+ }
168
+ export { remove as delete };
169
+
170
+ // url signs a link to the file as it is now, on the tool's team host: anyone
171
+ // who has it opens the file, without signing in, for expiresIn seconds
172
+ // (15 minutes), or until the file changes or goes. Give it to a member's
173
+ // browser, never to a public page.
174
+ export async function url(name: string): Promise<{ url: string; expiresIn: number }> {
175
+ checkName(name);
176
+ const response = await ask("POST", "/files/url", { body: JSON.stringify({ name }), type: "application/json" });
177
+ if (response.status !== 200) throw await refusal(response);
178
+ const body = (await json(response)) as { url?: unknown; expires_in?: unknown } | null;
179
+ if (!body || typeof body.url !== "string" || !/^https:\/\/[^/?#]+\/_chest\/files\/[A-Za-z0-9_.-]{1,1536}$/u.test(body.url) || typeof body.expires_in !== "number" || !Number.isInteger(body.expires_in) || body.expires_in <= 0) throw new Unavailable();
180
+ return { url: body.url, expiresIn: body.expires_in };
181
+ }
@@ -0,0 +1,75 @@
1
+ import { createHmac, timingSafeEqual } from "node:crypto";
2
+ import type { IncomingMessage } from "node:http";
3
+
4
+ // The member the Chest asserts on a request of the team host of a server
5
+ // tool (/chest and below), read from the Chest-Member header. photo and role
6
+ // are absent when the Chest names none.
7
+ export type Member = {
8
+ id: string;
9
+ firstName: string;
10
+ lastName: string;
11
+ name: string;
12
+ email: string;
13
+ photo?: string;
14
+ role?: string;
15
+ isAdmin: boolean;
16
+ isBuilder: boolean;
17
+ };
18
+
19
+ // The key of the assertions is HMAC-SHA256 of this label under the text of
20
+ // CHEST_TOKEN, exactly as the Chest derives it (chest/toolfront).
21
+ const label = "Chest-Member v1";
22
+ // Clocks of the Chest and of the container may differ by this much, in seconds.
23
+ const skew = 5;
24
+ // An assertion is a few hundred bytes; anything longer is not one.
25
+ const maxLength = 8192;
26
+ const compact = /^([A-Za-z0-9_-]+)\.([A-Za-z0-9_-]+)\.([A-Za-z0-9_-]+)$/u;
27
+ const claims = ["iss", "aud", "iat", "exp", "sub", "given_name", "family_name", "name", "email", "picture", "role", "admin", "builder"] as const;
28
+
29
+ function assertionOf(request: IncomingMessage | Request): string | null {
30
+ const headers = request.headers as Headers | IncomingMessage["headers"];
31
+ // A Web Request joins repeated headers with ", ", which no assertion
32
+ // contains; a Node request keeps them as an array: both are refused.
33
+ const value = typeof (headers as Headers).get === "function" ? (headers as Headers).get("chest-member") : (headers as IncomingMessage["headers"])["chest-member"];
34
+ return typeof value === "string" && value.length <= maxLength ? value : null;
35
+ }
36
+
37
+ function json(part: string): Record<string, unknown> | null {
38
+ try {
39
+ const value = JSON.parse(Buffer.from(part, "base64url").toString("utf8")) as unknown;
40
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : null;
41
+ } catch {
42
+ return null;
43
+ }
44
+ }
45
+
46
+ // member returns who the Chest says is making this request, or null when the
47
+ // request carries no valid assertion — absent, malformed, signed with another
48
+ // key, for another tool, expired or not yet valid —, or when CHEST_TOKEN or
49
+ // CHEST_TOOL is missing. It never throws for what a request carries. Only the
50
+ // Chest's front reaches the tool; the signature is a second defence, and the
51
+ // tool still decides what a member may do with its own rules.
52
+ export function member(request: IncomingMessage | Request): Member | null {
53
+ const token = process.env["CHEST_TOKEN"];
54
+ const tool = process.env["CHEST_TOOL"];
55
+ if (!token || !/^[A-Za-z0-9_-]{43,512}$/u.test(token) || !tool) return null;
56
+ const assertion = assertionOf(request);
57
+ const parts = assertion === null ? null : compact.exec(assertion);
58
+ if (!parts) return null;
59
+ const [, encodedHeader = "", encodedPayload = "", encodedSignature = ""] = parts;
60
+ const header = json(encodedHeader);
61
+ if (!header || Object.keys(header).length !== 2 || header["alg"] !== "HS256" || header["typ"] !== "JWT") return null;
62
+ const key = createHmac("sha256", Buffer.from(token, "utf8")).update(label).digest();
63
+ const expected = createHmac("sha256", key).update(encodedHeader + "." + encodedPayload).digest();
64
+ const signature = Buffer.from(encodedSignature, "base64url");
65
+ if (signature.length !== expected.length || !timingSafeEqual(signature, expected)) return null;
66
+ const payload = json(encodedPayload);
67
+ if (!payload || !claims.every(name => Object.hasOwn(payload, name))) return null;
68
+ const { iss, aud, iat, exp, sub, given_name, family_name, name, email, picture, role, admin, builder } = payload;
69
+ if (typeof iss !== "string" || iss === "" || aud !== tool || typeof sub !== "string" || sub === "") return null;
70
+ if (typeof iat !== "number" || !Number.isSafeInteger(iat) || typeof exp !== "number" || !Number.isSafeInteger(exp) || exp <= iat) return null;
71
+ const now = Math.floor(Date.now() / 1000);
72
+ if (iat > now + skew || exp <= now - skew) return null;
73
+ if (typeof given_name !== "string" || typeof family_name !== "string" || typeof name !== "string" || typeof email !== "string" || typeof picture !== "string" || typeof role !== "string" || typeof admin !== "boolean" || typeof builder !== "boolean") return null;
74
+ return { id: sub, firstName: given_name, lastName: family_name, name, email, ...(picture === "" ? {} : { photo: picture }), ...(role === "" ? {} : { role }), isAdmin: admin, isBuilder: builder };
75
+ }
@@ -0,0 +1,5 @@
1
+ export * from "./src/errors.js";
2
+ export * from "./src/member.js";
3
+ export * from "./src/database.js";
4
+ export * as files from "./src/files.js";
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +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,mBAAmB,CAAC;AAClC,OAAO,KAAK,KAAK,MAAM,gBAAgB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,12 @@
1
+ // The package root: every published module of the SDK (tool contract v2).
2
+ // Each one is also its own subpath (@argentic/chest-sdk/member, /database,
3
+ // /files, /errors), which pulls in nothing else. The files API is a namespace
4
+ // here, as its names (get, put, list, delete, url) are too plain to stand
5
+ // alone. channel, record, requests and worker in client/src are the retired
6
+ // v1 contract: kept for the Chest repository's vendored copy, never built
7
+ // into nor published with the package.
8
+ export * from "./src/errors.js";
9
+ export * from "./src/member.js";
10
+ export * from "./src/database.js";
11
+ export * as files from "./src/files.js";
12
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../client/index.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAC1E,2EAA2E;AAC3E,8EAA8E;AAC9E,0EAA0E;AAC1E,4EAA4E;AAC5E,0EAA0E;AAC1E,uCAAuC;AACvC,cAAc,iBAAiB,CAAC;AAChC,cAAc,iBAAiB,CAAC;AAChC,cAAc,mBAAmB,CAAC;AAClC,OAAO,KAAK,KAAK,MAAM,gBAAgB,CAAC"}
@@ -0,0 +1,2 @@
1
+ export declare function databaseUrl(): string;
2
+ //# sourceMappingURL=database.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../client/src/database.ts"],"names":[],"mappings":"AAaA,wBAAgB,WAAW,IAAI,MAAM,CAcpC"}
@@ -0,0 +1,30 @@
1
+ import { CapabilityNotGranted } from "./errors.js";
2
+ // The address of the tool's own PostgreSQL database, as the Chest gives it
3
+ // to a server tool whose chest.json declares "capabilities": ["database"]:
4
+ // postgres://<user>:<password>@127.0.0.1:<port>/<database>?sslmode=disable,
5
+ // where the port is the tool's launcher, which relays each connection to the
6
+ // Chest (the container has no network). Hand it to a PostgreSQL client —
7
+ // postgres (porsager) or pg; the SDK carries none. PGHOST, PGPORT, PGUSER,
8
+ // PGPASSWORD and PGDATABASE say the same for a client that reads them.
9
+ //
10
+ // Throws CapabilityNotGranted when the Chest gave no database: the version
11
+ // does not declare it, or a DATABASE_URL of the tool's own is not the
12
+ // Chest's. The value is a secret: never log it, never send it to a browser.
13
+ export function databaseUrl() {
14
+ const value = process.env["DATABASE_URL"];
15
+ if (typeof value !== "string" || value.length > 1024)
16
+ throw new CapabilityNotGranted("database");
17
+ let url;
18
+ try {
19
+ url = new URL(value);
20
+ }
21
+ catch {
22
+ throw new CapabilityNotGranted("database");
23
+ }
24
+ 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 !== "") {
26
+ throw new CapabilityNotGranted("database");
27
+ }
28
+ return value;
29
+ }
30
+ //# sourceMappingURL=database.js.map
@@ -0,0 +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"}
@@ -0,0 +1,18 @@
1
+ export declare class ChestError extends Error {
2
+ readonly code: string;
3
+ readonly status: number;
4
+ constructor(code: string, status: number, message: string);
5
+ }
6
+ export declare class CapabilityNotGranted extends ChestError {
7
+ constructor(capability: string);
8
+ }
9
+ export declare class QuotaExceeded extends ChestError {
10
+ constructor();
11
+ }
12
+ export declare class TooLarge extends ChestError {
13
+ constructor();
14
+ }
15
+ export declare class Unavailable extends ChestError {
16
+ constructor();
17
+ }
18
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../client/src/errors.ts"],"names":[],"mappings":"AAEA,qBAAa,UAAW,SAAQ,KAAK;IACnC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,YAAY,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAKxD;CACF;AAKD,qBAAa,oBAAqB,SAAQ,UAAU;IAClD,YAAY,UAAU,EAAE,MAAM,EAE7B;CACF;AAID,qBAAa,aAAc,SAAQ,UAAU;IAC3C,cAEC;CACF;AAGD,qBAAa,QAAS,SAAQ,UAAU;IACtC,cAEC;CACF;AAID,qBAAa,WAAY,SAAQ,UAAU;IACzC,cAEC;CACF"}
@@ -0,0 +1,41 @@
1
+ // What the SDK throws when the Chest does not give what a tool asks: a code
2
+ // the tool can test, and the HTTP status the Chest answers for it.
3
+ export class ChestError extends Error {
4
+ code;
5
+ status;
6
+ constructor(code, status, message) {
7
+ super(message);
8
+ this.name = new.target.name;
9
+ this.code = code;
10
+ this.status = status;
11
+ }
12
+ }
13
+ // The version of the tool in service does not declare the capability in its
14
+ // chest.json ("capabilities"), or it was not approved: the Chest gives it
15
+ // nothing.
16
+ export class CapabilityNotGranted extends ChestError {
17
+ constructor(capability) {
18
+ super("capability_not_granted", 403, `the Chest did not grant the capability "${capability}" to this version of the tool`);
19
+ }
20
+ }
21
+ // What the tool keeps would go beyond what its Chest gives it: its total
22
+ // (1 GiB of files) or its count (10,000 objects).
23
+ export class QuotaExceeded extends ChestError {
24
+ constructor() {
25
+ super("quota_exceeded", 429, "the Chest refused: the tool's quota would be exceeded");
26
+ }
27
+ }
28
+ // One object is beyond the bound of one (32 MiB for a file).
29
+ export class TooLarge extends ChestError {
30
+ constructor() {
31
+ super("too_large", 413, "the Chest refused: the object is too large");
32
+ }
33
+ }
34
+ // The Chest did not answer, or not as it does: nothing is known of what was
35
+ // asked — a write may or may not have happened.
36
+ export class Unavailable extends ChestError {
37
+ constructor() {
38
+ super("unavailable", 503, "the Chest is unavailable");
39
+ }
40
+ }
41
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../client/src/errors.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,mEAAmE;AACnE,MAAM,OAAO,UAAW,SAAQ,KAAK;IAC1B,IAAI,CAAS;IACb,MAAM,CAAS;IACxB,YAAY,IAAY,EAAE,MAAc,EAAE,OAAe;QACvD,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC;QAC5B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,CAAC;CACF;AAED,4EAA4E;AAC5E,0EAA0E;AAC1E,WAAW;AACX,MAAM,OAAO,oBAAqB,SAAQ,UAAU;IAClD,YAAY,UAAkB;QAC5B,KAAK,CAAC,wBAAwB,EAAE,GAAG,EAAE,2CAA2C,UAAU,+BAA+B,CAAC,CAAC;IAC7H,CAAC;CACF;AAED,yEAAyE;AACzE,kDAAkD;AAClD,MAAM,OAAO,aAAc,SAAQ,UAAU;IAC3C;QACE,KAAK,CAAC,gBAAgB,EAAE,GAAG,EAAE,uDAAuD,CAAC,CAAC;IACxF,CAAC;CACF;AAED,6DAA6D;AAC7D,MAAM,OAAO,QAAS,SAAQ,UAAU;IACtC;QACE,KAAK,CAAC,WAAW,EAAE,GAAG,EAAE,4CAA4C,CAAC,CAAC;IACxE,CAAC;CACF;AAED,4EAA4E;AAC5E,gDAAgD;AAChD,MAAM,OAAO,WAAY,SAAQ,UAAU;IACzC;QACE,KAAK,CAAC,aAAa,EAAE,GAAG,EAAE,0BAA0B,CAAC,CAAC;IACxD,CAAC;CACF"}
@@ -0,0 +1,28 @@
1
+ export type FileObject = {
2
+ name: string;
3
+ type: string;
4
+ size: number;
5
+ updated: string;
6
+ };
7
+ export type FileData = {
8
+ data: Uint8Array;
9
+ type: string;
10
+ size: number;
11
+ };
12
+ export type FilePage = {
13
+ files: FileObject[];
14
+ next: string | null;
15
+ };
16
+ export declare function put(name: string, data: Uint8Array | string, type?: string): Promise<FileObject>;
17
+ export declare function get(name: string): Promise<FileData | null>;
18
+ export declare function list(options?: {
19
+ prefix?: string;
20
+ after?: string;
21
+ }): Promise<FilePage>;
22
+ declare function remove(name: string): Promise<boolean>;
23
+ export { remove as delete };
24
+ export declare function url(name: string): Promise<{
25
+ url: string;
26
+ expiresIn: number;
27
+ }>;
28
+ //# sourceMappingURL=files.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"files.d.ts","sourceRoot":"","sources":["../../client/src/files.ts"],"names":[],"mappings":"AAkBA,MAAM,MAAM,UAAU,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AACvF,MAAM,MAAM,QAAQ,GAAG;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AACxE,MAAM,MAAM,QAAQ,GAAG;IAAE,KAAK,EAAE,UAAU,EAAE,CAAC;IAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAC;AA+FpE,wBAAsB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAQrG;AAGD,wBAAsB,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAUhE;AAKD,wBAAsB,IAAI,CAAC,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAa/F;AAGD,iBAAe,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CASpD;AACD,OAAO,EAAE,MAAM,IAAI,MAAM,EAAE,CAAC;AAM5B,wBAAsB,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC,CAOnF"}
@@ -0,0 +1,178 @@
1
+ import { CapabilityNotGranted, ChestError, QuotaExceeded, TooLarge, Unavailable } from "./errors.js";
2
+ const maxObject = 32 << 20;
3
+ const maxAnswer = 4 << 20;
4
+ const deadline = 120000;
5
+ 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;
6
+ // base is the Chest's API as the launcher gives it; without, the version
7
+ // does not keep files.
8
+ function base() {
9
+ const value = process.env["CHEST_API"];
10
+ if (typeof value !== "string" || !/^http:\/\/127\.0\.0\.1:[1-9][0-9]{0,4}$/u.test(value) || Number(value.slice(17)) > 65535)
11
+ throw new CapabilityNotGranted("files");
12
+ return value;
13
+ }
14
+ function checkName(name) {
15
+ if (typeof name !== "string" || !namePattern.test(name))
16
+ throw new ChestError("invalid_name", 400, "invalid file name");
17
+ return name;
18
+ }
19
+ // ask sends one request to the Chest; a failure to reach it is Unavailable.
20
+ async function ask(method, path, init = {}) {
21
+ const headers = {};
22
+ if (init.type !== undefined)
23
+ headers["Content-Type"] = init.type;
24
+ try {
25
+ return await fetch(base() + path, { method, headers, ...(init.body !== undefined ? { body: init.body } : {}), redirect: "error", signal: AbortSignal.timeout(deadline) });
26
+ }
27
+ catch (error) {
28
+ if (error instanceof ChestError)
29
+ throw error;
30
+ throw new Unavailable();
31
+ }
32
+ }
33
+ // read takes a body of limit bytes at most; beyond, or cut, the answer is not
34
+ // the Chest's.
35
+ async function read(response, limit) {
36
+ const declared = Number(response.headers.get("content-length") ?? "0");
37
+ if (declared > limit)
38
+ throw new Unavailable();
39
+ const chunks = [];
40
+ let size = 0;
41
+ try {
42
+ for await (const chunk of response.body ?? []) {
43
+ size += chunk.byteLength;
44
+ if (size > limit)
45
+ throw new Unavailable();
46
+ chunks.push(chunk);
47
+ }
48
+ }
49
+ catch {
50
+ throw new Unavailable();
51
+ }
52
+ const all = new Uint8Array(size);
53
+ let at = 0;
54
+ for (const chunk of chunks) {
55
+ all.set(chunk, at);
56
+ at += chunk.byteLength;
57
+ }
58
+ return all;
59
+ }
60
+ async function json(response) {
61
+ try {
62
+ return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(await read(response, maxAnswer)));
63
+ }
64
+ catch (error) {
65
+ if (error instanceof ChestError)
66
+ throw error;
67
+ throw new Unavailable();
68
+ }
69
+ }
70
+ // refusal turns an answer that is not a success into what the tool tests.
71
+ async function refusal(response) {
72
+ if (response.status === 403)
73
+ return new CapabilityNotGranted("files");
74
+ if (response.status === 413)
75
+ return new TooLarge();
76
+ if (response.status === 429)
77
+ return new QuotaExceeded();
78
+ if (response.status >= 500)
79
+ return new Unavailable();
80
+ let code = "refused";
81
+ try {
82
+ const body = await json(response);
83
+ const given = body?.error;
84
+ if (typeof given === "string" && /^[a-z_]{1,40}$/u.test(given))
85
+ code = given;
86
+ }
87
+ catch {
88
+ // The code stays « refused ».
89
+ }
90
+ return new ChestError(code, response.status, `the Chest refused: ${code}`);
91
+ }
92
+ function isObject(value) {
93
+ if (!value || typeof value !== "object" || Array.isArray(value))
94
+ return false;
95
+ const o = value;
96
+ return typeof o["name"] === "string" && namePattern.test(o["name"]) && typeof o["type"] === "string" && o["type"].length <= 200 && typeof o["size"] === "number" && Number.isSafeInteger(o["size"]) && o["size"] >= 0 && typeof o["updated"] === "string";
97
+ }
98
+ function object(value) {
99
+ if (!isObject(value))
100
+ throw new Unavailable();
101
+ return { name: value.name, type: value.type, size: value.size, updated: value.updated };
102
+ }
103
+ // put keeps data as the file name, of type type (application/octet-stream
104
+ // when none), replacing the one of that name.
105
+ export async function put(name, data, type) {
106
+ checkName(name);
107
+ if (!(data instanceof Uint8Array) && typeof data !== "string")
108
+ throw new TypeError("data must be bytes or text");
109
+ const body = typeof data === "string" ? new TextEncoder().encode(data) : new Uint8Array(data);
110
+ if (body.byteLength > maxObject)
111
+ throw new TooLarge();
112
+ const response = await ask("PUT", "/files/" + name, { body, type: type ?? (typeof data === "string" ? "text/plain; charset=utf-8" : "application/octet-stream") });
113
+ if (response.status !== 201)
114
+ throw await refusal(response);
115
+ return object(await json(response));
116
+ }
117
+ // get reads a file; null when the tool has none of that name.
118
+ export async function get(name) {
119
+ checkName(name);
120
+ const response = await ask("GET", "/files/" + name);
121
+ if (response.status === 404) {
122
+ await response.body?.cancel();
123
+ return null;
124
+ }
125
+ if (response.status !== 200)
126
+ throw await refusal(response);
127
+ const data = await read(response, maxObject);
128
+ return { data, type: response.headers.get("content-type") ?? "application/octet-stream", size: data.byteLength };
129
+ }
130
+ // list says the files whose name starts with prefix, in the order of their
131
+ // names, after the name after, 1000 at most; next is the name to ask after
132
+ // for the following page, null once all were said.
133
+ export async function list(options = {}) {
134
+ const query = new URLSearchParams();
135
+ if (options.prefix !== undefined && options.prefix !== "") {
136
+ if (typeof options.prefix !== "string" || options.prefix.length > 807 || !/^[A-Za-z0-9._\/-]*$/u.test(options.prefix))
137
+ throw new ChestError("invalid_name", 400, "invalid prefix");
138
+ query.set("prefix", options.prefix);
139
+ }
140
+ if (options.after !== undefined && options.after !== "")
141
+ query.set("after", checkName(options.after));
142
+ const response = await ask("GET", "/files" + (query.size ? "?" + query.toString() : ""));
143
+ if (response.status !== 200)
144
+ throw await refusal(response);
145
+ const body = await json(response);
146
+ const page = body;
147
+ if (!page || !Array.isArray(page.files) || page.files.length > 1000 || !(page.next === null || (typeof page.next === "string" && namePattern.test(page.next))))
148
+ throw new Unavailable();
149
+ return { files: page.files.map(object), next: page.next };
150
+ }
151
+ // remove deletes a file; false when the tool had none of that name.
152
+ async function remove(name) {
153
+ checkName(name);
154
+ const response = await ask("DELETE", "/files/" + name);
155
+ if (response.status === 204)
156
+ return true;
157
+ if (response.status === 404) {
158
+ await response.body?.cancel();
159
+ return false;
160
+ }
161
+ throw await refusal(response);
162
+ }
163
+ export { remove as delete };
164
+ // url signs a link to the file as it is now, on the tool's team host: anyone
165
+ // who has it opens the file, without signing in, for expiresIn seconds
166
+ // (15 minutes), or until the file changes or goes. Give it to a member's
167
+ // browser, never to a public page.
168
+ export async function url(name) {
169
+ checkName(name);
170
+ const response = await ask("POST", "/files/url", { body: JSON.stringify({ name }), type: "application/json" });
171
+ if (response.status !== 200)
172
+ throw await refusal(response);
173
+ const body = (await json(response));
174
+ if (!body || typeof body.url !== "string" || !/^https:\/\/[^/?#]+\/_chest\/files\/[A-Za-z0-9_.-]{1,1536}$/u.test(body.url) || typeof body.expires_in !== "number" || !Number.isInteger(body.expires_in) || body.expires_in <= 0)
175
+ throw new Unavailable();
176
+ return { url: body.url, expiresIn: body.expires_in };
177
+ }
178
+ //# sourceMappingURL=files.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"files.js","sourceRoot":"","sources":["../../client/src/files.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ import type { IncomingMessage } from "node:http";
2
+ export type Member = {
3
+ id: string;
4
+ firstName: string;
5
+ lastName: string;
6
+ name: string;
7
+ email: string;
8
+ photo?: string;
9
+ role?: string;
10
+ isAdmin: boolean;
11
+ isBuilder: boolean;
12
+ };
13
+ export declare function member(request: IncomingMessage | Request): Member | null;
14
+ //# sourceMappingURL=member.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"member.d.ts","sourceRoot":"","sources":["../../client/src/member.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAKjD,MAAM,MAAM,MAAM,GAAG;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,OAAO,CAAC;CACpB,CAAC;AAmCF,wBAAgB,MAAM,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,GAAG,MAAM,GAAG,IAAI,CAuBxE"}
@@ -0,0 +1,66 @@
1
+ import { createHmac, timingSafeEqual } from "node:crypto";
2
+ // The key of the assertions is HMAC-SHA256 of this label under the text of
3
+ // CHEST_TOKEN, exactly as the Chest derives it (chest/toolfront).
4
+ const label = "Chest-Member v1";
5
+ // Clocks of the Chest and of the container may differ by this much, in seconds.
6
+ const skew = 5;
7
+ // An assertion is a few hundred bytes; anything longer is not one.
8
+ const maxLength = 8192;
9
+ const compact = /^([A-Za-z0-9_-]+)\.([A-Za-z0-9_-]+)\.([A-Za-z0-9_-]+)$/u;
10
+ const claims = ["iss", "aud", "iat", "exp", "sub", "given_name", "family_name", "name", "email", "picture", "role", "admin", "builder"];
11
+ function assertionOf(request) {
12
+ const headers = request.headers;
13
+ // A Web Request joins repeated headers with ", ", which no assertion
14
+ // contains; a Node request keeps them as an array: both are refused.
15
+ const value = typeof headers.get === "function" ? headers.get("chest-member") : headers["chest-member"];
16
+ return typeof value === "string" && value.length <= maxLength ? value : null;
17
+ }
18
+ function json(part) {
19
+ try {
20
+ const value = JSON.parse(Buffer.from(part, "base64url").toString("utf8"));
21
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
22
+ }
23
+ catch {
24
+ return null;
25
+ }
26
+ }
27
+ // member returns who the Chest says is making this request, or null when the
28
+ // request carries no valid assertion — absent, malformed, signed with another
29
+ // key, for another tool, expired or not yet valid —, or when CHEST_TOKEN or
30
+ // CHEST_TOOL is missing. It never throws for what a request carries. Only the
31
+ // Chest's front reaches the tool; the signature is a second defence, and the
32
+ // tool still decides what a member may do with its own rules.
33
+ export function member(request) {
34
+ const token = process.env["CHEST_TOKEN"];
35
+ const tool = process.env["CHEST_TOOL"];
36
+ if (!token || !/^[A-Za-z0-9_-]{43,512}$/u.test(token) || !tool)
37
+ return null;
38
+ const assertion = assertionOf(request);
39
+ const parts = assertion === null ? null : compact.exec(assertion);
40
+ if (!parts)
41
+ return null;
42
+ const [, encodedHeader = "", encodedPayload = "", encodedSignature = ""] = parts;
43
+ const header = json(encodedHeader);
44
+ if (!header || Object.keys(header).length !== 2 || header["alg"] !== "HS256" || header["typ"] !== "JWT")
45
+ return null;
46
+ const key = createHmac("sha256", Buffer.from(token, "utf8")).update(label).digest();
47
+ const expected = createHmac("sha256", key).update(encodedHeader + "." + encodedPayload).digest();
48
+ const signature = Buffer.from(encodedSignature, "base64url");
49
+ if (signature.length !== expected.length || !timingSafeEqual(signature, expected))
50
+ return null;
51
+ const payload = json(encodedPayload);
52
+ if (!payload || !claims.every(name => Object.hasOwn(payload, name)))
53
+ return null;
54
+ const { iss, aud, iat, exp, sub, given_name, family_name, name, email, picture, role, admin, builder } = payload;
55
+ if (typeof iss !== "string" || iss === "" || aud !== tool || typeof sub !== "string" || sub === "")
56
+ return null;
57
+ if (typeof iat !== "number" || !Number.isSafeInteger(iat) || typeof exp !== "number" || !Number.isSafeInteger(exp) || exp <= iat)
58
+ return null;
59
+ const now = Math.floor(Date.now() / 1000);
60
+ if (iat > now + skew || exp <= now - skew)
61
+ return null;
62
+ if (typeof given_name !== "string" || typeof family_name !== "string" || typeof name !== "string" || typeof email !== "string" || typeof picture !== "string" || typeof role !== "string" || typeof admin !== "boolean" || typeof builder !== "boolean")
63
+ return null;
64
+ return { id: sub, firstName: given_name, lastName: family_name, name, email, ...(picture === "" ? {} : { photo: picture }), ...(role === "" ? {} : { role }), isAdmin: admin, isBuilder: builder };
65
+ }
66
+ //# sourceMappingURL=member.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"member.js","sourceRoot":"","sources":["../../client/src/member.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,77 @@
1
+ {
2
+ "name": "@argentic/chest-sdk",
3
+ "version": "0.1.0",
4
+ "description": "The client a Chest server tool embeds: the member the Chest asserts on a request, the tool's own database and its private files.",
5
+ "keywords": [
6
+ "chest",
7
+ "argentic",
8
+ "sdk",
9
+ "chest-tool",
10
+ "member",
11
+ "files",
12
+ "database"
13
+ ],
14
+ "homepage": "https://github.com/chest-by-argentic/Chest-SDK#readme",
15
+ "bugs": {
16
+ "url": "https://github.com/chest-by-argentic/Chest-SDK/issues"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/chest-by-argentic/Chest-SDK.git"
21
+ },
22
+ "license": "MIT",
23
+ "author": "Argentic",
24
+ "type": "module",
25
+ "engines": {
26
+ "node": ">=22"
27
+ },
28
+ "sideEffects": false,
29
+ "exports": {
30
+ ".": {
31
+ "types": "./dist/index.d.ts",
32
+ "default": "./dist/index.js"
33
+ },
34
+ "./errors": {
35
+ "types": "./dist/src/errors.d.ts",
36
+ "default": "./dist/src/errors.js"
37
+ },
38
+ "./member": {
39
+ "types": "./dist/src/member.d.ts",
40
+ "default": "./dist/src/member.js"
41
+ },
42
+ "./database": {
43
+ "types": "./dist/src/database.d.ts",
44
+ "default": "./dist/src/database.js"
45
+ },
46
+ "./files": {
47
+ "types": "./dist/src/files.d.ts",
48
+ "default": "./dist/src/files.js"
49
+ },
50
+ "./package.json": "./package.json"
51
+ },
52
+ "files": [
53
+ "dist",
54
+ "client/index.ts",
55
+ "client/src/errors.ts",
56
+ "client/src/member.ts",
57
+ "client/src/database.ts",
58
+ "client/src/files.ts"
59
+ ],
60
+ "publishConfig": {
61
+ "access": "public",
62
+ "provenance": true
63
+ },
64
+ "scripts": {
65
+ "clean": "node -e \"for (const d of ['dist', 'build']) require('node:fs').rmSync(d, {recursive: true, force: true})\"",
66
+ "build": "npm run clean && tsc -p tsconfig.build.json",
67
+ "test": "npm run build && tsc -p tsconfig.json && node --test build/client/test/*.test.js",
68
+ "check:package": "node scripts/check-package.mjs",
69
+ "prepack": "npm run build",
70
+ "prepublishOnly": "npm test && npm run check:package"
71
+ },
72
+ "devDependencies": {
73
+ "@types/node": "24.13.5",
74
+ "esbuild": "0.28.2",
75
+ "typescript": "7.0.2"
76
+ }
77
+ }