@usestatemachines/sdk 0.11.0 → 0.12.1

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 (105) hide show
  1. package/README.md +126 -21
  2. package/agents/SKILL.md +33 -11
  3. package/agents/index.js +7 -7
  4. package/agents/llms.txt +14 -11
  5. package/agents/prompt.md +4 -4
  6. package/esm/extensions/client.d.ts +72 -8
  7. package/esm/extensions/client.js +89 -5
  8. package/esm/extensions/credentials.js +4 -0
  9. package/esm/extensions/environment.d.ts +18 -2
  10. package/esm/extensions/errors.d.ts +33 -0
  11. package/esm/extensions/errors.js +53 -0
  12. package/esm/extensions/import-data.d.ts +43 -0
  13. package/esm/extensions/import-data.js +88 -0
  14. package/esm/extensions/index.d.ts +4 -3
  15. package/esm/extensions/index.js +1 -1
  16. package/esm/extensions/packaging.d.ts +1 -0
  17. package/esm/extensions/packaging.js +1 -0
  18. package/esm/extensions/zip.d.ts +8 -0
  19. package/esm/extensions/zip.js +121 -0
  20. package/esm/funcs/environments-actors.d.ts +11 -0
  21. package/esm/funcs/environments-actors.js +102 -0
  22. package/esm/funcs/get-import-file.d.ts +12 -0
  23. package/esm/funcs/get-import-file.js +97 -0
  24. package/esm/funcs/list-import-files.d.ts +12 -0
  25. package/esm/funcs/list-import-files.js +98 -0
  26. package/esm/funcs/upload-import.d.ts +12 -0
  27. package/esm/funcs/upload-import.js +96 -0
  28. package/esm/lib/config.d.ts +2 -2
  29. package/esm/lib/config.js +2 -2
  30. package/esm/models/absent-record.d.ts +7 -3
  31. package/esm/models/absent-record.js +3 -2
  32. package/esm/models/actor.d.ts +3 -0
  33. package/esm/models/app-user.d.ts +17 -0
  34. package/esm/models/app-user.js +18 -0
  35. package/esm/models/credentials.d.ts +6 -1
  36. package/esm/models/credentials.js +2 -0
  37. package/esm/models/environment.d.ts +4 -4
  38. package/esm/models/environment.js +3 -4
  39. package/esm/models/error.d.ts +7 -0
  40. package/esm/models/error.js +4 -0
  41. package/esm/models/filled-field.d.ts +14 -0
  42. package/esm/models/filled-field.js +16 -0
  43. package/esm/models/import-actor.d.ts +16 -0
  44. package/esm/models/import-actor.js +15 -0
  45. package/esm/models/import-file.d.ts +56 -0
  46. package/esm/models/import-file.js +67 -0
  47. package/esm/models/import-folder.d.ts +56 -0
  48. package/esm/models/import-folder.js +75 -0
  49. package/esm/models/import-problem.d.ts +4 -4
  50. package/esm/models/import-problem.js +1 -1
  51. package/esm/models/index.d.ts +18 -1
  52. package/esm/models/index.js +18 -1
  53. package/esm/models/known-difference.d.ts +21 -0
  54. package/esm/models/known-difference.js +16 -0
  55. package/esm/models/launch-ui-session.d.ts +12 -2
  56. package/esm/models/launch-ui-session.js +2 -1
  57. package/esm/models/made-id.d.ts +22 -0
  58. package/esm/models/made-id.js +17 -0
  59. package/esm/models/matched-record.d.ts +22 -0
  60. package/esm/models/matched-record.js +17 -0
  61. package/esm/models/object-load.d.ts +21 -0
  62. package/esm/models/object-load.js +17 -0
  63. package/esm/models/operations/create-credentials.d.ts +10 -0
  64. package/esm/models/operations/create-credentials.js +1 -0
  65. package/esm/models/operations/get-import-file.d.ts +13 -0
  66. package/esm/models/operations/get-import-file.js +12 -0
  67. package/esm/models/operations/index.d.ts +4 -0
  68. package/esm/models/operations/index.js +4 -0
  69. package/esm/models/operations/list-actors.d.ts +32 -0
  70. package/esm/models/operations/list-actors.js +26 -0
  71. package/esm/models/operations/list-import-files.d.ts +15 -0
  72. package/esm/models/operations/list-import-files.js +13 -0
  73. package/esm/models/operations/list-snapshots.d.ts +2 -2
  74. package/esm/models/operations/list-snapshots.js +1 -1
  75. package/esm/models/operations/upload-import.d.ts +13 -0
  76. package/esm/models/operations/upload-import.js +17 -0
  77. package/esm/models/problem-counts.d.ts +17 -0
  78. package/esm/models/problem-counts.js +14 -0
  79. package/esm/models/received-import.d.ts +9 -0
  80. package/esm/models/received-import.js +13 -0
  81. package/esm/models/record-source.d.ts +13 -0
  82. package/esm/models/{absent-record-source.js → record-source.js} +3 -3
  83. package/esm/models/snapshot-app-full-load-counts.d.ts +57 -0
  84. package/esm/models/snapshot-app-full-load-counts.js +25 -0
  85. package/esm/models/snapshot-app-full-load.d.ts +101 -0
  86. package/esm/models/snapshot-app-full-load.js +42 -0
  87. package/esm/models/snapshot-app-limited-load-counts.d.ts +16 -0
  88. package/esm/models/snapshot-app-limited-load-counts.js +14 -0
  89. package/esm/models/snapshot-app-limited-load.d.ts +18 -0
  90. package/esm/models/snapshot-app-limited-load.js +16 -0
  91. package/esm/models/snapshot-app-load-counts.d.ts +13 -0
  92. package/esm/models/snapshot-app-load-counts.js +15 -0
  93. package/esm/models/snapshot-app-load.d.ts +5 -12
  94. package/esm/models/snapshot-app-load.js +6 -6
  95. package/esm/models/snapshot-list-item.d.ts +53 -0
  96. package/esm/models/snapshot-list-item.js +42 -0
  97. package/esm/models/snapshot.d.ts +1 -1
  98. package/esm/models/version.d.ts +3 -0
  99. package/esm/sdk/environments.d.ts +1 -0
  100. package/esm/sdk/environments.js +4 -0
  101. package/esm/sdk/sdk.d.ts +3 -0
  102. package/esm/sdk/sdk.js +12 -0
  103. package/openapi.json +1868 -510
  104. package/package.json +8 -4
  105. package/esm/models/absent-record-source.d.ts +0 -13
package/README.md CHANGED
@@ -67,14 +67,16 @@ dates, and API key kind. Expiry dates are `Date` objects when present, or `null`
67
67
  | `sm` | `me()` |
68
68
  | `sm.apps` | `list(query?)`, `versions(appId, query?)` |
69
69
  | `sm.workspaces` | `list(query?)`, `get(id)` |
70
- | `sm.environments` | `create(input, options?)`, `list(query?)`, `get(id)`, `update(id, { name?, labels? })`, `delete(id)`, `wait(id, { status })`, `connect(id, appName, { actorId?, services? }?)` |
71
- | `sm.snapshots` | `create({ environmentId, name? } \| { importId, name? }, options?)`, `list(query?)`, `get(id)`, `update(id, { name })`, `delete(id)`, `wait(id, { status })` |
70
+ | `sm.environments` | `create(input, options?)`, `list(query?)`, `get(id)`, `update(id, { name?, labels? })`, `delete(id)`, `wait(id, { status })`, `connect(id, appName, { as?, services? }?)`, `actors(id, appName, { search?, limit?, cursor? }?)`, `launchUiSession(id, appName, input)`, `revokeUiSession(id, appName, sessionId)` |
71
+ | `sm.snapshots` | `create({ environmentId, name? } \| { importId, name? } \| { import, name?, workspaceId? }, options?)`, `list(query?)`, `get(id)`, `update(id, { name })`, `delete(id)`, `wait(id, { status })` |
72
72
  | `sm.imports` | `upload(zip, options?)`, `list(query?)`, `get(id)`, `delete(id)` |
73
73
  | `sm.requests` | `list(query)`, `get(id)`, `stats(query)`, `body(id, 'request' \| 'response')` |
74
74
  | `sm.auditEvents` | `list(query?)` |
75
75
 
76
- Every environment the SDK returns carries bound helpers: `env.connect(appName)`, `env.delete()`, `env.refresh()` and `env.snapshots.create({ name? })`. They are
77
- not enumerable, so `JSON.stringify(env)` holds only the API's fields.
76
+ Every environment the SDK returns carries bound helpers: `env.connect(appName)`, `env.connectAll({ as })`,
77
+ `env.actors(appName)`, `env.launchUiSession(appName, input)`, `env.revokeUiSession(appName, sessionId)`,
78
+ `env.delete()`, `env.refresh()` and `env.snapshots.create({ name? })`. They are not enumerable, so
79
+ `JSON.stringify(env)` holds only the API's fields.
78
80
 
79
81
  ## Apps
80
82
 
@@ -111,13 +113,14 @@ as `setup.preset`.
111
113
  checks `services` again when the environment starts, and a value that it refuses fails the start
112
114
  with `failure.code: 'invalid_services'`.
113
115
 
114
- `sm.apps.list()` lists the apps. `sm.apps.versions(appId)` lists each version's services and
115
- actors. A new environment runs each app's default version.
116
+ `sm.apps.list()` lists the apps. `sm.apps.versions(appId)` lists each version's services, and its
117
+ `actors`: the users of the app's seed, for display. A new environment runs each app's default
118
+ version.
116
119
 
117
120
  ## Credentials
118
121
 
119
- `env.connect(appName, { actorId?, services? })` returns the API's `Credentials`: `token`,
120
- `services`, `expiresAt` and `login`, plus `fetch(path, init?)` and `service(id)`.
122
+ `env.connect(appName, { as?, services? })` returns the API's `Credentials`: `actorId`, `actor`,
123
+ `token`, `services`, `expiresAt` and `login`, plus `fetch(path, init?)` and `service(id)`.
121
124
 
122
125
  - Each entry of `services` has an `id`, a `url` and the `headers` that every request to that URL
123
126
  needs. A service's `url` is the app URL followed by the service's `path`. The `api` service is
@@ -137,8 +140,18 @@ actors. A new environment runs each app's default version.
137
140
  `SessionHeader/sessionId` yourself.
138
141
  - `fetch` never sends your API key, never follows redirects, never retries and only reaches paths
139
142
  under the `api` service URL.
140
- - `actorId` defaults to the app's default actor (for Salesforce, the administrator). Pick another
141
- from the version's `actors` to act with that user's permissions.
143
+ - `as` names the user to act as, by email, username or ID, such as
144
+ `env.connect('salesforce', { as: 'alice.park@acme.example' })`. Any user who can sign in to the
145
+ running app can act, including a user that an import or a program created. Without `as`, the
146
+ credentials act as the app's default user. When the app's data came from an import whose
147
+ `actors` names users for the app, the default user is the first of them. Otherwise it is the
148
+ app's administrator. `actorId` is the old name of `as`.
149
+ - `actor` holds the `id`, `name`, `email`, `username`, `role` and `default` of the user that the
150
+ credentials act as. `actorId` holds the app's own ID of that user, whatever `as` named.
151
+ - A user that the app does not have throws `ActorNotFoundError`, whose `nearest` lists the users
152
+ who can act that are nearest to `as`, each with the fields of `actor`. To act as one of them,
153
+ send its `id` as `as`. A user that the app does not let sign in now, such as an inactive user,
154
+ throws `ActorCannotSignInError`.
142
155
  - `login` holds what the vendor's own login call needs, when the version declares one for the
143
156
  actor.
144
157
  - `token` and each service's `headers` are secrets. To use another client, such as jsforce, send
@@ -147,14 +160,58 @@ actors. A new environment runs each app's default version.
147
160
  Environments created with `tokenInUrl: true` carry the environment token in each service's `url`
148
161
  instead of in its `headers`.
149
162
 
163
+ `env.actors(appName, { search?, limit?, cursor? })` returns one page of the users who can act in
164
+ the app, the default user first. `search` matches part of a name, an email or a username in any
165
+ case, or a whole ID.
166
+
167
+ `env.connectAll({ as })` connects to every app of the environment as the user that `as` names. It
168
+ returns `credentials`, by app name, for each app that lets the user act, and `errors`, with the
169
+ `ActorNotFoundError` or `ActorCannotSignInError` of each other app. Any other error throws.
170
+
150
171
  ## Browser UI sessions
151
172
 
173
+ A UI session signs one browser in to a browser service of an app, as one actor. Each app of an
174
+ environment lists its browser services in `browserServices`, each with its `serviceId` and the
175
+ `documents` routes that a session can open first.
176
+
177
+ Your server launches the session with the API key, then gives the launch only to the browser of
178
+ the user it is for:
179
+
180
+ ```ts
181
+ import { StateMachines } from '@usestatemachines/sdk';
182
+
183
+ const sm = new StateMachines();
184
+ const env = await sm.environments.get('env_...');
185
+ const launch = await env.launchUiSession('linear', {
186
+ serviceId: 'ui',
187
+ mode: 'page',
188
+ launcherOrigin: 'https://app.example.com',
189
+ entryPath: '/',
190
+ as: 'riley@example.com',
191
+ });
192
+ // Send the launch to the browser of that user, such as in the response of its own request.
193
+ ```
194
+
195
+ `launcherOrigin` is the exact origin of the page that posts the grant. `entryPath` is the path that
196
+ the session opens first, with an optional query, and it must match one of the service's document
197
+ routes. `as` names the actor as `connect` takes it. The launch holds a one-use `grant` that
198
+ expires soon, at `grantExpiresAt`, and no reusable credential. The session ends at `expiresAt` or
199
+ when your server calls `env.revokeUiSession(appName, launch.sessionId)`.
200
+
152
201
  `@usestatemachines/sdk/ui` is the only part of the SDK for a browser. It holds no client and no
153
- credential. Your server launches a UI session with the API key, then gives the launch only to
154
- the browser of the user it is for. In that browser, `submitLaunch(launch, target)` posts the
155
- launch's one-use grant into `target`: an iframe for an `embedded` launch, or a window for a
156
- `page` launch. Open the window in the click handler, such as with
157
- `window.open('about:blank', name)`, so the browser does not block it.
202
+ credential. In the browser, `submitLaunch(launch, target)` posts the launch's grant into
203
+ `target`: an iframe for an `embedded` launch, or a window for a `page` launch. Open the window
204
+ in the click handler so the browser does not block it:
205
+
206
+ ```ts
207
+ import { submitLaunch } from '@usestatemachines/sdk/ui';
208
+
209
+ button.addEventListener('click', async () => {
210
+ const target = window.open('about:blank', 'linear');
211
+ const launch = await (await fetch('/launch-linear', { method: 'POST' })).json();
212
+ submitLaunch(launch, target);
213
+ });
214
+ ```
158
215
 
159
216
  `listenToSession(launch, iframe, onMessage, { signal })` calls `onMessage` with `ready`,
160
217
  `failure` and `logout`. It passes only messages from the session's origin, from that iframe and
@@ -163,6 +220,9 @@ loaded to your page. So wait for `ready` with a timeout, and on `failure` or the
163
220
  a `page` launch. The page that posts the grant must send its `Origin`, so do not serve it with
164
221
  `Referrer-Policy: no-referrer`.
165
222
 
223
+ To open a browser service from a terminal or a coding agent, run `statemachines open <environment>`.
224
+ The CLI serves the page that posts the grant on `127.0.0.1`.
225
+
166
226
  ## Snapshots
167
227
 
168
228
  ```ts
@@ -182,11 +242,51 @@ const copy = await sm.environments.create({ snapshotId: built.id });
182
242
  ```
183
243
 
184
244
  A snapshot from a `ready` import holds every app of the import, each on its default version,
185
- with empty setup and services. Each app's `load` counts the records of the import that did not
186
- load, as `absentCount`, and lists the first of them, as `absent`. An app that refuses the import
187
- fails the snapshot with `failure.code: 'invalid_import'`, and `SnapshotFailedError.problems`
188
- lists its problems. An environment from the snapshot does not load the import, so it starts
189
- after the import is deleted.
245
+ with empty setup and services. Each app's `load` is the report of its load: the counts per
246
+ object, the IDs that the app made, the records that took over seed records, the filled values,
247
+ the actors, the seed users that the load deactivated, the records that did not load, the known
248
+ differences, and the problems. Each list holds its first items beside the count of all of them,
249
+ such as `absent` and `absentCount`. `actors` keeps the order of the import's `actors`, so the
250
+ first is the default user. A record that wrote a text ID, such as `acme`, holds it in `text`.
251
+ `sm.snapshots.list()` answers each app's `load` with every count and no list, so read one
252
+ snapshot with `sm.snapshots.get(id)` for the lists. An app that
253
+ refuses the import fails the snapshot with `failure.code: 'invalid_import'`, and
254
+ `SnapshotFailedError.problems` lists its problems. An environment from the snapshot does not
255
+ load the import, so it starts after the import is deleted.
256
+
257
+ ```ts
258
+ const accounts = await sm.snapshots.create({
259
+ import: {
260
+ format: 'statemachines.import/v1',
261
+ name: 'q3-accounts',
262
+ apps: {
263
+ salesforce: {
264
+ records: {
265
+ Account: [{ Id: '001g5000003wfcFAAQ', Name: 'Brightwater Shipping' }],
266
+ },
267
+ },
268
+ },
269
+ },
270
+ });
271
+ ```
272
+
273
+ `{ import }` takes an import as one object, the manifest with each app's records in it. The SDK
274
+ writes the object as a zip file, uploads it as `sm.imports.upload` does, and builds the snapshot
275
+ from the import, in one call. Each app holds its manifest settings, such as `loadMode`, and its
276
+ `records` by object. `content` holds the bytes of the files that the app's records name, by path
277
+ inside the app's `content/` folder, such as Box's `file_version/41500000004`.
278
+
279
+ The zip file holds the manifest as `statemachines.json`, without `records` and `content`. It
280
+ holds the records of each object as `<app>/records/<Object>.jsonl`, one record per line. So a
281
+ problem at line N of `salesforce/records/Account.jsonl` names
282
+ `apps.salesforce.records.Account[N - 1]`. A problem of `statemachines.json` points into the
283
+ manifest, such as `/apps/salesforce/loadMode` for `apps.salesforce.loadMode`.
284
+
285
+ Data that the SDK cannot write as a zip file, such as records that are not plain objects, throws
286
+ `StateMachinesError` with `code: 'invalid_request'` and `fields` before any request. An import
287
+ that its check leaves `invalid` throws `ImportInvalidError`, and the SDK creates no snapshot. The
288
+ import and the snapshot share one `idempotencyKey`, so a retry with the same key and the same
289
+ data finds both.
190
290
 
191
291
  ## Waiting
192
292
 
@@ -224,6 +324,9 @@ response body.
224
324
  | `StateMachinesError` | any failed call | `code`, `status`, `requestId`, `fields`, `retryAfterMs` |
225
325
  | `EnvironmentFailedError` (a `StateMachinesError`) | a waited environment failed | `environment` |
226
326
  | `SnapshotFailedError` (a `StateMachinesError`) | a waited snapshot failed | `snapshot` and `problems`. `problems` is empty unless an app refused the import that the snapshot was built from. |
327
+ | `ImportInvalidError` (a `StateMachinesError`) | the check of the import that `snapshots.create({ import })` uploaded left it `invalid` | `import` and `problems` |
328
+ | `ActorNotFoundError` (a `StateMachinesError`) | the app has no user that `as` names | `appName` and `nearest` |
329
+ | `ActorCannotSignInError` (a `StateMachinesError`) | the app does not let the user that `as` names sign in now | `appName` |
227
330
  | `UnexpectedStatusError` | a waited resource cannot reach the status without another call | `resource`, `status` |
228
331
  | `WaitTimeoutError` | a wait passed `timeoutMs` | `resource` (the last read), `timeoutMs` |
229
332
 
@@ -233,12 +336,14 @@ response body.
233
336
  | `unauthenticated` | 401 | A missing, expired or revoked key. |
234
337
  | `forbidden` | 403 | The key lacks the permission. |
235
338
  | `not_found` | 404 | No such resource in this workspace. |
236
- | `invalid_status` | 409 | The status does not allow the call. |
339
+ | `invalid_status` | 409 | The status does not allow the call. Credentials need a `running` environment. |
237
340
  | `snapshot_in_progress` | 409 | The environment has a snapshot in `saving`. |
238
341
  | `idempotency_key_reused` | 409 | The same `idempotencyKey` with a different input. |
239
342
  | `workspace_archived` | 409 | A write in an archived workspace. |
343
+ | `actor_cannot_sign_in` | 409 | The app does not let the user sign in now. `fields.as` gives the reason. |
240
344
  | `body_too_large` | 413 | The request body is over the API's size limit, which the message names. |
241
345
  | `version_unavailable` | 422 | The app has no default version. |
346
+ | `actor_not_found` | 422 | The app has no user with that email, username or ID. `nearest` lists the nearest users. |
242
347
  | `environment_limit_reached` | 429 | Too many environments `starting` or `running`. |
243
348
  | `internal_error` | 500 | An error in State Machines. |
244
349
  | `unavailable` | 503 | Retried automatically. |
package/agents/SKILL.md CHANGED
@@ -15,17 +15,17 @@ The SDK reads `STATEMACHINES_API_KEY` from the process environment. It does not
15
15
 
16
16
  If the project's existing command loads `STATEMACHINES_API_KEY`, keep using that command and secret loader. Keep the same credential loader, CLI profile and API origin across setup commands. Call `sm.me()` in that process to verify access and workspace scope. `me.limits` can be null.
17
17
 
18
- Otherwise, run `npx @usestatemachines/cli@0.1.1 whoami --json`. Confirm the returned account and organization match the task, then use the workspace commands below. If the CLI credential is missing, expired or revoked, complete this login:
18
+ Otherwise, run `npx @usestatemachines/cli@0.1.4 whoami --json`. Confirm the returned account and organization match the task, then use the workspace commands below. If the CLI credential is missing, expired or revoked, complete this login:
19
19
 
20
- 1. Run `npx @usestatemachines/cli@0.1.1 login --no-wait --json`. For an expired or revoked saved session, add `--yes` to replace that invalid session in the same profile. Do not replace a valid session for a different account.
20
+ 1. Run `npx @usestatemachines/cli@0.1.4 login --no-wait --json`. For an expired or revoked saved session, add `--yes` to replace that invalid session in the same profile. Do not replace a valid session for a different account.
21
21
  2. Show the returned `verificationUrl` and `userCode`. Ask the user to approve only the login you just started. A matching code does not identify the computer requesting access. Wait for the user to approve the login.
22
- 3. After that approval, run `npx @usestatemachines/cli@0.1.1 login --resume --json` with the same profile used to start the login. Resume that attempt instead of starting another login. If it expired, offer a new login.
22
+ 3. After that approval, run `npx @usestatemachines/cli@0.1.4 login --resume --json` with the same profile used to start the login. Resume that attempt instead of starting another login. If it expired, offer a new login.
23
23
 
24
- If a pending login's approval URL or code is unavailable, ask before recovering it. Explain that recovery may finish an already-approved login. After confirmation, run `npx @usestatemachines/cli@0.1.1 login --resume --json --stream` with the same profile and API origin. Keep that process running and show its URL and code. Request approval only for a recognized login. Wait for `signed-in` before continuing setup. Do not start another login or resume process. If streaming output is unavailable, report the blocker without reading credential files.
24
+ If a pending login's approval URL or code is unavailable, ask before recovering it. Explain that recovery may finish an already-approved login. After confirmation, run `npx @usestatemachines/cli@0.1.4 login --resume --json --stream` with the same profile and API origin. Keep that process running and show its URL and code. Request approval only for a recognized login. Wait for `signed-in` before continuing setup. Do not start another login or resume process. If streaming output is unavailable, report the blocker without reading credential files.
25
25
 
26
- If a command returns `cli.not_linked`, run `npx @usestatemachines/cli@0.1.1 workspaces list --json`. Ask the user which workspace to use when there is more than one. Run `npx @usestatemachines/cli@0.1.1 link --workspace <workspace-id> --json` with the selected ID.
26
+ If a command returns `cli.not_linked`, run `npx @usestatemachines/cli@0.1.4 workspaces list --json`. Ask the user which workspace to use when there is more than one. Run `npx @usestatemachines/cli@0.1.4 link --workspace <workspace-id> --json` with the selected ID.
27
27
 
28
- Run the user's existing command with `npx @usestatemachines/cli@0.1.1 run -- <command>`. The child receives a workspace key. The CLI keeps the credential out of its output.
28
+ Run the user's existing command with `npx @usestatemachines/cli@0.1.4 run -- <command>`. The child receives a workspace key. The CLI keeps the credential out of its output.
29
29
 
30
30
  If access is denied, the API origin differs, or the workspace differs from the task, report the blocker. Do not invite or remove members, or change permissions.
31
31
 
@@ -35,7 +35,7 @@ Never print, log or commit API keys, `credentials.token`, the `headers` of `cred
35
35
 
36
36
  Apply the rest of this skill once the user has chosen a task. `sm.apps.list()` returns app IDs and each app's `defaultVersionId`. Every list returns `{ data, nextCursor }`. Follow cursors until you find the entry or `nextCursor` is null.
37
37
 
38
- Read the app's default version from `sm.apps.versions(appId)` before you write code. It is the current guidance for that app. Each of its `services` has an `id`, a `visibility`, `limitations` and a `prompt`, and a service that serves MCP lists its `servers` with their `tools`. Its `actors` are the app's users, each with an `id`, a `name` and a `summary`. Check the `limitations` against the behavior under test, as [known differences](https://app.usestatemachines.com/docs/apps/known-differences.md) does for Salesforce. A replica does not import a customer's data or run arbitrary customer automation.
38
+ Read the app's default version from `sm.apps.versions(appId)` before you write code. It is the current guidance for that app. Each of its `services` has an `id`, a `visibility`, `limitations` and a `prompt`, and a service that serves MCP lists its `servers` with their `tools`. Its `actors` are users of the app's seed, for display. They do not say who can act. Check the `limitations` against the behavior under test, as [known differences](https://app.usestatemachines.com/docs/apps/known-differences.md) does for Salesforce. A replica never calls the vendor and does not run arbitrary customer automation. An environment holds a customer's org data only when it starts from a snapshot created from an import of that data.
39
39
 
40
40
  - Salesforce: the `api` service serves REST, Bulk API 2.0 and SOAP. Its `mcp` service is `preview`.
41
41
  - Box: the `api` service serves the Box Platform API, uploads and OAuth. Its `mcp` service is `preview`.
@@ -45,18 +45,38 @@ Read the app's default version from `sm.apps.versions(appId)` before you write c
45
45
 
46
46
  Create only the apps the task needs. A string such as `'linear'` is both the app ID and the app's name in the environment. Use `{ appId, name, setup, services }` to choose a name, run two copies of one app, pass `setup`, or configure a service. For example, `services` sets which MCP servers and tools the `mcp` service serves. Later calls take the name. Set `tokenInUrl: true` only for a client that cannot send headers.
47
47
 
48
- `env.connect(name)` acts as the app's default actor and lists each `public` service. Pass `{ services: ['mcp'] }` to also list a `preview` service. To act as another user, pass an `actorId` from the `actors` of the exact version that `env.apps` names for the app. Never use a role label, a workspace member ID, or the first version in a list.
48
+ `env.connect(name)` acts as the app's default user and lists each `public` service. Pass `{ services: ['mcp'] }` to also list a `preview` service. To act as another user of the app, pass `as` with the user's email, username or ID, such as `env.connect('salesforce', { as: 'alice.park@acme.example' })`. Any user who can sign in to the running app can act, including a user that the test created. `credentials.actor` names the user that the credentials act as. Never pass a role label or a workspace member ID.
49
49
 
50
+ - `env.actors(name, { search })` pages through the users who can act, the default user first. Use it to find a user when the task does not name one by email, username or ID.
51
+ - `env.connectAll({ as })` connects to every app of the environment as one person. Its `credentials` hold each app that lets the user act, by app name. Its `errors` hold the `ActorNotFoundError` or `ActorCannotSignInError` of each other app. Any other error throws.
52
+ - A user that the app does not have throws `ActorNotFoundError`, and `err.nearest` lists the nearest users who can act. A user that the app does not let sign in now, such as an inactive user, throws `ActorCannotSignInError`.
50
53
  - `credentials.fetch(path)` calls the `api` service with its headers and never follows redirects. It throws when the credentials list no `api` service, as for Linear.
51
54
  - For MCP, `credentials.service('mcp').servers` lists the MCP servers, and `.server(name)` returns one server's `url` and `headers` to give the MCP client.
52
55
  - An external client must send a service's `headers` only to that service's `url`, with redirects off. For Salesforce SOAP, put the actor token in `SessionHeader/sessionId`. See [credentials](https://app.usestatemachines.com/docs/environments/credentials.md).
53
- - On a `tokenInUrl` environment, a Salesforce or Box MCP request without an `Authorization` header acts as the default actor. To act as `actorId`, send the service's headers. Linear refuses an MCP request without them, so a Linear MCP client always sends the `mcp` service's headers.
56
+ - On a `tokenInUrl` environment, an MCP request without an `Authorization` header acts as the app's default user. To act as the user of the credentials, send the service's headers.
54
57
 
55
58
  Point the existing integration's test transport at these URLs and credentials. If that needs a broader change, explain it before you expand the task.
56
59
 
60
+ ## Use an app in a browser
61
+
62
+ Use a browser only when the task needs an app's UI. Each entry of `env.apps` lists the app's UI in `browserServices`, with each service's `id` and the `documents` routes that a session can open first. Linear's `ui` service is one. A UI session signs one browser in to the service as one actor.
63
+
64
+ 1. Create the environment with `{ wait: false }` and keep its ID. `open` waits for `running`, so you do not need to wait first.
65
+ 2. Start `npx @usestatemachines/cli@0.1.4 open <environment-id> --json` in the background. It opens the only app with a browser service, and that app's only browser service. When the environment has more, name the app as the second argument and the service with `--service`. `--actor` takes a user as `connect --actor` does.
66
+ 3. Read the line `Open http://127.0.0.1:<port>/<token> in the browser ...` from the command's stderr. While the environment starts, the line `Waiting for <environment-id> to run (now starting).` comes first. If the environment fails or is deleted first, the command fails and prints no link. Open that URL in your browser tool before you do anything else. The command launches the session only when the browser opens the URL, and the browser then loads the app as the actor.
67
+ 4. Use the app in that browser tab. The command exits once the browser opened the link, and its JSON result names the session, the actor, the path that it opened and when the session expires.
68
+ 5. Read each write back through the app's MCP server or API with `env.connect`. Do not trust the page alone.
69
+ 6. Delete the environment. The session ends with it.
70
+
71
+ The link works once. While the command runs, a second visit shows that the link was used. After the command exits, the link no longer answers. Run `open` again for a new link. The session's grant lives for a short time, which is why the command launches the session only when the browser opens the link. If no browser opens the link within the `--timeout` seconds, the command fails with `cli.wait_timeout`.
72
+
73
+ A page titled `Access required` or `Open this app again` means that the session ended. Run `open` again for a new link. If `open` then fails because the environment is deleted, create a new environment.
74
+
75
+ To open one record, pass its URL to `--path`. MCP tools return the vendor's URLs, such as `https://linear.app/<workspace>/issue/<id>/<slug>` for a Linear issue, and the replica serves the same paths. The command keeps the URL's path and query and opens them in the replica. A vendor URL that you open as it is goes to the vendor, not to the replica. Without `--path`, the command opens the service's first route without parameters. The API refuses a path that matches none of the service's `documents` routes.
76
+
57
77
  ## Seed your own data
58
78
 
59
- Create the data a test reads before the test runs. Write it through the app's own API or MCP tools as the actor that owns it: Salesforce REST, composite or Bulk API 2.0, Linear MCP create tools, or Box uploads. A Linear workspace starts with one team and no issues. Use `setup` only for identity and configuration. To reuse seeded data, save it with `env.snapshots.create({ name })`, then create each test's environment from `{ snapshotId }`. See [isolated tests](https://app.usestatemachines.com/docs/sdk-api/testing.md).
79
+ Create the data a test reads before the test runs. Write it through the app's own API or MCP tools as the actor that owns it: Salesforce REST, composite or Bulk API 2.0, Linear MCP create tools, or Box uploads. A Linear workspace starts with one team and no issues. Use `setup` only for identity and configuration. To start from records, users and files that you write in one file, build a snapshot from an import, as [build a snapshot from an import](https://app.usestatemachines.com/docs/snapshots/import.md) shows. To reuse seeded data, save it with `env.snapshots.create({ name })`, then create each test's environment from `{ snapshotId }`. See [isolated tests](https://app.usestatemachines.com/docs/sdk-api/testing.md).
60
80
 
61
81
  ## Verify the result
62
82
 
@@ -74,7 +94,7 @@ When `sm.me().limits` is non-null, it gives the cap on concurrent environments a
74
94
 
75
95
  ## Optional connection check
76
96
 
77
- Run this only when the user asks to verify the connection. It proves the connection and cleanup, not an existing integration's behavior. Save it in the project's examples or test location. Use the existing TypeScript runner and secret loader, or pass that runner through `npx @usestatemachines/cli@0.1.1 run -- <command>`. For an empty Node.js project, save `state-quickstart.mts` and run `npx @usestatemachines/cli@0.1.1 run -- npx tsx state-quickstart.mts`.
97
+ Run this only when the user asks to verify the connection. It proves the connection and cleanup, not an existing integration's behavior. Save it in the project's examples or test location. Use the existing TypeScript runner and secret loader, or pass that runner through `npx @usestatemachines/cli@0.1.4 run -- <command>`. For an empty Node.js project, save `state-quickstart.mts` and run `npx @usestatemachines/cli@0.1.4 run -- npx tsx state-quickstart.mts`.
78
98
 
79
99
  ```ts
80
100
  import { StateMachines } from '@usestatemachines/sdk';
@@ -104,6 +124,8 @@ Management failures throw SDK errors. App HTTP failures are returned as `Respons
104
124
  | `unauthenticated` | The key is missing, expired or revoked. Fix the credential in the process that runs the task. |
105
125
  | `invalid_request` | Read `err.fields` and the installed method signature. |
106
126
  | `version_unavailable` | Discover the app ID and available versions. Stop if the required version is unavailable. |
127
+ | `ActorNotFoundError` | The app has no user with that email, username or ID. Check `err.nearest` or `env.actors(appName, { search })`. Act as another user only when the task allows it. |
128
+ | `ActorCannotSignInError` | The app does not let the user sign in now, for the reason in `err.fields.as`. Report it. Do not act as another user to make the test pass. |
107
129
  | `environment_limit_reached` | Clean up only this task's known leftovers, or report the capacity blocker. |
108
130
  | `EnvironmentFailedError` | Read `err.environment.failure.code`: `start_failed`, `pause_removed`, `version_unavailable`, `invalid_setup` or `invalid_services`. Clean up the owned environment. |
109
131
  | `WaitTimeoutError` or abort | Use the kept environment ID. A non-null `WaitTimeoutError.resource` also carries it. If neither an ID nor a replay key is available, report unresolved cleanup. Do not guess from a list. |