@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.
- package/.ai/skills/test-hardening/SKILL.md +1 -1
- package/README.md +69 -12
- package/assets/flowdular-banner.png +0 -0
- package/modules/agents/package.json +1 -1
- package/modules/auth/package.json +1 -1
- package/modules/auth/src/client/AuthenticationCore.tsrx +3 -1
- package/modules/auth/src/server/endpoints.ts +3 -1
- package/modules/auth/src/server/runtime.ts +4 -0
- package/modules/automations/package.json +1 -1
- package/modules/automations-workflows-integration/package.json +1 -1
- package/modules/profile/package.json +1 -1
- package/modules/sandbox/package.json +1 -1
- package/modules/system/package.json +1 -1
- package/modules/users/package.json +1 -1
- package/modules/workflows/package.json +1 -1
- package/package.json +5 -13
- package/packages/ai-provider/package.json +1 -1
- package/packages/cli-protocol/package.json +1 -1
- package/packages/client/package.json +1 -1
- package/packages/client/src/routing.ts +39 -2
- package/packages/client/src/shell/BrandMark.tsrx +5 -1
- package/packages/client/src/shell/navigation.ts +3 -1
- package/packages/client/src/state.ts +6 -2
- package/packages/contracts/package.json +1 -1
- package/packages/contracts/schemas/project.schema.json +16 -42
- package/packages/database/package.json +1 -1
- package/packages/database-pglite/package.json +1 -1
- package/packages/database-testing/package.json +1 -1
- package/packages/dev-console/package.json +1 -1
- package/packages/harness/package.json +1 -1
- package/packages/kernel/package.json +1 -1
- package/packages/server/package.json +1 -1
- package/packages/server/src/application-routes.ts +80 -8
- package/packages/server/src/index.ts +4 -1
- package/packages/server/src/web-html.ts +9 -5
- package/packages/server/src/web.ts +59 -7
- package/packages/ui/package.json +1 -1
- package/packages/coding-agent/package.json +0 -44
- package/packages/coding-agent/src/drivers/byok.ts +0 -322
- package/packages/coding-agent/src/drivers/claude-code.ts +0 -290
- package/packages/coding-agent/src/drivers/codex.ts +0 -281
- package/packages/coding-agent/src/index.ts +0 -56
- package/packages/coding-agent/src/registry.ts +0 -119
- package/packages/coding-agent/src/roles/contract.ts +0 -60
- package/packages/coding-agent/src/roles/defaults.ts +0 -134
- package/packages/coding-agent/src/roles/handoff.ts +0 -41
- package/packages/coding-agent/src/roles/registry.ts +0 -156
- package/packages/coding-agent/src/roles/skills.ts +0 -72
- package/packages/coding-agent/src/roles/sync.ts +0 -88
- package/packages/coding-agent/src/types.ts +0 -102
- package/packages/coding-agent/src/workspace.ts +0 -275
- package/packages/sandbox/README.md +0 -691
- package/packages/sandbox/bin/flowdular-sandbox.mjs +0 -393
- package/packages/sandbox/dist/client/assets/App-ClMOrpyg.js +0 -11
- package/packages/sandbox/dist/client/assets/PreviewHost-BHiHrfbQ.js +0 -1
- package/packages/sandbox/dist/client/assets/auto-buGEq6de.js +0 -3
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv.woff +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT.woff +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-ext-500-normal-CAhNIIs5.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-ext-500-normal-CZ70TYgx.woff +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-vietnamese-500-normal-C8zxqsMH.woff +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-vietnamese-500-normal-DZ4AoWbu.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-sans-cyrillic-ext-wght-normal-d45eAU9y.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-sans-cyrillic-wght-normal-BAAhND-U.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-sans-greek-wght-normal-CmyJS8uq.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-sans-latin-ext-wght-normal-CIII54If.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-sans-latin-wght-normal-IvpUvPa2.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-sans-vietnamese-wght-normal-Dg1JeJN0.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/index-CvwhlQ87.js +0 -4
- package/packages/sandbox/dist/client/assets/styles-BPWaGf55.js +0 -3
- package/packages/sandbox/dist/client/assets/styles-Dx1_KOag.css +0 -1
- package/packages/sandbox/dist/client/favicon.svg +0 -1
- package/packages/sandbox/dist/server/assets/auto-CVGNYyb3.js +0 -12785
- package/packages/sandbox/dist/server/assets/chunk-DDJLRBDX-3OUNo51g.js +0 -4125
- package/packages/sandbox/dist/server/assets/nodefs-DVSI7kG0.js +0 -24
- package/packages/sandbox/dist/server/assets/opfs-ahp-D56iokt1.js +0 -388
- package/packages/sandbox/dist/server/assets/token-Bvaq7Anz.js +0 -55
- package/packages/sandbox/dist/server/assets/token-util-BqSvdIPQ.js +0 -3
- package/packages/sandbox/dist/server/assets/token-util-CktFFT4z.js +0 -494
- package/packages/sandbox/dist/server/entry.js +0 -91671
- package/packages/sandbox/dist/server/favicon.svg +0 -1
- package/packages/sandbox/dist/server/index.html +0 -161
- package/packages/sandbox/index.html +0 -159
- package/packages/sandbox/octane.config.ts +0 -36
- package/packages/sandbox/package.json +0 -83
- package/packages/sandbox/public/favicon.svg +0 -1
- package/packages/sandbox/public/og.png +0 -0
- package/packages/sandbox/src/App.tsrx +0 -924
- package/packages/sandbox/src/client/ApprovalHandoff.tsrx +0 -39
- package/packages/sandbox/src/client/ChatPane.tsrx +0 -694
- package/packages/sandbox/src/client/ConnectView.tsrx +0 -86
- package/packages/sandbox/src/client/DiffPane.tsrx +0 -150
- package/packages/sandbox/src/client/EjectModal.tsrx +0 -571
- package/packages/sandbox/src/client/GateResults.tsrx +0 -60
- package/packages/sandbox/src/client/GitHubSettingsModal.tsrx +0 -219
- package/packages/sandbox/src/client/NewSessionForm.tsrx +0 -214
- package/packages/sandbox/src/client/SessionBar.tsrx +0 -151
- package/packages/sandbox/src/client/SessionPicker.tsrx +0 -472
- package/packages/sandbox/src/client/SpecEditor.tsrx +0 -109
- package/packages/sandbox/src/client/SpecReviewCard.tsrx +0 -321
- package/packages/sandbox/src/client/WorkCard.tsrx +0 -128
- package/packages/sandbox/src/client/WorkbenchModal.tsrx +0 -161
- package/packages/sandbox/src/client/WorkspaceMenu.tsrx +0 -100
- package/packages/sandbox/src/client/api.ts +0 -797
- package/packages/sandbox/src/client/attachments.ts +0 -73
- package/packages/sandbox/src/client/dashboard.ts +0 -46
- package/packages/sandbox/src/client/github-settings.ts +0 -48
- package/packages/sandbox/src/client/highlight.ts +0 -105
- package/packages/sandbox/src/client/i18n.ts +0 -32
- package/packages/sandbox/src/client/locales/en.json +0 -466
- package/packages/sandbox/src/client/locales/pl.json +0 -466
- package/packages/sandbox/src/client/session-labels.ts +0 -77
- package/packages/sandbox/src/client/state.ts +0 -108
- package/packages/sandbox/src/index.ts +0 -10
- package/packages/sandbox/src/preview/PreviewHost.tsrx +0 -373
- package/packages/sandbox/src/preview/load-module.ts +0 -16
- package/packages/sandbox/src/server/attachments.ts +0 -308
- package/packages/sandbox/src/server/auto-review.ts +0 -181
- package/packages/sandbox/src/server/checkpoints.ts +0 -90
- package/packages/sandbox/src/server/config.ts +0 -460
- package/packages/sandbox/src/server/dashboard.ts +0 -245
- package/packages/sandbox/src/server/delivery/configuration.ts +0 -193
- package/packages/sandbox/src/server/delivery/git-pr.ts +0 -1473
- package/packages/sandbox/src/server/delivery/index.ts +0 -83
- package/packages/sandbox/src/server/delivery/local.ts +0 -151
- package/packages/sandbox/src/server/delivery/official-modules.ts +0 -516
- package/packages/sandbox/src/server/delivery/plan.ts +0 -218
- package/packages/sandbox/src/server/delivery/policies.ts +0 -239
- package/packages/sandbox/src/server/delivery/record.ts +0 -41
- package/packages/sandbox/src/server/delivery/steps.ts +0 -464
- package/packages/sandbox/src/server/delivery/types.ts +0 -158
- package/packages/sandbox/src/server/dependencies.ts +0 -88
- package/packages/sandbox/src/server/diff.ts +0 -230
- package/packages/sandbox/src/server/gates.ts +0 -388
- package/packages/sandbox/src/server/index.ts +0 -139
- package/packages/sandbox/src/server/path-guard.ts +0 -282
- package/packages/sandbox/src/server/planning.ts +0 -749
- package/packages/sandbox/src/server/platform-client.ts +0 -189
- package/packages/sandbox/src/server/preview-database-host.ts +0 -340
- package/packages/sandbox/src/server/preview-database-protocol.ts +0 -163
- package/packages/sandbox/src/server/preview-database-proxy.ts +0 -356
- package/packages/sandbox/src/server/preview-database.ts +0 -109
- package/packages/sandbox/src/server/preview-ipc.ts +0 -15
- package/packages/sandbox/src/server/preview-modules.ts +0 -83
- package/packages/sandbox/src/server/preview-revision.ts +0 -44
- package/packages/sandbox/src/server/preview-runtime.ts +0 -467
- package/packages/sandbox/src/server/preview-worker-manager.ts +0 -436
- package/packages/sandbox/src/server/preview-worker.ts +0 -168
- package/packages/sandbox/src/server/reference.ts +0 -251
- package/packages/sandbox/src/server/routes.ts +0 -2258
- package/packages/sandbox/src/server/runtime.ts +0 -188
- package/packages/sandbox/src/server/session-owner.ts +0 -23
- package/packages/sandbox/src/server/sessions.ts +0 -849
- package/packages/sandbox/src/server/spec.ts +0 -471
- package/packages/sandbox/src/server/turn-lifecycle.ts +0 -56
- package/packages/sandbox/src/server/turns.ts +0 -993
- package/packages/sandbox/src/server/workspace-install.ts +0 -266
- package/packages/sandbox/src/server/workspace-root.ts +0 -73
- package/packages/sandbox/src/styles.css +0 -1573
- 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);
|