@toshicloud/sdk 0.6.0 → 0.7.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.
Files changed (44) hide show
  1. package/README.md +39 -510
  2. package/dist/client.d.ts +6 -4
  3. package/dist/client.js +4 -2
  4. package/dist/gen/toshi/permission_pb.d.ts +12 -0
  5. package/dist/gen/toshi/permission_pb.js +14 -2
  6. package/dist/gen/toshi/workspace/v1/connections_pb.d.ts +213 -19
  7. package/dist/gen/toshi/workspace/v1/connections_pb.js +97 -25
  8. package/dist/gen/toshi/workspace/v1/domains_pb.d.ts +26 -26
  9. package/dist/gen/toshi/workspace/v1/domains_pb.js +4 -4
  10. package/dist/gen/toshi/workspace/v1/{channels_pb.d.ts → endpoints_pb.d.ts} +945 -822
  11. package/dist/gen/toshi/workspace/v1/endpoints_pb.js +488 -0
  12. package/dist/gen/toshi/workspace/v1/grants_pb.d.ts +10 -10
  13. package/dist/gen/toshi/workspace/v1/grants_pb.js +3 -3
  14. package/dist/gen/toshi/workspace/v1/hostjobs_pb.d.ts +46 -22
  15. package/dist/gen/toshi/workspace/v1/hostjobs_pb.js +1 -1
  16. package/dist/gen/toshi/workspace/v1/hosts_pb.d.ts +110 -36
  17. package/dist/gen/toshi/workspace/v1/hosts_pb.js +1 -1
  18. package/dist/gen/toshi/workspace/v1/model_credentials_pb.d.ts +654 -0
  19. package/dist/gen/toshi/workspace/v1/model_credentials_pb.js +110 -0
  20. package/dist/gen/toshi/workspace/v1/recordings_pb.d.ts +21 -2
  21. package/dist/gen/toshi/workspace/v1/recordings_pb.js +1 -1
  22. package/dist/gen/toshi/workspace/v1/roles_pb.d.ts +1 -1
  23. package/dist/gen/toshi/workspace/v1/roles_pb.js +1 -1
  24. package/dist/gen/toshi/workspace/v1/secrets_pb.d.ts +19 -2
  25. package/dist/gen/toshi/workspace/v1/secrets_pb.js +1 -1
  26. package/dist/gen/toshi/workspace/v1/triggers_pb.d.ts +28 -28
  27. package/dist/gen/toshi/workspace/v1/triggers_pb.js +6 -6
  28. package/dist/gen/toshi/workspace/v1/webhook_pb.d.ts +202 -202
  29. package/dist/gen/toshi/workspace/v1/webhook_pb.js +46 -46
  30. package/dist/gen/toshi/workspace/v1/workloads_pb.d.ts +552 -29
  31. package/dist/gen/toshi/workspace/v1/workloads_pb.js +85 -55
  32. package/dist/gen/toshi/workspace/v1/workspace_pb.d.ts +639 -119
  33. package/dist/gen/toshi/workspace/v1/workspace_pb.js +174 -88
  34. package/dist/gen/toshi/workspace/v1/workspace_service_pb.d.ts +101 -4
  35. package/dist/gen/toshi/workspace/v1/workspace_service_pb.js +20 -15
  36. package/dist/index.d.ts +2 -1
  37. package/dist/index.js +2 -1
  38. package/dist/retry.js +33 -22
  39. package/dist/test/alarms.js +1 -1
  40. package/dist/test/procedures.js +35 -25
  41. package/dist/version.d.ts +1 -1
  42. package/dist/version.js +1 -1
  43. package/package.json +1 -1
  44. package/dist/gen/toshi/workspace/v1/channels_pb.js +0 -483
package/README.md CHANGED
@@ -1,22 +1,34 @@
1
- # toshi-ts
1
+ <p align="center">
2
+ <img src="docs/assets/toshi-logo.svg" alt="Toshi" width="120">
3
+ </p>
2
4
 
3
- A TypeScript client for the Toshi Workspace API, `toshi.workspace.v1`,
4
- published as ES modules with type declarations.
5
+ <h1 align="center">toshi-ts</h1>
5
6
 
6
- ```sh
7
- npm install @toshicloud/sdk @bufbuild/protobuf @connectrpc/connect
8
- ```
7
+ <p align="center">The TypeScript SDK for the Toshi API.</p>
8
+
9
+ <p align="center">
10
+ <img alt="licence" src="https://img.shields.io/badge/licence-Apache--2.0-blue">
11
+ <img alt="node" src="https://img.shields.io/badge/node-22%2B-339933">
12
+ <img alt="status" src="https://img.shields.io/badge/status-early-orange">
13
+ </p>
14
+
15
+ <p align="center">
16
+ <a href="docs/usage.md">Get started</a> ·
17
+ <a href="docs/usage.md">Docs</a> ·
18
+ <a href="https://github.com/toshicloud/toshi-ts/issues">Issues</a>
19
+ </p>
9
20
 
10
- The browser entry needs a transport such as `@connectrpc/connect-web`; the
11
- Node entry needs `@connectrpc/connect-node`. Both are peer dependencies, so
12
- install the ones your application uses.
21
+ A TypeScript client for the Toshi Workspace API, `toshi.workspace.v1`, published as ES modules with type declarations, for Node and browser programs that start jobs, sandboxes and services on Toshi and follow them. It retries safe calls, resumes watches and ships an in-memory fake Toshi for tests.
13
22
 
14
- Versions up to 0.1.2 were published as `@toshicloud/toshi-ts`.
23
+ > **Early stage.** The package stays at 0.x until a Toshi release serves the API; the latest release is the one to use.
24
+ > Interfaces may change before 1.0.
25
+ > Read the [release notes](https://github.com/toshicloud/toshi-ts/releases) when upgrading.
15
26
 
16
- ## Using the client
27
+ ## Quick start
17
28
 
18
- In Node, `@toshicloud/sdk/node` builds the transport and fills the server
19
- and the token from the environment:
29
+ ```sh
30
+ npm install @toshicloud/sdk @bufbuild/protobuf @connectrpc/connect @connectrpc/connect-node
31
+ ```
20
32
 
21
33
  <!-- examples/quick-start.ts -->
22
34
 
@@ -29,505 +41,22 @@ const { projects } = await toshi.project.listProjects({});
29
41
  console.log(projects.length);
30
42
  ```
31
43
 
32
- In a browser, with `@connectrpc/connect-web`:
33
-
34
- <!-- examples/browser.ts -->
35
-
36
- ```ts
37
- import { createConnectTransport } from "@connectrpc/connect-web";
38
- import { createToshiClient } from "@toshicloud/sdk";
39
-
40
- // The token the browser app already holds.
41
- declare const token: string;
42
-
43
- export const toshi = createToshiClient({
44
- transport: (interceptors) =>
45
- createConnectTransport({ baseUrl: "https://toshi.example.com", interceptors }),
46
- token,
47
- project: "default",
48
- });
49
- ```
50
-
51
- Interceptors of your own go before the client's. The Node entry takes
52
- `interceptors`; with a transport of your own, put them first:
53
- `interceptors: [...mine, ...interceptors]`. Each unary call enters them
54
- once, with its retries inside, and they never see the Authorization
55
- header: the client sets it on a copy of the request below them.
56
-
57
- The browser entry reads no environment variable and no file. The Node
58
- entry reads `TOSHI_SERVER`, `TOSHI_TOKEN`, `TOSHI_TOKEN_FILE` and
59
- `TOSHI_CA_FILE`; see Node options.
60
-
61
- ### Options
62
-
63
- - `tokenSource`: a function that gives each request attempt its token, so a
64
- refreshed token is used without a new client; it wins over `token`. See
65
- Tokens that change.
66
- - `token`: sent as `Authorization: Bearer <token>` on every call.
67
- - `project`, `environment`: fill the request's `project` and
68
- `environment` fields when they are empty. A value in the request wins.
69
- `environment` is left empty on a request with `shared: true`.
70
- - `retry`: `{ maxAttempts, initialBackoffMs, maxBackoffMs }`, by default
71
- 5, 200 ms and 10 s, with full jitter. Reads, and the calls whose request
72
- takes a `request_key`, are retried; a stream and every other call is not.
73
- `retry: false` turns off retries and watch reconnects.
74
- - `defaultTimeoutMs`: bounds each unary call except the Wait calls,
75
- which their request's timeout bounds; streams ignore it.
76
-
77
- A call may set `timeoutMs` instead: it bounds the whole call, retries
78
- included, and on a watch it bounds the open. It composes with `signal`.
79
- `ToshiOptions` is the object the client takes, and `CallOptions` the second
80
- argument of each call, which carries `signal` and `timeoutMs`.
81
-
82
- ### Node options
83
-
84
- `@toshicloud/sdk/node` builds a connect-node transport over HTTP/2 and
85
- fills what is missing from the environment:
86
-
87
- - `server`, else `TOSHI_SERVER`: the control plane's URL. Without either,
88
- the client refuses to start.
89
- - `tokenSource`, `token`, `tokenFile`, `TOSHI_TOKEN`, `TOSHI_TOKEN_FILE`, in
90
- that order. A token file is trimmed, and an empty file is an error.
91
- - `caFile`, else `TOSHI_CA_FILE`: a PEM bundle trusted besides Node's
92
- built-in roots.
93
- - `userAgent`: put in front of the User-Agent, for example "myapp/1.2".
94
- - `interceptors`: run before the client's own, in the order given.
95
- - `defaultTimeoutMs`: bounds each unary call except the Wait calls,
96
- which their request's timeout bounds; streams ignore it.
97
- - `transport`: a transport of your own; then `server` and `caFile` are not
98
- required.
99
-
100
- Every request carries `User-Agent: toshi-ts/<version> (node/<node
101
- version>)`, with `userAgent` and one space in front when it is set. The
102
- root entry sends no User-Agent: browsers own it.
103
-
104
- ### Tokens that change
105
-
106
- `tokenSource` gives the client a function it calls for the token of every
107
- request attempt, so a token that is refreshed, for example from a secret
108
- manager, is used without a new client. It wins over every other token
109
- option. The client asks it once per attempt, a retry included, and may
110
- ask it concurrently, so it must be fast: keep the token somewhere and
111
- refresh it outside the call. A source that throws, or that answers an
112
- empty token, fails the call with `UNAUTHENTICATED`, with the error as the
113
- cause, and the call is not retried.
114
-
115
- <!-- examples/token-source.ts -->
116
-
117
- ```ts
118
- // A token a refresh loop replaces; the client asks for it on every call.
119
- let token = "first-token";
120
-
121
- export const toshi = createToshiClient({
122
- project: "default",
123
- tokenSource: () => token,
124
- });
125
-
126
- export function rotate(next: string): void {
127
- token = next;
128
- }
129
- ```
130
-
131
- A token file, from `tokenFile` or `TOSHI_TOKEN_FILE`, is read again
132
- before a request when its modification time or size has changed, so a
133
- rotated token, such as a Kubernetes projected service account token, is
134
- picked up without a new client.
135
-
136
- ### The client's shape
137
-
138
- The client has one field per Workspace API service: `project`, `secret`,
139
- `webhook`, `display`, `channel`, `recording`, `volume`, `artifact`,
140
- `artifactUpload`, `workspace`, `hostJob`, `host`, `token`, `auditEvent`,
141
- `filler`, `grant`, `storageProvider`, `product`, `discovery`, `connection`,
142
- `role`, `trigger`, `account`, `image` and `domain`, each holding that service's
143
- generated client.
144
- `workload` holds `WorkloadService`'s calls, grouped by the type they
145
- name: `workload.jobs`, `workload.sandboxes` and `workload.services`, one
146
- method for each call (`create`, `get`, `list`, `update`, `delete`, `wait`,
147
- `watch`, and `cancel` on a job, `execute`, `start` and `stop` on a sandbox,
148
- `start`, `stop`, `setMode`, `listRuns`, `wake`, `hold`, `release`,
149
- `listRevisions`, `getRevision`, `rollback` and `listActivations` on a
150
- service). A service's alarms are `setAlarm`, `getAlarm`, `listAlarms`
151
- (every alarm in one answer, with no page token), `deleteAlarm`,
152
- `pauseAlarm`, `resumeAlarm` and `runAlarmNow`, and its sign-ins are
153
- `startSignIn`, which hands back the sign-in's stream as the server sends
154
- it, `answerSignIn`, `cancelSignIn` and `getSignInStatus`. The calls that
155
- span the three types sit on `workload` itself: `listEvents`, `watchEvents`,
156
- `emitEvent`, `holdWorkload`, `releaseWorkload`, `getOperation` and
157
- `waitOperation`. `holdWorkload` and `releaseWorkload` are operator-only and
158
- need the operator's headers, which the caller adds with an interceptor
159
- (see Operator calls). `run` sits at the top of the client, as
160
- `toshi run` sits at the top of the CLI.
161
-
162
- <!-- examples/client-shape.ts -->
163
-
164
- ```ts
165
- const { jobs } = await toshi.workload.jobs.list({ pageSize: 10 });
166
- const { job } = await toshi.workload.jobs.get({ id: jobs[0].id });
167
- console.log(job?.id);
168
- ```
169
-
170
- ### Operator calls
171
-
172
- An operator's token sends its reason in `Toshi-Operator-Reason` and the
173
- workspace it acts on in `Toshi-Workspace` on every Workspace API call; the
174
- server refuses one without them. Both belong to the client rather than to a
175
- call, so the caller sets them with an interceptor of its own: the `transport`
176
- option receives the client's interceptors, and the caller adds the one that
177
- writes the headers. `workload.holdWorkload` places an operator's hold on a
178
- sandbox or service, stopping it if it runs, and `workload.releaseWorkload`
179
- lifts the hold and starts nothing; only an operator may make either call.
180
-
181
- <!-- examples/operator-reason.ts -->
182
-
183
- ```ts
184
- import type { Interceptor } from "@connectrpc/connect";
185
- import { createConnectTransport } from "@connectrpc/connect-node";
186
-
187
- import { createToshiClient } from "@toshicloud/sdk/node";
188
-
189
- const baseUrl = "https://toshi.example.com";
190
-
191
- // An operator's token, the reason its calls carry and the workspace they
192
- // act on.
193
- declare const token: string;
194
-
195
- const operator: Interceptor = (next) => (req) => {
196
- req.header.set("Toshi-Operator-Reason", "investigating a report");
197
- req.header.set("Toshi-Workspace", "acme");
198
- return next(req);
199
- };
200
-
201
- export const toshi = createToshiClient({
202
- transport: (interceptors) =>
203
- createConnectTransport({
204
- baseUrl,
205
- httpVersion: "2",
206
- interceptors: [...interceptors, operator],
207
- }),
208
- token,
209
- project: "default",
210
- });
211
- ```
212
-
213
- ### Lists
214
-
215
- `workload.jobs.all`, `workload.sandboxes.all`,
216
- `workload.sandboxes.allPreviewShareLinks`, `workload.services.all`,
217
- `workload.services.allRuns` and `workload.allEvents` take the request their
218
- `list` method takes and yield every item, one page after another. Each
219
- response's `nextPageToken` becomes the next request's `pageToken`, and a
220
- `pageToken` in the request resumes from that page. A service with no
221
- `all` method is one `paginate` call away: it takes the list call and a
222
- function that picks a response's items.
223
-
224
- <!-- examples/lists.ts -->
225
-
226
- ```ts
227
- import { paginate } from "@toshicloud/sdk";
228
-
229
- import { toshi } from "./client.js";
230
-
231
- // Every job that matches the request, one page after another.
232
- for await (const job of toshi.workload.jobs.all({ pageSize: 10 })) {
233
- console.log(job.id);
234
- }
235
-
236
- // A service with no all() method: paginate takes its list call and items.
237
- for await (const project of paginate(
238
- {},
239
- (req, opts) => toshi.project.listProjects(req, opts),
240
- (resp) => resp.projects,
241
- )) {
242
- console.log(project.name);
243
- }
244
- ```
245
-
246
- `paginate` sends a copy of the request, so the caller's request keeps its
247
- fields. Every page is a call of its own, so `opts` goes to each one, and
248
- `timeoutMs` bounds a page and not the whole iteration. An error throws once
249
- and ends the iteration; leaving the loop sends no further request.
250
- `PageRequest` is a list request, which may name the page to read first with
251
- `pageToken`, and `PageResponse` a list response, which names the page after
252
- it with `nextPageToken`.
253
-
254
- ### Retries
255
-
256
- The client retries a read, and a write that carries a request key, when
257
- the connection fails: a `ConnectError` with code `Unavailable`, a
258
- connection reset or destroyed stream that `@connectrpc/connect-node`
259
- reports, or a stream the server refused before reading it
260
- (`REFUSED_STREAM`). A write that takes a request key gets a random one when the
261
- request has none, so the server applies a retried write once. Other
262
- writes are never retried.
263
-
264
- A `ResourceExhausted` refusal that names how long to wait, as the API's
265
- rate limit does, is retried the same way. The wait comes from its
266
- `google.rpc.RetryInfo` detail, else from its `Retry-After` header. So is an
267
- HTTP 429 with a `Retry-After`, which reaches the client as `Unavailable`.
268
- The client waits the longer of that wait and the backoff, never more than
269
- `maxBackoffMs`, and stops when the call's signal aborts. A
270
- `ResourceExhausted` that names no wait, such as a plan's limit, reaches the
271
- caller unchanged, as do every other error the server sends, including
272
- `Aborted` and `InvalidArgument`, and any connection failure the client
273
- cannot tell apart from a server error.
274
-
275
- `RetryPolicy` says how often and how long a call is retried, `maxAttempts`
276
- counting the first try; `defaultRetryPolicy` is the default — five
277
- attempts, a 200 ms initial backoff and a 10 s cap.
278
-
279
- ### Waiting and watching
280
-
281
- - `workload.jobs.wait(req)`, `workload.sandboxes.wait(req)` and
282
- `workload.services.wait(req)` ask again until the workload reaches the
283
- state; `workload.waitOperation(req)` asks until the operation is done. A
284
- call starts at least a second, and at most 1.2 seconds, after the start of
285
- the one before, and a signal ends a pause with its own error.
286
- - `workload.getOperation(req)` returns one operation by id: a call that
287
- takes long hands one back, and this asks for it again.
288
- - An event's payload says what happened: a trigger's `TriggerResult` names
289
- the key's `reason` and its workload, and with `process_exited` or
290
- `setup_failed` it carries the sandbox's `exitCode` and `message`.
291
- - `workload.listEvents(req)` returns the events after a cursor, oldest
292
- first, a page at a time, for a client that polls or catches up after
293
- downtime; `workload.watchEvents(req)` is the live form.
294
- - `workload.jobs.watch(req)`, `workload.sandboxes.watch(req)`,
295
- `workload.services.watch(req)`, `workload.watchEvents(req)` and
296
- `workload.jobs.watchOutput(req)` resolve to a watch you read with
297
- `for await`. A watch opens its stream when you start reading and
298
- reconnects after a connection failure from where it stopped: `cursor`
299
- for the typed watches and events, `offsets` (bytes, as `bigint`) for
300
- output. With no cursor, a typed watch lists that type first, and
301
- `snapshot` holds them once its promise resolves; `watchEvents`
302
- yields the past events page by page before the live ones. `close()` or
303
- leaving the loop ends the stream. The output watch ends at the output's
304
- end.
305
- - Every watch yields the stream's responses: a job watch yields
306
- `WatchJobsResponse` (`job`, `cursor`, `type`), an event watch yields
307
- `WatchEventsResponse` (`event`), and the output watch yields
308
- `WatchOutputResponse` (`stdout`, `stderr` or `end`).
309
-
310
- <!-- examples/watch-events.ts -->
311
-
312
- ```ts
313
- const id = "workload-id";
314
- const watch = await toshi.workload.watchEvents({ workloadId: id });
315
- for await (const { event } of watch) {
316
- console.log(event?.name);
317
- }
318
- // watch.cursor resumes a later watch after the last event read.
319
- ```
320
-
321
- Every helper takes `{ signal }`, an `AbortSignal` that ends it.
322
-
323
- ### Running a command
324
-
325
- `workload.sandboxes.execute(start)` runs a command in a running sandbox
326
- and resolves once the server has started it. The process is one stream of output chunks, `{ stream: "stdout" |
327
- "stderr", data: Uint8Array }`. `write` sends standard input in frames of
328
- at most 64 KiB and resolves once the stream has taken the last one; await
329
- it before the next `write`, which is refused while one is pending, so the
330
- client holds at most one buffer of input. `closeStdin` ends standard input
331
- after a pending write, and a later `write` is refused. `wait` settles when
332
- the command ends.
333
-
334
- <!-- examples/execute.ts -->
335
-
336
- ```ts
337
- const id = "sandbox-id";
338
- const proc = await toshi.workload.sandboxes.execute({
339
- sandboxId: id,
340
- command: { argv: ["cat"] },
341
- });
342
- await proc.write("hello\n");
343
- await proc.closeStdin();
344
- for await (const chunk of proc) {
345
- process[chunk.stream].write(chunk.data);
346
- }
347
- await proc.wait(); // rejects with ExitError on a non-zero exit
348
- ```
349
-
350
- Without a loop, `wait()` reads and drops the output itself. It rejects
351
- with `ExitError` when the command fails, is cancelled or ends without an
352
- exit status, and with `StreamLostError` when the stream ends first. The
353
- client never runs a command again by itself. Standard input cannot be
354
- replayed, so the caller decides whether to start again with the same
355
- `requestKey`. Leaving the loop early cancels the stream, and `wait()`
356
- then rejects with a `Canceled` error.
357
-
358
- Execute needs a bidirectional stream: `@connectrpc/connect-node` with
359
- `httpVersion: "2"`. In a browser, `execute` rejects at once and makes no
360
- call. Over a transport without HTTP/2, the call fails with the
361
- transport's or the server's error.
362
-
363
- ### Running a job
364
-
365
- `run(req)` creates a job with a `CreateJobRequest` and resolves to the
366
- job and its output, which is what `toshi run` does: `CreateJob`, then
367
- `WatchOutput`. It yields output chunks like an execution, and `wait()`
368
- resolves when the job exits 0 and rejects with `JobExitError` otherwise.
369
-
370
- <!-- examples/run-job.ts -->
371
-
372
- ```ts
373
- const run = await toshi.run({
374
- spec: { command: { argv: ["./test.sh"] } },
375
- });
376
- for await (const chunk of run) {
377
- process[chunk.stream].write(chunk.data);
378
- }
379
- await run.wait(); // rejects with JobExitError on a non-zero exit
380
- ```
381
-
382
- Stopping the loop stops only the output stream: the job keeps running,
383
- and `workload.jobs.cancel` ends it.
384
-
385
- ## Products that are off
386
-
387
- A call for a product that is off for the workspace rejects with a
388
- `ProductError`, `wait`, the watches and `execute` included.
389
- `isProductError(err)` tells it apart. It is still a `ConnectError` with the
390
- same `code`, and it has the product's ID (`product`), the `reason`
391
- (`ProductReason.Unavailable`), the input that turned the product off
392
- (`productCause`: `build`, `operator`, `plan` or `workspace`) and, for a
393
- product the deployment turns off, how far it has wound down (`state`:
394
- `no_new_work`, `draining` or `stopped`).
395
-
396
- <!-- examples/product-error.ts -->
397
-
398
- ```ts
399
- import { isProductError } from "@toshicloud/sdk";
400
-
401
- import { toshi } from "./client.js";
402
-
403
- // Turn a product on for the workspace.
404
- const enable = { id: "acme_ci", enabled: true };
405
- try {
406
- await toshi.product.updateProduct(enable);
407
- } catch (err) {
408
- if (!isProductError(err)) {
409
- throw err;
410
- }
411
- // Off for the workspace: which product, the input that turns it off, and,
412
- // for the deployment's config, how far it has wound down.
413
- console.log(err.product, err.productCause, err.state);
414
- }
415
- ```
416
-
417
- ## Refusals with a reason
418
-
419
- A refusal that is not a product's rejects with a `RefusalError`;
420
- `isRefusalError(err)` tells it apart. It is still a `ConnectError` with the
421
- same `code`, and its `reason` is one of `RefusalReason`:
422
- `RefusalReason.ImageInUse` when an image a workload or a revision still uses
423
- cannot be deleted, `RefusalReason.ImageNotReady` when a job, a sandbox or a
424
- service revision names a workspace image with no ready version,
425
- `RefusalReason.PlanExcludesImages` when the plan has no images, `RefusalReason.LastIdentity` when the caller's last identity cannot
426
- be unlinked, and so on for images, volumes, roles and display sessions.
427
-
428
- `workload.services.rollback` asks the revision's registry whether it still
429
- serves the revision's image before it selects it, and refuses with
430
- `RefusalReason.ImageNotServed` when the registry answered that it does not
431
- serve the image, or `RefusalReason.ImageCheckFailed` when it could not say.
432
- A revision's `imageCheck` carries the last answer: its `outcome`, the
433
- registry's `status` and `message`, and when it was asked, `checkedAt`.
434
- Reading a revision asks no registry.
435
-
436
- ## Testing with `@toshicloud/sdk/test`
437
-
438
- `createFake()` serves the whole Workspace API in memory: no server, no port,
439
- nothing to close, and no Node module, so it runs in the same runtimes as the
440
- client. `fake.client()` returns a `ToshiClient` wired to it, with the token
441
- `test-token` and the scope `default`/`production`, which is enough to drive
442
- `run`, the watchers and the generated clients from a test.
443
-
444
- <!-- examples/testing.ts -->
445
-
446
- ```ts
447
- import { createFake } from "@toshicloud/sdk/test";
448
-
449
- const fake = createFake();
450
- fake.job("job-1").stdout("deployed\n").exitCode(0);
451
-
452
- const toshi = fake.client();
453
- const run = await toshi.run({ spec: { command: { argv: ["./deploy.sh"] } } });
454
- for await (const chunk of run) {
455
- process[chunk.stream].write(chunk.data);
456
- }
457
- await run.wait();
458
- ```
459
-
460
- The fake also keeps a service's alarms: `setAlarm`, `getAlarm`, `listAlarms`,
461
- `deleteAlarm`, `pauseAlarm`, `resumeAlarm` and `runAlarmNow` answer from its
462
- state machine.
463
-
464
- `fake.on(method, impl)` replaces the state machine for one method with a hook
465
- typed by that method: one call for unary, server-stream and bidirectional
466
- methods alike. `fake.calls()` returns every call the fake received, oldest
467
- first, with the procedure, the request and the headers.
468
-
469
- ## Generated code
470
-
471
- `src/gen/toshi/workspace/v1/` is generated by
472
- [`protoc-gen-es`](https://github.com/bufbuild/protobuf-es) from the protos
473
- in `proto/toshi/workspace/v1` of the
474
- [toshi-api](https://github.com/toshicloud/toshi-api) repository, one
475
- `*_pb.ts` file per proto, and `src/gen/toshi/permission_pb.ts` from
476
- `proto/toshi/permission.proto`, which they import. The generated code is committed and imports
477
- only `@bufbuild/protobuf`, so installing the package needs no generator.
478
-
479
- `pin.env` names the toshi-api commit the code is generated from:
480
- `5474559e89f7faf4b718fd5f1d64ff7457657477` today, the commit of the
481
- release tag `v0.5.0`.
482
- `mise.toml` pins the versions of Node, pnpm and `buf`;
483
- `package.json` pins `protoc-gen-es` at the same version as the
484
- `@bufbuild/protobuf` development dependency.
485
-
486
- To regenerate:
487
-
488
- ```sh
489
- mise install
490
- pnpm install --frozen-lockfile
491
- mise exec -- scripts/generate.sh
492
- ```
493
-
494
- Moving to another commit or tag is one pull request that changes
495
- `pin.env` and `src/gen/` together. CI regenerates the code from `pin.env`
496
- and fails if the result differs from what is committed
497
- (`scripts/check-generated.sh`).
44
+ The Node entry reads `TOSHI_SERVER` and `TOSHI_TOKEN` from the environment; the [usage guide](docs/usage.md) continues from there, including the browser entry.
498
45
 
499
- ## Versions
46
+ ## Documentation
500
47
 
501
- The package's major version follows the API's: `1.x` will serve
502
- `toshi.workspace.v1`, and a `toshi.workspace.v2` gets a new major
503
- version. Until a Toshi release serves the API, the package stays at
504
- `0.x` and is generated from a pinned commit. `VERSION` is the package
505
- version, the one `package.json` carries.
506
-
507
- ## Publishing
508
-
509
- Releases are published to npm by the publish workflow
510
- (`.github/workflows/publish.yml`) through npm trusted publishing: the
511
- workflow authenticates with its GitHub OIDC token, and no npm token
512
- exists. It runs on a `vX.Y.Z` tag that matches the version in
513
- `package.json`. A new version is a commit on `main` that sets the version
514
- in `package.json`, followed by its tag.
515
-
516
- ## Integration test
517
-
518
- `test/integration/` runs the client against a real `toshi serve` over
519
- HTTPS with a test CA it mints itself. `TOSHI_INTEGRATION_BIN` names the
520
- `toshi` binary it runs; when `TOSHI_INTEGRATION_EXPECT_COMMIT` is set, the
521
- binary must report that commit:
522
-
523
- ```sh
524
- TOSHI_INTEGRATION_BIN=/path/to/toshi mise exec -- pnpm vitest run test/integration
525
- ```
48
+ - [Usage](docs/usage.md): the client, options, retries, lists, watches, commands and jobs
49
+ - [Errors](docs/errors.md): products that are off and refusals with a reason
50
+ - [Testing with `@toshicloud/sdk/test`](docs/testing.md): the in-memory fake
51
+ - [Development](docs/development.md): generated code, versions, publishing and the integration test
52
+ - [The package on npm](https://www.npmjs.com/package/@toshicloud/sdk)
53
+ - [The Toshi API](https://github.com/toshicloud/toshi-api) the client calls
54
+ - [Releases](https://github.com/toshicloud/toshi-ts/releases)
55
+ - [Security](SECURITY.md) · [Licence](LICENSE)
526
56
 
527
- The Toshi server's CI runs it against each server change. Without
528
- `TOSHI_INTEGRATION_BIN`, `pnpm test` skips the integration test and says
529
- why.
57
+ ## Status and support
530
58
 
531
- ## Licence
59
+ Issues and questions go to [GitHub issues](https://github.com/toshicloud/toshi-ts/issues).
60
+ Security reports follow [SECURITY.md](SECURITY.md).
532
61
 
533
- Apache License 2.0, see LICENSE.
62
+ Apache-2.0
package/dist/client.d.ts CHANGED
@@ -3,7 +3,7 @@ import type { Client, Interceptor, Transport } from "@connectrpc/connect";
3
3
  import { AccountService } from "./gen/toshi/workspace/v1/account_pb.js";
4
4
  import { ArtifactService, ArtifactUploadService } from "./gen/toshi/workspace/v1/artifacts_pb.js";
5
5
  import { AuditEventService } from "./gen/toshi/workspace/v1/audit_events_pb.js";
6
- import { ChannelService } from "./gen/toshi/workspace/v1/channels_pb.js";
6
+ import { EndpointService } from "./gen/toshi/workspace/v1/endpoints_pb.js";
7
7
  import { ConnectionService } from "./gen/toshi/workspace/v1/connections_pb.js";
8
8
  import { DiscoveryService } from "./gen/toshi/workspace/v1/discovery_pb.js";
9
9
  import { DisplayService } from "./gen/toshi/workspace/v1/display_pb.js";
@@ -13,6 +13,7 @@ import { GrantService } from "./gen/toshi/workspace/v1/grants_pb.js";
13
13
  import { ImageService } from "./gen/toshi/workspace/v1/images_pb.js";
14
14
  import { HostJobService } from "./gen/toshi/workspace/v1/hostjobs_pb.js";
15
15
  import { HostService } from "./gen/toshi/workspace/v1/hosts_pb.js";
16
+ import { ModelCredentialService } from "./gen/toshi/workspace/v1/model_credentials_pb.js";
16
17
  import { ProductService } from "./gen/toshi/workspace/v1/products_pb.js";
17
18
  import { ProjectService } from "./gen/toshi/workspace/v1/projects_pb.js";
18
19
  import { RecordingService } from "./gen/toshi/workspace/v1/recordings_pb.js";
@@ -62,7 +63,7 @@ export interface ToshiClient {
62
63
  readonly secret: Client<typeof SecretService>;
63
64
  readonly webhook: Client<typeof WebhookService>;
64
65
  readonly display: Client<typeof DisplayService>;
65
- readonly channel: Client<typeof ChannelService>;
66
+ readonly endpoint: Client<typeof EndpointService>;
66
67
  readonly recording: Client<typeof RecordingService>;
67
68
  readonly volume: Client<typeof VolumeService>;
68
69
  readonly artifact: Client<typeof ArtifactService>;
@@ -84,6 +85,7 @@ export interface ToshiClient {
84
85
  readonly account: Client<typeof AccountService>;
85
86
  readonly image: Client<typeof ImageService>;
86
87
  readonly domain: Client<typeof DomainService>;
88
+ readonly modelCredential: Client<typeof ModelCredentialService>;
87
89
  run(req: MessageInitShape<typeof CreateJobRequestSchema>, opts?: CallOptions): Promise<JobRun>;
88
90
  }
89
91
  /** WorkloadService's calls, grouped by the type they name. */
@@ -180,7 +182,7 @@ export interface ServicesClient {
180
182
  listActivations(req: MessageInitShape<typeof ListServiceActivationsRequestSchema>, opts?: CallOptions): Promise<ListServiceActivationsResponse>;
181
183
  /**
182
184
  * Creates or replaces the service's alarm with this name and returns it: at each instant of its
183
- * schedule the alarm publishes its event, with its data, on one of the service's event channels.
185
+ * schedule the alarm publishes its event, with its data, on one of the service's event endpoints.
184
186
  */
185
187
  setAlarm(req: MessageInitShape<typeof SetServiceAlarmRequestSchema>, opts?: CallOptions): Promise<SetServiceAlarmResponse>;
186
188
  /** Returns one alarm and its most recent occurrences, newest first. */
@@ -190,7 +192,7 @@ export interface ServicesClient {
190
192
  * there is no `allAlarms`.
191
193
  */
192
194
  listAlarms(req: MessageInitShape<typeof ListServiceAlarmsRequestSchema>, opts?: CallOptions): Promise<ListServiceAlarmsResponse>;
193
- /** Removes an alarm and its occurrences; messages it already published stay on the channel. */
195
+ /** Removes an alarm and its occurrences; messages it already published stay on the endpoint. */
194
196
  deleteAlarm(req: MessageInitShape<typeof DeleteServiceAlarmRequestSchema>, opts?: CallOptions): Promise<DeleteServiceAlarmResponse>;
195
197
  /** Stops an alarm publishing until it is resumed; pausing a paused alarm changes nothing. */
196
198
  pauseAlarm(req: MessageInitShape<typeof PauseServiceAlarmRequestSchema>, opts?: CallOptions): Promise<PauseServiceAlarmResponse>;
package/dist/client.js CHANGED
@@ -4,7 +4,7 @@ import { executeCommand } from "./execute.js";
4
4
  import { AccountService } from "./gen/toshi/workspace/v1/account_pb.js";
5
5
  import { ArtifactService, ArtifactUploadService } from "./gen/toshi/workspace/v1/artifacts_pb.js";
6
6
  import { AuditEventService } from "./gen/toshi/workspace/v1/audit_events_pb.js";
7
- import { ChannelService } from "./gen/toshi/workspace/v1/channels_pb.js";
7
+ import { EndpointService } from "./gen/toshi/workspace/v1/endpoints_pb.js";
8
8
  import { ConnectionService } from "./gen/toshi/workspace/v1/connections_pb.js";
9
9
  import { DiscoveryService } from "./gen/toshi/workspace/v1/discovery_pb.js";
10
10
  import { DisplayService } from "./gen/toshi/workspace/v1/display_pb.js";
@@ -14,6 +14,7 @@ import { GrantService } from "./gen/toshi/workspace/v1/grants_pb.js";
14
14
  import { ImageService } from "./gen/toshi/workspace/v1/images_pb.js";
15
15
  import { HostJobService } from "./gen/toshi/workspace/v1/hostjobs_pb.js";
16
16
  import { HostService } from "./gen/toshi/workspace/v1/hosts_pb.js";
17
+ import { ModelCredentialService } from "./gen/toshi/workspace/v1/model_credentials_pb.js";
17
18
  import { ProductService } from "./gen/toshi/workspace/v1/products_pb.js";
18
19
  import { ProjectService } from "./gen/toshi/workspace/v1/projects_pb.js";
19
20
  import { RecordingService } from "./gen/toshi/workspace/v1/recordings_pb.js";
@@ -62,7 +63,7 @@ export function createToshiClient(options) {
62
63
  secret: createClient(SecretService, transport),
63
64
  webhook: createClient(WebhookService, transport),
64
65
  display: createClient(DisplayService, transport),
65
- channel: createClient(ChannelService, transport),
66
+ endpoint: createClient(EndpointService, transport),
66
67
  recording: createClient(RecordingService, transport),
67
68
  volume: createClient(VolumeService, transport),
68
69
  artifact: createClient(ArtifactService, transport),
@@ -156,5 +157,6 @@ export function createToshiClient(options) {
156
157
  account: createClient(AccountService, transport),
157
158
  image: createClient(ImageService, transport),
158
159
  domain: createClient(DomainService, transport),
160
+ modelCredential: createClient(ModelCredentialService, transport),
159
161
  };
160
162
  }
@@ -232,6 +232,18 @@ export declare const by_field: GenExtension<MethodOptions, boolean>;
232
232
  * @generated from extension: toshi.MCP mcp = 51005;
233
233
  */
234
234
  export declare const mcp: GenExtension<MethodOptions, MCP>;
235
+ /**
236
+ * own marks a call a member may make for what is their own with
237
+ * membership alone. A caller who holds a role in the workspace but not the
238
+ * method's permission is admitted for their own resources only: the
239
+ * handler refuses any other resource, as the permission check would, with
240
+ * PERMISSION_MISSING, and a list holds only the caller's own. A caller
241
+ * with the permission is admitted as usual. A call that acts for no
242
+ * person needs the permission.
243
+ *
244
+ * @generated from extension: bool own = 51006;
245
+ */
246
+ export declare const own: GenExtension<MethodOptions, boolean>;
235
247
  /**
236
248
  * field_permission names what a caller needs to change the field through
237
249
  * the mask of an update call marked by_field: a field group of its own,