@mettlecast/domain-cli 0.2.60 → 0.2.62

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 (107) hide show
  1. package/dist/builder/build-registry.d.ts +1 -1
  2. package/dist/builder/build-registry.js +1 -36
  3. package/dist/builder/build-types.d.ts +1 -1
  4. package/dist/builder/load-module.d.ts +1 -1
  5. package/dist/cli.js +21 -3
  6. package/dist/commands/add-api.js +2 -2
  7. package/dist/commands/add-domain.js +4 -4
  8. package/dist/commands/add-fixture-factory.js +5 -6
  9. package/dist/commands/build-catalog.d.ts +6 -22
  10. package/dist/commands/build-catalog.js +7 -18
  11. package/dist/commands/build.js +2 -1
  12. package/dist/commands/check-hashes.d.ts +2 -0
  13. package/dist/commands/check-hashes.js +8 -0
  14. package/dist/commands/dev.js +1 -1
  15. package/dist/commands/doctor.js +70 -41
  16. package/dist/commands/explain.js +13 -13
  17. package/dist/commands/generate-openapi.d.ts +10 -1
  18. package/dist/commands/generate-openapi.js +19 -33
  19. package/dist/commands/regenerate-modules-hashes.d.ts +25 -0
  20. package/dist/commands/regenerate-modules-hashes.js +58 -0
  21. package/dist/commands/show.d.ts +2 -3
  22. package/dist/commands/show.js +0 -2
  23. package/dist/commands/test.js +0 -1
  24. package/dist/commands/update-all.d.ts +21 -0
  25. package/dist/commands/update-all.js +62 -0
  26. package/dist/commands/upgrade-backend.js +3 -3
  27. package/dist/commands/validate.js +12 -90
  28. package/dist/server/api-server.d.ts +1 -1
  29. package/dist/server/mount-routes.d.ts +11 -2
  30. package/dist/server/mount-routes.js +20 -8
  31. package/dist/templates/api-skeleton.d.ts +5 -0
  32. package/dist/templates/api-skeleton.js +28 -27
  33. package/dist/templates/claude-md.js +1 -1
  34. package/dist/templates/patterns/api/create-with-event.d.ts +4 -0
  35. package/dist/templates/patterns/api/create-with-event.js +38 -32
  36. package/dist/templates/patterns/api/idempotent-mutation.d.ts +4 -0
  37. package/dist/templates/patterns/api/idempotent-mutation.js +47 -41
  38. package/dist/templates/patterns/api/paginated-list.d.ts +4 -0
  39. package/dist/templates/patterns/api/paginated-list.js +30 -24
  40. package/dist/templates/patterns/api/simple-crud.d.ts +4 -0
  41. package/dist/templates/patterns/api/simple-crud.js +46 -35
  42. package/dist/templates/patterns/api/streaming-list.d.ts +4 -0
  43. package/dist/templates/patterns/api/streaming-list.js +46 -41
  44. package/dist/templates/patterns/api/system-admin.d.ts +4 -0
  45. package/dist/templates/patterns/api/system-admin.js +59 -52
  46. package/dist/templates/patterns/api/webhook-receiver-style.d.ts +4 -0
  47. package/dist/templates/patterns/api/webhook-receiver-style.js +43 -35
  48. package/dist/types.d.ts +100 -0
  49. package/dist/types.js +1 -0
  50. package/dist/utils/file-helpers.d.ts +0 -2
  51. package/dist/utils/file-helpers.js +2 -3
  52. package/dist/utils/manifest.d.ts +1 -0
  53. package/dist/utils/manifest.js +16 -1
  54. package/dist/utils/scaffold-config.d.ts +6 -0
  55. package/dist/utils/scaffold-config.js +2 -0
  56. package/package.json +1 -1
  57. package/src/__tests__/build-registry.test.ts +43 -20
  58. package/src/__tests__/build-types.test.ts +4 -7
  59. package/src/__tests__/builder/walkDomainDir.test.ts +19 -21
  60. package/src/__tests__/commands/add-api.test.ts +12 -10
  61. package/src/__tests__/commands/add-domain.test.ts +8 -5
  62. package/src/__tests__/commands/build-flows.test.ts +55 -0
  63. package/src/__tests__/commands/check-hashes.test.ts +31 -0
  64. package/src/__tests__/commands/create-project.test.ts +5 -5
  65. package/src/__tests__/commands/dev.test.ts +0 -1
  66. package/src/__tests__/commands/regenerate-modules-hashes.test.ts +170 -0
  67. package/src/__tests__/commands/update-all.test.ts +322 -0
  68. package/src/__tests__/doctor.test.ts +73 -0
  69. package/src/__tests__/mount-routes.test.ts +64 -23
  70. package/src/__tests__/package-freshness.test.ts +1 -21
  71. package/src/__tests__/smoke/scaffold.test.ts +13 -15
  72. package/src/__tests__/utils/manifest.test.ts +128 -0
  73. package/src/__tests__/validate.test.ts +21 -103
  74. package/src/builder/build-registry.ts +7 -44
  75. package/src/builder/build-types.ts +1 -1
  76. package/src/cli.ts +23 -3
  77. package/src/commands/add-api.ts +2 -2
  78. package/src/commands/add-domain.ts +4 -4
  79. package/src/commands/add-fixture-factory.ts +5 -6
  80. package/src/commands/build-catalog.ts +13 -35
  81. package/src/commands/build.ts +3 -2
  82. package/src/commands/check-hashes.ts +12 -0
  83. package/src/commands/dev.ts +1 -1
  84. package/src/commands/doctor.ts +72 -41
  85. package/src/commands/explain.ts +13 -13
  86. package/src/commands/generate-openapi.ts +30 -52
  87. package/src/commands/regenerate-modules-hashes.ts +89 -0
  88. package/src/commands/show.ts +2 -5
  89. package/src/commands/test.ts +0 -1
  90. package/src/commands/update-all.ts +79 -0
  91. package/src/commands/upgrade-backend.ts +3 -3
  92. package/src/commands/validate.ts +11 -96
  93. package/src/server/api-server.ts +1 -1
  94. package/src/server/mount-routes.ts +21 -10
  95. package/src/templates/api-skeleton.ts +29 -28
  96. package/src/templates/claude-md.ts +1 -1
  97. package/src/templates/patterns/api/create-with-event.ts +39 -33
  98. package/src/templates/patterns/api/idempotent-mutation.ts +48 -42
  99. package/src/templates/patterns/api/paginated-list.ts +31 -25
  100. package/src/templates/patterns/api/simple-crud.ts +47 -36
  101. package/src/templates/patterns/api/streaming-list.ts +47 -42
  102. package/src/templates/patterns/api/system-admin.ts +60 -53
  103. package/src/templates/patterns/api/webhook-receiver-style.ts +48 -40
  104. package/src/types.ts +128 -0
  105. package/src/utils/file-helpers.ts +2 -5
  106. package/src/utils/manifest.ts +16 -1
  107. package/src/utils/scaffold-config.ts +9 -0
@@ -1,7 +1,12 @@
1
1
  /**
2
2
  * generate-openapi — read a domain's registry and produce an OpenAPI 3.1
3
3
  * specification as JSON. The registry is consumed at build time; each
4
- * `defineApi` entry contributes one path under the domain's route prefix.
4
+ * `defineAction({ exposure: { type: 'api', ... } })` entry contributes
5
+ * one path under the domain's route prefix.
6
+ *
7
+ * Issue #4689: the legacy `defineApi` factory was removed and the
8
+ * `registry.apis` slot is now always empty in new registries. This
9
+ * command therefore reads exclusively from `actions[]`.
5
10
  *
6
11
  * Usage: npx mc-domain-module generate-openapi <domain>
7
12
  */
@@ -11,6 +16,10 @@ export interface GenerateOpenapiOptions {
11
16
  /** Output path. Defaults to domains/<domain>/api/openapi.generated.json */
12
17
  output?: string;
13
18
  }
19
+ /**
20
+ * Run the generate-openapi command: read the domain's registry and emit
21
+ * an OpenAPI 3.1 spec covering every API-exposed action.
22
+ */
14
23
  export declare function runGenerateOpenapi(options: GenerateOpenapiOptions): Promise<string>;
15
24
  /**
16
25
  * CLI entry point.
@@ -1,7 +1,12 @@
1
1
  /**
2
2
  * generate-openapi — read a domain's registry and produce an OpenAPI 3.1
3
3
  * specification as JSON. The registry is consumed at build time; each
4
- * `defineApi` entry contributes one path under the domain's route prefix.
4
+ * `defineAction({ exposure: { type: 'api', ... } })` entry contributes
5
+ * one path under the domain's route prefix.
6
+ *
7
+ * Issue #4689: the legacy `defineApi` factory was removed and the
8
+ * `registry.apis` slot is now always empty in new registries. This
9
+ * command therefore reads exclusively from `actions[]`.
5
10
  *
6
11
  * Usage: npx mc-domain-module generate-openapi <domain>
7
12
  */
@@ -9,29 +14,9 @@ import { readFile, writeFile, mkdir } from 'node:fs/promises';
9
14
  import { join } from 'node:path';
10
15
  import { cliLogger } from '../utils/logger.js';
11
16
  /**
12
- * Extract a Zod schema shape from a registry entry version.
13
- * The registry may store schemas as `input`/`output` (raw Zod
14
- * objects) or `inputSchema`/`outputSchema` (JSON Schema shapes).
15
- * Returns a best-effort JSON Schema object for the OpenAPI spec.
17
+ * Run the generate-openapi command: read the domain's registry and emit
18
+ * an OpenAPI 3.1 spec covering every API-exposed action.
16
19
  */
17
- function extractSchema(version, field) {
18
- // Prefer JSON Schema if present
19
- const schemaField = field === 'input' ? version.inputSchema : version.outputSchema;
20
- if (schemaField && typeof schemaField === 'object' && schemaField !== null) {
21
- return schemaField;
22
- }
23
- // Fall back to the raw Zod shape — try to produce a minimal JSON
24
- // Schema from the Zod _def. For full support, add zod-to-json-schema
25
- // to the CLI dependencies and call `zodToJsonSchema(zodSchema)`.
26
- const raw = field === 'input' ? version.input : version.output;
27
- if (raw && typeof raw === 'object' && raw !== null) {
28
- // Attempt minimal mapping: if the Zod shape has a `type` field
29
- // from its _def, describe it as JSON Schema.
30
- const def = raw;
31
- return { type: def.type ?? 'object', properties: def.properties, required: def.required };
32
- }
33
- return { type: 'object' };
34
- }
35
20
  export async function runGenerateOpenapi(options) {
36
21
  const projectRoot = options.projectRoot ?? process.cwd();
37
22
  const domain = options.domain;
@@ -41,22 +26,23 @@ export async function runGenerateOpenapi(options) {
41
26
  const registryJson = await readFile(registryPath, 'utf8');
42
27
  const registry = JSON.parse(registryJson);
43
28
  const paths = {};
44
- for (const api of registry.apis) {
45
- const method = (api.method ?? 'get').toLowerCase();
46
- const fullPath = `/v1/${domain}${api.path}`;
47
- const v1 = api.versions['v1'] ?? api.versions[Object.keys(api.versions)[0]];
48
- if (!v1)
29
+ for (const action of registry.actions ?? []) {
30
+ if (action.exposure?.type !== 'api')
49
31
  continue;
32
+ const method = (action.exposure.method ?? 'get').toLowerCase();
33
+ const fullPath = `/v1/${domain}${action.exposure.path}`;
34
+ const inputSchema = action.inputSchema ?? { type: 'object' };
35
+ const outputSchema = action.outputSchema ?? { type: 'object' };
50
36
  if (!paths[fullPath])
51
37
  paths[fullPath] = {};
52
38
  paths[fullPath][method] = {
53
- operationId: `${domain}.${api.id}`,
54
- summary: `${domain}.${api.id}`,
55
- description: `Version: ${v1.status}`,
39
+ operationId: `${domain}.${action.id}`,
40
+ summary: `${domain}.${action.id}`,
41
+ description: 'Action-first contract (defineAction + exposure.type=api).',
56
42
  requestBody: {
57
43
  content: {
58
44
  'application/json': {
59
- schema: extractSchema(v1, 'input'),
45
+ schema: inputSchema,
60
46
  },
61
47
  },
62
48
  },
@@ -65,7 +51,7 @@ export async function runGenerateOpenapi(options) {
65
51
  description: 'OK',
66
52
  content: {
67
53
  'application/json': {
68
- schema: extractSchema(v1, 'output'),
54
+ schema: outputSchema,
69
55
  },
70
56
  },
71
57
  },
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Options for the regenerate-modules-hashes command.
3
+ */
4
+ export interface RegenerateModulesHashesOptions {
5
+ /** Root directory of the project (defaults to cwd). */
6
+ projectRoot?: string;
7
+ }
8
+ /**
9
+ * Manifest of infra/modules/ files with their SHA256 hashes.
10
+ */
11
+ export interface ModulesHashesManifest {
12
+ /** Schema version. */
13
+ version: '1';
14
+ /** ISO timestamp when the manifest was generated. */
15
+ generatedAt: string;
16
+ /** Map of relative file path to SHA256 hex digest. */
17
+ files: Record<string, string>;
18
+ }
19
+ /**
20
+ * Walk infra/modules/, compute a SHA256 for every file, and write the result
21
+ * to .mc/modules-hashes.json. Used to refresh the scaffold drift baseline.
22
+ * @param opts Options including project root directory.
23
+ * @returns Absolute path to the written manifest file.
24
+ */
25
+ export declare function runRegenerateModulesHashes(opts?: RegenerateModulesHashesOptions): Promise<string>;
@@ -0,0 +1,58 @@
1
+ import { readFile, readdir, writeFile, mkdir } from 'node:fs/promises';
2
+ import { join, relative } from 'node:path';
3
+ import { createHash } from 'node:crypto';
4
+ import { cliLogger } from '../utils/logger.js';
5
+ /**
6
+ * Walk infra/modules/, compute a SHA256 for every file, and write the result
7
+ * to .mc/modules-hashes.json. Used to refresh the scaffold drift baseline.
8
+ * @param opts Options including project root directory.
9
+ * @returns Absolute path to the written manifest file.
10
+ */
11
+ export async function runRegenerateModulesHashes(opts = {}) {
12
+ const root = opts.projectRoot ?? process.cwd();
13
+ const mcDir = join(root, '.mc');
14
+ const manifestPath = join(mcDir, 'modules-hashes.json');
15
+ const modulesDir = join(root, 'infra', 'modules');
16
+ // Files/directories to skip when walking infra/modules/
17
+ const SKIP_NAMES = new Set(['node_modules', 'dist', 'cdk.out', 'package-lock.json', '.npmrc']);
18
+ /** Normalize CRLF → LF so hashes match across platforms (Windows vs Linux). */
19
+ function normalizeLineEndings(buf) {
20
+ const str = buf.toString('utf8');
21
+ if (!str.includes('\r\n'))
22
+ return buf;
23
+ return Buffer.from(str.replace(/\r\n/g, '\n'), 'utf8');
24
+ }
25
+ // Walk infra/modules/ and collect every file path
26
+ async function walk(dir) {
27
+ const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
28
+ const files = [];
29
+ for (const entry of entries) {
30
+ if (SKIP_NAMES.has(entry.name))
31
+ continue;
32
+ const full = join(dir, entry.name);
33
+ if (entry.isDirectory())
34
+ files.push(...await walk(full));
35
+ else if (entry.isFile())
36
+ files.push(full);
37
+ }
38
+ return files;
39
+ }
40
+ const onDiskFiles = await walk(modulesDir);
41
+ // Compute SHA256 for each file (line-ending-normalised)
42
+ const files = {};
43
+ for (const absPath of onDiskFiles) {
44
+ const relPath = relative(root, absPath).replace(/\\/g, '/');
45
+ const content = await readFile(absPath);
46
+ const normalized = normalizeLineEndings(content);
47
+ files[relPath] = createHash('sha256').update(normalized).digest('hex');
48
+ }
49
+ const manifest = {
50
+ version: '1',
51
+ generatedAt: new Date().toISOString(),
52
+ files,
53
+ };
54
+ await mkdir(mcDir, { recursive: true });
55
+ await writeFile(manifestPath, JSON.stringify(manifest, null, 2), 'utf8');
56
+ cliLogger.info({ path: manifestPath, fileCount: Object.keys(files).length }, 'modules-hashes manifest regenerated');
57
+ return manifestPath;
58
+ }
@@ -13,10 +13,10 @@ export interface ShowDomainOptions {
13
13
  domainsDir?: string;
14
14
  }
15
15
  /**
16
- * Represents a primitive entry (API, subscriber, action, or job).
16
+ * Represents a primitive entry (action, subscriber, or job).
17
17
  */
18
18
  interface PrimitiveEntry {
19
- type: 'api' | 'subscriber' | 'action' | 'job';
19
+ type: 'subscriber' | 'action' | 'job';
20
20
  id: string;
21
21
  file: string;
22
22
  }
@@ -26,7 +26,6 @@ interface PrimitiveEntry {
26
26
  interface DomainSummary {
27
27
  domain: string;
28
28
  primitives: {
29
- apis: PrimitiveEntry[];
30
29
  subscribers: PrimitiveEntry[];
31
30
  actions: PrimitiveEntry[];
32
31
  jobs: PrimitiveEntry[];
@@ -74,7 +74,6 @@ export async function runShowDomain(opts) {
74
74
  throw new Error(`Domain "${opts.domain}" not found at ${domainDir}`);
75
75
  }
76
76
  // Scan primitives
77
- const apis = await scanPrimitives(join(domainDir, 'api'), 'api');
78
77
  const subscribers = await scanPrimitives(join(domainDir, 'subscribers'), 'subscriber');
79
78
  const actions = await scanPrimitives(join(domainDir, 'actions'), 'action');
80
79
  const jobs = await scanPrimitives(join(domainDir, 'jobs'), 'job');
@@ -113,7 +112,6 @@ export async function runShowDomain(opts) {
113
112
  const summary = {
114
113
  domain: opts.domain,
115
114
  primitives: {
116
- apis,
117
115
  subscribers,
118
116
  actions,
119
117
  jobs,
@@ -14,7 +14,6 @@ export async function runTest(options) {
14
14
  const fixturePath = resolve(options.fixturePath);
15
15
  const { registry } = await buildRegistry(domainRoot);
16
16
  const allEntries = [
17
- ...registry.apis,
18
17
  ...registry.webhooks,
19
18
  ...registry.subscribers,
20
19
  ...registry.schedules,
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Run the full scaffold update pipeline:
3
+ * 1. Build each domain registry from `.mc/scaffold-config.json`
4
+ * 2. Build the combined domain catalog
5
+ * 3. Build flows registry
6
+ * 4. Build UI manifests
7
+ * 5. Regenerate module hashes
8
+ * 6. Run doctor
9
+ *
10
+ * Per-domain build failures are logged and skipped so that one bad domain does
11
+ * not abort the whole pipeline. The remaining steps still run.
12
+ *
13
+ * @param opts - Optional projectRoot override. Defaults to cwd.
14
+ * @returns success derived from doctor pass/fail and a human-readable summary.
15
+ */
16
+ export declare function runUpdateAll(opts?: {
17
+ projectRoot?: string;
18
+ }): Promise<{
19
+ success: boolean;
20
+ summary: string;
21
+ }>;
@@ -0,0 +1,62 @@
1
+ import { join } from 'node:path';
2
+ import { cliLogger } from '../utils/logger.js';
3
+ import { readScaffoldConfig } from '../utils/scaffold-config.js';
4
+ import { runBuild } from './build.js';
5
+ import { runBuildCatalog } from './build-catalog.js';
6
+ import { runBuildFlows } from './build-flows.js';
7
+ import { runBuildUi } from './build-ui.js';
8
+ import { runRegenerateModulesHashes } from './regenerate-modules-hashes.js';
9
+ import { runDoctor } from './doctor.js';
10
+ /**
11
+ * Run the full scaffold update pipeline:
12
+ * 1. Build each domain registry from `.mc/scaffold-config.json`
13
+ * 2. Build the combined domain catalog
14
+ * 3. Build flows registry
15
+ * 4. Build UI manifests
16
+ * 5. Regenerate module hashes
17
+ * 6. Run doctor
18
+ *
19
+ * Per-domain build failures are logged and skipped so that one bad domain does
20
+ * not abort the whole pipeline. The remaining steps still run.
21
+ *
22
+ * @param opts - Optional projectRoot override. Defaults to cwd.
23
+ * @returns success derived from doctor pass/fail and a human-readable summary.
24
+ */
25
+ export async function runUpdateAll(opts) {
26
+ const projectRoot = opts?.projectRoot ?? process.cwd();
27
+ cliLogger.info({ projectRoot }, 'update-all: starting');
28
+ // 1. Read scaffold-config to discover domains
29
+ const scaffoldConfig = await readScaffoldConfig(projectRoot);
30
+ const domainIds = scaffoldConfig.domainIds ?? [];
31
+ cliLogger.info({ domainCount: domainIds.length }, 'update-all: building domain registries');
32
+ // 2. Build each domain. Per-domain failures are logged and skipped.
33
+ for (const domainId of domainIds) {
34
+ const domainRoot = join(projectRoot, 'domains', domainId);
35
+ try {
36
+ cliLogger.info({ domainId, domainRoot }, 'update-all: building domain');
37
+ await runBuild({ domainRoot });
38
+ }
39
+ catch (err) {
40
+ cliLogger.error({ domainId, err: err instanceof Error ? err.message : String(err) }, 'update-all: domain build failed — continuing');
41
+ }
42
+ }
43
+ // 3. Build catalog from .mc
44
+ const mcDir = join(projectRoot, '.mc');
45
+ cliLogger.info({ mcDir }, 'update-all: building domain catalog');
46
+ await runBuildCatalog(mcDir);
47
+ // 4. Build flows
48
+ cliLogger.info({ projectRoot }, 'update-all: building flows registry');
49
+ await runBuildFlows({ projectRoot });
50
+ // 5. Build UI manifests
51
+ cliLogger.info({ projectRoot }, 'update-all: building UI manifests');
52
+ await runBuildUi({ projectRoot });
53
+ // 6. Regenerate module hashes
54
+ cliLogger.info({ projectRoot }, 'update-all: regenerating module hashes');
55
+ await runRegenerateModulesHashes({ projectRoot });
56
+ // 7. Run doctor — final gate
57
+ cliLogger.info({ projectRoot }, 'update-all: running doctor');
58
+ const report = await runDoctor({ projectRoot });
59
+ cliLogger.info({ pass: report.pass, exitCode: report.exitCode, checkCount: report.checks.length }, 'update-all: doctor complete');
60
+ const summary = `Updated ${domainIds.length} domains; doctor ${report.pass ? 'PASS' : 'FAIL'}`;
61
+ return { success: report.pass, summary };
62
+ }
@@ -4,10 +4,10 @@ const MIGRATIONS = [
4
4
  {
5
5
  fromMajor: 1,
6
6
  toMajor: 2,
7
- description: 'Migrate defineApi from v1 to v2: rename `versions.v1.handler` shape',
7
+ description: 'Audit v1 domain backends for action-first API exposure requirements',
8
8
  transform(repoRoot, dryRun) {
9
- // Placeholder: real migration uses ts-morph to rewrite handler signatures
10
- console.log(`[G20] Would apply: defineApi v1→v2 migration in ${repoRoot}/domains/**`);
9
+ // Placeholder: real migration uses ts-morph to rewrite handler signatures.
10
+ console.log(`[G20] Would audit action-first API exposure in ${repoRoot}/domains/**`);
11
11
  if (!dryRun) {
12
12
  console.log('[G20] ts-morph transform: (not yet implemented — add jscodeshift transforms here)');
13
13
  }
@@ -90,37 +90,17 @@ async function checkRawPathViolations(_repoRoot, config) {
90
90
  * Each rule maps 1:1 to an aspect annotation code so downstream tooling
91
91
  * can correlate build-time and synth-time failures.
92
92
  *
93
- * NOTE on `SECURITY_MISSING_SECURITY_EXCEPTION` for legacy `defineApi`
94
- * entries: the registry builder does not propagate `securityException`
95
- * from legacy sourcethat field only exists on action-style API
96
- * exposures, which are covered by the existing `AUTH_NONE_REQUIRES_EXCEPTION`
97
- * rule in `checkActionFirstSecurity`. The aspect's corresponding check
98
- * (`assertAnonymousRoutesHaveException`) catches the legacy case at
99
- * synth time so a developer migrating to action-style APIs gets the
100
- * right guidance at the right layer.
93
+ * Issue #4689: the legacy `defineApi` factory and `registry.apis` field
94
+ * were removed. Deployment-time API invariants are enforced against the
95
+ * `actions[]` API-exposure surfacesee `checkActionFirstSecurity`.
101
96
  */
102
- function checkDeploymentSecurity(apis, actions) {
103
- const errors = [];
104
- for (const api of apis) {
105
- // Non-anonymous APIs whose path lacks the tenant placeholder cannot
106
- // bind `ctx.tenant.id` from the URL. The aspect surfaces this as
107
- // `SECURITY_MISSING_TENANT_PATH`; the CLI version is a faster gate so
108
- // CI doesn't pay the synth cost.
109
- if (api.authType !== 'none' && !api.path.includes('/v1/tenants/{tenantId}/')) {
110
- errors.push({
111
- code: 'SECURITY_MISSING_TENANT_PATH',
112
- message: `API '${api.id}' (${api.method} ${api.path}) is JWT-protected but its path does not include the canonical '/v1/tenants/{tenantId}/' placeholder. The runtime cannot bind ctx.tenant.id from a path that lacks the placeholder.`,
113
- });
114
- }
115
- }
97
+ function checkDeploymentSecurity(_actions) {
116
98
  // Action API exposures are already covered by `checkActionFirstSecurity`
117
99
  // for `tenancy: 'required'` paths and `auth: 'none'` exceptions. The
118
100
  // codes there (TENANT_API_PATH_REQUIRED, AUTH_NONE_REQUIRES_EXCEPTION)
119
101
  // are kept stable for back-compat — they map to the same aspect codes.
120
- // Suppress the unused-actions lint while keeping the parameter shape
121
- // for the future where actions gain new deployment-time invariants.
122
- void actions;
123
- return errors;
102
+ //
103
+ return [];
124
104
  }
125
105
  /**
126
106
  * Action-first security validation rules (#4619, Wave 6 Task 6.1).
@@ -229,29 +209,6 @@ function checkApiExposureSecurity(actionId, exposure) {
229
209
  }
230
210
  return errors;
231
211
  }
232
- /**
233
- * DEFINE_API_LEGACY_USAGE — the action-first migration replaces
234
- * standalone `defineApi` calls with `defineAction` + `exposure.type:
235
- * 'api'`. Every `defineApi` call still in the source produces a row in
236
- * `registry.apis`; the validator emits one error per legacy API to
237
- * enforce the alpha breaking migration. The validate command's fixture
238
- * suite intentionally does not use `defineApi`, so this rule does not
239
- * break existing unit tests.
240
- */
241
- function checkDefineApiLegacyUsage(apiCount, apis) {
242
- if (apiCount === 0)
243
- return [];
244
- const errors = [];
245
- for (const api of apis) {
246
- if (api.kind !== 'api')
247
- continue;
248
- errors.push({
249
- code: 'DEFINE_API_LEGACY_USAGE',
250
- message: `API '${api.id}' uses legacy \`defineApi\`. Migrate to \`defineAction\` with \`exposure: { type: 'api', path, method, auth, tenancy }\` (#4619).`,
251
- });
252
- }
253
- return errors;
254
- }
255
212
  /**
256
213
  * Run the validate command: build the registry and perform structural validation.
257
214
  * Exits the process with code 1 if validation fails (CI gate usage).
@@ -267,9 +224,8 @@ export async function runValidate(domainRoot, exitOnFailure = true, config = { m
267
224
  for (const w of warnings) {
268
225
  cliLogger.warn(w);
269
226
  }
270
- errors.push(...checkDuplicateIds(registry.apis, 'api'), ...checkDuplicateIds(registry.webhooks, 'webhook'), ...checkDuplicateIds(registry.subscribers, 'subscriber'), ...checkDuplicateIds(registry.schedules, 'schedule'), ...checkDuplicateIds(registry.jobs, 'job'), ...checkDuplicateIds(registry.actions, 'action'), ...checkDuplicateIds(registry.integrations, 'integration'), ...checkDuplicateIds(registry.events, 'event'));
227
+ errors.push(...checkDuplicateIds(registry.webhooks, 'webhook'), ...checkDuplicateIds(registry.subscribers, 'subscriber'), ...checkDuplicateIds(registry.schedules, 'schedule'), ...checkDuplicateIds(registry.jobs, 'job'), ...checkDuplicateIds(registry.actions, 'action'), ...checkDuplicateIds(registry.integrations, 'integration'), ...checkDuplicateIds(registry.events, 'event'));
271
228
  const entriesWithFiles = [
272
- ...registry.apis,
273
229
  ...registry.webhooks,
274
230
  ...registry.subscribers,
275
231
  ...registry.schedules,
@@ -291,54 +247,20 @@ export async function runValidate(domainRoot, exitOnFailure = true, config = { m
291
247
  }
292
248
  }));
293
249
  errors.push(...checkSubscriberSemverRanges(registry.subscribers));
294
- const VALID_METHODS = new Set(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']);
295
- for (const api of registry.apis) {
296
- const entry = api;
297
- errors.push(...checkLifecycleConsistency(entry, `api '${api.id}'`));
298
- if (!api.requestSchema) {
299
- errors.push({
300
- code: 'MISSING_API_REQUEST_SCHEMA',
301
- message: `API '${api.id}' (${api.method} ${api.path}) has no request schema. Define input/output in defineApi versions.`,
302
- });
303
- }
304
- if (!api.responseSchema) {
305
- errors.push({
306
- code: 'MISSING_API_RESPONSE_SCHEMA',
307
- message: `API '${api.id}' (${api.method} ${api.path}) has no response schema. Define input/output in defineApi versions.`,
308
- });
309
- }
310
- if (!VALID_METHODS.has(api.method)) {
311
- errors.push({
312
- code: 'INVALID_API_METHOD',
313
- message: `API '${api.id}' (${api.method} ${api.path}) has invalid method "${api.method}". Use one of: ${[...VALID_METHODS].join(', ')}.`,
314
- });
315
- }
316
- const isVoidInput = api.requestSchema?.type === 'null';
317
- if (!isVoidInput && !api.examples?.request) {
318
- errors.push({
319
- code: 'MISSING_API_REQUEST_EXAMPLE',
320
- message: `API '${api.id}' (${api.method} ${api.path}) has no request example. Add examples: { request: {...}, response: {...} } to the defineApi config.`,
321
- });
322
- }
323
- if (!api.examples?.response) {
324
- errors.push({
325
- code: 'MISSING_API_RESPONSE_EXAMPLE',
326
- message: `API '${api.id}' (${api.method} ${api.path}) has no response example. Add examples: { request: {...}, response: {...} } to the defineApi config.`,
327
- });
328
- }
329
- }
250
+ // Issue #4689: defineApi and registry.apis were removed. The action-first
251
+ // validation rules in `checkActionFirstSecurity` cover the action surface
252
+ // that now owns all HTTP endpoints.
330
253
  const rawPathErrors = await checkRawPathViolations(registry.domainRoot, config);
331
254
  errors.push(...rawPathErrors);
332
255
  // Wave 6 Task 6.1: action-first security validation gates (#4619).
333
256
  // Runs against the registry built above so the rules see the same
334
257
  // shape the CDK packer will eventually consume.
335
258
  errors.push(...checkActionFirstSecurity(registry.actions));
336
- errors.push(...checkDefineApiLegacyUsage(registry.apis.length, registry.apis));
337
259
  // Issue #4662 Task D — deployment-time security gates. These mirror
338
260
  // the CDK synth-time `SecurityAssertionAspect` so violations are
339
261
  // caught before any AWS deployment is attempted.
340
- errors.push(...checkDeploymentSecurity(registry.apis, registry.actions));
341
- const totalPrimitives = registry.apis.length + registry.webhooks.length +
262
+ errors.push(...checkDeploymentSecurity(registry.actions));
263
+ const totalPrimitives = registry.webhooks.length +
342
264
  registry.subscribers.length + registry.schedules.length +
343
265
  registry.jobs.length + registry.actions.length;
344
266
  if (totalPrimitives === 0) {
@@ -1,5 +1,5 @@
1
1
  import { type FastifyInstance } from 'fastify';
2
- import type { DomainRegistry } from '@mettlecast/domain-cdk-packer';
2
+ import type { DomainRegistry } from '../types.js';
3
3
  /**
4
4
  * Options for creating the local API server.
5
5
  */
@@ -1,5 +1,5 @@
1
1
  import type { FastifyInstance } from 'fastify';
2
- import type { DomainRegistry } from '@mettlecast/domain-cdk-packer';
2
+ import type { DomainRegistry } from '../types.js';
3
3
  /**
4
4
  * Options for mountRoutes.
5
5
  */
@@ -13,7 +13,16 @@ export interface MountRoutesOptions {
13
13
  }
14
14
  /**
15
15
  * Mount all API routes from the DomainRegistry onto the Fastify server.
16
- * Each route dynamically imports its handler file and hydrates a local dev ctx per request.
16
+ *
17
+ * Issue #4689: the only HTTP endpoint surface in the new registry is
18
+ * `actions[]` whose `exposure.type === 'api'`. `defineApi` was removed
19
+ * and `registry.apis` is always empty in fresh registries, so the
20
+ * dev server iterates the action array and mounts each API exposure
21
+ * directly. Internal-only actions are intentionally skipped — they
22
+ * are reachable only through `ctx.actions`.
23
+ *
24
+ * Each route dynamically imports its handler file and hydrates a local
25
+ * dev ctx per request.
17
26
  * @param options - MountRoutesOptions.
18
27
  */
19
28
  export declare function mountRoutes(options: MountRoutesOptions): Promise<void>;
@@ -5,23 +5,35 @@ function toFastifyPath(path) {
5
5
  }
6
6
  /**
7
7
  * Mount all API routes from the DomainRegistry onto the Fastify server.
8
- * Each route dynamically imports its handler file and hydrates a local dev ctx per request.
8
+ *
9
+ * Issue #4689: the only HTTP endpoint surface in the new registry is
10
+ * `actions[]` whose `exposure.type === 'api'`. `defineApi` was removed
11
+ * and `registry.apis` is always empty in fresh registries, so the
12
+ * dev server iterates the action array and mounts each API exposure
13
+ * directly. Internal-only actions are intentionally skipped — they
14
+ * are reachable only through `ctx.actions`.
15
+ *
16
+ * Each route dynamically imports its handler file and hydrates a local
17
+ * dev ctx per request.
9
18
  * @param options - MountRoutesOptions.
10
19
  */
11
20
  export async function mountRoutes(options) {
12
21
  const { server, registry, domainRoot } = options;
13
- for (const api of registry.apis) {
14
- const handlerAbsPath = join(domainRoot, api.handlerFile);
15
- const fastifyPath = toFastifyPath(api.path);
22
+ for (const action of registry.actions) {
23
+ if (action.exposure?.type !== 'api')
24
+ continue;
25
+ const exposure = action.exposure;
26
+ const handlerAbsPath = join(domainRoot, action.handlerFile);
27
+ const fastifyPath = toFastifyPath(exposure.path);
16
28
  const mod = await import(handlerAbsPath);
17
- const def = Object.values(mod).find(v => v && typeof v === 'object' && v['id'] === api.id);
29
+ const def = Object.values(mod).find(v => v && typeof v === 'object' && v['id'] === action.id);
18
30
  if (!def || typeof def['handler'] !== 'function') {
19
- server.log.warn({ apiId: api.id, handlerAbsPath }, 'No handler function found, skipping route');
31
+ server.log.warn({ actionId: action.id, handlerAbsPath }, 'No handler function found, skipping route');
20
32
  continue;
21
33
  }
22
34
  const handler = def['handler'];
23
35
  server.route({
24
- method: api.method === 'ANY' ? ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'] : [api.method],
36
+ method: [exposure.method],
25
37
  url: fastifyPath,
26
38
  handler: async (request, reply) => {
27
39
  const { ctx } = await hydrateLocalCtx({ domainRoot });
@@ -37,6 +49,6 @@ export async function mountRoutes(options) {
37
49
  await reply.send(result);
38
50
  },
39
51
  });
40
- server.log.info({ method: api.method, path: fastifyPath, apiId: api.id }, 'Route mounted');
52
+ server.log.info({ method: exposure.method, path: fastifyPath, actionId: action.id }, 'Route mounted');
41
53
  }
42
54
  }
@@ -1,5 +1,10 @@
1
1
  /**
2
2
  * Template for generating API handler skeleton.
3
+ *
4
+ * Action-first contract: HTTP endpoints are declared with `defineAction`
5
+ * + `exposure: { type: 'api', ... }`. The legacy `defineApi` factory was
6
+ * removed in #4689 — the only public HTTP endpoint primitive is now an
7
+ * action whose exposure type is `'api'`.
3
8
  */
4
9
  /**
5
10
  * Generates an API handler skeleton with the given domain, API, and tenancy.