create-flowdular 0.5.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) 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/test-hardening/SKILL.md +2 -2
  10. package/agent-template/.agents/skills/workflow-development/SKILL.md +95 -9
  11. package/agent-template/.ai/blueprints/add-migration/required-files.yaml +1 -1
  12. package/agent-template/.ai/blueprints/new-module/required-files.yaml +1 -1
  13. package/agent-template/.ai/platform-capabilities.md +7 -5
  14. package/agent-template/.ai/policies/capabilities.yaml +28 -12
  15. package/agent-template/.ai/skills/README.md +1 -1
  16. package/agent-template/.ai/skills/agent-tool-design/SKILL.md +1 -1
  17. package/agent-template/.ai/skills/auth-security-review/SKILL.md +1 -1
  18. package/agent-template/.ai/skills/database-adapter/SKILL.md +5 -5
  19. package/agent-template/.ai/skills/database-adapter/references/first-run-and-matrix.md +2 -2
  20. package/agent-template/.ai/skills/deploy-operate/SKILL.md +1 -1
  21. package/agent-template/.ai/skills/migration-authoring/SKILL.md +4 -4
  22. package/agent-template/.ai/skills/module-new/SKILL.md +1 -1
  23. package/agent-template/.ai/skills/test-hardening/SKILL.md +2 -2
  24. package/agent-template/.ai/skills/workflow-development/SKILL.md +96 -10
  25. package/agent-template/.claude/skills/agent-tool-design/SKILL.md +1 -1
  26. package/agent-template/.claude/skills/auth-security-review/SKILL.md +1 -1
  27. package/agent-template/.claude/skills/database-adapter/SKILL.md +5 -5
  28. package/agent-template/.claude/skills/database-adapter/references/first-run-and-matrix.md +2 -2
  29. package/agent-template/.claude/skills/deploy-operate/SKILL.md +1 -1
  30. package/agent-template/.claude/skills/migration-authoring/SKILL.md +4 -4
  31. package/agent-template/.claude/skills/module-new/SKILL.md +1 -1
  32. package/agent-template/.claude/skills/test-hardening/SKILL.md +2 -2
  33. package/agent-template/.claude/skills/workflow-development/SKILL.md +95 -9
  34. package/agent-template/docs/adr/0007-module-owned-agents.md +35 -1
  35. package/agent-template/docs/agent-contract.md +2 -2
  36. package/agent-template/docs/cli.md +24 -3
  37. package/agent-template/docs/configuration.md +32 -5
  38. package/agent-template/docs/database-adapters.md +20 -20
  39. package/agent-template/docs/design-system.md +1 -1
  40. package/agent-template/docs/getting-started.md +25 -32
  41. package/agent-template/docs/module-distribution.md +79 -86
  42. package/agent-template/docs/module-web-surfaces.md +9 -7
  43. package/agent-template/docs/modules.md +3 -1
  44. package/agent-template/platform/scripts/build.mjs +7 -0
  45. package/dist/bin.js +3 -6
  46. package/package.json +1 -1
  47. package/template/default/.env.example +3 -3
  48. package/template/default/.vercelignore +8 -0
  49. package/template/default/README.md +20 -11
  50. package/template/default/_gitignore +3 -2
  51. package/template/default/infra/README.md +86 -65
  52. package/template/default/infra/docker/.env.example +66 -0
  53. package/template/default/infra/docker/Dockerfile +24 -10
  54. package/template/default/infra/docker/app-entrypoint.mjs +5 -0
  55. package/template/default/infra/docker/compose.yaml +105 -58
  56. package/template/default/infra/docker/database-urls.mjs +28 -0
  57. package/template/default/infra/docker/pitr.sh +177 -0
  58. package/template/default/infra/docker/postgres/10-roles.sh +16 -12
  59. package/template/default/infra/docker/start.mjs +402 -0
  60. package/template/default/infra/kubernetes/database-secret.example.yaml +3 -3
  61. package/template/default/infra/vercel/README.md +262 -0
  62. package/template/default/infra/vercel/build.mjs +214 -0
  63. package/template/default/infra/vercel/handler.mjs +100 -0
  64. package/template/default/modules/example/migrations/0001_example_core.up.sql +2 -2
  65. package/template/default/modules/example/module.json +1 -1
  66. package/template/default/modules/example/package.json +3 -3
  67. package/template/default/modules/example/spec/module.yaml +1 -1
  68. package/template/default/modules/example/src/services/migration.ts +2 -2
  69. package/template/default/modules/example/tests/module.test.ts +1 -1
  70. package/template/default/package.json +2 -2
  71. package/template/default/platform/octane.config.ts +252 -156
  72. package/template/default/platform/package.json +5 -5
  73. package/template/default/platform/scripts/build.mjs +56 -0
  74. package/template/default/platform/scripts/dev.mjs +38 -0
  75. package/template/default/platform/src/generated/modules.server.ts +1 -0
  76. package/template/default/platform/src/server/database.ts +24 -0
  77. package/template/default/platform/src/server/runtime-role.ts +33 -0
  78. package/template/default/platform/src/server/setup/access.ts +160 -0
  79. package/template/default/platform/src/server/setup/adapters.ts +554 -0
  80. package/template/default/platform/src/server/setup/environment.ts +154 -0
  81. package/template/default/platform/src/server/setup/gate.ts +84 -0
  82. package/template/default/platform/src/server/setup/index.ts +181 -0
  83. package/template/default/platform/src/server/setup/modules.ts +123 -0
  84. package/template/default/platform/src/server/setup/page.ts +497 -0
  85. package/template/default/platform/src/server/setup/routes.ts +787 -0
  86. package/template/default/platform/src/server/setup/sanitize.ts +111 -0
  87. package/template/default/platform/src/server/setup/seed.ts +145 -0
  88. package/template/default/platform/src/server/setup/token.ts +79 -0
  89. package/template/default/platform/src/server/worker-tick.ts +193 -0
  90. package/template/default/platform/src/server/workspace-root.ts +16 -0
  91. package/template/default/render.yaml +70 -0
  92. 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,24 @@ 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';
45
47
  import { createMetricsRoutes } from './src/server/metrics.ts';
48
+ import {
49
+ platformRuntimeRole,
50
+ startModuleWorkers,
51
+ } from './src/server/runtime-role.ts';
52
+ import {
53
+ createWorkerTickEndpoint,
54
+ createWorkerTicker,
55
+ WORKER_TICK_PATH,
56
+ workerTickConfigFromEnvironment,
57
+ } from './src/server/worker-tick.ts';
46
58
  import { createPlatformObservability } from './src/server/tracing.ts';
47
59
  import {
48
60
  createStorageKeyring,
@@ -50,6 +62,13 @@ import {
50
62
  createStorageRoutes,
51
63
  storageConfigFromEnvironment,
52
64
  } from './src/server/storage.ts';
65
+ import {
66
+ clearSetupToken,
67
+ configuredDatabaseNeedsFirstRun,
68
+ createFirstRunSetup,
69
+ createInPlaceFirstRun,
70
+ } from './src/server/setup/index.ts';
71
+ import { findWorkspaceRoot } from './src/server/workspace-root.ts';
53
72
 
54
73
  function checkedRoutes<T extends Parameters<typeof assertRouteConflicts>[0]>(
55
74
  routes: T,
@@ -59,184 +78,261 @@ function checkedRoutes<T extends Parameters<typeof assertRouteConflicts>[0]>(
59
78
  }
60
79
 
61
80
  const SHELL = ['App', '/src/App.tsrx'] as const;
62
- const workspaceRoot = resolve(import.meta.dirname, '..');
81
+ const workspaceRoot = findWorkspaceRoot(import.meta.dirname);
63
82
  const building = process.env.FD_INTERNAL_BUILD === 'true';
64
83
 
65
- /* Composed first and drained last: a trace or an error report is evidence about
84
+ function firstRunConfig(databasePreconfigured = false) {
85
+ const setup = createFirstRunSetup({
86
+ applicationPath: applicationBasePath,
87
+ webMountPaths: moduleWebMounts.map((site) => site.path),
88
+ environment: process.env,
89
+ databasePreconfigured,
90
+ workspaceRoot,
91
+ });
92
+ return defineConfig({
93
+ router: {
94
+ routes: checkedRoutes([healthEndpoint.serverRoute, ...setup.routes]),
95
+ },
96
+ });
97
+ }
98
+
99
+ async function createPlatformConfig() {
100
+ loadPlatformEnvironmentFile(workspaceRoot);
101
+ const serverless = process.env.FD_DEPLOYMENT_TARGET === 'vercel';
102
+ if (!building && !platformDatabaseConfigured(process.env)) {
103
+ if (serverless)
104
+ throw new Error(
105
+ 'Vercel requires a configured external PostgreSQL database before deployment.',
106
+ );
107
+ return firstRunConfig();
108
+ }
109
+ /* A Vercel Function can neither restart into the application after setup
110
+ nor keep a token file, so it composes the application and serves setup
111
+ inside it until the first workspace exists. */
112
+ const needsFirstRun =
113
+ !building &&
114
+ (await configuredDatabaseNeedsFirstRun(process.env, workspaceRoot));
115
+ if (needsFirstRun && !serverless) return firstRunConfig(true);
116
+ clearSetupToken(workspaceRoot);
117
+ const runtimeRole = platformRuntimeRole(process.env);
118
+ const workerTick =
119
+ runtimeRole === 'tick'
120
+ ? workerTickConfigFromEnvironment(process.env)
121
+ : null;
122
+
123
+ /* Composed first and drained last: a trace or an error report is evidence about
66
124
  the boot that follows it, and both egresses refuse a misconfigured endpoint
67
125
  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
126
+ const observability = createPlatformObservability({
127
+ environment: building
128
+ ? { ...process.env, NODE_ENV: 'development' }
129
+ : process.env,
130
+ });
131
+ const databases = createPlatformDatabaseProvider(
132
+ databaseProviderConfigFromEnvironment(
133
+ building ? { ...process.env, NODE_ENV: 'development' } : process.env,
134
+ workspaceRoot,
135
+ ),
136
+ );
137
+ const storageKeyring = createStorageKeyring(process.env, workspaceRoot);
138
+ const storage = createStoragePort(
139
+ storageConfigFromEnvironment(
140
+ building ? { ...process.env, NODE_ENV: 'development' } : process.env,
141
+ workspaceRoot,
142
+ ),
143
+ { keyring: storageKeyring },
144
+ );
145
+ const configuredApplicationPath = validateApplicationPath(
146
+ process.env.FD_APPLICATION_PATH ?? applicationBasePath,
147
+ );
148
+ /* One outbound transport for the whole deployment. auth.core is only its
91
149
  first sender; the SMTP client comes from that module because it is the one
92
150
  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
151
+ const mail = createMailPort(
152
+ mailConfigFromEnvironment(
153
+ building ? { ...process.env, NODE_ENV: 'development' } : process.env,
154
+ ),
155
+ { createSmtpTransport: nodemailerSmtpTransport },
156
+ );
157
+ /* Created before the auth runtime so auth.core, which composes outside the
100
158
  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
- });
159
+ const dataClasses = createDataClassRegistry();
160
+ const authRuntime = createAuthRuntime({
161
+ ...authRuntimeOptionsFromEnvironment(
162
+ building ? { ...process.env, NODE_ENV: 'development' } : process.env,
163
+ workspaceRoot,
164
+ ),
165
+ applicationPath: configuredApplicationPath,
166
+ databases,
167
+ dataClasses,
168
+ mail,
169
+ metrics: createModuleMetrics('auth.core'),
170
+ });
113
171
 
114
- /* Module APIs come from the generated composition. Enable or disable modules
172
+ /* Module APIs come from the generated composition. Enable or disable modules
115
173
  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
174
+ const settings = authRuntime.moduleSettings;
175
+ const firstRun = needsFirstRun
176
+ ? createInPlaceFirstRun({
177
+ environment: process.env,
178
+ workspaceRoot,
179
+ applicationPath: configuredApplicationPath,
180
+ webMountPaths: moduleWebMounts.map((site) => site.path),
181
+ passThrough: ['/api/health', '/api/ready', WORKER_TICK_PATH],
182
+ workspaceExists: async () =>
183
+ (await authRuntime.service()).hasAnyTenant(),
184
+ })
185
+ : null;
186
+ const agentDefinitions = createPlatformAgentRegistry();
187
+ const moduleCompositions = composeModuleServer({
188
+ environment: process.env,
189
+ workspaceRoot,
190
+ auth: authRuntime,
191
+ settings,
192
+ agentTools: createPlatformToolRegistry(),
193
+ agentDefinitions,
194
+ capabilities: createPlatformCapabilityRegistry(),
195
+ dataClasses,
196
+ databases,
197
+ storage,
198
+ /* The relay auth.core resolves per message from its stored settings, falling
130
199
  back to the environment port above, so every module of the installation
131
200
  sends through the same one. */
132
- mail: authRuntime.mail,
133
- /* Rebound to the composing module by the generated composition; this binding
201
+ mail: authRuntime.mail,
202
+ /* Rebound to the composing module by the generated composition; this binding
134
203
  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
204
+ metrics: createModuleMetrics('platform'),
205
+ tracer: serverTracer(),
206
+ });
207
+ for (const composition of moduleCompositions) {
208
+ if (composition.settings) settings.declare(composition.settings);
209
+ }
210
+ const ticker = workerTick
211
+ ? createWorkerTicker(moduleCompositions, { windowMs: workerTick.windowMs })
212
+ : null;
213
+ agentDefinitions.seal();
214
+ /* Sealed once every composition has run and before any start hook reads the
143
215
  catalogue, so every reader sees the declarations the modules agreed on. */
144
- dataClasses.seal();
216
+ dataClasses.seal();
217
+
218
+ let stopping = false;
219
+ const shutdown = async () => {
220
+ if (stopping) return;
221
+ stopping = true;
222
+ await ticker?.close();
223
+ for (const composition of moduleCompositions) {
224
+ await composition.stop?.();
225
+ await composition.dispose?.();
226
+ }
227
+ await authRuntime.dispose();
228
+ await storage.dispose();
229
+ await databases.dispose();
230
+ /* Last, so the spans and error reports this process queued while it stopped
231
+ still leave with it. */
232
+ await observability.dispose();
233
+ };
145
234
 
146
- /* check() proves the runtime role holds neither SUPERUSER nor BYPASSRLS before
235
+ /* check() proves the runtime role holds neither SUPERUSER nor BYPASSRLS before
147
236
  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
- }
237
+ if (!building) {
238
+ try {
239
+ await databases.check();
240
+ /* Platform-scoped settings are read by background work before any request
241
+ could prime them; a workspace is primed by the authentication middleware. */
242
+ await settings.prime(PLATFORM_SETTINGS_TENANT);
243
+ for (const composition of moduleCompositions)
244
+ await composition.prepare?.();
245
+ for (const composition of moduleCompositions) composition.start?.();
246
+ await startModuleWorkers(moduleCompositions, runtimeRole);
247
+ } catch (error) {
248
+ /* A worker that started before the failure would keep running in a
249
+ process that never serves. The boot failure is the one rethrown. */
250
+ await shutdown().catch((cleanupError: unknown) => {
251
+ serverLogger().error('platform boot cleanup failed', {
252
+ module: 'platform',
253
+ err: cleanupError,
254
+ });
255
+ });
256
+ throw error;
257
+ }
258
+ }
156
259
 
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?.();
260
+ // Bundling needs route declarations without background work or retained leases.
261
+ if (building) {
262
+ await shutdown();
263
+ } else {
264
+ process.once('SIGINT', () => void shutdown());
265
+ process.once('SIGTERM', () => void shutdown());
164
266
  }
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
267
 
180
- export default defineConfig({
181
- middlewares: [
182
- /* Ahead of the authentication: a cross-origin preflight carries no
268
+ return defineConfig({
269
+ middlewares: [
270
+ /* Ahead of the authentication: a cross-origin preflight carries no
183
271
  credential and has to be answered before anything asks for one. The
184
272
  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 === '/'),
273
+ createCorsMiddleware({
274
+ allowOrigin: (origin) => authRuntime.apiOriginAllowed(origin),
196
275
  }),
197
- healthEndpoint.serverRoute,
198
- createReadinessEndpoint(databases).serverRoute,
199
- ...createMetricsRoutes({ environment: process.env }),
200
- /* Describes every operation the presented credential may call, built
276
+ ...(firstRun ? [firstRun.middleware] : []),
277
+ authRuntime.middleware,
278
+ ],
279
+ router: {
280
+ routes: checkedRoutes([
281
+ ...createApplicationRoutes({
282
+ path: configuredApplicationPath,
283
+ entry: SHELL,
284
+ publicRoot: moduleWebMounts.some((site) => site.path === '/'),
285
+ }),
286
+ healthEndpoint.serverRoute,
287
+ createReadinessEndpoint(databases).serverRoute,
288
+ ...(ticker && workerTick
289
+ ? [createWorkerTickEndpoint(ticker, workerTick).serverRoute]
290
+ : []),
291
+ ...createMetricsRoutes({ environment: process.env }),
292
+ /* Describes every operation the presented credential may call, built
201
293
  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
294
+ ...createOpenApiRoutes({
295
+ resolveIdentity: endpointIdentityFromContext,
296
+ publicBaseUrl: authRuntime.publicBaseUrl,
297
+ }),
298
+ ...createStorageRoutes({
299
+ storage,
300
+ keyring: storageKeyring,
301
+ environment: process.env,
302
+ }),
303
+ ...createAuthRoutes(authRuntime),
304
+ ...(firstRun?.routes ?? []),
305
+ ...moduleCompositions.flatMap((composition) => composition.routes),
306
+ ...createModuleWebRoutes({
307
+ modules: moduleCompositions,
308
+ mounts: moduleWebMounts,
309
+ applicationPath: configuredApplicationPath,
310
+ /* A build composes with NODE_ENV forced to development, so the flag
218
311
  asks for both: only a development server serves a page whose
219
312
  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
313
+ development: !building && process.env.NODE_ENV !== 'production',
314
+ resolveIdentity: async (context) => {
315
+ const principal = principalFromContext(context);
316
+ if (!principal) return null;
317
+ /* The /api gate does not reach these pages, and a member who
225
318
  still owes enrolment must not read workspace data from one.
226
319
  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
- });
320
+ if (
321
+ !isTokenPrincipal(context) &&
322
+ !(await mfaEnrolmentSatisfied(authRuntime, principal))
323
+ ) {
324
+ return null;
325
+ }
326
+ return {
327
+ subjectId: principal.accountId,
328
+ tenantId: principal.tenantId,
329
+ permissions: new Set(principal.scopes),
330
+ };
331
+ },
332
+ }),
333
+ ]),
334
+ },
335
+ });
336
+ }
337
+
338
+ 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.0"
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
+ }
@@ -1,5 +1,6 @@
1
1
  import { resolve } from 'node:path';
2
2
  import { fileURLToPath } from 'node:url';
3
+ import { spawnSync } from 'node:child_process';
3
4
  import { createServer } from 'vite';
4
5
  import {
5
6
  createOctaneLogger,
@@ -18,6 +19,7 @@ const flag = (name) => {
18
19
  };
19
20
  const port = Number(flag('--port'));
20
21
  const host = flag('--host');
22
+ if (Number.isInteger(port) && port > 0) process.env.PORT = String(port);
21
23
  const verbose =
22
24
  process.argv.includes('--verbose') ||
23
25
  process.argv.includes('-v') ||
@@ -59,6 +61,42 @@ printReady({
59
61
  ],
60
62
  });
61
63
 
64
+ function openBrowser(url) {
65
+ if (
66
+ process.platform === 'linux' &&
67
+ !process.env.DISPLAY &&
68
+ !process.env.WAYLAND_DISPLAY
69
+ )
70
+ return false;
71
+ const command =
72
+ process.platform === 'darwin'
73
+ ? 'open'
74
+ : process.platform === 'win32'
75
+ ? 'rundll32.exe'
76
+ : 'xdg-open';
77
+ const args =
78
+ process.platform === 'win32' ? ['url.dll,FileProtocolHandler', url] : [url];
79
+ const result = spawnSync(command, args, { stdio: 'ignore', timeout: 10000 });
80
+ return !result.error && result.status === 0;
81
+ }
82
+
83
+ const localUrl = server.resolvedUrls?.local?.[0];
84
+ if (localUrl && !process.argv.includes('--no-open')) {
85
+ const setupUrl = new URL('/setup', localUrl).href;
86
+ try {
87
+ const response = await fetch(setupUrl, {
88
+ signal: AbortSignal.timeout(30000),
89
+ });
90
+ if (response.ok && (await response.text()).includes('setup-page')) {
91
+ console.log(`First-run setup: ${setupUrl}`);
92
+ if (!openBrowser(setupUrl))
93
+ console.log('Open the setup URL in a browser on this workstation.');
94
+ }
95
+ } catch {
96
+ // The server remains usable when a browser is unavailable.
97
+ }
98
+ }
99
+
62
100
  /* Closing without exiting lets octane.config.ts release the database on the
63
101
  same signal; the process ends once both have drained. */
64
102
  const stop = () => void server.close().finally(restoreConsole);
@@ -121,6 +121,7 @@ export function composeModuleServer(
121
121
  'agents.run-queue',
122
122
  'agents.run-execution.v2',
123
123
  'agents.actions.v1',
124
+ 'agents.actions.v2',
124
125
  'agents.decisions.v1',
125
126
  ],
126
127
  requires: [
@@ -7,8 +7,11 @@ import {
7
7
  } from '@flowdular/sdk/database';
8
8
  import { createPgliteCluster } from '@flowdular/sdk/database-pglite';
9
9
  import { Pool } from 'pg';
10
+ import process from 'node:process';
11
+ import { resolve } from 'node:path';
10
12
 
11
13
  export { databaseProviderConfigFromEnvironment };
14
+ export type PlatformDatabaseProvider = ConfiguredDatabaseProvider;
12
15
 
13
16
  /* The deployable owns both PostgreSQL drivers: node-postgres for a deployment
14
17
  and the embedded build for a local run. @flowdular/sdk/database stays driver free,
@@ -23,3 +26,24 @@ export function createPlatformDatabaseProvider(
23
26
  ...factories,
24
27
  });
25
28
  }
29
+
30
+ /** A missing PostgreSQL URL is an unconfigured installation. */
31
+ export function platformDatabaseConfigured(
32
+ environment: NodeJS.ProcessEnv,
33
+ ): boolean {
34
+ const adapter =
35
+ environment.FD_DATABASE_ADAPTER?.trim() ||
36
+ (environment.NODE_ENV === 'production' ? 'postgresql' : 'pglite');
37
+ return (
38
+ adapter !== 'postgresql' || Boolean(environment.FD_DATABASE_URL?.trim())
39
+ );
40
+ }
41
+
42
+ /** An orchestrator's environment takes precedence over the generated .env. */
43
+ export function loadPlatformEnvironmentFile(workspaceRoot: string): void {
44
+ try {
45
+ process.loadEnvFile(resolve(workspaceRoot, '.env'));
46
+ } catch {
47
+ // Container deployments configure the process without a workspace .env.
48
+ }
49
+ }