@usestatemachines/sdk 0.1.1 → 0.2.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 (39) hide show
  1. package/README.md +175 -75
  2. package/agents/SKILL.md +24 -44
  3. package/agents/index.js +7 -7
  4. package/agents/llms.txt +18 -39
  5. package/agents/prompt.md +11 -34
  6. package/esm/extensions/apps.d.ts +8 -0
  7. package/esm/extensions/apps.js +23 -0
  8. package/esm/extensions/client.d.ts +33 -3
  9. package/esm/extensions/client.js +144 -28
  10. package/esm/extensions/errors.d.ts +24 -5
  11. package/esm/extensions/errors.js +3 -2
  12. package/esm/extensions/index.d.ts +3 -1
  13. package/esm/extensions/index.js +1 -0
  14. package/esm/extensions/running-environment.d.ts +29 -0
  15. package/esm/extensions/running-environment.js +35 -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 +1 -0
  19. package/esm/extensions/wait.js +1 -1
  20. package/esm/funcs/create-environment-definition.d.ts +11 -0
  21. package/esm/funcs/create-environment-definition.js +94 -0
  22. package/esm/lib/config.d.ts +2 -2
  23. package/esm/lib/config.js +2 -2
  24. package/esm/models/connection-secret.d.ts +16 -0
  25. package/esm/models/connection-secret.js +13 -0
  26. package/esm/models/connection.d.ts +14 -0
  27. package/esm/models/connection.js +14 -0
  28. package/esm/models/error-response.d.ts +4 -0
  29. package/esm/models/error-response.js +4 -0
  30. package/esm/models/operations/create-environment-definition.d.ts +143 -0
  31. package/esm/models/operations/create-environment-definition.js +90 -0
  32. package/esm/models/operations/index.d.ts +1 -0
  33. package/esm/models/operations/index.js +1 -0
  34. package/esm/models/operations/reveal-connection.d.ts +26 -4
  35. package/esm/models/operations/reveal-connection.js +28 -2
  36. package/esm/sdk/sdk.d.ts +3 -1
  37. package/esm/sdk/sdk.js +5 -0
  38. package/openapi.json +394 -7
  39. package/package.json +1 -1
package/README.md CHANGED
@@ -1,55 +1,141 @@
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, 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
-
21
- // A failed create carries err.idempotencyKey: see "Recovering a failed create" in the README.
22
- const environment = await sm.environments.create({
23
- name: 'quickstart',
24
- // The default Salesforce app; sm.catalog.get() lists every app.
25
- apps: [{ key: 'crm', appId: 'salesforce' }],
26
- });
20
+ const sm = new StateMachines(); // reads STATE_API_KEY
21
+ const env = await sm.environments.start({ apps: ['salesforce'] });
27
22
 
28
23
  try {
29
- await sm.environments.waitUntilReady(environment.id);
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
+ ```
30
31
 
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;
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.
53
+
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'));
35
68
 
36
- if (!actor) {
37
- throw new Error('The Salesforce app has no actors.');
69
+ if (!rep) {
70
+ throw new Error('No sales representative in this release.');
38
71
  }
39
72
 
40
- const crm = await sm.environments.connect(environment.id, 'crm', {
41
- actorId: actor.id,
42
- apiId: 'rest',
43
- });
44
- const response = await crm.fetch('/services/data/v67.0/limits');
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
45
88
 
46
- console.log(response.status, await response.json());
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
96
+ const environment = await sm.environments.create({
97
+ name: 'nightly',
98
+ // key is your own name for the app; later calls take it.
99
+ apps: [{ key: 'crm', appId: 'salesforce' }],
100
+ lifetimeMinutes: 30,
101
+ labels: { suite: 'nightly' },
102
+ });
103
+
104
+ try {
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);
47
110
  } finally {
48
- // Stop even when a step fails; lifetime counts from the start.
49
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);
50
114
  }
51
115
  ```
52
116
 
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.
127
+
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`.
131
+
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
+
53
139
  ## Authentication
54
140
 
55
141
  `new StateMachines()` reads the API key from `STATE_API_KEY`, but only where a global `process`
@@ -86,24 +172,6 @@ A user session that has no workspace can't supply one, so those calls throw and
86
172
  Other constructor options: `serverURL`, `fetch` (a custom fetch implementation), `httpClient`
87
173
  (copied, never changed; not combined with `fetch`) and `timeoutMs` (see below).
88
174
 
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
175
  ## Retries and timeouts
108
176
 
109
177
  Every API call retries on its own, with growing, jittered delays of up to about 5 seconds between
@@ -123,7 +191,8 @@ attempts:
123
191
  - `timeoutMs` (constructor option, default 60 seconds) limits **each attempt**. A timed-out
124
192
  attempt is retried with a fresh timeout.
125
193
  - A `signal` passed to a call aborts the whole call, including retries. The call rejects with a
126
- `StateMachinesError` whose `status` is `null`.
194
+ `StateMachinesError` whose `status` is `null`. The waits, and `start()` while it waits, reject
195
+ with `signal.reason` instead (see [Full control](#full-control)).
127
196
 
128
197
  ```ts
129
198
  const environment = await sm.environments.get('env_123', {
@@ -160,21 +229,12 @@ async function createOnce() {
160
229
  const environment = await createOnce();
161
230
  ```
162
231
 
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
- ```
232
+ `start()` makes the same guarantee for its create step: call
233
+ `sm.environments.start(input, { idempotencyKey: err.idempotencyKey })` with the same input. A key
234
+ only recovers a failed create. Don't reuse the key of a `start()` whose wait failed: the replay
235
+ returns that same environment, which `start()` stopped (or, with your own key, left running for
236
+ you to stop). And don't replay an error whose `cause`
237
+ is a `TypeError`: the input was invalid and nothing was sent.
178
238
 
179
239
  ## Errors
180
240
 
@@ -188,14 +248,21 @@ Every failed API call throws a `StateMachinesError`:
188
248
  | `fields` | `Record<string, string[]> \| null` | Per-field validation messages. |
189
249
  | `retryAfter` | `number \| null` | Milliseconds the API asked you to wait. |
190
250
  | `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. |
251
+ | `idempotencyKey` | `string \| undefined` | Set on `environments.create` failures, including the create step of `start()`. |
192
252
 
193
- `status` is `null` when no HTTP response was received, and also when a response wasn't valid JSON.
253
+ `status` is `null` when no HTTP response was received, when a response wasn't valid JSON, and when
254
+ the SDK rejected the input before sending (its `cause` is then a `TypeError`).
194
255
  Error messages never include response bodies. The waits throw their own classes:
195
256
  `EnvironmentFailedError` and `DisposalIncompleteError` (both with `environment`) and
196
257
  `TimeoutError` (with `timeoutMs` and the last `environment` seen, or `null`). Check errors with
197
258
  `instanceof`, not `name`: a platform abort is also named `TimeoutError`.
198
259
 
260
+ An SDK error that `start()` rethrows after a failed wait also carries `environmentId`, `stopped`
261
+ and, when the stop failed, `stopError`. The stop gets 30 seconds and doesn't wait for disposal.
262
+ Invalid `apps` fail before any request, as a `StateMachinesError` with `status: null`, the
263
+ `idempotencyKey`, and one of these messages: `Pass at least one app.`,
264
+ `App key "<key>" is used twice.` or `"<s>" can't be used as an app key; pass { key, appId } instead.`
265
+
199
266
  ```ts
200
267
  import { ErrorCode, StateMachinesError } from '@usestatemachines/sdk';
201
268
 
@@ -231,9 +298,16 @@ do {
231
298
 
232
299
  ## Connections
233
300
 
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
301
+ `connect()` needs the app to be ready, which `start()` guarantees. It takes `{ apiId, actorId }`
302
+ and throws a `TypeError` before revealing any token when a choice is missing or wrong:
303
+
304
+ - `apiId` is required when the app has several API profiles, as Salesforce does:
305
+ `Choose one API for <appKey>: <ids>.` There is no default API.
306
+ - `actorId` defaults to the actor of the environment's declared default connection, or else the
307
+ app's first actor (for Salesforce, the administrator). One that isn't among the app's actors
308
+ throws `Unknown actor <id> for <appKey>. Choose one of: <ids>.`
309
+
310
+ It returns a `Connection` with `baseUrl`, `environmentId`, `appKey`, `actorId`, `api` and
237
311
  `expiresAt`, and these ways to call the app:
238
312
 
239
313
  - `headers()` returns `X-State-Environment-Token` plus the app's actor header, such as
@@ -258,10 +332,7 @@ release does), takes the actor token in the envelope's `SessionHeader/sessionId`
258
332
  value you put in the envelope:
259
333
 
260
334
  ```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
- });
335
+ const soap = await env.connect('salesforce', { apiId: 'soap-partner' });
265
336
  const envelope = `<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/" xmlns="urn:partner.soap.sforce.com">
266
337
  <env:Header><SessionHeader><sessionId>${soap.actorToken()}</sessionId></SessionHeader></env:Header>
267
338
  <env:Body><undelete><ids>001000000000000AAA</ids></undelete></env:Body>
@@ -279,12 +350,9 @@ as jsforce only sends it if you configure custom headers. Prefer `Connection.fet
279
350
  don't let the client follow redirects, which would forward the tokens to another host:
280
351
 
281
352
  ```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(),
353
+ const salesforce = await env.connect('salesforce', { apiId: 'rest' });
354
+ const response = await fetch(new URL('services/data/v67.0/limits', salesforce.baseUrl), {
355
+ headers: salesforce.headers(),
288
356
  redirect: 'manual',
289
357
  });
290
358
  ```
@@ -305,6 +373,38 @@ TypeScript consumers need `lib: ["DOM"]` or `@types/node`, for the `fetch`, `Req
305
373
  version.
306
374
  - The API may add error codes and enum values, such as a new status or stop reason. Enums are
307
375
  open: handle values you don't recognise.
376
+ - 0.2.0 adds APIs and keeps every 0.1.x call, with these deliberate changes:
377
+ - `connect()` without `actorId` now succeeds when an app has several actors, acting as the
378
+ default actor (for Salesforce, the administrator, with full access).
379
+ - An empty `actorId` (`''`) now throws `Unknown actor`, instead of acting as an app's only actor.
380
+ - `create()` rejects an empty `apps` list or a repeated app key before sending a request, as a
381
+ `StateMachinesError` with `status: null`.
382
+ - `connect()`'s error messages now name the app: `Unknown actor <id> for <appKey>. …` and
383
+ `Choose one API for <appKey>: …`.
384
+
385
+ ## Known limitations
386
+
387
+ - Vendor clients such as jsforce don't send `X-State-Environment-Token` unless you configure
388
+ custom headers, so the gateway rejects them. Use `Connection.fetch`, or pass `headers()` as
389
+ shown in [Connections](#connections).
390
+ - There is no default API profile: pass `apiId` whenever an app has more than one.
391
+ - Without a declared default connection, the default actor is the first one the release lists;
392
+ releases don't declare a default actor yet.
393
+ - `await using` isn't supported. Stop the environment in `finally`.
394
+
395
+ ## What's new in 0.2.0
396
+
397
+ - `sm.environments.start()` creates an environment and waits until it's running. It returns a
398
+ `RunningEnvironment` with `connect`, `actors`, `stop` and `waitUntilStopped`.
399
+ - String apps: `apps: ['salesforce']` means `[{ key: 'salesforce', appId: 'salesforce' }]`, in
400
+ `start()` and `create()`.
401
+ - `actors(appKey)` lists an app's actors in the release's order.
402
+ - `actorId` is optional in `connect()`. It defaults to the environment's declared default
403
+ connection's actor, or else the app's first actor.
404
+ - When readiness fails, `start()` stops the environment before it rethrows.
405
+ - Cancelling a call now takes effect immediately (fixes 0.1.1). A `signal` used to reach an
406
+ in-flight request only when its attempt timeout (60 seconds by default) fired. An abort during
407
+ a retry delay still takes effect when the delay ends.
308
408
 
309
409
  ## License
310
410
 
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, 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,57 +8,34 @@ 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`.
@@ -70,20 +47,23 @@ try {
70
47
 
71
48
  ## Recovering a failed create
72
49
 
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.
50
+ 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
51
 
75
52
  ## Troubleshooting
76
53
 
77
54
  | Symptom | Cause | Fix |
78
55
  | --- | --- | --- |
79
- | `release-unavailable`, 422 | Unknown or retired `appId` or `appReleaseId`. | Check the IDs against `sm.catalog.get()`. Omit `appReleaseId` for the default release. |
56
+ | `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
57
  | `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. |
58
+ | `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
59
  | `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`. |
60
+ | `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. |
61
+ | `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`. |
62
+ | `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. |
63
+ | `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. |
64
+ | `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()`. |
65
+ | `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. |
66
+ | `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
67
  | 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. |
88
68
  | `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
69