@flowdular/sdk 0.1.0 → 0.2.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 (175) hide show
  1. package/.ai/skills/test-hardening/SKILL.md +1 -1
  2. package/README.md +69 -12
  3. package/assets/flowdular-banner.png +0 -0
  4. package/modules/agents/package.json +1 -1
  5. package/modules/auth/package.json +1 -1
  6. package/modules/auth/src/client/AuthenticationCore.tsrx +3 -1
  7. package/modules/auth/src/server/endpoints.ts +3 -1
  8. package/modules/auth/src/server/runtime.ts +4 -0
  9. package/modules/automations/package.json +1 -1
  10. package/modules/automations-workflows-integration/package.json +1 -1
  11. package/modules/profile/package.json +1 -1
  12. package/modules/sandbox/package.json +1 -1
  13. package/modules/system/package.json +1 -1
  14. package/modules/users/package.json +1 -1
  15. package/modules/workflows/package.json +1 -1
  16. package/package.json +5 -13
  17. package/packages/ai-provider/package.json +1 -1
  18. package/packages/cli-protocol/package.json +1 -1
  19. package/packages/client/package.json +1 -1
  20. package/packages/client/src/routing.ts +39 -2
  21. package/packages/client/src/shell/BrandMark.tsrx +5 -1
  22. package/packages/client/src/shell/navigation.ts +3 -1
  23. package/packages/client/src/state.ts +6 -2
  24. package/packages/contracts/package.json +1 -1
  25. package/packages/contracts/schemas/project.schema.json +16 -42
  26. package/packages/database/package.json +1 -1
  27. package/packages/database-pglite/package.json +2 -2
  28. package/packages/database-pglite/src/driver.ts +21 -3
  29. package/packages/database-testing/package.json +1 -1
  30. package/packages/dev-console/package.json +1 -1
  31. package/packages/harness/package.json +1 -1
  32. package/packages/kernel/package.json +1 -1
  33. package/packages/server/package.json +1 -1
  34. package/packages/server/src/application-routes.ts +80 -8
  35. package/packages/server/src/index.ts +4 -1
  36. package/packages/server/src/web-html.ts +9 -5
  37. package/packages/server/src/web.ts +59 -7
  38. package/packages/ui/package.json +1 -1
  39. package/packages/coding-agent/package.json +0 -44
  40. package/packages/coding-agent/src/drivers/byok.ts +0 -322
  41. package/packages/coding-agent/src/drivers/claude-code.ts +0 -290
  42. package/packages/coding-agent/src/drivers/codex.ts +0 -281
  43. package/packages/coding-agent/src/index.ts +0 -56
  44. package/packages/coding-agent/src/registry.ts +0 -119
  45. package/packages/coding-agent/src/roles/contract.ts +0 -60
  46. package/packages/coding-agent/src/roles/defaults.ts +0 -134
  47. package/packages/coding-agent/src/roles/handoff.ts +0 -41
  48. package/packages/coding-agent/src/roles/registry.ts +0 -156
  49. package/packages/coding-agent/src/roles/skills.ts +0 -72
  50. package/packages/coding-agent/src/roles/sync.ts +0 -88
  51. package/packages/coding-agent/src/types.ts +0 -102
  52. package/packages/coding-agent/src/workspace.ts +0 -275
  53. package/packages/sandbox/README.md +0 -691
  54. package/packages/sandbox/bin/flowdular-sandbox.mjs +0 -393
  55. package/packages/sandbox/dist/client/assets/App-ClMOrpyg.js +0 -11
  56. package/packages/sandbox/dist/client/assets/PreviewHost-BHiHrfbQ.js +0 -1
  57. package/packages/sandbox/dist/client/assets/auto-buGEq6de.js +0 -3
  58. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
  59. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
  60. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv.woff +0 -0
  61. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag.woff2 +0 -0
  62. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
  63. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
  64. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT.woff +0 -0
  65. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T.woff2 +0 -0
  66. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
  67. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
  68. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
  69. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
  70. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
  71. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
  72. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-ext-500-normal-CAhNIIs5.woff2 +0 -0
  73. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-ext-500-normal-CZ70TYgx.woff +0 -0
  74. package/packages/sandbox/dist/client/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
  75. package/packages/sandbox/dist/client/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
  76. package/packages/sandbox/dist/client/assets/ibm-plex-mono-vietnamese-500-normal-C8zxqsMH.woff +0 -0
  77. package/packages/sandbox/dist/client/assets/ibm-plex-mono-vietnamese-500-normal-DZ4AoWbu.woff2 +0 -0
  78. package/packages/sandbox/dist/client/assets/ibm-plex-sans-cyrillic-ext-wght-normal-d45eAU9y.woff2 +0 -0
  79. package/packages/sandbox/dist/client/assets/ibm-plex-sans-cyrillic-wght-normal-BAAhND-U.woff2 +0 -0
  80. package/packages/sandbox/dist/client/assets/ibm-plex-sans-greek-wght-normal-CmyJS8uq.woff2 +0 -0
  81. package/packages/sandbox/dist/client/assets/ibm-plex-sans-latin-ext-wght-normal-CIII54If.woff2 +0 -0
  82. package/packages/sandbox/dist/client/assets/ibm-plex-sans-latin-wght-normal-IvpUvPa2.woff2 +0 -0
  83. package/packages/sandbox/dist/client/assets/ibm-plex-sans-vietnamese-wght-normal-Dg1JeJN0.woff2 +0 -0
  84. package/packages/sandbox/dist/client/assets/index-CvwhlQ87.js +0 -4
  85. package/packages/sandbox/dist/client/assets/styles-BPWaGf55.js +0 -3
  86. package/packages/sandbox/dist/client/assets/styles-Dx1_KOag.css +0 -1
  87. package/packages/sandbox/dist/client/favicon.svg +0 -1
  88. package/packages/sandbox/dist/server/assets/auto-CVGNYyb3.js +0 -12785
  89. package/packages/sandbox/dist/server/assets/chunk-DDJLRBDX-3OUNo51g.js +0 -4125
  90. package/packages/sandbox/dist/server/assets/nodefs-DVSI7kG0.js +0 -24
  91. package/packages/sandbox/dist/server/assets/opfs-ahp-D56iokt1.js +0 -388
  92. package/packages/sandbox/dist/server/assets/token-Bvaq7Anz.js +0 -55
  93. package/packages/sandbox/dist/server/assets/token-util-BqSvdIPQ.js +0 -3
  94. package/packages/sandbox/dist/server/assets/token-util-CktFFT4z.js +0 -494
  95. package/packages/sandbox/dist/server/entry.js +0 -91671
  96. package/packages/sandbox/dist/server/favicon.svg +0 -1
  97. package/packages/sandbox/dist/server/index.html +0 -161
  98. package/packages/sandbox/index.html +0 -159
  99. package/packages/sandbox/octane.config.ts +0 -36
  100. package/packages/sandbox/package.json +0 -83
  101. package/packages/sandbox/public/favicon.svg +0 -1
  102. package/packages/sandbox/public/og.png +0 -0
  103. package/packages/sandbox/src/App.tsrx +0 -924
  104. package/packages/sandbox/src/client/ApprovalHandoff.tsrx +0 -39
  105. package/packages/sandbox/src/client/ChatPane.tsrx +0 -694
  106. package/packages/sandbox/src/client/ConnectView.tsrx +0 -86
  107. package/packages/sandbox/src/client/DiffPane.tsrx +0 -150
  108. package/packages/sandbox/src/client/EjectModal.tsrx +0 -571
  109. package/packages/sandbox/src/client/GateResults.tsrx +0 -60
  110. package/packages/sandbox/src/client/GitHubSettingsModal.tsrx +0 -219
  111. package/packages/sandbox/src/client/NewSessionForm.tsrx +0 -214
  112. package/packages/sandbox/src/client/SessionBar.tsrx +0 -151
  113. package/packages/sandbox/src/client/SessionPicker.tsrx +0 -472
  114. package/packages/sandbox/src/client/SpecEditor.tsrx +0 -109
  115. package/packages/sandbox/src/client/SpecReviewCard.tsrx +0 -321
  116. package/packages/sandbox/src/client/WorkCard.tsrx +0 -128
  117. package/packages/sandbox/src/client/WorkbenchModal.tsrx +0 -161
  118. package/packages/sandbox/src/client/WorkspaceMenu.tsrx +0 -100
  119. package/packages/sandbox/src/client/api.ts +0 -797
  120. package/packages/sandbox/src/client/attachments.ts +0 -73
  121. package/packages/sandbox/src/client/dashboard.ts +0 -46
  122. package/packages/sandbox/src/client/github-settings.ts +0 -48
  123. package/packages/sandbox/src/client/highlight.ts +0 -105
  124. package/packages/sandbox/src/client/i18n.ts +0 -32
  125. package/packages/sandbox/src/client/locales/en.json +0 -466
  126. package/packages/sandbox/src/client/locales/pl.json +0 -466
  127. package/packages/sandbox/src/client/session-labels.ts +0 -77
  128. package/packages/sandbox/src/client/state.ts +0 -108
  129. package/packages/sandbox/src/index.ts +0 -10
  130. package/packages/sandbox/src/preview/PreviewHost.tsrx +0 -373
  131. package/packages/sandbox/src/preview/load-module.ts +0 -16
  132. package/packages/sandbox/src/server/attachments.ts +0 -308
  133. package/packages/sandbox/src/server/auto-review.ts +0 -181
  134. package/packages/sandbox/src/server/checkpoints.ts +0 -90
  135. package/packages/sandbox/src/server/config.ts +0 -460
  136. package/packages/sandbox/src/server/dashboard.ts +0 -245
  137. package/packages/sandbox/src/server/delivery/configuration.ts +0 -193
  138. package/packages/sandbox/src/server/delivery/git-pr.ts +0 -1473
  139. package/packages/sandbox/src/server/delivery/index.ts +0 -83
  140. package/packages/sandbox/src/server/delivery/local.ts +0 -151
  141. package/packages/sandbox/src/server/delivery/official-modules.ts +0 -516
  142. package/packages/sandbox/src/server/delivery/plan.ts +0 -218
  143. package/packages/sandbox/src/server/delivery/policies.ts +0 -239
  144. package/packages/sandbox/src/server/delivery/record.ts +0 -41
  145. package/packages/sandbox/src/server/delivery/steps.ts +0 -464
  146. package/packages/sandbox/src/server/delivery/types.ts +0 -158
  147. package/packages/sandbox/src/server/dependencies.ts +0 -88
  148. package/packages/sandbox/src/server/diff.ts +0 -230
  149. package/packages/sandbox/src/server/gates.ts +0 -388
  150. package/packages/sandbox/src/server/index.ts +0 -139
  151. package/packages/sandbox/src/server/path-guard.ts +0 -282
  152. package/packages/sandbox/src/server/planning.ts +0 -749
  153. package/packages/sandbox/src/server/platform-client.ts +0 -189
  154. package/packages/sandbox/src/server/preview-database-host.ts +0 -340
  155. package/packages/sandbox/src/server/preview-database-protocol.ts +0 -163
  156. package/packages/sandbox/src/server/preview-database-proxy.ts +0 -356
  157. package/packages/sandbox/src/server/preview-database.ts +0 -109
  158. package/packages/sandbox/src/server/preview-ipc.ts +0 -15
  159. package/packages/sandbox/src/server/preview-modules.ts +0 -83
  160. package/packages/sandbox/src/server/preview-revision.ts +0 -44
  161. package/packages/sandbox/src/server/preview-runtime.ts +0 -467
  162. package/packages/sandbox/src/server/preview-worker-manager.ts +0 -436
  163. package/packages/sandbox/src/server/preview-worker.ts +0 -168
  164. package/packages/sandbox/src/server/reference.ts +0 -251
  165. package/packages/sandbox/src/server/routes.ts +0 -2258
  166. package/packages/sandbox/src/server/runtime.ts +0 -188
  167. package/packages/sandbox/src/server/session-owner.ts +0 -23
  168. package/packages/sandbox/src/server/sessions.ts +0 -849
  169. package/packages/sandbox/src/server/spec.ts +0 -471
  170. package/packages/sandbox/src/server/turn-lifecycle.ts +0 -56
  171. package/packages/sandbox/src/server/turns.ts +0 -993
  172. package/packages/sandbox/src/server/workspace-install.ts +0 -266
  173. package/packages/sandbox/src/server/workspace-root.ts +0 -73
  174. package/packages/sandbox/src/styles.css +0 -1573
  175. package/packages/sandbox/vite.config.ts +0 -133
@@ -1,436 +0,0 @@
1
- import { sandboxDirectory } from './config.ts';
2
- import { spawn, type ChildProcess } from 'node:child_process';
3
- import { randomBytes } from 'node:crypto';
4
- import { mkdirSync, realpathSync } from 'node:fs';
5
- import { dirname, join, resolve } from 'node:path';
6
- import { fileURLToPath } from 'node:url';
7
- import { createPreviewDatabaseProvider } from './preview-database.ts';
8
- import {
9
- createPreviewDatabaseHost,
10
- type PreviewDatabaseHost,
11
- } from './preview-database-host.ts';
12
- import type { PreviewComposition, PreviewRuntime } from './preview-runtime.ts';
13
- import type { SandboxSession } from './sessions.ts';
14
- import { resolvePreviewModules } from './preview-modules.ts';
15
- import { previewRevision } from './preview-revision.ts';
16
- import { sendPreviewDatabaseReply } from './preview-ipc.ts';
17
-
18
- const START_TIMEOUT_MS = 10_000;
19
- const REQUEST_TIMEOUT_MS = 15_000;
20
-
21
- export interface IsolatedPreviewOptions {
22
- readonly startTimeoutMs?: number;
23
- readonly requestTimeoutMs?: number;
24
- /* Diagnostics must not retain the child. Tests use this to prove that
25
- concurrent first requests share one worker. */
26
- readonly onWorkerLifecycle?: (
27
- event: 'started' | 'released',
28
- sessionId: string,
29
- ) => void;
30
- }
31
-
32
- interface WorkerReady {
33
- readonly type: 'ready';
34
- readonly port: number;
35
- }
36
-
37
- interface WorkerFailure {
38
- readonly type: 'error';
39
- readonly message: string;
40
- }
41
-
42
- interface PreviewMeta {
43
- readonly sessionId: string;
44
- readonly revision: string;
45
- readonly moduleId: string;
46
- readonly modules: PreviewComposition['modules'];
47
- readonly credentials: PreviewComposition['credentials'];
48
- readonly moduleScopes: PreviewComposition['moduleScopes'];
49
- readonly error: string | null;
50
- readonly routes: number;
51
- }
52
-
53
- interface IsolatedWorker {
54
- readonly child: ChildProcess;
55
- readonly origin: string;
56
- }
57
-
58
- interface WorkerSlot {
59
- readonly revision: string;
60
- readonly keys: PreviewKeys;
61
- readonly child: ChildProcess;
62
- readonly host: PreviewDatabaseHost;
63
- readonly ready: Promise<IsolatedWorker>;
64
- }
65
-
66
- interface PreviewKeys {
67
- readonly FD_AGENT_CREDENTIAL_KEY: string;
68
- readonly FD_AGENT_RUN_GRANT_KEY: string;
69
- }
70
-
71
- function timeoutSignal(timeoutMs: number): {
72
- readonly signal: AbortSignal;
73
- readonly clear: () => void;
74
- } {
75
- const controller = new AbortController();
76
- const timer = setTimeout(() => controller.abort(), timeoutMs);
77
- timer.unref();
78
- return { signal: controller.signal, clear: () => clearTimeout(timer) };
79
- }
80
-
81
- /* The worker answers on its own loopback port, so a browser request crossing
82
- into it carries an origin that names the sandbox server, not the worker. A
83
- draft module's authentication routes compare the Origin header with the URL
84
- they were reached on and would reject every call. The sandbox checked the
85
- browser origin before this hop, so the hop presents its own. Referer names
86
- the sandbox page for the same reason and is dropped; the session travels in
87
- x-flowdular-preview-session. */
88
- export function previewHopHeaders(
89
- source: Headers,
90
- workerOrigin: string,
91
- ): Headers {
92
- const headers = new Headers(source);
93
- headers.delete('host');
94
- headers.delete('connection');
95
- headers.delete('content-length');
96
- headers.delete('referer');
97
- if (headers.has('origin')) headers.set('origin', workerOrigin);
98
- return headers;
99
- }
100
-
101
- async function requestWorker(
102
- worker: IsolatedWorker,
103
- path: string,
104
- request: Request,
105
- timeoutMs: number,
106
- ): Promise<Response> {
107
- const timeout = timeoutSignal(timeoutMs);
108
- try {
109
- const headers = previewHopHeaders(request.headers, worker.origin);
110
- const method = request.method.toUpperCase();
111
- const body =
112
- method === 'GET' || method === 'HEAD'
113
- ? undefined
114
- : await request.arrayBuffer();
115
- return await fetch(new URL(path, worker.origin), {
116
- method,
117
- headers,
118
- ...(body ? { body } : {}),
119
- signal: timeout.signal,
120
- });
121
- } finally {
122
- timeout.clear();
123
- }
124
- }
125
-
126
- function startWorker(
127
- workspaceRoot: string,
128
- session: SandboxSession,
129
- revision: string,
130
- startTimeoutMs: number,
131
- /* The previous worker's engine holds the same data directory until it has
132
- closed, and two embedded PostgreSQL instances must never share one. */
133
- drained: Promise<void> | undefined,
134
- /* Source reloads keep the session's vault and retained grants readable. */
135
- keys: PreviewKeys = {
136
- FD_AGENT_CREDENTIAL_KEY: randomBytes(32).toString('base64'),
137
- FD_AGENT_RUN_GRANT_KEY: randomBytes(32).toString('base64'),
138
- },
139
- ): WorkerSlot {
140
- /* Permission roots must be physical paths. On macOS /var is a symlink to
141
- /private/var; granting the lexical path still makes Node deny the realpath
142
- lookup before the worker reaches the session. */
143
- const canonicalWorkspaceRoot = realpathSync(workspaceRoot);
144
- const entry = fileURLToPath(new URL('./preview-worker.ts', import.meta.url));
145
- const repositoryRoot = resolve(dirname(entry), '../../../..');
146
- const sessionRoot = join(
147
- sandboxDirectory(canonicalWorkspaceRoot),
148
- 'sessions',
149
- session.id,
150
- );
151
- const readable = [
152
- sessionRoot,
153
- join(repositoryRoot, 'packages'),
154
- join(repositoryRoot, 'modules'),
155
- join(repositoryRoot, 'node_modules'),
156
- ];
157
- const writable = [
158
- /* Authentication and module databases are the only persistent state a
159
- preview composition owns. Session metadata, chat, source, checkpoints
160
- and attachments stay read-only even to draft top-level code. */
161
- join(sessionRoot, 'data'),
162
- join(sessionRoot, '.flowdular'),
163
- ];
164
- for (const path of writable)
165
- mkdirSync(path, { recursive: true, mode: 0o700 });
166
- /* The preview gets only a tiny, explicit environment. In particular it
167
- cannot inherit platform, BYOK or GitHub credentials from the sandbox. */
168
- const child = spawn(
169
- process.execPath,
170
- [
171
- /* Native transformation avoids tsx, whose loader starts an esbuild
172
- child process and would force us to grant draft code that capability. */
173
- '--experimental-transform-types',
174
- '--permission',
175
- ...readable.map((path) => `--allow-fs-read=${path}`),
176
- ...writable.map((path) => `--allow-fs-write=${path}`),
177
- entry,
178
- canonicalWorkspaceRoot,
179
- ],
180
- {
181
- cwd: canonicalWorkspaceRoot,
182
- env: {
183
- NODE_ENV: process.env.NODE_ENV === 'test' ? 'test' : 'production',
184
- // Ephemeral keys belong only to this isolated preview runtime.
185
- ...keys,
186
- FD_INTERNAL_SANDBOX_PREVIEW_WORKER: '1',
187
- FD_INTERNAL_SANDBOX_STATE_ROOT:
188
- sandboxDirectory(canonicalWorkspaceRoot) ===
189
- join(canonicalWorkspaceRoot, '.coreloom', 'sandbox')
190
- ? '.coreloom'
191
- : '.flowdular',
192
- },
193
- stdio: ['ignore', 'ignore', 'ignore', 'ipc'],
194
- /* Database rows carry bigints, byte arrays and dates, none of which
195
- survive the default JSON channel. */
196
- serialization: 'advanced',
197
- },
198
- );
199
- /* The worker cannot host the engine: Node's permission model denies
200
- process.binding, which the embedded PostgreSQL build needs during static
201
- init. The engine stays here and the worker drives it over this channel. */
202
- const host = createPreviewDatabaseHost({
203
- open: async () => {
204
- await drained;
205
- return createPreviewDatabaseProvider(join(sessionRoot, 'data'));
206
- },
207
- send: (reply) => sendPreviewDatabaseReply(child, reply),
208
- });
209
- child.on('message', (message) => {
210
- host.accept(message);
211
- });
212
- const closeHost = () => void host.close().catch(() => undefined);
213
- child.once('exit', closeHost);
214
- child.once('disconnect', closeHost);
215
- const ready = new Promise<IsolatedWorker>((resolvePromise, reject) => {
216
- let settled = false;
217
- const finish = (callback: () => void) => {
218
- if (settled) return;
219
- settled = true;
220
- clearTimeout(timer);
221
- callback();
222
- };
223
- const timer = setTimeout(
224
- () =>
225
- finish(() => {
226
- child.kill('SIGKILL');
227
- reject(
228
- new Error('The isolated preview worker did not start in time.'),
229
- );
230
- }),
231
- startTimeoutMs,
232
- );
233
- timer.unref();
234
- child.on('message', (message: WorkerReady | WorkerFailure) => {
235
- if (message?.type === 'ready' && Number.isInteger(message.port)) {
236
- finish(() =>
237
- resolvePromise({ child, origin: `http://127.0.0.1:${message.port}` }),
238
- );
239
- }
240
- if (message?.type === 'error') {
241
- finish(() => reject(new Error(message.message)));
242
- }
243
- });
244
- child.once('error', (error) => finish(() => reject(error)));
245
- child.once('exit', (code, signal) =>
246
- finish(() =>
247
- reject(
248
- new Error(
249
- `The isolated preview worker stopped before it was ready (${signal ?? code ?? 'unknown'}).`,
250
- ),
251
- ),
252
- ),
253
- );
254
- });
255
- return { child, host, ready, revision, keys };
256
- }
257
-
258
- /* The manager owns process lifetime. A worker crash or timeout destroys only
259
- that preview. The sandbox keeps serving sessions, bridge requests and turns. */
260
- export function createIsolatedPreviewRuntime(
261
- workspaceRoot: string,
262
- options: IsolatedPreviewOptions = {},
263
- ): PreviewRuntime {
264
- const startTimeoutMs = options.startTimeoutMs ?? START_TIMEOUT_MS;
265
- const requestTimeoutMs = options.requestTimeoutMs ?? REQUEST_TIMEOUT_MS;
266
- const workers = new Map<string, WorkerSlot>();
267
- const cached = new Map<string, PreviewComposition>();
268
- const composing = new Map<
269
- string,
270
- { pending: Promise<PreviewComposition>; controller: AbortController }
271
- >();
272
- /* Self-clearing: an entry lives only until the engine it names has closed. */
273
- const draining = new Map<string, Promise<void>>();
274
- let disposed = false;
275
- const observe = (event: 'started' | 'released', sessionId: string) => {
276
- try {
277
- options.onWorkerLifecycle?.(event, sessionId);
278
- } catch {
279
- /* Diagnostics never own or interrupt the worker lifecycle. */
280
- }
281
- };
282
- const release = (sessionId: string, expected?: WorkerSlot) => {
283
- const slot = workers.get(sessionId);
284
- if (!slot || (expected && slot !== expected))
285
- return draining.get(sessionId) ?? Promise.resolve();
286
- workers.delete(sessionId);
287
- cached.delete(sessionId);
288
- const drained = slot.host.close().catch(() => undefined);
289
- draining.set(sessionId, drained);
290
- void drained.then(() => {
291
- if (draining.get(sessionId) === drained) draining.delete(sessionId);
292
- });
293
- slot.child.kill('SIGKILL');
294
- observe('released', sessionId);
295
- return drained;
296
- };
297
- const compose = async (session: SandboxSession, signal: AbortSignal) => {
298
- const revision = await previewRevision(
299
- await resolvePreviewModules(workspaceRoot, session),
300
- );
301
- signal.throwIfAborted();
302
- let slot = workers.get(session.id);
303
- const keys = slot?.keys;
304
- if (
305
- slot &&
306
- (slot.revision !== revision ||
307
- slot.child.exitCode !== null ||
308
- slot.child.killed)
309
- ) {
310
- release(session.id, slot);
311
- slot = undefined;
312
- }
313
- if (!slot) {
314
- slot = startWorker(
315
- workspaceRoot,
316
- session,
317
- revision,
318
- startTimeoutMs,
319
- draining.get(session.id),
320
- keys,
321
- );
322
- workers.set(session.id, slot);
323
- observe('started', session.id);
324
- const owned = slot;
325
- owned.child.once('exit', () => {
326
- if (workers.get(session.id) === owned) release(session.id, owned);
327
- });
328
- }
329
- let worker: IsolatedWorker;
330
- try {
331
- worker = await slot.ready;
332
- signal.throwIfAborted();
333
- } catch (error) {
334
- release(session.id, slot);
335
- throw error;
336
- }
337
- let response: Response;
338
- try {
339
- response = await requestWorker(
340
- worker,
341
- '/compose',
342
- new Request('http://preview/compose', {
343
- method: 'POST',
344
- headers: { 'content-type': 'application/json' },
345
- body: JSON.stringify(session),
346
- }),
347
- requestTimeoutMs,
348
- );
349
- } catch (error) {
350
- release(session.id, slot);
351
- throw error;
352
- }
353
- if (!response.ok) {
354
- release(session.id, slot);
355
- const detail = await response.text().catch(() => '');
356
- throw new Error(
357
- `The isolated preview worker could not compose the draft.${
358
- detail ? ` ${detail.slice(0, 500)}` : ''
359
- }`,
360
- );
361
- }
362
- const meta = (await response.json()) as PreviewMeta;
363
- signal.throwIfAborted();
364
- const current = cached.get(session.id);
365
- if (current && current.revision === meta.revision) return current;
366
- const owned = slot;
367
- const composition: PreviewComposition = {
368
- ...meta,
369
- auth: null as never,
370
- router: null as never,
371
- routes: Array.from({ length: meta.routes }, () => null as never),
372
- request: async (request) => {
373
- try {
374
- const forwarded = new Request(request, {
375
- headers: new Headers(request.headers),
376
- });
377
- forwarded.headers.set('x-flowdular-preview-session', session.id);
378
- return await requestWorker(
379
- worker,
380
- `/request${new URL(request.url).pathname}${new URL(request.url).search}`,
381
- forwarded,
382
- requestTimeoutMs,
383
- );
384
- } catch {
385
- release(session.id, owned);
386
- return Response.json(
387
- {
388
- error: {
389
- code: 'PREVIEW_WORKER_UNAVAILABLE',
390
- message:
391
- 'The isolated preview stopped. Reload the preview to start a new one.',
392
- },
393
- },
394
- { status: 503 },
395
- );
396
- }
397
- },
398
- dispose: () => release(session.id, owned),
399
- };
400
- cached.set(session.id, composition);
401
- return composition;
402
- };
403
- return {
404
- cached: (sessionId) => cached.get(sessionId) ?? null,
405
- forget: (sessionId) => {
406
- composing.get(sessionId)?.controller.abort();
407
- composing.delete(sessionId);
408
- return release(sessionId);
409
- },
410
- dispose: () => {
411
- disposed = true;
412
- for (const entry of composing.values()) entry.controller.abort();
413
- for (const sessionId of [...workers.keys()]) release(sessionId);
414
- cached.clear();
415
- composing.clear();
416
- },
417
- compose: (session: SandboxSession) => {
418
- if (disposed) {
419
- return Promise.reject(
420
- new Error('The isolated preview runtime was already disposed.'),
421
- );
422
- }
423
- const active = composing.get(session.id);
424
- if (active) return active.pending;
425
- const controller = new AbortController();
426
- const pending = compose(session, controller.signal);
427
- const entry = { pending, controller };
428
- composing.set(session.id, entry);
429
- const clear = () => {
430
- if (composing.get(session.id) === entry) composing.delete(session.id);
431
- };
432
- void pending.then(clear, clear);
433
- return pending;
434
- },
435
- };
436
- }
@@ -1,168 +0,0 @@
1
- import { createServer } from 'node:http';
2
- import { createRouter, type ServerRoute } from '@octanejs/app-core';
3
- import { createRemoteDatabaseProvider } from './preview-database-proxy.ts';
4
- import { createInProcessPreviewRuntime } from './preview-runtime.ts';
5
- import type { SandboxSession } from './sessions.ts';
6
-
7
- const workspaceRoot = process.argv[2];
8
- if (!workspaceRoot)
9
- throw new Error('The preview worker requires a workspace path.');
10
-
11
- const channel = process.send?.bind(process);
12
- if (!channel)
13
- throw new Error('The preview worker requires an IPC channel to the sandbox.');
14
-
15
- const runtime = createInProcessPreviewRuntime(
16
- workspaceRoot,
17
- createRemoteDatabaseProvider({
18
- send: (request) => {
19
- channel(request);
20
- },
21
- subscribe: (listener) => {
22
- process.on('message', listener);
23
- },
24
- }),
25
- );
26
-
27
- async function readBody(
28
- request: import('node:http').IncomingMessage,
29
- ): Promise<Buffer> {
30
- const chunks: Buffer[] = [];
31
- for await (const chunk of request) chunks.push(Buffer.from(chunk));
32
- return Buffer.concat(chunks);
33
- }
34
-
35
- function send(
36
- response: import('node:http').ServerResponse,
37
- value: Response,
38
- ): void {
39
- response.statusCode = value.status;
40
- for (const [name, content] of value.headers)
41
- response.setHeader(name, content);
42
- void value.arrayBuffer().then((body) => response.end(Buffer.from(body)));
43
- }
44
-
45
- async function compose(request: Request): Promise<Response> {
46
- const session = (await request.json()) as SandboxSession;
47
- const composition = await runtime.compose(session);
48
- return Response.json({
49
- sessionId: composition.sessionId,
50
- revision: composition.revision,
51
- moduleId: composition.moduleId,
52
- modules: composition.modules,
53
- credentials: composition.credentials,
54
- moduleScopes: composition.moduleScopes,
55
- error: composition.error,
56
- routes: composition.routes.length,
57
- });
58
- }
59
-
60
- async function previewRequest(request: Request): Promise<Response> {
61
- const sessionId = request.headers.get('x-flowdular-preview-session');
62
- if (!sessionId)
63
- return Response.json(
64
- {
65
- error: {
66
- code: 'PREVIEW_SESSION_REQUIRED',
67
- message: 'A preview session is required.',
68
- },
69
- },
70
- { status: 400 },
71
- );
72
- const composition = runtime.cached(sessionId);
73
- if (!composition)
74
- return new Response(null, {
75
- status: 404,
76
- headers: { 'x-flowdular-preview-unmatched': '1' },
77
- });
78
- const url = new URL(request.url);
79
- const match = composition.router.match(request.method, url.pathname);
80
- if (!match || match.route.type !== 'server') {
81
- return new Response(null, {
82
- status: 404,
83
- headers: { 'x-flowdular-preview-unmatched': '1' },
84
- });
85
- }
86
- const context = {
87
- request,
88
- params: match.params,
89
- url,
90
- state: new Map(),
91
- };
92
- return composition.auth.middleware(context, async () =>
93
- (match.route as ServerRoute).handler(context),
94
- );
95
- }
96
-
97
- const server = createServer(async (incoming, outgoing) => {
98
- try {
99
- const origin = `http://127.0.0.1:${(incoming.socket.address() as { port: number }).port}`;
100
- const body = await readBody(incoming);
101
- const request = new Request(new URL(incoming.url ?? '/', origin), {
102
- method: incoming.method ?? 'GET',
103
- headers: incoming.headers as HeadersInit,
104
- ...(body.length > 0 ? { body: new Uint8Array(body) } : {}),
105
- });
106
- if (
107
- new URL(request.url).pathname === '/compose' &&
108
- request.method === 'POST'
109
- ) {
110
- send(outgoing, await compose(request));
111
- return;
112
- }
113
- if (new URL(request.url).pathname.startsWith('/request/')) {
114
- const path =
115
- new URL(request.url).pathname.slice('/request'.length) +
116
- new URL(request.url).search;
117
- const forwarded = new Request(new URL(path, origin), {
118
- method: request.method,
119
- headers: request.headers,
120
- ...(body.length > 0 ? { body: new Uint8Array(body) } : {}),
121
- });
122
- send(outgoing, await previewRequest(forwarded));
123
- return;
124
- }
125
- send(
126
- outgoing,
127
- Response.json(
128
- {
129
- error: {
130
- code: 'PREVIEW_WORKER_ROUTE_NOT_FOUND',
131
- message: 'Preview worker route not found.',
132
- },
133
- },
134
- { status: 404 },
135
- ),
136
- );
137
- } catch (error) {
138
- send(
139
- outgoing,
140
- Response.json(
141
- {
142
- error: {
143
- code: 'PREVIEW_WORKER_FAILED',
144
- message:
145
- error instanceof Error
146
- ? error.message.slice(0, 300)
147
- : 'Preview worker failed.',
148
- },
149
- },
150
- { status: 500 },
151
- ),
152
- );
153
- }
154
- });
155
-
156
- server.listen(0, '127.0.0.1', () => {
157
- const address = server.address();
158
- if (!address || typeof address === 'string')
159
- throw new Error('Preview worker did not bind a loopback port.');
160
- process.send?.({ type: 'ready', port: address.port });
161
- });
162
-
163
- const shutdown = () => server.close(() => process.exit(0));
164
- process.once('SIGTERM', shutdown);
165
- process.once('SIGINT', shutdown);
166
- /* An IPC disconnect means the sandbox generation that owned this worker is
167
- gone. Do not leave a preview server orphaned across shutdown or HMR. */
168
- process.once('disconnect', shutdown);