@argentic/chest-sdk 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +208 -43
- package/client/index.ts +4 -3
- package/client/src/api.ts +18 -0
- package/client/src/chest.ts +30 -6
- package/client/src/events.ts +12 -104
- package/client/src/files.ts +9 -9
- package/client/src/members.ts +18 -13
- package/client/src/schedules.ts +87 -0
- package/client/src/signed.ts +166 -0
- package/client/src/testing.ts +132 -54
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/src/api.d.ts +2 -0
- package/dist/src/api.d.ts.map +1 -1
- package/dist/src/api.js +18 -0
- package/dist/src/api.js.map +1 -1
- package/dist/src/chest.d.ts +5 -0
- package/dist/src/chest.d.ts.map +1 -1
- package/dist/src/chest.js +13 -1
- package/dist/src/chest.js.map +1 -1
- package/dist/src/events.d.ts +3 -6
- package/dist/src/events.d.ts.map +1 -1
- package/dist/src/events.js +8 -102
- package/dist/src/events.js.map +1 -1
- package/dist/src/files.d.ts +1 -0
- package/dist/src/files.d.ts.map +1 -1
- package/dist/src/files.js +6 -7
- package/dist/src/files.js.map +1 -1
- package/dist/src/members.d.ts +1 -1
- package/dist/src/members.d.ts.map +1 -1
- package/dist/src/members.js +6 -5
- package/dist/src/members.js.map +1 -1
- package/dist/src/schedules.d.ts +15 -0
- package/dist/src/schedules.d.ts.map +1 -0
- package/dist/src/schedules.js +45 -0
- package/dist/src/schedules.js.map +1 -0
- package/dist/src/signed.d.ts +29 -0
- package/dist/src/signed.d.ts.map +1 -0
- package/dist/src/signed.js +139 -0
- package/dist/src/signed.js.map +1 -0
- package/dist/src/testing.d.ts +15 -5
- package/dist/src/testing.d.ts.map +1 -1
- package/dist/src/testing.js +124 -52
- package/dist/src/testing.js.map +1 -1
- package/package.json +14 -3
package/README.md
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
# Chest SDK
|
|
2
2
|
|
|
3
|
-
`@argentic/chest-sdk` is what a server tool (tool contract
|
|
3
|
+
`@argentic/chest-sdk` is what a server tool (tool contract 0.4) embeds to talk
|
|
4
4
|
with its Chest: the member the Chest asserts on a request, the Chest itself
|
|
5
|
-
(its organization, time zone and
|
|
6
|
-
who have the tool, the address of the tool's own
|
|
7
|
-
files, the badges and notifications it shows members
|
|
8
|
-
events of its members' lifecycle, AI models through the
|
|
9
|
-
tool's tests, a fake Chest.
|
|
10
|
-
|
|
5
|
+
(its organization, time zone, language and currency, and where the tool is
|
|
6
|
+
reached), the other members who have the tool, the address of the tool's own
|
|
7
|
+
database, its private files, the badges and notifications it shows members
|
|
8
|
+
inside the Chest, the events of its members' lifecycle, AI models through the
|
|
9
|
+
Chest — and, for the tool's tests, a fake Chest. It also publishes the tool
|
|
10
|
+
contract ([`contract/`](contract/README.md): what `chest.json` may say, what
|
|
11
|
+
the Chest builds, what migrations may do, the policies it adds) and `chest
|
|
12
|
+
check`, the Chest's own validator. The SDK has no dependency: it only
|
|
13
|
+
imports `node:*`.
|
|
11
14
|
|
|
12
15
|
```sh
|
|
13
16
|
npm install @argentic/chest-sdk
|
|
@@ -18,23 +21,24 @@ Node 22 or later. ESM only, compiled JavaScript with its type declarations.
|
|
|
18
21
|
## Imports
|
|
19
22
|
|
|
20
23
|
Each module is its own subpath and pulls in nothing else; the root gives them
|
|
21
|
-
all, with the files, members, notifications, events and ai APIs
|
|
22
|
-
namespaces `files`, `members`, `notifications`, `events
|
|
23
|
-
testing module is not in the root).
|
|
24
|
+
all, with the files, members, notifications, events, schedules and ai APIs
|
|
25
|
+
as the namespaces `files`, `members`, `notifications`, `events`, `schedules`
|
|
26
|
+
and `ai` (the testing module is not in the root).
|
|
24
27
|
|
|
25
28
|
| Import | Gives |
|
|
26
29
|
|---|---|
|
|
27
30
|
| `@argentic/chest-sdk/member` | `member(request)`, type `Member`: the member of a request on the team host of a server tool, with the language the Chest speaks to them and the zone they work in, read from the `Chest-Member` assertion and verified; `null` without a valid assertion. `memberIdPattern`, `groupIdPattern`, `languagePattern`, `timeZonePattern`: the grammars of the identifiers (`mbr_…`, `grp_…`), of a language and of a zone |
|
|
28
|
-
| `@argentic/chest-sdk/chest` | `chest`, type `Chest`: the Chest the tool runs in — `chest.organization.name`, `chest.timeZone`, `chest.language`, `chest.today()` —, the same for every member, on a request or outside one |
|
|
31
|
+
| `@argentic/chest-sdk/chest` | `chest`, type `Chest`: the Chest the tool runs in — `chest.organization.name`, `chest.timeZone`, `chest.language`, `chest.currency`, `chest.today()` — and where the tool is reached — `chest.tool.teamUrl`, `chest.tool.publicUrl` —, the same for every member, on a request or outside one |
|
|
29
32
|
| `@argentic/chest-sdk/members` | `list`, `get`, `lookup`, `groups.list`, `forget`, types `MemberPage`, `Lookup`, `FormerMember`, `Group`: the members who have the tool (capability `members`, their addresses with `members.email`) |
|
|
30
33
|
| `@argentic/chest-sdk/notifications` | `notify`, `withdraw`, `badge.set`, `badge.setMany`, types `Notice`, `Delivery`, `BadgeCount`, `BadgeWrite`: counters on the tool's tile and items in members' inboxes, inside the Chest (capability `notifications`) |
|
|
31
34
|
| `@argentic/chest-sdk/events` | `handle`, `verify`, `acknowledgeErasure`, `memorySeen`, `erasureIdPattern`, types `ChestEvent`, `MemberUpdated`, `AccessRevoked`, `MemberRemoved`, `MemberErased`, `MemberChange`, `Handlers`, `Seen`: the events of the members' lifecycle the Chest posts to the tool's `/chest-events` (`"receives": ["member.*"]`), verified, deduplicated by id, and the acknowledgment of an erasure |
|
|
35
|
+
| `@argentic/chest-sdk/schedules` | `handle`, `verify`, types `Run`, `Handlers`, `Seen`: the runs of the tool's schedules (`"schedules"` in `chest.json`) the Chest posts to its `/chest-schedules` at their times, verified, deduplicated by id |
|
|
32
36
|
| `@argentic/chest-sdk/ai` | `chat`, `embed`, `models`, `usage`, types `Alias`, `Provider`, `ChatMessage`, `ChatTool`, `ToolChoice`, `ResponseFormat`, `ChatOptions`, `ChatResult`, `ChatChunk`, `ToolCall`, `ToolCallDelta`, `Usage`, `EmbedOptions`, `Embeddings`, `AiModel`, `AiUsage`: AI models through the Chest, on the owner's connectors, metered against the tool's monthly cap (capability `ai`) |
|
|
33
37
|
| `@argentic/chest-sdk/database` | `databaseUrl()`: the address of the tool's own PostgreSQL database (capability `database`) |
|
|
34
38
|
| `@argentic/chest-sdk/files` | `put`, `get`, `stat`, `list`, `move`, `delete`, `url`, `uploadUrl`, types `FileObject`, `FileData`, `FilePage`: the tool's private files (capability `files`), kept by the Chest, a 15-minute signed link to one (or to its thumbnail), and uploads straight from a member's browser |
|
|
35
39
|
| `@argentic/chest-sdk/errors` | `ChestError` (`code`, `status`), `CapabilityNotGranted` (403), `TooLarge` (413), `QuotaExceeded` (429), `RateLimited` (429), `Unavailable` (503), and for AI `AiCapReached` (402), `AiModelNotAllowed` (403), `AiRefused` (422), `AiUnavailable` (502, 503), type `AiUnavailableReason`: what the SDK throws when the Chest does not give what a tool asks |
|
|
36
|
-
| `@argentic/chest-sdk/testing` | `signAssertion`, `withMember`, `fakeChest`, types `AssertionOptions`, `FakeChest`, `FakeChestOptions`, `FakeGroup`, `FakeFile`, `FakeNotification`, `FakeEvent`, `FakeAi`, `FakeAiModel`, `FakeAiReply`, `FakeAiCall`: for the tool's own tests only |
|
|
37
|
-
| `@argentic/chest-sdk` | all of the above but `testing`; `files`, `members`, `notifications`, `events` and `ai` as namespaces |
|
|
40
|
+
| `@argentic/chest-sdk/testing` | `signAssertion`, `withMember`, `fakeChest`, types `AssertionOptions`, `FakeChest`, `FakeChestOptions`, `FakeGroup`, `FakeFile`, `FakeFormer`, `FakeNotification`, `FakeEvent`, `FakeRun`, `FakeAi`, `FakeAiModel`, `FakeAiReply`, `FakeAiCall`: for the tool's own tests only |
|
|
41
|
+
| `@argentic/chest-sdk` | all of the above but `testing`; `files`, `members`, `notifications`, `events`, `schedules` and `ai` as namespaces |
|
|
38
42
|
|
|
39
43
|
```ts
|
|
40
44
|
import { member } from "@argentic/chest-sdk/member";
|
|
@@ -49,6 +53,10 @@ import { CapabilityNotGranted } from "@argentic/chest-sdk/errors";
|
|
|
49
53
|
// or: import { member, chest, databaseUrl, files, members, notifications, events, ai } from "@argentic/chest-sdk";
|
|
50
54
|
```
|
|
51
55
|
|
|
56
|
+
`chest check`, the Chest's validator, is a separate development package,
|
|
57
|
+
`@argentic/chest-check`, not published yet (see [Check a tool](#check-a-tool--chest-check)):
|
|
58
|
+
this one stays a small runtime client.
|
|
59
|
+
|
|
52
60
|
Types refer to `node:http` (`IncomingMessage`): a TypeScript project needs
|
|
53
61
|
`@types/node`, as any Node project does. Both `moduleResolution` `bundler` and
|
|
54
62
|
`nodenext` work.
|
|
@@ -73,20 +81,53 @@ export function GET(request: Request) {
|
|
|
73
81
|
|
|
74
82
|
## The contract, in short
|
|
75
83
|
|
|
76
|
-
A
|
|
84
|
+
A tool is an ordinary web server in a container without network, run by
|
|
77
85
|
its Chest. The Chest's front is the only one to reach it; the tool reaches only
|
|
78
86
|
what its launcher gives it on `127.0.0.1` (its database, the Chest's API for
|
|
79
87
|
its files, its members, its notifications and AI), and the Chest posts it the
|
|
80
|
-
events it receives on `/chest-events
|
|
88
|
+
events it receives on `/chest-events` and the runs of its schedules on
|
|
89
|
+
`/chest-schedules`, through the same launcher. Rights come
|
|
81
90
|
from the Chest — the signed member, the capabilities approved for the
|
|
82
91
|
version — and the Chest enforces them even outside the SDK:
|
|
83
|
-
the SDK makes the calls easier, it is not a security boundary. The
|
|
84
|
-
|
|
85
|
-
|
|
92
|
+
the SDK makes the calls easier, it is not a security boundary. The contract
|
|
93
|
+
itself — every key of `chest.json` and its bounds, what the Chest builds,
|
|
94
|
+
what migrations may create, the Content-Security-Policy it adds, Next.js on
|
|
95
|
+
a Chest — is [`contract/README.md`](contract/README.md), rendered from the
|
|
96
|
+
Chest's own code.
|
|
97
|
+
|
|
98
|
+
## Check a tool — `chest check`
|
|
99
|
+
|
|
100
|
+
```sh
|
|
101
|
+
# once, in a clone of chest-by-argentic/Chest-SDK (not on npm yet)
|
|
102
|
+
npm ci # builds check/ too
|
|
103
|
+
npx chest check /path/to/the/tool # --json for agents and CI
|
|
104
|
+
# or, in the tool's repository, a local devDependency
|
|
105
|
+
npm install --save-dev /path/to/Chest-SDK/check
|
|
106
|
+
npx chest check
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
`@argentic/chest-check` is `check/` of this repository, not published on npm
|
|
110
|
+
yet: it runs from a clone.
|
|
111
|
+
|
|
112
|
+
The Chest's own validator — the code a Chest runs on every repository it
|
|
113
|
+
builds, compiled to WebAssembly (1.6 MB, in its own package, `check/` of
|
|
114
|
+
this repository, so that a tool's runtime dependencies stay small) — judges the repository as
|
|
115
|
+
the Chest would receive it: the files Git tracks or would add, as they are
|
|
116
|
+
now, committed or not. It says `OK` with the tool's name, roles, what it
|
|
117
|
+
asks and its migrations, or `Refused` with the Chest's reason (`manifest`,
|
|
118
|
+
`migrations`, `no_lock`, `newer_chest`…) and the rule broken; exit status 0,
|
|
119
|
+
1, or 2 when it could not run (not a Git repository). It reads nothing but
|
|
120
|
+
the archive it is given, and needs no network and no Chest. Details:
|
|
121
|
+
[`contract/README.md`](contract/README.md#check-a-repository).
|
|
86
122
|
|
|
87
|
-
|
|
123
|
+
`chest.json` names the version of the contract the tool is written for,
|
|
124
|
+
`"chest": "0.4"` — the MAJOR.MINOR of this SDK. A Chest older than that
|
|
125
|
+
refuses the tool with “This tool needs a newer version of your Chest”;
|
|
126
|
+
up to its own version, a key it does not know is refused, never ignored.
|
|
88
127
|
|
|
89
|
-
|
|
128
|
+
## `member(request)` — the member of a request
|
|
129
|
+
|
|
130
|
+
A tool is an ordinary web server; on its team host, the Chest relays
|
|
90
131
|
`/chest` and everything below it with the `Chest-Member` header of the
|
|
91
132
|
signed-in member. `member(request)` accepts a Node request (`IncomingMessage`)
|
|
92
133
|
or a Web `Request` and returns a `Member`, the type the `members` API
|
|
@@ -176,13 +217,20 @@ import { chest } from "@argentic/chest-sdk/chest";
|
|
|
176
217
|
chest.organization.name; // "Acme SAS": the organization the Chest is of, as its owner wrote it
|
|
177
218
|
chest.timeZone; // "Europe/Paris": an IANA zone, "UTC" until the owner sets one
|
|
178
219
|
chest.language; // "fr": the Chest's own language (a member's is member(request).language)
|
|
220
|
+
chest.currency; // "EUR": the Chest's currency, ISO 4217
|
|
221
|
+
chest.tool.teamUrl; // "https://tasks-chest.acme.argentic.work": where members open /chest
|
|
222
|
+
chest.tool.publicUrl; // "https://status.acme.com": the public part (its custom domain), or null
|
|
179
223
|
chest.today(); // "2026-09-30": the date now in the Chest's zone (or chest.today(at))
|
|
180
224
|
```
|
|
181
225
|
|
|
182
226
|
The Chest gives these to every tool in its environment at each start
|
|
183
|
-
(`CHEST_ORGANIZATION`, `CHEST_TIME_ZONE`, `CHEST_LANGUAGE`
|
|
184
|
-
|
|
185
|
-
|
|
227
|
+
(`CHEST_ORGANIZATION`, `CHEST_TIME_ZONE`, `CHEST_LANGUAGE`, `CHEST_CURRENCY`,
|
|
228
|
+
`CHEST_TEAM_URL`, `CHEST_PUBLIC_URL`), and starts every tool that is awake
|
|
229
|
+
again when one changes — the owner changes the first four in Settings →
|
|
230
|
+
General; a custom domain served, or no longer, changes the public address —;
|
|
231
|
+
a tool asleep reads them when it wakes. The tool never asks its own admin for
|
|
232
|
+
the company's name, zone or currency, nor guesses its own address from a
|
|
233
|
+
request. They are there outside a
|
|
186
234
|
request too: a scheduled job, a start-up task, an export. No capability is
|
|
187
235
|
needed: nothing here is more than what the Chest's pages show its members.
|
|
188
236
|
|
|
@@ -195,6 +243,16 @@ needed: nothing here is more than what the Chest's pages show its members.
|
|
|
195
243
|
- `language` is the language of what the tool writes for no one in
|
|
196
244
|
particular: a public page before the visitor chooses, an export's default.
|
|
197
245
|
A page of `/chest` speaks `member(request).language` instead.
|
|
246
|
+
- `currency` is the ISO 4217 code of the Chest's currency (`"EUR"` until
|
|
247
|
+
the owner sets one): the amounts of a quote, a price, an expense. Format
|
|
248
|
+
them with `Intl.NumberFormat(language, { style: "currency", currency:
|
|
249
|
+
chest.currency })`.
|
|
250
|
+
- `tool.teamUrl` and `tool.publicUrl` are origins, without a path: build a
|
|
251
|
+
link where no request tells the host — an email sent from a scheduled job,
|
|
252
|
+
a calendar feed — with `new URL("/chest/tasks/42", chest.tool.teamUrl)`.
|
|
253
|
+
`publicUrl` is the company's own domain once the owner connected one, else
|
|
254
|
+
the tool's public host; `null` for a tool without a public part. Store
|
|
255
|
+
paths in your data, never these origins: they change.
|
|
198
256
|
- `today(at?)` is `YYYY-MM-DD` in the Chest's zone, for now or for an instant
|
|
199
257
|
(`Date` or milliseconds): compare it with dates your database keeps as
|
|
200
258
|
`date`, never with `new Date().toISOString().slice(0, 10)`, which is UTC's.
|
|
@@ -203,11 +261,12 @@ Each value is read from the environment at each access, and checked: outside a
|
|
|
203
261
|
Chest (a development server without the variables), or for a value the Chest
|
|
204
262
|
never gives, reading it throws a `ChestError` with the code `not_in_chest` —
|
|
205
263
|
a wrong zone read silently is exactly what this module exists to prevent. In
|
|
206
|
-
tests, `fakeChest({chest: {organization, timeZone, language
|
|
264
|
+
tests, `fakeChest({chest: {organization, timeZone, language, currency,
|
|
265
|
+
teamUrl, publicUrl}})` sets them.
|
|
207
266
|
|
|
208
267
|
## `members` — who has the tool
|
|
209
268
|
|
|
210
|
-
A
|
|
269
|
+
A tool that declares `"capabilities": ["members"]` (approved like a
|
|
211
270
|
permission: “Sees the name, photo, role and groups of the members who have
|
|
212
271
|
access to it.”) reads the members who have it, through the Chest's API
|
|
213
272
|
(`CHEST_API`, as for files). `"members.email"`, a permission of its own that
|
|
@@ -224,8 +283,9 @@ const teams = await members.groups.list(); // [
|
|
|
224
283
|
|
|
225
284
|
- **Who**: exactly the members who have the tool now — by a grant, a group,
|
|
226
285
|
open to all, or because they run it (owner, admins, its builders);
|
|
227
|
-
recomputed at every call.
|
|
228
|
-
|
|
286
|
+
recomputed at every call. `list` and `get` see only them (`get` → `null`
|
|
287
|
+
for anyone else); `lookup` also names those the tool had who no longer
|
|
288
|
+
have it (below).
|
|
229
289
|
- **`list({after, limit, q, role, group})`**: ordered by name (accents aside)
|
|
230
290
|
then identifier; `limit` 100 by default, 500 at most; `next` is an opaque
|
|
231
291
|
cursor for `after`, `null` after the last page. `q` finds the start of a
|
|
@@ -233,10 +293,14 @@ const teams = await members.groups.list(); // [
|
|
|
233
293
|
—, whatever its case and accents; `role` and `group` keep the members of that
|
|
234
294
|
role or group.
|
|
235
295
|
- **`lookup(ids)`**: each identifier once, in the order given: `members`,
|
|
236
|
-
`former`
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
296
|
+
`former` — those the tool had who no longer have it: `{id, name, status:
|
|
297
|
+
"no_access"}`, a member of the Chest who lost access to the tool (“Léa
|
|
298
|
+
Dubois (no access)”: the laptops she holds, the goals that need a new
|
|
299
|
+
owner); `{id, name, status: "former"}`, someone who left the Chest, so a
|
|
300
|
+
record still reads “Camille Martin (former member)”; `{id, name: null,
|
|
301
|
+
status: "erased"}` once the owner had their data erased, rendered “Former
|
|
302
|
+
member” — and `unknown`: an identifier the tool never had (the Chest names
|
|
303
|
+
nobody the tool never had, not even a member of the Chest). The SDK asks 200 at a time and
|
|
240
304
|
keeps each answer a minute in the process (5,000 at most); `forget()`
|
|
241
305
|
empties it, and so does every event of the members' lifecycle
|
|
242
306
|
(`events.handle`).
|
|
@@ -269,7 +333,7 @@ To search tasks by assignee name: `members.list({ q })` first, then
|
|
|
269
333
|
|
|
270
334
|
## `notifications` — badges and inbox items
|
|
271
335
|
|
|
272
|
-
A
|
|
336
|
+
A tool that declares `"capabilities": ["notifications"]` (approved like a
|
|
273
337
|
permission: “Shows counters and sends notifications, inside the Chest, to the
|
|
274
338
|
members who have access to it.”) tells its members what needs their
|
|
275
339
|
attention, inside the Chest only — no email, no push to a phone. Two
|
|
@@ -349,13 +413,13 @@ away by itself once handled.
|
|
|
349
413
|
|
|
350
414
|
## `events` — the members' lifecycle
|
|
351
415
|
|
|
352
|
-
A
|
|
416
|
+
A tool that holds `members` and declares `"receives": ["member.*"]` in its
|
|
353
417
|
`chest.json` (approved like a permission: “Is told when the members who have
|
|
354
418
|
access to it change or leave.”) is told, on its own `POST /chest-events`:
|
|
355
419
|
|
|
356
420
|
| Event | `data` | When |
|
|
357
421
|
|---|---|---|
|
|
358
|
-
| `member.updated` | `{id, changed: ("name" \| "photo" \| "role" \| "groups" \| "email")[]}` | Something the tool sees of a member who has it changed (`email` only with `members.email`) |
|
|
422
|
+
| `member.updated` | `{id, changed: ("name" \| "photo" \| "role" \| "groups" \| "email" \| "language" \| "timeZone")[]}` | Something the tool sees of a member who has it changed (`email` only with `members.email`; `language` and `timeZone`: the language the Chest speaks to them and the zone they work in — a digest's words and hour) |
|
|
359
423
|
| `access.revoked` | `{id}` | The member lost access to the tool but stays in the Chest |
|
|
360
424
|
| `member.removed` | `{id}` | The member left the Chest: `lookup` now reads them `former` |
|
|
361
425
|
| `member.erased` | `{id, erasure, deadline}` | The owner asked for this person's data to be erased: delete or anonymise what the tool keeps of them before `deadline` (30 days), then `acknowledgeErasure(erasure)` |
|
|
@@ -420,7 +484,7 @@ export async function POST(request: Request) {
|
|
|
420
484
|
|
|
421
485
|
## `ai` — AI models through the Chest
|
|
422
486
|
|
|
423
|
-
A
|
|
487
|
+
A tool that declares the `ai` capability calls AI models through its
|
|
424
488
|
Chest. The Chest's owner connects OpenRouter with the company's own key;
|
|
425
489
|
the tool calls models by four **aliases**: `default`, `fast`, `smart`,
|
|
426
490
|
`embedding`, each led by the Chest to a model it chose (the owner may choose
|
|
@@ -529,7 +593,7 @@ before it.
|
|
|
529
593
|
|
|
530
594
|
## `databaseUrl()` — database of a server tool
|
|
531
595
|
|
|
532
|
-
A
|
|
596
|
+
A tool that declares `"capabilities": ["database"]` in its `chest.json`
|
|
533
597
|
gets a PostgreSQL database of its own (the capability is shown and approved
|
|
534
598
|
like a permission, in the approval screen). The container has no network: its
|
|
535
599
|
launcher listens on `127.0.0.1` and relays each connection to the Chest. The
|
|
@@ -565,7 +629,7 @@ previous version working — going back to the previous version undoes nothing.
|
|
|
565
629
|
|
|
566
630
|
## `files` — files of a server tool
|
|
567
631
|
|
|
568
|
-
A
|
|
632
|
+
A tool that declares `"capabilities": ["files"]` (approved like a permission)
|
|
569
633
|
keeps private files **through its Chest**, never on its disk (the container's
|
|
570
634
|
root is read-only). The launcher gives the tool
|
|
571
635
|
`CHEST_API=http://127.0.0.1:<port>` — its own port, relayed to the Chest; the
|
|
@@ -576,7 +640,7 @@ its own files only.
|
|
|
576
640
|
import * as files from "@argentic/chest-sdk/files";
|
|
577
641
|
await files.put("photos/cat.png", bytes, "image/png"); // Uint8Array or text
|
|
578
642
|
const file = await files.get("photos/cat.png"); // {data, type, size} or null
|
|
579
|
-
const info = await files.stat("photos/cat.png"); // {name, type, size, updated, width?, height?} or null
|
|
643
|
+
const info = await files.stat("photos/cat.png"); // {name, type, size, sha256, updated, width?, height?} or null
|
|
580
644
|
const { files: page, next } = await files.list({ prefix: "photos/" }); // 1000 per page
|
|
581
645
|
await files.move("photos/cat.png", "archive/cat.png"); // atomic; replaces archive/cat.png
|
|
582
646
|
await files.delete("archive/cat.png"); // true, or false if it did not exist
|
|
@@ -657,6 +721,18 @@ otherwise); thumbnails are made once, not counted in the quota. `stat` gives
|
|
|
657
721
|
`width` and `height` for these images. Give a link to a member's browser,
|
|
658
722
|
never to a public page.
|
|
659
723
|
|
|
724
|
+
Every file the Chest answers (`put`, `stat`, `list`, `move`) carries
|
|
725
|
+
`sha256`, the digest of its content in hex, as the Chest took it: compare
|
|
726
|
+
it, or detect a duplicate receipt, without reading the file again.
|
|
727
|
+
|
|
728
|
+
The SDK takes a link or an upload address from the Chest only in `https`
|
|
729
|
+
on the team host. The one exception is a fake Chest of the same process
|
|
730
|
+
(`@argentic/chest-sdk/testing`): while it runs, the links and uploads it
|
|
731
|
+
signs on its own origin, `http://127.0.0.1:<port>`, are taken too. Only the
|
|
732
|
+
testing module opens that exception, for that origin alone, until
|
|
733
|
+
`close()`; nothing in the environment does, so production code that never
|
|
734
|
+
imports the testing module never takes a local link.
|
|
735
|
+
|
|
660
736
|
Errors: `CapabilityNotGranted` (a version without the capability, or no
|
|
661
737
|
`CHEST_API`), `TooLarge` (413), `QuotaExceeded` (429), `Unavailable` (the
|
|
662
738
|
Chest not reached, or an answer that is not its own: a write may or may not
|
|
@@ -664,6 +740,79 @@ have happened), `ChestError` for the rest (`invalid_type`, `no_thumbnail`,
|
|
|
664
740
|
`not_found` for `url` and `move`…). Removing the tool removes its files; a new
|
|
665
741
|
version keeps them.
|
|
666
742
|
|
|
743
|
+
## `schedules` — work the tool does by itself
|
|
744
|
+
|
|
745
|
+
Nothing runs in a tool's container between requests — the Chest puts a tool
|
|
746
|
+
nobody uses to sleep —: a morning digest, reminders, a purge or a badge kept
|
|
747
|
+
true overnight come from the Chest, which calls the tool at set times. The
|
|
748
|
+
tool declares each schedule in its `chest.json`, a name and a cron line read
|
|
749
|
+
on the wall clock of the Chest's time zone (`chest.timeZone`), approved in
|
|
750
|
+
words (“Runs by itself: morning, weekdays at 7:30 AM”):
|
|
751
|
+
|
|
752
|
+
```jsonc
|
|
753
|
+
// chest.json
|
|
754
|
+
{ "schedules": [{ "name": "morning", "cron": "30 7 * * 1-5" }, { "name": "retry-mail", "cron": "*/15 * * * *" }] }
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
```ts
|
|
758
|
+
// app/chest-schedules/route.ts — at the root, outside /chest: the Chest calls
|
|
759
|
+
// it through the tool's launcher, never from a browser (its front answers 404 there).
|
|
760
|
+
import * as schedules from "@argentic/chest-sdk/schedules";
|
|
761
|
+
import { chest } from "@argentic/chest-sdk/chest";
|
|
762
|
+
|
|
763
|
+
export async function POST(request: Request) {
|
|
764
|
+
return new Response(null, { status: await schedules.handle(request, {
|
|
765
|
+
morning: async () => { await sendDigest(chest.today()); },
|
|
766
|
+
"retry-mail": () => retryOutbox(),
|
|
767
|
+
}, { seen }) });
|
|
768
|
+
}
|
|
769
|
+
```
|
|
770
|
+
|
|
771
|
+
- **The line**: five fields — minute, hour, day of the month, month, day of
|
|
772
|
+
the week —, each numbers, `*`, ranges (`1-5`), lists (`1,15`) and steps
|
|
773
|
+
(`*/15`); Sunday is 0 or 7; no names nor `@daily`, one space between
|
|
774
|
+
fields. When both days are restricted, either one runs (as cron). A time
|
|
775
|
+
a change of clock skips runs once, shifted; a repeated one runs once.
|
|
776
|
+
- **Bounds** (the Chest's, checked when the manifest is read): 8 schedules,
|
|
777
|
+
names of 1 to 32 lowercase letters, digits and hyphens, each running 15
|
|
778
|
+
minutes apart at least; 5 minutes a run.
|
|
779
|
+
- **Approval**: running by itself is a permission, one sentence per
|
|
780
|
+
schedule. A later version that changes, adds or removes schedules of a
|
|
781
|
+
tool that already had one asks nothing more.
|
|
782
|
+
- **Delivery**: `POST /chest-schedules`, the tool woken first when it
|
|
783
|
+
sleeps, body `{id: "run_…", name, scheduledAt, attempt}` signed for this
|
|
784
|
+
tool (`Chest-Schedule` header, HS256 under a key derived from
|
|
785
|
+
`CHEST_TOKEN` with the label `Chest-Schedule v1` — the scheme of events,
|
|
786
|
+
under a key of its own —, naming the run and the SHA-256 of the body, 60
|
|
787
|
+
seconds). `scheduledAt` is the time the run stands for (UTC); a run asked
|
|
788
|
+
now stands for the time it was asked.
|
|
789
|
+
- **Answer once the work is done**, within 5 minutes: a 2xx is done; a 404
|
|
790
|
+
(a schedule without a handler) is given up at once; anything else, or no
|
|
791
|
+
answer, is delivered again, the same run with the same id, after 1, 5 and
|
|
792
|
+
15 minutes (`attempt` 2 to 4), unless the next time of its schedule comes
|
|
793
|
+
first. Runs of one schedule never overlap: a time that comes while the
|
|
794
|
+
previous run still runs is skipped. A server that was stopped runs a
|
|
795
|
+
missed time once when it starts again — the latest, never a backlog.
|
|
796
|
+
Longer work: do a batch per run and keep your place in the database.
|
|
797
|
+
- **`handle(request, handlers, {seen?})`** answers the status to give the
|
|
798
|
+
Chest: 401 for what is not a run of the Chest for this tool, 404 for a
|
|
799
|
+
schedule without a handler, 204 for a run handled or one already in
|
|
800
|
+
`seen`. It reads the body (1 KiB at most): mount it before any body
|
|
801
|
+
parser. A handler that throws leaves the run unseen and `handle` throws:
|
|
802
|
+
answer 500, it comes again. `seen` is as for `events` (`events.memorySeen`
|
|
803
|
+
by default; a table of the tool's for runs that must never be done twice —
|
|
804
|
+
the same table serves both, the ids never meet). Make handlers idempotent
|
|
805
|
+
anyway.
|
|
806
|
+
- **`verify(request)`** is the run of a delivery, or `null`; for a tool that
|
|
807
|
+
routes runs itself.
|
|
808
|
+
- **The Chest's times, the members' zones**: a line is the company's clock.
|
|
809
|
+
To reach each member at *their* 8:00, run hourly (`0 * * * *`) and pick
|
|
810
|
+
the members whose local hour it is (`members.list`, `member.timeZone`).
|
|
811
|
+
- **Whoever runs the tool** sees each schedule on its overview — when it
|
|
812
|
+
runs next, its last runs and why one failed — and may **Run now**; an
|
|
813
|
+
agent reads `GET /api/v1/tools/<tool>/schedules` and runs one with
|
|
814
|
+
`POST /api/v1/tools/<tool>/schedules/run {name}` (a token that writes).
|
|
815
|
+
|
|
667
816
|
## `testing` — a tool's own tests
|
|
668
817
|
|
|
669
818
|
`@argentic/chest-sdk/testing` is for tests, never imported by production code.
|
|
@@ -681,6 +830,7 @@ assert.ok(chest.files.has("reports/2026.pdf"));
|
|
|
681
830
|
assert.deepEqual(chest.notifications, [{ member: camille.id, title: "New task", path: "/chest/tasks/42", key: "task:42" }]);
|
|
682
831
|
assert.equal(chest.badges.get(camille.id), 1);
|
|
683
832
|
assert.equal(chest.ai[0]?.path, "/ai/chat");
|
|
833
|
+
assert.equal(await chest.run("morning", request => handler(request)), 204);
|
|
684
834
|
await chest.close();
|
|
685
835
|
```
|
|
686
836
|
|
|
@@ -688,9 +838,11 @@ await chest.close();
|
|
|
688
838
|
|---|---|
|
|
689
839
|
| `signAssertion(member, {token?, tool?, now?})` | A `Chest-Member` header value signed like the Chest's for that `Member` (the token and tool of the environment by default), signed as given, so a language or a zone the Chest never sends makes `member()` refuse it |
|
|
690
840
|
| `withMember(request, member, options?)` | The request carrying that assertion (the options of `signAssertion`): a new Web `Request`, or the same Node request |
|
|
691
|
-
| `fakeChest({members?, former?, groups?, capabilities?, receives?, files?, ai?, chest?})` | An HTTP server on `127.0.0.1` that sets `CHEST_API`, `CHEST_TOKEN`, `CHEST_TOOL` (`tool` unless set), the Chest's `CHEST_ORGANIZATION`, `CHEST_TIME_ZONE`, `CHEST_LANGUAGE` (`chest: {organization, timeZone, language}`: `"Test organization"`, `"UTC"`, `"en"` by default) and answers members, groups, files, badges, notifications, AI and erasure acknowledgments with a Chest's bounds, quotas and errors; a capability left out answers 403 (`members`, `files`, `notifications` and `ai` by default; `members.email` adds the addresses; `receives` is `["member.*"]` by default, `[]` refuses acknowledgments).
|
|
841
|
+
| `fakeChest({members?, former?, groups?, capabilities?, receives?, files?, ai?, chest?})` | An HTTP server on `127.0.0.1` that sets `CHEST_API`, `CHEST_TOKEN`, `CHEST_TOOL` (`tool` unless set), the Chest's `CHEST_ORGANIZATION`, `CHEST_TIME_ZONE`, `CHEST_LANGUAGE`, `CHEST_CURRENCY`, `CHEST_TEAM_URL`, `CHEST_PUBLIC_URL` (`chest: {organization, timeZone, language, currency, teamUrl, publicUrl}`: `"Test organization"`, `"UTC"`, `"en"`, `"EUR"`, `https://<tool>-chest.chest.test`, `https://<tool>.chest.test` by default; `publicUrl: null` for a tool without a public part) and answers members, groups, files, badges, notifications, AI and erasure acknowledgments with a Chest's bounds, quotas and errors; a capability left out answers 403 (`members`, `files`, `notifications` and `ai` by default; `members.email` adds the addresses; `receives` is `["member.*"]` by default, `[]` refuses acknowledgments). `former: [{id, name?, status?}]` are those the tool had who no longer have it: `lookup` answers them `no_access`, `former` (by default) or `erased` |
|
|
842
|
+
| Links and uploads | The fake serves the team host's part of the files on its own origin (`chest.api`): a link from `files.url` opens the content it was signed for (the image itself for a thumbnail — a fake does not reduce it; `no_thumbnail` for a file that is not a JPEG, PNG, GIF or WebP image), until it expires or the file changes; an address from `files.uploadUrl` takes one `PUT`, within its life, of the types and size it names and whose first bytes are those of its type (403 `invalid_token`, 415 `type_refused`, 400 `type_mismatch`, 413 `too_large`, as the Chest's), named by the Chest in a folder (20 hex characters and the type's ending), and answers `201 {name, type, size}`. It checks no session: a test's `fetch` is the member's browser |
|
|
692
843
|
| `chest.emit(event, to)` | Delivers an event (`{type, data, id?, occurredAt?}`: a new id and now by default; name an id to deliver the same event twice) signed as the Chest signs it, to `to` — the tool's address (`POST <to>/chest-events`) or a function of a Web `Request` — and says the status it answered. A `member.erased` makes its erasure one the tool may acknowledge |
|
|
693
844
|
| `ai: {models?, reply?, cap?, unavailable?}` | The fake Chest's AI, deterministic and without any provider. `models`: the aliases the tool declared, `{alias, model, provider?, input?, output?}` (all four by default, `fake-default`…`fake-embedding`, provider `openrouter`, 1 and 2 USD per million tokens); another alias answers `model_not_allowed`. `reply(request)`: what a chat answers, given the wire request — a string, or `{text?, toolCalls?: {name, arguments, id?}[]}` (by default the last user message, echoed); streamed, it comes word by word, each tool call's arguments in two pieces, then the finish reason and the usage. Embeddings are unit vectors from a hash of each text (8 dimensions unless `dimensions`). Tokens count one per 4 characters; once the spending reaches `cap` (euros, 5 by default; 0 refuses at once) a call answers `cap_reached`. `unavailable` (`no_connector`, `provider_key_invalid`, `provider_unavailable`) makes chat and embeddings answer it. 60 requests a minute |
|
|
845
|
+
| `chest.run(name, to, {id?, scheduledAt?, attempt?})` | Delivers a run of the schedule `name` (a new id, now and attempt 1 by default; name an id to deliver the same run twice) signed as the Chest signs it, to `to` — the tool's address (`POST <to>/chest-schedules`) or a function of a Web `Request` — and says the status it answered |
|
|
694
846
|
| `chest.ai` | The tool's calls to AI, `{path, body}` in order (`body` null for a `GET`) |
|
|
695
847
|
| `chest.acknowledged` | The erasures the tool acknowledged, each once |
|
|
696
848
|
| `chest.members`, `chest.groups`, `chest.files` | What the fake Chest holds, to change or assert on; its `members` are those who have the tool |
|
|
@@ -700,7 +852,9 @@ await chest.close();
|
|
|
700
852
|
## Version
|
|
701
853
|
|
|
702
854
|
The package version is `version` in `package.json` (semver), published by a
|
|
703
|
-
tag `vX.Y.Z` (see `PUBLISHING.md`).
|
|
855
|
+
tag `vX.Y.Z` (see `PUBLISHING.md`). Its MAJOR.MINOR is the version of the
|
|
856
|
+
tool contract it is written for (`"chest"` in `chest.json`): 0.4.x for the
|
|
857
|
+
contract 0.4. A new contract version is a new MINOR of the SDK.
|
|
704
858
|
|
|
705
859
|
## The MCP server
|
|
706
860
|
|
|
@@ -718,11 +872,22 @@ that carry a manifest — never offers it.
|
|
|
718
872
|
|
|
719
873
|
```sh
|
|
720
874
|
npm ci
|
|
721
|
-
npm test # build dist/, compile the tests into build/,
|
|
722
|
-
|
|
723
|
-
#
|
|
875
|
+
npm test # build dist/, compile the tests into build/, check that
|
|
876
|
+
# contract/README.md says what contract.json says, run them,
|
|
877
|
+
# then check/'s (the chest command)
|
|
878
|
+
npm run check:package # npm pack both packages, the SDK under 200 KiB, install into a
|
|
879
|
+
# temp project, run chest check, import every subpath from Node
|
|
880
|
+
# and through esbuild, type-check a TS consumer
|
|
724
881
|
```
|
|
725
882
|
|
|
883
|
+
`contract/contract.json`, `check/check.wasm.gz` and
|
|
884
|
+
`check/check.wasm.sha256` are written by the Chest's repository
|
|
885
|
+
(`scripts/build-contract.mjs`) from the code that decides; never edit them
|
|
886
|
+
here. `npm run contract` renders the parts of `contract/README.md` they say;
|
|
887
|
+
the words around them are written here. `check/` is the workspace of
|
|
888
|
+
`@argentic/chest-check`, released with the SDK under the same version
|
|
889
|
+
(`PUBLISHING.md`); `check/src/cli.ts` is the `chest` command.
|
|
890
|
+
|
|
726
891
|
`client/src` holds the modules, `client/index.ts` the package root,
|
|
727
892
|
`client/test` the tests. `npm run build` compiles `client/index.ts`, the
|
|
728
893
|
nine published modules (`errors`, `member`, `members`, `database`, `files`,
|
package/client/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// The package root: every published module a tool's code uses (tool contract
|
|
2
|
-
//
|
|
3
|
-
// /database, /files, /members, /notifications, /events, /ai, /errors), which
|
|
4
|
-
// pulls in nothing else. The files, members, notifications, events and ai
|
|
2
|
+
// 0.4). Each one is also its own subpath (@argentic/chest-sdk/member, /chest,
|
|
3
|
+
// /database, /files, /members, /notifications, /events, /schedules, /ai, /errors), which
|
|
4
|
+
// pulls in nothing else. The files, members, notifications, events, schedules and ai
|
|
5
5
|
// APIs are namespaces here, as their names (get, list, stat, move, notify,
|
|
6
6
|
// verify, chat…) are too plain to stand alone. @argentic/chest-sdk/testing is for a tool's tests only, and
|
|
7
7
|
// is not here.
|
|
@@ -13,4 +13,5 @@ export * as files from "./src/files.js";
|
|
|
13
13
|
export * as members from "./src/members.js";
|
|
14
14
|
export * as notifications from "./src/notifications.js";
|
|
15
15
|
export * as events from "./src/events.js";
|
|
16
|
+
export * as schedules from "./src/schedules.js";
|
|
16
17
|
export * as ai from "./src/ai.js";
|
package/client/src/api.ts
CHANGED
|
@@ -9,6 +9,24 @@ import { CapabilityNotGranted, ChestError, QuotaExceeded, RateLimited, TooLarge,
|
|
|
9
9
|
const maxAnswer = 4 << 20;
|
|
10
10
|
const deadline = 120000;
|
|
11
11
|
|
|
12
|
+
// fakeOrigins are the origins of the fake Chests a test started in this
|
|
13
|
+
// process (testing.ts, fakeChest), each http://127.0.0.1:<port>: links and
|
|
14
|
+
// uploads a fake gives on them are taken as the Chest's https ones are. Only
|
|
15
|
+
// the testing module adds to it — the one module no production code
|
|
16
|
+
// imports —, and only for as long as its fake runs; an origin is never read
|
|
17
|
+
// from the environment, so nothing set around a tool widens what it accepts.
|
|
18
|
+
export const fakeOrigins = new Set<string>();
|
|
19
|
+
|
|
20
|
+
// chestLink reads a link to the team host the Chest answered, at path (its
|
|
21
|
+
// links, its uploads): the token it carries, or undefined for an address
|
|
22
|
+
// that is not one — https, or the origin of a fake Chest of this process.
|
|
23
|
+
export function chestLink(url: unknown, path: string): string | undefined {
|
|
24
|
+
if (typeof url !== "string") return undefined;
|
|
25
|
+
const found = /^(https:\/\/[A-Za-z0-9.-]{1,253}(?::[0-9]{1,5})?|http:\/\/127\.0\.0\.1:[0-9]{1,5})(\/_chest\/[a-z/]+\/)([A-Za-z0-9_-]+\.[A-Za-z0-9_-]+)$/u.exec(url);
|
|
26
|
+
if (!found || found[2] !== path || (found[1]!.startsWith("http:") && !fakeOrigins.has(found[1]!))) return undefined;
|
|
27
|
+
return found[3];
|
|
28
|
+
}
|
|
29
|
+
|
|
12
30
|
// base is the Chest's API as the launcher gives it; without, the version holds
|
|
13
31
|
// none of the capabilities that use it.
|
|
14
32
|
function base(capability: string): string {
|
package/client/src/chest.ts
CHANGED
|
@@ -2,11 +2,13 @@ import { ChestError } from "./errors.js";
|
|
|
2
2
|
import { languagePattern, timeZonePattern } from "./member.js";
|
|
3
3
|
|
|
4
4
|
// The Chest the tool runs in, the same for every member and every request:
|
|
5
|
-
// the organization it is of, its time zone
|
|
6
|
-
// them in the tool's
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
5
|
+
// the organization it is of, its time zone, its language and its currency —
|
|
6
|
+
// and where this tool is reached. The Chest sets them in the tool's
|
|
7
|
+
// environment at each start (CHEST_ORGANIZATION, CHEST_TIME_ZONE,
|
|
8
|
+
// CHEST_LANGUAGE, CHEST_CURRENCY, CHEST_TEAM_URL, CHEST_PUBLIC_URL) and
|
|
9
|
+
// starts the tool again, when it is awake, as soon as one changes, so they
|
|
10
|
+
// are there outside any request too: in a scheduled job, at start-up, in a
|
|
11
|
+
// migration script. The Chest also sets its time zone
|
|
10
12
|
// as the zone of the tool's database sessions: there, current_date and
|
|
11
13
|
// now()::date are the Chest's day too.
|
|
12
14
|
//
|
|
@@ -18,6 +20,14 @@ import { languagePattern, timeZonePattern } from "./member.js";
|
|
|
18
20
|
// - language is the Chest's own language, a primary tag ("en", "fr"): the
|
|
19
21
|
// language of what the tool writes for no one in particular (a public page
|
|
20
22
|
// before the visitor chooses, an export). A member's is member.language.
|
|
23
|
+
// - currency is the ISO 4217 code of the Chest's currency ("EUR" until the
|
|
24
|
+
// owner sets one): the amounts the tool writes — a quote, a price.
|
|
25
|
+
// - tool.teamUrl is the origin of the tool's team host, where its members
|
|
26
|
+
// open /chest; tool.publicUrl the origin of its public part — the
|
|
27
|
+
// company's own domain when the owner connected one —, null for a tool
|
|
28
|
+
// without a public part. Origins, without a path: a link in an email is
|
|
29
|
+
// new URL("/chest/tasks/42", chest.tool.teamUrl). Store paths, never
|
|
30
|
+
// these origins: they change with a custom domain.
|
|
21
31
|
// - today() is the date ("YYYY-MM-DD") in the Chest's zone, now or at the
|
|
22
32
|
// instant given.
|
|
23
33
|
//
|
|
@@ -28,13 +38,19 @@ export type Chest = {
|
|
|
28
38
|
readonly organization: { readonly name: string };
|
|
29
39
|
readonly timeZone: string;
|
|
30
40
|
readonly language: string;
|
|
41
|
+
readonly currency: string;
|
|
42
|
+
readonly tool: { readonly teamUrl: string; readonly publicUrl: string | null };
|
|
31
43
|
today(at?: Date | number): string;
|
|
32
44
|
};
|
|
33
45
|
|
|
34
46
|
// The shapes the Chest gives: the organization's (2 to 80 characters,
|
|
35
47
|
// counted as code points, without control characters), a zone's
|
|
36
|
-
// (timeZonePattern, and one this runtime knows), a language's
|
|
48
|
+
// (timeZonePattern, and one this runtime knows), a language's, a currency's,
|
|
49
|
+
// an origin's.
|
|
37
50
|
const organizationPattern = /^[^\u0000-\u001f\u007f-\u009f]{2,80}$/u;
|
|
51
|
+
// An ISO 4217 code; an https origin without a path, as the Chest gives them.
|
|
52
|
+
const currencyPattern = /^[A-Z]{3}$/u;
|
|
53
|
+
const originPattern = /^https:\/\/[a-z0-9]([a-z0-9.-]{0,251}[a-z0-9])?(:[0-9]{1,5})?$/u;
|
|
38
54
|
|
|
39
55
|
function read(name: string, valid: (value: string) => boolean): string {
|
|
40
56
|
const value = process.env[name];
|
|
@@ -72,6 +88,14 @@ export const chest: Chest = {
|
|
|
72
88
|
get language() {
|
|
73
89
|
return read("CHEST_LANGUAGE", value => languagePattern.test(value));
|
|
74
90
|
},
|
|
91
|
+
get currency() {
|
|
92
|
+
return read("CHEST_CURRENCY", value => currencyPattern.test(value));
|
|
93
|
+
},
|
|
94
|
+
get tool() {
|
|
95
|
+
const teamUrl = read("CHEST_TEAM_URL", value => originPattern.test(value));
|
|
96
|
+
const publicUrl = process.env["CHEST_PUBLIC_URL"] === undefined ? null : read("CHEST_PUBLIC_URL", value => originPattern.test(value));
|
|
97
|
+
return { teamUrl, publicUrl };
|
|
98
|
+
},
|
|
75
99
|
today(at: Date | number = Date.now()): string {
|
|
76
100
|
const instant = typeof at === "number" ? new Date(at) : at;
|
|
77
101
|
if (Number.isNaN(instant.getTime())) throw new RangeError("today() needs a valid date");
|