@usestatemachines/sdk 0.1.1 → 0.3.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 (76) hide show
  1. package/README.md +265 -75
  2. package/agents/SKILL.md +33 -46
  3. package/agents/index.js +7 -7
  4. package/agents/llms.txt +25 -40
  5. package/agents/prompt.md +11 -34
  6. package/esm/extensions/apps.d.ts +18 -0
  7. package/esm/extensions/apps.js +39 -0
  8. package/esm/extensions/client.d.ts +66 -3
  9. package/esm/extensions/client.js +236 -35
  10. package/esm/extensions/errors.d.ts +50 -6
  11. package/esm/extensions/errors.js +37 -3
  12. package/esm/extensions/index.d.ts +5 -3
  13. package/esm/extensions/index.js +2 -1
  14. package/esm/extensions/running-environment.d.ts +32 -0
  15. package/esm/extensions/running-environment.js +44 -0
  16. package/esm/extensions/signal.d.ts +1 -2
  17. package/esm/extensions/signal.js +9 -4
  18. package/esm/extensions/wait.d.ts +11 -3
  19. package/esm/extensions/wait.js +23 -13
  20. package/esm/funcs/create-environment-definition.d.ts +11 -0
  21. package/esm/funcs/create-environment-definition.js +94 -0
  22. package/esm/funcs/environments-list.js +1 -0
  23. package/esm/funcs/snapshots-capture.d.ts +11 -0
  24. package/esm/funcs/snapshots-capture.js +94 -0
  25. package/esm/funcs/snapshots-delete.d.ts +11 -0
  26. package/esm/funcs/snapshots-delete.js +92 -0
  27. package/esm/funcs/snapshots-get.d.ts +11 -0
  28. package/esm/funcs/snapshots-get.js +92 -0
  29. package/esm/funcs/snapshots-list.d.ts +11 -0
  30. package/esm/funcs/snapshots-list.js +98 -0
  31. package/esm/funcs/snapshots-update.d.ts +11 -0
  32. package/esm/funcs/snapshots-update.js +93 -0
  33. package/esm/lib/config.d.ts +2 -2
  34. package/esm/lib/config.js +2 -2
  35. package/esm/models/app-instance-snapshot.d.ts +15 -0
  36. package/esm/models/app-instance-snapshot.js +19 -0
  37. package/esm/models/connection-secret.d.ts +16 -0
  38. package/esm/models/connection-secret.js +13 -0
  39. package/esm/models/connection.d.ts +14 -0
  40. package/esm/models/connection.js +14 -0
  41. package/esm/models/create-environment-input.d.ts +6 -2
  42. package/esm/models/create-environment-input.js +3 -1
  43. package/esm/models/environment-app.d.ts +1 -0
  44. package/esm/models/environment-app.js +1 -0
  45. package/esm/models/environment-snapshot.d.ts +39 -0
  46. package/esm/models/environment-snapshot.js +41 -0
  47. package/esm/models/environment.d.ts +1 -0
  48. package/esm/models/environment.js +1 -0
  49. package/esm/models/error-response.d.ts +5 -0
  50. package/esm/models/error-response.js +5 -0
  51. package/esm/models/index.d.ts +2 -0
  52. package/esm/models/index.js +2 -0
  53. package/esm/models/operations/capture-snapshot.d.ts +41 -0
  54. package/esm/models/operations/capture-snapshot.js +36 -0
  55. package/esm/models/operations/create-environment-definition.d.ts +143 -0
  56. package/esm/models/operations/create-environment-definition.js +90 -0
  57. package/esm/models/operations/delete-snapshot.d.ts +23 -0
  58. package/esm/models/operations/delete-snapshot.js +20 -0
  59. package/esm/models/operations/get-snapshot.d.ts +23 -0
  60. package/esm/models/operations/get-snapshot.js +20 -0
  61. package/esm/models/operations/index.d.ts +6 -0
  62. package/esm/models/operations/index.js +6 -0
  63. package/esm/models/operations/list-environments.d.ts +2 -0
  64. package/esm/models/operations/list-environments.js +1 -0
  65. package/esm/models/operations/list-snapshots.d.ts +30 -0
  66. package/esm/models/operations/list-snapshots.js +25 -0
  67. package/esm/models/operations/reveal-connection.d.ts +26 -4
  68. package/esm/models/operations/reveal-connection.js +28 -2
  69. package/esm/models/operations/update-snapshot.d.ts +37 -0
  70. package/esm/models/operations/update-snapshot.js +29 -0
  71. package/esm/sdk/sdk.d.ts +6 -1
  72. package/esm/sdk/sdk.js +10 -0
  73. package/esm/sdk/snapshots.d.ts +9 -0
  74. package/esm/sdk/snapshots.js +27 -0
  75. package/openapi.json +1471 -144
  76. package/package.json +10 -11
package/README.md CHANGED
@@ -1,55 +1,200 @@
1
1
  # @usestatemachines/sdk
2
2
 
3
- TypeScript SDK for [State](https://usestatemachines.com): create disposable environments of
4
- replica apps, wait until they are ready, connect to them, and clean them up.
3
+ TypeScript SDK for [State](https://usestatemachines.com): start disposable environments of replica
4
+ apps, connect to them, snapshot their data, and stop them.
5
5
 
6
6
  - Node.js 20.20 or later (22 or 24 recommended), and Cloudflare Workers. ESM only.
7
7
  - Retries transient failures safely, including `create`, which is idempotent.
8
8
 
9
- ## Install and quickstart
9
+ ## Install
10
10
 
11
11
  ```sh
12
12
  npm install @usestatemachines/sdk
13
13
  ```
14
14
 
15
+ ## Quickstart
16
+
15
17
  ```ts
16
18
  import { StateMachines } from '@usestatemachines/sdk';
17
19
 
18
- // Reads STATE_API_KEY. The workspace comes from the key.
19
- const sm = new StateMachines();
20
+ const sm = new StateMachines(); // reads STATE_API_KEY
21
+ const env = await sm.environments.start({ apps: ['salesforce'] });
22
+
23
+ try {
24
+ const salesforce = await env.connect('salesforce', { apiId: 'rest' });
25
+ const res = await salesforce.fetch('/services/data/v67.0/limits');
26
+ console.log(res.status, await res.json());
27
+ } finally {
28
+ await env.stop();
29
+ }
30
+ ```
31
+
32
+ `start()` creates the environment, waits until it's `running`, and returns a `RunningEnvironment`.
33
+ It takes `{ readyTimeoutMs, signal }`: the wait gives up after `readyTimeoutMs` (more than 0; 10
34
+ minutes by default). A string app is shorthand for `{ key: s, appId: s }`; nothing else is inferred.
35
+ Salesforce offers several APIs, so pass `apiId`.
36
+
37
+ If the wait fails after the environment was created, `start()` stops the environment, then
38
+ rethrows the original error. The SDK's errors (`EnvironmentFailedError`, `TimeoutError` or a
39
+ `StateMachinesError`; check with `instanceof`) carry `environmentId` and `stopped`. `stopped` is
40
+ `true` when the stop request succeeded. When it's `false`, the stop wasn't confirmed:
41
+ `stopError` says why, and you should call `sm.environments.stop(err.environmentId)`, which is safe
42
+ to repeat. The exception is your own key, below.
43
+
44
+ When you abort the `signal` during the wait, `start()` still stops the environment. If the stop
45
+ succeeded, it rejects with your `signal.reason`, unchanged. If not, it rejects with a
46
+ `StateMachinesError` whose `cause` leads to your reason, carrying `environmentId`, `stopped` and
47
+ `stopError`, so you can still clean up.
48
+
49
+ If you pass your own `idempotencyKey`, `start()` doesn't stop the environment on failure: another
50
+ `start()` with the same key gets the same environment and may be using it. The error carries
51
+ `environmentId` with `stopped: false` and no `stopError`. Stop it only once no other `start()` with
52
+ that key needs it.
20
53
 
21
- // A failed create carries err.idempotencyKey: see "Recovering a failed create" in the README.
54
+ If the create step fails, including an abort, `start()` stops nothing and rejects with a
55
+ `StateMachinesError` that carries `idempotencyKey`. The API may still have created the
56
+ environment, so replay with that key (see
57
+ [Recovering a failed create](#recovering-a-failed-create)) and stop what it returns.
58
+
59
+ ## Choosing an actor
60
+
61
+ ```ts
62
+ const env = await sm.environments.start({ apps: ['salesforce'] });
63
+
64
+ try {
65
+ // The app's users. connect() uses the default actor unless you pick one.
66
+ const actors = await env.actors('salesforce');
67
+ const rep = actors.find((actor) => actor.summary.startsWith('Sales Representative'));
68
+
69
+ if (!rep) {
70
+ throw new Error('No sales representative in this release.');
71
+ }
72
+
73
+ // Act with a sales rep's permissions to test what they can and can't do.
74
+ const salesforce = await env.connect('salesforce', { actorId: rep.id, apiId: 'rest' });
75
+ const res = await salesforce.fetch('/services/data/v67.0/sobjects/Account');
76
+ console.log(res.status);
77
+ } finally {
78
+ await env.stop();
79
+ }
80
+ ```
81
+
82
+ Actors are the app's users, such as Salesforce user IDs (`005…`), and every request runs with the
83
+ chosen actor's permissions. Without an `actorId`, `connect()` uses the environment's declared
84
+ default connection, if it has one, or else the first actor the app lists, the administrator, so
85
+ pick a less-privileged actor to test permission behaviour.
86
+
87
+ ## Full control
88
+
89
+ Use `create`, `waitUntilReady` and `connect` directly when you want to run each step yourself, or
90
+ keep an environment up after a failed boot so you can inspect it. You don't need this path for
91
+ app keys, labels or `lifetimeMinutes`: `start()` takes the same input.
92
+ **Nothing is stopped automatically on this path**: stop the environment yourself, even when a step
93
+ throws.
94
+
95
+ ```ts
22
96
  const environment = await sm.environments.create({
23
- name: 'quickstart',
24
- // The default Salesforce app; sm.catalog.get() lists every app.
97
+ name: 'nightly',
98
+ // key is your own name for the app; later calls take it.
25
99
  apps: [{ key: 'crm', appId: 'salesforce' }],
100
+ lifetimeMinutes: 30,
101
+ labels: { suite: 'nightly' },
26
102
  });
27
103
 
28
104
  try {
29
- await sm.environments.waitUntilReady(environment.id);
105
+ await sm.environments.waitUntilReady(environment.id, { timeoutMs: 5 * 60_000 });
106
+
107
+ const crm = await sm.environments.connect(environment.id, 'crm', { apiId: 'rest' });
108
+ const res = await crm.fetch('/services/data/v67.0/limits');
109
+ console.log(res.status);
110
+ } finally {
111
+ await sm.environments.stop(environment.id);
112
+ // In CI: fail the run when cleanup can't be confirmed.
113
+ await sm.environments.waitUntilStopped(environment.id);
114
+ }
115
+ ```
30
116
 
31
- // Salesforce has several actors (its users) and API profiles, so pick one of each.
32
- // The connection lists the actors; this acts as the first.
33
- const { actors } = await sm.environments.getConnection(environment.id, 'crm');
34
- const [actor] = actors;
117
+ 1. `create(input)` returns one `Environment` as soon as the API accepts it, before the apps boot.
118
+ 2. `waitUntilReady(id)` polls until it is `running`. It throws `EnvironmentFailedError` if the
119
+ environment becomes `failed`, `stopping` or `stopped`, and `TimeoutError` after `timeoutMs`
120
+ (default 10 minutes). It **never stops the environment**, even when it throws.
121
+ 3. `connect(id, appKey, { apiId, actorId })` returns a `Connection` to one app (see
122
+ [Connections](#connections)).
123
+ 4. `stop(id)` returns as soon as the API accepts it, and a second call also resolves.
124
+ `waitUntilStopped(id)` resolves only when the environment is `stopped` or `failed` **and**
125
+ every app's `disposal` is `complete`. If any disposal is `failed` or `unknown`, it throws
126
+ `DisposalIncompleteError` with the environment.
35
127
 
36
- if (!actor) {
37
- throw new Error('The Salesforce app has no actors.');
38
- }
128
+ `lifetimeMinutes` counts from when the environment starts, so boot time uses part of it. When it
129
+ runs out, the environment stops with `stopReason: 'expired'`. The default and maximum are in
130
+ `(await sm.credential.get()).limits.environmentLifetime`.
39
131
 
40
- const crm = await sm.environments.connect(environment.id, 'crm', {
41
- actorId: actor.id,
42
- apiId: 'rest',
132
+ Both waits accept `{ timeoutMs, signal }`. Aborting the `signal` rejects the wait with
133
+ `signal.reason`. They poll at growing intervals of about 1 to 5 seconds, with random jitter, and
134
+ keep polling through transient failures.
135
+
136
+ `start()` and `create()` take the same input, including `{ key, appId }` objects for a custom key
137
+ or two instances of one app.
138
+
139
+ ## Snapshots
140
+
141
+ A snapshot saves the data of a running environment's apps. Seed one environment, snapshot it, and
142
+ start later environments from the snapshot instead of seeding each one.
143
+
144
+ ```ts
145
+ const env = await sm.environments.start({ apps: ['salesforce'] });
146
+
147
+ try {
148
+ const salesforce = await env.connect('salesforce', { apiId: 'rest' });
149
+ await salesforce.fetch('/services/data/v67.0/sobjects/Account', {
150
+ method: 'POST',
151
+ headers: { 'Content-Type': 'application/json' },
152
+ body: JSON.stringify({ Name: 'Acme' }),
43
153
  });
44
- const response = await crm.fetch('/services/data/v67.0/limits');
45
154
 
46
- console.log(response.status, await response.json());
155
+ // Waits until the snapshot is ready.
156
+ const snapshot = await env.snapshot({ name: 'seeded accounts' });
157
+ console.log(snapshot.id, snapshot.sizeBytes);
47
158
  } finally {
48
- // Stop even when a step fails; lifetime counts from the start.
49
- await sm.environments.stop(environment.id);
159
+ await env.stop();
50
160
  }
51
161
  ```
52
162
 
163
+ ```ts
164
+ // Later, or in every CI job: start from the seeded state.
165
+ const restored = await sm.environments.start({ snapshotId: 'snp_…' });
166
+ ```
167
+
168
+ Pass `snapshotId` instead of `apps` to `start()` or `create()`. Each app keeps the key it had when
169
+ captured; `snapshotAppInstanceKeys` restores only some of them. The new environment's
170
+ `sourceEnvironmentSnapshotId` and each app's `sourceAppInstanceSnapshotId` name the source.
171
+
172
+ - A snapshot is taken per app instance (`consistency: 'per-app-instance'`), not at one instant
173
+ across apps. It can only be taken while the environment is `running` and each app is `ready`.
174
+ - `readyTimeoutMs` bounds the wait for `ready` (10 minutes by default), as in `start()`. While the
175
+ API is busy (429 `snapshot-limit`), the capture is retried with the same idempotency key for up
176
+ to 60 seconds.
177
+ - The SDK never deletes a snapshot for you, and every capture error thrown after the snapshot was
178
+ created carries `snapshotId`. A wait past `readyTimeoutMs` throws `SnapshotTimeoutError`, and an
179
+ abort after the create leaves the capture running on the server: `sm.snapshots.waitUntilReady(err.snapshotId)`
180
+ waits again. A failed capture throws `SnapshotFailedError` and stays failed: capture again.
181
+ - Capturing needs `snapshots:create` and `snapshots:read`, restoring needs `snapshots:read`,
182
+ renaming needs `snapshots:create`, and deleting needs `snapshots:delete`. Keys created by members don't hold `snapshots:delete`, so
183
+ `delete` returns 403 for them.
184
+
185
+ `sm.snapshots` has the individual steps:
186
+
187
+ - `create(environmentId, input)` returns the API's answer at once, possibly still `capturing`, and
188
+ doesn't retry `snapshot-limit`. It needs only `snapshots:create`. `waitUntilReady(id)` waits for
189
+ `ready`. `capture(environmentId, input, options)` does both; `env.snapshot()` calls it.
190
+ - `list({ sourceEnvironmentId, cursor, limit })` returns one page, newest first, without deleted
191
+ snapshots. `get(id)` also returns deleted ones, with `deletedAt` set.
192
+ - `rename(id, { name, expectedVersion })` needs the snapshot's current `version`. A stale one, or a
193
+ deleted snapshot, is 409 `version-conflict`, and so is a retried rename whose first answer was
194
+ lost: `get(id)` shows the current name and version. `name: null` clears the name.
195
+ - `delete(id)` marks the snapshot deleted and returns it. A snapshot that is still `capturing`
196
+ can't be deleted (409 `not-ready`).
197
+
53
198
  ## Authentication
54
199
 
55
200
  `new StateMachines()` reads the API key from `STATE_API_KEY`, but only where a global `process`
@@ -74,7 +219,8 @@ export default {
74
219
  };
75
220
  ```
76
221
 
77
- Calls that need a workspace (`environments.list` and `environments.create`) use, in order:
222
+ Calls that need a workspace (`environments.list`, `environments.create`, `snapshots.list`,
223
+ `snapshots.create` and `snapshots.capture`) use, in order:
78
224
 
79
225
  1. `workspaceId` passed to that call
80
226
  2. `workspaceId` passed to the constructor
@@ -86,24 +232,6 @@ A user session that has no workspace can't supply one, so those calls throw and
86
232
  Other constructor options: `serverURL`, `fetch` (a custom fetch implementation), `httpClient`
87
233
  (copied, never changed; not combined with `fetch`) and `timeoutMs` (see below).
88
234
 
89
- ## Environment lifecycle
90
-
91
- 1. `create(input)` returns the environment as soon as the API accepts it, before the apps boot.
92
- 2. `waitUntilReady(id)` polls until it is `running`. It throws `EnvironmentFailedError` if the
93
- environment becomes `failed`, `stopping` or `stopped`, and `TimeoutError` after `timeoutMs`
94
- (default 10 minutes). It **never stops the environment**, even when it throws.
95
- 3. `connect(id, appKey, { actorId, apiId })` returns a `Connection` to one app (see
96
- [Connections](#connections)).
97
- 4. `stop(id)` stops it. Always stop in a `finally`.
98
-
99
- `lifetimeMinutes` counts from when the environment starts, so boot time uses part of it. When it
100
- runs out, the environment stops with `stopReason: 'expired'`. The default and maximum are in
101
- `(await sm.credential.get()).limits.environmentLifetime`.
102
-
103
- Both waits accept `{ timeoutMs, signal }`. Aborting the `signal` rejects the wait with
104
- `signal.reason`. They poll at growing intervals of about 1 to 5 seconds, with random jitter, and
105
- keep polling through transient failures.
106
-
107
235
  ## Retries and timeouts
108
236
 
109
237
  Every API call retries on its own, with growing, jittered delays of up to about 5 seconds between
@@ -114,6 +242,7 @@ attempts:
114
242
  | `502`, `503`, `504` | Yes | The API is saturated (`503`), or Cloudflare can't reach it for a moment (`502`, `504`). |
115
243
  | No response: connection error or attempt timeout | Yes | The request may never have arrived. |
116
244
  | `429` (`environment-limit`) | No | A quota on live environments. Stop one, then create again. |
245
+ | `429` (`snapshot-limit`) | Only by `snapshots.capture()` and `env.snapshot()`, for up to 60 seconds | The API is running as many captures as it can. |
117
246
  | Any other status: other `4xx`, and other `5xx` | No | Not retried by the SDK. |
118
247
 
119
248
  - A `Retry-After` header is honoured, up to 5 seconds.
@@ -123,7 +252,8 @@ attempts:
123
252
  - `timeoutMs` (constructor option, default 60 seconds) limits **each attempt**. A timed-out
124
253
  attempt is retried with a fresh timeout.
125
254
  - A `signal` passed to a call aborts the whole call, including retries. The call rejects with a
126
- `StateMachinesError` whose `status` is `null`.
255
+ `StateMachinesError` whose `status` is `null`. The waits, and `start()` while it waits, reject
256
+ with `signal.reason` instead (see [Full control](#full-control)).
127
257
 
128
258
  ```ts
129
259
  const environment = await sm.environments.get('env_123', {
@@ -160,21 +290,12 @@ async function createOnce() {
160
290
  const environment = await createOnce();
161
291
  ```
162
292
 
163
- ## Cleanup
164
-
165
- `stop` returns as soon as the API accepts it. To confirm cleanup, call `waitUntilStopped`. It
166
- resolves only when:
167
-
168
- - the environment is `stopped` or `failed`, **and**
169
- - every app's `disposal` is `complete`.
170
-
171
- If any app's disposal is `failed` or `unknown`, it throws `DisposalIncompleteError` with the
172
- environment. We recommend it in CI, so a run fails when cleanup can't be confirmed:
173
-
174
- ```ts
175
- await sm.environments.stop(environment.id);
176
- await sm.environments.waitUntilStopped(environment.id);
177
- ```
293
+ `start()` makes the same guarantee for its create step: call
294
+ `sm.environments.start(input, { idempotencyKey: err.idempotencyKey })` with the same input. A key
295
+ only recovers a failed create. Don't reuse the key of a `start()` whose wait failed: the replay
296
+ returns that same environment, which `start()` stopped (or, with your own key, left running for
297
+ you to stop). And don't replay an error whose `cause`
298
+ is a `TypeError`: the input was invalid and nothing was sent.
178
299
 
179
300
  ## Errors
180
301
 
@@ -188,14 +309,36 @@ Every failed API call throws a `StateMachinesError`:
188
309
  | `fields` | `Record<string, string[]> \| null` | Per-field validation messages. |
189
310
  | `retryAfter` | `number \| null` | Milliseconds the API asked you to wait. |
190
311
  | `retryable` | `boolean` | `true` for a `502`, `503` or `504`, a connection error or an attempt timeout. `false` for caller aborts, request validation errors and responses that aren't valid JSON. |
191
- | `idempotencyKey` | `string \| undefined` | Set on `environments.create` failures. |
312
+ | `idempotencyKey` | `string \| undefined` | Set on `environments.create` failures, including the create step of `start()`, and on `snapshots.create` and `snapshots.capture` failures. |
192
313
 
193
- `status` is `null` when no HTTP response was received, and also when a response wasn't valid JSON.
314
+ `status` is `null` when no HTTP response was received, when a response wasn't valid JSON, and when
315
+ the SDK rejected the input before sending (its `cause` is then a `TypeError`).
194
316
  Error messages never include response bodies. The waits throw their own classes:
195
317
  `EnvironmentFailedError` and `DisposalIncompleteError` (both with `environment`) and
196
318
  `TimeoutError` (with `timeoutMs` and the last `environment` seen, or `null`). Check errors with
197
319
  `instanceof`, not `name`: a platform abort is also named `TimeoutError`.
198
320
 
321
+ An SDK error that `start()` rethrows after a failed wait also carries `environmentId`, `stopped`
322
+ and, when the stop failed, `stopError`. The stop gets 30 seconds and doesn't wait for disposal.
323
+ Invalid `apps` fail before any request, as a `StateMachinesError` with `status: null`, the
324
+ `idempotencyKey`, and one of these messages: `Pass at least one app.`,
325
+ `App key "<key>" is used twice.` or `"<s>" can't be used as an app key; pass { key, appId } instead.`
326
+ Starting from a snapshot fails the same way with `Pass apps or snapshotId.`,
327
+ `Pass apps or snapshotId, not both.` or `Pass snapshotAppInstanceKeys with snapshotId.`
328
+
329
+ Snapshot waits throw `SnapshotFailedError`, with the `snapshot` (its `errorCode` is
330
+ `capture-failed`, or `capture-interrupted` when the API restarted mid-capture), and
331
+ `SnapshotTimeoutError`, with `timeoutMs` and the last `snapshot` seen, or `null`. Both carry
332
+ `snapshotId`. From `capture`, they and any `StateMachinesError` thrown after the snapshot was
333
+ created also carry `snapshotId` and `idempotencyKey`.
334
+
335
+ | Code | Status | Meaning |
336
+ | --- | --- | --- |
337
+ | `snapshot-limit` | 429 | The API is running as many captures as it can. `capture` retries it for up to 60 seconds; `create` doesn't. |
338
+ | `not-ready` | 409 | Capture: the environment isn't `running` or an app isn't `ready`. Restore: the snapshot isn't `ready` yet. Delete: the snapshot is still `capturing`. |
339
+ | `version-conflict` | 409 | `rename` sent a stale `expectedVersion`, or the snapshot was deleted. `get` it: if `deletedAt` is set it's gone, otherwise retry with its `version`. |
340
+ | `release-unavailable` | 422 | Restore: no release on this deployment matches the snapshot's apps. |
341
+
199
342
  ```ts
200
343
  import { ErrorCode, StateMachinesError } from '@usestatemachines/sdk';
201
344
 
@@ -212,8 +355,8 @@ try {
212
355
 
213
356
  ## Pagination
214
357
 
215
- `environments.list`, `environments.events` and `workspaces.events` return one page,
216
- `{ data, nextCursor }`. Pass `nextCursor` back as `cursor` until it is `null`:
358
+ `environments.list`, `environments.events`, `workspaces.events` and `snapshots.list` return one
359
+ page, `{ data, nextCursor }`. Pass `nextCursor` back as `cursor` until it is `null`:
217
360
 
218
361
  ```ts
219
362
  let cursor: string | undefined;
@@ -231,9 +374,16 @@ do {
231
374
 
232
375
  ## Connections
233
376
 
234
- `connect(id, appKey)` requires the app to be ready. If the app has several actors, pass
235
- `{ actorId }`; if it has several API profiles, pass `{ apiId }`. Salesforce has both, so pass both.
236
- Otherwise it throws a `TypeError` that names the choices. It returns a `Connection` with `baseUrl`, `environmentId`, `appKey`, `actorId`, `api` and
377
+ `connect()` needs the app to be ready, which `start()` guarantees. It takes `{ apiId, actorId }`
378
+ and throws a `TypeError` before revealing any token when a choice is missing or wrong:
379
+
380
+ - `apiId` is required when the app has several API profiles, as Salesforce does:
381
+ `Choose one API for <appKey>: <ids>.` There is no default API.
382
+ - `actorId` defaults to the actor of the environment's declared default connection, or else the
383
+ app's first actor (for Salesforce, the administrator). One that isn't among the app's actors
384
+ throws `Unknown actor <id> for <appKey>. Choose one of: <ids>.`
385
+
386
+ It returns a `Connection` with `baseUrl`, `environmentId`, `appKey`, `actorId`, `api` and
237
387
  `expiresAt`, and these ways to call the app:
238
388
 
239
389
  - `headers()` returns `X-State-Environment-Token` plus the app's actor header, such as
@@ -258,10 +408,7 @@ release does), takes the actor token in the envelope's `SessionHeader/sessionId`
258
408
  value you put in the envelope:
259
409
 
260
410
  ```ts
261
- const soap = await sm.environments.connect(environment.id, 'crm', {
262
- actorId: actor.id, // from getConnection, as in the quickstart
263
- apiId: 'soap-partner',
264
- });
411
+ const soap = await env.connect('salesforce', { apiId: 'soap-partner' });
265
412
  const envelope = `<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/" xmlns="urn:partner.soap.sforce.com">
266
413
  <env:Header><SessionHeader><sessionId>${soap.actorToken()}</sessionId></SessionHeader></env:Header>
267
414
  <env:Body><undelete><ids>001000000000000AAA</ids></undelete></env:Body>
@@ -279,12 +426,9 @@ as jsforce only sends it if you configure custom headers. Prefer `Connection.fet
279
426
  don't let the client follow redirects, which would forward the tokens to another host:
280
427
 
281
428
  ```ts
282
- const crm = await sm.environments.connect(environment.id, 'crm', {
283
- actorId: actor.id, // from getConnection, as in the quickstart
284
- apiId: 'rest',
285
- });
286
- const response = await fetch(new URL('services/data/v67.0/limits', crm.baseUrl), {
287
- headers: crm.headers(),
429
+ const salesforce = await env.connect('salesforce', { apiId: 'rest' });
430
+ const response = await fetch(new URL('services/data/v67.0/limits', salesforce.baseUrl), {
431
+ headers: salesforce.headers(),
288
432
  redirect: 'manual',
289
433
  });
290
434
  ```
@@ -305,6 +449,52 @@ TypeScript consumers need `lib: ["DOM"]` or `@types/node`, for the `fetch`, `Req
305
449
  version.
306
450
  - The API may add error codes and enum values, such as a new status or stop reason. Enums are
307
451
  open: handle values you don't recognise.
452
+ - 0.3.0 adds the snapshot APIs and keeps every 0.2.0 call. An input with neither `apps` nor
453
+ `snapshotId` now fails with `Pass apps or snapshotId.` `Environment` and its apps gain
454
+ `sourceEnvironmentSnapshotId` and `sourceAppInstanceSnapshotId` (`null` unless restored), so
455
+ object literals typed as `Environment` need them.
456
+ - 0.2.0 adds APIs and keeps every 0.1.x call, with these deliberate changes:
457
+ - `connect()` without `actorId` now succeeds when an app has several actors, acting as the
458
+ default actor (for Salesforce, the administrator, with full access).
459
+ - An empty `actorId` (`''`) now throws `Unknown actor`, instead of acting as an app's only actor.
460
+ - `create()` rejects an empty `apps` list or a repeated app key before sending a request, as a
461
+ `StateMachinesError` with `status: null`.
462
+ - `connect()`'s error messages now name the app: `Unknown actor <id> for <appKey>. …` and
463
+ `Choose one API for <appKey>: …`.
464
+
465
+ ## Known limitations
466
+
467
+ - Vendor clients such as jsforce don't send `X-State-Environment-Token` unless you configure
468
+ custom headers, so the gateway rejects them. Use `Connection.fetch`, or pass `headers()` as
469
+ shown in [Connections](#connections).
470
+ - There is no default API profile: pass `apiId` whenever an app has more than one.
471
+ - Without a declared default connection, the default actor is the first one the release lists;
472
+ releases don't declare a default actor yet.
473
+ - `await using` isn't supported. Stop the environment in `finally`.
474
+
475
+ ## What's new in 0.3.0
476
+
477
+ - `sm.snapshots`: `capture`, `create`, `waitUntilReady`, `get`, `list`, `rename` and `delete`
478
+ (see [Snapshots](#snapshots)).
479
+ - `env.snapshot()` captures a running environment and waits until the snapshot is `ready`.
480
+ - `start({ snapshotId })` and `create({ snapshotId })` start an environment from a snapshot.
481
+ Environments carry `sourceEnvironmentSnapshotId`, and apps `sourceAppInstanceSnapshotId`.
482
+ - `environments.list({ snapshotId })` lists the environments started from a snapshot.
483
+ - `SnapshotFailedError` and `SnapshotTimeoutError`.
484
+
485
+ ## What's new in 0.2.0
486
+
487
+ - `sm.environments.start()` creates an environment and waits until it's running. It returns a
488
+ `RunningEnvironment` with `connect`, `actors`, `stop` and `waitUntilStopped`.
489
+ - String apps: `apps: ['salesforce']` means `[{ key: 'salesforce', appId: 'salesforce' }]`, in
490
+ `start()` and `create()`.
491
+ - `actors(appKey)` lists an app's actors in the release's order.
492
+ - `actorId` is optional in `connect()`. It defaults to the environment's declared default
493
+ connection's actor, or else the app's first actor.
494
+ - When readiness fails, `start()` stops the environment before it rethrows.
495
+ - Cancelling a call now takes effect immediately (fixes 0.1.1). A `signal` used to reach an
496
+ in-flight request only when its attempt timeout (60 seconds by default) fired. An abort during
497
+ a retry delay still takes effect when the delay ends.
308
498
 
309
499
  ## License
310
500
 
package/agents/SKILL.md CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: state-machines
3
- description: Use when an agent or test needs a disposable replica of a SaaS app such as Salesforce, to run integration tests or try API calls against a sandboxed copy instead of a real org. Covers the @usestatemachines/sdk TypeScript SDK - creating environments, connecting as an actor, cleanup, and errors such as environment-limit and release-unavailable. Also use when the user mentions State Machines, STATE_API_KEY or @usestatemachines/sdk.
3
+ description: Use when an agent or test needs a disposable replica of a SaaS app such as Salesforce, to run integration tests or try API calls against a sandboxed copy instead of a real org. Covers the @usestatemachines/sdk TypeScript SDK - starting environments, choosing an actor, snapshots, cleanup, and errors such as environment-limit and release-unavailable. Also use when the user mentions State Machines, STATE_API_KEY or @usestatemachines/sdk.
4
4
  ---
5
5
 
6
6
  Read `node_modules/@usestatemachines/sdk/README.md` before relying on memory; it matches the installed version. If the package is missing, run `npm install @usestatemachines/sdk`.
@@ -8,83 +8,70 @@ Read `node_modules/@usestatemachines/sdk/README.md` before relying on memory; it
8
8
  ## Workflow
9
9
 
10
10
  1. Check that `STATE_API_KEY` is set without printing it (`test -n "$STATE_API_KEY"`). If it isn't, ask the user to set it; the SDK doesn't read `.env` files.
11
- 2. Create the environment with only the apps the task needs. Log `await sm.catalog.get()` for each app's `appId` and the actors and API profiles its release offers; don't guess IDs. `key` is your own name for the app, and later calls take it.
12
- 3. Open a `try` right after `create` returns, and call `stop` in its `finally`.
13
- 4. `await sm.environments.waitUntilReady(environment.id)`. It never stops the environment.
14
- 5. Connect as one actor through one API profile. Salesforce has several of each, so name both: `sm.environments.connect(environment.id, 'crm', { actorId: actor.id, apiId: 'rest' })`, taking `actor` from `(await sm.environments.getConnection(environment.id, 'crm')).actors`. Salesforce actor IDs are its user IDs, such as `005…`, never a role name, so read them from the connection instead of hard-coding one. Choose the actor whose permissions the task needs; each release in the catalog lists them.
15
- 6. Call the app with the connection's `fetch`. A leading `/` means the root of `baseUrl`.
16
- 7. In `finally`, `await sm.environments.stop(environment.id)`. In CI, then await `sm.environments.waitUntilStopped(environment.id)`.
11
+ 2. Start the environment with only the apps the task needs: `const env = await sm.environments.start({ apps: ['salesforce'] })`. Log `await sm.catalog.get()` for each app's ID and the actors and API profiles its release offers; don't guess IDs. A string app means `{ key: s, appId: s }`; use `{ key, appId }` for your own key. `start()` waits until the environment is `running`. If that fails, it stops the environment and rethrows; an `EnvironmentFailedError` or `TimeoutError` carries `err.environmentId` and `err.stopped`.
12
+ 3. Open a `try` right after `start()` returns, and call `env.stop()` in its `finally`.
13
+ 4. Connect through one API profile. Salesforce offers several and has no default, so pass `apiId`: `env.connect('salesforce', { apiId: 'rest' })`. This acts as the environment's declared default connection's actor, or else the app's first actor, the administrator. To act with another user's permissions, choose one from `await env.actors('salesforce')` and pass its `id` as `actorId`. Salesforce actor IDs are its user IDs, such as `005…`, never a role name, so read them instead of hard-coding one.
14
+ 5. Call the app with the connection's `fetch`. A leading `/` means the root of `baseUrl`.
15
+ 6. In `finally`, `await env.stop()`. In CI, then `await env.waitUntilStopped()`.
17
16
 
18
17
  The whole flow, as the package ships it. It is ESM with top-level `await`: save it as `state-quickstart.mts` and run `npx tsx state-quickstart.mts`.
19
18
 
20
19
  ```ts
21
20
  import { StateMachines } from '@usestatemachines/sdk';
22
21
 
23
- // Reads STATE_API_KEY. The workspace comes from the key.
24
- const sm = new StateMachines();
25
-
26
- // A failed create carries err.idempotencyKey: see "Recovering a failed create" in the README.
27
- const environment = await sm.environments.create({
28
- name: 'quickstart',
29
- // The default Salesforce app; sm.catalog.get() lists every app.
30
- apps: [{ key: 'crm', appId: 'salesforce' }],
31
- });
22
+ const sm = new StateMachines(); // reads STATE_API_KEY
23
+ const env = await sm.environments.start({ apps: ['salesforce'] });
32
24
 
33
25
  try {
34
- await sm.environments.waitUntilReady(environment.id);
35
-
36
- // Salesforce has several actors (its users) and API profiles, so pick one of each.
37
- // The connection lists the actors; this acts as the first.
38
- const { actors } = await sm.environments.getConnection(environment.id, 'crm');
39
- const [actor] = actors;
40
-
41
- if (!actor) {
42
- throw new Error('The Salesforce app has no actors.');
43
- }
44
-
45
- const crm = await sm.environments.connect(environment.id, 'crm', {
46
- actorId: actor.id,
47
- apiId: 'rest',
48
- });
49
- const response = await crm.fetch('/services/data/v67.0/limits');
50
-
51
- console.log(response.status, await response.json());
26
+ const salesforce = await env.connect('salesforce', { apiId: 'rest' });
27
+ const res = await salesforce.fetch('/services/data/v67.0/limits');
28
+ console.log(res.status, await res.json());
52
29
  } finally {
53
- // Stop even when a step fails; lifetime counts from the start.
54
- await sm.environments.stop(environment.id);
30
+ await env.stop();
55
31
  }
56
32
  ```
57
33
 
58
34
  ## Rules
59
35
 
60
36
  - Always stop in `finally`. `waitUntilStopped` confirms every app was disposed; use it in CI so a run fails when cleanup can't be confirmed.
61
- - Lifetime counts from the start, including boot. It defaults to 15 minutes, or the deployment maximum if that is lower: `(await sm.credential.get()).limits.environmentLifetime`. Set `lifetimeMinutes` in `create` for longer runs.
37
+ - To keep a failed environment for inspection, use the full-control path: `sm.environments.create`, then `sm.environments.waitUntilReady(environment.id)`, then `sm.environments.connect(environment.id, key, options)`. Nothing on that path stops the environment for you.
38
+ - Lifetime counts from the start, including boot. It defaults to 15 minutes, or the deployment maximum if that is lower: `(await sm.credential.get()).limits.environmentLifetime`. Set `lifetimeMinutes` in `start` or `create` for longer runs.
62
39
  - Live environments are capped per organization: `limits.liveEnvironments` from `sm.credential.get()`. Stop the ones you no longer need.
63
40
  - Never print, log or commit `STATE_API_KEY`, tokens, or `headers()` or `actorToken()` output.
64
41
  - Salesforce's SOAP profile (`soap-partner`) takes the actor token in the envelope, not a header: put `connection.actorToken()` in `SessionHeader/sessionId`. For it, `headers()` carries only `X-State-Environment-Token`.
65
42
  - `connection.fetch` never sends your API key, never follows redirects and only reaches URLs under `baseUrl`.
66
43
  - Runtimes: Node.js 20.20 or later (22 or 24 recommended), and Cloudflare Workers. ESM only. TypeScript needs `lib: ["DOM"]` or `@types/node`. Never ship an API key to a browser.
67
44
  - In Cloudflare Workers, pass the key explicitly: `new StateMachines({ apiKey: env.STATE_API_KEY })`, and bound calls with `{ signal: AbortSignal.timeout(20_000) }`, since retries can take minutes.
68
- - `environments.list`, `environments.events` and `workspaces.events` return one page, `{ data, nextCursor }`. Pass `nextCursor` back as `cursor` until it is `null`.
69
- - Check errors with `instanceof` (`StateMachinesError`, `EnvironmentFailedError`, `DisposalIncompleteError`, `TimeoutError`), not by `name`.
45
+ - `environments.list`, `environments.events`, `workspaces.events` and `snapshots.list` return one page, `{ data, nextCursor }`. Pass `nextCursor` back as `cursor` until it is `null`.
46
+ - To reuse seeded data, `await env.snapshot({ name })` once, then `sm.environments.start({ snapshotId })` in place of `apps`. Capturing needs `snapshots:create` and `snapshots:read`. Snapshots are never deleted automatically, not even failed ones.
47
+ - Check errors with `instanceof` (`StateMachinesError`, `EnvironmentFailedError`, `DisposalIncompleteError`, `TimeoutError`, `SnapshotFailedError`, `SnapshotTimeoutError`), not by `name`.
70
48
 
71
49
  ## Recovering a failed create
72
50
 
73
- The SDK already retries `502`, `503`, `504` and connection errors with one idempotency key. If `create` still throws, don't call it again as a new request: that can start a second environment. Every error from `create` carries `err.idempotencyKey`. Call `sm.environments.create(input, { idempotencyKey: err.idempotencyKey })` with the same `input`. It returns the environment the first call created, or creates it if that call never arrived. A different input fails with `idempotency-conflict`. Keys are kept for a day.
51
+ The SDK already retries `502`, `503`, `504` and connection errors with one idempotency key. If `start()` or `create` still throws with `err.idempotencyKey`, the create failed: don't call it again as a new request, which can start a second environment. Call `sm.environments.start(input, { idempotencyKey: err.idempotencyKey })` with the same `input`. Don't replay when `err.status` is `null` and `err.cause` is a `TypeError`: the input was invalid and nothing was sent; fix the input instead. A key only recovers a failed create: don't reuse the key of a `start()` whose wait failed, because the replay returns that stopped environment. With your own key, `start()` doesn't stop the environment on failure, because another `start()` with that key may be using it: the error carries `err.environmentId`, `err.stopped` is `false` and there is no `err.stopError`. Stop it only once nothing else needs it. It returns the environment the first call created, or creates it if that call never arrived. A different input fails with `idempotency-conflict`. Keys are kept for a day.
74
52
 
75
53
  ## Troubleshooting
76
54
 
77
55
  | Symptom | Cause | Fix |
78
56
  | --- | --- | --- |
79
- | `release-unavailable`, 422 | Unknown or retired `appId` or `appReleaseId`. | Check the IDs against `sm.catalog.get()`. Omit `appReleaseId` for the default release. |
57
+ | `release-unavailable`, 422 | Unknown or retired `appId` or `appReleaseId`. A string app is used as the `appId`. | Check the IDs against `sm.catalog.get()`. Omit `appReleaseId` for the default release. |
80
58
  | `release-unavailable`, 503, or a failed app with that `errorCode` | This deployment doesn't serve the release. The SDK retries a 503 within a 10-minute budget. | Omit `appReleaseId`. If it persists, report the `requestId`. Pass a `signal` to bound the wait. |
81
- | `environment-limit`, 429 | The organization is at its live-environment cap. Not retried. | `sm.environments.list({ status: 'live' })`, stop one you no longer need, then create again. |
59
+ | `environment-limit`, 429 | The organization is at its live-environment cap. Not retried. | `sm.environments.list({ status: 'live' })`, stop one you no longer need, then start again. |
82
60
  | `lifetime-too-long`, 422 | `lifetimeMinutes` is above the maximum. | Use at most `limits.environmentLifetime.maximumMinutes`. |
83
- | `TypeError: Choose one actor: ...` or `Choose one API: ...` | The app has several actors or API profiles, or the ID doesn't exist. The message lists the valid IDs. | Pass both `actorId` and `apiId` from the listed IDs. |
84
- | `TypeError: App ... is not ready to connect` | connect() ran before the app was `ready`. | Await `waitUntilReady` first. |
85
- | `connection-unavailable`, 409 | The app stopped, or the environment is no longer `running`. | Check `sm.environments.get(environment.id)`; if it stopped or expired, create a new one. |
86
- | `EnvironmentFailedError` from `waitUntilReady` | The environment became `failed`, `stopping` or `stopped`. | Read `err.environment.stopReason` and each app's `errorCode`. Still stop it in `finally`. |
61
+ | `TypeError: Unknown actor <id> for <appKey>. Choose one of: <ids>.` | The `actorId` isn't one of the app's actors. No token was revealed. | Pass an `id` from `await env.actors(appKey)`, or omit `actorId` to act as the default actor. |
62
+ | `TypeError: Choose one API for <appKey>: <ids>.` | The app has several API profiles and `apiId` is missing or unknown. Salesforce always needs one. | Pass `apiId` from the listed IDs, such as `rest`. |
63
+ | `start()` rejects with `EnvironmentFailedError` or `TimeoutError` | The environment failed, or wasn't `running` within `readyTimeoutMs` (10 minutes by default). If `err.stopped` is `true`, `start()` already stopped it. | Read `err.environment` (`null` on a `TimeoutError` that saw no read): its `stopReason` and each app's `errorCode`. If `err.stopped` is `false`, read `err.stopError` and call `sm.environments.stop(err.environmentId)`. To keep a failed environment, use the full-control path. |
64
+ | `StateMachinesError` (`status` null) for invalid `apps`: `Pass at least one app.`, `App key "<key>" is used twice.` or `"<s>" can't be used as an app key; pass { key, appId } instead.` | Empty list, duplicate key, or a string that isn't a valid key. No request was sent. | Pass one to ten apps with unique keys; use `{ key, appId }` when the ID isn't a valid key. |
65
+ | `TypeError: App ... is not ready to connect` | On the full-control path, connect() ran before the app was `ready`. | Await `waitUntilReady` first, or use `start()`. |
66
+ | `connection-unavailable`, 409 | The app stopped, or the environment is no longer `running`. | Check `sm.environments.get(env.id)`; if it stopped or expired, start a new one. |
67
+ | `EnvironmentFailedError` from `waitUntilReady` | On the full-control path, the environment became `failed`, `stopping` or `stopped`. | Read `err.environment.stopReason` and each app's `errorCode`. Still stop it in `finally`. |
87
68
  | A vendor client such as jsforce is rejected by the gateway | The gateway needs `X-State-Environment-Token` on every request; jsforce only sends it with custom headers configured. | Prefer `connection.fetch`. If you pass `headers()` to another client, send them only to `baseUrl` and disable redirects. |
69
+ | `not-ready`, 409 | Capture: the environment isn't `running` or an app isn't `ready`. Restore: the snapshot isn't `ready` yet. Delete: the snapshot is still `capturing`. | Capture from the handle `start()` returns. Before a restore or delete, `await sm.snapshots.waitUntilReady(snapshotId)`. |
70
+ | `snapshot-limit`, 429 | The API is running as many captures as it can. `capture` and `env.snapshot()` already retried it for 60 seconds. | Wait, then capture again. |
71
+ | `SnapshotFailedError` | The capture failed. `err.snapshot.errorCode` is `capture-failed`, or `capture-interrupted` when the API restarted mid-capture. The snapshot is kept. | Capture again with a new idempotency key (the default); the old key returns the same failed snapshot. |
72
+ | `SnapshotTimeoutError` | The snapshot wasn't `ready` within `readyTimeoutMs`. The capture continues on the server. | `await sm.snapshots.waitUntilReady(err.snapshotId)`. |
73
+ | `forbidden`, 403, from `sm.snapshots.delete` | The key lacks `snapshots:delete`. Keys created by members never hold it. | Use a key whose creator holds `snapshots:delete`, or keep the snapshot. |
74
+ | `release-unavailable`, 422, on a restore | No release on this deployment matches the snapshot's apps. | Start from `apps`, seed again and capture a new snapshot. |
88
75
  | `TypeError: Pass apiKey or set STATE_API_KEY.` | No key in the environment. | Ask the user to set `STATE_API_KEY`; in Workers, pass `apiKey`. |
89
76
 
90
77
  More: https://state-web.usestatemachines.workers.dev/llms.txt and https://www.npmjs.com/package/@usestatemachines/sdk