@empire-builder-kit/nx 1.0.0-beta.4
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/LICENSE +201 -0
- package/NOTICE +5 -0
- package/README.md +25 -0
- package/executors.json +54 -0
- package/generators.json +35 -0
- package/migrations/standard-pre-v1-to-v1.md +11 -0
- package/package.json +66 -0
- package/src/blueprints/execution.d.ts +31 -0
- package/src/blueprints/execution.js +166 -0
- package/src/blueprints/execution.js.map +1 -0
- package/src/blueprints/index.d.ts +5 -0
- package/src/blueprints/index.js +9 -0
- package/src/blueprints/index.js.map +1 -0
- package/src/blueprints/inputs.d.ts +43 -0
- package/src/blueprints/inputs.js +103 -0
- package/src/blueprints/inputs.js.map +1 -0
- package/src/blueprints/manifest.d.ts +241 -0
- package/src/blueprints/manifest.js +369 -0
- package/src/blueprints/manifest.js.map +1 -0
- package/src/blueprints/resolver.d.ts +28 -0
- package/src/blueprints/resolver.js +214 -0
- package/src/blueprints/resolver.js.map +1 -0
- package/src/blueprints/transitions.d.ts +20 -0
- package/src/blueprints/transitions.js +61 -0
- package/src/blueprints/transitions.js.map +1 -0
- package/src/executors/deploy/deploy.d.ts +4 -0
- package/src/executors/deploy/deploy.js +6 -0
- package/src/executors/deploy/deploy.js.map +1 -0
- package/src/executors/dev/dev.d.ts +4 -0
- package/src/executors/dev/dev.js +6 -0
- package/src/executors/dev/dev.js.map +1 -0
- package/src/executors/dev-doctor/dev-doctor.d.ts +18 -0
- package/src/executors/dev-doctor/dev-doctor.js +145 -0
- package/src/executors/dev-doctor/dev-doctor.js.map +1 -0
- package/src/executors/dev-doctor/schema.json +17 -0
- package/src/executors/dev-setup/dev-setup.d.ts +11 -0
- package/src/executors/dev-setup/dev-setup.js +454 -0
- package/src/executors/dev-setup/dev-setup.js.map +1 -0
- package/src/executors/dev-setup/schema.json +16 -0
- package/src/executors/fleet/fleet.d.ts +51 -0
- package/src/executors/fleet/fleet.js +529 -0
- package/src/executors/fleet/fleet.js.map +1 -0
- package/src/executors/fleet/schema.json +25 -0
- package/src/executors/foundation-preflight/foundation-preflight.d.ts +15 -0
- package/src/executors/foundation-preflight/foundation-preflight.js +94 -0
- package/src/executors/foundation-preflight/foundation-preflight.js.map +1 -0
- package/src/executors/foundation-preflight/schema.json +31 -0
- package/src/executors/foundation-remove-preflight/foundation-remove-preflight.d.ts +13 -0
- package/src/executors/foundation-remove-preflight/foundation-remove-preflight.js +69 -0
- package/src/executors/foundation-remove-preflight/foundation-remove-preflight.js.map +1 -0
- package/src/executors/foundation-remove-preflight/schema.json +17 -0
- package/src/executors/refresh/refresh.d.ts +4 -0
- package/src/executors/refresh/refresh.js +6 -0
- package/src/executors/refresh/refresh.js.map +1 -0
- package/src/executors/remove/remove.d.ts +4 -0
- package/src/executors/remove/remove.js +6 -0
- package/src/executors/remove/remove.js.map +1 -0
- package/src/executors/sst-lifecycle/run-sst-command.d.ts +28 -0
- package/src/executors/sst-lifecycle/run-sst-command.js +154 -0
- package/src/executors/sst-lifecycle/run-sst-command.js.map +1 -0
- package/src/executors/sst-lifecycle/schema.json +45 -0
- package/src/executors/unlock/unlock.d.ts +4 -0
- package/src/executors/unlock/unlock.js +6 -0
- package/src/executors/unlock/unlock.js.map +1 -0
- package/src/foundation/aurora-logical-database.d.ts +76 -0
- package/src/foundation/aurora-logical-database.js +419 -0
- package/src/foundation/aurora-logical-database.js.map +1 -0
- package/src/foundation/discovery.d.ts +55 -0
- package/src/foundation/discovery.js +55 -0
- package/src/foundation/discovery.js.map +1 -0
- package/src/foundation/index.d.ts +6 -0
- package/src/foundation/index.js +10 -0
- package/src/foundation/index.js.map +1 -0
- package/src/foundation/local-database-contract.d.ts +30 -0
- package/src/foundation/local-database-contract.js +75 -0
- package/src/foundation/local-database-contract.js.map +1 -0
- package/src/foundation/preflight.d.ts +31 -0
- package/src/foundation/preflight.js +114 -0
- package/src/foundation/preflight.js.map +1 -0
- package/src/foundation/provision-database-bindings.d.ts +30 -0
- package/src/foundation/provision-database-bindings.js +113 -0
- package/src/foundation/provision-database-bindings.js.map +1 -0
- package/src/foundation/provision-logical-database.d.ts +26 -0
- package/src/foundation/provision-logical-database.js +162 -0
- package/src/foundation/provision-logical-database.js.map +1 -0
- package/src/foundation/requirements.d.ts +37 -0
- package/src/foundation/requirements.js +141 -0
- package/src/foundation/requirements.js.map +1 -0
- package/src/foundation/stages.d.ts +14 -0
- package/src/foundation/stages.js +51 -0
- package/src/foundation/stages.js.map +1 -0
- package/src/generators/function/function.d.ts +13 -0
- package/src/generators/function/function.js +71 -0
- package/src/generators/function/function.js.map +1 -0
- package/src/generators/function/schema.d.ts +5 -0
- package/src/generators/function/schema.json +20 -0
- package/src/generators/job/job.d.ts +4 -0
- package/src/generators/job/job.js +5 -0
- package/src/generators/job/job.js.map +1 -0
- package/src/generators/preset/generator.d.ts +5 -0
- package/src/generators/preset/generator.js +41 -0
- package/src/generators/preset/generator.js.map +1 -0
- package/src/generators/preset/schema.d.ts +4 -0
- package/src/generators/preset/schema.json +22 -0
- package/src/generators/preset/workspace-files.d.ts +18 -0
- package/src/generators/preset/workspace-files.js +895 -0
- package/src/generators/preset/workspace-files.js.map +1 -0
- package/src/generators/service/service.d.ts +4 -0
- package/src/generators/service/service.js +5 -0
- package/src/generators/service/service.js.map +1 -0
- package/src/generators/slice/schema.d.ts +7 -0
- package/src/generators/slice/schema.json +38 -0
- package/src/generators/slice/slice.d.ts +26 -0
- package/src/generators/slice/slice.js +1676 -0
- package/src/generators/slice/slice.js.map +1 -0
- package/src/generators/upgrade/schema.d.ts +8 -0
- package/src/generators/upgrade/schema.json +31 -0
- package/src/generators/upgrade/upgrade.d.ts +27 -0
- package/src/generators/upgrade/upgrade.js +666 -0
- package/src/generators/upgrade/upgrade.js.map +1 -0
- package/src/generators/workload/schema.d.ts +6 -0
- package/src/generators/workload/schema.json +21 -0
- package/src/generators/workload/workload.d.ts +16 -0
- package/src/generators/workload/workload.js +157 -0
- package/src/generators/workload/workload.js.map +1 -0
- package/src/index.d.ts +5 -0
- package/src/index.js +9 -0
- package/src/index.js.map +1 -0
- package/src/local-dev/contracts.d.ts +31 -0
- package/src/local-dev/contracts.js +126 -0
- package/src/local-dev/contracts.js.map +1 -0
- package/src/local-dev/gateway-daemon.d.ts +4 -0
- package/src/local-dev/gateway-daemon.js +39 -0
- package/src/local-dev/gateway-daemon.js.map +1 -0
- package/src/local-dev/gateway.d.ts +60 -0
- package/src/local-dev/gateway.js +368 -0
- package/src/local-dev/gateway.js.map +1 -0
- package/src/local-dev/identity.d.ts +16 -0
- package/src/local-dev/identity.js +101 -0
- package/src/local-dev/identity.js.map +1 -0
- package/src/local-dev/index.d.ts +7 -0
- package/src/local-dev/index.js +11 -0
- package/src/local-dev/index.js.map +1 -0
- package/src/local-dev/portless.d.ts +15 -0
- package/src/local-dev/portless.js +115 -0
- package/src/local-dev/portless.js.map +1 -0
- package/src/local-dev/ports.d.ts +10 -0
- package/src/local-dev/ports.js +39 -0
- package/src/local-dev/ports.js.map +1 -0
- package/src/local-dev/proxy.d.ts +37 -0
- package/src/local-dev/proxy.js +271 -0
- package/src/local-dev/proxy.js.map +1 -0
- package/src/local-dev/tls.d.ts +24 -0
- package/src/local-dev/tls.js +294 -0
- package/src/local-dev/tls.js.map +1 -0
- package/src/ownership/composition.d.ts +16 -0
- package/src/ownership/composition.js +125 -0
- package/src/ownership/composition.js.map +1 -0
- package/src/ownership/foundation-record.d.ts +66 -0
- package/src/ownership/foundation-record.js +108 -0
- package/src/ownership/foundation-record.js.map +1 -0
- package/src/ownership/hash.d.ts +2 -0
- package/src/ownership/hash.js +10 -0
- package/src/ownership/hash.js.map +1 -0
- package/src/ownership/index.d.ts +6 -0
- package/src/ownership/index.js +10 -0
- package/src/ownership/index.js.map +1 -0
- package/src/ownership/json-ownership.d.ts +13 -0
- package/src/ownership/json-ownership.js +147 -0
- package/src/ownership/json-ownership.js.map +1 -0
- package/src/ownership/record.d.ts +186 -0
- package/src/ownership/record.js +336 -0
- package/src/ownership/record.js.map +1 -0
- package/src/ownership/workspace-record.d.ts +66 -0
- package/src/ownership/workspace-record.js +108 -0
- package/src/ownership/workspace-record.js.map +1 -0
- package/src/schemas/blueprint-input-schema-v1.json +23 -0
- package/src/schemas/blueprint-v1.json +170 -0
- package/src/schemas/foundation-ownership-v2.json +42 -0
- package/src/schemas/foundation-requirement-v1.json +32 -0
- package/src/schemas/generated-ownership-v2.json +117 -0
- package/src/schemas/workspace-ownership-v2.json +42 -0
- package/src/validation.d.ts +14 -0
- package/src/validation.js +14 -0
- package/src/validation.js.map +1 -0
|
@@ -0,0 +1,1676 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.STANDARD_LOCAL_DEV_ADAPTER = exports.PRODUCT_PACKAGE_MANAGED_JSON_POINTERS = void 0;
|
|
4
|
+
exports.createBuiltInSlicePlan = createBuiltInSlicePlan;
|
|
5
|
+
exports.sliceGenerator = sliceGenerator;
|
|
6
|
+
const devkit_1 = require("@nx/devkit");
|
|
7
|
+
const node_path_1 = require("node:path");
|
|
8
|
+
const index_js_1 = require("../../blueprints/index.js");
|
|
9
|
+
const index_js_2 = require("../../foundation/index.js");
|
|
10
|
+
const index_js_3 = require("../../ownership/index.js");
|
|
11
|
+
const PRODUCT_VERSIONS = {
|
|
12
|
+
awsCognito: '^3.700.0',
|
|
13
|
+
awsEventBridge: '^3.700.0',
|
|
14
|
+
awsRdsData: '^3.700.0',
|
|
15
|
+
awsSecretsManager: '^3.700.0',
|
|
16
|
+
awsSsm: '^3.700.0',
|
|
17
|
+
drizzle: '^0.45.1',
|
|
18
|
+
drizzleKit: '^0.31.4',
|
|
19
|
+
next: '16.2.3',
|
|
20
|
+
pg: '^8.22.0',
|
|
21
|
+
playwright: '^1.59.1',
|
|
22
|
+
react: '19.2.4',
|
|
23
|
+
reactDom: '19.2.4',
|
|
24
|
+
typesNode: '25.5.0',
|
|
25
|
+
typesReact: '19.2.14',
|
|
26
|
+
typesReactDom: '19.2.3',
|
|
27
|
+
vitest: '4.1.4',
|
|
28
|
+
};
|
|
29
|
+
exports.PRODUCT_PACKAGE_MANAGED_JSON_POINTERS = [
|
|
30
|
+
'/name',
|
|
31
|
+
'/version',
|
|
32
|
+
'/private',
|
|
33
|
+
'/dependencies/@aws-sdk~1client-cognito-identity-provider',
|
|
34
|
+
'/dependencies/@aws-sdk~1client-eventbridge',
|
|
35
|
+
'/dependencies/@aws-sdk~1client-rds-data',
|
|
36
|
+
'/dependencies/@aws-sdk~1client-secrets-manager',
|
|
37
|
+
'/dependencies/@aws-sdk~1client-ssm',
|
|
38
|
+
'/dependencies/@empire-builder-kit~1runtime',
|
|
39
|
+
'/dependencies/drizzle-orm',
|
|
40
|
+
'/dependencies/next',
|
|
41
|
+
'/dependencies/react',
|
|
42
|
+
'/dependencies/react-dom',
|
|
43
|
+
'/devDependencies/@playwright~1test',
|
|
44
|
+
'/devDependencies/@types~1node',
|
|
45
|
+
'/devDependencies/@types~1react',
|
|
46
|
+
'/devDependencies/@types~1react-dom',
|
|
47
|
+
'/devDependencies/drizzle-kit',
|
|
48
|
+
'/devDependencies/pg',
|
|
49
|
+
'/devDependencies/vitest',
|
|
50
|
+
'/exports/.~1contracts',
|
|
51
|
+
];
|
|
52
|
+
const STANDARD_LOCAL_DEV_ADAPTER = (name) => ({
|
|
53
|
+
adapterVersion: 1,
|
|
54
|
+
auth: {
|
|
55
|
+
callbackPaths: [`/${name}/api/auth/callback`],
|
|
56
|
+
logoutPaths: [`/${name}/api/auth/sign-out`],
|
|
57
|
+
},
|
|
58
|
+
databaseMode: 'local',
|
|
59
|
+
healthPath: `/${name}/api/health`,
|
|
60
|
+
kind: 'web',
|
|
61
|
+
portEnvironment: 'EBK_DEV_PORT',
|
|
62
|
+
routePrefix: `/${name}`,
|
|
63
|
+
target: 'dev',
|
|
64
|
+
websocket: true,
|
|
65
|
+
});
|
|
66
|
+
exports.STANDARD_LOCAL_DEV_ADAPTER = STANDARD_LOCAL_DEV_ADAPTER;
|
|
67
|
+
function readJson(tree, path, label) {
|
|
68
|
+
const contents = tree.read(path, 'utf8');
|
|
69
|
+
if (contents === null) {
|
|
70
|
+
throw new Error(`${label} does not exist at ${path}.`);
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
return JSON.parse(contents);
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
throw new Error(`${label} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function requireSliceName(name) {
|
|
80
|
+
const value = name.trim();
|
|
81
|
+
if (value.length > 63 || !/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(value)) {
|
|
82
|
+
throw new Error('Slice name must start with a lowercase letter, contain at most 63 characters, and use only lowercase letters, numbers, and hyphens.');
|
|
83
|
+
}
|
|
84
|
+
return value;
|
|
85
|
+
}
|
|
86
|
+
function workspaceScope(packageJson) {
|
|
87
|
+
const name = packageJson.name?.trim();
|
|
88
|
+
if (!name || !/^[a-z0-9][a-z0-9-]*$/.test(name)) {
|
|
89
|
+
throw new Error('Workspace package.json must have an unscoped lowercase name before generating a slice.');
|
|
90
|
+
}
|
|
91
|
+
return `@${name}`;
|
|
92
|
+
}
|
|
93
|
+
function json(value) {
|
|
94
|
+
return `${JSON.stringify(value, null, 2)}\n`;
|
|
95
|
+
}
|
|
96
|
+
function parseBlueprintInputs(inputs) {
|
|
97
|
+
if (inputs === undefined) {
|
|
98
|
+
return {};
|
|
99
|
+
}
|
|
100
|
+
if (typeof inputs !== 'string') {
|
|
101
|
+
return inputs;
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
const parsed = JSON.parse(inputs);
|
|
105
|
+
if (typeof parsed !== 'object' ||
|
|
106
|
+
parsed === null ||
|
|
107
|
+
Array.isArray(parsed)) {
|
|
108
|
+
throw new Error('value must be a JSON object');
|
|
109
|
+
}
|
|
110
|
+
return parsed;
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
throw new Error(`Blueprint --inputs must be valid JSON object syntax: ${error instanceof Error ? error.message : String(error)}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function renderStandardProductFiles(name, pascalName) {
|
|
117
|
+
return {
|
|
118
|
+
'database/bindings.json': json({
|
|
119
|
+
primary: {
|
|
120
|
+
active: 'primary',
|
|
121
|
+
provisioned: ['primary'],
|
|
122
|
+
},
|
|
123
|
+
}),
|
|
124
|
+
'contracts/events.ts': `export interface ${pascalName}CreatedData {
|
|
125
|
+
id: string;
|
|
126
|
+
occurredBy: string;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export const ${pascalName}Created = {
|
|
130
|
+
detailType: '${pascalName}Created',
|
|
131
|
+
schemaVersion: 1,
|
|
132
|
+
source: '${name}',
|
|
133
|
+
} as const;
|
|
134
|
+
`,
|
|
135
|
+
'database/schema.ts': `import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core';
|
|
136
|
+
|
|
137
|
+
export const records = pgTable('records', {
|
|
138
|
+
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
|
139
|
+
id: uuid('id').defaultRandom().primaryKey(),
|
|
140
|
+
name: text('name').notNull(),
|
|
141
|
+
});
|
|
142
|
+
`,
|
|
143
|
+
'database/migrations/0000_initial.sql': `CREATE TABLE IF NOT EXISTS "records" (
|
|
144
|
+
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
145
|
+
"name" text NOT NULL,
|
|
146
|
+
"created_at" timestamptz NOT NULL DEFAULT now()
|
|
147
|
+
);
|
|
148
|
+
`,
|
|
149
|
+
'database/migrations/meta/_journal.json': json({
|
|
150
|
+
dialect: 'postgresql',
|
|
151
|
+
entries: [
|
|
152
|
+
{
|
|
153
|
+
idx: 0,
|
|
154
|
+
tag: '0000_initial',
|
|
155
|
+
version: '7',
|
|
156
|
+
when: 0,
|
|
157
|
+
breakpoints: true,
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
version: '7',
|
|
161
|
+
}),
|
|
162
|
+
'database/migrations/meta/0000_snapshot.json': json({
|
|
163
|
+
id: 'fdcf5ce3-ada5-44bf-bd9e-e01f2abe3bc4',
|
|
164
|
+
prevId: '00000000-0000-0000-0000-000000000000',
|
|
165
|
+
version: '7',
|
|
166
|
+
dialect: 'postgresql',
|
|
167
|
+
tables: {
|
|
168
|
+
'public.records': {
|
|
169
|
+
name: 'records',
|
|
170
|
+
schema: '',
|
|
171
|
+
columns: {
|
|
172
|
+
created_at: {
|
|
173
|
+
name: 'created_at',
|
|
174
|
+
type: 'timestamp with time zone',
|
|
175
|
+
primaryKey: false,
|
|
176
|
+
notNull: true,
|
|
177
|
+
default: 'now()',
|
|
178
|
+
},
|
|
179
|
+
id: {
|
|
180
|
+
name: 'id',
|
|
181
|
+
type: 'uuid',
|
|
182
|
+
primaryKey: true,
|
|
183
|
+
notNull: true,
|
|
184
|
+
default: 'gen_random_uuid()',
|
|
185
|
+
},
|
|
186
|
+
name: {
|
|
187
|
+
name: 'name',
|
|
188
|
+
type: 'text',
|
|
189
|
+
primaryKey: false,
|
|
190
|
+
notNull: true,
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
indexes: {},
|
|
194
|
+
foreignKeys: {},
|
|
195
|
+
compositePrimaryKeys: {},
|
|
196
|
+
uniqueConstraints: {},
|
|
197
|
+
policies: {},
|
|
198
|
+
checkConstraints: {},
|
|
199
|
+
isRLSEnabled: false,
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
enums: {},
|
|
203
|
+
schemas: {},
|
|
204
|
+
sequences: {},
|
|
205
|
+
roles: {},
|
|
206
|
+
policies: {},
|
|
207
|
+
views: {},
|
|
208
|
+
_meta: { columns: {}, schemas: {}, tables: {} },
|
|
209
|
+
}),
|
|
210
|
+
'database/drizzle.config.ts': `import { createDatabaseBindingPlan, resolveLocalDatabaseContract } from '@empire-builder-kit/nx/foundation';
|
|
211
|
+
import { defineConfig } from 'drizzle-kit';
|
|
212
|
+
import databaseBindings from './bindings.json';
|
|
213
|
+
|
|
214
|
+
const plan = createDatabaseBindingPlan(databaseBindings, {
|
|
215
|
+
slice: '${name}',
|
|
216
|
+
stage: 'development',
|
|
217
|
+
});
|
|
218
|
+
const activeDatabase = plan.active.primary;
|
|
219
|
+
if (!activeDatabase) throw new Error('Database binding "primary" is required.');
|
|
220
|
+
const localDatabase = resolveLocalDatabaseContract({
|
|
221
|
+
database: activeDatabase,
|
|
222
|
+
slice: '${name}',
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
export default defineConfig({
|
|
226
|
+
dbCredentials: {
|
|
227
|
+
url: process.env.DATABASE_URL ?? \`postgresql://\${localDatabase.username}:\${localDatabase.password}@\${localDatabase.host}:\${localDatabase.port}/\${localDatabase.database}\`,
|
|
228
|
+
},
|
|
229
|
+
dialect: 'postgresql',
|
|
230
|
+
out: './database/migrations',
|
|
231
|
+
schema: './database/schema.ts',
|
|
232
|
+
});
|
|
233
|
+
`,
|
|
234
|
+
'database/docker-compose.yml': `services:
|
|
235
|
+
database:
|
|
236
|
+
image: postgres:17-alpine
|
|
237
|
+
environment:
|
|
238
|
+
POSTGRES_DB: \${EBK_LOCAL_DATABASE}
|
|
239
|
+
POSTGRES_PASSWORD: \${EBK_LOCAL_DATABASE_PASSWORD}
|
|
240
|
+
POSTGRES_USER: \${EBK_LOCAL_DATABASE_USERNAME}
|
|
241
|
+
healthcheck:
|
|
242
|
+
test: [CMD-SHELL, "pg_isready -U \${EBK_LOCAL_DATABASE_USERNAME} -d \${EBK_LOCAL_DATABASE}"]
|
|
243
|
+
interval: 2s
|
|
244
|
+
timeout: 3s
|
|
245
|
+
retries: 15
|
|
246
|
+
ports:
|
|
247
|
+
- '\${EBK_LOCAL_DATABASE_PORT}:5432'
|
|
248
|
+
volumes:
|
|
249
|
+
- database-data:/var/lib/postgresql/data
|
|
250
|
+
|
|
251
|
+
volumes:
|
|
252
|
+
database-data:
|
|
253
|
+
`,
|
|
254
|
+
'api/health.ts': `import { createHttpJsonHandler } from '@empire-builder-kit/runtime/lambda';
|
|
255
|
+
|
|
256
|
+
export const handler = createHttpJsonHandler({
|
|
257
|
+
app: '${name}',
|
|
258
|
+
operation: 'health',
|
|
259
|
+
handle: () => ({ service: '${name}', status: 'ok' }),
|
|
260
|
+
});
|
|
261
|
+
`,
|
|
262
|
+
'web/src/app/api/health/route.ts': `export function GET() {
|
|
263
|
+
return Response.json({ service: '${name}-web', status: 'ok' });
|
|
264
|
+
}
|
|
265
|
+
`,
|
|
266
|
+
'api/protected.ts': `import { createBetterAuthConfig } from '@empire-builder-kit/runtime/auth';
|
|
267
|
+
import { createAuthGuard } from '@empire-builder-kit/runtime/auth-backend';
|
|
268
|
+
import { createHttpJsonHandler } from '@empire-builder-kit/runtime/lambda';
|
|
269
|
+
|
|
270
|
+
let guard: ReturnType<typeof createAuthGuard> | undefined;
|
|
271
|
+
function getGuard() {
|
|
272
|
+
return guard ??= createAuthGuard({
|
|
273
|
+
config: createBetterAuthConfig({
|
|
274
|
+
clientId: process.env.AUTH_CLIENT_ID ?? '',
|
|
275
|
+
region: process.env.AWS_REGION ?? '',
|
|
276
|
+
userPoolId: process.env.AUTH_USER_POOL_ID ?? '',
|
|
277
|
+
}),
|
|
278
|
+
policy: { defaultKind: 'customer' },
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export const handler = createHttpJsonHandler({
|
|
283
|
+
app: '${name}',
|
|
284
|
+
operation: 'protected-example',
|
|
285
|
+
async handle({ event }) {
|
|
286
|
+
const principal = await getGuard().authorize(event);
|
|
287
|
+
return { message: 'Authenticated request accepted.', principal };
|
|
288
|
+
},
|
|
289
|
+
});
|
|
290
|
+
`,
|
|
291
|
+
'api/event-consumer.ts': `import { parseSqsEventBridgeRecords } from '@empire-builder-kit/runtime/events';
|
|
292
|
+
import type { ${pascalName}CreatedData } from '../contracts/events.js';
|
|
293
|
+
|
|
294
|
+
interface QueueRecord { body: string; messageId: string }
|
|
295
|
+
interface QueueEvent { Records: QueueRecord[] }
|
|
296
|
+
|
|
297
|
+
export async function handler(event: QueueEvent) {
|
|
298
|
+
const batch = parseSqsEventBridgeRecords<${pascalName}CreatedData>(event.Records);
|
|
299
|
+
for (const { detailType, envelope } of batch.events) {
|
|
300
|
+
console.info(JSON.stringify({
|
|
301
|
+
eventId: envelope.meta.eventId,
|
|
302
|
+
eventType: detailType,
|
|
303
|
+
message: '${name} event consumed',
|
|
304
|
+
}));
|
|
305
|
+
}
|
|
306
|
+
return {
|
|
307
|
+
batchItemFailures: batch.failures.map(({ messageId }) => ({
|
|
308
|
+
itemIdentifier: messageId ?? '',
|
|
309
|
+
})),
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
`,
|
|
313
|
+
'api/publish-created.ts': `import { EventBridgeClient, PutEventsCommand } from '@aws-sdk/client-eventbridge';
|
|
314
|
+
import { createBetterAuthConfig } from '@empire-builder-kit/runtime/auth';
|
|
315
|
+
import { createAuthGuard } from '@empire-builder-kit/runtime/auth-backend';
|
|
316
|
+
import { createEventPublisher } from '@empire-builder-kit/runtime/events';
|
|
317
|
+
import { createHttpJsonHandler } from '@empire-builder-kit/runtime/lambda';
|
|
318
|
+
import { ${pascalName}Created } from '../contracts/events.js';
|
|
319
|
+
|
|
320
|
+
const client = new EventBridgeClient({});
|
|
321
|
+
let guard: ReturnType<typeof createAuthGuard> | undefined;
|
|
322
|
+
let publish: ReturnType<typeof createEventPublisher> | undefined;
|
|
323
|
+
function getGuard() {
|
|
324
|
+
return guard ??= createAuthGuard({
|
|
325
|
+
config: createBetterAuthConfig({
|
|
326
|
+
clientId: process.env.AUTH_CLIENT_ID ?? '',
|
|
327
|
+
region: process.env.AWS_REGION ?? '',
|
|
328
|
+
userPoolId: process.env.AUTH_USER_POOL_ID ?? '',
|
|
329
|
+
}),
|
|
330
|
+
policy: { defaultKind: 'customer' },
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
function getPublisher() {
|
|
334
|
+
return publish ??= createEventPublisher({
|
|
335
|
+
busName: process.env.EVENT_BUS_NAME ?? '',
|
|
336
|
+
stage: process.env.EBK_STAGE ?? '',
|
|
337
|
+
send: (entry) => client.send(new PutEventsCommand({ Entries: [{
|
|
338
|
+
Detail: entry.detail,
|
|
339
|
+
DetailType: entry.detailType,
|
|
340
|
+
EventBusName: entry.eventBusName,
|
|
341
|
+
Source: entry.source,
|
|
342
|
+
}] })),
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export const handler = createHttpJsonHandler({
|
|
347
|
+
app: '${name}',
|
|
348
|
+
operation: 'publish-created',
|
|
349
|
+
async handle({ context, event }) {
|
|
350
|
+
const principal = await getGuard().authorize(event);
|
|
351
|
+
const publishedEvent = await getPublisher()(${pascalName}Created, {
|
|
352
|
+
id: crypto.randomUUID(),
|
|
353
|
+
occurredBy: principal.id,
|
|
354
|
+
}, { context });
|
|
355
|
+
return { eventId: publishedEvent.meta.eventId };
|
|
356
|
+
},
|
|
357
|
+
});
|
|
358
|
+
`,
|
|
359
|
+
'infra/product.ts': `import { provisionDatabaseBindings } from '@empire-builder-kit/nx/foundation';
|
|
360
|
+
import databaseBindings from '../database/bindings.json';
|
|
361
|
+
import { getFoundationInfrastructure } from './foundation.js';
|
|
362
|
+
// <ebk:composition-imports:start>
|
|
363
|
+
// <ebk:composition-imports:end>
|
|
364
|
+
|
|
365
|
+
export function product() {
|
|
366
|
+
const devPortInput = $dev ? (process.env.EBK_DEV_PORT ?? '3000') : '3000';
|
|
367
|
+
if (!/^\\d+$/.test(devPortInput)) {
|
|
368
|
+
throw new Error('EBK_DEV_PORT must be an integer from 1024 through 65535.');
|
|
369
|
+
}
|
|
370
|
+
const devPort = Number(devPortInput);
|
|
371
|
+
if (!Number.isSafeInteger(devPort) || devPort < 1024 || devPort > 65535) {
|
|
372
|
+
throw new Error('EBK_DEV_PORT must be an integer from 1024 through 65535.');
|
|
373
|
+
}
|
|
374
|
+
const foundation = getFoundationInfrastructure();
|
|
375
|
+
const { db: database } = provisionDatabaseBindings(foundation, {
|
|
376
|
+
bindings: databaseBindings,
|
|
377
|
+
slice: '${name}',
|
|
378
|
+
}).primary;
|
|
379
|
+
// <ebk:workload-registrations:start>
|
|
380
|
+
// <ebk:workload-registrations:end>
|
|
381
|
+
|
|
382
|
+
const authClient = foundation.userPool.addClient('${pascalName}WebAuthClient', {
|
|
383
|
+
transform: {
|
|
384
|
+
client: {
|
|
385
|
+
explicitAuthFlows: [
|
|
386
|
+
'ALLOW_USER_PASSWORD_AUTH',
|
|
387
|
+
'ALLOW_REFRESH_TOKEN_AUTH',
|
|
388
|
+
],
|
|
389
|
+
preventUserExistenceErrors: 'ENABLED',
|
|
390
|
+
},
|
|
391
|
+
},
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
const api = new sst.aws.ApiGatewayV2('${pascalName}Api');
|
|
395
|
+
api.route('GET /health', {
|
|
396
|
+
environment: { EBK_STAGE: $app.stage },
|
|
397
|
+
handler: 'api/health.handler',
|
|
398
|
+
logging: { format: 'json', retention: '1 month' },
|
|
399
|
+
});
|
|
400
|
+
api.route('GET /protected', {
|
|
401
|
+
environment: {
|
|
402
|
+
AUTH_CLIENT_ID: authClient.id,
|
|
403
|
+
AUTH_USER_POOL_ID: foundation.userPool.id,
|
|
404
|
+
EBK_STAGE: $app.stage,
|
|
405
|
+
},
|
|
406
|
+
handler: 'api/protected.handler',
|
|
407
|
+
link: [database],
|
|
408
|
+
logging: { format: 'json', retention: '1 month' },
|
|
409
|
+
});
|
|
410
|
+
api.route('POST /events/example', {
|
|
411
|
+
environment: {
|
|
412
|
+
AUTH_CLIENT_ID: authClient.id,
|
|
413
|
+
AUTH_USER_POOL_ID: foundation.userPool.id,
|
|
414
|
+
EBK_STAGE: $app.stage,
|
|
415
|
+
EVENT_BUS_NAME: foundation.discovery.eventBusName,
|
|
416
|
+
},
|
|
417
|
+
handler: 'api/publish-created.handler',
|
|
418
|
+
link: [foundation.eventBus],
|
|
419
|
+
logging: { format: 'json', retention: '1 month' },
|
|
420
|
+
});
|
|
421
|
+
// <ebk:function-registrations:start>
|
|
422
|
+
// <ebk:function-registrations:end>
|
|
423
|
+
|
|
424
|
+
const eventDlq = new sst.aws.Queue('${pascalName}EventDlq');
|
|
425
|
+
const eventQueue = new sst.aws.Queue('${pascalName}EventQueue', {
|
|
426
|
+
dlq: { queue: eventDlq.arn, retry: 5 },
|
|
427
|
+
});
|
|
428
|
+
eventQueue.subscribe({
|
|
429
|
+
environment: { EBK_STAGE: $app.stage },
|
|
430
|
+
handler: 'api/event-consumer.handler',
|
|
431
|
+
logging: { format: 'json', retention: '1 month' },
|
|
432
|
+
}, {
|
|
433
|
+
batch: { partialResponses: true },
|
|
434
|
+
});
|
|
435
|
+
foundation.eventBus.subscribeQueue(
|
|
436
|
+
'${pascalName}CreatedEventRule',
|
|
437
|
+
eventQueue,
|
|
438
|
+
{
|
|
439
|
+
pattern: {
|
|
440
|
+
detail: { meta: { stage: [$app.stage] } },
|
|
441
|
+
detailType: ['${pascalName}Created'],
|
|
442
|
+
source: ['${name}'],
|
|
443
|
+
},
|
|
444
|
+
},
|
|
445
|
+
);
|
|
446
|
+
|
|
447
|
+
const web = new sst.aws.Nextjs('${pascalName}Web', {
|
|
448
|
+
dev: {
|
|
449
|
+
command: \`pnpm exec next dev web --hostname 127.0.0.1 --port \${devPort}\`,
|
|
450
|
+
directory: '.',
|
|
451
|
+
title: '${pascalName} web',
|
|
452
|
+
url: \`http://127.0.0.1:\${devPort}/${name}\`,
|
|
453
|
+
},
|
|
454
|
+
environment: {
|
|
455
|
+
AUTH_CLIENT_ID: authClient.id,
|
|
456
|
+
AUTH_USER_POOL_ID: foundation.userPool.id,
|
|
457
|
+
EBK_STAGE: $app.stage,
|
|
458
|
+
EBK_FLEET_ORIGIN: process.env.EBK_FLEET_ORIGIN ?? '',
|
|
459
|
+
EBK_SECURE_COOKIES: process.env.EBK_SECURE_COOKIES ?? '',
|
|
460
|
+
NEXT_PUBLIC_API_URL: api.url,
|
|
461
|
+
},
|
|
462
|
+
link: [database, foundation.userPool],
|
|
463
|
+
path: 'web',
|
|
464
|
+
router: { instance: foundation.router, path: '/${name}' },
|
|
465
|
+
transform: {
|
|
466
|
+
server: {
|
|
467
|
+
logging: { format: 'json', retention: '1 month' },
|
|
468
|
+
},
|
|
469
|
+
},
|
|
470
|
+
vpc: foundation.vpc,
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
const apiAlarm = new aws.cloudwatch.MetricAlarm('${pascalName}ApiErrors', {
|
|
474
|
+
alarmActions: [foundation.discovery.alertTopicArn],
|
|
475
|
+
comparisonOperator: 'GreaterThanOrEqualToThreshold',
|
|
476
|
+
dimensions: { ApiId: api.id },
|
|
477
|
+
evaluationPeriods: 1,
|
|
478
|
+
metricName: '5xx',
|
|
479
|
+
namespace: 'AWS/ApiGateway',
|
|
480
|
+
period: 300,
|
|
481
|
+
statistic: 'Sum',
|
|
482
|
+
threshold: 1,
|
|
483
|
+
treatMissingData: 'notBreaching',
|
|
484
|
+
});
|
|
485
|
+
const dlqAlarm = new aws.cloudwatch.MetricAlarm('${pascalName}EventDlqDepth', {
|
|
486
|
+
alarmActions: [foundation.discovery.alertTopicArn],
|
|
487
|
+
comparisonOperator: 'GreaterThanOrEqualToThreshold',
|
|
488
|
+
dimensions: { QueueName: eventDlq.name },
|
|
489
|
+
evaluationPeriods: 1,
|
|
490
|
+
metricName: 'ApproximateNumberOfMessagesVisible',
|
|
491
|
+
namespace: 'AWS/SQS',
|
|
492
|
+
period: 300,
|
|
493
|
+
statistic: 'Maximum',
|
|
494
|
+
threshold: 1,
|
|
495
|
+
treatMissingData: 'notBreaching',
|
|
496
|
+
});
|
|
497
|
+
const dashboard = new aws.cloudwatch.Dashboard('${pascalName}Dashboard', {
|
|
498
|
+
dashboardName: '${name}-' + $app.stage,
|
|
499
|
+
dashboardBody: JSON.stringify({
|
|
500
|
+
widgets: [{
|
|
501
|
+
type: 'text', x: 0, y: 0, width: 24, height: 3,
|
|
502
|
+
properties: { markdown: '# ${pascalName}\\nOwner: product' },
|
|
503
|
+
}],
|
|
504
|
+
}),
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
return {
|
|
508
|
+
api,
|
|
509
|
+
authClient,
|
|
510
|
+
dashboard,
|
|
511
|
+
database,
|
|
512
|
+
dlqAlarm,
|
|
513
|
+
eventDlq,
|
|
514
|
+
eventQueue,
|
|
515
|
+
apiAlarm,
|
|
516
|
+
web,
|
|
517
|
+
// <ebk:composition-outputs:start>
|
|
518
|
+
// <ebk:composition-outputs:end>
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
`,
|
|
522
|
+
'web/next-env.d.ts': `/// <reference types="next" />
|
|
523
|
+
/// <reference types="next/image-types/global" />
|
|
524
|
+
`,
|
|
525
|
+
'web/tsconfig.json': json({
|
|
526
|
+
extends: '../tsconfig.json',
|
|
527
|
+
compilerOptions: {
|
|
528
|
+
incremental: true,
|
|
529
|
+
plugins: [{ name: 'next' }],
|
|
530
|
+
},
|
|
531
|
+
include: ['next-env.d.ts', '.next/types/**/*.ts', '**/*.ts', '**/*.tsx'],
|
|
532
|
+
exclude: ['node_modules'],
|
|
533
|
+
}),
|
|
534
|
+
'web/next.config.ts': `import type { NextConfig } from 'next';
|
|
535
|
+
|
|
536
|
+
const fleetOrigin = process.env.EBK_FLEET_ORIGIN?.trim();
|
|
537
|
+
|
|
538
|
+
const config: NextConfig = {
|
|
539
|
+
...(fleetOrigin ? { allowedDevOrigins: [new URL(fleetOrigin).hostname] } : {}),
|
|
540
|
+
basePath: '/${name}',
|
|
541
|
+
reactStrictMode: true,
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
export default config;
|
|
545
|
+
`,
|
|
546
|
+
'web/open-next.config.ts': `const config = {
|
|
547
|
+
// OpenNext runs from this web directory. Point it at the slice package so it
|
|
548
|
+
// builds only this application instead of recursively invoking the Nx root.
|
|
549
|
+
buildCommand: 'pnpm exec next build web',
|
|
550
|
+
default: {},
|
|
551
|
+
packageJsonPath: '../package.json',
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
export default config;
|
|
555
|
+
`,
|
|
556
|
+
'web/src/app/globals.css': `:root { color-scheme: light dark; font-family: system-ui, sans-serif; }
|
|
557
|
+
body { margin: 0; background: #07111f; color: #edf6ff; }
|
|
558
|
+
main { max-width: 64rem; margin: 0 auto; padding: 5rem 1.5rem; }
|
|
559
|
+
.card { background: #102238; border: 1px solid #29425e; border-radius: 1rem; padding: 1.5rem; }
|
|
560
|
+
a { color: #78d5ff; }
|
|
561
|
+
input, button { box-sizing: border-box; display: block; margin-top: .75rem; padding: .75rem; width: 100%; }
|
|
562
|
+
button { background: #48b7de; border: 0; color: #04111b; cursor: pointer; font-weight: 700; }
|
|
563
|
+
`,
|
|
564
|
+
'web/src/app/layout.tsx': `import type { Metadata } from 'next';
|
|
565
|
+
import type { ReactNode } from 'react';
|
|
566
|
+
import './globals.css';
|
|
567
|
+
|
|
568
|
+
export const metadata: Metadata = { title: '${pascalName}' };
|
|
569
|
+
|
|
570
|
+
export default function Layout({ children }: Readonly<{ children: ReactNode }>) {
|
|
571
|
+
return <html lang="en"><body>{children}</body></html>;
|
|
572
|
+
}
|
|
573
|
+
`,
|
|
574
|
+
'web/src/app/page.tsx': `export default function Home() {
|
|
575
|
+
return <main>
|
|
576
|
+
<p>Empire Builder Kit Product Slice</p>
|
|
577
|
+
<h1>${pascalName}</h1>
|
|
578
|
+
<div className="card">
|
|
579
|
+
<p>The web app, Lambda API, logical Postgres database, Cognito client,
|
|
580
|
+
event buffer/DLQ, dashboard, and alarms are wired and owned here.</p>
|
|
581
|
+
<a href="/${name}/sign-in">Sign in</a>
|
|
582
|
+
</div>
|
|
583
|
+
</main>;
|
|
584
|
+
}
|
|
585
|
+
`,
|
|
586
|
+
'web/src/app/sign-in/page.tsx': `'use client';
|
|
587
|
+
|
|
588
|
+
import { useState, type FormEvent } from 'react';
|
|
589
|
+
|
|
590
|
+
export default function SignIn() {
|
|
591
|
+
const [message, setMessage] = useState('');
|
|
592
|
+
async function submit(event: FormEvent<HTMLFormElement>) {
|
|
593
|
+
event.preventDefault();
|
|
594
|
+
const data = new FormData(event.currentTarget);
|
|
595
|
+
const response = await fetch('/${name}/api/auth/sign-in', {
|
|
596
|
+
body: JSON.stringify({ email: data.get('email'), password: data.get('password') }),
|
|
597
|
+
headers: { 'content-type': 'application/json' },
|
|
598
|
+
method: 'POST',
|
|
599
|
+
});
|
|
600
|
+
setMessage(response.ok ? 'Signed in.' : 'Sign-in failed.');
|
|
601
|
+
}
|
|
602
|
+
return <main><div className="card"><h1>Sign in</h1>
|
|
603
|
+
<form onSubmit={submit}>
|
|
604
|
+
<label>Email<input name="email" type="email" autoComplete="username" required /></label>
|
|
605
|
+
<label>Password<input name="password" type="password" autoComplete="current-password" required /></label>
|
|
606
|
+
<button type="submit">Sign in</button>
|
|
607
|
+
</form><p role="status">{message}</p>
|
|
608
|
+
<a href="/${name}/password-reset">Reset password</a>
|
|
609
|
+
</div></main>;
|
|
610
|
+
}
|
|
611
|
+
`,
|
|
612
|
+
'web/src/app/password-reset/page.tsx': `'use client';
|
|
613
|
+
|
|
614
|
+
import { useState, type FormEvent } from 'react';
|
|
615
|
+
|
|
616
|
+
export default function PasswordReset() {
|
|
617
|
+
const [message, setMessage] = useState('');
|
|
618
|
+
async function submit(event: FormEvent<HTMLFormElement>) {
|
|
619
|
+
event.preventDefault();
|
|
620
|
+
const data = new FormData(event.currentTarget);
|
|
621
|
+
const response = await fetch('/${name}/api/auth/password-reset', {
|
|
622
|
+
body: JSON.stringify({ email: data.get('email') }),
|
|
623
|
+
headers: { 'content-type': 'application/json' },
|
|
624
|
+
method: 'POST',
|
|
625
|
+
});
|
|
626
|
+
setMessage(response.ok ? 'If the account exists, a reset code was sent.' : 'Request failed.');
|
|
627
|
+
}
|
|
628
|
+
return <main><div className="card"><h1>Reset password</h1>
|
|
629
|
+
<form onSubmit={submit}><label>Email<input name="email" type="email" required /></label>
|
|
630
|
+
<button type="submit">Send reset code</button></form><p role="status">{message}</p>
|
|
631
|
+
</div></main>;
|
|
632
|
+
}
|
|
633
|
+
`,
|
|
634
|
+
'web/src/app/api/auth/sign-in/route.ts': `import { InitiateAuthCommand, CognitoIdentityProviderClient } from '@aws-sdk/client-cognito-identity-provider';
|
|
635
|
+
import { createSessionTokenCookiePlan } from '@empire-builder-kit/runtime/auth-session';
|
|
636
|
+
import { cookies } from 'next/headers';
|
|
637
|
+
import { NextResponse } from 'next/server';
|
|
638
|
+
|
|
639
|
+
const cognito = new CognitoIdentityProviderClient({});
|
|
640
|
+
|
|
641
|
+
export async function POST(request: Request) {
|
|
642
|
+
const body = await request.json() as { email?: string; password?: string };
|
|
643
|
+
if (!body.email || !body.password) return NextResponse.json({ ok: false }, { status: 400 });
|
|
644
|
+
const result = await cognito.send(new InitiateAuthCommand({
|
|
645
|
+
AuthFlow: 'USER_PASSWORD_AUTH',
|
|
646
|
+
ClientId: process.env.AUTH_CLIENT_ID,
|
|
647
|
+
AuthParameters: { USERNAME: body.email, PASSWORD: body.password },
|
|
648
|
+
}));
|
|
649
|
+
const auth = result.AuthenticationResult;
|
|
650
|
+
if (!auth?.AccessToken || !auth.ExpiresIn) return NextResponse.json({ ok: false }, { status: 401 });
|
|
651
|
+
const secure = process.env.EBK_SECURE_COOKIES === 'true' || process.env.NODE_ENV === 'production';
|
|
652
|
+
const store = await cookies();
|
|
653
|
+
for (const { name, value, ...options } of createSessionTokenCookiePlan({
|
|
654
|
+
accessToken: auth.AccessToken,
|
|
655
|
+
expiresAt: Math.floor(Date.now() / 1000) + auth.ExpiresIn,
|
|
656
|
+
refreshToken: auth.RefreshToken,
|
|
657
|
+
}, { namespace: '${name}', secure })) store.set(name, value, options);
|
|
658
|
+
return NextResponse.json({ ok: true });
|
|
659
|
+
}
|
|
660
|
+
`,
|
|
661
|
+
'web/src/app/api/auth/refresh/route.ts': `import { InitiateAuthCommand, CognitoIdentityProviderClient } from '@aws-sdk/client-cognito-identity-provider';
|
|
662
|
+
import { createSessionTokenCookiePlan, getSessionTokenCookieNames } from '@empire-builder-kit/runtime/auth-session';
|
|
663
|
+
import { cookies } from 'next/headers';
|
|
664
|
+
import { NextResponse } from 'next/server';
|
|
665
|
+
|
|
666
|
+
const cognito = new CognitoIdentityProviderClient({});
|
|
667
|
+
|
|
668
|
+
export async function POST() {
|
|
669
|
+
const store = await cookies();
|
|
670
|
+
const secure = process.env.EBK_SECURE_COOKIES === 'true' || process.env.NODE_ENV === 'production';
|
|
671
|
+
const names = getSessionTokenCookieNames({ namespace: '${name}', secure });
|
|
672
|
+
const refreshToken = store.get(names.refresh)?.value;
|
|
673
|
+
if (!refreshToken) return NextResponse.json({ ok: false }, { status: 401 });
|
|
674
|
+
const result = await cognito.send(new InitiateAuthCommand({
|
|
675
|
+
AuthFlow: 'REFRESH_TOKEN_AUTH', ClientId: process.env.AUTH_CLIENT_ID,
|
|
676
|
+
AuthParameters: { REFRESH_TOKEN: refreshToken },
|
|
677
|
+
}));
|
|
678
|
+
const auth = result.AuthenticationResult;
|
|
679
|
+
if (!auth?.AccessToken || !auth.ExpiresIn) return NextResponse.json({ ok: false }, { status: 401 });
|
|
680
|
+
for (const { name, value, ...options } of createSessionTokenCookiePlan({
|
|
681
|
+
accessToken: auth.AccessToken,
|
|
682
|
+
expiresAt: Math.floor(Date.now() / 1000) + auth.ExpiresIn,
|
|
683
|
+
refreshToken: auth.RefreshToken ?? refreshToken,
|
|
684
|
+
}, { namespace: '${name}', secure })) store.set(name, value, options);
|
|
685
|
+
return NextResponse.json({ ok: true });
|
|
686
|
+
}
|
|
687
|
+
`,
|
|
688
|
+
'web/src/app/api/auth/sign-out/route.ts': `import { createSessionTokenCookieDeletionPlan } from '@empire-builder-kit/runtime/auth-session';
|
|
689
|
+
import { cookies } from 'next/headers';
|
|
690
|
+
import { NextResponse } from 'next/server';
|
|
691
|
+
|
|
692
|
+
export async function POST() {
|
|
693
|
+
const store = await cookies();
|
|
694
|
+
const secure = process.env.EBK_SECURE_COOKIES === 'true' || process.env.NODE_ENV === 'production';
|
|
695
|
+
for (const { name, value, ...options } of createSessionTokenCookieDeletionPlan({
|
|
696
|
+
namespace: '${name}',
|
|
697
|
+
secure,
|
|
698
|
+
})) store.set(name, value, options);
|
|
699
|
+
return NextResponse.json({ ok: true });
|
|
700
|
+
}
|
|
701
|
+
`,
|
|
702
|
+
'web/src/app/api/auth/password-reset/route.ts': `import { ForgotPasswordCommand, CognitoIdentityProviderClient } from '@aws-sdk/client-cognito-identity-provider';
|
|
703
|
+
import { NextResponse } from 'next/server';
|
|
704
|
+
|
|
705
|
+
const cognito = new CognitoIdentityProviderClient({});
|
|
706
|
+
export async function POST(request: Request) {
|
|
707
|
+
const body = await request.json() as { email?: string };
|
|
708
|
+
if (body.email) await cognito.send(new ForgotPasswordCommand({
|
|
709
|
+
ClientId: process.env.AUTH_CLIENT_ID, Username: body.email,
|
|
710
|
+
}));
|
|
711
|
+
return NextResponse.json({ ok: true });
|
|
712
|
+
}
|
|
713
|
+
`,
|
|
714
|
+
'web/src/app/api/auth/callback/route.ts': `import { NextResponse } from 'next/server';
|
|
715
|
+
|
|
716
|
+
export function GET() {
|
|
717
|
+
return NextResponse.json({
|
|
718
|
+
message: 'The Standard Blueprint uses server-side Cognito password auth; OAuth Blueprints may replace this callback.',
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
`,
|
|
722
|
+
'web/src/app/api/protected/route.ts': `import { getSessionTokenCookieNames } from '@empire-builder-kit/runtime/auth-session';
|
|
723
|
+
import { cookies } from 'next/headers';
|
|
724
|
+
import { NextResponse } from 'next/server';
|
|
725
|
+
|
|
726
|
+
export async function GET() {
|
|
727
|
+
const store = await cookies();
|
|
728
|
+
const names = getSessionTokenCookieNames({
|
|
729
|
+
namespace: '${name}',
|
|
730
|
+
secure: process.env.EBK_SECURE_COOKIES === 'true' || process.env.NODE_ENV === 'production',
|
|
731
|
+
});
|
|
732
|
+
const accessToken = store.get(names.access)?.value;
|
|
733
|
+
if (!accessToken) return NextResponse.json({ ok: false }, { status: 401 });
|
|
734
|
+
const response = await fetch((process.env.NEXT_PUBLIC_API_URL ?? '') + '/protected', {
|
|
735
|
+
headers: { authorization: 'Bearer ' + accessToken },
|
|
736
|
+
});
|
|
737
|
+
return new NextResponse(await response.text(), {
|
|
738
|
+
headers: { 'content-type': 'application/json' },
|
|
739
|
+
status: response.status,
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
`,
|
|
743
|
+
'web/src/app/page.spec.tsx': `import { renderToStaticMarkup } from 'react-dom/server';
|
|
744
|
+
import { describe, expect, it } from 'vitest';
|
|
745
|
+
import Home from './page.js';
|
|
746
|
+
|
|
747
|
+
describe('home page', () => {
|
|
748
|
+
it('renders the Product Slice identity and sign-in path', () => {
|
|
749
|
+
const html = renderToStaticMarkup(<Home />);
|
|
750
|
+
expect(html).toContain('${pascalName}');
|
|
751
|
+
expect(html).toContain('/sign-in');
|
|
752
|
+
});
|
|
753
|
+
});
|
|
754
|
+
`,
|
|
755
|
+
'vitest.config.ts': `import { configDefaults, defineConfig } from 'vitest/config';
|
|
756
|
+
export default defineConfig({
|
|
757
|
+
test: {
|
|
758
|
+
environment: 'node',
|
|
759
|
+
env: { EBK_STAGE: 'development' },
|
|
760
|
+
exclude: [...configDefaults.exclude, '**/dist/**', 'e2e/**'],
|
|
761
|
+
},
|
|
762
|
+
});
|
|
763
|
+
`,
|
|
764
|
+
'e2e/product.spec.ts': `import { expect, test } from '@playwright/test';
|
|
765
|
+
|
|
766
|
+
test('renders the product and sign-in journey', async ({ page }) => {
|
|
767
|
+
await page.goto(process.env.EBK_SMOKE_URL ?? 'http://localhost:3000/${name}');
|
|
768
|
+
await expect(page.getByRole('heading', { name: '${pascalName}' })).toBeVisible();
|
|
769
|
+
await page.getByRole('link', { name: 'Sign in' }).click();
|
|
770
|
+
await expect(page.getByRole('heading', { name: 'Sign in' })).toBeVisible();
|
|
771
|
+
});
|
|
772
|
+
`,
|
|
773
|
+
'playwright.config.ts': `import { defineConfig } from '@playwright/test';
|
|
774
|
+
export default defineConfig({
|
|
775
|
+
testDir: './e2e',
|
|
776
|
+
use: { trace: 'retain-on-failure' },
|
|
777
|
+
});
|
|
778
|
+
`,
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
function renderSliceFiles(name, scope, runtimeVersion, ownership) {
|
|
782
|
+
const pascalName = name
|
|
783
|
+
.split('-')
|
|
784
|
+
.map((segment) => segment[0]?.toUpperCase() + segment.slice(1))
|
|
785
|
+
.join('');
|
|
786
|
+
return {
|
|
787
|
+
'.ebk/ownership.json': json(ownership),
|
|
788
|
+
'README.md': `# ${name}
|
|
789
|
+
|
|
790
|
+
This Product Slice is an independent business and deployment boundary generated from the
|
|
791
|
+
built-in Empire Builder Kit Standard Blueprint.
|
|
792
|
+
|
|
793
|
+
It consumes the Standard Foundation through its versioned capability contract. Do not import
|
|
794
|
+
files from \`packages/foundation/infra\` or from another slice's internals.
|
|
795
|
+
|
|
796
|
+
## Included product baseline
|
|
797
|
+
|
|
798
|
+
- Next.js web application with sign-in, refresh, sign-out, callback, and password-reset paths
|
|
799
|
+
- Lambda HTTP API with health and JWT-protected routes
|
|
800
|
+
- stage- and slice-scoped logical Postgres database plus migration history
|
|
801
|
+
- typed EventBridge publisher and SQS/DLQ-backed consumer
|
|
802
|
+
- Product Slice dashboard and API/DLQ alarms routed through Foundation alerts
|
|
803
|
+
- Vitest source test and Playwright deployed-smoke path
|
|
804
|
+
|
|
805
|
+
Keep business contracts in \`contracts/\`. Add long-running Services and finite Jobs only when
|
|
806
|
+
the product needs them.
|
|
807
|
+
|
|
808
|
+
Add product dependencies with \`pnpm --filter ${scope}/${name} add <package>\`. EBK owns only
|
|
809
|
+
its recorded framework keys in this package manifest, so health checks and upgrades preserve
|
|
810
|
+
dependencies, scripts, and other keys added by the product team.
|
|
811
|
+
|
|
812
|
+
## Local database
|
|
813
|
+
|
|
814
|
+
Run \`pnpm nx run ${name}:dev --stage=development\`. The target starts Docker Postgres,
|
|
815
|
+
applies local migrations, and then starts SST live mode. Set \`EBK_DEV_PORT\` to choose the
|
|
816
|
+
Next.js port; it defaults to \`3000\`. The web readiness endpoint is
|
|
817
|
+
\`http://127.0.0.1:<port>/${name}/api/health\`.
|
|
818
|
+
|
|
819
|
+
The workspace fleet target discovers this slice through its versioned
|
|
820
|
+
\`metadata.ebk.localDev\` adapter. It supplies the exact HTTPS fleet origin, secure-cookie
|
|
821
|
+
signal, callback/logout URLs, and allocated internal port; do not persist a developer port in
|
|
822
|
+
this project.
|
|
823
|
+
|
|
824
|
+
\`database/bindings.json\` is the adopter-owned database selection contract. Add a candidate
|
|
825
|
+
identifier to \`primary.provisioned\` to provision it without moving workload traffic. After the
|
|
826
|
+
candidate is migrated and verified through the database-replacement runbook, change only
|
|
827
|
+
\`primary.active\` to select it on the next reviewed deployment. Do not edit managed
|
|
828
|
+
\`infra/product.ts\` or provision databases with Foundation master credentials.
|
|
829
|
+
|
|
830
|
+
The readiness route is framework-managed for local orchestration. Put product-specific
|
|
831
|
+
dependency and status checks under \`/${name}/api/status\` rather than editing that route.
|
|
832
|
+
|
|
833
|
+
Run \`pnpm nx run ${name}:db-local-down\` when finished. The named volume preserves local data
|
|
834
|
+
between sessions.
|
|
835
|
+
|
|
836
|
+
Use \`pnpm nx run ${name}:dev --stage=<personal-name>\` for a personal live relay. The target
|
|
837
|
+
maps that consumer stage to the \`development\` Foundation and reports both stages during
|
|
838
|
+
preflight. It never deploys a personal Foundation. A normal full deploy at a mapped personal
|
|
839
|
+
stage fails closed; use a canonical stage for \`${name}:deploy\`.
|
|
840
|
+
`,
|
|
841
|
+
'contracts/index.ts': `export const ${pascalName}ContractVersion = '1.0.0' as const;
|
|
842
|
+
|
|
843
|
+
// Export transport-neutral commands, events, DTOs, and clients from this package surface.
|
|
844
|
+
`,
|
|
845
|
+
'docs/architecture.md': `# ${name} architecture
|
|
846
|
+
|
|
847
|
+
## Boundary
|
|
848
|
+
|
|
849
|
+
This slice owns its product behavior, routes, app clients, logical databases, event rules,
|
|
850
|
+
queues, alarms, dashboards, and runbooks. The Foundation owns shared capacity and policy.
|
|
851
|
+
|
|
852
|
+
## Dependencies
|
|
853
|
+
|
|
854
|
+
- Foundation contract: \`^1\`
|
|
855
|
+
- Canonical stages map to the same Foundation stage; personal/preview stages map only to
|
|
856
|
+
\`development\`
|
|
857
|
+
- Other slices: contracts-only imports
|
|
858
|
+
`,
|
|
859
|
+
'docs/contracts.md': `# ${name} contracts
|
|
860
|
+
|
|
861
|
+
Published contracts belong in \`contracts/\`. Keep them transport-neutral and versioned.
|
|
862
|
+
Record consumed slice contracts here before adding a source dependency.
|
|
863
|
+
`,
|
|
864
|
+
'docs/runbooks.md': `# ${name} runbooks
|
|
865
|
+
|
|
866
|
+
Document deployment smoke checks, alarms, rollback, event redrive, database migration, and
|
|
867
|
+
Service or Job recovery procedures as those capabilities are added.
|
|
868
|
+
`,
|
|
869
|
+
'ebk-globals.d.ts': `declare const sst: any;
|
|
870
|
+
declare const aws: any;
|
|
871
|
+
declare const $app: { name: string; stage: string };
|
|
872
|
+
declare const $config: any;
|
|
873
|
+
declare const $dev: boolean;
|
|
874
|
+
declare const $interpolate: (
|
|
875
|
+
strings: TemplateStringsArray,
|
|
876
|
+
...values: unknown[]
|
|
877
|
+
) => unknown;
|
|
878
|
+
declare const $util: any;
|
|
879
|
+
`,
|
|
880
|
+
'infra/foundation.ts': `import {
|
|
881
|
+
foundationSsmPath,
|
|
882
|
+
productSliceRegistrationPath,
|
|
883
|
+
resolveFoundationStages,
|
|
884
|
+
type FoundationDiscovery,
|
|
885
|
+
} from '@empire-builder-kit/nx/foundation';
|
|
886
|
+
|
|
887
|
+
const stages = resolveFoundationStages({
|
|
888
|
+
consumerStage: $app.stage,
|
|
889
|
+
foundationStage: process.env.EBK_FOUNDATION_STAGE,
|
|
890
|
+
});
|
|
891
|
+
|
|
892
|
+
function foundationParameter(name: Parameters<typeof foundationSsmPath>[1]) {
|
|
893
|
+
return aws.ssm.getParameterOutput({
|
|
894
|
+
name: foundationSsmPath(stages.foundationStage, name),
|
|
895
|
+
}).value;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
export function getFoundation(): FoundationDiscovery {
|
|
899
|
+
return {
|
|
900
|
+
alertTopicArn: foundationParameter('alertTopicArn'),
|
|
901
|
+
capabilities: foundationParameter('capabilities').apply((value: string) =>
|
|
902
|
+
value.split(',').map((capability) => capability.trim()),
|
|
903
|
+
),
|
|
904
|
+
clusterId: foundationParameter('clusterId'),
|
|
905
|
+
contractVersion: foundationParameter('contractVersion'),
|
|
906
|
+
databaseClusterArn: foundationParameter('databaseClusterArn'),
|
|
907
|
+
databaseClusterId: foundationParameter('databaseClusterId'),
|
|
908
|
+
databaseMasterSecretArn: foundationParameter('databaseMasterSecretArn'),
|
|
909
|
+
eventBusArn: foundationParameter('eventBusArn'),
|
|
910
|
+
eventBusName: foundationParameter('eventBusName'),
|
|
911
|
+
hasPublicDomain: foundationParameter('hasPublicDomain').apply(
|
|
912
|
+
(value: string) => value === 'true',
|
|
913
|
+
),
|
|
914
|
+
identityUserPoolArn: foundationParameter('identityUserPoolArn'),
|
|
915
|
+
identityUserPoolId: foundationParameter('identityUserPoolId'),
|
|
916
|
+
rootDomain: foundationParameter('rootDomain').apply((value: string) =>
|
|
917
|
+
value === 'none' ? undefined : value,
|
|
918
|
+
),
|
|
919
|
+
routerDistributionId: foundationParameter('routerDistributionId'),
|
|
920
|
+
routerUrl: foundationParameter('routerUrl'),
|
|
921
|
+
schemaVersion: foundationParameter('schemaVersion').apply(Number),
|
|
922
|
+
vpcId: foundationParameter('vpcId'),
|
|
923
|
+
} as FoundationDiscovery;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
export function getFoundationInfrastructure() {
|
|
927
|
+
const discovery = getFoundation();
|
|
928
|
+
const vpc = sst.aws.Vpc.get('FoundationVpc', discovery.vpcId);
|
|
929
|
+
const cluster = sst.aws.Cluster.get('FoundationCluster', {
|
|
930
|
+
id: discovery.clusterId,
|
|
931
|
+
vpc,
|
|
932
|
+
});
|
|
933
|
+
const router = sst.aws.Router.get(
|
|
934
|
+
'FoundationRouter',
|
|
935
|
+
discovery.routerDistributionId,
|
|
936
|
+
);
|
|
937
|
+
const userPool = sst.aws.CognitoUserPool.get(
|
|
938
|
+
'FoundationIdentity',
|
|
939
|
+
discovery.identityUserPoolId,
|
|
940
|
+
);
|
|
941
|
+
const eventBus = sst.aws.Bus.get(
|
|
942
|
+
'FoundationEventBus',
|
|
943
|
+
discovery.eventBusName,
|
|
944
|
+
);
|
|
945
|
+
const database = sst.aws.Aurora.get(
|
|
946
|
+
'FoundationDatabase',
|
|
947
|
+
discovery.databaseClusterId,
|
|
948
|
+
);
|
|
949
|
+
|
|
950
|
+
return {
|
|
951
|
+
cluster,
|
|
952
|
+
database: {
|
|
953
|
+
cluster: database,
|
|
954
|
+
clusterArn: discovery.databaseClusterArn,
|
|
955
|
+
masterSecretArn: discovery.databaseMasterSecretArn,
|
|
956
|
+
},
|
|
957
|
+
discovery,
|
|
958
|
+
eventBus,
|
|
959
|
+
router,
|
|
960
|
+
userPool,
|
|
961
|
+
vpc,
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
export function registerProductSlice() {
|
|
966
|
+
return new aws.ssm.Parameter('${pascalName}ProductSliceRegistration', {
|
|
967
|
+
name: productSliceRegistrationPath(
|
|
968
|
+
stages.foundationStage,
|
|
969
|
+
stages.consumerStage,
|
|
970
|
+
'${name}',
|
|
971
|
+
),
|
|
972
|
+
type: 'String',
|
|
973
|
+
value: JSON.stringify({
|
|
974
|
+
consumerStage: stages.consumerStage,
|
|
975
|
+
foundationContract: '^1.0.0',
|
|
976
|
+
foundationStage: stages.foundationStage,
|
|
977
|
+
name: '${name}',
|
|
978
|
+
registrationVersion: 1,
|
|
979
|
+
}),
|
|
980
|
+
});
|
|
981
|
+
}
|
|
982
|
+
`,
|
|
983
|
+
'infra/index.ts': `import { getFoundation, registerProductSlice } from './foundation.js';
|
|
984
|
+
import { product } from './product.js';
|
|
985
|
+
|
|
986
|
+
export function app() {
|
|
987
|
+
const foundation = getFoundation();
|
|
988
|
+
const registration = registerProductSlice();
|
|
989
|
+
const resources = product();
|
|
990
|
+
return {
|
|
991
|
+
apiUrl: resources.api.url,
|
|
992
|
+
authClientId: resources.authClient.id,
|
|
993
|
+
foundationContractVersion: foundation.contractVersion,
|
|
994
|
+
name: '${name}',
|
|
995
|
+
registration: registration.name,
|
|
996
|
+
stage: $app.stage,
|
|
997
|
+
webUrl: resources.web.url,
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
`,
|
|
1001
|
+
'package.json': json({
|
|
1002
|
+
name: `${scope}/${name}`,
|
|
1003
|
+
version: '0.0.0',
|
|
1004
|
+
private: true,
|
|
1005
|
+
dependencies: {
|
|
1006
|
+
'@aws-sdk/client-cognito-identity-provider': PRODUCT_VERSIONS.awsCognito,
|
|
1007
|
+
'@aws-sdk/client-eventbridge': PRODUCT_VERSIONS.awsEventBridge,
|
|
1008
|
+
'@aws-sdk/client-rds-data': PRODUCT_VERSIONS.awsRdsData,
|
|
1009
|
+
'@aws-sdk/client-secrets-manager': PRODUCT_VERSIONS.awsSecretsManager,
|
|
1010
|
+
'@aws-sdk/client-ssm': PRODUCT_VERSIONS.awsSsm,
|
|
1011
|
+
'@empire-builder-kit/runtime': runtimeVersion,
|
|
1012
|
+
'drizzle-orm': PRODUCT_VERSIONS.drizzle,
|
|
1013
|
+
next: PRODUCT_VERSIONS.next,
|
|
1014
|
+
react: PRODUCT_VERSIONS.react,
|
|
1015
|
+
'react-dom': PRODUCT_VERSIONS.reactDom,
|
|
1016
|
+
},
|
|
1017
|
+
devDependencies: {
|
|
1018
|
+
'@playwright/test': PRODUCT_VERSIONS.playwright,
|
|
1019
|
+
'@types/node': PRODUCT_VERSIONS.typesNode,
|
|
1020
|
+
'@types/react': PRODUCT_VERSIONS.typesReact,
|
|
1021
|
+
'@types/react-dom': PRODUCT_VERSIONS.typesReactDom,
|
|
1022
|
+
'drizzle-kit': PRODUCT_VERSIONS.drizzleKit,
|
|
1023
|
+
pg: PRODUCT_VERSIONS.pg,
|
|
1024
|
+
vitest: PRODUCT_VERSIONS.vitest,
|
|
1025
|
+
},
|
|
1026
|
+
exports: {
|
|
1027
|
+
'./contracts': {
|
|
1028
|
+
types: './contracts/index.ts',
|
|
1029
|
+
default: './contracts/index.ts',
|
|
1030
|
+
},
|
|
1031
|
+
},
|
|
1032
|
+
}),
|
|
1033
|
+
'project.json': json({
|
|
1034
|
+
name,
|
|
1035
|
+
$schema: '../../node_modules/nx/schemas/project-schema.json',
|
|
1036
|
+
projectType: 'application',
|
|
1037
|
+
sourceRoot: `packages/${name}/infra`,
|
|
1038
|
+
implicitDependencies: ['foundation'],
|
|
1039
|
+
tags: [
|
|
1040
|
+
`scope:${name}`,
|
|
1041
|
+
'type:slice',
|
|
1042
|
+
'type:sst',
|
|
1043
|
+
'layer:product',
|
|
1044
|
+
'visibility:internal',
|
|
1045
|
+
],
|
|
1046
|
+
metadata: {
|
|
1047
|
+
ebk: {
|
|
1048
|
+
localDev: (0, exports.STANDARD_LOCAL_DEV_ADAPTER)(name),
|
|
1049
|
+
},
|
|
1050
|
+
},
|
|
1051
|
+
targets: {
|
|
1052
|
+
build: {
|
|
1053
|
+
executor: 'nx:run-commands',
|
|
1054
|
+
dependsOn: ['typecheck'],
|
|
1055
|
+
options: {
|
|
1056
|
+
command: 'next build web',
|
|
1057
|
+
cwd: `packages/${name}`,
|
|
1058
|
+
},
|
|
1059
|
+
},
|
|
1060
|
+
'db-check': {
|
|
1061
|
+
executor: 'nx:run-commands',
|
|
1062
|
+
options: {
|
|
1063
|
+
command: 'node scripts/check-database.mjs',
|
|
1064
|
+
cwd: `packages/${name}`,
|
|
1065
|
+
},
|
|
1066
|
+
},
|
|
1067
|
+
'db-generate': {
|
|
1068
|
+
executor: 'nx:run-commands',
|
|
1069
|
+
options: {
|
|
1070
|
+
command: 'drizzle-kit generate --config=database/drizzle.config.ts',
|
|
1071
|
+
cwd: `packages/${name}`,
|
|
1072
|
+
},
|
|
1073
|
+
},
|
|
1074
|
+
'db-migrate': {
|
|
1075
|
+
executor: 'nx:run-commands',
|
|
1076
|
+
dependsOn: [{ target: 'deploy-infrastructure', params: 'forward' }],
|
|
1077
|
+
options: {
|
|
1078
|
+
command: 'node scripts/migrate-database.mjs',
|
|
1079
|
+
cwd: `packages/${name}`,
|
|
1080
|
+
forwardAllArgs: true,
|
|
1081
|
+
},
|
|
1082
|
+
},
|
|
1083
|
+
'db-local-down': {
|
|
1084
|
+
executor: 'nx:run-commands',
|
|
1085
|
+
options: {
|
|
1086
|
+
command: 'node scripts/local-database.mjs down',
|
|
1087
|
+
cwd: `packages/${name}`,
|
|
1088
|
+
},
|
|
1089
|
+
},
|
|
1090
|
+
'db-local-migrate': {
|
|
1091
|
+
executor: 'nx:run-commands',
|
|
1092
|
+
dependsOn: ['db-local-up'],
|
|
1093
|
+
options: {
|
|
1094
|
+
command: 'drizzle-kit migrate --config=database/drizzle.config.ts',
|
|
1095
|
+
cwd: `packages/${name}`,
|
|
1096
|
+
},
|
|
1097
|
+
},
|
|
1098
|
+
'db-local-up': {
|
|
1099
|
+
executor: 'nx:run-commands',
|
|
1100
|
+
dependsOn: ['db-check'],
|
|
1101
|
+
options: {
|
|
1102
|
+
command: 'node scripts/local-database.mjs up',
|
|
1103
|
+
cwd: `packages/${name}`,
|
|
1104
|
+
},
|
|
1105
|
+
},
|
|
1106
|
+
deploy: {
|
|
1107
|
+
executor: 'nx:run-commands',
|
|
1108
|
+
dependsOn: [{ target: 'db-migrate', params: 'forward' }],
|
|
1109
|
+
options: {
|
|
1110
|
+
command: 'node scripts/confirm-deploy.mjs',
|
|
1111
|
+
cwd: `packages/${name}`,
|
|
1112
|
+
forwardAllArgs: true,
|
|
1113
|
+
},
|
|
1114
|
+
},
|
|
1115
|
+
'deploy-infrastructure': {
|
|
1116
|
+
executor: '@empire-builder-kit/nx:deploy',
|
|
1117
|
+
dependsOn: [
|
|
1118
|
+
'verify',
|
|
1119
|
+
{ target: 'foundation-ready', params: 'forward' },
|
|
1120
|
+
],
|
|
1121
|
+
options: {
|
|
1122
|
+
cwd: `packages/${name}`,
|
|
1123
|
+
stage: 'development',
|
|
1124
|
+
stageMode: 'consumer',
|
|
1125
|
+
},
|
|
1126
|
+
},
|
|
1127
|
+
dev: {
|
|
1128
|
+
executor: '@empire-builder-kit/nx:dev',
|
|
1129
|
+
dependsOn: [
|
|
1130
|
+
'dev-prerequisite',
|
|
1131
|
+
'db-local-migrate',
|
|
1132
|
+
{ target: 'foundation-preflight', params: 'forward' },
|
|
1133
|
+
],
|
|
1134
|
+
options: {
|
|
1135
|
+
cwd: `packages/${name}`,
|
|
1136
|
+
stage: 'development',
|
|
1137
|
+
stageMode: 'consumer',
|
|
1138
|
+
},
|
|
1139
|
+
},
|
|
1140
|
+
'dev-prerequisite': {
|
|
1141
|
+
executor: 'nx:run-commands',
|
|
1142
|
+
dependsOn: ['db-check', 'typecheck'],
|
|
1143
|
+
options: {
|
|
1144
|
+
command: 'node scripts/verify-live-mode.mjs',
|
|
1145
|
+
cwd: `packages/${name}`,
|
|
1146
|
+
},
|
|
1147
|
+
},
|
|
1148
|
+
lint: {
|
|
1149
|
+
executor: 'nx:run-commands',
|
|
1150
|
+
options: {
|
|
1151
|
+
command: 'eslint .',
|
|
1152
|
+
cwd: `packages/${name}`,
|
|
1153
|
+
},
|
|
1154
|
+
},
|
|
1155
|
+
'foundation-preflight': {
|
|
1156
|
+
executor: '@empire-builder-kit/nx:foundation-preflight',
|
|
1157
|
+
options: {
|
|
1158
|
+
capabilities: [...ownership.foundation.capabilities],
|
|
1159
|
+
contract: ownership.foundation.contract,
|
|
1160
|
+
stage: 'development',
|
|
1161
|
+
},
|
|
1162
|
+
},
|
|
1163
|
+
'foundation-ready': {
|
|
1164
|
+
executor: '@empire-builder-kit/nx:foundation-preflight',
|
|
1165
|
+
dependsOn: [
|
|
1166
|
+
{ projects: ['foundation'], target: 'verify', params: 'ignore' },
|
|
1167
|
+
],
|
|
1168
|
+
options: {
|
|
1169
|
+
capabilities: [...ownership.foundation.capabilities],
|
|
1170
|
+
contract: ownership.foundation.contract,
|
|
1171
|
+
deployIfMissing: true,
|
|
1172
|
+
foundationProject: 'foundation',
|
|
1173
|
+
stage: 'development',
|
|
1174
|
+
},
|
|
1175
|
+
},
|
|
1176
|
+
refresh: {
|
|
1177
|
+
executor: '@empire-builder-kit/nx:refresh',
|
|
1178
|
+
options: {
|
|
1179
|
+
cwd: `packages/${name}`,
|
|
1180
|
+
stage: 'development',
|
|
1181
|
+
stageMode: 'consumer',
|
|
1182
|
+
},
|
|
1183
|
+
},
|
|
1184
|
+
remove: {
|
|
1185
|
+
executor: '@empire-builder-kit/nx:remove',
|
|
1186
|
+
options: {
|
|
1187
|
+
cwd: `packages/${name}`,
|
|
1188
|
+
stage: 'development',
|
|
1189
|
+
stageMode: 'consumer',
|
|
1190
|
+
},
|
|
1191
|
+
},
|
|
1192
|
+
'smoke-deployed': {
|
|
1193
|
+
executor: 'nx:run-commands',
|
|
1194
|
+
options: {
|
|
1195
|
+
command: 'playwright install chromium && playwright test',
|
|
1196
|
+
cwd: `packages/${name}`,
|
|
1197
|
+
},
|
|
1198
|
+
},
|
|
1199
|
+
'smoke-deployed-check': {
|
|
1200
|
+
executor: 'nx:run-commands',
|
|
1201
|
+
options: {
|
|
1202
|
+
command: 'playwright test --list',
|
|
1203
|
+
cwd: `packages/${name}`,
|
|
1204
|
+
},
|
|
1205
|
+
},
|
|
1206
|
+
test: {
|
|
1207
|
+
executor: 'nx:run-commands',
|
|
1208
|
+
options: {
|
|
1209
|
+
command: 'vitest run',
|
|
1210
|
+
cwd: `packages/${name}`,
|
|
1211
|
+
},
|
|
1212
|
+
},
|
|
1213
|
+
typecheck: {
|
|
1214
|
+
executor: 'nx:run-commands',
|
|
1215
|
+
options: {
|
|
1216
|
+
command: 'tsc --noEmit -p tsconfig.json',
|
|
1217
|
+
cwd: `packages/${name}`,
|
|
1218
|
+
},
|
|
1219
|
+
},
|
|
1220
|
+
unlock: {
|
|
1221
|
+
executor: '@empire-builder-kit/nx:unlock',
|
|
1222
|
+
options: {
|
|
1223
|
+
cwd: `packages/${name}`,
|
|
1224
|
+
stage: 'development',
|
|
1225
|
+
stageMode: 'consumer',
|
|
1226
|
+
},
|
|
1227
|
+
},
|
|
1228
|
+
verify: {
|
|
1229
|
+
executor: 'nx:run-commands',
|
|
1230
|
+
dependsOn: [
|
|
1231
|
+
'build',
|
|
1232
|
+
'db-check',
|
|
1233
|
+
'lint',
|
|
1234
|
+
'smoke-deployed-check',
|
|
1235
|
+
'test',
|
|
1236
|
+
'typecheck',
|
|
1237
|
+
],
|
|
1238
|
+
options: {
|
|
1239
|
+
command: 'node scripts/verify-slice.mjs',
|
|
1240
|
+
cwd: `packages/${name}`,
|
|
1241
|
+
},
|
|
1242
|
+
},
|
|
1243
|
+
},
|
|
1244
|
+
}),
|
|
1245
|
+
'scripts/verify-slice.mjs': `import { readFileSync } from 'node:fs';
|
|
1246
|
+
|
|
1247
|
+
const ownership = JSON.parse(
|
|
1248
|
+
readFileSync(new URL('../.ebk/ownership.json', import.meta.url), 'utf8'),
|
|
1249
|
+
);
|
|
1250
|
+
if (ownership.schemaVersion !== ${index_js_3.GENERATED_OWNERSHIP_SCHEMA_VERSION} || ownership.slice !== '${name}') {
|
|
1251
|
+
throw new Error('Slice ownership metadata is missing or incompatible.');
|
|
1252
|
+
}
|
|
1253
|
+
if (!ownership.foundation.capabilities.includes('deployment')) {
|
|
1254
|
+
throw new Error('Slice does not declare the Foundation deployment capability.');
|
|
1255
|
+
}
|
|
1256
|
+
process.stdout.write('${name} slice source contract verified.\\n');
|
|
1257
|
+
`,
|
|
1258
|
+
'scripts/verify-live-mode.mjs': `const origin = process.env.EBK_FLEET_ORIGIN?.trim();
|
|
1259
|
+
if (origin) {
|
|
1260
|
+
const parsed = new URL(origin);
|
|
1261
|
+
const supportedHostname = parsed.hostname.endsWith('.ebk.localhost') || parsed.hostname.endsWith('.test');
|
|
1262
|
+
if (parsed.protocol !== 'https:' || parsed.origin !== origin || !supportedHostname) {
|
|
1263
|
+
throw new Error('EBK_FLEET_ORIGIN must be an exact HTTPS *.ebk.localhost or adopter-managed *.test origin.');
|
|
1264
|
+
}
|
|
1265
|
+
if (process.env.EBK_SECURE_COOKIES !== 'true') {
|
|
1266
|
+
throw new Error('Fleet development requires EBK_SECURE_COOKIES=true.');
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
process.stdout.write('${name} live-mode prerequisites verified.\\n');
|
|
1270
|
+
`,
|
|
1271
|
+
'scripts/check-database.mjs': `import { existsSync, readFileSync } from 'node:fs';
|
|
1272
|
+
import { createDatabaseBindingPlan } from '@empire-builder-kit/nx/foundation';
|
|
1273
|
+
import databaseBindings from '../database/bindings.json' with { type: 'json' };
|
|
1274
|
+
|
|
1275
|
+
const migration = new URL('../database/migrations/0000_initial.sql', import.meta.url);
|
|
1276
|
+
if (!existsSync(migration) || !readFileSync(migration, 'utf8').includes('CREATE TABLE')) {
|
|
1277
|
+
throw new Error('The initial database migration is missing or invalid.');
|
|
1278
|
+
}
|
|
1279
|
+
const plan = createDatabaseBindingPlan(databaseBindings, {
|
|
1280
|
+
slice: '${name}',
|
|
1281
|
+
stage: process.env.EBK_STAGE ?? 'development',
|
|
1282
|
+
});
|
|
1283
|
+
if (!plan.active.primary) throw new Error('Database binding "primary" is required.');
|
|
1284
|
+
process.stdout.write('${name} database history verified.\\n');
|
|
1285
|
+
`,
|
|
1286
|
+
'scripts/local-database.mjs': `import { spawnSync } from 'node:child_process';
|
|
1287
|
+
import { createDatabaseBindingPlan, resolveLocalDatabaseContract } from '@empire-builder-kit/nx/foundation';
|
|
1288
|
+
import databaseBindings from '../database/bindings.json' with { type: 'json' };
|
|
1289
|
+
|
|
1290
|
+
const action = process.argv[2];
|
|
1291
|
+
if (action !== 'up' && action !== 'down') {
|
|
1292
|
+
throw new Error('Usage: node scripts/local-database.mjs <up|down>');
|
|
1293
|
+
}
|
|
1294
|
+
const plan = createDatabaseBindingPlan(databaseBindings, {
|
|
1295
|
+
slice: '${name}',
|
|
1296
|
+
stage: 'development',
|
|
1297
|
+
});
|
|
1298
|
+
const activeDatabase = plan.active.primary;
|
|
1299
|
+
if (!activeDatabase) throw new Error('Database binding "primary" is required.');
|
|
1300
|
+
const local = resolveLocalDatabaseContract({
|
|
1301
|
+
database: activeDatabase,
|
|
1302
|
+
slice: '${name}',
|
|
1303
|
+
});
|
|
1304
|
+
const project = process.env.COMPOSE_PROJECT_NAME ?? ('ebk-${name}-' + activeDatabase)
|
|
1305
|
+
.replace(/[^a-z0-9_-]/g, '_')
|
|
1306
|
+
.toLowerCase();
|
|
1307
|
+
const args = ['compose', '--project-name', project, '-f', 'database/docker-compose.yml'];
|
|
1308
|
+
args.push(...(action === 'up' ? ['up', '-d', '--wait'] : ['down']));
|
|
1309
|
+
const result = spawnSync('docker', args, {
|
|
1310
|
+
env: {
|
|
1311
|
+
...process.env,
|
|
1312
|
+
EBK_LOCAL_DATABASE: local.database,
|
|
1313
|
+
EBK_LOCAL_DATABASE_PASSWORD: local.password,
|
|
1314
|
+
EBK_LOCAL_DATABASE_PORT: String(local.port),
|
|
1315
|
+
EBK_LOCAL_DATABASE_USERNAME: local.username,
|
|
1316
|
+
},
|
|
1317
|
+
stdio: 'inherit',
|
|
1318
|
+
});
|
|
1319
|
+
if (result.error) throw result.error;
|
|
1320
|
+
if (result.status !== 0) process.exit(result.status ?? 1);
|
|
1321
|
+
`,
|
|
1322
|
+
'scripts/confirm-deploy.mjs': `const stageArg = process.argv.find((value) => value.startsWith('--stage='));
|
|
1323
|
+
const stage = stageArg?.slice('--stage='.length) ?? 'development';
|
|
1324
|
+
process.stdout.write('${name} infrastructure and database migrations completed for ' + stage + '.\\n');
|
|
1325
|
+
`,
|
|
1326
|
+
'scripts/migrate-database.mjs': `import { createHash } from 'node:crypto';
|
|
1327
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
1328
|
+
import { createDatabaseBindingPlan, resolveFoundationStages } from '@empire-builder-kit/nx/foundation';
|
|
1329
|
+
import {
|
|
1330
|
+
BeginTransactionCommand,
|
|
1331
|
+
CommitTransactionCommand,
|
|
1332
|
+
ExecuteStatementCommand,
|
|
1333
|
+
RDSDataClient,
|
|
1334
|
+
RollbackTransactionCommand,
|
|
1335
|
+
} from '@aws-sdk/client-rds-data';
|
|
1336
|
+
import { DescribeSecretCommand, SecretsManagerClient } from '@aws-sdk/client-secrets-manager';
|
|
1337
|
+
import { GetParameterCommand, SSMClient } from '@aws-sdk/client-ssm';
|
|
1338
|
+
import databaseBindings from '../database/bindings.json' with { type: 'json' };
|
|
1339
|
+
|
|
1340
|
+
const stageArg = process.argv.find((value) => value.startsWith('--stage='));
|
|
1341
|
+
const foundationStageArg = process.argv.find((value) => value.startsWith('--foundationStage='));
|
|
1342
|
+
const { consumerStage: stage, foundationStage } = resolveFoundationStages({
|
|
1343
|
+
consumerStage: stageArg?.slice('--stage='.length) ?? 'development',
|
|
1344
|
+
foundationStage: foundationStageArg?.slice('--foundationStage='.length) ?? process.env.EBK_FOUNDATION_STAGE,
|
|
1345
|
+
});
|
|
1346
|
+
const bindingPlan = createDatabaseBindingPlan(databaseBindings, {
|
|
1347
|
+
slice: '${name}',
|
|
1348
|
+
stage,
|
|
1349
|
+
});
|
|
1350
|
+
const activeDatabase = bindingPlan.active.primary;
|
|
1351
|
+
if (!activeDatabase) throw new Error('Database binding "primary" is required.');
|
|
1352
|
+
const database = bindingPlan.identifiers[activeDatabase].database;
|
|
1353
|
+
const clusterParameter = '/ebk/' + foundationStage + '/foundation/databaseClusterArn';
|
|
1354
|
+
const clusterResponse = await new SSMClient({}).send(
|
|
1355
|
+
new GetParameterCommand({ Name: clusterParameter }),
|
|
1356
|
+
);
|
|
1357
|
+
const resourceArn = clusterResponse.Parameter?.Value;
|
|
1358
|
+
if (!resourceArn) throw new Error('Foundation database cluster ARN is missing at ' + clusterParameter + '.');
|
|
1359
|
+
const secretId = 'ebk/' + stage + '/${name}/database/' + activeDatabase + '/readwrite';
|
|
1360
|
+
const secretResponse = await new SecretsManagerClient({}).send(
|
|
1361
|
+
new DescribeSecretCommand({ SecretId: secretId }),
|
|
1362
|
+
);
|
|
1363
|
+
const secretArn = secretResponse.ARN;
|
|
1364
|
+
if (!secretArn) throw new Error('Logical database secret ARN is missing for ' + secretId + '.');
|
|
1365
|
+
|
|
1366
|
+
const client = new RDSDataClient({});
|
|
1367
|
+
const resumeRetryDelaysMs = [1_000, 2_000, 4_000, 8_000, 16_000];
|
|
1368
|
+
const sleep = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
1369
|
+
const send = async (command) => {
|
|
1370
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
1371
|
+
try {
|
|
1372
|
+
return await client.send(command);
|
|
1373
|
+
} catch (error) {
|
|
1374
|
+
const resumable = error && typeof error === 'object' && error.name === 'DatabaseResumingException';
|
|
1375
|
+
if (!resumable || attempt >= resumeRetryDelaysMs.length) throw error;
|
|
1376
|
+
await sleep(resumeRetryDelaysMs[attempt]);
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
};
|
|
1380
|
+
const execute = (sql, parameters = [], transactionId) => send(
|
|
1381
|
+
new ExecuteStatementCommand({
|
|
1382
|
+
database,
|
|
1383
|
+
parameters,
|
|
1384
|
+
resourceArn,
|
|
1385
|
+
secretArn,
|
|
1386
|
+
sql,
|
|
1387
|
+
transactionId,
|
|
1388
|
+
}),
|
|
1389
|
+
);
|
|
1390
|
+
|
|
1391
|
+
await execute('CREATE TABLE IF NOT EXISTS _ebk_migrations (name text PRIMARY KEY, checksum text NOT NULL, applied_at timestamptz NOT NULL DEFAULT now())');
|
|
1392
|
+
const directory = new URL('../database/migrations/', import.meta.url);
|
|
1393
|
+
const files = (await readdir(directory)).filter((file) => file.endsWith('.sql')).sort();
|
|
1394
|
+
for (const name of files) {
|
|
1395
|
+
const sql = await readFile(new URL(name, directory), 'utf8');
|
|
1396
|
+
const checksum = createHash('sha256').update(sql).digest('hex');
|
|
1397
|
+
const existing = await execute(
|
|
1398
|
+
'SELECT checksum FROM _ebk_migrations WHERE name = :name',
|
|
1399
|
+
[{ name: 'name', value: { stringValue: name } }],
|
|
1400
|
+
);
|
|
1401
|
+
const appliedChecksum = existing.records?.[0]?.[0]?.stringValue;
|
|
1402
|
+
if (appliedChecksum === checksum) continue;
|
|
1403
|
+
if (appliedChecksum) throw new Error('Applied migration changed: ' + name);
|
|
1404
|
+
|
|
1405
|
+
const transaction = await send(new BeginTransactionCommand({
|
|
1406
|
+
database,
|
|
1407
|
+
resourceArn,
|
|
1408
|
+
secretArn,
|
|
1409
|
+
}));
|
|
1410
|
+
if (!transaction.transactionId) throw new Error('Data API did not return a transaction id.');
|
|
1411
|
+
try {
|
|
1412
|
+
const statements = sql.split(/^--> statement-breakpoint\\s*$/m).map((value) => value.trim()).filter(Boolean);
|
|
1413
|
+
for (const statement of statements) await execute(statement, [], transaction.transactionId);
|
|
1414
|
+
await execute(
|
|
1415
|
+
'INSERT INTO _ebk_migrations (name, checksum) VALUES (:name, :checksum)',
|
|
1416
|
+
[
|
|
1417
|
+
{ name: 'name', value: { stringValue: name } },
|
|
1418
|
+
{ name: 'checksum', value: { stringValue: checksum } },
|
|
1419
|
+
],
|
|
1420
|
+
transaction.transactionId,
|
|
1421
|
+
);
|
|
1422
|
+
await send(new CommitTransactionCommand({
|
|
1423
|
+
resourceArn,
|
|
1424
|
+
secretArn,
|
|
1425
|
+
transactionId: transaction.transactionId,
|
|
1426
|
+
}));
|
|
1427
|
+
} catch (error) {
|
|
1428
|
+
await send(new RollbackTransactionCommand({
|
|
1429
|
+
resourceArn,
|
|
1430
|
+
secretArn,
|
|
1431
|
+
transactionId: transaction.transactionId,
|
|
1432
|
+
}));
|
|
1433
|
+
throw error;
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
process.stdout.write('${name} database migrations completed for ' + stage + '.\\n');
|
|
1437
|
+
`,
|
|
1438
|
+
'sst-env.d.ts': `// Bootstrap placeholder. SST overwrites this file after install, dev, or deploy.
|
|
1439
|
+
export {};
|
|
1440
|
+
`,
|
|
1441
|
+
'sst.config.ts': `export default $config({
|
|
1442
|
+
app(input: { stage?: string } | undefined) {
|
|
1443
|
+
const production = input?.stage === 'production';
|
|
1444
|
+
return {
|
|
1445
|
+
name: '${name}',
|
|
1446
|
+
removal: production ? 'retain' : 'remove',
|
|
1447
|
+
protect: production,
|
|
1448
|
+
home: 'aws',
|
|
1449
|
+
providers: {
|
|
1450
|
+
aws: { region: process.env.AWS_REGION ?? 'us-east-1' },
|
|
1451
|
+
},
|
|
1452
|
+
};
|
|
1453
|
+
},
|
|
1454
|
+
async run() {
|
|
1455
|
+
const { app } = await import('./infra/index.js');
|
|
1456
|
+
return app();
|
|
1457
|
+
},
|
|
1458
|
+
});
|
|
1459
|
+
`,
|
|
1460
|
+
'tsconfig.json': json({
|
|
1461
|
+
extends: '../../tsconfig.base.json',
|
|
1462
|
+
compilerOptions: {
|
|
1463
|
+
allowJs: true,
|
|
1464
|
+
esModuleInterop: true,
|
|
1465
|
+
jsx: 'react-jsx',
|
|
1466
|
+
lib: ['dom', 'dom.iterable', 'es2022'],
|
|
1467
|
+
plugins: [{ name: 'next' }],
|
|
1468
|
+
resolveJsonModule: true,
|
|
1469
|
+
types: ['node'],
|
|
1470
|
+
},
|
|
1471
|
+
include: [
|
|
1472
|
+
'contracts/**/*.ts',
|
|
1473
|
+
'database/**/*.ts',
|
|
1474
|
+
'api/**/*.ts',
|
|
1475
|
+
'infra/**/*.ts',
|
|
1476
|
+
'jobs/**/*.ts',
|
|
1477
|
+
'services/**/*.ts',
|
|
1478
|
+
'web/**/*.ts',
|
|
1479
|
+
'web/**/*.tsx',
|
|
1480
|
+
'vitest.config.ts',
|
|
1481
|
+
'sst.config.ts',
|
|
1482
|
+
'sst-env.d.ts',
|
|
1483
|
+
'ebk-globals.d.ts',
|
|
1484
|
+
],
|
|
1485
|
+
exclude: ['.sst', 'dist', 'node_modules'],
|
|
1486
|
+
}),
|
|
1487
|
+
...renderStandardProductFiles(name, pascalName),
|
|
1488
|
+
};
|
|
1489
|
+
}
|
|
1490
|
+
function classifyPaths(files) {
|
|
1491
|
+
const seededPaths = new Set([
|
|
1492
|
+
'README.md',
|
|
1493
|
+
'database/bindings.json',
|
|
1494
|
+
'database/schema.ts',
|
|
1495
|
+
'web/src/app/globals.css',
|
|
1496
|
+
'web/src/app/layout.tsx',
|
|
1497
|
+
'web/src/app/page.spec.tsx',
|
|
1498
|
+
'web/src/app/page.tsx',
|
|
1499
|
+
'web/src/app/password-reset/page.tsx',
|
|
1500
|
+
'web/src/app/api/protected/route.ts',
|
|
1501
|
+
'web/src/app/sign-in/page.tsx',
|
|
1502
|
+
]);
|
|
1503
|
+
const seededPrefixes = [
|
|
1504
|
+
'api/',
|
|
1505
|
+
'contracts/',
|
|
1506
|
+
'database/migrations/',
|
|
1507
|
+
'docs/',
|
|
1508
|
+
'e2e/',
|
|
1509
|
+
];
|
|
1510
|
+
const toolOwnedPaths = new Set(['sst-env.d.ts', 'web/next-env.d.ts']);
|
|
1511
|
+
return Object.entries(files)
|
|
1512
|
+
.filter(([path]) => path !== '.ebk/ownership.json' && !toolOwnedPaths.has(path))
|
|
1513
|
+
.map(([path, contents]) => {
|
|
1514
|
+
const mode = seededPaths.has(path) ||
|
|
1515
|
+
seededPrefixes.some((prefix) => path.startsWith(prefix))
|
|
1516
|
+
? 'seeded'
|
|
1517
|
+
: 'managed';
|
|
1518
|
+
const managedJsonPointers = path === 'package.json'
|
|
1519
|
+
? [...exports.PRODUCT_PACKAGE_MANAGED_JSON_POINTERS]
|
|
1520
|
+
: undefined;
|
|
1521
|
+
if (managedJsonPointers) {
|
|
1522
|
+
(0, index_js_3.assertManagedJsonPointersExist)(contents, managedJsonPointers, `Built-in Standard Blueprint ${path}`);
|
|
1523
|
+
}
|
|
1524
|
+
return {
|
|
1525
|
+
...(managedJsonPointers ? { managedJsonPointers } : {}),
|
|
1526
|
+
mode,
|
|
1527
|
+
origin: 'blueprint',
|
|
1528
|
+
path,
|
|
1529
|
+
sha256: (0, index_js_3.hashOwnedContent)(contents, { managedJsonPointers }),
|
|
1530
|
+
};
|
|
1531
|
+
})
|
|
1532
|
+
.sort((left, right) => left.path.localeCompare(right.path));
|
|
1533
|
+
}
|
|
1534
|
+
function createBuiltInSlicePlan(name, scope, runtimeVersion, manifest, inputs) {
|
|
1535
|
+
const provisionalOwnership = {
|
|
1536
|
+
blueprint: { name: manifest.name, version: manifest.version },
|
|
1537
|
+
contracts: { consumes: [], publishes: [`${scope}/${name}/contracts`] },
|
|
1538
|
+
foundation: manifest.foundation,
|
|
1539
|
+
inputs,
|
|
1540
|
+
paths: [],
|
|
1541
|
+
projects: [name],
|
|
1542
|
+
schemaVersion: index_js_3.GENERATED_OWNERSHIP_SCHEMA_VERSION,
|
|
1543
|
+
slice: name,
|
|
1544
|
+
verification: manifest.verification,
|
|
1545
|
+
};
|
|
1546
|
+
const provisionalFiles = renderSliceFiles(name, scope, runtimeVersion, provisionalOwnership);
|
|
1547
|
+
const ownership = {
|
|
1548
|
+
...provisionalOwnership,
|
|
1549
|
+
paths: classifyPaths(provisionalFiles),
|
|
1550
|
+
};
|
|
1551
|
+
return {
|
|
1552
|
+
files: renderSliceFiles(name, scope, runtimeVersion, ownership),
|
|
1553
|
+
ownership,
|
|
1554
|
+
};
|
|
1555
|
+
}
|
|
1556
|
+
async function sliceGenerator(tree, options) {
|
|
1557
|
+
const name = requireSliceName(options.name);
|
|
1558
|
+
const root = node_path_1.posix.join('packages', name);
|
|
1559
|
+
if (tree.exists(root)) {
|
|
1560
|
+
throw new Error(`Product Slice ${name} already exists at ${root}.`);
|
|
1561
|
+
}
|
|
1562
|
+
const packageJson = readJson(tree, 'package.json', 'Workspace package.json');
|
|
1563
|
+
const blueprint = (0, index_js_1.resolveBlueprint)({
|
|
1564
|
+
localPackages: packageJson.ebk?.blueprints?.localPackages,
|
|
1565
|
+
reference: options.blueprint,
|
|
1566
|
+
trust: {
|
|
1567
|
+
packages: packageJson.ebk?.blueprints?.trustedPackages ?? [],
|
|
1568
|
+
},
|
|
1569
|
+
workspaceRoot: tree.root,
|
|
1570
|
+
});
|
|
1571
|
+
const inputResult = (0, index_js_1.validateBlueprintInputs)(blueprint.inputSchema, parseBlueprintInputs(options.inputs));
|
|
1572
|
+
if (!inputResult.valid) {
|
|
1573
|
+
throw new Error(`Blueprint inputs are invalid:\n${inputResult.issues
|
|
1574
|
+
.map((issue) => `- ${issue.path}: ${issue.message}`)
|
|
1575
|
+
.join('\n')}`);
|
|
1576
|
+
}
|
|
1577
|
+
const foundationDescriptor = readJson(tree, 'packages/foundation/contracts/foundation.json', 'Standard Foundation descriptor');
|
|
1578
|
+
const compatibility = (0, index_js_2.validateFoundationCompatibility)(blueprint.manifest.foundation, foundationDescriptor);
|
|
1579
|
+
if (!compatibility.valid) {
|
|
1580
|
+
throw new Error(`Standard Foundation is incompatible with this Blueprint:\n${compatibility.issues
|
|
1581
|
+
.map((issue) => `- ${issue.path}: ${issue.message}`)
|
|
1582
|
+
.join('\n')}`);
|
|
1583
|
+
}
|
|
1584
|
+
const scope = workspaceScope(packageJson);
|
|
1585
|
+
if (blueprint.source !== 'built-in') {
|
|
1586
|
+
if (!blueprint.generatorPath) {
|
|
1587
|
+
throw new Error('Resolved external Blueprint has no generator entry point.');
|
|
1588
|
+
}
|
|
1589
|
+
const generated = await (0, index_js_1.executeBlueprintGenerator)(blueprint.generatorPath, {
|
|
1590
|
+
foundation: compatibility.value,
|
|
1591
|
+
inputs: inputResult.value,
|
|
1592
|
+
manifest: blueprint.manifest,
|
|
1593
|
+
name,
|
|
1594
|
+
root,
|
|
1595
|
+
workspace: {
|
|
1596
|
+
name: packageJson.name ?? '',
|
|
1597
|
+
scope,
|
|
1598
|
+
},
|
|
1599
|
+
});
|
|
1600
|
+
const ownershipPaths = generated.files.map(({ contents, managedJsonPointers, mode, path }) => ({
|
|
1601
|
+
...(managedJsonPointers ? { managedJsonPointers } : {}),
|
|
1602
|
+
mode,
|
|
1603
|
+
origin: 'blueprint',
|
|
1604
|
+
path,
|
|
1605
|
+
sha256: (0, index_js_3.hashOwnedContent)(contents, { managedJsonPointers }),
|
|
1606
|
+
}));
|
|
1607
|
+
ownershipPaths.sort((left, right) => left.path.localeCompare(right.path));
|
|
1608
|
+
const ownership = {
|
|
1609
|
+
blueprint: {
|
|
1610
|
+
name: blueprint.manifest.name,
|
|
1611
|
+
version: blueprint.manifest.version,
|
|
1612
|
+
},
|
|
1613
|
+
contracts: generated.contracts,
|
|
1614
|
+
foundation: blueprint.manifest.foundation,
|
|
1615
|
+
inputs: inputResult.value,
|
|
1616
|
+
paths: ownershipPaths,
|
|
1617
|
+
projects: generated.projects,
|
|
1618
|
+
schemaVersion: index_js_3.GENERATED_OWNERSHIP_SCHEMA_VERSION,
|
|
1619
|
+
slice: name,
|
|
1620
|
+
verification: blueprint.manifest.verification,
|
|
1621
|
+
};
|
|
1622
|
+
const ownershipValidation = (0, index_js_3.validateGeneratedOwnershipRecord)(ownership);
|
|
1623
|
+
if (!ownershipValidation.valid) {
|
|
1624
|
+
throw new Error(`Generated ownership metadata is invalid:\n${ownershipValidation.issues
|
|
1625
|
+
.map((issue) => `- ${issue.path}: ${issue.message}`)
|
|
1626
|
+
.join('\n')}`);
|
|
1627
|
+
}
|
|
1628
|
+
for (const file of generated.files) {
|
|
1629
|
+
tree.write(node_path_1.posix.join(root, file.path), file.contents);
|
|
1630
|
+
}
|
|
1631
|
+
tree.write(node_path_1.posix.join(root, '.ebk/ownership.json'), json(ownershipValidation.value));
|
|
1632
|
+
if (blueprint.source === 'installed') {
|
|
1633
|
+
packageJson.devDependencies = {
|
|
1634
|
+
...packageJson.devDependencies,
|
|
1635
|
+
[blueprint.manifest.name]: blueprint.manifest.version,
|
|
1636
|
+
};
|
|
1637
|
+
tree.write('package.json', json(packageJson));
|
|
1638
|
+
}
|
|
1639
|
+
if (!options.skipFormat) {
|
|
1640
|
+
await (0, devkit_1.formatFiles)(tree);
|
|
1641
|
+
}
|
|
1642
|
+
(0, index_js_3.restampProductSliceOwnership)(tree, name);
|
|
1643
|
+
if (!options.skipFormat) {
|
|
1644
|
+
await (0, devkit_1.formatFiles)(tree);
|
|
1645
|
+
}
|
|
1646
|
+
return options.skipInstall
|
|
1647
|
+
? undefined
|
|
1648
|
+
: () => (0, devkit_1.installPackagesTask)(tree, true);
|
|
1649
|
+
}
|
|
1650
|
+
const runtimeVersion = packageJson.dependencies?.['@empire-builder-kit/runtime'];
|
|
1651
|
+
if (!runtimeVersion) {
|
|
1652
|
+
throw new Error('Workspace package.json must depend on @empire-builder-kit/runtime before generating a Product Slice.');
|
|
1653
|
+
}
|
|
1654
|
+
const plan = createBuiltInSlicePlan(name, scope, runtimeVersion, blueprint.manifest, inputResult.value);
|
|
1655
|
+
const ownershipValidation = (0, index_js_3.validateGeneratedOwnershipRecord)(plan.ownership);
|
|
1656
|
+
if (!ownershipValidation.valid) {
|
|
1657
|
+
throw new Error(`Generated ownership metadata is invalid:\n${ownershipValidation.issues
|
|
1658
|
+
.map((issue) => `- ${issue.path}: ${issue.message}`)
|
|
1659
|
+
.join('\n')}`);
|
|
1660
|
+
}
|
|
1661
|
+
for (const [relativePath, contents] of Object.entries(plan.files)) {
|
|
1662
|
+
tree.write(node_path_1.posix.join(root, relativePath), contents);
|
|
1663
|
+
}
|
|
1664
|
+
if (!options.skipFormat) {
|
|
1665
|
+
await (0, devkit_1.formatFiles)(tree);
|
|
1666
|
+
}
|
|
1667
|
+
(0, index_js_3.restampProductSliceOwnership)(tree, name);
|
|
1668
|
+
if (!options.skipFormat) {
|
|
1669
|
+
await (0, devkit_1.formatFiles)(tree);
|
|
1670
|
+
}
|
|
1671
|
+
return options.skipInstall
|
|
1672
|
+
? undefined
|
|
1673
|
+
: () => (0, devkit_1.installPackagesTask)(tree, true);
|
|
1674
|
+
}
|
|
1675
|
+
exports.default = sliceGenerator;
|
|
1676
|
+
//# sourceMappingURL=slice.js.map
|