create-flowdular 0.5.1 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -5
- package/agent-template/.agents/skills/agent-tool-design/SKILL.md +1 -1
- package/agent-template/.agents/skills/auth-security-review/SKILL.md +1 -1
- package/agent-template/.agents/skills/database-adapter/SKILL.md +5 -5
- package/agent-template/.agents/skills/database-adapter/references/first-run-and-matrix.md +2 -2
- package/agent-template/.agents/skills/deploy-operate/SKILL.md +1 -1
- package/agent-template/.agents/skills/migration-authoring/SKILL.md +4 -4
- package/agent-template/.agents/skills/module-new/SKILL.md +1 -1
- package/agent-template/.agents/skills/spec-approval/SKILL.md +6 -2
- package/agent-template/.agents/skills/spec-interview/SKILL.md +2 -2
- package/agent-template/.agents/skills/test-hardening/SKILL.md +2 -2
- package/agent-template/.agents/skills/workflow-development/SKILL.md +95 -9
- package/agent-template/.ai/agents/sandbox/business-manager.md +2 -2
- package/agent-template/.ai/blueprints/add-migration/required-files.yaml +1 -1
- package/agent-template/.ai/blueprints/new-module/required-files.yaml +1 -1
- package/agent-template/.ai/platform-capabilities.md +10 -8
- package/agent-template/.ai/policies/capabilities.yaml +28 -12
- package/agent-template/.ai/skills/README.md +1 -1
- package/agent-template/.ai/skills/agent-tool-design/SKILL.md +1 -1
- package/agent-template/.ai/skills/auth-security-review/SKILL.md +1 -1
- package/agent-template/.ai/skills/database-adapter/SKILL.md +5 -5
- package/agent-template/.ai/skills/database-adapter/references/first-run-and-matrix.md +2 -2
- package/agent-template/.ai/skills/deploy-operate/SKILL.md +1 -1
- package/agent-template/.ai/skills/migration-authoring/SKILL.md +4 -4
- package/agent-template/.ai/skills/module-new/SKILL.md +1 -1
- package/agent-template/.ai/skills/spec-approval/SKILL.md +6 -2
- package/agent-template/.ai/skills/spec-interview/SKILL.md +2 -2
- package/agent-template/.ai/skills/test-hardening/SKILL.md +2 -2
- package/agent-template/.ai/skills/workflow-development/SKILL.md +96 -10
- package/agent-template/.claude/skills/agent-tool-design/SKILL.md +1 -1
- package/agent-template/.claude/skills/auth-security-review/SKILL.md +1 -1
- package/agent-template/.claude/skills/database-adapter/SKILL.md +5 -5
- package/agent-template/.claude/skills/database-adapter/references/first-run-and-matrix.md +2 -2
- package/agent-template/.claude/skills/deploy-operate/SKILL.md +1 -1
- package/agent-template/.claude/skills/migration-authoring/SKILL.md +4 -4
- package/agent-template/.claude/skills/module-new/SKILL.md +1 -1
- package/agent-template/.claude/skills/spec-approval/SKILL.md +6 -2
- package/agent-template/.claude/skills/spec-interview/SKILL.md +2 -2
- package/agent-template/.claude/skills/test-hardening/SKILL.md +2 -2
- package/agent-template/.claude/skills/workflow-development/SKILL.md +95 -9
- package/agent-template/docs/adr/0003-module-settings.md +2 -0
- package/agent-template/docs/adr/0007-module-owned-agents.md +35 -1
- package/agent-template/docs/agent-contract.md +3 -3
- package/agent-template/docs/cli-extensions.md +1 -0
- package/agent-template/docs/cli.md +40 -3
- package/agent-template/docs/configuration.md +64 -9
- package/agent-template/docs/database-adapters.md +30 -22
- package/agent-template/docs/design-system.md +7 -3
- package/agent-template/docs/getting-started.md +29 -32
- package/agent-template/docs/module-distribution.md +79 -86
- package/agent-template/docs/module-web-surfaces.md +9 -7
- package/agent-template/docs/modules.md +6 -2
- package/agent-template/docs/sandbox.md +23 -4
- package/agent-template/platform/scripts/build.mjs +7 -0
- package/dist/bin.js +3 -6
- package/package.json +1 -1
- package/template/default/.env.example +8 -3
- package/template/default/.vercelignore +8 -0
- package/template/default/README.md +20 -11
- package/template/default/_gitignore +3 -2
- package/template/default/infra/README.md +86 -65
- package/template/default/infra/docker/.env.example +71 -0
- package/template/default/infra/docker/Dockerfile +29 -11
- package/template/default/infra/docker/app-entrypoint.mjs +5 -0
- package/template/default/infra/docker/compose.yaml +109 -58
- package/template/default/infra/docker/database-urls.mjs +28 -0
- package/template/default/infra/docker/pitr.sh +177 -0
- package/template/default/infra/docker/postgres/10-roles.sh +16 -12
- package/template/default/infra/docker/start.mjs +402 -0
- package/template/default/infra/kubernetes/database-secret.example.yaml +3 -3
- package/template/default/infra/kubernetes/deployment.yaml +5 -0
- package/template/default/infra/sdk-module-manifests.mjs +118 -0
- package/template/default/infra/vercel/README.md +262 -0
- package/template/default/infra/vercel/build.mjs +223 -0
- package/template/default/infra/vercel/handler.mjs +100 -0
- package/template/default/modules/example/migrations/0001_example_core.up.sql +2 -2
- package/template/default/modules/example/module.json +1 -1
- package/template/default/modules/example/package.json +3 -3
- package/template/default/modules/example/spec/module.yaml +1 -1
- package/template/default/modules/example/src/services/migration.ts +2 -2
- package/template/default/modules/example/tests/module.test.ts +1 -1
- package/template/default/package.json +2 -3
- package/template/default/platform/octane.config.ts +290 -156
- package/template/default/platform/package.json +5 -5
- package/template/default/platform/scripts/build.mjs +56 -0
- package/template/default/platform/scripts/dev.mjs +101 -18
- package/template/default/platform/src/generated/modules.server.ts +1 -0
- package/template/default/platform/src/server/database.ts +24 -0
- package/template/default/platform/src/server/lifecycle.ts +325 -0
- package/template/default/platform/src/server/runtime-role.ts +33 -0
- package/template/default/platform/src/server/setup/access.ts +160 -0
- package/template/default/platform/src/server/setup/adapters.ts +554 -0
- package/template/default/platform/src/server/setup/environment.ts +154 -0
- package/template/default/platform/src/server/setup/gate.ts +84 -0
- package/template/default/platform/src/server/setup/index.ts +181 -0
- package/template/default/platform/src/server/setup/modules.ts +119 -0
- package/template/default/platform/src/server/setup/page.ts +548 -0
- package/template/default/platform/src/server/setup/routes.ts +788 -0
- package/template/default/platform/src/server/setup/sanitize.ts +111 -0
- package/template/default/platform/src/server/setup/seed.ts +192 -0
- package/template/default/platform/src/server/setup/token.ts +79 -0
- package/template/default/platform/src/server/worker-tick.ts +193 -0
- package/template/default/platform/src/server/workspace-root.ts +16 -0
- package/template/default/render.yaml +70 -0
- package/template/default/vercel.json +5 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/* Vercel assigns a different origin to every preview. Read its own deployment
|
|
2
|
+
hostname before importing Octane, whose auth runtime captures the origin at boot. */
|
|
3
|
+
process.env.NODE_ENV = 'production';
|
|
4
|
+
process.env.FD_DEPLOYMENT_TARGET = 'vercel';
|
|
5
|
+
process.env.FD_TRUST_PROXY = 'true';
|
|
6
|
+
process.env.FD_AUTH_SECURE_COOKIE = 'true';
|
|
7
|
+
if (!process.env.FD_AUTH_PUBLIC_ORIGIN && process.env.VERCEL_URL) {
|
|
8
|
+
const host = process.env.VERCEL_URL;
|
|
9
|
+
if (
|
|
10
|
+
!/^[-a-z0-9.]+$/i.test(host) ||
|
|
11
|
+
host.startsWith('.') ||
|
|
12
|
+
host.endsWith('.') ||
|
|
13
|
+
host.includes('..') ||
|
|
14
|
+
!host.includes('.')
|
|
15
|
+
) {
|
|
16
|
+
throw new Error('VERCEL_URL must be a hostname without a scheme or path.');
|
|
17
|
+
}
|
|
18
|
+
process.env.FD_AUTH_PUBLIC_ORIGIN = `https://${host}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/* Vercel Cron presents CRON_SECRET as its bearer token, so the tick secret is
|
|
22
|
+
that value unless the deployment names its own. */
|
|
23
|
+
if (!process.env.FD_WORKER_TICK_SECRET && process.env.CRON_SECRET) {
|
|
24
|
+
process.env.FD_WORKER_TICK_SECRET = process.env.CRON_SECRET;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const { nodeHandler } = await import('./platform/dist/server/entry.js');
|
|
28
|
+
|
|
29
|
+
const WORKER_TICK_PATH = '/api/internal/worker/tick';
|
|
30
|
+
const KICK_INTERVAL_MS = 5_000;
|
|
31
|
+
/* The worker function keeps running after this request hangs up: Vercel ends
|
|
32
|
+
an invocation on disconnect only for functions that opt into cancellation. */
|
|
33
|
+
const KICK_HANDOFF_MS = 2_000;
|
|
34
|
+
const REQUEST_CONTEXT = Symbol.for('@vercel/request-context');
|
|
35
|
+
let lastKick = 0;
|
|
36
|
+
|
|
37
|
+
function kickWorker(origin, secret) {
|
|
38
|
+
const now = Date.now();
|
|
39
|
+
if (now - lastKick < KICK_INTERVAL_MS) return Promise.resolve();
|
|
40
|
+
lastKick = now;
|
|
41
|
+
const headers = { authorization: `Bearer ${secret}` };
|
|
42
|
+
if (process.env.VERCEL_AUTOMATION_BYPASS_SECRET) {
|
|
43
|
+
headers['x-vercel-protection-bypass'] =
|
|
44
|
+
process.env.VERCEL_AUTOMATION_BYPASS_SECRET;
|
|
45
|
+
}
|
|
46
|
+
return fetch(new URL(WORKER_TICK_PATH, origin), {
|
|
47
|
+
method: 'POST',
|
|
48
|
+
headers,
|
|
49
|
+
signal: AbortSignal.timeout(KICK_HANDOFF_MS),
|
|
50
|
+
}).then(
|
|
51
|
+
(response) => response.body?.cancel(),
|
|
52
|
+
() => undefined,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* The web function claims no queued work, so a request that changed state asks
|
|
57
|
+
the worker function for a short tick instead of leaving it to the next cron. */
|
|
58
|
+
function withWorkerKick(handler) {
|
|
59
|
+
const origin = process.env.FD_AUTH_PUBLIC_ORIGIN;
|
|
60
|
+
const secret = process.env.FD_WORKER_TICK_SECRET;
|
|
61
|
+
if (process.env.FD_RUNTIME_ROLE !== 'web' || !origin || !secret) {
|
|
62
|
+
return handler;
|
|
63
|
+
}
|
|
64
|
+
return (request, response) => {
|
|
65
|
+
const waitUntil = globalThis[REQUEST_CONTEXT]?.get?.()?.waitUntil;
|
|
66
|
+
if (
|
|
67
|
+
waitUntil &&
|
|
68
|
+
!['GET', 'HEAD', 'OPTIONS'].includes(request.method) &&
|
|
69
|
+
request.url?.startsWith('/api/')
|
|
70
|
+
) {
|
|
71
|
+
waitUntil(
|
|
72
|
+
new Promise((resolve) => {
|
|
73
|
+
response.once('close', () => {
|
|
74
|
+
resolve(
|
|
75
|
+
response.statusCode < 400
|
|
76
|
+
? kickWorker(origin, secret)
|
|
77
|
+
: undefined,
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
}),
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
return handler(request, response);
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/* Vercel terminates TLS before the function, and Octane's Node adapter builds
|
|
88
|
+
every request URL as http://<host>. An absolute https target keeps the
|
|
89
|
+
origin that same-origin checks compare against. */
|
|
90
|
+
function withHttpsOrigin(handler) {
|
|
91
|
+
return (request, response) => {
|
|
92
|
+
const host = request.headers?.host;
|
|
93
|
+
if (host && request.url?.startsWith('/')) {
|
|
94
|
+
request.url = `https://${host}${request.url}`;
|
|
95
|
+
}
|
|
96
|
+
return handler(request, response);
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export default withWorkerKick(withHttpsOrigin(nodeHandler));
|
|
@@ -10,5 +10,5 @@ CREATE INDEX IF NOT EXISTS example_notes_tenant_created_idx ON example_notes (te
|
|
|
10
10
|
ALTER TABLE example_notes ENABLE ROW LEVEL SECURITY;
|
|
11
11
|
ALTER TABLE example_notes FORCE ROW LEVEL SECURITY;
|
|
12
12
|
CREATE POLICY example_notes_tenant_policy ON example_notes
|
|
13
|
-
USING (tenant_id = current_setting('
|
|
14
|
-
WITH CHECK (tenant_id = current_setting('
|
|
13
|
+
USING (tenant_id = current_setting('flowdular.tenant_id', true))
|
|
14
|
+
WITH CHECK (tenant_id = current_setting('flowdular.tenant_id', true));
|
|
@@ -14,9 +14,9 @@
|
|
|
14
14
|
"test": "vitest run"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"octane": "0.1
|
|
18
|
-
"segment-state": "0.
|
|
19
|
-
"@flowdular/sdk": "0.
|
|
17
|
+
"octane": "0.9.1",
|
|
18
|
+
"segment-state": "0.4.0",
|
|
19
|
+
"@flowdular/sdk": "0.6.1"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"@tsrx/typescript-plugin": "0.3.120",
|
|
@@ -19,7 +19,7 @@ locales:
|
|
|
19
19
|
- pl
|
|
20
20
|
invariants:
|
|
21
21
|
- Persistence runs on the shared asynchronous database contract. The module receives a leased handle from platform composition, never a driver or a connection string, and provides explicit SQL for every dialect it declares.
|
|
22
|
-
- The PostgreSQL notes table enables and forces row-level security with USING and WITH CHECK policies bound to transaction-local
|
|
22
|
+
- The PostgreSQL notes table enables and forces row-level security with USING and WITH CHECK policies bound to transaction-local flowdular.tenant_id, and the runtime role holds neither SUPERUSER nor BYPASSRLS. Migrations use a separate lease.
|
|
23
23
|
- Every note is owned by exactly one tenant and every query uses the trusted tenant identifier.
|
|
24
24
|
permissions:
|
|
25
25
|
- id: example.notes.read
|
|
@@ -16,8 +16,8 @@ CREATE INDEX IF NOT EXISTS example_notes_tenant_created_idx ON example_notes (te
|
|
|
16
16
|
ALTER TABLE example_notes ENABLE ROW LEVEL SECURITY;
|
|
17
17
|
ALTER TABLE example_notes FORCE ROW LEVEL SECURITY;
|
|
18
18
|
CREATE POLICY example_notes_tenant_policy ON example_notes
|
|
19
|
-
USING (tenant_id = current_setting('
|
|
20
|
-
WITH CHECK (tenant_id = current_setting('
|
|
19
|
+
USING (tenant_id = current_setting('flowdular.tenant_id', true))
|
|
20
|
+
WITH CHECK (tenant_id = current_setting('flowdular.tenant_id', true));
|
|
21
21
|
`;
|
|
22
22
|
|
|
23
23
|
export const databaseMigrations: readonly DatabaseMigration[] = [
|
|
@@ -61,7 +61,7 @@ describe('example.core migrations', () => {
|
|
|
61
61
|
const sql = migration.sql.postgresql ?? '';
|
|
62
62
|
expect(sql).toContain('ENABLE ROW LEVEL SECURITY');
|
|
63
63
|
expect(sql).toContain('FORCE ROW LEVEL SECURITY');
|
|
64
|
-
expect(sql).toContain("current_setting('
|
|
64
|
+
expect(sql).toContain("current_setting('flowdular.tenant_id', true)");
|
|
65
65
|
expect(sql).toContain('WITH CHECK');
|
|
66
66
|
}
|
|
67
67
|
});
|
|
@@ -15,7 +15,6 @@
|
|
|
15
15
|
"format": "prettier --write .",
|
|
16
16
|
"format:check": "prettier --check .",
|
|
17
17
|
"flowdular": "flowdular",
|
|
18
|
-
"cl": "flowdular",
|
|
19
18
|
"doctor": "flowdular doctor",
|
|
20
19
|
"verify": "pnpm rules:check && pnpm typecheck && pnpm test && flowdular spec validate --all && flowdular module validate && pnpm format:check",
|
|
21
20
|
"rules:generate": "rulesync generate",
|
|
@@ -23,10 +22,10 @@
|
|
|
23
22
|
"build": "flowdular module sync --apply && pnpm --filter @app/platform build"
|
|
24
23
|
},
|
|
25
24
|
"devDependencies": {
|
|
26
|
-
"@flowdular/sandbox": "0.
|
|
25
|
+
"@flowdular/sandbox": "0.6.1",
|
|
27
26
|
"@tsrx/prettier-plugin": "0.3.120",
|
|
28
27
|
"prettier": "3.6.2",
|
|
29
|
-
"flowdular": "0.
|
|
28
|
+
"flowdular": "0.6.1",
|
|
30
29
|
"rulesync": "16.21.0"
|
|
31
30
|
}
|
|
32
31
|
}
|