@flowdrop/flowdrop 1.14.0 → 2.0.0-beta.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 (218) hide show
  1. package/CHANGELOG.md +475 -0
  2. package/MIGRATION-2.0.md +472 -0
  3. package/README.md +23 -23
  4. package/dist/adapters/WorkflowAdapter.d.ts +1 -1
  5. package/dist/adapters/WorkflowAdapter.js +14 -8
  6. package/dist/adapters/agentspec/AgentSpecAdapter.js +7 -7
  7. package/dist/chat/batchFeedback.d.ts +39 -0
  8. package/dist/chat/batchFeedback.js +51 -0
  9. package/dist/commands/executor.js +15 -1
  10. package/dist/commands/storeIntegration.svelte.d.ts +4 -1
  11. package/dist/commands/storeIntegration.svelte.js +26 -21
  12. package/dist/commands/types.d.ts +2 -0
  13. package/dist/components/App.svelte +162 -192
  14. package/dist/components/App.svelte.d.ts +47 -8
  15. package/dist/components/ConfigForm.svelte +110 -66
  16. package/dist/components/ConfigModal.svelte +7 -2
  17. package/dist/components/ConnectionLine.svelte +4 -2
  18. package/dist/components/Navbar.svelte +61 -1
  19. package/dist/components/NodeSidebar.svelte +27 -45
  20. package/dist/components/NodeStatusOverlay.svelte +94 -6
  21. package/dist/components/NodeSwapPicker.svelte +10 -8
  22. package/dist/components/PipelineStatus.svelte +16 -67
  23. package/dist/components/PortCoordinateTracker.svelte +5 -6
  24. package/dist/components/SchemaForm.stories.svelte +1 -3
  25. package/dist/components/SchemaForm.svelte +45 -40
  26. package/dist/components/SchemaForm.svelte.d.ts +0 -8
  27. package/dist/components/SettingsModal.svelte +8 -3
  28. package/dist/components/SettingsPanel.svelte +20 -4
  29. package/dist/components/SwapMappingEditor.svelte +67 -49
  30. package/dist/components/SwapMappingEditor.svelte.d.ts +0 -2
  31. package/dist/components/UniversalNode.svelte +9 -7
  32. package/dist/components/WorkflowEditor.svelte +118 -111
  33. package/dist/components/WorkflowEditor.svelte.d.ts +18 -10
  34. package/dist/components/chat/AIChatPanel.svelte +93 -89
  35. package/dist/components/chat/AIChatPanel.svelte.d.ts +0 -4
  36. package/dist/components/chat/CommandPreview.svelte +2 -1
  37. package/dist/components/console/CommandConsole.svelte +7 -5
  38. package/dist/components/console/ConsoleAutocomplete.svelte +10 -11
  39. package/dist/components/console/ConsoleAutocomplete.svelte.d.ts +6 -0
  40. package/dist/components/console/ConsoleInput.svelte +15 -6
  41. package/dist/components/console/ConsoleOutput.svelte +2 -1
  42. package/dist/components/form/FormArray.svelte +5 -9
  43. package/dist/components/form/FormArray.svelte.d.ts +2 -1
  44. package/dist/components/form/FormAutocomplete.svelte +29 -13
  45. package/dist/components/form/FormField.svelte +4 -2
  46. package/dist/components/form/FormFieldLight.svelte +4 -2
  47. package/dist/components/form/FormMarkdownEditor.svelte +9 -4
  48. package/dist/components/form/FormRangeField.svelte +1 -0
  49. package/dist/components/form/FormTemplateEditor.svelte +11 -3
  50. package/dist/components/form/FormToggle.svelte +5 -12
  51. package/dist/components/form/FormToggle.svelte.d.ts +4 -2
  52. package/dist/components/form/templateAutocomplete.js +1 -5
  53. package/dist/components/form/types.d.ts +1 -14
  54. package/dist/components/interrupt/FormPrompt.svelte +3 -2
  55. package/dist/components/interrupt/InterruptBubble.svelte +16 -17
  56. package/dist/components/interrupt/ReviewPrompt.svelte +10 -3
  57. package/dist/components/interrupt/TextInputPrompt.svelte +2 -1
  58. package/dist/components/layouts/MainLayout.svelte +20 -13
  59. package/dist/components/layouts/MainLayout.svelte.d.ts +4 -0
  60. package/dist/components/nodes/AtomNode.svelte +292 -0
  61. package/dist/components/nodes/AtomNode.svelte.d.ts +26 -0
  62. package/dist/components/nodes/GatewayNode.svelte +19 -10
  63. package/dist/components/nodes/IdeaNode.svelte +7 -0
  64. package/dist/components/nodes/SimpleNode.svelte +11 -6
  65. package/dist/components/nodes/SquareNode.svelte +15 -8
  66. package/dist/components/nodes/TerminalNode.svelte +9 -4
  67. package/dist/components/nodes/ToolNode.svelte +7 -1
  68. package/dist/components/nodes/WorkflowNode.svelte +16 -7
  69. package/dist/components/playground/ChatInput.svelte +11 -14
  70. package/dist/components/playground/ChatPanel.svelte +6 -49
  71. package/dist/components/playground/ChatPanel.svelte.d.ts +0 -14
  72. package/dist/components/playground/ControlPanel.svelte +134 -123
  73. package/dist/components/playground/ControlPanel.svelte.d.ts +3 -0
  74. package/dist/components/playground/ExecutionLogs.svelte +11 -9
  75. package/dist/components/playground/InputCollector.svelte +11 -9
  76. package/dist/components/playground/MessageStream.svelte +17 -23
  77. package/dist/components/playground/PipelineKanbanView.svelte +65 -6
  78. package/dist/components/playground/PipelinePanel.svelte +11 -5
  79. package/dist/components/playground/PipelineTableView.svelte +186 -44
  80. package/dist/components/playground/Playground.svelte +95 -92
  81. package/dist/components/playground/Playground.svelte.d.ts +2 -0
  82. package/dist/components/playground/PlaygroundApp.svelte +6 -1
  83. package/dist/components/playground/PlaygroundApp.svelte.d.ts +3 -0
  84. package/dist/components/playground/PlaygroundModal.svelte +13 -3
  85. package/dist/components/playground/PlaygroundModal.svelte.d.ts +3 -0
  86. package/dist/components/playground/PlaygroundStudio.svelte +34 -32
  87. package/dist/components/playground/PlaygroundStudio.svelte.d.ts +3 -0
  88. package/dist/components/playground/SessionManager.svelte +9 -12
  89. package/dist/components/playground/pipelineViewUtils.svelte.d.ts +28 -0
  90. package/dist/components/playground/pipelineViewUtils.svelte.js +38 -1
  91. package/dist/config/endpoints.d.ts +0 -7
  92. package/dist/config/endpoints.js +2 -10
  93. package/dist/core/index.d.ts +4 -4
  94. package/dist/core/index.js +6 -6
  95. package/dist/display/index.d.ts +0 -2
  96. package/dist/display/index.js +0 -6
  97. package/dist/editor/index.d.ts +19 -20
  98. package/dist/editor/index.js +25 -35
  99. package/dist/form/code.d.ts +25 -15
  100. package/dist/form/code.js +44 -41
  101. package/dist/form/fieldRegistry.d.ts +17 -13
  102. package/dist/form/fieldRegistry.js +32 -12
  103. package/dist/form/full.d.ts +17 -13
  104. package/dist/form/full.js +22 -27
  105. package/dist/form/index.d.ts +3 -3
  106. package/dist/form/index.js +3 -3
  107. package/dist/form/markdown.d.ts +13 -8
  108. package/dist/form/markdown.js +22 -23
  109. package/dist/helpers/proximityConnect.d.ts +7 -3
  110. package/dist/helpers/proximityConnect.js +19 -6
  111. package/dist/helpers/workflowEditorHelper.d.ts +12 -5
  112. package/dist/helpers/workflowEditorHelper.js +27 -25
  113. package/dist/index.d.ts +28 -24
  114. package/dist/index.js +27 -50
  115. package/dist/messages/defaults.d.ts +2 -5
  116. package/dist/messages/defaults.js +3 -6
  117. package/dist/messages/index.d.ts +0 -1
  118. package/dist/messages/index.js +0 -1
  119. package/dist/mocks/app-forms.d.ts +6 -2
  120. package/dist/mocks/app-forms.js +11 -4
  121. package/dist/openapi/v1/openapi.yaml +227 -164
  122. package/dist/playground/index.d.ts +2 -3
  123. package/dist/playground/index.js +2 -30
  124. package/dist/playground/mount.d.ts +15 -0
  125. package/dist/playground/mount.js +46 -20
  126. package/dist/registry/{BaseRegistry.d.ts → BaseRegistry.svelte.d.ts} +22 -1
  127. package/dist/registry/{BaseRegistry.js → BaseRegistry.svelte.js} +37 -1
  128. package/dist/registry/builtinFormats.d.ts +9 -18
  129. package/dist/registry/builtinFormats.js +9 -39
  130. package/dist/registry/builtinNodes.d.ts +1 -26
  131. package/dist/registry/builtinNodes.js +14 -50
  132. package/dist/registry/index.d.ts +3 -4
  133. package/dist/registry/index.js +4 -6
  134. package/dist/registry/nodeComponentRegistry.d.ts +182 -15
  135. package/dist/registry/nodeComponentRegistry.js +235 -17
  136. package/dist/registry/workflowFormatRegistry.d.ts +14 -9
  137. package/dist/registry/workflowFormatRegistry.js +24 -8
  138. package/dist/{schema → schemas}/index.d.ts +2 -2
  139. package/dist/{schema → schemas}/index.js +2 -2
  140. package/dist/schemas/v1/workflow.schema.json +53 -6
  141. package/dist/services/agentSpecExecutionService.js +0 -1
  142. package/dist/services/apiVariableService.d.ts +2 -1
  143. package/dist/services/apiVariableService.js +5 -22
  144. package/dist/services/autoSaveService.d.ts +7 -0
  145. package/dist/services/autoSaveService.js +6 -4
  146. package/dist/services/chatService.d.ts +8 -4
  147. package/dist/services/chatService.js +15 -15
  148. package/dist/services/draftStorage.d.ts +129 -13
  149. package/dist/services/draftStorage.js +185 -37
  150. package/dist/services/dynamicSchemaService.d.ts +2 -1
  151. package/dist/services/dynamicSchemaService.js +5 -22
  152. package/dist/services/globalSave.d.ts +13 -12
  153. package/dist/services/globalSave.js +29 -51
  154. package/dist/services/historyService.d.ts +9 -3
  155. package/dist/services/historyService.js +9 -3
  156. package/dist/services/interruptService.d.ts +14 -9
  157. package/dist/services/interruptService.js +27 -27
  158. package/dist/services/nodeExecutionService.d.ts +18 -3
  159. package/dist/services/nodeExecutionService.js +71 -45
  160. package/dist/services/playgroundService.d.ts +14 -9
  161. package/dist/services/playgroundService.js +31 -30
  162. package/dist/services/variableService.d.ts +2 -1
  163. package/dist/services/variableService.js +2 -2
  164. package/dist/services/workflowStorage.js +6 -6
  165. package/dist/stores/apiContext.d.ts +45 -0
  166. package/dist/stores/apiContext.js +65 -0
  167. package/dist/stores/categoriesStore.svelte.d.ts +28 -23
  168. package/dist/stores/categoriesStore.svelte.js +70 -64
  169. package/dist/stores/getInstance.svelte.d.ts +39 -0
  170. package/dist/stores/getInstance.svelte.js +65 -0
  171. package/dist/stores/historyStore.svelte.d.ts +77 -93
  172. package/dist/stores/historyStore.svelte.js +134 -160
  173. package/dist/stores/instanceContainer.svelte.d.ts +111 -0
  174. package/dist/stores/instanceContainer.svelte.js +114 -0
  175. package/dist/stores/interruptStore.svelte.d.ts +112 -82
  176. package/dist/stores/interruptStore.svelte.js +253 -226
  177. package/dist/stores/pipelinePanelStore.svelte.d.ts +27 -3
  178. package/dist/stores/pipelinePanelStore.svelte.js +61 -14
  179. package/dist/stores/playgroundStore.svelte.d.ts +169 -216
  180. package/dist/stores/playgroundStore.svelte.js +515 -572
  181. package/dist/stores/portCoordinateStore.svelte.d.ts +57 -51
  182. package/dist/stores/portCoordinateStore.svelte.js +109 -98
  183. package/dist/stores/settingsStore.svelte.d.ts +4 -1
  184. package/dist/stores/settingsStore.svelte.js +47 -12
  185. package/dist/stores/workflowStore.svelte.d.ts +178 -213
  186. package/dist/stores/workflowStore.svelte.js +449 -501
  187. package/dist/stories/EdgeDecorator.svelte +5 -2
  188. package/dist/stories/NodeDecorator.svelte +5 -3
  189. package/dist/svelte-app.d.ts +60 -10
  190. package/dist/svelte-app.js +157 -53
  191. package/dist/types/events.d.ts +6 -3
  192. package/dist/types/index.d.ts +71 -6
  193. package/dist/types/navbar.d.ts +7 -0
  194. package/dist/types/playground.d.ts +18 -3
  195. package/dist/types/settings.d.ts +13 -0
  196. package/dist/types/settings.js +1 -0
  197. package/dist/utils/colors.d.ts +47 -21
  198. package/dist/utils/colors.js +69 -68
  199. package/dist/utils/connections.d.ts +9 -15
  200. package/dist/utils/connections.js +13 -32
  201. package/dist/utils/duration.d.ts +13 -0
  202. package/dist/utils/duration.js +45 -0
  203. package/dist/utils/formMerge.d.ts +36 -0
  204. package/dist/utils/formMerge.js +70 -0
  205. package/dist/utils/icons.d.ts +5 -2
  206. package/dist/utils/icons.js +6 -5
  207. package/dist/utils/nodeSwap.d.ts +6 -2
  208. package/dist/utils/nodeSwap.js +62 -126
  209. package/dist/utils/nodeTypes.d.ts +17 -8
  210. package/dist/utils/nodeTypes.js +27 -19
  211. package/dist/utils/performanceUtils.js +7 -0
  212. package/package.json +6 -5
  213. package/dist/messages/deprecation.d.ts +0 -20
  214. package/dist/messages/deprecation.js +0 -33
  215. package/dist/registry/plugin.d.ts +0 -215
  216. package/dist/registry/plugin.js +0 -249
  217. package/dist/services/api.d.ts +0 -129
  218. package/dist/services/api.js +0 -217
@@ -1,12 +1,82 @@
1
1
  /**
2
2
  * Draft Storage Service for FlowDrop
3
3
  *
4
- * Handles saving and loading workflow drafts to/from localStorage.
4
+ * Handles saving and loading workflow drafts to/from browser storage
5
+ * (localStorage by default, configurable via {@link setDraftStorage}).
5
6
  * Provides interval-based auto-save functionality.
6
7
  *
7
8
  * @module services/draftStorage
8
9
  */
9
10
  import type { Workflow } from '../types/index.js';
11
+ /**
12
+ * Minimal storage adapter used for draft persistence.
13
+ *
14
+ * Implement this to back drafts with anything other than the built-in
15
+ * `localStorage` / `sessionStorage` options — e.g. an in-memory store, a
16
+ * key-prefixed wrapper, or a synchronous write-through cache.
17
+ *
18
+ * Note the interface is deliberately **synchronous**. Async backends
19
+ * (IndexedDB, network storage, WebCrypto-encrypted stores) cannot implement
20
+ * it directly — put a synchronous in-memory cache in front and flush to the
21
+ * async backend out of band. An `async` method assigned here will type-check
22
+ * (a Promise is assignable to `void`) but its errors are silently swallowed.
23
+ */
24
+ export interface DraftStorageAdapter {
25
+ /** Read a value, or null when absent */
26
+ getItem(key: string): string | null;
27
+ /** Write a value */
28
+ setItem(key: string, value: string): void;
29
+ /** Remove a value */
30
+ removeItem(key: string): void;
31
+ /** List all keys currently held by the adapter */
32
+ keys(): string[];
33
+ }
34
+ /**
35
+ * Built-in storage backends
36
+ * - 'local': localStorage — drafts persist on the device even after the tab
37
+ * or browser is closed, until saved or cleared
38
+ * - 'session': sessionStorage — drafts are scoped to the tab and removed
39
+ * when it closes (they do not survive crash-and-reopen)
40
+ */
41
+ export type DraftStorageType = 'local' | 'session';
42
+ /** Accepted values for the `draftStorage` mount option */
43
+ export type DraftStorageOption = DraftStorageType | DraftStorageAdapter;
44
+ /**
45
+ * Resolve a {@link DraftStorageOption} to a concrete adapter.
46
+ *
47
+ * `'local'` / `'session'` map to the built-in Web Storage adapters, a custom
48
+ * adapter is returned as-is, and `undefined` resolves to the current
49
+ * module-level default (see {@link setDraftStorage}).
50
+ */
51
+ export declare function resolveDraftStorage(option?: DraftStorageOption): DraftStorageAdapter;
52
+ /**
53
+ * Configure the module-level default for where workflow drafts are persisted.
54
+ *
55
+ * - `'local'` (default): `localStorage` — drafts survive reloads and browser
56
+ * restarts, and remain stored on the device even after the tab is closed,
57
+ * until saved or cleared.
58
+ * - `'session'`: `sessionStorage` — drafts are scoped to the current tab and
59
+ * removed when it closes. Note this also means drafts do not survive a
60
+ * crash-and-reopen.
61
+ * - A custom {@link DraftStorageAdapter} for anything else.
62
+ *
63
+ * This sets the default used by the standalone helpers and by managers
64
+ * constructed without an explicit `storage` option. Mounted apps capture
65
+ * their own adapter at mount time, so calling this does not retarget
66
+ * already-running instances. With multiple mounts the most recent mount's
67
+ * backend wins *for the standalone helpers only*.
68
+ *
69
+ * Security note: neither built-in backend protects against same-origin
70
+ * script access (XSS) — both are readable by any script on the page. If
71
+ * workflows may contain secrets in node configs, disable drafts via
72
+ * `features.autoSaveDraft: false` or keep them off-disk with a custom
73
+ * in-memory adapter.
74
+ */
75
+ export declare function setDraftStorage(option: DraftStorageOption): void;
76
+ /**
77
+ * Get the current module-level default draft storage adapter
78
+ */
79
+ export declare function getDraftStorage(): DraftStorageAdapter;
10
80
  /**
11
81
  * Draft metadata stored alongside the workflow
12
82
  */
@@ -19,7 +89,7 @@ interface DraftMetadata {
19
89
  workflowName?: string;
20
90
  }
21
91
  /**
22
- * Complete draft data stored in localStorage
92
+ * Complete draft data stored in draft storage
23
93
  */
24
94
  interface StoredDraft {
25
95
  /** The workflow data */
@@ -27,6 +97,14 @@ interface StoredDraft {
27
97
  /** Draft metadata */
28
98
  metadata: DraftMetadata;
29
99
  }
100
+ /**
101
+ * Storage prefix of the page-default instance (`flowdrop:draft:default`).
102
+ *
103
+ * Every instance — including the default — uses an id-scoped prefix since
104
+ * v2.0. Standalone helper callers that don't pass a prefix get this one, so
105
+ * they keep addressing the default editor's drafts.
106
+ */
107
+ export declare const DEFAULT_INSTANCE_DRAFT_PREFIX = "flowdrop:draft:default";
30
108
  /**
31
109
  * Generate a storage key for a workflow
32
110
  *
@@ -35,48 +113,70 @@ interface StoredDraft {
35
113
  *
36
114
  * @param workflowId - The workflow ID (optional)
37
115
  * @param customKey - Custom storage key provided by enterprise (optional)
116
+ * @param prefix - Key namespace; pass a FlowDrop instance's `storagePrefix`
117
+ * to scope drafts per instance. Defaults to the page-default instance's
118
+ * prefix.
38
119
  * @returns The storage key to use
39
120
  */
40
- export declare function getDraftStorageKey(workflowId?: string, customKey?: string): string;
121
+ export declare function getDraftStorageKey(workflowId?: string, customKey?: string, prefix?: string): string;
122
+ /**
123
+ * One-time migration of a 1.x draft key to its 2.0 scoped equivalent.
124
+ *
125
+ * In 1.x the page-default instance stored drafts under the bare
126
+ * `flowdrop:draft:<workflowId>` key; since 2.0 it uses
127
+ * `flowdrop:draft:default:<workflowId>`. When the scoped key is empty and the
128
+ * legacy key holds a draft, the draft is moved (copied, then the legacy key
129
+ * removed) so users upgrading mid-edit don't lose work.
130
+ *
131
+ * @param legacyKey - The 1.x bare-prefix key
132
+ * @param scopedKey - The 2.0 instance-scoped key
133
+ * @param storage - Adapter to migrate within (defaults to the module-level default)
134
+ */
135
+ export declare function migrateLegacyDraftKey(legacyKey: string, scopedKey: string, storage?: DraftStorageAdapter): void;
41
136
  /**
42
- * Save a workflow draft to localStorage
137
+ * Save a workflow draft to draft storage
43
138
  *
44
139
  * @param workflow - The workflow to save
45
140
  * @param storageKey - The storage key to use
141
+ * @param storage - Adapter to write to (defaults to the module-level default)
46
142
  * @returns true if saved successfully, false otherwise
47
143
  */
48
- export declare function saveDraft(workflow: Workflow, storageKey: string): boolean;
144
+ export declare function saveDraft(workflow: Workflow, storageKey: string, storage?: DraftStorageAdapter): boolean;
49
145
  /**
50
- * Load a workflow draft from localStorage
146
+ * Load a workflow draft from draft storage
51
147
  *
52
148
  * @param storageKey - The storage key to load from
149
+ * @param storage - Adapter to read from (defaults to the module-level default)
53
150
  * @returns The stored draft, or null if not found
54
151
  */
55
- export declare function loadDraft(storageKey: string): StoredDraft | null;
152
+ export declare function loadDraft(storageKey: string, storage?: DraftStorageAdapter): StoredDraft | null;
56
153
  /**
57
- * Delete a workflow draft from localStorage
154
+ * Delete a workflow draft from draft storage
58
155
  *
59
156
  * @param storageKey - The storage key to delete
157
+ * @param storage - Adapter to delete from (defaults to the module-level default)
60
158
  */
61
- export declare function deleteDraft(storageKey: string): void;
159
+ export declare function deleteDraft(storageKey: string, storage?: DraftStorageAdapter): void;
62
160
  /**
63
161
  * Check if a draft exists for a given storage key
64
162
  *
65
163
  * @param storageKey - The storage key to check
164
+ * @param storage - Adapter to check (defaults to the module-level default)
66
165
  * @returns true if a draft exists
67
166
  */
68
- export declare function hasDraft(storageKey: string): boolean;
167
+ export declare function hasDraft(storageKey: string, storage?: DraftStorageAdapter): boolean;
69
168
  /**
70
169
  * Get draft metadata without loading the full workflow
71
170
  *
72
171
  * Useful for displaying draft information without parsing the entire workflow.
73
172
  *
74
173
  * @param storageKey - The storage key to check
174
+ * @param storage - Adapter to read from (defaults to the module-level default)
75
175
  * @returns Draft metadata, or null if not found
76
176
  */
77
- export declare function getDraftMetadata(storageKey: string): DraftMetadata | null;
177
+ export declare function getDraftMetadata(storageKey: string, storage?: DraftStorageAdapter): DraftMetadata | null;
78
178
  /**
79
- * Clear all FlowDrop drafts from localStorage
179
+ * Clear all FlowDrop drafts from draft storage
80
180
  *
81
181
  * Removes every key beginning with `flowdrop:draft:`. Intended to be called
82
182
  * from a host application's logout handler so workflow drafts do not persist
@@ -85,9 +185,10 @@ export declare function getDraftMetadata(storageKey: string): DraftMetadata | nu
85
185
  * @param extraKeys - Additional explicit keys to remove. Pass any custom
86
186
  * `draftStorageKey` values configured at mount time so they are cleared
87
187
  * alongside the default-prefixed keys.
188
+ * @param storage - Adapter to clear (defaults to the module-level default)
88
189
  * @returns The number of entries removed.
89
190
  */
90
- export declare function clearAllDrafts(extraKeys?: readonly string[]): number;
191
+ export declare function clearAllDrafts(extraKeys?: readonly string[], storage?: DraftStorageAdapter): number;
91
192
  /**
92
193
  * Draft auto-save manager
93
194
  *
@@ -107,6 +208,17 @@ export declare class DraftAutoSaveManager {
107
208
  private getWorkflow;
108
209
  /** Function to check if workflow is dirty */
109
210
  private isDirty;
211
+ /**
212
+ * Runtime gate for draft persistence (e.g. a user-facing opt-out setting).
213
+ * Checked on every save, so it can change after construction.
214
+ */
215
+ private isPersistenceAllowed;
216
+ /**
217
+ * Storage adapter this instance writes to.
218
+ * Captured at construction, so a later `setDraftStorage()` call (e.g. from
219
+ * another FlowDrop mount on the same page) cannot retarget this manager.
220
+ */
221
+ private storage;
110
222
  /** Last saved workflow hash (for change detection) */
111
223
  private lastSavedHash;
112
224
  /**
@@ -120,6 +232,10 @@ export declare class DraftAutoSaveManager {
120
232
  enabled: boolean;
121
233
  getWorkflow: () => Workflow | null;
122
234
  isDirty: () => boolean;
235
+ /** Optional runtime gate — return false to suppress draft writes (default: always allowed) */
236
+ isPersistenceAllowed?: () => boolean;
237
+ /** Storage backend for this instance (default: the module-level default at construction time) */
238
+ storage?: DraftStorageOption;
123
239
  });
124
240
  /**
125
241
  * Start auto-save interval
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * Draft Storage Service for FlowDrop
3
3
  *
4
- * Handles saving and loading workflow drafts to/from localStorage.
4
+ * Handles saving and loading workflow drafts to/from browser storage
5
+ * (localStorage by default, configurable via {@link setDraftStorage}).
5
6
  * Provides interval-based auto-save functionality.
6
7
  *
7
8
  * @module services/draftStorage
@@ -11,6 +12,96 @@ import { logger } from '../utils/logger.js';
11
12
  * Default storage key prefix
12
13
  */
13
14
  const STORAGE_KEY_PREFIX = 'flowdrop:draft';
15
+ /**
16
+ * Wrap a Web Storage object (localStorage/sessionStorage) as a DraftStorageAdapter.
17
+ * The storage global is resolved lazily so SSR and test environments that
18
+ * replace the globals keep working.
19
+ */
20
+ function createWebStorageAdapter(getStorage) {
21
+ return {
22
+ getItem: (key) => getStorage().getItem(key),
23
+ setItem: (key, value) => getStorage().setItem(key, value),
24
+ removeItem: (key) => getStorage().removeItem(key),
25
+ keys: () => {
26
+ const storage = getStorage();
27
+ const keys = [];
28
+ for (let i = 0; i < storage.length; i++) {
29
+ const key = storage.key(i);
30
+ if (key !== null) {
31
+ keys.push(key);
32
+ }
33
+ }
34
+ return keys;
35
+ }
36
+ };
37
+ }
38
+ const WEB_STORAGE_ADAPTERS = {
39
+ local: createWebStorageAdapter(() => localStorage),
40
+ session: createWebStorageAdapter(() => sessionStorage)
41
+ };
42
+ /**
43
+ * Module-level default adapter (localStorage by default).
44
+ *
45
+ * Used by the standalone helpers (`saveDraft`, `clearAllDrafts`, ...) when no
46
+ * explicit adapter is passed, and as the fallback for managers constructed
47
+ * without a `storage` option. Per-instance code (each `DraftAutoSaveManager`,
48
+ * each mounted app) captures its own resolved adapter instead, so multiple
49
+ * FlowDrop instances with different backends do not interfere.
50
+ */
51
+ let activeAdapter = WEB_STORAGE_ADAPTERS.local;
52
+ /**
53
+ * Resolve a {@link DraftStorageOption} to a concrete adapter.
54
+ *
55
+ * `'local'` / `'session'` map to the built-in Web Storage adapters, a custom
56
+ * adapter is returned as-is, and `undefined` resolves to the current
57
+ * module-level default (see {@link setDraftStorage}).
58
+ */
59
+ export function resolveDraftStorage(option) {
60
+ if (option === undefined) {
61
+ return activeAdapter;
62
+ }
63
+ return typeof option === 'string' ? WEB_STORAGE_ADAPTERS[option] : option;
64
+ }
65
+ /**
66
+ * Configure the module-level default for where workflow drafts are persisted.
67
+ *
68
+ * - `'local'` (default): `localStorage` — drafts survive reloads and browser
69
+ * restarts, and remain stored on the device even after the tab is closed,
70
+ * until saved or cleared.
71
+ * - `'session'`: `sessionStorage` — drafts are scoped to the current tab and
72
+ * removed when it closes. Note this also means drafts do not survive a
73
+ * crash-and-reopen.
74
+ * - A custom {@link DraftStorageAdapter} for anything else.
75
+ *
76
+ * This sets the default used by the standalone helpers and by managers
77
+ * constructed without an explicit `storage` option. Mounted apps capture
78
+ * their own adapter at mount time, so calling this does not retarget
79
+ * already-running instances. With multiple mounts the most recent mount's
80
+ * backend wins *for the standalone helpers only*.
81
+ *
82
+ * Security note: neither built-in backend protects against same-origin
83
+ * script access (XSS) — both are readable by any script on the page. If
84
+ * workflows may contain secrets in node configs, disable drafts via
85
+ * `features.autoSaveDraft: false` or keep them off-disk with a custom
86
+ * in-memory adapter.
87
+ */
88
+ export function setDraftStorage(option) {
89
+ activeAdapter = resolveDraftStorage(option);
90
+ }
91
+ /**
92
+ * Get the current module-level default draft storage adapter
93
+ */
94
+ export function getDraftStorage() {
95
+ return activeAdapter;
96
+ }
97
+ /**
98
+ * Storage prefix of the page-default instance (`flowdrop:draft:default`).
99
+ *
100
+ * Every instance — including the default — uses an id-scoped prefix since
101
+ * v2.0. Standalone helper callers that don't pass a prefix get this one, so
102
+ * they keep addressing the default editor's drafts.
103
+ */
104
+ export const DEFAULT_INSTANCE_DRAFT_PREFIX = `${STORAGE_KEY_PREFIX}:default`;
14
105
  /**
15
106
  * Generate a storage key for a workflow
16
107
  *
@@ -19,25 +110,58 @@ const STORAGE_KEY_PREFIX = 'flowdrop:draft';
19
110
  *
20
111
  * @param workflowId - The workflow ID (optional)
21
112
  * @param customKey - Custom storage key provided by enterprise (optional)
113
+ * @param prefix - Key namespace; pass a FlowDrop instance's `storagePrefix`
114
+ * to scope drafts per instance. Defaults to the page-default instance's
115
+ * prefix.
22
116
  * @returns The storage key to use
23
117
  */
24
- export function getDraftStorageKey(workflowId, customKey) {
118
+ export function getDraftStorageKey(workflowId, customKey, prefix = DEFAULT_INSTANCE_DRAFT_PREFIX) {
25
119
  if (customKey) {
26
120
  return customKey;
27
121
  }
28
122
  if (workflowId) {
29
- return `${STORAGE_KEY_PREFIX}:${workflowId}`;
123
+ return `${prefix}:${workflowId}`;
30
124
  }
31
- return `${STORAGE_KEY_PREFIX}:new`;
125
+ return `${prefix}:new`;
32
126
  }
33
127
  /**
34
- * Save a workflow draft to localStorage
128
+ * One-time migration of a 1.x draft key to its 2.0 scoped equivalent.
129
+ *
130
+ * In 1.x the page-default instance stored drafts under the bare
131
+ * `flowdrop:draft:<workflowId>` key; since 2.0 it uses
132
+ * `flowdrop:draft:default:<workflowId>`. When the scoped key is empty and the
133
+ * legacy key holds a draft, the draft is moved (copied, then the legacy key
134
+ * removed) so users upgrading mid-edit don't lose work.
135
+ *
136
+ * @param legacyKey - The 1.x bare-prefix key
137
+ * @param scopedKey - The 2.0 instance-scoped key
138
+ * @param storage - Adapter to migrate within (defaults to the module-level default)
139
+ */
140
+ export function migrateLegacyDraftKey(legacyKey, scopedKey, storage = activeAdapter) {
141
+ if (legacyKey === scopedKey)
142
+ return;
143
+ try {
144
+ if (storage.getItem(scopedKey) !== null)
145
+ return;
146
+ const legacy = storage.getItem(legacyKey);
147
+ if (legacy === null)
148
+ return;
149
+ storage.setItem(scopedKey, legacy);
150
+ storage.removeItem(legacyKey);
151
+ }
152
+ catch (error) {
153
+ logger.warn('Failed to migrate legacy draft key:', error);
154
+ }
155
+ }
156
+ /**
157
+ * Save a workflow draft to draft storage
35
158
  *
36
159
  * @param workflow - The workflow to save
37
160
  * @param storageKey - The storage key to use
161
+ * @param storage - Adapter to write to (defaults to the module-level default)
38
162
  * @returns true if saved successfully, false otherwise
39
163
  */
40
- export function saveDraft(workflow, storageKey) {
164
+ export function saveDraft(workflow, storageKey, storage = activeAdapter) {
41
165
  try {
42
166
  const draft = {
43
167
  workflow,
@@ -47,62 +171,65 @@ export function saveDraft(workflow, storageKey) {
47
171
  workflowName: workflow.name
48
172
  }
49
173
  };
50
- localStorage.setItem(storageKey, JSON.stringify(draft));
174
+ storage.setItem(storageKey, JSON.stringify(draft));
51
175
  return true;
52
176
  }
53
177
  catch (error) {
54
- // localStorage might be full or disabled
55
- logger.warn('Failed to save draft to localStorage:', error);
178
+ // Storage might be full or disabled
179
+ logger.warn('Failed to save draft to storage:', error);
56
180
  return false;
57
181
  }
58
182
  }
59
183
  /**
60
- * Load a workflow draft from localStorage
184
+ * Load a workflow draft from draft storage
61
185
  *
62
186
  * @param storageKey - The storage key to load from
187
+ * @param storage - Adapter to read from (defaults to the module-level default)
63
188
  * @returns The stored draft, or null if not found
64
189
  */
65
- export function loadDraft(storageKey) {
190
+ export function loadDraft(storageKey, storage = activeAdapter) {
66
191
  try {
67
- const stored = localStorage.getItem(storageKey);
192
+ const stored = storage.getItem(storageKey);
68
193
  if (!stored) {
69
194
  return null;
70
195
  }
71
196
  const draft = JSON.parse(stored);
72
197
  // Validate the draft structure
73
198
  if (!draft.workflow || !draft.metadata) {
74
- logger.warn('Invalid draft structure in localStorage');
199
+ logger.warn('Invalid draft structure in storage');
75
200
  return null;
76
201
  }
77
202
  return draft;
78
203
  }
79
204
  catch (error) {
80
- logger.warn('Failed to load draft from localStorage:', error);
205
+ logger.warn('Failed to load draft from storage:', error);
81
206
  return null;
82
207
  }
83
208
  }
84
209
  /**
85
- * Delete a workflow draft from localStorage
210
+ * Delete a workflow draft from draft storage
86
211
  *
87
212
  * @param storageKey - The storage key to delete
213
+ * @param storage - Adapter to delete from (defaults to the module-level default)
88
214
  */
89
- export function deleteDraft(storageKey) {
215
+ export function deleteDraft(storageKey, storage = activeAdapter) {
90
216
  try {
91
- localStorage.removeItem(storageKey);
217
+ storage.removeItem(storageKey);
92
218
  }
93
219
  catch (error) {
94
- logger.warn('Failed to delete draft from localStorage:', error);
220
+ logger.warn('Failed to delete draft from storage:', error);
95
221
  }
96
222
  }
97
223
  /**
98
224
  * Check if a draft exists for a given storage key
99
225
  *
100
226
  * @param storageKey - The storage key to check
227
+ * @param storage - Adapter to check (defaults to the module-level default)
101
228
  * @returns true if a draft exists
102
229
  */
103
- export function hasDraft(storageKey) {
230
+ export function hasDraft(storageKey, storage = activeAdapter) {
104
231
  try {
105
- return localStorage.getItem(storageKey) !== null;
232
+ return storage.getItem(storageKey) !== null;
106
233
  }
107
234
  catch {
108
235
  return false;
@@ -114,14 +241,15 @@ export function hasDraft(storageKey) {
114
241
  * Useful for displaying draft information without parsing the entire workflow.
115
242
  *
116
243
  * @param storageKey - The storage key to check
244
+ * @param storage - Adapter to read from (defaults to the module-level default)
117
245
  * @returns Draft metadata, or null if not found
118
246
  */
119
- export function getDraftMetadata(storageKey) {
120
- const draft = loadDraft(storageKey);
247
+ export function getDraftMetadata(storageKey, storage = activeAdapter) {
248
+ const draft = loadDraft(storageKey, storage);
121
249
  return draft?.metadata ?? null;
122
250
  }
123
251
  /**
124
- * Clear all FlowDrop drafts from localStorage
252
+ * Clear all FlowDrop drafts from draft storage
125
253
  *
126
254
  * Removes every key beginning with `flowdrop:draft:`. Intended to be called
127
255
  * from a host application's logout handler so workflow drafts do not persist
@@ -130,29 +258,29 @@ export function getDraftMetadata(storageKey) {
130
258
  * @param extraKeys - Additional explicit keys to remove. Pass any custom
131
259
  * `draftStorageKey` values configured at mount time so they are cleared
132
260
  * alongside the default-prefixed keys.
261
+ * @param storage - Adapter to clear (defaults to the module-level default)
133
262
  * @returns The number of entries removed.
134
263
  */
135
- export function clearAllDrafts(extraKeys = []) {
264
+ export function clearAllDrafts(extraKeys = [], storage = activeAdapter) {
136
265
  try {
137
266
  const keysToRemove = new Set();
138
- for (let i = 0; i < localStorage.length; i++) {
139
- const key = localStorage.key(i);
140
- if (key && key.startsWith(`${STORAGE_KEY_PREFIX}:`)) {
267
+ for (const key of storage.keys()) {
268
+ if (key.startsWith(`${STORAGE_KEY_PREFIX}:`)) {
141
269
  keysToRemove.add(key);
142
270
  }
143
271
  }
144
272
  for (const key of extraKeys) {
145
- if (localStorage.getItem(key) !== null) {
273
+ if (storage.getItem(key) !== null) {
146
274
  keysToRemove.add(key);
147
275
  }
148
276
  }
149
277
  for (const key of keysToRemove) {
150
- localStorage.removeItem(key);
278
+ storage.removeItem(key);
151
279
  }
152
280
  return keysToRemove.size;
153
281
  }
154
282
  catch (error) {
155
- logger.warn('Failed to clear drafts from localStorage:', error);
283
+ logger.warn('Failed to clear drafts from storage:', error);
156
284
  return 0;
157
285
  }
158
286
  }
@@ -175,6 +303,17 @@ export class DraftAutoSaveManager {
175
303
  getWorkflow;
176
304
  /** Function to check if workflow is dirty */
177
305
  isDirty;
306
+ /**
307
+ * Runtime gate for draft persistence (e.g. a user-facing opt-out setting).
308
+ * Checked on every save, so it can change after construction.
309
+ */
310
+ isPersistenceAllowed;
311
+ /**
312
+ * Storage adapter this instance writes to.
313
+ * Captured at construction, so a later `setDraftStorage()` call (e.g. from
314
+ * another FlowDrop mount on the same page) cannot retarget this manager.
315
+ */
316
+ storage;
178
317
  /** Last saved workflow hash (for change detection) */
179
318
  lastSavedHash = null;
180
319
  /**
@@ -188,6 +327,8 @@ export class DraftAutoSaveManager {
188
327
  this.enabled = options.enabled;
189
328
  this.getWorkflow = options.getWorkflow;
190
329
  this.isDirty = options.isDirty;
330
+ this.isPersistenceAllowed = options.isPersistenceAllowed ?? (() => true);
331
+ this.storage = resolveDraftStorage(options.storage);
191
332
  }
192
333
  /**
193
334
  * Start auto-save interval
@@ -217,7 +358,7 @@ export class DraftAutoSaveManager {
217
358
  * @returns true if a draft was saved
218
359
  */
219
360
  saveIfDirty() {
220
- if (!this.enabled) {
361
+ if (!this.enabled || !this.isPersistenceAllowed()) {
221
362
  return false;
222
363
  }
223
364
  const workflow = this.getWorkflow();
@@ -233,7 +374,7 @@ export class DraftAutoSaveManager {
233
374
  if (currentHash === this.lastSavedHash) {
234
375
  return false;
235
376
  }
236
- const saved = saveDraft(workflow, this.storageKey);
377
+ const saved = saveDraft(workflow, this.storageKey, this.storage);
237
378
  if (saved) {
238
379
  this.lastSavedHash = currentHash;
239
380
  }
@@ -247,11 +388,14 @@ export class DraftAutoSaveManager {
247
388
  * @returns true if saved successfully
248
389
  */
249
390
  forceSave() {
391
+ if (!this.isPersistenceAllowed()) {
392
+ return false;
393
+ }
250
394
  const workflow = this.getWorkflow();
251
395
  if (!workflow) {
252
396
  return false;
253
397
  }
254
- const saved = saveDraft(workflow, this.storageKey);
398
+ const saved = saveDraft(workflow, this.storageKey, this.storage);
255
399
  if (saved) {
256
400
  this.lastSavedHash = this.hashWorkflow(workflow);
257
401
  }
@@ -261,7 +405,7 @@ export class DraftAutoSaveManager {
261
405
  * Clear the draft from storage
262
406
  */
263
407
  clearDraft() {
264
- deleteDraft(this.storageKey);
408
+ deleteDraft(this.storageKey, this.storage);
265
409
  this.lastSavedHash = null;
266
410
  }
267
411
  /**
@@ -284,10 +428,14 @@ export class DraftAutoSaveManager {
284
428
  */
285
429
  updateStorageKey(newKey) {
286
430
  // If there's an existing draft with the old key, migrate it
287
- const existingDraft = loadDraft(this.storageKey);
431
+ const existingDraft = loadDraft(this.storageKey, this.storage);
288
432
  if (existingDraft && this.storageKey !== newKey) {
289
- deleteDraft(this.storageKey);
290
- saveDraft(existingDraft.workflow, newKey);
433
+ deleteDraft(this.storageKey, this.storage);
434
+ // Migration is still a write — respect the user's opt-out. The old
435
+ // draft is deleted either way, which is in line with opting out.
436
+ if (this.isPersistenceAllowed()) {
437
+ saveDraft(existingDraft.workflow, newKey, this.storage);
438
+ }
291
439
  }
292
440
  this.storageKey = newKey;
293
441
  }
@@ -6,6 +6,7 @@
6
6
  * @module services/dynamicSchemaService
7
7
  */
8
8
  import type { ConfigSchema, DynamicSchemaEndpoint, ExternalEditLink, ConfigEditOptions, WorkflowNode } from '../types/index.js';
9
+ import type { EndpointConfig } from '../config/endpoints.js';
9
10
  /**
10
11
  * Result of a dynamic schema fetch operation
11
12
  */
@@ -41,7 +42,7 @@ export interface DynamicSchemaResult {
41
42
  * }
42
43
  * ```
43
44
  */
44
- export declare function fetchDynamicSchema(endpoint: DynamicSchemaEndpoint, node: WorkflowNode, workflowId?: string): Promise<DynamicSchemaResult>;
45
+ export declare function fetchDynamicSchema(endpointConfig: EndpointConfig | null, endpoint: DynamicSchemaEndpoint, node: WorkflowNode, workflowId?: string): Promise<DynamicSchemaResult>;
45
46
  /**
46
47
  * Resolves an external edit link URL with template variables.
47
48
  *