@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.
- package/README.md +39 -510
- package/dist/client.d.ts +6 -4
- package/dist/client.js +4 -2
- package/dist/gen/toshi/permission_pb.d.ts +12 -0
- package/dist/gen/toshi/permission_pb.js +14 -2
- package/dist/gen/toshi/workspace/v1/connections_pb.d.ts +213 -19
- package/dist/gen/toshi/workspace/v1/connections_pb.js +97 -25
- package/dist/gen/toshi/workspace/v1/domains_pb.d.ts +26 -26
- package/dist/gen/toshi/workspace/v1/domains_pb.js +4 -4
- package/dist/gen/toshi/workspace/v1/{channels_pb.d.ts → endpoints_pb.d.ts} +945 -822
- package/dist/gen/toshi/workspace/v1/endpoints_pb.js +488 -0
- package/dist/gen/toshi/workspace/v1/grants_pb.d.ts +10 -10
- package/dist/gen/toshi/workspace/v1/grants_pb.js +3 -3
- package/dist/gen/toshi/workspace/v1/hostjobs_pb.d.ts +46 -22
- package/dist/gen/toshi/workspace/v1/hostjobs_pb.js +1 -1
- package/dist/gen/toshi/workspace/v1/hosts_pb.d.ts +110 -36
- package/dist/gen/toshi/workspace/v1/hosts_pb.js +1 -1
- package/dist/gen/toshi/workspace/v1/model_credentials_pb.d.ts +654 -0
- package/dist/gen/toshi/workspace/v1/model_credentials_pb.js +110 -0
- package/dist/gen/toshi/workspace/v1/recordings_pb.d.ts +21 -2
- package/dist/gen/toshi/workspace/v1/recordings_pb.js +1 -1
- package/dist/gen/toshi/workspace/v1/roles_pb.d.ts +1 -1
- package/dist/gen/toshi/workspace/v1/roles_pb.js +1 -1
- package/dist/gen/toshi/workspace/v1/secrets_pb.d.ts +19 -2
- package/dist/gen/toshi/workspace/v1/secrets_pb.js +1 -1
- package/dist/gen/toshi/workspace/v1/triggers_pb.d.ts +28 -28
- package/dist/gen/toshi/workspace/v1/triggers_pb.js +6 -6
- package/dist/gen/toshi/workspace/v1/webhook_pb.d.ts +202 -202
- package/dist/gen/toshi/workspace/v1/webhook_pb.js +46 -46
- package/dist/gen/toshi/workspace/v1/workloads_pb.d.ts +552 -29
- package/dist/gen/toshi/workspace/v1/workloads_pb.js +85 -55
- package/dist/gen/toshi/workspace/v1/workspace_pb.d.ts +639 -119
- package/dist/gen/toshi/workspace/v1/workspace_pb.js +174 -88
- package/dist/gen/toshi/workspace/v1/workspace_service_pb.d.ts +101 -4
- package/dist/gen/toshi/workspace/v1/workspace_service_pb.js +20 -15
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/retry.js +33 -22
- package/dist/test/alarms.js +1 -1
- package/dist/test/procedures.js +35 -25
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/dist/gen/toshi/workspace/v1/channels_pb.js +0 -483
package/README.md
CHANGED
|
@@ -1,22 +1,34 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="docs/assets/toshi-logo.svg" alt="Toshi" width="120">
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
published as ES modules with type declarations.
|
|
5
|
+
<h1 align="center">toshi-ts</h1>
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
27
|
+
## Quick start
|
|
17
28
|
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
46
|
+
## Documentation
|
|
500
47
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
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
|
-
|
|
528
|
-
`TOSHI_INTEGRATION_BIN`, `pnpm test` skips the integration test and says
|
|
529
|
-
why.
|
|
57
|
+
## Status and support
|
|
530
58
|
|
|
531
|
-
|
|
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
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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,
|