@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 +21 -0
- package/README.md +206 -0
- package/client/index.ts +11 -0
- package/client/src/database.ts +28 -0
- package/client/src/errors.ts +44 -0
- package/client/src/files.ts +181 -0
- package/client/src/member.ts +75 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/src/database.d.ts +2 -0
- package/dist/src/database.d.ts.map +1 -0
- package/dist/src/database.js +30 -0
- package/dist/src/database.js.map +1 -0
- package/dist/src/errors.d.ts +18 -0
- package/dist/src/errors.d.ts.map +1 -0
- package/dist/src/errors.js +41 -0
- package/dist/src/errors.js.map +1 -0
- package/dist/src/files.d.ts +28 -0
- package/dist/src/files.d.ts.map +1 -0
- package/dist/src/files.js +178 -0
- package/dist/src/files.js.map +1 -0
- package/dist/src/member.d.ts +14 -0
- package/dist/src/member.d.ts.map +1 -0
- package/dist/src/member.js +66 -0
- package/dist/src/member.js.map +1 -0
- package/package.json +77 -0
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.
|
package/client/index.ts
ADDED
|
@@ -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
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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 @@
|
|
|
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
|
+
}
|