@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.
Files changed (185) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +5 -0
  3. package/README.md +25 -0
  4. package/executors.json +54 -0
  5. package/generators.json +35 -0
  6. package/migrations/standard-pre-v1-to-v1.md +11 -0
  7. package/package.json +66 -0
  8. package/src/blueprints/execution.d.ts +31 -0
  9. package/src/blueprints/execution.js +166 -0
  10. package/src/blueprints/execution.js.map +1 -0
  11. package/src/blueprints/index.d.ts +5 -0
  12. package/src/blueprints/index.js +9 -0
  13. package/src/blueprints/index.js.map +1 -0
  14. package/src/blueprints/inputs.d.ts +43 -0
  15. package/src/blueprints/inputs.js +103 -0
  16. package/src/blueprints/inputs.js.map +1 -0
  17. package/src/blueprints/manifest.d.ts +241 -0
  18. package/src/blueprints/manifest.js +369 -0
  19. package/src/blueprints/manifest.js.map +1 -0
  20. package/src/blueprints/resolver.d.ts +28 -0
  21. package/src/blueprints/resolver.js +214 -0
  22. package/src/blueprints/resolver.js.map +1 -0
  23. package/src/blueprints/transitions.d.ts +20 -0
  24. package/src/blueprints/transitions.js +61 -0
  25. package/src/blueprints/transitions.js.map +1 -0
  26. package/src/executors/deploy/deploy.d.ts +4 -0
  27. package/src/executors/deploy/deploy.js +6 -0
  28. package/src/executors/deploy/deploy.js.map +1 -0
  29. package/src/executors/dev/dev.d.ts +4 -0
  30. package/src/executors/dev/dev.js +6 -0
  31. package/src/executors/dev/dev.js.map +1 -0
  32. package/src/executors/dev-doctor/dev-doctor.d.ts +18 -0
  33. package/src/executors/dev-doctor/dev-doctor.js +145 -0
  34. package/src/executors/dev-doctor/dev-doctor.js.map +1 -0
  35. package/src/executors/dev-doctor/schema.json +17 -0
  36. package/src/executors/dev-setup/dev-setup.d.ts +11 -0
  37. package/src/executors/dev-setup/dev-setup.js +454 -0
  38. package/src/executors/dev-setup/dev-setup.js.map +1 -0
  39. package/src/executors/dev-setup/schema.json +16 -0
  40. package/src/executors/fleet/fleet.d.ts +51 -0
  41. package/src/executors/fleet/fleet.js +529 -0
  42. package/src/executors/fleet/fleet.js.map +1 -0
  43. package/src/executors/fleet/schema.json +25 -0
  44. package/src/executors/foundation-preflight/foundation-preflight.d.ts +15 -0
  45. package/src/executors/foundation-preflight/foundation-preflight.js +94 -0
  46. package/src/executors/foundation-preflight/foundation-preflight.js.map +1 -0
  47. package/src/executors/foundation-preflight/schema.json +31 -0
  48. package/src/executors/foundation-remove-preflight/foundation-remove-preflight.d.ts +13 -0
  49. package/src/executors/foundation-remove-preflight/foundation-remove-preflight.js +69 -0
  50. package/src/executors/foundation-remove-preflight/foundation-remove-preflight.js.map +1 -0
  51. package/src/executors/foundation-remove-preflight/schema.json +17 -0
  52. package/src/executors/refresh/refresh.d.ts +4 -0
  53. package/src/executors/refresh/refresh.js +6 -0
  54. package/src/executors/refresh/refresh.js.map +1 -0
  55. package/src/executors/remove/remove.d.ts +4 -0
  56. package/src/executors/remove/remove.js +6 -0
  57. package/src/executors/remove/remove.js.map +1 -0
  58. package/src/executors/sst-lifecycle/run-sst-command.d.ts +28 -0
  59. package/src/executors/sst-lifecycle/run-sst-command.js +154 -0
  60. package/src/executors/sst-lifecycle/run-sst-command.js.map +1 -0
  61. package/src/executors/sst-lifecycle/schema.json +45 -0
  62. package/src/executors/unlock/unlock.d.ts +4 -0
  63. package/src/executors/unlock/unlock.js +6 -0
  64. package/src/executors/unlock/unlock.js.map +1 -0
  65. package/src/foundation/aurora-logical-database.d.ts +76 -0
  66. package/src/foundation/aurora-logical-database.js +419 -0
  67. package/src/foundation/aurora-logical-database.js.map +1 -0
  68. package/src/foundation/discovery.d.ts +55 -0
  69. package/src/foundation/discovery.js +55 -0
  70. package/src/foundation/discovery.js.map +1 -0
  71. package/src/foundation/index.d.ts +6 -0
  72. package/src/foundation/index.js +10 -0
  73. package/src/foundation/index.js.map +1 -0
  74. package/src/foundation/local-database-contract.d.ts +30 -0
  75. package/src/foundation/local-database-contract.js +75 -0
  76. package/src/foundation/local-database-contract.js.map +1 -0
  77. package/src/foundation/preflight.d.ts +31 -0
  78. package/src/foundation/preflight.js +114 -0
  79. package/src/foundation/preflight.js.map +1 -0
  80. package/src/foundation/provision-database-bindings.d.ts +30 -0
  81. package/src/foundation/provision-database-bindings.js +113 -0
  82. package/src/foundation/provision-database-bindings.js.map +1 -0
  83. package/src/foundation/provision-logical-database.d.ts +26 -0
  84. package/src/foundation/provision-logical-database.js +162 -0
  85. package/src/foundation/provision-logical-database.js.map +1 -0
  86. package/src/foundation/requirements.d.ts +37 -0
  87. package/src/foundation/requirements.js +141 -0
  88. package/src/foundation/requirements.js.map +1 -0
  89. package/src/foundation/stages.d.ts +14 -0
  90. package/src/foundation/stages.js +51 -0
  91. package/src/foundation/stages.js.map +1 -0
  92. package/src/generators/function/function.d.ts +13 -0
  93. package/src/generators/function/function.js +71 -0
  94. package/src/generators/function/function.js.map +1 -0
  95. package/src/generators/function/schema.d.ts +5 -0
  96. package/src/generators/function/schema.json +20 -0
  97. package/src/generators/job/job.d.ts +4 -0
  98. package/src/generators/job/job.js +5 -0
  99. package/src/generators/job/job.js.map +1 -0
  100. package/src/generators/preset/generator.d.ts +5 -0
  101. package/src/generators/preset/generator.js +41 -0
  102. package/src/generators/preset/generator.js.map +1 -0
  103. package/src/generators/preset/schema.d.ts +4 -0
  104. package/src/generators/preset/schema.json +22 -0
  105. package/src/generators/preset/workspace-files.d.ts +18 -0
  106. package/src/generators/preset/workspace-files.js +895 -0
  107. package/src/generators/preset/workspace-files.js.map +1 -0
  108. package/src/generators/service/service.d.ts +4 -0
  109. package/src/generators/service/service.js +5 -0
  110. package/src/generators/service/service.js.map +1 -0
  111. package/src/generators/slice/schema.d.ts +7 -0
  112. package/src/generators/slice/schema.json +38 -0
  113. package/src/generators/slice/slice.d.ts +26 -0
  114. package/src/generators/slice/slice.js +1676 -0
  115. package/src/generators/slice/slice.js.map +1 -0
  116. package/src/generators/upgrade/schema.d.ts +8 -0
  117. package/src/generators/upgrade/schema.json +31 -0
  118. package/src/generators/upgrade/upgrade.d.ts +27 -0
  119. package/src/generators/upgrade/upgrade.js +666 -0
  120. package/src/generators/upgrade/upgrade.js.map +1 -0
  121. package/src/generators/workload/schema.d.ts +6 -0
  122. package/src/generators/workload/schema.json +21 -0
  123. package/src/generators/workload/workload.d.ts +16 -0
  124. package/src/generators/workload/workload.js +157 -0
  125. package/src/generators/workload/workload.js.map +1 -0
  126. package/src/index.d.ts +5 -0
  127. package/src/index.js +9 -0
  128. package/src/index.js.map +1 -0
  129. package/src/local-dev/contracts.d.ts +31 -0
  130. package/src/local-dev/contracts.js +126 -0
  131. package/src/local-dev/contracts.js.map +1 -0
  132. package/src/local-dev/gateway-daemon.d.ts +4 -0
  133. package/src/local-dev/gateway-daemon.js +39 -0
  134. package/src/local-dev/gateway-daemon.js.map +1 -0
  135. package/src/local-dev/gateway.d.ts +60 -0
  136. package/src/local-dev/gateway.js +368 -0
  137. package/src/local-dev/gateway.js.map +1 -0
  138. package/src/local-dev/identity.d.ts +16 -0
  139. package/src/local-dev/identity.js +101 -0
  140. package/src/local-dev/identity.js.map +1 -0
  141. package/src/local-dev/index.d.ts +7 -0
  142. package/src/local-dev/index.js +11 -0
  143. package/src/local-dev/index.js.map +1 -0
  144. package/src/local-dev/portless.d.ts +15 -0
  145. package/src/local-dev/portless.js +115 -0
  146. package/src/local-dev/portless.js.map +1 -0
  147. package/src/local-dev/ports.d.ts +10 -0
  148. package/src/local-dev/ports.js +39 -0
  149. package/src/local-dev/ports.js.map +1 -0
  150. package/src/local-dev/proxy.d.ts +37 -0
  151. package/src/local-dev/proxy.js +271 -0
  152. package/src/local-dev/proxy.js.map +1 -0
  153. package/src/local-dev/tls.d.ts +24 -0
  154. package/src/local-dev/tls.js +294 -0
  155. package/src/local-dev/tls.js.map +1 -0
  156. package/src/ownership/composition.d.ts +16 -0
  157. package/src/ownership/composition.js +125 -0
  158. package/src/ownership/composition.js.map +1 -0
  159. package/src/ownership/foundation-record.d.ts +66 -0
  160. package/src/ownership/foundation-record.js +108 -0
  161. package/src/ownership/foundation-record.js.map +1 -0
  162. package/src/ownership/hash.d.ts +2 -0
  163. package/src/ownership/hash.js +10 -0
  164. package/src/ownership/hash.js.map +1 -0
  165. package/src/ownership/index.d.ts +6 -0
  166. package/src/ownership/index.js +10 -0
  167. package/src/ownership/index.js.map +1 -0
  168. package/src/ownership/json-ownership.d.ts +13 -0
  169. package/src/ownership/json-ownership.js +147 -0
  170. package/src/ownership/json-ownership.js.map +1 -0
  171. package/src/ownership/record.d.ts +186 -0
  172. package/src/ownership/record.js +336 -0
  173. package/src/ownership/record.js.map +1 -0
  174. package/src/ownership/workspace-record.d.ts +66 -0
  175. package/src/ownership/workspace-record.js +108 -0
  176. package/src/ownership/workspace-record.js.map +1 -0
  177. package/src/schemas/blueprint-input-schema-v1.json +23 -0
  178. package/src/schemas/blueprint-v1.json +170 -0
  179. package/src/schemas/foundation-ownership-v2.json +42 -0
  180. package/src/schemas/foundation-requirement-v1.json +32 -0
  181. package/src/schemas/generated-ownership-v2.json +117 -0
  182. package/src/schemas/workspace-ownership-v2.json +42 -0
  183. package/src/validation.d.ts +14 -0
  184. package/src/validation.js +14 -0
  185. 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