@statelyai/sdk 0.6.0 → 0.6.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
@@ -137,6 +137,8 @@ import {
137
137
  createStatelyClient,
138
138
  createStatelyEmbed,
139
139
  createStatelyInspector,
140
+ createS3AssetUploadAdapter,
141
+ createSupabaseAssetUploadAdapter,
140
142
  fromStudioMachine,
141
143
  graphToMachineConfig,
142
144
  graphToXStateTS,
@@ -156,6 +158,10 @@ import {
156
158
  } from '@statelyai/sdk/api';
157
159
  import { fromStudioMachine, toStudioMachine } from '@statelyai/sdk/graph';
158
160
  import { planSync, pullSync, pushSync } from '@statelyai/sdk/sync';
161
+ import {
162
+ createS3AssetUploadAdapter,
163
+ createSupabaseAssetUploadAdapter,
164
+ } from '@statelyai/sdk/assetStorage';
159
165
  import type { GraphPatch } from '@statelyai/sdk/patchTypes';
160
166
  ```
161
167
 
@@ -462,9 +468,54 @@ const embed = createStatelyEmbed({
462
468
  });
463
469
  ```
464
470
 
471
+ For reusable integrations, pass an upload adapter instead of wiring the callback inline:
472
+
473
+ ```ts
474
+ import { createStatelyEmbed, createS3AssetUploadAdapter } from '@statelyai/sdk';
475
+
476
+ const adapter = createS3AssetUploadAdapter({
477
+ bucket: 'assets',
478
+ publicBaseUrl: 'https://cdn.example.com/assets',
479
+ async getUploadTarget({ key, contentType }) {
480
+ const response = await fetch('/api/assets/presign', {
481
+ method: 'POST',
482
+ headers: { 'Content-Type': 'application/json' },
483
+ body: JSON.stringify({ key, contentType }),
484
+ });
485
+ return response.json();
486
+ },
487
+ });
488
+
489
+ const embed = createStatelyEmbed({
490
+ baseUrl: 'https://stately.ai',
491
+ assets: { adapter },
492
+ });
493
+ ```
494
+
495
+ Supabase storage can use the same adapter contract:
496
+
497
+ ```ts
498
+ import {
499
+ createStatelyEmbed,
500
+ createSupabaseAssetUploadAdapter,
501
+ } from '@statelyai/sdk';
502
+
503
+ const adapter = createSupabaseAssetUploadAdapter({
504
+ client: supabaseClient,
505
+ bucket: 'assets',
506
+ projectVersionId,
507
+ });
508
+
509
+ const embed = createStatelyEmbed({
510
+ baseUrl: 'https://stately.ai',
511
+ assets: { adapter },
512
+ });
513
+ ```
514
+
465
515
  | Option | Type | Description |
466
516
  | ----------------- | ------------------------------------------------------------------------- | ----------------------------------------------------------- |
467
- | `onUploadRequest` | `(file: File, context: { stateNodeId: string }) => Promise<UploadResult>` | **Required.** Called when the editor needs to upload a file |
517
+ | `onUploadRequest` | `(file: File, context: { stateNodeId: string }) => Promise<UploadResult>` | Called when the editor needs to upload a file |
518
+ | `adapter` | `AssetUploadAdapter` | Provider-specific uploader, such as the S3 or Supabase adapter |
468
519
  | `accept` | `string[]` | Accepted MIME types. Supports wildcards like `image/*` |
469
520
  | `maxFileSize` | `number` | Max file size in bytes. Defaults to `10_485_760` |
470
521
 
@@ -0,0 +1,73 @@
1
+ import { m as UploadResult } from "./protocol-CDoCcaIP.mjs";
2
+
3
+ //#region src/assetStorage.d.ts
4
+ interface AssetUploadContext {
5
+ stateNodeId: string;
6
+ }
7
+ interface AssetUploadRequest extends AssetUploadContext {
8
+ file: File;
9
+ }
10
+ interface AssetUploadAdapter {
11
+ upload(request: AssetUploadRequest): Promise<UploadResult>;
12
+ accept?: string[];
13
+ maxFileSize?: number;
14
+ }
15
+ type AssetObjectKeyFactory = (request: AssetUploadRequest) => string | Promise<string>;
16
+ interface S3UploadTarget {
17
+ uploadUrl: string;
18
+ publicUrl?: string;
19
+ method?: 'PUT' | 'POST';
20
+ headers?: Record<string, string>;
21
+ fields?: Record<string, string>;
22
+ }
23
+ interface CreateS3AssetUploadAdapterOptions {
24
+ bucket?: string;
25
+ publicBaseUrl?: string;
26
+ accept?: string[];
27
+ maxFileSize?: number;
28
+ key?: AssetObjectKeyFactory;
29
+ getUploadTarget: (request: AssetUploadRequest & {
30
+ key: string;
31
+ contentType: string;
32
+ bucket?: string;
33
+ }) => Promise<S3UploadTarget>;
34
+ }
35
+ interface SupabaseStorageBucket {
36
+ upload(path: string, file: File, options?: {
37
+ cacheControl?: string;
38
+ contentType?: string;
39
+ upsert?: boolean;
40
+ }): Promise<{
41
+ data: {
42
+ path: string;
43
+ } | null;
44
+ error: Error | {
45
+ message: string;
46
+ } | null;
47
+ }>;
48
+ getPublicUrl?(path: string): {
49
+ data: {
50
+ publicUrl: string;
51
+ };
52
+ };
53
+ }
54
+ interface SupabaseStorageClient {
55
+ storage: {
56
+ from(bucket: string): SupabaseStorageBucket;
57
+ };
58
+ }
59
+ interface CreateSupabaseAssetUploadAdapterOptions {
60
+ client: SupabaseStorageClient;
61
+ bucket?: string;
62
+ publicBaseUrl?: string;
63
+ projectVersionId?: string;
64
+ accept?: string[];
65
+ maxFileSize?: number;
66
+ cacheControl?: string;
67
+ upsert?: boolean;
68
+ key?: AssetObjectKeyFactory;
69
+ }
70
+ declare function createS3AssetUploadAdapter(options: CreateS3AssetUploadAdapterOptions): AssetUploadAdapter;
71
+ declare function createSupabaseAssetUploadAdapter(options: CreateSupabaseAssetUploadAdapterOptions): AssetUploadAdapter;
72
+ //#endregion
73
+ export { AssetObjectKeyFactory, AssetUploadAdapter, AssetUploadContext, AssetUploadRequest, CreateS3AssetUploadAdapterOptions, CreateSupabaseAssetUploadAdapterOptions, S3UploadTarget, SupabaseStorageClient, createS3AssetUploadAdapter, createSupabaseAssetUploadAdapter };
@@ -0,0 +1,99 @@
1
+ //#region src/assetStorage.ts
2
+ function sanitizeFileName(name) {
3
+ return name.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
4
+ }
5
+ function defaultObjectKey({ file, stateNodeId }) {
6
+ const fileName = sanitizeFileName(file.name) || "asset";
7
+ return `${stateNodeId}/${crypto.randomUUID()}-${fileName}`;
8
+ }
9
+ function createProjectObjectKey(projectVersionId) {
10
+ return ({ file }) => {
11
+ const fileName = sanitizeFileName(file.name) || "asset";
12
+ return `${projectVersionId}/${crypto.randomUUID()}-${fileName}`;
13
+ };
14
+ }
15
+ function joinUrl(baseUrl, key) {
16
+ return `${baseUrl.replace(/\/+$/, "")}/${key.split("/").map(encodeURIComponent).join("/")}`;
17
+ }
18
+ async function uploadToTarget(target, file) {
19
+ if (target.method === "POST" || target.fields) {
20
+ const formData = new FormData();
21
+ Object.entries(target.fields ?? {}).forEach(([name, value]) => {
22
+ formData.append(name, value);
23
+ });
24
+ formData.append("file", file);
25
+ const response = await fetch(target.uploadUrl, {
26
+ method: "POST",
27
+ body: formData
28
+ });
29
+ if (!response.ok) throw new Error(`Asset upload failed with ${response.status}`);
30
+ return;
31
+ }
32
+ const response = await fetch(target.uploadUrl, {
33
+ method: target.method ?? "PUT",
34
+ headers: {
35
+ "Content-Type": file.type || "application/octet-stream",
36
+ ...target.headers
37
+ },
38
+ body: file
39
+ });
40
+ if (!response.ok) throw new Error(`Asset upload failed with ${response.status}`);
41
+ }
42
+ function createS3AssetUploadAdapter(options) {
43
+ return {
44
+ accept: options.accept,
45
+ maxFileSize: options.maxFileSize,
46
+ async upload(request) {
47
+ const key = await (options.key ?? defaultObjectKey)(request);
48
+ const target = await options.getUploadTarget({
49
+ ...request,
50
+ key,
51
+ contentType: request.file.type || "application/octet-stream",
52
+ bucket: options.bucket
53
+ });
54
+ await uploadToTarget(target, request.file);
55
+ const url = target.publicUrl ?? (options.publicBaseUrl ? joinUrl(options.publicBaseUrl, key) : void 0);
56
+ if (!url) throw new Error("S3 asset upload adapter requires a publicUrl or publicBaseUrl");
57
+ return {
58
+ url,
59
+ name: request.file.name,
60
+ metadata: {
61
+ bucket: options.bucket,
62
+ key
63
+ }
64
+ };
65
+ }
66
+ };
67
+ }
68
+ function createSupabaseAssetUploadAdapter(options) {
69
+ const bucket = options.bucket ?? "assets";
70
+ return {
71
+ accept: options.accept,
72
+ maxFileSize: options.maxFileSize,
73
+ async upload(request) {
74
+ const storage = options.client.storage.from(bucket);
75
+ const key = await (options.key ?? (options.projectVersionId ? createProjectObjectKey(options.projectVersionId) : defaultObjectKey))(request);
76
+ const { data, error } = await storage.upload(key, request.file, {
77
+ cacheControl: options.cacheControl,
78
+ contentType: request.file.type || "application/octet-stream",
79
+ upsert: options.upsert
80
+ });
81
+ if (error) throw new Error(error.message);
82
+ if (!data?.path) throw new Error("Supabase asset upload did not return a path");
83
+ const publicUrl = storage.getPublicUrl?.(data.path)?.data.publicUrl ?? (options.publicBaseUrl ? joinUrl(options.publicBaseUrl, data.path) : void 0);
84
+ if (!publicUrl) throw new Error("Supabase asset upload adapter requires getPublicUrl or publicBaseUrl");
85
+ return {
86
+ url: publicUrl,
87
+ name: request.file.name,
88
+ metadata: {
89
+ bucket,
90
+ path: data.path,
91
+ key: data.path
92
+ }
93
+ };
94
+ }
95
+ };
96
+ }
97
+
98
+ //#endregion
99
+ export { createS3AssetUploadAdapter, createSupabaseAssetUploadAdapter };
package/dist/cli.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { ConnectedRepo, CreateProjectInput, ProjectData, StudioClient } from "./studio.mjs";
2
- import "./graph-CB-ALrdk.mjs";
2
+ import "./graph-DpBGHZwl.mjs";
3
3
  import { SyncPlan } from "./sync.mjs";
4
4
  import { Command } from "@oclif/core";
5
5
  import * as _oclif_core_interfaces0 from "@oclif/core/interfaces";
package/dist/cli.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { createStatelyClient } from "./studio.mjs";
3
- import "./graphToXStateTS-CvXM8wHL.mjs";
3
+ import "./graphToXStateTS-Gzh0ZqbN.mjs";
4
4
  import { planSync, pullSync } from "./sync.mjs";
5
5
  import fs from "node:fs/promises";
6
6
  import * as path$1 from "node:path";
package/dist/embed.d.mts CHANGED
@@ -1,4 +1,5 @@
1
- import { a as EmbedMode, c as ExportFormatMap, d as MachineSourceLocations, f as ProjectEmbedMachine, i as EmbedEventName, l as InitOptions, m as UploadResult, n as EmbedEventHandler, o as ExportCallOptions, r as EmbedEventMap, s as ExportFormat, t as CommentsConfig, u as MachineInitOptions } from "./protocol-CEbWQPYe.mjs";
1
+ import { a as EmbedMode, c as ExportFormatMap, d as MachineSourceLocations, f as ProjectEmbedMachine, i as EmbedEventName, l as InitOptions, m as UploadResult, n as EmbedEventHandler, o as ExportCallOptions, r as EmbedEventMap, s as ExportFormat, t as CommentsConfig, u as MachineInitOptions } from "./protocol-CDoCcaIP.mjs";
2
+ import { AssetUploadAdapter } from "./assetStorage.mjs";
2
3
 
3
4
  //#region src/embed.d.ts
4
5
  interface AssetConfig {
@@ -7,9 +8,15 @@ interface AssetConfig {
7
8
  * Receives a real File object (reconstructed from serialized data).
8
9
  * Throwing or rejecting will show an error toast in the embed.
9
10
  */
10
- onUploadRequest: (file: File, context: {
11
+ onUploadRequest?: (file: File, context: {
11
12
  stateNodeId: string;
12
13
  }) => Promise<UploadResult>;
14
+ /**
15
+ * Storage adapter used by the embed upload bridge. This keeps the editor
16
+ * protocol storage-neutral while allowing integrations to plug in S3, R2,
17
+ * Supabase, or any other backing store.
18
+ */
19
+ adapter?: AssetUploadAdapter;
13
20
  /**
14
21
  * Accepted MIME types. Supports wildcards (e.g. 'image/*').
15
22
  * @default ['image/*']
package/dist/embed.mjs CHANGED
@@ -43,6 +43,13 @@ function createStatelyEmbed(options) {
43
43
  const pendingMessages = [];
44
44
  const events = createEventRegistry();
45
45
  const exportManager = createPendingExportManager((message) => send(message));
46
+ const assetUploadAdapter = options.assets?.adapter;
47
+ const uploadAsset = options.assets?.onUploadRequest ? (file, context) => options.assets.onUploadRequest(file, context) : assetUploadAdapter ? (file, context) => assetUploadAdapter.upload({
48
+ file,
49
+ ...context
50
+ }) : void 0;
51
+ const assetAccept = options.assets?.accept ?? assetUploadAdapter?.accept;
52
+ const assetMaxFileSize = options.assets?.maxFileSize ?? assetUploadAdapter?.maxFileSize;
46
53
  function send(msg) {
47
54
  if (!transport?.ready) {
48
55
  pendingMessages.push(msg);
@@ -63,11 +70,11 @@ function createStatelyEmbed(options) {
63
70
  case "@statelyai.ready": {
64
71
  const ready = data;
65
72
  flush();
66
- if (options.assets?.onUploadRequest) send({
73
+ if (uploadAsset) send({
67
74
  type: "@statelyai.uploadCapabilities",
68
75
  enabled: true,
69
- accept: options.assets.accept,
70
- maxFileSize: options.assets.maxFileSize
76
+ accept: assetAccept,
77
+ maxFileSize: assetMaxFileSize
71
78
  });
72
79
  options.onReady?.();
73
80
  events.emit("ready", { version: ready.version });
@@ -129,7 +136,7 @@ function createStatelyEmbed(options) {
129
136
  }
130
137
  case "@statelyai.uploadRequest": {
131
138
  const req = data;
132
- if (!options.assets?.onUploadRequest) {
139
+ if (!uploadAsset) {
133
140
  send({
134
141
  type: "@statelyai.error",
135
142
  requestId: req.requestId,
@@ -138,7 +145,7 @@ function createStatelyEmbed(options) {
138
145
  });
139
146
  break;
140
147
  }
141
- const maxSize = options.assets.maxFileSize ?? 10485760;
148
+ const maxSize = assetMaxFileSize ?? 10485760;
142
149
  if (req.file.size > maxSize) {
143
150
  send({
144
151
  type: "@statelyai.error",
@@ -151,8 +158,7 @@ function createStatelyEmbed(options) {
151
158
  const binary = atob(req.file.data);
152
159
  const bytes = new Uint8Array(binary.length);
153
160
  for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
154
- const file = new File([bytes], req.file.name, { type: req.file.mimeType });
155
- options.assets.onUploadRequest(file, { stateNodeId: req.stateNodeId }).then((result) => {
161
+ uploadAsset(new File([bytes], req.file.name, { type: req.file.mimeType }), { stateNodeId: req.stateNodeId }).then((result) => {
156
162
  send({
157
163
  type: "@statelyai.uploadResponse",
158
164
  requestId: req.requestId,
package/dist/graph.d.mts CHANGED
@@ -1,2 +1,2 @@
1
- import { _ as studioMachineConverter, a as StatelyGraphData, c as StatelyInvoke, d as StudioAction, f as StudioEdge, g as fromStudioMachine, h as StudioNode, i as StatelyGraph, l as StatelyNodeData, m as StudioMachine, n as StatelyActorImplementation, o as StatelyGuard, p as StudioEventTypeData, r as StatelyEdgeData, s as StatelyImplementation, t as StatelyAction, u as StatelyTagImplementation, v as toStudioMachine } from "./graph-CB-ALrdk.mjs";
1
+ import { _ as studioMachineConverter, a as StatelyGraphData, c as StatelyInvoke, d as StudioAction, f as StudioEdge, g as fromStudioMachine, h as StudioNode, i as StatelyGraph, l as StatelyNodeData, m as StudioMachine, n as StatelyActorImplementation, o as StatelyGuard, p as StudioEventTypeData, r as StatelyEdgeData, s as StatelyImplementation, t as StatelyAction, u as StatelyTagImplementation, v as toStudioMachine } from "./graph-DpBGHZwl.mjs";
2
2
  export { StatelyAction, StatelyActorImplementation, StatelyEdgeData, StatelyGraph, StatelyGraphData, StatelyGuard, StatelyImplementation, StatelyInvoke, StatelyNodeData, StatelyTagImplementation, StudioAction, StudioEdge, StudioEventTypeData, StudioMachine, StudioNode, fromStudioMachine, studioMachineConverter, toStudioMachine };
package/dist/graph.mjs CHANGED
@@ -2,8 +2,12 @@ import { createFormatConverter, createGraph } from "@statelyai/graph";
2
2
 
3
3
  //#region src/graph.ts
4
4
  const EXPR_ACTION_TYPE = "xstate.expr";
5
+ function stripMarkdownLinks(code) {
6
+ return code.replace(/\[([^\]\n]+)\]\(([^)\n]+)\)/g, "$1");
7
+ }
5
8
  function stripExportDefault(code) {
6
- return code.match(/^export\s+default\s+(.+)/s)?.[1]?.trim() ?? code.trim();
9
+ const normalized = stripMarkdownLinks(code).trim();
10
+ return normalized.match(/^export\s+default\s+(.+)/s)?.[1]?.trim() ?? normalized;
7
11
  }
8
12
  function toJsonObject(value) {
9
13
  return value;
@@ -332,7 +336,7 @@ function fromStudioMachine(studioMachine) {
332
336
  ...edge.data.guard ? { guard: {
333
337
  type: edge.data.guard.type,
334
338
  params: toUnknownRecord(edge.data.guard.params),
335
- ...edge.data.guard.kind === "inline" ? { code: edge.data.guard.type } : {}
339
+ ...edge.data.guard.kind === "inline" ? { code: stripExportDefault(studioMachine.implementations?.guards?.[edge.data.guard.type]?.code ?? edge.data.guard.type) } : {}
336
340
  } } : {},
337
341
  actions: edge.data.actions.map((action) => fromStudioAction(action, studioMachine.implementations?.actions)),
338
342
  ...edge.data.description ? { description: edge.data.description } : {},
@@ -350,9 +354,9 @@ function fromStudioMachine(studioMachine) {
350
354
  data: {
351
355
  ...studioMachine.schemas ? { schemas: studioMachine.schemas } : {},
352
356
  ...studioMachine.implementations ? { implementations: {
353
- actions: Object.values(studioMachine.implementations.actions).map(fromActionSource),
354
- guards: Object.values(studioMachine.implementations.guards).map(fromGuardSource),
355
- actors: Object.values(studioMachine.implementations.actors).map(fromActorSource),
357
+ actions: Object.entries(studioMachine.implementations.actions).filter(([key]) => !key.startsWith("inline:")).map(([, source]) => fromActionSource(source)),
358
+ guards: Object.entries(studioMachine.implementations.guards).filter(([key]) => !key.startsWith("inline:")).map(([, guard]) => fromGuardSource(guard)),
359
+ actors: Object.entries(studioMachine.implementations.actors).filter(([key]) => !key.startsWith("inline:") && !/:invocation\[\d+\]$/.test(key)).map(([, actor]) => fromActorSource(actor)),
356
360
  delays: [],
357
361
  tags: []
358
362
  } } : {}