@flowdular/sdk 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) 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 +1 -1
  28. package/packages/database-testing/package.json +1 -1
  29. package/packages/dev-console/package.json +1 -1
  30. package/packages/harness/package.json +1 -1
  31. package/packages/kernel/package.json +1 -1
  32. package/packages/server/package.json +1 -1
  33. package/packages/server/src/application-routes.ts +80 -8
  34. package/packages/server/src/index.ts +4 -1
  35. package/packages/server/src/web-html.ts +9 -5
  36. package/packages/server/src/web.ts +59 -7
  37. package/packages/ui/package.json +1 -1
  38. package/packages/coding-agent/package.json +0 -44
  39. package/packages/coding-agent/src/drivers/byok.ts +0 -322
  40. package/packages/coding-agent/src/drivers/claude-code.ts +0 -290
  41. package/packages/coding-agent/src/drivers/codex.ts +0 -281
  42. package/packages/coding-agent/src/index.ts +0 -56
  43. package/packages/coding-agent/src/registry.ts +0 -119
  44. package/packages/coding-agent/src/roles/contract.ts +0 -60
  45. package/packages/coding-agent/src/roles/defaults.ts +0 -134
  46. package/packages/coding-agent/src/roles/handoff.ts +0 -41
  47. package/packages/coding-agent/src/roles/registry.ts +0 -156
  48. package/packages/coding-agent/src/roles/skills.ts +0 -72
  49. package/packages/coding-agent/src/roles/sync.ts +0 -88
  50. package/packages/coding-agent/src/types.ts +0 -102
  51. package/packages/coding-agent/src/workspace.ts +0 -275
  52. package/packages/sandbox/README.md +0 -691
  53. package/packages/sandbox/bin/flowdular-sandbox.mjs +0 -393
  54. package/packages/sandbox/dist/client/assets/App-ClMOrpyg.js +0 -11
  55. package/packages/sandbox/dist/client/assets/PreviewHost-BHiHrfbQ.js +0 -1
  56. package/packages/sandbox/dist/client/assets/auto-buGEq6de.js +0 -3
  57. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
  58. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
  59. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv.woff +0 -0
  60. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag.woff2 +0 -0
  61. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
  62. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
  63. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT.woff +0 -0
  64. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T.woff2 +0 -0
  65. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
  66. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
  67. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
  68. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
  69. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
  70. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
  71. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-ext-500-normal-CAhNIIs5.woff2 +0 -0
  72. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-ext-500-normal-CZ70TYgx.woff +0 -0
  73. package/packages/sandbox/dist/client/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
  74. package/packages/sandbox/dist/client/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
  75. package/packages/sandbox/dist/client/assets/ibm-plex-mono-vietnamese-500-normal-C8zxqsMH.woff +0 -0
  76. package/packages/sandbox/dist/client/assets/ibm-plex-mono-vietnamese-500-normal-DZ4AoWbu.woff2 +0 -0
  77. package/packages/sandbox/dist/client/assets/ibm-plex-sans-cyrillic-ext-wght-normal-d45eAU9y.woff2 +0 -0
  78. package/packages/sandbox/dist/client/assets/ibm-plex-sans-cyrillic-wght-normal-BAAhND-U.woff2 +0 -0
  79. package/packages/sandbox/dist/client/assets/ibm-plex-sans-greek-wght-normal-CmyJS8uq.woff2 +0 -0
  80. package/packages/sandbox/dist/client/assets/ibm-plex-sans-latin-ext-wght-normal-CIII54If.woff2 +0 -0
  81. package/packages/sandbox/dist/client/assets/ibm-plex-sans-latin-wght-normal-IvpUvPa2.woff2 +0 -0
  82. package/packages/sandbox/dist/client/assets/ibm-plex-sans-vietnamese-wght-normal-Dg1JeJN0.woff2 +0 -0
  83. package/packages/sandbox/dist/client/assets/index-CvwhlQ87.js +0 -4
  84. package/packages/sandbox/dist/client/assets/styles-BPWaGf55.js +0 -3
  85. package/packages/sandbox/dist/client/assets/styles-Dx1_KOag.css +0 -1
  86. package/packages/sandbox/dist/client/favicon.svg +0 -1
  87. package/packages/sandbox/dist/server/assets/auto-CVGNYyb3.js +0 -12785
  88. package/packages/sandbox/dist/server/assets/chunk-DDJLRBDX-3OUNo51g.js +0 -4125
  89. package/packages/sandbox/dist/server/assets/nodefs-DVSI7kG0.js +0 -24
  90. package/packages/sandbox/dist/server/assets/opfs-ahp-D56iokt1.js +0 -388
  91. package/packages/sandbox/dist/server/assets/token-Bvaq7Anz.js +0 -55
  92. package/packages/sandbox/dist/server/assets/token-util-BqSvdIPQ.js +0 -3
  93. package/packages/sandbox/dist/server/assets/token-util-CktFFT4z.js +0 -494
  94. package/packages/sandbox/dist/server/entry.js +0 -91671
  95. package/packages/sandbox/dist/server/favicon.svg +0 -1
  96. package/packages/sandbox/dist/server/index.html +0 -161
  97. package/packages/sandbox/index.html +0 -159
  98. package/packages/sandbox/octane.config.ts +0 -36
  99. package/packages/sandbox/package.json +0 -83
  100. package/packages/sandbox/public/favicon.svg +0 -1
  101. package/packages/sandbox/public/og.png +0 -0
  102. package/packages/sandbox/src/App.tsrx +0 -924
  103. package/packages/sandbox/src/client/ApprovalHandoff.tsrx +0 -39
  104. package/packages/sandbox/src/client/ChatPane.tsrx +0 -694
  105. package/packages/sandbox/src/client/ConnectView.tsrx +0 -86
  106. package/packages/sandbox/src/client/DiffPane.tsrx +0 -150
  107. package/packages/sandbox/src/client/EjectModal.tsrx +0 -571
  108. package/packages/sandbox/src/client/GateResults.tsrx +0 -60
  109. package/packages/sandbox/src/client/GitHubSettingsModal.tsrx +0 -219
  110. package/packages/sandbox/src/client/NewSessionForm.tsrx +0 -214
  111. package/packages/sandbox/src/client/SessionBar.tsrx +0 -151
  112. package/packages/sandbox/src/client/SessionPicker.tsrx +0 -472
  113. package/packages/sandbox/src/client/SpecEditor.tsrx +0 -109
  114. package/packages/sandbox/src/client/SpecReviewCard.tsrx +0 -321
  115. package/packages/sandbox/src/client/WorkCard.tsrx +0 -128
  116. package/packages/sandbox/src/client/WorkbenchModal.tsrx +0 -161
  117. package/packages/sandbox/src/client/WorkspaceMenu.tsrx +0 -100
  118. package/packages/sandbox/src/client/api.ts +0 -797
  119. package/packages/sandbox/src/client/attachments.ts +0 -73
  120. package/packages/sandbox/src/client/dashboard.ts +0 -46
  121. package/packages/sandbox/src/client/github-settings.ts +0 -48
  122. package/packages/sandbox/src/client/highlight.ts +0 -105
  123. package/packages/sandbox/src/client/i18n.ts +0 -32
  124. package/packages/sandbox/src/client/locales/en.json +0 -466
  125. package/packages/sandbox/src/client/locales/pl.json +0 -466
  126. package/packages/sandbox/src/client/session-labels.ts +0 -77
  127. package/packages/sandbox/src/client/state.ts +0 -108
  128. package/packages/sandbox/src/index.ts +0 -10
  129. package/packages/sandbox/src/preview/PreviewHost.tsrx +0 -373
  130. package/packages/sandbox/src/preview/load-module.ts +0 -16
  131. package/packages/sandbox/src/server/attachments.ts +0 -308
  132. package/packages/sandbox/src/server/auto-review.ts +0 -181
  133. package/packages/sandbox/src/server/checkpoints.ts +0 -90
  134. package/packages/sandbox/src/server/config.ts +0 -460
  135. package/packages/sandbox/src/server/dashboard.ts +0 -245
  136. package/packages/sandbox/src/server/delivery/configuration.ts +0 -193
  137. package/packages/sandbox/src/server/delivery/git-pr.ts +0 -1473
  138. package/packages/sandbox/src/server/delivery/index.ts +0 -83
  139. package/packages/sandbox/src/server/delivery/local.ts +0 -151
  140. package/packages/sandbox/src/server/delivery/official-modules.ts +0 -516
  141. package/packages/sandbox/src/server/delivery/plan.ts +0 -218
  142. package/packages/sandbox/src/server/delivery/policies.ts +0 -239
  143. package/packages/sandbox/src/server/delivery/record.ts +0 -41
  144. package/packages/sandbox/src/server/delivery/steps.ts +0 -464
  145. package/packages/sandbox/src/server/delivery/types.ts +0 -158
  146. package/packages/sandbox/src/server/dependencies.ts +0 -88
  147. package/packages/sandbox/src/server/diff.ts +0 -230
  148. package/packages/sandbox/src/server/gates.ts +0 -388
  149. package/packages/sandbox/src/server/index.ts +0 -139
  150. package/packages/sandbox/src/server/path-guard.ts +0 -282
  151. package/packages/sandbox/src/server/planning.ts +0 -749
  152. package/packages/sandbox/src/server/platform-client.ts +0 -189
  153. package/packages/sandbox/src/server/preview-database-host.ts +0 -340
  154. package/packages/sandbox/src/server/preview-database-protocol.ts +0 -163
  155. package/packages/sandbox/src/server/preview-database-proxy.ts +0 -356
  156. package/packages/sandbox/src/server/preview-database.ts +0 -109
  157. package/packages/sandbox/src/server/preview-ipc.ts +0 -15
  158. package/packages/sandbox/src/server/preview-modules.ts +0 -83
  159. package/packages/sandbox/src/server/preview-revision.ts +0 -44
  160. package/packages/sandbox/src/server/preview-runtime.ts +0 -467
  161. package/packages/sandbox/src/server/preview-worker-manager.ts +0 -436
  162. package/packages/sandbox/src/server/preview-worker.ts +0 -168
  163. package/packages/sandbox/src/server/reference.ts +0 -251
  164. package/packages/sandbox/src/server/routes.ts +0 -2258
  165. package/packages/sandbox/src/server/runtime.ts +0 -188
  166. package/packages/sandbox/src/server/session-owner.ts +0 -23
  167. package/packages/sandbox/src/server/sessions.ts +0 -849
  168. package/packages/sandbox/src/server/spec.ts +0 -471
  169. package/packages/sandbox/src/server/turn-lifecycle.ts +0 -56
  170. package/packages/sandbox/src/server/turns.ts +0 -993
  171. package/packages/sandbox/src/server/workspace-install.ts +0 -266
  172. package/packages/sandbox/src/server/workspace-root.ts +0 -73
  173. package/packages/sandbox/src/styles.css +0 -1573
  174. 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);