@statelyai/sdk 0.2.0 → 0.3.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.
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,6 +102,64 @@ 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)`
@@ -48,8 +168,8 @@ Creates an embed instance.
48
168
 
49
169
  | Option | Type | Description |
50
170
  | ---------- | -------------------------------- | ----------------------------------------- |
51
- | `baseUrl` | `string` | **Required.** Base URL of the Stately app |
52
- | `apiKey` | `string` | **Required.** Your Stately API key |
171
+ | `baseUrl` | `string` | **Required.** Base URL of the Stately app |
172
+ | `apiKey` | `string` | API key for authentication (see [Authentication](#authentication)) |
53
173
  | `origin` | `string` | Custom target origin for postMessage |
54
174
  | `onReady` | `() => void` | Called when embed is ready |
55
175
  | `onLoaded` | `(graph) => void` | Called when machine is loaded |
@@ -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-DTIQmjHH.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 };