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.
Files changed (105) hide show
  1. package/README.md +8 -5
  2. package/agent-template/.agents/skills/agent-tool-design/SKILL.md +1 -1
  3. package/agent-template/.agents/skills/auth-security-review/SKILL.md +1 -1
  4. package/agent-template/.agents/skills/database-adapter/SKILL.md +5 -5
  5. package/agent-template/.agents/skills/database-adapter/references/first-run-and-matrix.md +2 -2
  6. package/agent-template/.agents/skills/deploy-operate/SKILL.md +1 -1
  7. package/agent-template/.agents/skills/migration-authoring/SKILL.md +4 -4
  8. package/agent-template/.agents/skills/module-new/SKILL.md +1 -1
  9. package/agent-template/.agents/skills/spec-approval/SKILL.md +6 -2
  10. package/agent-template/.agents/skills/spec-interview/SKILL.md +2 -2
  11. package/agent-template/.agents/skills/test-hardening/SKILL.md +2 -2
  12. package/agent-template/.agents/skills/workflow-development/SKILL.md +95 -9
  13. package/agent-template/.ai/agents/sandbox/business-manager.md +2 -2
  14. package/agent-template/.ai/blueprints/add-migration/required-files.yaml +1 -1
  15. package/agent-template/.ai/blueprints/new-module/required-files.yaml +1 -1
  16. package/agent-template/.ai/platform-capabilities.md +10 -8
  17. package/agent-template/.ai/policies/capabilities.yaml +28 -12
  18. package/agent-template/.ai/skills/README.md +1 -1
  19. package/agent-template/.ai/skills/agent-tool-design/SKILL.md +1 -1
  20. package/agent-template/.ai/skills/auth-security-review/SKILL.md +1 -1
  21. package/agent-template/.ai/skills/database-adapter/SKILL.md +5 -5
  22. package/agent-template/.ai/skills/database-adapter/references/first-run-and-matrix.md +2 -2
  23. package/agent-template/.ai/skills/deploy-operate/SKILL.md +1 -1
  24. package/agent-template/.ai/skills/migration-authoring/SKILL.md +4 -4
  25. package/agent-template/.ai/skills/module-new/SKILL.md +1 -1
  26. package/agent-template/.ai/skills/spec-approval/SKILL.md +6 -2
  27. package/agent-template/.ai/skills/spec-interview/SKILL.md +2 -2
  28. package/agent-template/.ai/skills/test-hardening/SKILL.md +2 -2
  29. package/agent-template/.ai/skills/workflow-development/SKILL.md +96 -10
  30. package/agent-template/.claude/skills/agent-tool-design/SKILL.md +1 -1
  31. package/agent-template/.claude/skills/auth-security-review/SKILL.md +1 -1
  32. package/agent-template/.claude/skills/database-adapter/SKILL.md +5 -5
  33. package/agent-template/.claude/skills/database-adapter/references/first-run-and-matrix.md +2 -2
  34. package/agent-template/.claude/skills/deploy-operate/SKILL.md +1 -1
  35. package/agent-template/.claude/skills/migration-authoring/SKILL.md +4 -4
  36. package/agent-template/.claude/skills/module-new/SKILL.md +1 -1
  37. package/agent-template/.claude/skills/spec-approval/SKILL.md +6 -2
  38. package/agent-template/.claude/skills/spec-interview/SKILL.md +2 -2
  39. package/agent-template/.claude/skills/test-hardening/SKILL.md +2 -2
  40. package/agent-template/.claude/skills/workflow-development/SKILL.md +95 -9
  41. package/agent-template/docs/adr/0003-module-settings.md +2 -0
  42. package/agent-template/docs/adr/0007-module-owned-agents.md +35 -1
  43. package/agent-template/docs/agent-contract.md +3 -3
  44. package/agent-template/docs/cli-extensions.md +1 -0
  45. package/agent-template/docs/cli.md +40 -3
  46. package/agent-template/docs/configuration.md +64 -9
  47. package/agent-template/docs/database-adapters.md +30 -22
  48. package/agent-template/docs/design-system.md +7 -3
  49. package/agent-template/docs/getting-started.md +29 -32
  50. package/agent-template/docs/module-distribution.md +79 -86
  51. package/agent-template/docs/module-web-surfaces.md +9 -7
  52. package/agent-template/docs/modules.md +6 -2
  53. package/agent-template/docs/sandbox.md +23 -4
  54. package/agent-template/platform/scripts/build.mjs +7 -0
  55. package/dist/bin.js +3 -6
  56. package/package.json +1 -1
  57. package/template/default/.env.example +8 -3
  58. package/template/default/.vercelignore +8 -0
  59. package/template/default/README.md +20 -11
  60. package/template/default/_gitignore +3 -2
  61. package/template/default/infra/README.md +86 -65
  62. package/template/default/infra/docker/.env.example +71 -0
  63. package/template/default/infra/docker/Dockerfile +29 -11
  64. package/template/default/infra/docker/app-entrypoint.mjs +5 -0
  65. package/template/default/infra/docker/compose.yaml +109 -58
  66. package/template/default/infra/docker/database-urls.mjs +28 -0
  67. package/template/default/infra/docker/pitr.sh +177 -0
  68. package/template/default/infra/docker/postgres/10-roles.sh +16 -12
  69. package/template/default/infra/docker/start.mjs +402 -0
  70. package/template/default/infra/kubernetes/database-secret.example.yaml +3 -3
  71. package/template/default/infra/kubernetes/deployment.yaml +5 -0
  72. package/template/default/infra/sdk-module-manifests.mjs +118 -0
  73. package/template/default/infra/vercel/README.md +262 -0
  74. package/template/default/infra/vercel/build.mjs +223 -0
  75. package/template/default/infra/vercel/handler.mjs +100 -0
  76. package/template/default/modules/example/migrations/0001_example_core.up.sql +2 -2
  77. package/template/default/modules/example/module.json +1 -1
  78. package/template/default/modules/example/package.json +3 -3
  79. package/template/default/modules/example/spec/module.yaml +1 -1
  80. package/template/default/modules/example/src/services/migration.ts +2 -2
  81. package/template/default/modules/example/tests/module.test.ts +1 -1
  82. package/template/default/package.json +2 -3
  83. package/template/default/platform/octane.config.ts +290 -156
  84. package/template/default/platform/package.json +5 -5
  85. package/template/default/platform/scripts/build.mjs +56 -0
  86. package/template/default/platform/scripts/dev.mjs +101 -18
  87. package/template/default/platform/src/generated/modules.server.ts +1 -0
  88. package/template/default/platform/src/server/database.ts +24 -0
  89. package/template/default/platform/src/server/lifecycle.ts +325 -0
  90. package/template/default/platform/src/server/runtime-role.ts +33 -0
  91. package/template/default/platform/src/server/setup/access.ts +160 -0
  92. package/template/default/platform/src/server/setup/adapters.ts +554 -0
  93. package/template/default/platform/src/server/setup/environment.ts +154 -0
  94. package/template/default/platform/src/server/setup/gate.ts +84 -0
  95. package/template/default/platform/src/server/setup/index.ts +181 -0
  96. package/template/default/platform/src/server/setup/modules.ts +119 -0
  97. package/template/default/platform/src/server/setup/page.ts +548 -0
  98. package/template/default/platform/src/server/setup/routes.ts +788 -0
  99. package/template/default/platform/src/server/setup/sanitize.ts +111 -0
  100. package/template/default/platform/src/server/setup/seed.ts +192 -0
  101. package/template/default/platform/src/server/setup/token.ts +79 -0
  102. package/template/default/platform/src/server/worker-tick.ts +193 -0
  103. package/template/default/platform/src/server/workspace-root.ts +16 -0
  104. package/template/default/render.yaml +70 -0
  105. 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 = resolve(import.meta.dirname, '..');
85
+ const workspaceRoot = findWorkspaceRoot(import.meta.dirname);
63
86
  const building = process.env.FD_INTERNAL_BUILD === 'true';
64
87
 
65
- /* Composed first and drained last: a trace or an error report is evidence about
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
- environment: building
70
- ? { ...process.env, NODE_ENV: 'development' }
71
- : process.env,
72
- });
73
- const databases = createPlatformDatabaseProvider(
74
- databaseProviderConfigFromEnvironment(
75
- building ? { ...process.env, NODE_ENV: 'development' } : process.env,
76
- workspaceRoot,
77
- ),
78
- );
79
- const storageKeyring = createStorageKeyring(process.env, workspaceRoot);
80
- const storage = createStoragePort(
81
- storageConfigFromEnvironment(
82
- building ? { ...process.env, NODE_ENV: 'development' } : process.env,
83
- workspaceRoot,
84
- ),
85
- { keyring: storageKeyring },
86
- );
87
- const configuredApplicationPath = validateApplicationPath(
88
- process.env.FD_APPLICATION_PATH ?? applicationBasePath,
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
- mailConfigFromEnvironment(
95
- building ? { ...process.env, NODE_ENV: 'development' } : process.env,
96
- ),
97
- { createSmtpTransport: nodemailerSmtpTransport },
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
- ...authRuntimeOptionsFromEnvironment(
104
- building ? { ...process.env, NODE_ENV: 'development' } : process.env,
105
- workspaceRoot,
106
- ),
107
- applicationPath: configuredApplicationPath,
108
- databases,
109
- dataClasses,
110
- mail,
111
- metrics: createModuleMetrics('auth.core'),
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 agentDefinitions = createPlatformAgentRegistry();
118
- const moduleCompositions = composeModuleServer({
119
- environment: process.env,
120
- workspaceRoot,
121
- auth: authRuntime,
122
- settings,
123
- agentTools: createPlatformToolRegistry(),
124
- agentDefinitions,
125
- capabilities: createPlatformCapabilityRegistry(),
126
- dataClasses,
127
- databases,
128
- storage,
129
- /* The relay auth.core resolves per message from its stored settings, falling
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
- mail: authRuntime.mail,
133
- /* Rebound to the composing module by the generated composition; this binding
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
- metrics: createModuleMetrics('platform'),
136
- tracer: serverTracer(),
137
- });
138
- for (const composition of moduleCompositions) {
139
- if (composition.settings) settings.declare(composition.settings);
140
- }
141
- agentDefinitions.seal();
142
- /* Sealed once every composition has run and before any start hook reads the
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
- await databases.check();
150
- /* Platform-scoped settings are read by background work before any request
151
- could prime them; a workspace is primed by the authentication middleware. */
152
- await settings.prime(PLATFORM_SETTINGS_TENANT);
153
- for (const composition of moduleCompositions) await composition.prepare?.();
154
- for (const composition of moduleCompositions) composition.start?.();
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
- let stopping = false;
158
- const shutdown = async () => {
159
- if (stopping) return;
160
- stopping = true;
161
- for (const composition of moduleCompositions) {
162
- await composition.stop?.();
163
- await composition.dispose?.();
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
- export default defineConfig({
181
- middlewares: [
182
- /* Ahead of the authentication: a cross-origin preflight carries no
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
- createCorsMiddleware({
186
- allowOrigin: (origin) => authRuntime.apiOriginAllowed(origin),
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
- healthEndpoint.serverRoute,
198
- createReadinessEndpoint(databases).serverRoute,
199
- ...createMetricsRoutes({ environment: process.env }),
200
- /* Describes every operation the presented credential may call, built
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
- ...createOpenApiRoutes({
203
- resolveIdentity: endpointIdentityFromContext,
204
- publicBaseUrl: authRuntime.publicBaseUrl,
205
- }),
206
- ...createStorageRoutes({
207
- storage,
208
- keyring: storageKeyring,
209
- environment: process.env,
210
- }),
211
- ...createAuthRoutes(authRuntime),
212
- ...moduleCompositions.flatMap((composition) => composition.routes),
213
- ...createModuleWebRoutes({
214
- modules: moduleCompositions,
215
- mounts: moduleWebMounts,
216
- applicationPath: configuredApplicationPath,
217
- /* A build composes with NODE_ENV forced to development, so the flag
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
- development: !building && process.env.NODE_ENV !== 'production',
221
- resolveIdentity: async (context) => {
222
- const principal = principalFromContext(context);
223
- if (!principal) return null;
224
- /* The /api gate does not reach these pages, and a member who
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
- if (
228
- !isTokenPrincipal(context) &&
229
- !(await mfaEnrolmentSatisfied(authRuntime, principal))
230
- ) {
231
- return null;
232
- }
233
- return {
234
- subjectId: principal.accountId,
235
- tenantId: principal.tenantId,
236
- permissions: new Set(principal.scopes),
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.38",
15
- "@octanejs/vite-plugin": "0.1.51",
16
- "octane": "0.1.51",
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.5.1"
18
+ "@flowdular/sdk": "0.6.1"
19
19
  },
20
20
  "devDependencies": {
21
- "@octanejs/app-core": "0.0.47",
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
+ }