create-flowdular 0.4.3 → 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.
- package/README.md +16 -10
- package/agent-template/.agents/skills/agent-tool-design/SKILL.md +1 -1
- package/agent-template/.agents/skills/auth-security-review/SKILL.md +2 -2
- package/agent-template/.agents/skills/bug-hunt/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-interview/SKILL.md +20 -20
- package/agent-template/.agents/skills/test-hardening/SKILL.md +2 -2
- package/agent-template/.agents/skills/ux-design/SKILL.md +1 -1
- package/agent-template/.agents/skills/workflow-development/SKILL.md +95 -9
- package/agent-template/.ai/README.md +5 -3
- package/agent-template/.ai/agents/README.md +1 -1
- package/agent-template/.ai/agents/sandbox/agentic-engineer.md +1 -0
- package/agent-template/.ai/agents/sandbox/backend-engineer.md +1 -0
- package/agent-template/.ai/agents/sandbox/frontend-engineer.md +1 -0
- package/agent-template/.ai/blueprints/add-migration/README.md +1 -1
- 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/examples/bad/client-imports-server/README.md +1 -1
- package/agent-template/.ai/examples/bad/missing-acl/README.md +1 -1
- package/agent-template/.ai/examples/bad/tenant-from-body/README.md +1 -1
- package/agent-template/.ai/guides/application-development.md +7 -5
- package/agent-template/.ai/platform-capabilities.md +9 -5
- package/agent-template/.ai/policies/capabilities.yaml +28 -12
- package/agent-template/.ai/policies/task-budgets.yaml +1 -1
- package/agent-template/.ai/rules/flowdular.md +3 -2
- 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 +2 -2
- package/agent-template/.ai/skills/bug-hunt/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-interview/SKILL.md +20 -20
- package/agent-template/.ai/skills/test-hardening/SKILL.md +2 -2
- package/agent-template/.ai/skills/ux-design/SKILL.md +1 -1
- package/agent-template/.ai/skills/workflow-development/SKILL.md +96 -10
- package/agent-template/.ai/subagents/module-executor.md +25 -0
- package/agent-template/.ai/subagents/reviewer.md +23 -0
- package/agent-template/.ai/subagents/spec-author.md +23 -0
- package/agent-template/.claude/agents/module-executor.md +22 -0
- package/agent-template/.claude/agents/reviewer.md +24 -0
- package/agent-template/.claude/agents/spec-author.md +20 -0
- package/agent-template/.claude/skills/agent-tool-design/SKILL.md +1 -1
- package/agent-template/.claude/skills/auth-security-review/SKILL.md +2 -2
- package/agent-template/.claude/skills/bug-hunt/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-interview/SKILL.md +20 -20
- package/agent-template/.claude/skills/test-hardening/SKILL.md +2 -2
- package/agent-template/.claude/skills/ux-design/SKILL.md +1 -1
- package/agent-template/.claude/skills/workflow-development/SKILL.md +95 -9
- package/agent-template/.codex/agents/module-executor.toml +17 -0
- package/agent-template/.codex/agents/reviewer.toml +14 -0
- package/agent-template/.codex/agents/spec-author.toml +15 -0
- package/agent-template/AGENTS.md +3 -2
- package/agent-template/CLAUDE.md +3 -2
- package/agent-template/docs/adr/0007-module-owned-agents.md +35 -1
- package/agent-template/docs/agent-contract.md +2 -2
- package/agent-template/docs/cli.md +24 -3
- package/agent-template/docs/configuration.md +59 -5
- package/agent-template/docs/database-adapters.md +20 -20
- package/agent-template/docs/design-system.md +3 -3
- package/agent-template/docs/getting-started.md +25 -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 +9 -1
- package/agent-template/docs/sandbox.md +117 -6
- package/agent-template/platform/scripts/build.mjs +7 -0
- package/agent-template/rulesync.jsonc +1 -1
- package/dist/bin.js +12 -6
- package/package.json +2 -2
- package/template/default/.env.example +10 -3
- package/template/default/.prettierignore +2 -0
- package/template/default/.vercelignore +8 -0
- package/template/default/README.md +26 -15
- package/template/default/_gitignore +3 -2
- package/template/default/infra/README.md +86 -65
- package/template/default/infra/docker/.env.example +66 -0
- package/template/default/infra/docker/Dockerfile +24 -10
- package/template/default/infra/docker/app-entrypoint.mjs +5 -0
- package/template/default/infra/docker/compose.yaml +105 -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/vercel/README.md +262 -0
- package/template/default/infra/vercel/build.mjs +214 -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 +3 -2
- package/template/default/platform/index.html +7 -19
- package/template/default/platform/octane.config.ts +252 -156
- package/template/default/platform/package.json +5 -5
- package/template/default/platform/public/favicon.svg +1 -1
- package/template/default/platform/scripts/build.mjs +56 -0
- package/template/default/platform/scripts/dev.mjs +38 -0
- package/template/default/platform/src/App.tsrx +25 -1
- package/template/default/platform/src/generated/modules.server.ts +3 -0
- package/template/default/platform/src/server/database.ts +24 -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 +123 -0
- package/template/default/platform/src/server/setup/page.ts +497 -0
- package/template/default/platform/src/server/setup/routes.ts +787 -0
- package/template/default/platform/src/server/setup/sanitize.ts +111 -0
- package/template/default/platform/src/server/setup/seed.ts +145 -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
|
@@ -3,8 +3,6 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
-
<meta name="theme-color" content="#141B2E" />
|
|
7
|
-
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
8
6
|
<style id="flowdular-critical-splash">
|
|
9
7
|
html,
|
|
10
8
|
body,
|
|
@@ -34,6 +32,9 @@
|
|
|
34
32
|
letter-spacing: 0.14em;
|
|
35
33
|
text-transform: uppercase;
|
|
36
34
|
}
|
|
35
|
+
.flowdular-splash__brand::after {
|
|
36
|
+
content: var(--flowdular-brand, 'Flowdular');
|
|
37
|
+
}
|
|
37
38
|
.flowdular-splash__stage {
|
|
38
39
|
min-width: 190px;
|
|
39
40
|
font-size: 10px;
|
|
@@ -80,27 +81,14 @@
|
|
|
80
81
|
aria-hidden="true"
|
|
81
82
|
>
|
|
82
83
|
<g fill="#ffffff">
|
|
83
|
-
<
|
|
84
|
-
<rect x="
|
|
85
|
-
<rect x="6.2" y="10.8" width="3.4" height="9.8" />
|
|
86
|
-
<circle cx="7.9" cy="20.6" r="1.7" />
|
|
87
|
-
<circle cx="16.1" cy="3.4" r="1.7" />
|
|
88
|
-
<rect x="14.4" y="3.4" width="3.4" height="9.8" />
|
|
89
|
-
<rect x="14.4" y="19" width="3.4" height="1.6" />
|
|
90
|
-
<circle cx="16.1" cy="20.6" r="1.7" />
|
|
84
|
+
<rect x="2" y="3" width="20" height="5" rx="2.5" />
|
|
85
|
+
<rect x="8" y="10" width="14" height="5" rx="2.5" />
|
|
91
86
|
</g>
|
|
92
87
|
<g fill="#e08a45">
|
|
93
|
-
<
|
|
94
|
-
<rect x="3.4" y="6.2" width="9.8" height="3.4" />
|
|
95
|
-
<rect x="19" y="6.2" width="1.6" height="3.4" />
|
|
96
|
-
<circle cx="20.6" cy="7.9" r="1.7" />
|
|
97
|
-
<circle cx="3.4" cy="16.1" r="1.7" />
|
|
98
|
-
<rect x="3.4" y="14.4" width="1.6" height="3.4" />
|
|
99
|
-
<rect x="10.8" y="14.4" width="9.8" height="3.4" />
|
|
100
|
-
<circle cx="20.6" cy="16.1" r="1.7" />
|
|
88
|
+
<rect x="14" y="17" width="8" height="5" rx="2.5" />
|
|
101
89
|
</g>
|
|
102
90
|
</svg>
|
|
103
|
-
<span
|
|
91
|
+
<span class="flowdular-splash__brand"></span>
|
|
104
92
|
<small id="flowdular-splash-stage" class="flowdular-splash__stage"
|
|
105
93
|
>Starting workspace</small
|
|
106
94
|
>
|
|
@@ -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 =
|
|
81
|
+
const workspaceRoot = findWorkspaceRoot(import.meta.dirname);
|
|
63
82
|
const building = process.env.FD_INTERNAL_BUILD === 'true';
|
|
64
83
|
|
|
65
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
133
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
});
|
|
138
|
-
for (const composition of moduleCompositions) {
|
|
139
|
-
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
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 === '/'),
|
|
273
|
+
createCorsMiddleware({
|
|
274
|
+
allowOrigin: (origin) => authRuntime.apiOriginAllowed(origin),
|
|
196
275
|
}),
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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.
|
|
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.0"
|
|
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",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><rect width="24" height="24" rx="5.4" fill="#
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><rect width="24" height="24" rx="5.4" fill="#141b2e"/><g transform="translate(12 12) scale(0.72) translate(-12 -12)"><rect x="2" y="3" width="20" height="5" rx="2.5" fill="#ffffff"/><rect x="8" y="10" width="14" height="5" rx="2.5" fill="#ffffff"/><rect x="14" y="17" width="8" height="5" rx="2.5" fill="#e08a45"/></g></svg>
|
|
@@ -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);
|