@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.
- package/README.md +175 -75
- package/agents/SKILL.md +24 -44
- package/agents/index.js +7 -7
- package/agents/llms.txt +18 -39
- package/agents/prompt.md +11 -34
- package/esm/extensions/apps.d.ts +8 -0
- package/esm/extensions/apps.js +23 -0
- package/esm/extensions/client.d.ts +33 -3
- package/esm/extensions/client.js +144 -28
- package/esm/extensions/errors.d.ts +24 -5
- package/esm/extensions/errors.js +3 -2
- package/esm/extensions/index.d.ts +3 -1
- package/esm/extensions/index.js +1 -0
- package/esm/extensions/running-environment.d.ts +29 -0
- package/esm/extensions/running-environment.js +35 -0
- package/esm/extensions/signal.d.ts +1 -2
- package/esm/extensions/signal.js +9 -4
- package/esm/extensions/wait.d.ts +1 -0
- package/esm/extensions/wait.js +1 -1
- package/esm/funcs/create-environment-definition.d.ts +11 -0
- package/esm/funcs/create-environment-definition.js +94 -0
- package/esm/lib/config.d.ts +2 -2
- package/esm/lib/config.js +2 -2
- package/esm/models/connection-secret.d.ts +16 -0
- package/esm/models/connection-secret.js +13 -0
- package/esm/models/connection.d.ts +14 -0
- package/esm/models/connection.js +14 -0
- package/esm/models/error-response.d.ts +4 -0
- package/esm/models/error-response.js +4 -0
- package/esm/models/operations/create-environment-definition.d.ts +143 -0
- package/esm/models/operations/create-environment-definition.js +90 -0
- package/esm/models/operations/index.d.ts +1 -0
- package/esm/models/operations/index.js +1 -0
- package/esm/models/operations/reveal-connection.d.ts +26 -4
- package/esm/models/operations/reveal-connection.js +28 -2
- package/esm/sdk/sdk.d.ts +3 -1
- package/esm/sdk/sdk.js +5 -0
- package/openapi.json +394 -7
- 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):
|
|
4
|
-
|
|
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
|
|
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
|
-
|
|
19
|
-
const
|
|
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
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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 (!
|
|
37
|
-
throw new Error('
|
|
69
|
+
if (!rep) {
|
|
70
|
+
throw new Error('No sales representative in this release.');
|
|
38
71
|
}
|
|
39
72
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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,
|
|
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(
|
|
235
|
-
|
|
236
|
-
|
|
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
|
|
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
|
|
283
|
-
|
|
284
|
-
|
|
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 -
|
|
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.
|
|
12
|
-
3. Open a `try` right after `
|
|
13
|
-
4. `
|
|
14
|
-
5.
|
|
15
|
-
6.
|
|
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
|
-
|
|
24
|
-
const
|
|
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
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
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
|
|
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
|
|
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:
|
|
84
|
-
| `TypeError:
|
|
85
|
-
| `
|
|
86
|
-
| `
|
|
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
|
|