create-flowdular 0.5.1 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -5
- package/agent-template/.agents/skills/agent-tool-design/SKILL.md +1 -1
- package/agent-template/.agents/skills/auth-security-review/SKILL.md +1 -1
- package/agent-template/.agents/skills/database-adapter/SKILL.md +5 -5
- package/agent-template/.agents/skills/database-adapter/references/first-run-and-matrix.md +2 -2
- package/agent-template/.agents/skills/deploy-operate/SKILL.md +1 -1
- package/agent-template/.agents/skills/migration-authoring/SKILL.md +4 -4
- package/agent-template/.agents/skills/module-new/SKILL.md +1 -1
- package/agent-template/.agents/skills/spec-approval/SKILL.md +6 -2
- package/agent-template/.agents/skills/spec-interview/SKILL.md +2 -2
- package/agent-template/.agents/skills/test-hardening/SKILL.md +2 -2
- package/agent-template/.agents/skills/workflow-development/SKILL.md +95 -9
- package/agent-template/.ai/agents/sandbox/business-manager.md +2 -2
- package/agent-template/.ai/blueprints/add-migration/required-files.yaml +1 -1
- package/agent-template/.ai/blueprints/new-module/required-files.yaml +1 -1
- package/agent-template/.ai/platform-capabilities.md +10 -8
- package/agent-template/.ai/policies/capabilities.yaml +28 -12
- package/agent-template/.ai/skills/README.md +1 -1
- package/agent-template/.ai/skills/agent-tool-design/SKILL.md +1 -1
- package/agent-template/.ai/skills/auth-security-review/SKILL.md +1 -1
- package/agent-template/.ai/skills/database-adapter/SKILL.md +5 -5
- package/agent-template/.ai/skills/database-adapter/references/first-run-and-matrix.md +2 -2
- package/agent-template/.ai/skills/deploy-operate/SKILL.md +1 -1
- package/agent-template/.ai/skills/migration-authoring/SKILL.md +4 -4
- package/agent-template/.ai/skills/module-new/SKILL.md +1 -1
- package/agent-template/.ai/skills/spec-approval/SKILL.md +6 -2
- package/agent-template/.ai/skills/spec-interview/SKILL.md +2 -2
- package/agent-template/.ai/skills/test-hardening/SKILL.md +2 -2
- package/agent-template/.ai/skills/workflow-development/SKILL.md +96 -10
- package/agent-template/.claude/skills/agent-tool-design/SKILL.md +1 -1
- package/agent-template/.claude/skills/auth-security-review/SKILL.md +1 -1
- package/agent-template/.claude/skills/database-adapter/SKILL.md +5 -5
- package/agent-template/.claude/skills/database-adapter/references/first-run-and-matrix.md +2 -2
- package/agent-template/.claude/skills/deploy-operate/SKILL.md +1 -1
- package/agent-template/.claude/skills/migration-authoring/SKILL.md +4 -4
- package/agent-template/.claude/skills/module-new/SKILL.md +1 -1
- package/agent-template/.claude/skills/spec-approval/SKILL.md +6 -2
- package/agent-template/.claude/skills/spec-interview/SKILL.md +2 -2
- package/agent-template/.claude/skills/test-hardening/SKILL.md +2 -2
- package/agent-template/.claude/skills/workflow-development/SKILL.md +95 -9
- package/agent-template/docs/adr/0003-module-settings.md +2 -0
- package/agent-template/docs/adr/0007-module-owned-agents.md +35 -1
- package/agent-template/docs/agent-contract.md +3 -3
- package/agent-template/docs/cli-extensions.md +1 -0
- package/agent-template/docs/cli.md +40 -3
- package/agent-template/docs/configuration.md +64 -9
- package/agent-template/docs/database-adapters.md +30 -22
- package/agent-template/docs/design-system.md +7 -3
- package/agent-template/docs/getting-started.md +29 -32
- package/agent-template/docs/module-distribution.md +79 -86
- package/agent-template/docs/module-web-surfaces.md +9 -7
- package/agent-template/docs/modules.md +6 -2
- package/agent-template/docs/sandbox.md +23 -4
- package/agent-template/platform/scripts/build.mjs +7 -0
- package/dist/bin.js +3 -6
- package/package.json +1 -1
- package/template/default/.env.example +8 -3
- package/template/default/.vercelignore +8 -0
- package/template/default/README.md +20 -11
- package/template/default/_gitignore +3 -2
- package/template/default/infra/README.md +86 -65
- package/template/default/infra/docker/.env.example +71 -0
- package/template/default/infra/docker/Dockerfile +29 -11
- package/template/default/infra/docker/app-entrypoint.mjs +5 -0
- package/template/default/infra/docker/compose.yaml +109 -58
- package/template/default/infra/docker/database-urls.mjs +28 -0
- package/template/default/infra/docker/pitr.sh +177 -0
- package/template/default/infra/docker/postgres/10-roles.sh +16 -12
- package/template/default/infra/docker/start.mjs +402 -0
- package/template/default/infra/kubernetes/database-secret.example.yaml +3 -3
- package/template/default/infra/kubernetes/deployment.yaml +5 -0
- package/template/default/infra/sdk-module-manifests.mjs +118 -0
- package/template/default/infra/vercel/README.md +262 -0
- package/template/default/infra/vercel/build.mjs +223 -0
- package/template/default/infra/vercel/handler.mjs +100 -0
- package/template/default/modules/example/migrations/0001_example_core.up.sql +2 -2
- package/template/default/modules/example/module.json +1 -1
- package/template/default/modules/example/package.json +3 -3
- package/template/default/modules/example/spec/module.yaml +1 -1
- package/template/default/modules/example/src/services/migration.ts +2 -2
- package/template/default/modules/example/tests/module.test.ts +1 -1
- package/template/default/package.json +2 -3
- package/template/default/platform/octane.config.ts +290 -156
- package/template/default/platform/package.json +5 -5
- package/template/default/platform/scripts/build.mjs +56 -0
- package/template/default/platform/scripts/dev.mjs +101 -18
- package/template/default/platform/src/generated/modules.server.ts +1 -0
- package/template/default/platform/src/server/database.ts +24 -0
- package/template/default/platform/src/server/lifecycle.ts +325 -0
- package/template/default/platform/src/server/runtime-role.ts +33 -0
- package/template/default/platform/src/server/setup/access.ts +160 -0
- package/template/default/platform/src/server/setup/adapters.ts +554 -0
- package/template/default/platform/src/server/setup/environment.ts +154 -0
- package/template/default/platform/src/server/setup/gate.ts +84 -0
- package/template/default/platform/src/server/setup/index.ts +181 -0
- package/template/default/platform/src/server/setup/modules.ts +119 -0
- package/template/default/platform/src/server/setup/page.ts +548 -0
- package/template/default/platform/src/server/setup/routes.ts +788 -0
- package/template/default/platform/src/server/setup/sanitize.ts +111 -0
- package/template/default/platform/src/server/setup/seed.ts +192 -0
- package/template/default/platform/src/server/setup/token.ts +79 -0
- package/template/default/platform/src/server/worker-tick.ts +193 -0
- package/template/default/platform/src/server/workspace-root.ts +16 -0
- package/template/default/render.yaml +70 -0
- package/template/default/vercel.json +5 -0
|
@@ -9,12 +9,12 @@ import {
|
|
|
9
9
|
validateApplicationPath,
|
|
10
10
|
createMailPort,
|
|
11
11
|
mailConfigFromEnvironment,
|
|
12
|
+
serverLogger,
|
|
12
13
|
} from '@flowdular/sdk/server';
|
|
13
14
|
import {
|
|
14
15
|
createDataClassRegistry,
|
|
15
16
|
PLATFORM_SETTINGS_TENANT,
|
|
16
17
|
} from '@flowdular/sdk/kernel';
|
|
17
|
-
import { resolve } from 'node:path';
|
|
18
18
|
import { defineConfig, RenderRoute } from '@octanejs/vite-plugin';
|
|
19
19
|
import {
|
|
20
20
|
authRuntimeOptionsFromEnvironment,
|
|
@@ -37,12 +37,28 @@ import {
|
|
|
37
37
|
import {
|
|
38
38
|
createPlatformDatabaseProvider,
|
|
39
39
|
databaseProviderConfigFromEnvironment,
|
|
40
|
+
loadPlatformEnvironmentFile,
|
|
41
|
+
platformDatabaseConfigured,
|
|
40
42
|
} from './src/server/database.ts';
|
|
41
43
|
import {
|
|
42
44
|
createReadinessEndpoint,
|
|
43
45
|
healthEndpoint,
|
|
44
46
|
} from './src/server/health.ts';
|
|
47
|
+
import {
|
|
48
|
+
createPlatformRuntimeLifecycle,
|
|
49
|
+
prepareAndActivatePlatformRuntimeLifecycle,
|
|
50
|
+
} from './src/server/lifecycle.ts';
|
|
45
51
|
import { createMetricsRoutes } from './src/server/metrics.ts';
|
|
52
|
+
import {
|
|
53
|
+
platformRuntimeRole,
|
|
54
|
+
startModuleWorkers,
|
|
55
|
+
} from './src/server/runtime-role.ts';
|
|
56
|
+
import {
|
|
57
|
+
createWorkerTickEndpoint,
|
|
58
|
+
createWorkerTicker,
|
|
59
|
+
WORKER_TICK_PATH,
|
|
60
|
+
workerTickConfigFromEnvironment,
|
|
61
|
+
} from './src/server/worker-tick.ts';
|
|
46
62
|
import { createPlatformObservability } from './src/server/tracing.ts';
|
|
47
63
|
import {
|
|
48
64
|
createStorageKeyring,
|
|
@@ -50,6 +66,13 @@ import {
|
|
|
50
66
|
createStorageRoutes,
|
|
51
67
|
storageConfigFromEnvironment,
|
|
52
68
|
} from './src/server/storage.ts';
|
|
69
|
+
import {
|
|
70
|
+
clearSetupToken,
|
|
71
|
+
configuredDatabaseNeedsFirstRun,
|
|
72
|
+
createFirstRunSetup,
|
|
73
|
+
createInPlaceFirstRun,
|
|
74
|
+
} from './src/server/setup/index.ts';
|
|
75
|
+
import { findWorkspaceRoot } from './src/server/workspace-root.ts';
|
|
53
76
|
|
|
54
77
|
function checkedRoutes<T extends Parameters<typeof assertRouteConflicts>[0]>(
|
|
55
78
|
routes: T,
|
|
@@ -59,184 +82,295 @@ function checkedRoutes<T extends Parameters<typeof assertRouteConflicts>[0]>(
|
|
|
59
82
|
}
|
|
60
83
|
|
|
61
84
|
const SHELL = ['App', '/src/App.tsrx'] as const;
|
|
62
|
-
const workspaceRoot =
|
|
85
|
+
const workspaceRoot = findWorkspaceRoot(import.meta.dirname);
|
|
63
86
|
const building = process.env.FD_INTERNAL_BUILD === 'true';
|
|
64
87
|
|
|
65
|
-
|
|
88
|
+
function firstRunConfig(databasePreconfigured = false) {
|
|
89
|
+
const setup = createFirstRunSetup({
|
|
90
|
+
applicationPath: applicationBasePath,
|
|
91
|
+
webMountPaths: moduleWebMounts.map((site) => site.path),
|
|
92
|
+
environment: process.env,
|
|
93
|
+
databasePreconfigured,
|
|
94
|
+
workspaceRoot,
|
|
95
|
+
});
|
|
96
|
+
return defineConfig({
|
|
97
|
+
router: {
|
|
98
|
+
routes: checkedRoutes([healthEndpoint.serverRoute, ...setup.routes]),
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function createPlatformConfig() {
|
|
104
|
+
if (process.env.FD_INTERNAL_PLATFORM_TERMINATING === 'true') {
|
|
105
|
+
throw new Error(
|
|
106
|
+
'Platform startup was requested while the process is stopping.',
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
loadPlatformEnvironmentFile(workspaceRoot);
|
|
110
|
+
const serverless = process.env.FD_DEPLOYMENT_TARGET === 'vercel';
|
|
111
|
+
if (!building && !platformDatabaseConfigured(process.env)) {
|
|
112
|
+
if (serverless)
|
|
113
|
+
throw new Error(
|
|
114
|
+
'Vercel requires a configured external PostgreSQL database before deployment.',
|
|
115
|
+
);
|
|
116
|
+
return firstRunConfig();
|
|
117
|
+
}
|
|
118
|
+
/* A Vercel Function can neither restart into the application after setup
|
|
119
|
+
nor keep a token file, so it composes the application and serves setup
|
|
120
|
+
inside it until the first workspace exists. */
|
|
121
|
+
const needsFirstRun =
|
|
122
|
+
!building &&
|
|
123
|
+
(await configuredDatabaseNeedsFirstRun(process.env, workspaceRoot));
|
|
124
|
+
if (needsFirstRun && !serverless) return firstRunConfig(true);
|
|
125
|
+
clearSetupToken(workspaceRoot);
|
|
126
|
+
const runtimeRole = platformRuntimeRole(process.env);
|
|
127
|
+
const workerTick =
|
|
128
|
+
runtimeRole === 'tick'
|
|
129
|
+
? workerTickConfigFromEnvironment(process.env)
|
|
130
|
+
: null;
|
|
131
|
+
|
|
132
|
+
/* Composed first and drained last: a trace or an error report is evidence about
|
|
66
133
|
the boot that follows it, and both egresses refuse a misconfigured endpoint
|
|
67
134
|
here rather than at the first request that needed them. */
|
|
68
|
-
const observability = createPlatformObservability({
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
});
|
|
73
|
-
const databases = createPlatformDatabaseProvider(
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
);
|
|
79
|
-
const storageKeyring = createStorageKeyring(process.env, workspaceRoot);
|
|
80
|
-
const storage = createStoragePort(
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
);
|
|
87
|
-
const configuredApplicationPath = validateApplicationPath(
|
|
88
|
-
|
|
89
|
-
);
|
|
90
|
-
/* One outbound transport for the whole deployment. auth.core is only its
|
|
135
|
+
const observability = createPlatformObservability({
|
|
136
|
+
environment: building
|
|
137
|
+
? { ...process.env, NODE_ENV: 'development' }
|
|
138
|
+
: process.env,
|
|
139
|
+
});
|
|
140
|
+
const databases = createPlatformDatabaseProvider(
|
|
141
|
+
databaseProviderConfigFromEnvironment(
|
|
142
|
+
building ? { ...process.env, NODE_ENV: 'development' } : process.env,
|
|
143
|
+
workspaceRoot,
|
|
144
|
+
),
|
|
145
|
+
);
|
|
146
|
+
const storageKeyring = createStorageKeyring(process.env, workspaceRoot);
|
|
147
|
+
const storage = createStoragePort(
|
|
148
|
+
storageConfigFromEnvironment(
|
|
149
|
+
building ? { ...process.env, NODE_ENV: 'development' } : process.env,
|
|
150
|
+
workspaceRoot,
|
|
151
|
+
),
|
|
152
|
+
{ keyring: storageKeyring },
|
|
153
|
+
);
|
|
154
|
+
const configuredApplicationPath = validateApplicationPath(
|
|
155
|
+
process.env.FD_APPLICATION_PATH ?? applicationBasePath,
|
|
156
|
+
);
|
|
157
|
+
/* One outbound transport for the whole deployment. auth.core is only its
|
|
91
158
|
first sender; the SMTP client comes from that module because it is the one
|
|
92
159
|
that declares the dependency. */
|
|
93
|
-
const mail = createMailPort(
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
);
|
|
99
|
-
/* Created before the auth runtime so auth.core, which composes outside the
|
|
160
|
+
const mail = createMailPort(
|
|
161
|
+
mailConfigFromEnvironment(
|
|
162
|
+
building ? { ...process.env, NODE_ENV: 'development' } : process.env,
|
|
163
|
+
),
|
|
164
|
+
{ createSmtpTransport: nodemailerSmtpTransport },
|
|
165
|
+
);
|
|
166
|
+
/* Created before the auth runtime so auth.core, which composes outside the
|
|
100
167
|
generated module list, declares its data classes into the same registry. */
|
|
101
|
-
const dataClasses = createDataClassRegistry();
|
|
102
|
-
const authRuntime = createAuthRuntime({
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
});
|
|
168
|
+
const dataClasses = createDataClassRegistry();
|
|
169
|
+
const authRuntime = createAuthRuntime({
|
|
170
|
+
...authRuntimeOptionsFromEnvironment(
|
|
171
|
+
building ? { ...process.env, NODE_ENV: 'development' } : process.env,
|
|
172
|
+
workspaceRoot,
|
|
173
|
+
),
|
|
174
|
+
applicationPath: configuredApplicationPath,
|
|
175
|
+
databases,
|
|
176
|
+
dataClasses,
|
|
177
|
+
mail,
|
|
178
|
+
metrics: createModuleMetrics('auth.core'),
|
|
179
|
+
});
|
|
113
180
|
|
|
114
|
-
/* Module APIs come from the generated composition. Enable or disable modules
|
|
181
|
+
/* Module APIs come from the generated composition. Enable or disable modules
|
|
115
182
|
with "pnpm flowdular module enable <id> --apply"; never wire them here by hand. */
|
|
116
|
-
const settings = authRuntime.moduleSettings;
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
183
|
+
const settings = authRuntime.moduleSettings;
|
|
184
|
+
const firstRun = needsFirstRun
|
|
185
|
+
? createInPlaceFirstRun({
|
|
186
|
+
environment: process.env,
|
|
187
|
+
workspaceRoot,
|
|
188
|
+
applicationPath: configuredApplicationPath,
|
|
189
|
+
webMountPaths: moduleWebMounts.map((site) => site.path),
|
|
190
|
+
passThrough: ['/api/health', '/api/ready', WORKER_TICK_PATH],
|
|
191
|
+
workspaceExists: async () =>
|
|
192
|
+
(await authRuntime.service()).hasAnyTenant(),
|
|
193
|
+
})
|
|
194
|
+
: null;
|
|
195
|
+
const agentDefinitions = createPlatformAgentRegistry();
|
|
196
|
+
const moduleCompositions = composeModuleServer({
|
|
197
|
+
environment: process.env,
|
|
198
|
+
workspaceRoot,
|
|
199
|
+
auth: authRuntime,
|
|
200
|
+
settings,
|
|
201
|
+
agentTools: createPlatformToolRegistry(),
|
|
202
|
+
agentDefinitions,
|
|
203
|
+
capabilities: createPlatformCapabilityRegistry(),
|
|
204
|
+
dataClasses,
|
|
205
|
+
databases,
|
|
206
|
+
storage,
|
|
207
|
+
/* The relay auth.core resolves per message from its stored settings, falling
|
|
130
208
|
back to the environment port above, so every module of the installation
|
|
131
209
|
sends through the same one. */
|
|
132
|
-
|
|
133
|
-
|
|
210
|
+
mail: authRuntime.mail,
|
|
211
|
+
/* Rebound to the composing module by the generated composition; this binding
|
|
134
212
|
is what a series the platform itself records would carry. */
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
});
|
|
138
|
-
for (const composition of moduleCompositions) {
|
|
139
|
-
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
|
|
213
|
+
metrics: createModuleMetrics('platform'),
|
|
214
|
+
tracer: serverTracer(),
|
|
215
|
+
});
|
|
216
|
+
for (const composition of moduleCompositions) {
|
|
217
|
+
if (composition.settings) settings.declare(composition.settings);
|
|
218
|
+
}
|
|
219
|
+
const ticker = workerTick
|
|
220
|
+
? createWorkerTicker(moduleCompositions, { windowMs: workerTick.windowMs })
|
|
221
|
+
: null;
|
|
222
|
+
agentDefinitions.seal();
|
|
223
|
+
/* Sealed once every composition has run and before any start hook reads the
|
|
143
224
|
catalogue, so every reader sees the declarations the modules agreed on. */
|
|
144
|
-
dataClasses.seal();
|
|
225
|
+
dataClasses.seal();
|
|
226
|
+
|
|
227
|
+
/* Each evaluation of this file is one generation, and Vite evaluates it more
|
|
228
|
+
than once per process. The generation retires when the next one is
|
|
229
|
+
prepared, when the development server stops, or on a stop signal. */
|
|
230
|
+
const lifecycle = createPlatformRuntimeLifecycle();
|
|
231
|
+
lifecycle.add(async () => {
|
|
232
|
+
await ticker?.close();
|
|
233
|
+
for (const composition of moduleCompositions) {
|
|
234
|
+
await composition.stop?.();
|
|
235
|
+
await composition.dispose?.();
|
|
236
|
+
}
|
|
237
|
+
await authRuntime.dispose();
|
|
238
|
+
await storage.dispose();
|
|
239
|
+
await databases.dispose();
|
|
240
|
+
/* Last, so the spans and error reports this process queued while it stopped
|
|
241
|
+
still leave with it. */
|
|
242
|
+
await observability.dispose();
|
|
243
|
+
});
|
|
145
244
|
|
|
146
|
-
/* check() proves the runtime role holds neither SUPERUSER nor BYPASSRLS before
|
|
245
|
+
/* check() proves the runtime role holds neither SUPERUSER nor BYPASSRLS before
|
|
147
246
|
any module reads a row. */
|
|
148
|
-
if (!building) {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
247
|
+
if (!building) {
|
|
248
|
+
try {
|
|
249
|
+
/* The previous generation retires once this one is prepared, and this
|
|
250
|
+
one starts its workers only after the previous has drained. */
|
|
251
|
+
await prepareAndActivatePlatformRuntimeLifecycle(lifecycle, [
|
|
252
|
+
async () => {
|
|
253
|
+
await databases.check();
|
|
254
|
+
},
|
|
255
|
+
/* Platform-scoped settings are read by background work before any request
|
|
256
|
+
could prime them; a workspace is primed by the authentication middleware. */
|
|
257
|
+
() => settings.prime(PLATFORM_SETTINGS_TENANT),
|
|
258
|
+
...moduleCompositions.map(
|
|
259
|
+
(composition) => () => composition.prepare?.(),
|
|
260
|
+
),
|
|
261
|
+
]);
|
|
262
|
+
/* A stop that arrived while this generation was preparing retired only
|
|
263
|
+
the generations active then, so this one retires itself. */
|
|
264
|
+
if (process.env.FD_INTERNAL_PLATFORM_TERMINATING === 'true') {
|
|
265
|
+
throw new Error(
|
|
266
|
+
'Platform startup was requested while the process is stopping.',
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
for (const composition of moduleCompositions) composition.start?.();
|
|
270
|
+
await startModuleWorkers(moduleCompositions, runtimeRole);
|
|
271
|
+
} catch (error) {
|
|
272
|
+
/* A worker that started before the failure would keep running in a
|
|
273
|
+
process that never serves. The boot failure is the one rethrown. */
|
|
274
|
+
await lifecycle.retire().catch((cleanupError: unknown) => {
|
|
275
|
+
serverLogger().error('platform boot cleanup failed', {
|
|
276
|
+
module: 'platform',
|
|
277
|
+
err: cleanupError,
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
throw error;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
156
283
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
284
|
+
// Bundling needs route declarations without background work or retained leases.
|
|
285
|
+
if (building) {
|
|
286
|
+
await lifecycle.retire();
|
|
287
|
+
} else {
|
|
288
|
+
/* scripts/dev.mjs retires every generation itself; a production server
|
|
289
|
+
has only these. */
|
|
290
|
+
const retire = () =>
|
|
291
|
+
void lifecycle.retire().catch((error: unknown) => {
|
|
292
|
+
serverLogger().error('platform shutdown failed', {
|
|
293
|
+
module: 'platform',
|
|
294
|
+
err: error,
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
process.once('SIGINT', retire);
|
|
298
|
+
process.once('SIGTERM', retire);
|
|
299
|
+
lifecycle.add(() => {
|
|
300
|
+
process.off('SIGINT', retire);
|
|
301
|
+
process.off('SIGTERM', retire);
|
|
302
|
+
});
|
|
164
303
|
}
|
|
165
|
-
await authRuntime.dispose();
|
|
166
|
-
await storage.dispose();
|
|
167
|
-
await databases.dispose();
|
|
168
|
-
/* Last, so the spans and error reports this process queued while it stopped
|
|
169
|
-
still leave with it. */
|
|
170
|
-
await observability.dispose();
|
|
171
|
-
};
|
|
172
|
-
// Bundling needs route declarations without background work or retained leases.
|
|
173
|
-
if (building) {
|
|
174
|
-
await shutdown();
|
|
175
|
-
} else {
|
|
176
|
-
process.once('SIGINT', () => void shutdown());
|
|
177
|
-
process.once('SIGTERM', () => void shutdown());
|
|
178
|
-
}
|
|
179
304
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
305
|
+
return defineConfig({
|
|
306
|
+
middlewares: [
|
|
307
|
+
/* Ahead of the authentication: a cross-origin preflight carries no
|
|
183
308
|
credential and has to be answered before anything asks for one. The
|
|
184
309
|
origins come from the API tokens this workspace issued. */
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
}),
|
|
188
|
-
authRuntime.middleware,
|
|
189
|
-
],
|
|
190
|
-
router: {
|
|
191
|
-
routes: checkedRoutes([
|
|
192
|
-
...createApplicationRoutes({
|
|
193
|
-
path: configuredApplicationPath,
|
|
194
|
-
entry: SHELL,
|
|
195
|
-
publicRoot: moduleWebMounts.some((site) => site.path === '/'),
|
|
310
|
+
createCorsMiddleware({
|
|
311
|
+
allowOrigin: (origin) => authRuntime.apiOriginAllowed(origin),
|
|
196
312
|
}),
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
313
|
+
lifecycle.middleware,
|
|
314
|
+
...(firstRun ? [firstRun.middleware] : []),
|
|
315
|
+
authRuntime.middleware,
|
|
316
|
+
],
|
|
317
|
+
router: {
|
|
318
|
+
routes: checkedRoutes([
|
|
319
|
+
...createApplicationRoutes({
|
|
320
|
+
path: configuredApplicationPath,
|
|
321
|
+
entry: SHELL,
|
|
322
|
+
publicRoot: moduleWebMounts.some((site) => site.path === '/'),
|
|
323
|
+
}),
|
|
324
|
+
healthEndpoint.serverRoute,
|
|
325
|
+
createReadinessEndpoint(databases).serverRoute,
|
|
326
|
+
...(ticker && workerTick
|
|
327
|
+
? [createWorkerTickEndpoint(ticker, workerTick).serverRoute]
|
|
328
|
+
: []),
|
|
329
|
+
...createMetricsRoutes({ environment: process.env }),
|
|
330
|
+
/* Describes every operation the presented credential may call, built
|
|
201
331
|
from the endpoints this application composed. */
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
332
|
+
...createOpenApiRoutes({
|
|
333
|
+
resolveIdentity: endpointIdentityFromContext,
|
|
334
|
+
publicBaseUrl: authRuntime.publicBaseUrl,
|
|
335
|
+
}),
|
|
336
|
+
...createStorageRoutes({
|
|
337
|
+
storage,
|
|
338
|
+
keyring: storageKeyring,
|
|
339
|
+
environment: process.env,
|
|
340
|
+
}),
|
|
341
|
+
...createAuthRoutes(authRuntime),
|
|
342
|
+
...(firstRun?.routes ?? []),
|
|
343
|
+
...moduleCompositions.flatMap((composition) => composition.routes),
|
|
344
|
+
...createModuleWebRoutes({
|
|
345
|
+
modules: moduleCompositions,
|
|
346
|
+
mounts: moduleWebMounts,
|
|
347
|
+
applicationPath: configuredApplicationPath,
|
|
348
|
+
/* A build composes with NODE_ENV forced to development, so the flag
|
|
218
349
|
asks for both: only a development server serves a page whose
|
|
219
350
|
stylesheets arrive after its markup. */
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
351
|
+
development: !building && process.env.NODE_ENV !== 'production',
|
|
352
|
+
resolveIdentity: async (context) => {
|
|
353
|
+
const principal = principalFromContext(context);
|
|
354
|
+
if (!principal) return null;
|
|
355
|
+
/* The /api gate does not reach these pages, and a member who
|
|
225
356
|
still owes enrolment must not read workspace data from one.
|
|
226
357
|
Machine credentials cannot enrol and stay exempt there too. */
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
});
|
|
358
|
+
if (
|
|
359
|
+
!isTokenPrincipal(context) &&
|
|
360
|
+
!(await mfaEnrolmentSatisfied(authRuntime, principal))
|
|
361
|
+
) {
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
return {
|
|
365
|
+
subjectId: principal.accountId,
|
|
366
|
+
tenantId: principal.tenantId,
|
|
367
|
+
permissions: new Set(principal.scopes),
|
|
368
|
+
};
|
|
369
|
+
},
|
|
370
|
+
}),
|
|
371
|
+
]),
|
|
372
|
+
},
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export default await createPlatformConfig();
|
|
@@ -11,14 +11,14 @@
|
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"@app/module-example": "workspace:*",
|
|
14
|
-
"@octanejs/seo": "0.0.
|
|
15
|
-
"@octanejs/vite-plugin": "0.1
|
|
16
|
-
"octane": "0.1
|
|
14
|
+
"@octanejs/seo": "0.0.46",
|
|
15
|
+
"@octanejs/vite-plugin": "0.2.1",
|
|
16
|
+
"octane": "0.9.1",
|
|
17
17
|
"pg": "8.23.0",
|
|
18
|
-
"@flowdular/sdk": "0.
|
|
18
|
+
"@flowdular/sdk": "0.6.1"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
|
-
"@octanejs/app-core": "0.
|
|
21
|
+
"@octanejs/app-core": "0.1.1",
|
|
22
22
|
"@tsrx/typescript-plugin": "0.3.120",
|
|
23
23
|
"@types/node": "24.13.3",
|
|
24
24
|
"@types/pg": "8.23.1",
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
|
|
7
|
+
const stateDirectory = mkdtempSync(join(tmpdir(), 'flowdular-build-'));
|
|
8
|
+
const buildSecret = () => randomBytes(32).toString('base64');
|
|
9
|
+
|
|
10
|
+
/* Vite clears its output trees, but can leave hidden runtime state at the
|
|
11
|
+
dist root. A prior preview must never become part of the next bundle. */
|
|
12
|
+
rmSync(join(import.meta.dirname, '..', 'dist'), {
|
|
13
|
+
recursive: true,
|
|
14
|
+
force: true,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
/* The Octane plugin evaluates the server composition while bundling it. Give
|
|
18
|
+
that build-time process isolated state and ephemeral keys, without using deployment database settings or encryption keys. The emitted server still reads its real
|
|
19
|
+
production environment when it starts. */
|
|
20
|
+
const environment = {
|
|
21
|
+
...process.env,
|
|
22
|
+
/* A production bundle is evaluated while it is built, but the build must not
|
|
23
|
+
connect to the deployment database. The embedded engine writes into the
|
|
24
|
+
throwaway state directory below; runtime still reads its real adapter. */
|
|
25
|
+
FD_ENV: 'development',
|
|
26
|
+
FD_INTERNAL_BUILD: 'true',
|
|
27
|
+
/* The bundler evaluates the server composition. It must not open a trace or
|
|
28
|
+
error egress to the deployment's collector while building. */
|
|
29
|
+
FD_TRACE_EXPORTER: 'none',
|
|
30
|
+
FD_ERROR_SINK: 'none',
|
|
31
|
+
FD_DATABASE_ADAPTER: 'pglite',
|
|
32
|
+
FD_DATABASE_PGLITE_DIRECTORY: join(stateDirectory, 'pglite'),
|
|
33
|
+
FD_AGENT_CREDENTIAL_KEY: buildSecret(),
|
|
34
|
+
FD_AGENT_RUN_GRANT_KEY: buildSecret(),
|
|
35
|
+
FD_APPROVAL_GRANT_KEY: buildSecret(),
|
|
36
|
+
FD_AUTOMATIONS_CREDENTIAL_KEY: buildSecret(),
|
|
37
|
+
FD_NOTIFICATIONS_SECRET_KEY: buildSecret(),
|
|
38
|
+
FD_WORKFLOWS_PAYLOAD_KEY: buildSecret(),
|
|
39
|
+
FD_WORKFLOWS_CURSOR_KEY: buildSecret(),
|
|
40
|
+
FD_STORAGE_ADAPTER: 'local',
|
|
41
|
+
FD_STORAGE_LOCAL_DIRECTORY: join(stateDirectory, 'storage'),
|
|
42
|
+
FD_STORAGE_ENCRYPTION_KEY: buildSecret(),
|
|
43
|
+
FD_CONNECTORS_SECRET_KEY: buildSecret(),
|
|
44
|
+
FD_AUDIT_ANCHOR_KEY: buildSecret(),
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const result = spawnSync('vite', ['build'], {
|
|
49
|
+
env: environment,
|
|
50
|
+
stdio: 'inherit',
|
|
51
|
+
});
|
|
52
|
+
if (result.error) throw result.error;
|
|
53
|
+
process.exitCode = result.status ?? 1;
|
|
54
|
+
} finally {
|
|
55
|
+
rmSync(stateDirectory, { recursive: true, force: true });
|
|
56
|
+
}
|