@toshicloud/sdk 0.3.2 → 0.5.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 (59) hide show
  1. package/README.md +189 -30
  2. package/dist/client.d.ts +30 -4
  3. package/dist/client.js +19 -6
  4. package/dist/errors.d.ts +47 -0
  5. package/dist/errors.js +136 -0
  6. package/dist/execute.js +15 -0
  7. package/dist/gen/toshi/permission_pb.d.ts +39 -0
  8. package/dist/gen/toshi/permission_pb.js +42 -0
  9. package/dist/gen/toshi/workspace/v1/artifacts_pb.d.ts +1 -1
  10. package/dist/gen/toshi/workspace/v1/artifacts_pb.js +3 -2
  11. package/dist/gen/toshi/workspace/v1/audit_events_pb.js +2 -1
  12. package/dist/gen/toshi/workspace/v1/channels_pb.js +2 -1
  13. package/dist/gen/toshi/workspace/v1/discovery_pb.d.ts +676 -0
  14. package/dist/gen/toshi/workspace/v1/discovery_pb.js +182 -0
  15. package/dist/gen/toshi/workspace/v1/display_pb.js +2 -1
  16. package/dist/gen/toshi/workspace/v1/fillers_pb.js +2 -1
  17. package/dist/gen/toshi/workspace/v1/hostjobs_pb.js +2 -1
  18. package/dist/gen/toshi/workspace/v1/hosts_pb.js +2 -1
  19. package/dist/gen/toshi/workspace/v1/products_pb.d.ts +703 -0
  20. package/dist/gen/toshi/workspace/v1/products_pb.js +106 -0
  21. package/dist/gen/toshi/workspace/v1/projects_pb.js +2 -1
  22. package/dist/gen/toshi/workspace/v1/recordings_pb.js +2 -1
  23. package/dist/gen/toshi/workspace/v1/secrets_pb.js +2 -1
  24. package/dist/gen/toshi/workspace/v1/storage_providers_pb.js +2 -1
  25. package/dist/gen/toshi/workspace/v1/tokens_pb.js +2 -1
  26. package/dist/gen/toshi/workspace/v1/volumes_pb.js +2 -1
  27. package/dist/gen/toshi/workspace/v1/webhook_pb.js +2 -1
  28. package/dist/gen/toshi/workspace/v1/workloads_pb.js +2 -1
  29. package/dist/gen/toshi/workspace/v1/workspace_pb.d.ts +2 -2
  30. package/dist/gen/toshi/workspace/v1/workspace_pb.js +1 -1
  31. package/dist/gen/toshi/workspace/v1/workspace_service_pb.js +2 -1
  32. package/dist/index.d.ts +6 -1
  33. package/dist/index.js +5 -1
  34. package/dist/interceptors.d.ts +17 -2
  35. package/dist/interceptors.js +69 -4
  36. package/dist/node/client.d.ts +35 -0
  37. package/dist/node/client.js +178 -0
  38. package/dist/node/index.d.ts +3 -0
  39. package/dist/node/index.js +4 -0
  40. package/dist/output.js +6 -1
  41. package/dist/paginate.d.ts +36 -0
  42. package/dist/paginate.js +37 -0
  43. package/dist/retry.js +4 -0
  44. package/dist/run.js +3 -2
  45. package/dist/stream.d.ts +8 -1
  46. package/dist/stream.js +24 -2
  47. package/dist/test/fake.d.ts +88 -0
  48. package/dist/test/fake.js +845 -0
  49. package/dist/test/index.d.ts +58 -0
  50. package/dist/test/index.js +57 -0
  51. package/dist/test/procedures.d.ts +18 -0
  52. package/dist/test/procedures.js +219 -0
  53. package/dist/timeout.d.ts +29 -0
  54. package/dist/timeout.js +67 -0
  55. package/dist/version.d.ts +2 -0
  56. package/dist/version.js +2 -0
  57. package/dist/wait.js +15 -12
  58. package/dist/watch.js +15 -2
  59. package/package.json +19 -3
package/README.md CHANGED
@@ -4,40 +4,26 @@ A TypeScript client for the Toshi Workspace API, `toshi.workspace.v1`,
4
4
  published as ES modules with type declarations.
5
5
 
6
6
  ```sh
7
- npm install @toshicloud/sdk @connectrpc/connect @connectrpc/connect-node
7
+ npm install @toshicloud/sdk @connectrpc/connect
8
8
  ```
9
9
 
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.
13
+
10
14
  Versions up to 0.1.2 were published as `@toshicloud/toshi-ts`.
11
15
 
12
16
  ## Using the client
13
17
 
14
- `createToshiClient` takes one set of options. The transport belongs to
15
- the caller: the client hands its interceptors to the `transport`
16
- function, and that function builds a transport with them. Every call then
17
- gets the same token, scope and retries: the helpers below and the
18
- generated clients alike.
19
-
20
- In Node, with `@connectrpc/connect-node`:
18
+ In Node, `@toshicloud/sdk/node` builds the transport and fills the server
19
+ and the token from the environment:
21
20
 
22
21
  <!-- examples/quick-start.ts -->
23
22
 
24
23
  ```ts
25
- import { readFileSync } from "node:fs";
26
- import { createConnectTransport } from "@connectrpc/connect-node";
27
- import { createToshiClient } from "@toshicloud/sdk";
24
+ import { createToshiClient } from "@toshicloud/sdk/node";
28
25
 
29
- const toshi = createToshiClient({
30
- transport: (interceptors) =>
31
- createConnectTransport({
32
- baseUrl: "https://toshi.example.com",
33
- httpVersion: "2",
34
- interceptors,
35
- // Only for a server whose certificate a private CA signed.
36
- nodeOptions: { ca: readFileSync("ca.pem") },
37
- }),
38
- token: process.env.TOSHI_TOKEN,
39
- project: "default",
40
- });
26
+ const toshi = createToshiClient({ project: "default" });
41
27
 
42
28
  const { projects } = await toshi.project.listProjects({});
43
29
  console.log(projects.length);
@@ -62,14 +48,21 @@ export const toshi = createToshiClient({
62
48
  });
63
49
  ```
64
50
 
65
- To add interceptors of your own, put them before the client's:
66
- `interceptors: [...mine, ...interceptors]`.
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.
67
56
 
68
- The package reads no environment variable and no file. The token, the
69
- server and any CA come from the caller.
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.
70
60
 
71
61
  ### Options
72
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.
73
66
  - `token`: sent as `Authorization: Bearer <token>` on every call.
74
67
  - `project`, `environment`: fill the request's `project` and
75
68
  `environment` fields when they are empty. A value in the request wins.
@@ -77,14 +70,73 @@ server and any CA come from the caller.
77
70
  - `retry`: `{ maxAttempts, initialBackoffMs, maxBackoffMs }`, by default
78
71
  5, 200 ms and 10 s, with full jitter. `retry: false` turns off retries
79
72
  and watch reconnects.
73
+ - `defaultTimeoutMs`: bounds each unary call except the Wait calls,
74
+ which their request's timeout bounds; streams ignore it.
75
+
76
+ A call may set `timeoutMs` instead: it bounds the whole call, retries
77
+ included, and on a watch it bounds the open. It composes with `signal`.
78
+
79
+ ### Node options
80
+
81
+ `@toshicloud/sdk/node` builds a connect-node transport over HTTP/2 and
82
+ fills what is missing from the environment:
83
+
84
+ - `server`, else `TOSHI_SERVER`: the control plane's URL. Without either,
85
+ the client refuses to start.
86
+ - `tokenSource`, `token`, `tokenFile`, `TOSHI_TOKEN`, `TOSHI_TOKEN_FILE`, in
87
+ that order. A token file is trimmed, and an empty file is an error.
88
+ - `caFile`, else `TOSHI_CA_FILE`: a PEM bundle trusted besides Node's
89
+ built-in roots.
90
+ - `userAgent`: put in front of the User-Agent, for example "myapp/1.2".
91
+ - `interceptors`: run before the client's own, in the order given.
92
+ - `defaultTimeoutMs`: bounds each unary call except the Wait calls,
93
+ which their request's timeout bounds; streams ignore it.
94
+ - `transport`: a transport of your own; then `server` and `caFile` are not
95
+ required.
96
+
97
+ Every request carries `User-Agent: toshi-ts/<version> (node/<node
98
+ version>)`, with `userAgent` and one space in front when it is set. The
99
+ root entry sends no User-Agent: browsers own it.
100
+
101
+ ### Tokens that change
102
+
103
+ `tokenSource` gives the client a function it calls for the token of every
104
+ request attempt, so a token that is refreshed, for example from a secret
105
+ manager, is used without a new client. It wins over every other token
106
+ option. The client asks it once per attempt, a retry included, and may
107
+ ask it concurrently, so it must be fast: keep the token somewhere and
108
+ refresh it outside the call. A source that throws, or that answers an
109
+ empty token, fails the call with `UNAUTHENTICATED`, with the error as the
110
+ cause, and the call is not retried.
111
+
112
+ <!-- examples/token-source.ts -->
113
+
114
+ ```ts
115
+ // A token a refresh loop replaces; the client asks for it on every call.
116
+ let token = "first-token";
117
+
118
+ export const toshi = createToshiClient({
119
+ project: "default",
120
+ tokenSource: () => token,
121
+ });
122
+
123
+ export function rotate(next: string): void {
124
+ token = next;
125
+ }
126
+ ```
127
+
128
+ A token file, from `tokenFile` or `TOSHI_TOKEN_FILE`, is read again
129
+ before a request when its modification time or size has changed, so a
130
+ rotated token, such as a Kubernetes projected service account token, is
131
+ picked up without a new client.
80
132
 
81
133
  ### The client's shape
82
134
 
83
135
  The client has one field per Workspace API service: `project`, `secret`,
84
136
  `webhook`, `display`, `channel`, `recording`, `volume`, `artifact`,
85
137
  `artifactUpload`, `workspace`, `hostJob`, `host`, `token`, `auditEvent`,
86
- `filler` and `storageProvider`, each holding that service's generated
87
- client.
138
+ `filler`, `storageProvider`, `product` and `discovery`, each holding that
139
+ service's generated client.
88
140
  `workload` holds `WorkloadService`'s calls, grouped by the type they
89
141
  name: `workload.jobs`, `workload.sandboxes` and `workload.services`, one
90
142
  method for each call (`create`, `get`, `list`, `update`, `delete`,
@@ -102,6 +154,43 @@ const { job } = await toshi.workload.jobs.get({ id: jobs[0].id });
102
154
  console.log(job?.id);
103
155
  ```
104
156
 
157
+ ### Lists
158
+
159
+ `workload.jobs.all`, `workload.sandboxes.all`, `workload.services.all`,
160
+ `workload.services.allRuns` and `workload.allEvents` take the request their
161
+ `list` method takes and yield every item, one page after another. Each
162
+ response's `nextPageToken` becomes the next request's `pageToken`, and a
163
+ `pageToken` in the request resumes from that page. A service with no
164
+ `all` method is one `paginate` call away: it takes the list call and a
165
+ function that picks a response's items.
166
+
167
+ <!-- examples/lists.ts -->
168
+
169
+ ```ts
170
+ import { paginate } from "@toshicloud/sdk";
171
+
172
+ import { toshi } from "./client.js";
173
+
174
+ // Every job that matches the request, one page after another.
175
+ for await (const job of toshi.workload.jobs.all({ pageSize: 10 })) {
176
+ console.log(job.id);
177
+ }
178
+
179
+ // A service with no all() method: paginate takes its list call and items.
180
+ for await (const project of paginate(
181
+ {},
182
+ (req, opts) => toshi.project.listProjects(req, opts),
183
+ (resp) => resp.projects,
184
+ )) {
185
+ console.log(project.name);
186
+ }
187
+ ```
188
+
189
+ `paginate` sends a copy of the request, so the caller's request keeps its
190
+ fields. Every page is a call of its own, so `opts` goes to each one, and
191
+ `timeoutMs` bounds a page and not the whole iteration. An error throws once
192
+ and ends the iteration; leaving the loop sends no further request.
193
+
105
194
  ### Retries
106
195
 
107
196
  The client retries a read, and a write that carries a request key, when
@@ -216,13 +305,83 @@ await run.wait(); // rejects with JobExitError on a non-zero exit
216
305
  Stopping the loop stops only the output stream: the job keeps running,
217
306
  and `workload.jobs.cancel` ends it.
218
307
 
308
+ ## Products that are off
309
+
310
+ A call for a product that is off for the workspace, or that turns on an
311
+ outside product without accepting the access it asks for, rejects with a
312
+ `ProductError`, `wait`, the watches and `execute` included.
313
+ `isProductError(err)` tells it apart. It is still a `ConnectError` with the
314
+ same `code`, and it has the product's ID (`product`), the `reason`
315
+ (`ProductReason.NotInstalled`, `Disabled`, `NotEntitled`, `TurnedOff` or
316
+ `AccessNotAccepted`), the setting that turns the product on (`feature`,
317
+ such as `runners.enabled`), how far a product the deployment disables has
318
+ wound down (`state`) and, for `AccessNotAccepted`, the access to accept
319
+ (`access`).
320
+
321
+ <!-- examples/product-error.ts -->
322
+
323
+ ```ts
324
+
325
+ // Turning an outside product on installs it, and the API asks the caller to
326
+ // accept the access it wants first.
327
+ const enable = { id: "acme_ci", enabled: true, acceptedAccess: [] as string[] };
328
+ try {
329
+ try {
330
+ await toshi.product.updateProduct(enable);
331
+ } catch (err) {
332
+ if (!isProductError(err) || err.reason !== ProductReason.AccessNotAccepted) {
333
+ throw err;
334
+ }
335
+ console.log(err.product, err.reason, err.access);
336
+ // Accept exactly the access the product asks for.
337
+ await toshi.product.updateProduct({ ...enable, acceptedAccess: err.access });
338
+ }
339
+ } catch (err) {
340
+ if (!isProductError(err)) {
341
+ throw err;
342
+ }
343
+ // Off for the workspace: say which product and why.
344
+ console.log(err.product, err.reason, err.feature);
345
+ }
346
+ ```
347
+
348
+ ## Testing with `@toshicloud/sdk/test`
349
+
350
+ `createFake()` serves the whole Workspace API in memory: no server, no port,
351
+ nothing to close, and no Node module, so it runs in the same runtimes as the
352
+ client. `fake.client()` returns a `ToshiClient` wired to it, with the token
353
+ `test-token` and the scope `default`/`production`, which is enough to drive
354
+ `run`, the watchers and the generated clients from a test.
355
+
356
+ <!-- examples/testing.ts -->
357
+
358
+ ```ts
359
+ import { createFake } from "@toshicloud/sdk/test";
360
+
361
+ const fake = createFake();
362
+ fake.job("job-1").stdout("deployed\n").exitCode(0);
363
+
364
+ const toshi = fake.client();
365
+ const run = await toshi.run({ spec: { command: { argv: ["./deploy.sh"] } } });
366
+ for await (const chunk of run) {
367
+ process[chunk.stream].write(chunk.data);
368
+ }
369
+ await run.wait();
370
+ ```
371
+
372
+ `fake.on(method, impl)` replaces the state machine for one method with a hook
373
+ typed by that method: one call for unary, server-stream and bidirectional
374
+ methods alike. `fake.calls()` returns every call the fake received, oldest
375
+ first, with the procedure, the request and the headers.
376
+
219
377
  ## Generated code
220
378
 
221
379
  `src/gen/toshi/workspace/v1/` is generated by
222
380
  [`protoc-gen-es`](https://github.com/bufbuild/protobuf-es) from the protos
223
381
  in `proto/toshi/workspace/v1` of the
224
382
  [toshi-api](https://github.com/toshicloud/toshi-api) repository, one
225
- `*_pb.ts` file per proto. The generated code is committed and imports
383
+ `*_pb.ts` file per proto, and `src/gen/toshi/permission_pb.ts` from
384
+ `proto/toshi/permission.proto`, which they import. The generated code is committed and imports
226
385
  only `@bufbuild/protobuf`, so installing the package needs no generator.
227
386
 
228
387
  `pin.env` names the toshi-api commit (or, once one exists, the release
package/dist/client.d.ts CHANGED
@@ -3,10 +3,12 @@ import type { Client, Interceptor, Transport } from "@connectrpc/connect";
3
3
  import { ArtifactService, ArtifactUploadService } from "./gen/toshi/workspace/v1/artifacts_pb.js";
4
4
  import { AuditEventService } from "./gen/toshi/workspace/v1/audit_events_pb.js";
5
5
  import { ChannelService } from "./gen/toshi/workspace/v1/channels_pb.js";
6
+ import { DiscoveryService } from "./gen/toshi/workspace/v1/discovery_pb.js";
6
7
  import { DisplayService } from "./gen/toshi/workspace/v1/display_pb.js";
7
8
  import { FillerService } from "./gen/toshi/workspace/v1/fillers_pb.js";
8
9
  import { HostJobService } from "./gen/toshi/workspace/v1/hostjobs_pb.js";
9
10
  import { HostService } from "./gen/toshi/workspace/v1/hosts_pb.js";
11
+ import { ProductService } from "./gen/toshi/workspace/v1/products_pb.js";
10
12
  import { ProjectService } from "./gen/toshi/workspace/v1/projects_pb.js";
11
13
  import { RecordingService } from "./gen/toshi/workspace/v1/recordings_pb.js";
12
14
  import { SecretService } from "./gen/toshi/workspace/v1/secrets_pb.js";
@@ -18,21 +20,32 @@ import { WorkspaceService } from "./gen/toshi/workspace/v1/workspace_service_pb.
18
20
  import { CancelJobRequestSchema, CreateJobRequestSchema, CreateSandboxRequestSchema, CreateServiceRequestSchema, DeleteJobRequestSchema, DeleteSandboxRequestSchema, DeleteServiceRequestSchema, GetJobRequestSchema, GetSandboxRequestSchema, GetServiceRequestSchema, ListJobsRequestSchema, ListSandboxesRequestSchema, ListServicesRequestSchema, UpdateJobRequestSchema, UpdateSandboxRequestSchema, UpdateServiceRequestSchema, WaitJobRequestSchema, WaitSandboxRequestSchema, WaitServiceRequestSchema, WatchJobsRequestSchema, WatchSandboxesRequestSchema, WatchServicesRequestSchema } from "./gen/toshi/workspace/v1/workloads_pb.js";
19
21
  import type { CancelJobResponse, CreateJobResponse, CreateSandboxResponse, CreateServiceResponse, DeleteJobResponse, DeleteSandboxResponse, DeleteServiceResponse, GetJobResponse, GetSandboxResponse, GetServiceResponse, ListJobsResponse, ListSandboxesResponse, ListServicesResponse, UpdateJobResponse, UpdateSandboxResponse, UpdateServiceResponse, WaitJobResponse, WaitSandboxResponse, WaitServiceResponse, WatchJobsResponse, WatchSandboxesResponse, WatchServicesResponse } from "./gen/toshi/workspace/v1/workloads_pb.js";
20
22
  import { ExecuteStartSchema, GetOperationRequestSchema, HoldServiceRequestSchema, ListEventsRequestSchema, ListServiceRunsRequestSchema, ReleaseServiceRequestSchema, SetServiceModeRequestSchema, StartServiceRequestSchema, StopServiceRequestSchema, WaitOperationRequestSchema, WakeServiceRequestSchema, WatchEventsRequestSchema, WatchOutputRequestSchema } from "./gen/toshi/workspace/v1/workspace_pb.js";
21
- import type { GetOperationResponse, HoldServiceResponse, Job, ListEventsResponse, ListServiceRunsResponse, Operation, ReleaseServiceResponse, Sandbox, Service, SetServiceModeResponse, StartServiceResponse, StopServiceResponse, WakeServiceResponse, WatchEventsResponse, WatchOutputResponse } from "./gen/toshi/workspace/v1/workspace_pb.js";
23
+ import type { Event, GetOperationResponse, HoldServiceResponse, Job, ListEventsResponse, ListServiceRunsResponse, Operation, ReleaseServiceResponse, Sandbox, Service, ServiceRun, SetServiceModeResponse, StartServiceResponse, StopServiceResponse, WakeServiceResponse, WatchEventsResponse, WatchOutputResponse } from "./gen/toshi/workspace/v1/workspace_pb.js";
22
24
  import type { RetryPolicy } from "./retry.js";
23
25
  /** How the client is configured; the caller builds the transport. */
24
26
  export interface ToshiOptions {
25
27
  /** Builds the caller's transport (connect-node or connect-web) with the client's interceptors. */
26
28
  transport: (interceptors: Interceptor[]) => Transport;
27
29
  token?: string;
30
+ /**
31
+ * Gives each request attempt its token, so a refreshed token is used without
32
+ * a new client; it wins over `token` and a retry asks it again. A source that
33
+ * throws or answers an empty token ends the call with Code.Unauthenticated,
34
+ * with its error as the cause, and the call is not retried.
35
+ */
36
+ tokenSource?: (signal: AbortSignal | undefined) => string | Promise<string>;
28
37
  project?: string;
29
38
  environment?: string;
30
39
  /** false turns off retries and watch reconnects. */
31
40
  retry?: Partial<RetryPolicy> | false;
41
+ /** Bounds each unary call except the Wait calls, which their request's timeout bounds; streams ignore it. */
42
+ defaultTimeoutMs?: number;
32
43
  }
33
44
  /** Per-call options. */
34
45
  export interface CallOptions {
35
46
  signal?: AbortSignal;
47
+ /** Bounds the whole call, retries included; on a watch, the open. */
48
+ timeoutMs?: number;
36
49
  }
37
50
  /** One client per Workspace API service, plus the helpers that live on top of them. */
38
51
  export interface ToshiClient {
@@ -53,6 +66,8 @@ export interface ToshiClient {
53
66
  readonly auditEvent: Client<typeof AuditEventService>;
54
67
  readonly filler: Client<typeof FillerService>;
55
68
  readonly storageProvider: Client<typeof StorageProviderService>;
69
+ readonly product: Client<typeof ProductService>;
70
+ readonly discovery: Client<typeof DiscoveryService>;
56
71
  run(req: MessageInitShape<typeof CreateJobRequestSchema>, opts?: CallOptions): Promise<JobRun>;
57
72
  }
58
73
  /** WorkloadService's calls, grouped by the type they name. */
@@ -63,6 +78,8 @@ export interface WorkloadClient {
63
78
  waitOperation(req: MessageInitShape<typeof WaitOperationRequestSchema>, opts?: CallOptions): Promise<Operation>;
64
79
  /** Lists the events after a cursor, a page at a time; `watchEvents` is the live form. */
65
80
  listEvents(req: MessageInitShape<typeof ListEventsRequestSchema>, opts?: CallOptions): Promise<ListEventsResponse>;
81
+ /** Yields every event listEvents lists, following the page token from one page to the next. */
82
+ allEvents(req: MessageInitShape<typeof ListEventsRequestSchema>, opts?: CallOptions): AsyncIterable<Event>;
66
83
  watchEvents(req: MessageInitShape<typeof WatchEventsRequestSchema>, opts?: CallOptions): Promise<EventWatch>;
67
84
  /** Returns one operation: the id a long call hands back, asked for again. */
68
85
  getOperation(req: MessageInitShape<typeof GetOperationRequestSchema>, opts?: CallOptions): Promise<GetOperationResponse>;
@@ -72,6 +89,8 @@ export interface JobsClient {
72
89
  create(req: MessageInitShape<typeof CreateJobRequestSchema>, opts?: CallOptions): Promise<CreateJobResponse>;
73
90
  get(req: MessageInitShape<typeof GetJobRequestSchema>, opts?: CallOptions): Promise<GetJobResponse>;
74
91
  list(req: MessageInitShape<typeof ListJobsRequestSchema>, opts?: CallOptions): Promise<ListJobsResponse>;
92
+ /** Yields every job that matches every filter that is set, following the page token from one page to the next. */
93
+ all(req: MessageInitShape<typeof ListJobsRequestSchema>, opts?: CallOptions): AsyncIterable<Job>;
75
94
  update(req: MessageInitShape<typeof UpdateJobRequestSchema>, opts?: CallOptions): Promise<UpdateJobResponse>;
76
95
  delete(req: MessageInitShape<typeof DeleteJobRequestSchema>, opts?: CallOptions): Promise<DeleteJobResponse>;
77
96
  cancel(req: MessageInitShape<typeof CancelJobRequestSchema>, opts?: CallOptions): Promise<CancelJobResponse>;
@@ -84,6 +103,8 @@ export interface SandboxesClient {
84
103
  create(req: MessageInitShape<typeof CreateSandboxRequestSchema>, opts?: CallOptions): Promise<CreateSandboxResponse>;
85
104
  get(req: MessageInitShape<typeof GetSandboxRequestSchema>, opts?: CallOptions): Promise<GetSandboxResponse>;
86
105
  list(req: MessageInitShape<typeof ListSandboxesRequestSchema>, opts?: CallOptions): Promise<ListSandboxesResponse>;
106
+ /** Yields every sandbox that matches every filter that is set, following the page token from one page to the next. */
107
+ all(req: MessageInitShape<typeof ListSandboxesRequestSchema>, opts?: CallOptions): AsyncIterable<Sandbox>;
87
108
  update(req: MessageInitShape<typeof UpdateSandboxRequestSchema>, opts?: CallOptions): Promise<UpdateSandboxResponse>;
88
109
  delete(req: MessageInitShape<typeof DeleteSandboxRequestSchema>, opts?: CallOptions): Promise<DeleteSandboxResponse>;
89
110
  wait(req: MessageInitShape<typeof WaitSandboxRequestSchema>, opts?: CallOptions): Promise<WaitSandboxResponse>;
@@ -95,6 +116,8 @@ export interface ServicesClient {
95
116
  create(req: MessageInitShape<typeof CreateServiceRequestSchema>, opts?: CallOptions): Promise<CreateServiceResponse>;
96
117
  get(req: MessageInitShape<typeof GetServiceRequestSchema>, opts?: CallOptions): Promise<GetServiceResponse>;
97
118
  list(req: MessageInitShape<typeof ListServicesRequestSchema>, opts?: CallOptions): Promise<ListServicesResponse>;
119
+ /** Yields every service that matches every filter that is set, following the page token from one page to the next. */
120
+ all(req: MessageInitShape<typeof ListServicesRequestSchema>, opts?: CallOptions): AsyncIterable<Service>;
98
121
  update(req: MessageInitShape<typeof UpdateServiceRequestSchema>, opts?: CallOptions): Promise<UpdateServiceResponse>;
99
122
  delete(req: MessageInitShape<typeof DeleteServiceRequestSchema>, opts?: CallOptions): Promise<DeleteServiceResponse>;
100
123
  wait(req: MessageInitShape<typeof WaitServiceRequestSchema>, opts?: CallOptions): Promise<WaitServiceResponse>;
@@ -106,6 +129,8 @@ export interface ServicesClient {
106
129
  wake(req: MessageInitShape<typeof WakeServiceRequestSchema>, opts?: CallOptions): Promise<WakeServiceResponse>;
107
130
  hold(req: MessageInitShape<typeof HoldServiceRequestSchema>, opts?: CallOptions): Promise<HoldServiceResponse>;
108
131
  listRuns(req: MessageInitShape<typeof ListServiceRunsRequestSchema>, opts?: CallOptions): Promise<ListServiceRunsResponse>;
132
+ /** Yields every run listRuns returns, the newest first, following the page token from one page to the next. */
133
+ allRuns(req: MessageInitShape<typeof ListServiceRunsRequestSchema>, opts?: CallOptions): AsyncIterable<ServiceRun>;
109
134
  }
110
135
  /** A live stream of a job's changes. */
111
136
  export interface JobWatch extends AsyncIterable<WatchJobsResponse> {
@@ -160,9 +185,10 @@ export interface JobRun extends AsyncIterable<OutputChunk> {
160
185
  wait(): Promise<void>;
161
186
  }
162
187
  /**
163
- * Creates the client: one normalized retry setting, the auth, scope, key and
164
- * retry interceptors (auth outermost, retry innermost), one transport for every
165
- * client and helper.
188
+ * Creates the client: one normalized retry setting, the product, scope, key,
189
+ * retry and auth interceptors (product outermost, so a product's refusal
190
+ * reaches the caller as a ProductError; auth innermost, so no interceptor sees
191
+ * the Authorization header), one transport for every client and helper.
166
192
  *
167
193
  * Since 0.2.0 the workload calls are grouped by the type they name under
168
194
  * `workload`; every other service, WorkspaceService's included, is handed out
package/dist/client.js CHANGED
@@ -4,10 +4,12 @@ import { executeCommand } from "./execute.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
6
  import { ChannelService } from "./gen/toshi/workspace/v1/channels_pb.js";
7
+ import { DiscoveryService } from "./gen/toshi/workspace/v1/discovery_pb.js";
7
8
  import { DisplayService } from "./gen/toshi/workspace/v1/display_pb.js";
8
9
  import { FillerService } from "./gen/toshi/workspace/v1/fillers_pb.js";
9
10
  import { HostJobService } from "./gen/toshi/workspace/v1/hostjobs_pb.js";
10
11
  import { HostService } from "./gen/toshi/workspace/v1/hosts_pb.js";
12
+ import { ProductService } from "./gen/toshi/workspace/v1/products_pb.js";
11
13
  import { ProjectService } from "./gen/toshi/workspace/v1/projects_pb.js";
12
14
  import { RecordingService } from "./gen/toshi/workspace/v1/recordings_pb.js";
13
15
  import { SecretService } from "./gen/toshi/workspace/v1/secrets_pb.js";
@@ -19,15 +21,18 @@ import { WorkspaceService } from "./gen/toshi/workspace/v1/workspace_service_pb.
19
21
  import { CancelJobRequestSchema, CreateJobRequestSchema, CreateSandboxRequestSchema, CreateServiceRequestSchema, DeleteJobRequestSchema, DeleteSandboxRequestSchema, DeleteServiceRequestSchema, GetJobRequestSchema, GetSandboxRequestSchema, GetServiceRequestSchema, ListJobsRequestSchema, ListSandboxesRequestSchema, ListServicesRequestSchema, UpdateJobRequestSchema, UpdateSandboxRequestSchema, UpdateServiceRequestSchema, WaitJobRequestSchema, WaitSandboxRequestSchema, WaitServiceRequestSchema, WatchJobsRequestSchema, WatchSandboxesRequestSchema, WatchServicesRequestSchema, WorkloadService, } from "./gen/toshi/workspace/v1/workloads_pb.js";
20
22
  import { ExecuteStartSchema, GetOperationRequestSchema, HoldServiceRequestSchema, ListEventsRequestSchema, ListServiceRunsRequestSchema, ReleaseServiceRequestSchema, SetServiceModeRequestSchema, StartServiceRequestSchema, StopServiceRequestSchema, WaitOperationRequestSchema, WakeServiceRequestSchema, WatchEventsRequestSchema, WatchOutputRequestSchema, } from "./gen/toshi/workspace/v1/workspace_pb.js";
21
23
  import { openOutputWatch } from "./output.js";
22
- import { authInterceptor, keyInterceptor, retryInterceptor, scopeInterceptor } from "./interceptors.js";
24
+ import { paginate } from "./paginate.js";
25
+ import { authInterceptor, keyInterceptor, productInterceptor, retryInterceptor, scopeInterceptor, } from "./interceptors.js";
23
26
  import { normalizeRetry } from "./retry.js";
27
+ import { withDefaultUnaryTimeout } from "./timeout.js";
24
28
  import { openEventWatch, openJobWatch, openSandboxWatch, openServiceWatch } from "./watch.js";
25
29
  import { runJob } from "./run.js";
26
30
  import { waitJob, waitOperation, waitSandbox, waitService } from "./wait.js";
27
31
  /**
28
- * Creates the client: one normalized retry setting, the auth, scope, key and
29
- * retry interceptors (auth outermost, retry innermost), one transport for every
30
- * client and helper.
32
+ * Creates the client: one normalized retry setting, the product, scope, key,
33
+ * retry and auth interceptors (product outermost, so a product's refusal
34
+ * reaches the caller as a ProductError; auth innermost, so no interceptor sees
35
+ * the Authorization header), one transport for every client and helper.
31
36
  *
32
37
  * Since 0.2.0 the workload calls are grouped by the type they name under
33
38
  * `workload`; every other service, WorkspaceService's included, is handed out
@@ -36,12 +41,13 @@ import { waitJob, waitOperation, waitSandbox, waitService } from "./wait.js";
36
41
  export function createToshiClient(options) {
37
42
  const retry = normalizeRetry(options.retry);
38
43
  const interceptors = [
39
- authInterceptor(options.token ?? ""),
44
+ productInterceptor(),
40
45
  scopeInterceptor(options.project ?? "", options.environment ?? ""),
41
46
  keyInterceptor(),
42
47
  retryInterceptor(retry),
48
+ authInterceptor(options.token ?? "", options.tokenSource),
43
49
  ];
44
- const transport = options.transport(interceptors);
50
+ const transport = withDefaultUnaryTimeout(options.transport(interceptors), options.defaultTimeoutMs);
45
51
  const workloadService = createClient(WorkloadService, transport);
46
52
  return {
47
53
  project: createClient(ProjectService, transport),
@@ -58,6 +64,7 @@ export function createToshiClient(options) {
58
64
  create: (req, opts) => workloadService.createJob(req, opts),
59
65
  get: (req, opts) => workloadService.getJob(req, opts),
60
66
  list: (req, opts) => workloadService.listJobs(req, opts),
67
+ all: (req, opts) => paginate(req, workloadService.listJobs, (resp) => resp.jobs, opts),
61
68
  update: (req, opts) => workloadService.updateJob(req, opts),
62
69
  delete: (req, opts) => workloadService.deleteJob(req, opts),
63
70
  cancel: (req, opts) => workloadService.cancelJob(req, opts),
@@ -69,6 +76,7 @@ export function createToshiClient(options) {
69
76
  create: (req, opts) => workloadService.createSandbox(req, opts),
70
77
  get: (req, opts) => workloadService.getSandbox(req, opts),
71
78
  list: (req, opts) => workloadService.listSandboxes(req, opts),
79
+ all: (req, opts) => paginate(req, workloadService.listSandboxes, (resp) => resp.sandboxes, opts),
72
80
  update: (req, opts) => workloadService.updateSandbox(req, opts),
73
81
  delete: (req, opts) => workloadService.deleteSandbox(req, opts),
74
82
  wait: (req, opts) => waitSandbox(workloadService, create(WaitSandboxRequestSchema, req), opts),
@@ -79,6 +87,7 @@ export function createToshiClient(options) {
79
87
  create: (req, opts) => workloadService.createService(req, opts),
80
88
  get: (req, opts) => workloadService.getService(req, opts),
81
89
  list: (req, opts) => workloadService.listServices(req, opts),
90
+ all: (req, opts) => paginate(req, workloadService.listServices, (resp) => resp.services, opts),
82
91
  update: (req, opts) => workloadService.updateService(req, opts),
83
92
  delete: (req, opts) => workloadService.deleteService(req, opts),
84
93
  wait: (req, opts) => waitService(workloadService, create(WaitServiceRequestSchema, req), opts),
@@ -90,9 +99,11 @@ export function createToshiClient(options) {
90
99
  wake: (req, opts) => workloadService.wakeService(req, opts),
91
100
  hold: (req, opts) => workloadService.holdService(req, opts),
92
101
  listRuns: (req, opts) => workloadService.listServiceRuns(req, opts),
102
+ allRuns: (req, opts) => paginate(req, workloadService.listServiceRuns, (resp) => resp.runs, opts),
93
103
  },
94
104
  waitOperation: (req, opts) => waitOperation(workloadService, create(WaitOperationRequestSchema, req), opts),
95
105
  listEvents: (req, opts) => workloadService.listEvents(req, opts),
106
+ allEvents: (req, opts) => paginate(req, workloadService.listEvents, (resp) => resp.events, opts),
96
107
  watchEvents: (req, opts) => openEventWatch(workloadService, retry, create(WatchEventsRequestSchema, req), opts),
97
108
  getOperation: (req, opts) => workloadService.getOperation(req, opts),
98
109
  },
@@ -104,5 +115,7 @@ export function createToshiClient(options) {
104
115
  auditEvent: createClient(AuditEventService, transport),
105
116
  filler: createClient(FillerService, transport),
106
117
  storageProvider: createClient(StorageProviderService, transport),
118
+ product: createClient(ProductService, transport),
119
+ discovery: createClient(DiscoveryService, transport),
107
120
  };
108
121
  }
package/dist/errors.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { ConnectError } from "@connectrpc/connect";
1
2
  import type { ExecutionExit, OutputEnd } from "./gen/toshi/workspace/v1/workspace_pb.js";
2
3
  /** A command that ended non-zero, or was cancelled before it had a status (errors.go:9-26). */
3
4
  export declare class ExitError extends Error {
@@ -26,3 +27,49 @@ export declare class StreamLostError extends Error {
26
27
  readonly requestKey: string;
27
28
  constructor(executionId: string, requestKey: string, cause?: unknown);
28
29
  }
30
+ /** Why the API refused a call for a product: the reason of the refusal's ErrorInfo detail. */
31
+ export type ProductReason = "PRODUCT_NOT_INSTALLED" | "PRODUCT_DISABLED" | "PRODUCT_NOT_ENTITLED" | "PRODUCT_TURNED_OFF" | "ACCESS_NOT_ACCEPTED";
32
+ /** The reasons a call is refused for a product. */
33
+ export declare const ProductReason: {
34
+ /** The deployment's build does not include the product; the code is Unimplemented. */
35
+ readonly NotInstalled: "PRODUCT_NOT_INSTALLED";
36
+ /** The deployment turned the product off; the code is FailedPrecondition. */
37
+ readonly Disabled: "PRODUCT_DISABLED";
38
+ /** The workspace's entitlements leave the product out; the code is PermissionDenied. */
39
+ readonly NotEntitled: "PRODUCT_NOT_ENTITLED";
40
+ /**
41
+ * The workspace's settings turn the product off, or an outside product's
42
+ * installation is turned off; the code is FailedPrecondition.
43
+ */
44
+ readonly TurnedOff: "PRODUCT_TURNED_OFF";
45
+ /**
46
+ * Turning an outside product on installs it, and the call did not accept the
47
+ * access it asks for; the code is FailedPrecondition.
48
+ */
49
+ readonly AccessNotAccepted: "ACCESS_NOT_ACCEPTED";
50
+ };
51
+ /**
52
+ * The API's refusal of a call for a product: one that is off for the
53
+ * workspace, or an outside product whose access the call did not accept.
54
+ * Every call the client makes throws it in place of the ConnectError, wait,
55
+ * watch and execute included. It is a ConnectError, so `instanceof
56
+ * ConnectError` and `code` read as before; test for it with
57
+ * {@link isProductError}.
58
+ */
59
+ export declare class ProductError extends ConnectError {
60
+ /** The product's ID, such as `runners`. */
61
+ readonly product: string;
62
+ readonly reason: ProductReason;
63
+ /** The setting that turns the product on, such as `runners.enabled`; empty for ACCESS_NOT_ACCEPTED. */
64
+ readonly feature: string;
65
+ /** How far a product the deployment disables has wound down: `no_new_work`, `draining` or `stopped`; empty otherwise. */
66
+ readonly state: string;
67
+ /** The access an outside product asks for, for ACCESS_NOT_ACCEPTED: accept exactly this list to install it. */
68
+ readonly access: string[];
69
+ constructor(err: ConnectError, reason: ProductReason, metadata: ReadonlyMap<string, string>);
70
+ static [Symbol.hasInstance](v: unknown): boolean;
71
+ }
72
+ /** Reports whether err is a product's refusal. */
73
+ export declare function isProductError(err: unknown): err is ProductError;
74
+ /** err as a ProductError when it is a product's refusal, and err unchanged otherwise. */
75
+ export declare function asProductError(err: unknown): unknown;