@usestatemachines/sdk 0.1.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 (203) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +311 -0
  3. package/agents/SKILL.md +90 -0
  4. package/agents/index.d.ts +13 -0
  5. package/agents/index.js +22 -0
  6. package/agents/llms.txt +93 -0
  7. package/agents/prompt.md +49 -0
  8. package/esm/core.d.ts +9 -0
  9. package/esm/core.js +12 -0
  10. package/esm/extensions/client.d.ts +81 -0
  11. package/esm/extensions/client.js +213 -0
  12. package/esm/extensions/connection.d.ts +42 -0
  13. package/esm/extensions/connection.js +133 -0
  14. package/esm/extensions/errors.d.ts +52 -0
  15. package/esm/extensions/errors.js +129 -0
  16. package/esm/extensions/index.d.ts +11 -0
  17. package/esm/extensions/index.js +7 -0
  18. package/esm/extensions/signal.d.ts +14 -0
  19. package/esm/extensions/signal.js +29 -0
  20. package/esm/extensions/wait.d.ts +19 -0
  21. package/esm/extensions/wait.js +102 -0
  22. package/esm/funcs/catalog-get.d.ts +11 -0
  23. package/esm/funcs/catalog-get.js +76 -0
  24. package/esm/funcs/catalog-release.d.ts +11 -0
  25. package/esm/funcs/catalog-release.js +92 -0
  26. package/esm/funcs/credential-get.d.ts +11 -0
  27. package/esm/funcs/credential-get.js +76 -0
  28. package/esm/funcs/environments-create.d.ts +11 -0
  29. package/esm/funcs/environments-create.js +94 -0
  30. package/esm/funcs/environments-events.d.ts +12 -0
  31. package/esm/funcs/environments-events.js +98 -0
  32. package/esm/funcs/environments-get-connection.d.ts +11 -0
  33. package/esm/funcs/environments-get-connection.js +96 -0
  34. package/esm/funcs/environments-get.d.ts +11 -0
  35. package/esm/funcs/environments-get.js +92 -0
  36. package/esm/funcs/environments-list.d.ts +11 -0
  37. package/esm/funcs/environments-list.js +100 -0
  38. package/esm/funcs/environments-reveal.d.ts +11 -0
  39. package/esm/funcs/environments-reveal.js +97 -0
  40. package/esm/funcs/environments-stop.d.ts +11 -0
  41. package/esm/funcs/environments-stop.js +92 -0
  42. package/esm/funcs/workspaces-events.d.ts +12 -0
  43. package/esm/funcs/workspaces-events.js +98 -0
  44. package/esm/funcs/workspaces-get.d.ts +11 -0
  45. package/esm/funcs/workspaces-get.js +92 -0
  46. package/esm/funcs/workspaces-list.d.ts +11 -0
  47. package/esm/funcs/workspaces-list.js +90 -0
  48. package/esm/hooks/hooks.d.ts +24 -0
  49. package/esm/hooks/hooks.js +79 -0
  50. package/esm/hooks/index.d.ts +2 -0
  51. package/esm/hooks/index.js +5 -0
  52. package/esm/hooks/types.d.ts +76 -0
  53. package/esm/hooks/types.js +4 -0
  54. package/esm/index.d.ts +5 -0
  55. package/esm/index.js +7 -0
  56. package/esm/lib/base64.d.ts +9 -0
  57. package/esm/lib/base64.js +30 -0
  58. package/esm/lib/config.d.ts +37 -0
  59. package/esm/lib/config.js +30 -0
  60. package/esm/lib/encodings.d.ts +54 -0
  61. package/esm/lib/encodings.js +406 -0
  62. package/esm/lib/files.d.ts +25 -0
  63. package/esm/lib/files.js +90 -0
  64. package/esm/lib/http.d.ts +66 -0
  65. package/esm/lib/http.js +208 -0
  66. package/esm/lib/logger.d.ts +5 -0
  67. package/esm/lib/logger.js +4 -0
  68. package/esm/lib/matchers.d.ts +58 -0
  69. package/esm/lib/matchers.js +201 -0
  70. package/esm/lib/primitives.d.ts +26 -0
  71. package/esm/lib/primitives.js +119 -0
  72. package/esm/lib/retries.d.ts +37 -0
  73. package/esm/lib/retries.js +156 -0
  74. package/esm/lib/schemas.d.ts +20 -0
  75. package/esm/lib/schemas.js +56 -0
  76. package/esm/lib/sdks.d.ts +62 -0
  77. package/esm/lib/sdks.js +289 -0
  78. package/esm/lib/security.d.ts +83 -0
  79. package/esm/lib/security.js +145 -0
  80. package/esm/lib/url.d.ts +4 -0
  81. package/esm/lib/url.js +23 -0
  82. package/esm/models/actor.d.ts +12 -0
  83. package/esm/models/actor.js +16 -0
  84. package/esm/models/api-profile.d.ts +17 -0
  85. package/esm/models/api-profile.js +22 -0
  86. package/esm/models/app.d.ts +14 -0
  87. package/esm/models/app.js +18 -0
  88. package/esm/models/catalog.d.ts +12 -0
  89. package/esm/models/catalog.js +15 -0
  90. package/esm/models/connection-secret.d.ts +14 -0
  91. package/esm/models/connection-secret.js +18 -0
  92. package/esm/models/connection.d.ts +26 -0
  93. package/esm/models/connection.js +30 -0
  94. package/esm/models/create-environment-input.d.ts +35 -0
  95. package/esm/models/create-environment-input.js +23 -0
  96. package/esm/models/credential-kind.d.ts +9 -0
  97. package/esm/models/credential-kind.js +10 -0
  98. package/esm/models/credential.d.ts +48 -0
  99. package/esm/models/credential.js +44 -0
  100. package/esm/models/environment-app.d.ts +42 -0
  101. package/esm/models/environment-app.js +44 -0
  102. package/esm/models/environment-lifetime.d.ts +10 -0
  103. package/esm/models/environment-lifetime.js +14 -0
  104. package/esm/models/environment.d.ts +47 -0
  105. package/esm/models/environment.js +48 -0
  106. package/esm/models/error-response.d.ts +45 -0
  107. package/esm/models/error-response.js +47 -0
  108. package/esm/models/errors/error-response.d.ts +18 -0
  109. package/esm/models/errors/error-response.js +32 -0
  110. package/esm/models/errors/http-client-errors.d.ts +43 -0
  111. package/esm/models/errors/http-client-errors.js +55 -0
  112. package/esm/models/errors/index.d.ts +6 -0
  113. package/esm/models/errors/index.js +9 -0
  114. package/esm/models/errors/response-validation-error.d.ts +25 -0
  115. package/esm/models/errors/response-validation-error.js +36 -0
  116. package/esm/models/errors/sdk-validation-error.d.ts +20 -0
  117. package/esm/models/errors/sdk-validation-error.js +51 -0
  118. package/esm/models/errors/state-machines-http-error.d.ts +18 -0
  119. package/esm/models/errors/state-machines-http-error.js +25 -0
  120. package/esm/models/errors/unexpected-response-error.d.ts +9 -0
  121. package/esm/models/errors/unexpected-response-error.js +29 -0
  122. package/esm/models/event-page.d.ts +11 -0
  123. package/esm/models/event-page.js +16 -0
  124. package/esm/models/event.d.ts +20 -0
  125. package/esm/models/event.js +22 -0
  126. package/esm/models/index.d.ts +19 -0
  127. package/esm/models/index.js +22 -0
  128. package/esm/models/operations/create-environments.d.ts +27 -0
  129. package/esm/models/operations/create-environments.js +27 -0
  130. package/esm/models/operations/get-catalog.d.ts +13 -0
  131. package/esm/models/operations/get-catalog.js +13 -0
  132. package/esm/models/operations/get-connection.d.ts +25 -0
  133. package/esm/models/operations/get-connection.js +21 -0
  134. package/esm/models/operations/get-credential.d.ts +13 -0
  135. package/esm/models/operations/get-credential.js +13 -0
  136. package/esm/models/operations/get-environment.d.ts +23 -0
  137. package/esm/models/operations/get-environment.js +20 -0
  138. package/esm/models/operations/get-release.d.ts +23 -0
  139. package/esm/models/operations/get-release.js +20 -0
  140. package/esm/models/operations/get-workspace.d.ts +23 -0
  141. package/esm/models/operations/get-workspace.js +20 -0
  142. package/esm/models/operations/index.d.ts +13 -0
  143. package/esm/models/operations/index.js +16 -0
  144. package/esm/models/operations/list-environment-events.d.ts +15 -0
  145. package/esm/models/operations/list-environment-events.js +13 -0
  146. package/esm/models/operations/list-environments.d.ts +49 -0
  147. package/esm/models/operations/list-environments.js +40 -0
  148. package/esm/models/operations/list-workspace-events.d.ts +15 -0
  149. package/esm/models/operations/list-workspace-events.js +13 -0
  150. package/esm/models/operations/list-workspaces.d.ts +32 -0
  151. package/esm/models/operations/list-workspaces.js +27 -0
  152. package/esm/models/operations/reveal-connection.d.ts +37 -0
  153. package/esm/models/operations/reveal-connection.js +29 -0
  154. package/esm/models/operations/stop-environment.d.ts +23 -0
  155. package/esm/models/operations/stop-environment.js +20 -0
  156. package/esm/models/release-actor.d.ts +11 -0
  157. package/esm/models/release-actor.js +15 -0
  158. package/esm/models/release.d.ts +26 -0
  159. package/esm/models/release.js +29 -0
  160. package/esm/models/security.d.ts +11 -0
  161. package/esm/models/security.js +11 -0
  162. package/esm/models/workspace.d.ts +16 -0
  163. package/esm/models/workspace.js +21 -0
  164. package/esm/sdk/catalog.d.ts +6 -0
  165. package/esm/sdk/catalog.js +15 -0
  166. package/esm/sdk/credential.d.ts +5 -0
  167. package/esm/sdk/credential.js +11 -0
  168. package/esm/sdk/environments.d.ts +12 -0
  169. package/esm/sdk/environments.js +35 -0
  170. package/esm/sdk/index.d.ts +1 -0
  171. package/esm/sdk/index.js +4 -0
  172. package/esm/sdk/sdk.d.ts +15 -0
  173. package/esm/sdk/sdk.js +26 -0
  174. package/esm/sdk/workspaces.d.ts +8 -0
  175. package/esm/sdk/workspaces.js +19 -0
  176. package/esm/types/async.d.ts +22 -0
  177. package/esm/types/async.js +27 -0
  178. package/esm/types/blobs.d.ts +3 -0
  179. package/esm/types/blobs.js +24 -0
  180. package/esm/types/const-date-time.d.ts +2 -0
  181. package/esm/types/const-date-time.js +9 -0
  182. package/esm/types/default-to-zero-value.d.ts +11 -0
  183. package/esm/types/default-to-zero-value.js +30 -0
  184. package/esm/types/enums.d.ts +8 -0
  185. package/esm/types/enums.js +26 -0
  186. package/esm/types/fp.d.ts +30 -0
  187. package/esm/types/fp.js +30 -0
  188. package/esm/types/index.d.ts +9 -0
  189. package/esm/types/index.js +9 -0
  190. package/esm/types/operations.d.ts +26 -0
  191. package/esm/types/operations.js +76 -0
  192. package/esm/types/primitives.d.ts +8 -0
  193. package/esm/types/primitives.js +108 -0
  194. package/esm/types/rfcdate.d.ts +20 -0
  195. package/esm/types/rfcdate.js +42 -0
  196. package/esm/types/smart-union.d.ts +6 -0
  197. package/esm/types/smart-union.js +116 -0
  198. package/esm/types/streams.d.ts +1 -0
  199. package/esm/types/streams.js +14 -0
  200. package/esm/types/unrecognized.d.ts +15 -0
  201. package/esm/types/unrecognized.js +30 -0
  202. package/openapi.json +2853 -0
  203. package/package.json +50 -0
@@ -0,0 +1,49 @@
1
+ Set up State Machines so this project can run code against a disposable replica of a SaaS app, such as Salesforce. Follow these steps in order.
2
+
3
+ 1. Install the SDK: `npm install @usestatemachines/sdk`. It is ESM only and runs on Node.js 20.20 or later and on Cloudflare Workers, not in browsers.
4
+ 2. Check that `STATE_API_KEY` is set without printing it: `test -n "$STATE_API_KEY"`. If it is missing, stop and ask me to set it; the SDK doesn't read `.env` files. Never print, log or commit keys or tokens, including `connection.headers()` and `connection.actorToken()`, which return live tokens. In Cloudflare Workers, pass the key as `new StateMachines({ apiKey })`.
5
+ 3. Save this quickstart unchanged as `state-quickstart.mts` and run `npx tsx state-quickstart.mts` to confirm the setup:
6
+
7
+ ```ts
8
+ import { StateMachines } from '@usestatemachines/sdk';
9
+
10
+ // Reads STATE_API_KEY. The workspace comes from the key.
11
+ const sm = new StateMachines();
12
+
13
+ // A failed create carries err.idempotencyKey: see "Recovering a failed create" in the README.
14
+ const environment = await sm.environments.create({
15
+ name: 'quickstart',
16
+ // The default Salesforce app; sm.catalog.get() lists every app.
17
+ apps: [{ key: 'crm', appId: 'salesforce' }],
18
+ });
19
+
20
+ try {
21
+ await sm.environments.waitUntilReady(environment.id);
22
+
23
+ // Salesforce has several actors (its users) and API profiles, so pick one of each.
24
+ // The connection lists the actors; this acts as the first.
25
+ const { actors } = await sm.environments.getConnection(environment.id, 'crm');
26
+ const [actor] = actors;
27
+
28
+ if (!actor) {
29
+ throw new Error('The Salesforce app has no actors.');
30
+ }
31
+
32
+ const crm = await sm.environments.connect(environment.id, 'crm', {
33
+ actorId: actor.id,
34
+ apiId: 'rest',
35
+ });
36
+ const response = await crm.fetch('/services/data/v67.0/limits');
37
+
38
+ console.log(response.status, await response.json());
39
+ } finally {
40
+ // Stop even when a step fails; lifetime counts from the start.
41
+ await sm.environments.stop(environment.id);
42
+ }
43
+ ```
44
+
45
+ 4. Adapt it to my task. Log `await sm.catalog.get()` to find each app's `appId` and the actors and API profiles its release offers; don't guess IDs. Create only the apps the task needs, and pass connect() the `actorId` and `apiId` the task needs.
46
+ 5. Always stop the environment in a `finally`. In CI, then await `sm.environments.waitUntilStopped(environment.id)` so the run fails if cleanup isn't confirmed.
47
+ 6. The SDK retries `502`, `503`, `504` and connection errors itself. If `create` still throws, call `sm.environments.create(input, { idempotencyKey: err.idempotencyKey })` with the same `input`. Never retry `create` without the key: that can start a second environment.
48
+
49
+ Read more: `node_modules/@usestatemachines/sdk/README.md`, https://state-web.usestatemachines.workers.dev/llms.txt and https://www.npmjs.com/package/@usestatemachines/sdk
package/esm/core.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ import { ClientSDK } from "./lib/sdks.js";
2
+ /**
3
+ * A minimal client to use when calling standalone SDK functions. Typically, an
4
+ * instance of this class would be instantiated once at the start of an
5
+ * application and passed around through some dependency injection mechanism to
6
+ * parts of an application that need to make SDK calls.
7
+ */
8
+ export declare class StateMachinesCore extends ClientSDK {
9
+ }
package/esm/core.js ADDED
@@ -0,0 +1,12 @@
1
+ /*
2
+ * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.
3
+ */
4
+ import { ClientSDK } from "./lib/sdks.js";
5
+ /**
6
+ * A minimal client to use when calling standalone SDK functions. Typically, an
7
+ * instance of this class would be instantiated once at the start of an
8
+ * application and passed around through some dependency injection mechanism to
9
+ * parts of an application that need to make SDK calls.
10
+ */
11
+ export class StateMachinesCore extends ClientSDK {
12
+ }
@@ -0,0 +1,81 @@
1
+ import { type Fetcher, HTTPClient } from '../lib/http.js';
2
+ import type { Catalog } from '../models/catalog.js';
3
+ import type { Connection as ConnectionDetails } from '../models/connection.js';
4
+ import type { ConnectionSecret } from '../models/connection-secret.js';
5
+ import type { CreateEnvironmentInput } from '../models/create-environment-input.js';
6
+ import type { Credential } from '../models/credential.js';
7
+ import type { Environment } from '../models/environment.js';
8
+ import type { EventPage } from '../models/event-page.js';
9
+ import type { ListEnvironmentsRequest } from '../models/operations/list-environments.js';
10
+ import type { ListWorkspacesRequest } from '../models/operations/list-workspaces.js';
11
+ import type { Release } from '../models/release.js';
12
+ import type { Workspace } from '../models/workspace.js';
13
+ import { Connection } from './connection.js';
14
+ import { type WaitOptions } from './wait.js';
15
+ export interface StateMachinesOptions {
16
+ /** Workspace API key. Defaults to `STATE_API_KEY` where a `process` exists. */
17
+ apiKey?: string;
18
+ serverURL?: string;
19
+ /** Used when a call omits `workspaceId`; otherwise read from the API key. */
20
+ workspaceId?: string;
21
+ fetch?: Fetcher;
22
+ /** Copied, never modified. Can't be combined with `fetch`. */
23
+ httpClient?: HTTPClient;
24
+ /** Timeout for each attempt of a request; retries get a fresh one. */
25
+ timeoutMs?: number;
26
+ }
27
+ export interface CallOptions {
28
+ signal?: AbortSignal;
29
+ }
30
+ export interface CreateOptions extends CallOptions {
31
+ idempotencyKey?: string;
32
+ workspaceId?: string;
33
+ }
34
+ export type ListEnvironmentsQuery = Omit<ListEnvironmentsRequest, 'workspaceId'> & {
35
+ workspaceId?: string;
36
+ };
37
+ export interface Page<T> {
38
+ data: T[];
39
+ nextCursor: string | null;
40
+ }
41
+ export interface ConnectOptions extends CallOptions {
42
+ actorId?: string;
43
+ apiId?: string;
44
+ }
45
+ export declare class StateMachines {
46
+ #private;
47
+ constructor(options?: StateMachinesOptions);
48
+ readonly credential: {
49
+ get: (options?: CallOptions | undefined) => Promise<Credential>;
50
+ };
51
+ readonly catalog: {
52
+ get: (options?: CallOptions | undefined) => Promise<Catalog>;
53
+ release: (releaseId: string, options?: CallOptions | undefined) => Promise<Release>;
54
+ };
55
+ readonly workspaces: {
56
+ list: (query?: ListWorkspacesRequest, options?: CallOptions | undefined) => Promise<Workspace[]>;
57
+ get: (workspaceId: string, options?: CallOptions | undefined) => Promise<Workspace>;
58
+ events: (workspaceId: string, query?: {
59
+ limit?: number;
60
+ cursor?: string;
61
+ }, options?: CallOptions | undefined) => Promise<EventPage>;
62
+ };
63
+ readonly environments: {
64
+ list: (query?: ListEnvironmentsQuery, options?: CallOptions | undefined) => Promise<Page<Environment>>;
65
+ get: (environmentId: string, options?: CallOptions | undefined) => Promise<Environment>;
66
+ /** Every failure carries the `idempotencyKey`; replaying the create with it is safe. */
67
+ create: (input: CreateEnvironmentInput, options?: CreateOptions) => Promise<Environment>;
68
+ stop: (environmentId: string, options?: CallOptions | undefined) => Promise<Environment>;
69
+ events: (environmentId: string, query?: {
70
+ limit?: number;
71
+ cursor?: string;
72
+ }, options?: CallOptions | undefined) => Promise<EventPage>;
73
+ getConnection: (environmentId: string, appKey: string, options?: CallOptions | undefined) => Promise<ConnectionDetails>;
74
+ reveal: (environmentId: string, appKey: string, actorId: string, options?: CallOptions | undefined) => Promise<ConnectionSecret>;
75
+ /** Resolves when the environment is running. Never stops it. */
76
+ waitUntilReady: (environmentId: string, options?: WaitOptions | undefined) => Promise<Environment>;
77
+ /** Resolves when the environment ended and every sandbox is confirmed disposed. */
78
+ waitUntilStopped: (environmentId: string, options?: WaitOptions | undefined) => Promise<Environment>;
79
+ connect: (environmentId: string, appKey: string, options?: ConnectOptions) => Promise<Connection>;
80
+ };
81
+ }
@@ -0,0 +1,213 @@
1
+ import { SDK_METADATA } from '../lib/config.js';
2
+ import { HTTPClient } from '../lib/http.js';
3
+ import { StateMachines as Generated } from '../sdk/sdk.js';
4
+ import { Connection } from './connection.js';
5
+ import { StateMachinesError, toStateMachinesError } from './errors.js';
6
+ import { linkSignal } from './signal.js';
7
+ import { disposed, poll, ready } from './wait.js';
8
+ /** Thrown from a hook so the generated retry loop stops, whatever the abort reason. */
9
+ class CallerAbortedError extends Error {
10
+ name = 'CallerAbortedError';
11
+ constructor(reason) {
12
+ super('The caller aborted the request.', { cause: reason });
13
+ }
14
+ }
15
+ export class StateMachines {
16
+ #sdk;
17
+ #fetch;
18
+ #workspaceId;
19
+ #credentialWorkspace;
20
+ constructor(options = {}) {
21
+ const apiKey = options.apiKey ?? environmentApiKey();
22
+ if (!apiKey) {
23
+ throw new TypeError('Pass apiKey or set STATE_API_KEY.');
24
+ }
25
+ // Checked here so a malformed key is never echoed by a header validation error.
26
+ if (!/^[\x21-\x7e]+$/.test(apiKey)) {
27
+ throw new TypeError('apiKey must be printable ASCII with no spaces.');
28
+ }
29
+ if (options.fetch && options.httpClient) {
30
+ throw new TypeError('Pass fetch or httpClient, not both.');
31
+ }
32
+ const fetcher = options.fetch;
33
+ // The arrow keeps `this` unbound; runtimes such as workerd reject fetch called on another object.
34
+ const timeoutMs = options.timeoutMs ?? 60000;
35
+ if (!(timeoutMs > 0 && Number.isFinite(timeoutMs))) {
36
+ throw new RangeError('timeoutMs must be a positive, finite number.');
37
+ }
38
+ const httpClient = options.httpClient?.clone() ??
39
+ new HTTPClient(fetcher ? { fetcher: (input) => fetcher(input) } : {});
40
+ addAttemptTimeout(httpClient, timeoutMs);
41
+ this.#fetch = fetcher;
42
+ this.#workspaceId = options.workspaceId;
43
+ this.#sdk = new Generated({
44
+ apiKey,
45
+ userAgent: `statemachines-sdk-ts/${SDK_METADATA.sdkVersion}`,
46
+ httpClient,
47
+ ...(options.serverURL ? { serverURL: options.serverURL } : {}),
48
+ });
49
+ }
50
+ credential = {
51
+ get: (options) => this.#unwrap(options, (call) => this.#sdk.credential.get(call)),
52
+ };
53
+ catalog = {
54
+ get: (options) => this.#unwrap(options, (call) => this.#sdk.catalog.get(call)),
55
+ release: (releaseId, options) => this.#unwrap(options, (call) => this.#sdk.catalog.release({ releaseId }, call)),
56
+ };
57
+ workspaces = {
58
+ list: (query = {}, options) => this.#unwrap(options, (call) => this.#sdk.workspaces.list(query, call)),
59
+ get: (workspaceId, options) => this.#unwrap(options, (call) => this.#sdk.workspaces.get({ workspaceId }, call)),
60
+ events: (workspaceId, query = {}, options) => this.#call(options, (call) => this.#sdk.workspaces.events({ ...query, workspaceId }, call)),
61
+ };
62
+ environments = {
63
+ list: (query = {}, options) => this.#call(options, async (call) => {
64
+ const { workspaceId, ...filters } = query;
65
+ const resolved = await this.#workspace(workspaceId, call.signal);
66
+ return this.#sdk.environments.list({ ...filters, workspaceId: resolved }, call);
67
+ }),
68
+ get: (environmentId, options) => this.#unwrap(options, (call) => this.#sdk.environments.get({ environmentId }, call)),
69
+ /** Every failure carries the `idempotencyKey`; replaying the create with it is safe. */
70
+ create: async (input, options = {}) => {
71
+ const idempotencyKey = options.idempotencyKey ?? crypto.randomUUID();
72
+ try {
73
+ return await this.#call(options, async (call) => {
74
+ const workspaceId = await this.#workspace(options.workspaceId, call.signal);
75
+ const { data } = await this.#sdk.environments.create({ workspaceId, idempotencyKey, body: input }, call);
76
+ const [environment] = data;
77
+ if (!environment) {
78
+ throw new Error('Invalid response from the State API: the create returned no environment.');
79
+ }
80
+ return environment;
81
+ });
82
+ }
83
+ catch (error) {
84
+ // Always a new instance: concurrent creates can share one failed workspace lookup.
85
+ throw new StateMachinesError(error, { idempotencyKey });
86
+ }
87
+ },
88
+ stop: (environmentId, options) => this.#unwrap(options, (call) => this.#sdk.environments.stop({ environmentId }, call)),
89
+ events: (environmentId, query = {}, options) => this.#call(options, (call) => this.#sdk.environments.events({ ...query, environmentId }, call)),
90
+ getConnection: (environmentId, appKey, options) => this.#unwrap(options, (call) => this.#sdk.environments.getConnection({ environmentId, appKey }, call)),
91
+ reveal: (environmentId, appKey, actorId, options) => this.#unwrap(options, (call) => this.#sdk.environments.reveal({ environmentId, appKey, body: { actorId } }, call)),
92
+ /** Resolves when the environment is running. Never stops it. */
93
+ waitUntilReady: (environmentId, options) => poll((signal) => this.#poll(environmentId, signal), ready, options),
94
+ /** Resolves when the environment ended and every sandbox is confirmed disposed. */
95
+ waitUntilStopped: (environmentId, options) => poll((signal) => this.#poll(environmentId, signal), disposed, options),
96
+ connect: async (environmentId, appKey, options = {}) => {
97
+ const details = await this.environments.getConnection(environmentId, appKey, options);
98
+ if (details.status !== 'ready' || !details.baseUrl) {
99
+ throw new TypeError(`App ${appKey} is not ready to connect (${details.status}).`);
100
+ }
101
+ const actor = pick('actor', details.actors, options.actorId);
102
+ const api = pick('API', details.apis, options.apiId);
103
+ const secret = await this.environments.reveal(environmentId, appKey, actor.id, options);
104
+ return new Connection(details.baseUrl, api, secret, this.#fetch);
105
+ },
106
+ };
107
+ /** One read for the poller, which owns retries, timing and the caller's signal. */
108
+ #poll(environmentId, signal) {
109
+ return this.#unwrap({ signal }, (call) => this.#sdk.environments.get({ environmentId }, { ...call, retries: { strategy: 'none' } }));
110
+ }
111
+ /** Resolves the workspace once per client; a failed lookup is retried by the next call. */
112
+ #workspace(explicit, signal) {
113
+ const workspaceId = explicit ?? this.#workspaceId;
114
+ if (workspaceId) {
115
+ return Promise.resolve(workspaceId);
116
+ }
117
+ this.#credentialWorkspace ?? (this.#credentialWorkspace = this.#lookupWorkspace());
118
+ // The lookup is shared, so one caller's abort only stops that caller waiting.
119
+ return abortable(this.#credentialWorkspace, signal);
120
+ }
121
+ #lookupWorkspace() {
122
+ const lookup = this.credential.get().then((credential) => {
123
+ if (!credential.workspaceId) {
124
+ throw new TypeError('This credential has no workspace. Pass workspaceId, or use a workspace API key.');
125
+ }
126
+ return credential.workspaceId;
127
+ });
128
+ lookup.catch(() => {
129
+ if (this.#credentialWorkspace === lookup) {
130
+ this.#credentialWorkspace = undefined;
131
+ }
132
+ });
133
+ return lookup;
134
+ }
135
+ #unwrap(options, run) {
136
+ return this.#call(options, async (call) => (await run(call)).data);
137
+ }
138
+ /**
139
+ * Runs one API call under its own signal. Closing it when the call settles
140
+ * releases every attempt's timeout timer and listener.
141
+ */
142
+ async #call(options, run) {
143
+ const scope = linkSignal(options?.signal);
144
+ try {
145
+ return await run({ signal: scope.signal });
146
+ }
147
+ catch (error) {
148
+ // A caller abort can surface as a raw TimeoutError while a body is read; it is never retryable.
149
+ throw toStateMachinesError(options?.signal?.aborted
150
+ ? new CallerAbortedError(options.signal.reason)
151
+ : error);
152
+ }
153
+ finally {
154
+ scope.close();
155
+ }
156
+ }
157
+ }
158
+ /**
159
+ * Gives each attempt its own timeout, linked to the caller's signal. The generated
160
+ * `timeoutMs` is never used: it is dropped when a caller passes a signal.
161
+ */
162
+ function addAttemptTimeout(httpClient, timeoutMs) {
163
+ const callers = new WeakMap();
164
+ httpClient.addHook('beforeRequest', (request) => {
165
+ const caller = request.signal;
166
+ if (caller.aborted) {
167
+ throw new CallerAbortedError(caller.reason);
168
+ }
169
+ const attempt = new Request(request, {
170
+ signal: linkSignal(caller, timeoutMs).signal,
171
+ });
172
+ callers.set(attempt, caller);
173
+ return attempt;
174
+ });
175
+ // Without this, a timeout-shaped abort reason looks like a retryable timeout to the generated loop.
176
+ httpClient.addHook('requestError', (_error, request) => {
177
+ const caller = callers.get(request);
178
+ if (caller?.aborted) {
179
+ throw new CallerAbortedError(caller.reason);
180
+ }
181
+ });
182
+ }
183
+ function abortable(promise, signal) {
184
+ if (!signal) {
185
+ return promise;
186
+ }
187
+ return new Promise((resolve, reject) => {
188
+ const onAbort = () => reject(signal.reason);
189
+ if (signal.aborted) {
190
+ onAbort();
191
+ return;
192
+ }
193
+ signal.addEventListener('abort', onAbort, { once: true });
194
+ promise
195
+ .then(resolve, reject)
196
+ .finally(() => signal.removeEventListener('abort', onAbort));
197
+ });
198
+ }
199
+ function pick(kind, choices, id) {
200
+ const found = id
201
+ ? choices.find((choice) => choice.id === id)
202
+ : choices.length === 1
203
+ ? choices[0]
204
+ : undefined;
205
+ if (!found) {
206
+ throw new TypeError(`Choose one ${kind}: ${choices.map((choice) => choice.id).join(', ') || 'none available'}.`);
207
+ }
208
+ return found;
209
+ }
210
+ function environmentApiKey() {
211
+ const runtime = globalThis;
212
+ return runtime.process?.env?.STATE_API_KEY;
213
+ }
@@ -0,0 +1,42 @@
1
+ import type { Fetcher } from '../lib/http.js';
2
+ import type { ApiProfile } from '../models/api-profile.js';
3
+ import type { ConnectionSecret } from '../models/connection-secret.js';
4
+ /**
5
+ * The HTTP header an app's API expects for its actor token, or `null` when the token goes in
6
+ * the request body instead: for `SOAP SessionHeader`, as the envelope's `SessionHeader/sessionId`.
7
+ */
8
+ export declare function formatActorHeader(actorHeader: string, token: string): {
9
+ name: string;
10
+ value: string;
11
+ } | null;
12
+ /** A live app connection. `fetch` only reaches paths under `baseUrl`. */
13
+ export declare class Connection {
14
+ #private;
15
+ readonly baseUrl: string;
16
+ readonly environmentId: string;
17
+ readonly appKey: string;
18
+ readonly actorId: string;
19
+ readonly api: ApiProfile;
20
+ readonly expiresAt: Date;
21
+ constructor(baseUrl: string, api: ApiProfile, secret: ConnectionSecret, fetch?: Fetcher);
22
+ /**
23
+ * The headers the gateway and the app need, for clients that can't use `fetch`. For an API
24
+ * whose actor token goes in the request body, such as SOAP, only the environment token.
25
+ */
26
+ headers(): Record<string, string>;
27
+ /**
28
+ * The live actor token, for APIs that take it in the request body: for a SOAP API, put it in
29
+ * the envelope's `SessionHeader/sessionId`. Treat it as a secret.
30
+ */
31
+ actorToken(): string;
32
+ /** Sends one request to the app. Never retries, never follows redirects, never adds the API key. */
33
+ fetch(path: string, init?: RequestInit): Promise<Response>;
34
+ toJSON(): {
35
+ baseUrl: string;
36
+ environmentId: string;
37
+ appKey: string;
38
+ actorId: string;
39
+ api: ApiProfile;
40
+ expiresAt: Date;
41
+ };
42
+ }
@@ -0,0 +1,133 @@
1
+ const HTTP_TOKEN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
2
+ // Salesforce's SOAP API takes the actor token in the envelope, not an HTTP header.
3
+ const SOAP_SESSION_HEADER = 'SOAP SessionHeader';
4
+ // Visible ASCII only: `Headers` would otherwise echo a bad value, token included, in its error.
5
+ const TOKEN_VALUE = /^[\x21-\x7e]+$/;
6
+ // Checked on the path part only (C7). Covers protocol-relative starts, backslashes, control
7
+ // characters and encoded dots, slashes and backslashes, including double encoding.
8
+ const UNSAFE_PATH = /^\/\/|\\|%(?:25)*(?:2e|2f|5c)|\p{Cc}/iu;
9
+ const OUTSIDE = 'Connection.fetch only reaches paths under the connection baseUrl.';
10
+ /**
11
+ * The HTTP header an app's API expects for its actor token, or `null` when the token goes in
12
+ * the request body instead: for `SOAP SessionHeader`, as the envelope's `SessionHeader/sessionId`.
13
+ */
14
+ export function formatActorHeader(actorHeader, token) {
15
+ if (actorHeader === SOAP_SESSION_HEADER) {
16
+ return null;
17
+ }
18
+ if (!HTTP_TOKEN.test(actorHeader) ||
19
+ actorHeader.toLowerCase().startsWith('x-state-')) {
20
+ throw new TypeError('The API profile names an invalid actor header.');
21
+ }
22
+ return actorHeader.toLowerCase() === 'authorization'
23
+ ? { name: 'Authorization', value: `Bearer ${token}` }
24
+ : { name: actorHeader, value: token };
25
+ }
26
+ /** A live app connection. `fetch` only reaches paths under `baseUrl`. */
27
+ export class Connection {
28
+ baseUrl;
29
+ environmentId;
30
+ appKey;
31
+ actorId;
32
+ api;
33
+ expiresAt;
34
+ #base;
35
+ #environmentToken;
36
+ #actorToken;
37
+ #actor;
38
+ #fetch;
39
+ constructor(baseUrl, api, secret, fetch) {
40
+ const base = URL.canParse(baseUrl) ? new URL(baseUrl) : null;
41
+ if (!base ||
42
+ !/^https?:$/.test(base.protocol) ||
43
+ base.username ||
44
+ base.password ||
45
+ base.search ||
46
+ base.hash) {
47
+ throw new TypeError('The connection baseUrl is not a plain http(s) URL.');
48
+ }
49
+ if (!TOKEN_VALUE.test(secret.environmentToken) ||
50
+ !TOKEN_VALUE.test(secret.actorToken)) {
51
+ throw new TypeError('The connection tokens are malformed.');
52
+ }
53
+ if (!base.pathname.endsWith('/')) {
54
+ base.pathname += '/';
55
+ }
56
+ this.#base = base;
57
+ this.#environmentToken = secret.environmentToken;
58
+ this.#actorToken = secret.actorToken;
59
+ this.#actor = formatActorHeader(api.actorHeader, secret.actorToken);
60
+ this.#fetch = fetch;
61
+ this.baseUrl = base.href;
62
+ this.environmentId = secret.environmentId;
63
+ this.appKey = secret.appKey;
64
+ this.actorId = secret.actorId;
65
+ this.api = api;
66
+ this.expiresAt = secret.expiresAt;
67
+ }
68
+ /**
69
+ * The headers the gateway and the app need, for clients that can't use `fetch`. For an API
70
+ * whose actor token goes in the request body, such as SOAP, only the environment token.
71
+ */
72
+ headers() {
73
+ return {
74
+ 'X-State-Environment-Token': this.#environmentToken,
75
+ ...(this.#actor ? { [this.#actor.name]: this.#actor.value } : {}),
76
+ };
77
+ }
78
+ /**
79
+ * The live actor token, for APIs that take it in the request body: for a SOAP API, put it in
80
+ * the envelope's `SessionHeader/sessionId`. Treat it as a secret.
81
+ */
82
+ actorToken() {
83
+ return this.#actorToken;
84
+ }
85
+ /** Sends one request to the app. Never retries, never follows redirects, never adds the API key. */
86
+ async fetch(path, init = {}) {
87
+ const url = this.#resolve(path);
88
+ const headers = new Headers(init.headers);
89
+ for (const name of headers.keys()) {
90
+ if (name.startsWith('x-state-')) {
91
+ throw new TypeError(`Header ${name} is reserved for State.`);
92
+ }
93
+ }
94
+ // Ours go last so a caller header of the same name can't replace them.
95
+ for (const [name, value] of Object.entries(this.headers())) {
96
+ headers.set(name, value);
97
+ }
98
+ const request = new Request(url, { ...init, headers, redirect: 'manual' });
99
+ // Called on `globalThis` at use time: a stored unbound `fetch` throws "Illegal invocation" in Workers.
100
+ return this.#fetch ? this.#fetch(request) : globalThis.fetch(request);
101
+ }
102
+ toJSON() {
103
+ return {
104
+ baseUrl: this.baseUrl,
105
+ environmentId: this.environmentId,
106
+ appKey: this.appKey,
107
+ actorId: this.actorId,
108
+ api: this.api,
109
+ expiresAt: this.expiresAt,
110
+ };
111
+ }
112
+ #resolve(path) {
113
+ const pathPart = path.split(/[?#]/, 1)[0] ?? '';
114
+ if (UNSAFE_PATH.test(pathPart)) {
115
+ throw new TypeError(OUTSIDE);
116
+ }
117
+ // A leading `/` means the root of the connection, not of the gateway.
118
+ const relative = path.startsWith('/') ? `.${path}` : path;
119
+ // `URL` errors carry the input, query included, so they are replaced rather than rethrown.
120
+ const url = URL.canParse(relative, this.#base)
121
+ ? new URL(relative, this.#base)
122
+ : null;
123
+ if (!url ||
124
+ url.origin !== this.#base.origin ||
125
+ url.username ||
126
+ url.password ||
127
+ !url.pathname.startsWith(this.#base.pathname) ||
128
+ UNSAFE_PATH.test(url.pathname)) {
129
+ throw new TypeError(OUTSIDE);
130
+ }
131
+ return url;
132
+ }
133
+ }
@@ -0,0 +1,52 @@
1
+ import type { Environment } from '../models/environment.js';
2
+ import type { Code } from '../models/error-response.js';
3
+ interface Details {
4
+ readonly message: string;
5
+ readonly cause?: unknown;
6
+ readonly status: number | null;
7
+ readonly code: Code | null;
8
+ readonly requestId: string | null;
9
+ readonly fields: Record<string, string[]> | null;
10
+ readonly retryAfter: number | null;
11
+ readonly retryable: boolean;
12
+ readonly idempotencyKey?: string;
13
+ }
14
+ /**
15
+ * Any failed API call: an error response, an invalid response, or no response at
16
+ * all (`status` null). The message never includes a response body.
17
+ */
18
+ export declare class StateMachinesError extends Error implements Details {
19
+ readonly name = "StateMachinesError";
20
+ readonly status: number | null;
21
+ readonly code: Code | null;
22
+ readonly requestId: string | null;
23
+ readonly fields: Record<string, string[]> | null;
24
+ /** Milliseconds the server asked the client to wait. */
25
+ readonly retryAfter: number | null;
26
+ /** Transient: a 502, 503 or 504, or the request never got a response. */
27
+ readonly retryable: boolean;
28
+ /** Set on `environments.create` failures; replay the create with it to recover. */
29
+ readonly idempotencyKey?: string;
30
+ /** Wraps any thrown value. Wrapping a `StateMachinesError` copies it, keeping its cause. */
31
+ constructor(error: unknown, extra?: {
32
+ idempotencyKey?: string;
33
+ });
34
+ }
35
+ export declare function toStateMachinesError(error: unknown): StateMachinesError;
36
+ export declare class EnvironmentFailedError extends Error {
37
+ readonly environment: Environment;
38
+ readonly name = "EnvironmentFailedError";
39
+ constructor(environment: Environment);
40
+ }
41
+ export declare class DisposalIncompleteError extends Error {
42
+ readonly environment: Environment;
43
+ readonly name = "DisposalIncompleteError";
44
+ constructor(environment: Environment);
45
+ }
46
+ export declare class TimeoutError extends Error {
47
+ readonly timeoutMs: number;
48
+ readonly environment: Environment | null;
49
+ readonly name = "TimeoutError";
50
+ constructor(timeoutMs: number, environment: Environment | null);
51
+ }
52
+ export {};