@statelyai/sdk 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @statelyai/sdk
2
2
 
3
- Embed the [Stately editor](https://stately.ai) in your app. Zero dependencies, fully typed.
3
+ Embed the [Stately editor](https://stately.ai) in your app and talk to the Stately Studio API. Zero dependencies, fully typed.
4
4
 
5
5
  ## Install
6
6
 
@@ -8,9 +8,13 @@ Embed the [Stately editor](https://stately.ai) in your app. Zero dependencies, f
8
8
  npm install @statelyai/sdk
9
9
  ```
10
10
 
11
- ## API key
11
+ ## Authentication
12
12
 
13
- An API key is required to use the embed SDK. To get one:
13
+ The editor uses a pluggable authentication system. How you configure it depends on whether you're using the hosted Stately platform or self-hosting.
14
+
15
+ ### With Stately (default)
16
+
17
+ An API key is required. To get one:
14
18
 
15
19
  1. Go to your [Stately settings](https://stately.ai/settings)
16
20
  2. Select the **API Key** tab
@@ -19,8 +23,68 @@ An API key is required to use the embed SDK. To get one:
19
23
 
20
24
  See the [Studio API docs](https://stately.ai/docs/studio-api) for more details.
21
25
 
26
+ Pass the key to the SDK:
27
+
28
+ ```ts
29
+ const embed = createStatelyEmbed({
30
+ baseUrl: 'https://stately.ai',
31
+ apiKey: 'your-api-key',
32
+ });
33
+ ```
34
+
35
+ The server validates the key against the Stately registry API.
36
+
37
+ ### Self-hosting
38
+
39
+ When self-hosting the editor, authentication is controlled by the `AUTH_PROVIDER` environment variable on the server:
40
+
41
+ | Value | Behavior |
42
+ | ---------- | ----------------------------------------------------------- |
43
+ | `stately` | Validates tokens against the Stately registry API |
44
+ | `none` | Allows all requests (no authentication) |
45
+ | _(unset)_ | `none` in development, `stately` in production |
46
+
47
+ For a fully self-contained deployment with no Stately dependency:
48
+
49
+ ```bash
50
+ AUTH_PROVIDER=none
51
+ ```
52
+
53
+ ### Custom authentication
54
+
55
+ You can implement your own auth by writing a custom `AuthValidator` — an async function that receives a token and returns whether it's valid:
56
+
57
+ ```ts
58
+ import type { AuthValidator } from '@/lib/auth';
59
+
60
+ const myValidator: AuthValidator = async (token) => {
61
+ const res = await fetch('https://my-auth-server.com/verify', {
62
+ headers: { Authorization: `Bearer ${token}` },
63
+ });
64
+ return res.ok;
65
+ };
66
+ ```
67
+
68
+ The built-in adapters are:
69
+
70
+ - **`createStatelyValidator(baseUrl?)`** — verifies against the Stately registry API (`/registry/api/v1/verify`)
71
+ - **`allowAllValidator`** — always returns `true`
72
+
73
+ To use a custom validator, call `getAuthValidator()` from `src/lib/auth` in the server-side page code, or replace the resolver logic in `src/lib/auth/index.tsx` to return your implementation.
74
+
75
+ ### Environment variables
76
+
77
+ | Variable | Purpose |
78
+ | ----------------------- | -------------------------------------------- |
79
+ | `AUTH_PROVIDER` | Auth strategy: `stately`, `none`, or unset |
80
+ | `STATELY_API_KEY` | Server-side API key for Stately data fetching |
81
+ | `STATELY_API_URL` | Stately API base URL (server-side override) |
82
+ | `NEXT_PUBLIC_BASE_URL` | Public-facing base URL |
83
+
22
84
  ## Quick start
23
85
 
86
+ ### Third-party embed (with API key)
87
+
24
88
  ```ts
25
89
  import { createStatelyEmbed } from '@statelyai/sdk';
26
90
 
@@ -29,10 +93,8 @@ const embed = createStatelyEmbed({
29
93
  apiKey: 'your-api-key',
30
94
  });
31
95
 
32
- // Mount into a container
33
96
  embed.mount(document.getElementById('editor')!);
34
97
 
35
- // Initialize with a machine
36
98
  embed.init({
37
99
  machine: myMachineConfig,
38
100
  mode: 'editing',
@@ -40,26 +102,84 @@ embed.init({
40
102
  });
41
103
  ```
42
104
 
105
+ ### Same-origin embed (cookie auth)
106
+
107
+ When the embed host and the editor share a domain (e.g. Stately Studio embedding the beta editor), Supabase session cookies handle auth automatically — no API key needed:
108
+
109
+ ```ts
110
+ const embed = createStatelyEmbed({
111
+ baseUrl: process.env.NEXT_PUBLIC_BETA_EDITOR_URL ?? window.location.origin,
112
+ });
113
+
114
+ embed.mount(container);
115
+ ```
116
+
117
+ ### Self-hosted (no auth)
118
+
119
+ When self-hosting with `AUTH_PROVIDER=none`, no token is required:
120
+
121
+ ```ts
122
+ const embed = createStatelyEmbed({
123
+ baseUrl: 'https://your-editor.example.com',
124
+ });
125
+
126
+ embed.mount(container);
127
+ ```
128
+
129
+ ## Module layout
130
+
131
+ The SDK ships direct root exports for the common entry points:
132
+
133
+ ```ts
134
+ import {
135
+ createStatelyInspector,
136
+ createStatelyEmbed,
137
+ createStatelyClient,
138
+ } from '@statelyai/sdk';
139
+ ```
140
+
141
+ It also supports subpath imports when you want a narrower surface:
142
+
143
+ ```ts
144
+ import { createStatelyClient } from '@statelyai/sdk/studio';
145
+ import { createStatelyInspector } from '@statelyai/sdk/inspect';
146
+ import { createStatelyEmbed } from '@statelyai/sdk/embed';
147
+ ```
148
+
149
+ ## Studio API client
150
+
151
+ ```ts
152
+ import { createStatelyClient } from '@statelyai/sdk';
153
+
154
+ const studio = createStatelyClient({
155
+ apiKey: process.env.STATELY_API_KEY,
156
+ });
157
+
158
+ const project = await studio.projects.get('project-id');
159
+ const machine = await studio.machines.get('machine-id', { version: '42' });
160
+ const extracted = await studio.code.extractMachines(sourceCode);
161
+ ```
162
+
43
163
  ## API
44
164
 
45
165
  ### `createStatelyEmbed(options)`
46
166
 
47
167
  Creates an embed instance.
48
168
 
49
- | Option | Type | Description |
50
- |--------|------|-------------|
51
- | `baseUrl` | `string` | **Required.** Base URL of the Stately app |
52
- | `apiKey` | `string` | **Required.** Your Stately API key |
53
- | `origin` | `string` | Custom target origin for postMessage |
54
- | `onReady` | `() => void` | Called when embed is ready |
55
- | `onLoaded` | `(graph) => void` | Called when machine is loaded |
56
- | `onChange` | `(graph, machineConfig) => void` | Called on every change |
57
- | `onSave` | `(graph, machineConfig) => void` | Called on save |
58
- | `onError` | `({ code, message }) => void` | Called on error |
169
+ | Option | Type | Description |
170
+ | ---------- | -------------------------------- | ----------------------------------------- |
171
+ | `baseUrl` | `string` | **Required.** Base URL of the Stately app |
172
+ | `apiKey` | `string` | API key for authentication (see [Authentication](#authentication)) |
173
+ | `origin` | `string` | Custom target origin for postMessage |
174
+ | `onReady` | `() => void` | Called when embed is ready |
175
+ | `onLoaded` | `(graph) => void` | Called when machine is loaded |
176
+ | `onChange` | `(graph, machineConfig) => void` | Called on every change |
177
+ | `onSave` | `(graph, machineConfig) => void` | Called on save |
178
+ | `onError` | `({ code, message }) => void` | Called on error |
59
179
 
60
180
  ### Embed methods
61
181
 
62
- #### `mount(container)` / `attach(iframe)`
182
+ #### `embed.mount(container)` / `embed.attach(iframe)`
63
183
 
64
184
  Mount creates an iframe inside a container element. Attach connects to an existing iframe.
65
185
 
@@ -71,7 +191,7 @@ const iframe = embed.mount(document.getElementById('editor')!);
71
191
  embed.attach(document.querySelector('iframe')!);
72
192
  ```
73
193
 
74
- #### `init(options)`
194
+ #### `embed.init(options)`
75
195
 
76
196
  Initialize the embed with a machine and display options.
77
197
 
@@ -79,8 +199,8 @@ Initialize the embed with a machine and display options.
79
199
  embed.init({
80
200
  machine: machineConfig,
81
201
  format: 'xstate', // optional
82
- mode: 'editing', // 'editing' | 'viewing' | 'simulating'
83
- theme: 'dark', // 'light' | 'dark'
202
+ mode: 'editing', // 'editing' | 'viewing' | 'simulating'
203
+ theme: 'dark', // 'light' | 'dark'
84
204
  readOnly: false,
85
205
  depth: 3,
86
206
  panels: {
@@ -91,15 +211,15 @@ embed.init({
91
211
  });
92
212
  ```
93
213
 
94
- #### `updateMachine(machine, format?)`
214
+ #### `embed.updateMachine(machine, format?)`
95
215
 
96
216
  Update the displayed machine.
97
217
 
98
- #### `setMode(mode)` / `setTheme(theme)`
218
+ #### `embed.setMode(mode)` / `embed.setTheme(theme)`
99
219
 
100
220
  Change the embed mode or theme at runtime.
101
221
 
102
- #### `export(format, options?)`
222
+ #### `embed.export(format, options?)`
103
223
 
104
224
  Export the current machine. Returns a promise.
105
225
 
@@ -111,7 +231,7 @@ const mermaid = await embed.export('mermaid');
111
231
 
112
232
  Supported formats: `xstate`, `json`, `digraph`, `mermaid`, `scxml`
113
233
 
114
- #### `on(event, handler)` / `off(event, handler)`
234
+ #### `embed.on(event, handler)` / `embed.off(event, handler)`
115
235
 
116
236
  Subscribe/unsubscribe to embed events: `ready`, `loaded`, `change`, `save`, `error`.
117
237
 
@@ -121,10 +241,10 @@ embed.on('change', ({ graph, machineConfig }) => {
121
241
  });
122
242
  ```
123
243
 
124
- #### `toast(message, type?)`
244
+ #### `embed.toast(message, type?)`
125
245
 
126
246
  Show a toast notification in the embed. Type: `'success' | 'error' | 'info' | 'warning'`
127
247
 
128
- #### `destroy()`
248
+ #### `embed.destroy()`
129
249
 
130
250
  Tear down the embed. Removes listeners, rejects pending promises, and removes the iframe if it was created via `mount()`.
@@ -0,0 +1,43 @@
1
+ import { a as ExportCallOptions, c as InitOptions, i as EmbedMode, n as EmbedEventMap, o as ExportFormat, r as EmbedEventName, s as ExportFormatMap, t as EmbedEventHandler } from "./protocol-BC-_s3if.mjs";
2
+
3
+ //#region src/embed.d.ts
4
+ interface StatelyEmbedOptions {
5
+ baseUrl: string;
6
+ apiKey?: string;
7
+ origin?: string;
8
+ onReady?: () => void;
9
+ onLoaded?: (graph: unknown) => void;
10
+ onChange?: (graph: unknown, machineConfig: unknown) => void;
11
+ onSave?: (graph: unknown, machineConfig: unknown) => void;
12
+ onError?: (error: {
13
+ code: string;
14
+ message: string;
15
+ }) => void;
16
+ }
17
+ interface StatelyEmbed {
18
+ /** Attach to an existing iframe element. Sets src and begins handshake. */
19
+ attach(iframe: HTMLIFrameElement): void;
20
+ /** Create an iframe and mount it into a container element. */
21
+ mount(container: HTMLElement): HTMLIFrameElement;
22
+ /** Send init message (queued if iframe not ready yet). */
23
+ init(options: InitOptions): void;
24
+ /** Update the machine (shorthand for update message). */
25
+ updateMachine(machine: unknown, format?: string): void;
26
+ /** Change the embed mode. */
27
+ setMode(mode: EmbedMode): void;
28
+ /** Change the embed theme. */
29
+ setTheme(theme: 'light' | 'dark'): void;
30
+ /** Export the current machine in a given format. Returns a promise. */
31
+ export<F extends ExportFormat>(format: F, options?: ExportCallOptions<F>): Promise<ExportFormatMap[F]['result']>;
32
+ /** Subscribe to an embed event. */
33
+ on<K extends EmbedEventName>(event: K, handler: EmbedEventHandler<K>): void;
34
+ /** Unsubscribe from an embed event. */
35
+ off<K extends EmbedEventName>(event: K, handler: EmbedEventHandler<K>): void;
36
+ /** Show a toast message in the embed. */
37
+ toast(message: string, toastType?: 'success' | 'error' | 'info' | 'warning'): void;
38
+ /** Tear down: remove listener, reject pending promises, optionally remove iframe. */
39
+ destroy(): void;
40
+ }
41
+ declare function createStatelyEmbed(options: StatelyEmbedOptions): StatelyEmbed;
42
+ //#endregion
43
+ export { type EmbedEventHandler, type EmbedEventMap, type EmbedEventName, type EmbedMode, type ExportCallOptions, type ExportFormat, type ExportFormatMap, type InitOptions, StatelyEmbed, StatelyEmbedOptions, createStatelyEmbed };
package/dist/embed.mjs ADDED
@@ -0,0 +1,175 @@
1
+ import { i as createPendingExportManager, o as toInitMessage, r as createEventRegistry, t as createPostMessageTransport } from "./transport-D352iKKa.mjs";
2
+
3
+ //#region src/embed.ts
4
+ function createStatelyEmbed(options) {
5
+ const base = options.baseUrl.replace(/\/+$/, "") + "/embed";
6
+ const embedUrl = options.apiKey ? `${base}?api_key=${encodeURIComponent(options.apiKey)}` : base;
7
+ const targetOrigin = options.origin ?? new URL(embedUrl).origin;
8
+ let iframe = null;
9
+ let transport = null;
10
+ let ownedIframe = false;
11
+ let destroyed = false;
12
+ const pendingMessages = [];
13
+ const events = createEventRegistry();
14
+ const exportManager = createPendingExportManager((message) => send(message));
15
+ function send(msg) {
16
+ if (!transport?.ready) {
17
+ pendingMessages.push(msg);
18
+ return;
19
+ }
20
+ transport.send(msg);
21
+ }
22
+ function flush() {
23
+ if (!transport) return;
24
+ while (pendingMessages.length > 0) {
25
+ const msg = pendingMessages.shift();
26
+ transport.send(msg);
27
+ }
28
+ }
29
+ function handleMessage(data) {
30
+ if (destroyed) return;
31
+ switch (data.type) {
32
+ case "@statelyai.ready": {
33
+ const ready = data;
34
+ flush();
35
+ options.onReady?.();
36
+ events.emit("ready", { version: ready.version });
37
+ break;
38
+ }
39
+ case "@statelyai.loaded": {
40
+ const loaded = data;
41
+ options.onLoaded?.(loaded.graph);
42
+ events.emit("loaded", { graph: loaded.graph });
43
+ break;
44
+ }
45
+ case "@statelyai.change": {
46
+ const change = data;
47
+ options.onChange?.(change.graph, change.machineConfig);
48
+ events.emit("change", {
49
+ graph: change.graph,
50
+ machineConfig: change.machineConfig
51
+ });
52
+ break;
53
+ }
54
+ case "@statelyai.save": {
55
+ const save = data;
56
+ options.onSave?.(save.graph, save.machineConfig);
57
+ events.emit("save", {
58
+ graph: save.graph,
59
+ machineConfig: save.machineConfig
60
+ });
61
+ break;
62
+ }
63
+ case "@statelyai.retrieved": {
64
+ const retrieved = data;
65
+ exportManager.resolve(retrieved.requestId, retrieved.data);
66
+ break;
67
+ }
68
+ case "@statelyai.error": {
69
+ const error = data;
70
+ const err = {
71
+ code: error.code,
72
+ message: error.message
73
+ };
74
+ exportManager.reject(new Error(err.message), error.requestId);
75
+ options.onError?.(err);
76
+ events.emit("error", err);
77
+ break;
78
+ }
79
+ }
80
+ }
81
+ function replaceTransport(nextTransport) {
82
+ transport?.destroy();
83
+ transport = nextTransport;
84
+ }
85
+ function setupTransport(el) {
86
+ const nextTransport = createPostMessageTransport({
87
+ iframe: el,
88
+ targetOrigin
89
+ });
90
+ replaceTransport(nextTransport);
91
+ nextTransport.onMessage(handleMessage);
92
+ nextTransport.onReady(flush);
93
+ }
94
+ return {
95
+ attach(el) {
96
+ if (destroyed) return;
97
+ const currentSrc = el.getAttribute("src");
98
+ if (currentSrc && currentSrc !== "about:blank" && el.src !== embedUrl) console.warn("Replacing existing iframe src during attach()", {
99
+ currentSrc,
100
+ nextSrc: embedUrl
101
+ });
102
+ iframe = el;
103
+ ownedIframe = false;
104
+ el.src = embedUrl;
105
+ setupTransport(el);
106
+ },
107
+ mount(container) {
108
+ if (destroyed) throw new Error("Embed is destroyed");
109
+ const el = document.createElement("iframe");
110
+ el.src = embedUrl;
111
+ el.style.border = "none";
112
+ el.style.width = "100%";
113
+ el.style.height = "100%";
114
+ el.setAttribute("sandbox", "allow-scripts allow-same-origin");
115
+ el.setAttribute("allow", "clipboard-read; clipboard-write");
116
+ container.appendChild(el);
117
+ iframe = el;
118
+ ownedIframe = true;
119
+ setupTransport(el);
120
+ return el;
121
+ },
122
+ init(opts) {
123
+ send(toInitMessage(opts));
124
+ },
125
+ updateMachine(machine, format) {
126
+ send({
127
+ type: "@statelyai.update",
128
+ machine,
129
+ format
130
+ });
131
+ },
132
+ setMode(mode) {
133
+ send({
134
+ type: "@statelyai.setMode",
135
+ mode
136
+ });
137
+ },
138
+ setTheme(theme) {
139
+ send({
140
+ type: "@statelyai.setTheme",
141
+ theme
142
+ });
143
+ },
144
+ toast(message, toastType) {
145
+ send({
146
+ type: "@statelyai.toast",
147
+ message,
148
+ toastType
149
+ });
150
+ },
151
+ export(format, callOptions) {
152
+ return exportManager.start(format, callOptions, "Embed is destroyed", () => destroyed);
153
+ },
154
+ on(event, handler) {
155
+ events.on(event, handler);
156
+ },
157
+ off(event, handler) {
158
+ events.off(event, handler);
159
+ },
160
+ destroy() {
161
+ if (destroyed) return;
162
+ destroyed = true;
163
+ transport?.destroy();
164
+ transport = null;
165
+ exportManager.clear("Embed destroyed");
166
+ events.clear();
167
+ pendingMessages.length = 0;
168
+ if (ownedIframe && iframe) iframe.remove();
169
+ iframe = null;
170
+ }
171
+ };
172
+ }
173
+
174
+ //#endregion
175
+ export { createStatelyEmbed };