@mettlecast/domain-cli 0.2.59 → 0.2.61

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 (119) hide show
  1. package/dist/builder/build-registry.d.ts +1 -1
  2. package/dist/builder/build-registry.js +129 -49
  3. package/dist/builder/build-types.d.ts +1 -1
  4. package/dist/builder/load-module.d.ts +1 -1
  5. package/dist/builder/load-module.js +3 -3
  6. package/dist/cli.js +3 -3
  7. package/dist/commands/add-api.js +2 -2
  8. package/dist/commands/add-domain.js +4 -4
  9. package/dist/commands/add-fixture-factory.js +5 -6
  10. package/dist/commands/build-catalog.d.ts +8 -24
  11. package/dist/commands/build-catalog.js +12 -23
  12. package/dist/commands/build-flows.js +1 -1
  13. package/dist/commands/build.js +7 -6
  14. package/dist/commands/check-hashes.js +2 -2
  15. package/dist/commands/create-project.js +1 -1
  16. package/dist/commands/dev.js +1 -1
  17. package/dist/commands/doctor.d.ts +5 -7
  18. package/dist/commands/doctor.js +110 -202
  19. package/dist/commands/explain.js +13 -13
  20. package/dist/commands/generate-openapi.d.ts +10 -1
  21. package/dist/commands/generate-openapi.js +19 -33
  22. package/dist/commands/power-tune.js +2 -2
  23. package/dist/commands/show-dns.d.ts +1 -1
  24. package/dist/commands/show-dns.js +5 -5
  25. package/dist/commands/show.d.ts +2 -3
  26. package/dist/commands/show.js +0 -2
  27. package/dist/commands/test.js +0 -1
  28. package/dist/commands/upgrade-backend.js +3 -3
  29. package/dist/commands/upgrade.js +17 -11
  30. package/dist/commands/validate.js +144 -39
  31. package/dist/server/api-server.d.ts +1 -1
  32. package/dist/server/mount-routes.d.ts +11 -2
  33. package/dist/server/mount-routes.js +20 -8
  34. package/dist/templates/api-skeleton.d.ts +5 -0
  35. package/dist/templates/api-skeleton.js +28 -27
  36. package/dist/templates/claude-md.js +1 -1
  37. package/dist/templates/patterns/api/create-with-event.d.ts +4 -0
  38. package/dist/templates/patterns/api/create-with-event.js +38 -32
  39. package/dist/templates/patterns/api/idempotent-mutation.d.ts +4 -0
  40. package/dist/templates/patterns/api/idempotent-mutation.js +47 -41
  41. package/dist/templates/patterns/api/paginated-list.d.ts +4 -0
  42. package/dist/templates/patterns/api/paginated-list.js +30 -24
  43. package/dist/templates/patterns/api/simple-crud.d.ts +4 -0
  44. package/dist/templates/patterns/api/simple-crud.js +46 -35
  45. package/dist/templates/patterns/api/streaming-list.d.ts +4 -0
  46. package/dist/templates/patterns/api/streaming-list.js +46 -41
  47. package/dist/templates/patterns/api/system-admin.d.ts +4 -0
  48. package/dist/templates/patterns/api/system-admin.js +59 -52
  49. package/dist/templates/patterns/api/webhook-receiver-style.d.ts +4 -0
  50. package/dist/templates/patterns/api/webhook-receiver-style.js +43 -35
  51. package/dist/types.d.ts +100 -0
  52. package/dist/types.js +1 -0
  53. package/dist/utils/file-helpers.d.ts +0 -2
  54. package/dist/utils/file-helpers.js +2 -3
  55. package/dist/utils/header-inject.js +2 -2
  56. package/dist/utils/install-file.d.ts +1 -1
  57. package/dist/utils/install-file.js +1 -1
  58. package/dist/utils/manifest.js +1 -2
  59. package/dist/utils/scaffold-config.d.ts +8 -2
  60. package/dist/utils/scaffold-config.js +3 -1
  61. package/package.json +1 -1
  62. package/src/__tests__/build-registry.test.ts +43 -20
  63. package/src/__tests__/build-types.test.ts +4 -7
  64. package/src/__tests__/builder/walkDomainDir.test.ts +19 -21
  65. package/src/__tests__/commands/add-api.test.ts +12 -10
  66. package/src/__tests__/commands/add-domain.test.ts +8 -5
  67. package/src/__tests__/commands/check-hashes.test.ts +9 -9
  68. package/src/__tests__/commands/create-project.test.ts +5 -5
  69. package/src/__tests__/commands/dev.test.ts +0 -1
  70. package/src/__tests__/commands/upgrade.test.ts +7 -7
  71. package/src/__tests__/doctor.test.ts +60 -67
  72. package/src/__tests__/mount-routes.test.ts +64 -23
  73. package/src/__tests__/package-freshness.test.ts +94 -0
  74. package/src/__tests__/scaffold-src/part-a-layout.test.ts +10 -10
  75. package/src/__tests__/scripts/package-scaffold.test.ts +5 -5
  76. package/src/__tests__/smoke/scaffold.test.ts +13 -15
  77. package/src/__tests__/utils/install-file.test.ts +2 -2
  78. package/src/__tests__/utils/manifest.test.ts +2 -2
  79. package/src/__tests__/validate.test.ts +570 -1
  80. package/src/builder/build-registry.ts +154 -59
  81. package/src/builder/build-types.ts +1 -1
  82. package/src/builder/load-module.ts +3 -3
  83. package/src/cli.ts +4 -4
  84. package/src/commands/add-api.ts +2 -2
  85. package/src/commands/add-domain.ts +4 -4
  86. package/src/commands/add-fixture-factory.ts +5 -6
  87. package/src/commands/build-catalog.ts +18 -40
  88. package/src/commands/build-flows.ts +1 -1
  89. package/src/commands/build.ts +8 -7
  90. package/src/commands/check-hashes.ts +2 -2
  91. package/src/commands/create-project.ts +1 -1
  92. package/src/commands/dev.ts +1 -1
  93. package/src/commands/doctor.ts +120 -218
  94. package/src/commands/explain.ts +13 -13
  95. package/src/commands/generate-openapi.ts +30 -52
  96. package/src/commands/power-tune.ts +2 -2
  97. package/src/commands/show-dns.ts +5 -5
  98. package/src/commands/show.ts +2 -5
  99. package/src/commands/test.ts +0 -1
  100. package/src/commands/upgrade-backend.ts +3 -3
  101. package/src/commands/upgrade.ts +16 -10
  102. package/src/commands/validate.ts +180 -40
  103. package/src/server/api-server.ts +1 -1
  104. package/src/server/mount-routes.ts +21 -10
  105. package/src/templates/api-skeleton.ts +29 -28
  106. package/src/templates/claude-md.ts +1 -1
  107. package/src/templates/patterns/api/create-with-event.ts +39 -33
  108. package/src/templates/patterns/api/idempotent-mutation.ts +48 -42
  109. package/src/templates/patterns/api/paginated-list.ts +31 -25
  110. package/src/templates/patterns/api/simple-crud.ts +47 -36
  111. package/src/templates/patterns/api/streaming-list.ts +47 -42
  112. package/src/templates/patterns/api/system-admin.ts +60 -53
  113. package/src/templates/patterns/api/webhook-receiver-style.ts +48 -40
  114. package/src/types.ts +128 -0
  115. package/src/utils/file-helpers.ts +2 -5
  116. package/src/utils/header-inject.ts +2 -2
  117. package/src/utils/install-file.ts +1 -1
  118. package/src/utils/manifest.ts +1 -2
  119. package/src/utils/scaffold-config.ts +12 -3
@@ -1,22 +1,123 @@
1
1
  import { relative, resolve } from 'node:path';
2
2
  import type {
3
3
  DomainRegistry,
4
- ApiRegistryEntry,
5
- ApiVersionSnapshot,
4
+ ActionRegistryEntry,
6
5
  SchemaSnapshot,
7
6
  WebhookRegistryEntry,
8
7
  SubscriberRegistryEntry,
9
8
  ScheduleRegistryEntry,
10
9
  JobRegistryEntry,
11
- ActionRegistryEntry,
12
10
  IntegrationRegistryEntry,
13
11
  EventRegistryEntry,
14
12
  DomainRegistryEntry,
15
13
  SerialDeploymentConfig,
16
- } from '@mettlecast/domain-cdk-packer';
14
+ } from '../types.js';
17
15
  import { walkDomainDir } from '../utils/file-helpers.js';
18
16
  import { loadModuleExports, type RawPrimitiveExport } from './load-module.js';
19
17
 
18
+ // Issue #4689: the legacy `defineApi` primitive was removed from
19
+ // `@mettlecast/domain-runtime`. The only HTTP endpoint surface in
20
+ // the new registry is `actions[]` whose `exposure.type === 'api'`.
21
+
22
+ type ActionBackendAccess = 'private' | 'domain' | 'platform';
23
+
24
+ type ActionExposure =
25
+ | { type: 'internal' }
26
+ | {
27
+ type: 'api';
28
+ path: string;
29
+ method: string;
30
+ auth: 'required' | 'none' | 'service';
31
+ tenancy: 'required' | 'none' | 'system';
32
+ roles?: string[];
33
+ securityException?: { reason: string };
34
+ authDeclared?: boolean;
35
+ tenancyDeclared?: boolean;
36
+ };
37
+
38
+ /**
39
+ * Coerce a raw value into a valid backendAccess scope, defaulting to
40
+ * 'private' when the value is missing or unrecognized. Used by the
41
+ * builder when reading the new-style `backendAccess` field directly.
42
+ */
43
+ function toBackendAccess(raw: unknown): ActionBackendAccess {
44
+ return raw === 'domain' || raw === 'platform' ? raw : 'private';
45
+ }
46
+
47
+ /**
48
+ * Map a legacy `visibility` scope to a `backendAccess` scope for the
49
+ * action-first migration. The legacy `workspace` scope (which previously
50
+ * permitted unauthenticated Function URL exposure) collapses to `domain`
51
+ * so that all cross-domain callers must go through `ctx.actions`.
52
+ */
53
+ function visibilityToBackendAccess(visibility: unknown): ActionBackendAccess {
54
+ if (visibility === 'workspace') return 'domain';
55
+ if (visibility === 'domain') return 'domain';
56
+ return 'private';
57
+ }
58
+
59
+ /**
60
+ * Best-effort reverse mapping from `backendAccess` to the legacy
61
+ * `visibility` field. Used to keep the deprecated field populated so
62
+ * existing CDK constructs that read it continue to behave the same way.
63
+ *
64
+ * - private -> private
65
+ * - domain -> domain
66
+ * - platform -> workspace (closest legacy equivalent for platform-level)
67
+ */
68
+ function backendAccessToVisibility(backendAccess: ActionBackendAccess): 'private' | 'domain' | 'workspace' {
69
+ if (backendAccess === 'platform') return 'workspace';
70
+ return backendAccess;
71
+ }
72
+
73
+ /**
74
+ * Type guard + sanitizer for an action's `exposure` field. Returns a
75
+ * well-typed `ActionExposure` (extended with `authDeclared` /
76
+ * `tenancyDeclared` tracking flags) or the provided fallback when the
77
+ * raw value does not match a supported exposure shape.
78
+ *
79
+ * The `authDeclared` / `tenancyDeclared` flags record whether the source
80
+ * code explicitly declared each field, or whether the builder fell back
81
+ * to the safe default. They are validation-only and consumed by the
82
+ * domain CLI's validate command (Wave 6 Task 6.1) to enforce the
83
+ * action-first security model (#4619).
84
+ */
85
+ function toExposure(raw: unknown, fallback: ActionExposure): ActionExposure {
86
+ if (!raw || typeof raw !== 'object') return fallback;
87
+ const candidate = raw as { type?: unknown };
88
+ if (candidate.type === 'internal') return { type: 'internal' };
89
+ if (candidate.type !== 'api') return fallback;
90
+ // Best-effort validation of api exposure fields; any missing required
91
+ // string field falls back to the supplied default exposure.
92
+ const api = raw as { path?: unknown; method?: unknown; auth?: unknown; tenancy?: unknown; roles?: unknown; securityException?: unknown };
93
+ if (typeof api.path !== 'string' || typeof api.method !== 'string') return fallback;
94
+ const authRaw = api.auth;
95
+ const tenancyRaw = api.tenancy;
96
+ const auth: 'required' | 'none' | 'service' =
97
+ authRaw === 'required' || authRaw === 'none' || authRaw === 'service' ? authRaw : 'required';
98
+ const tenancy: 'required' | 'none' | 'system' =
99
+ tenancyRaw === 'required' || tenancyRaw === 'none' || tenancyRaw === 'system' ? tenancyRaw : 'required';
100
+ const out: ActionExposure & { type: 'api'; authDeclared?: boolean; tenancyDeclared?: boolean } = {
101
+ type: 'api',
102
+ path: api.path,
103
+ method: api.method,
104
+ auth,
105
+ tenancy,
106
+ authDeclared: authRaw === 'required' || authRaw === 'none' || authRaw === 'service',
107
+ tenancyDeclared: tenancyRaw === 'required' || tenancyRaw === 'none' || tenancyRaw === 'system',
108
+ };
109
+ if (Array.isArray(api.roles)) {
110
+ out.roles = api.roles.filter((r): r is string => typeof r === 'string');
111
+ }
112
+ if (api.securityException && typeof api.securityException === 'object') {
113
+ const reason = (api.securityException as { reason?: unknown }).reason;
114
+ if (typeof reason === 'string') {
115
+ out.securityException = { reason };
116
+ }
117
+ }
118
+ return out;
119
+ }
120
+
20
121
  /** Returns true if a value looks like a JSON Schema object (has a 'type' or '$schema' property). */
21
122
  function isJsonSchema(v: unknown): boolean {
22
123
  return v !== null && typeof v === 'object' && !Array.isArray(v) &&
@@ -79,7 +180,7 @@ export async function buildRegistry(domainRoot: string): Promise<BuildResult> {
79
180
  const domainRaw = domainExports.find(e => e['_kind'] === 'domain');
80
181
  if (!domainRaw) {
81
182
  // Emit any suppressed tsx load errors to stderr before throwing so they appear in CI logs.
82
- for (const w of warnings) process.stderr.write(`[tib validate] ${w}\n`);
183
+ for (const w of warnings) process.stderr.write(`[mc-domain-module validate] ${w}\n`);
83
184
  throw new Error(`buildRegistry: no 'domain' export found in ${paths.domain}`);
84
185
  }
85
186
 
@@ -91,9 +192,8 @@ export async function buildRegistry(domainRoot: string): Promise<BuildResult> {
91
192
  defaultDeployment: deployment(domainRaw),
92
193
  };
93
194
 
94
- const [apiExports, webhookExports, subscriberExports, actionExports,
195
+ const [webhookExports, subscriberExports, actionExports,
95
196
  scheduleExports, jobExports, integrationExports, eventExports] = await Promise.all([
96
- Promise.all(paths.apis.map(load)),
97
197
  Promise.all(paths.webhooks.map(load)),
98
198
  Promise.all(paths.subscribers.map(load)),
99
199
  Promise.all(paths.actions.map(load)),
@@ -103,43 +203,6 @@ export async function buildRegistry(domainRoot: string): Promise<BuildResult> {
103
203
  paths.publishes ? load(paths.publishes) : Promise.resolve([]),
104
204
  ]);
105
205
 
106
- const VALID_API_METHODS = new Set(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']);
107
-
108
- const apis: ApiRegistryEntry[] = paths.apis.flatMap((filePath, i) =>
109
- (apiExports[i] ?? [])
110
- .filter(e => e['_kind'] === 'api')
111
- .map(e => {
112
- const rawMethod = typeof e['method'] === 'string' ? e['method'].toUpperCase() : '';
113
- if (!rawMethod || !VALID_API_METHODS.has(rawMethod)) {
114
- warnings.push(`${relPath(filePath)}: defineApi "${e['id']}" has invalid or missing method "${rawMethod || '(none)'}". Use one of: ${[...VALID_API_METHODS].join(', ')}.`);
115
- }
116
- const rawVersions = e['versions'] as Record<string, { input?: unknown; output?: unknown }> | undefined;
117
- const versionSnapshots: ApiVersionSnapshot[] = rawVersions
118
- ? Object.entries(rawVersions).map(([ver, v]) => ({
119
- version: ver,
120
- requestSchema: isJsonSchema(v?.input) ? (v.input as SchemaSnapshot) : undefined,
121
- responseSchema: isJsonSchema(v?.output) ? (v.output as SchemaSnapshot) : undefined,
122
- }))
123
- : [];
124
- const latestVersion = versionSnapshots[versionSnapshots.length - 1];
125
- return {
126
- id: String(e['id']),
127
- kind: 'api' as const,
128
- handlerFile: relPath(filePath),
129
- path: String(e['path']),
130
- method: VALID_API_METHODS.has(rawMethod) ? rawMethod : 'GET',
131
- authType: ((e['auth'] as { type?: string } | undefined)?.type as 'jwt' | 'api-key' | 'none' | undefined) ?? 'jwt',
132
- description: typeof e['description'] === 'string' ? e['description'] : undefined,
133
- deployment: deployment(e),
134
- outboundAccess: outboundAccess(e),
135
- requestSchema: latestVersion?.requestSchema,
136
- responseSchema: latestVersion?.responseSchema,
137
- versions: versionSnapshots.length > 0 ? versionSnapshots : undefined,
138
- examples: (e['examples'] as { request?: Record<string, unknown>; response?: Record<string, unknown> } | undefined),
139
- };
140
- })
141
- );
142
-
143
206
  const webhooks: WebhookRegistryEntry[] = paths.webhooks.flatMap((filePath, i) =>
144
207
  (webhookExports[i] ?? [])
145
208
  .filter(e => e['_kind'] === 'webhook')
@@ -199,22 +262,55 @@ export async function buildRegistry(domainRoot: string): Promise<BuildResult> {
199
262
  }))
200
263
  );
201
264
 
202
- const actions: ActionRegistryEntry[] = paths.actions.flatMap((filePath, i) =>
265
+ const actions = paths.actions.flatMap((filePath, i) =>
203
266
  (actionExports[i] ?? [])
204
267
  .filter(e => e['_kind'] === 'action')
205
- .map(e => ({
206
- id: String(e['id']),
207
- kind: 'action' as const,
208
- handlerFile: relPath(filePath),
209
- visibility: String(e['visibility']) as 'private' | 'domain' | 'workspace',
210
- idempotent: Boolean(e['idempotent'] ?? false),
211
- description: typeof e['description'] === 'string' ? e['description'] : undefined,
212
- deployment: deployment(e),
213
- outboundAccess: outboundAccess(e),
214
- inputSchema: isJsonSchema(e['input']) ? (e['input'] as SchemaSnapshot) : undefined,
215
- outputSchema: isJsonSchema(e['output']) ? (e['output'] as SchemaSnapshot) : undefined,
216
- }))
217
- );
268
+ .map(e => {
269
+ // Resolve backendAccess. New-style actions carry `backendAccess`
270
+ // directly. Legacy actions only carry `visibility`; in that case
271
+ // we collapse workspace -> domain per the migration spec, and we
272
+ // also remember the legacy flag so we can default exposure to
273
+ // `{ type: 'internal' }` for actions that have not opted in yet.
274
+ const hasBackendAccess = 'backendAccess' in e;
275
+ const rawBackendAccess = e['backendAccess'];
276
+ const rawVisibility = e['visibility'];
277
+ const backendAccess: ActionBackendAccess = hasBackendAccess
278
+ ? toBackendAccess(rawBackendAccess)
279
+ : visibilityToBackendAccess(rawVisibility);
280
+ const legacyVisibility = backendAccessToVisibility(backendAccess);
281
+
282
+ // Resolve exposure. New-style actions must declare their exposure;
283
+ // legacy actions that did not opt in default to `{ type: 'internal' }`
284
+ // so existing internal-only behavior is preserved during migration.
285
+ const rawExposure = e['exposure'];
286
+ const exposure: ActionExposure = toExposure(rawExposure, { type: 'internal' });
287
+ // Record whether the source explicitly declared the `exposure` field.
288
+ // Validation-only; consumed by the domain CLI's validate command
289
+ // (Wave 6 Task 6.1) to enforce `ACTION_EXPOSURE_REQUIRED`.
290
+ const exposureDeclared = rawExposure !== undefined && rawExposure !== null
291
+ && typeof rawExposure === 'object';
292
+
293
+ return {
294
+ id: String(e['id']),
295
+ kind: 'action' as const,
296
+ handlerFile: relPath(filePath),
297
+ backendAccess,
298
+ exposure,
299
+ exposureDeclared,
300
+ // Keep the legacy field populated so CDK constructs that still
301
+ // read `visibility` (e.g. action-construct.ts) keep working
302
+ // through the migration window. New constructs should read
303
+ // `backendAccess` and `exposure` instead.
304
+ visibility: legacyVisibility,
305
+ idempotent: Boolean(e['idempotent'] ?? false),
306
+ description: typeof e['description'] === 'string' ? e['description'] : undefined,
307
+ deployment: deployment(e),
308
+ outboundAccess: outboundAccess(e),
309
+ inputSchema: isJsonSchema(e['input']) ? (e['input'] as SchemaSnapshot) : undefined,
310
+ outputSchema: isJsonSchema(e['output']) ? (e['output'] as SchemaSnapshot) : undefined,
311
+ };
312
+ })
313
+ ) as ActionRegistryEntry[];
218
314
 
219
315
  const integrations: IntegrationRegistryEntry[] = paths.integrations.flatMap((_filePath, i) =>
220
316
  (integrationExports[i] ?? [])
@@ -242,7 +338,6 @@ export async function buildRegistry(domainRoot: string): Promise<BuildResult> {
242
338
  schemaVersion: '1',
243
339
  domainRoot,
244
340
  domain,
245
- apis,
246
341
  webhooks,
247
342
  subscribers,
248
343
  schedules,
@@ -1,4 +1,4 @@
1
- import type { DomainRegistry } from '@mettlecast/domain-cdk-packer';
1
+ import type { DomainRegistry } from '../types.js';
2
2
 
3
3
  /**
4
4
  * Generate a .d.ts file declaring typed `ctx.actions.call(actionId, input)`
@@ -113,9 +113,9 @@ process.stdout.write(JSON.stringify(results));
113
113
  export async function loadModuleExports(absoluteFilePath: string): Promise<RawPrimitiveExport[]> {
114
114
  // Use a subdir of the project root rather than OS tmpdir so that ESM import
115
115
  // resolution can walk up and find node_modules packages like zod-to-json-schema.
116
- const tibTmpDir = join(process.cwd(), '.tib', 'tmp');
117
- await mkdir(tibTmpDir, { recursive: true }).catch(() => undefined);
118
- const tempPath = join(tibTmpDir, `tib-load-${randomBytes(8).toString('hex')}.mts`);
116
+ const mcTmpDir = join(process.cwd(), '.mc', 'tmp');
117
+ await mkdir(mcTmpDir, { recursive: true }).catch(() => undefined);
118
+ const tempPath = join(mcTmpDir, `mc-load-${randomBytes(8).toString('hex')}.mts`);
119
119
  await writeFile(tempPath, makeEvalScript(absoluteFilePath), 'utf8');
120
120
 
121
121
  try {
package/src/cli.ts CHANGED
@@ -73,7 +73,7 @@ program
73
73
 
74
74
  program
75
75
  .command('dev <domain>')
76
- .description('Start a local HTTP server simulating API Gateway for all defineApi handlers')
76
+ .description('Start a local HTTP server simulating API Gateway for all API-exposed defineAction handlers')
77
77
  .option('--port <n>', 'Port to listen on', '3000')
78
78
  .action(async (domain: string, opts: { port?: string }) => {
79
79
  await runDev({ domainRoot: domain, port: opts.port ? parseInt(opts.port, 10) : 3000 });
@@ -82,9 +82,9 @@ program
82
82
  program
83
83
  .command('build-catalog')
84
84
  .description('Merge all per-domain registry files into .mc/domain-registry.json for TIB sync')
85
- .option('--tib-dir <path>', 'Path to the .tib directory (defaults to .tib in cwd)')
86
- .action(async (opts: { tibDir?: string }) => {
87
- await runBuildCatalog(opts.tibDir);
85
+ .option('--mc-dir <path>', 'Path to the .mc registry directory (defaults to .mc in cwd)')
86
+ .action(async (opts: { mcDir?: string }) => {
87
+ await runBuildCatalog(opts.mcDir);
88
88
  });
89
89
 
90
90
  program
@@ -96,7 +96,7 @@ export async function runAddApi(opts: AddApiOptions): Promise<void> {
96
96
  throw new Error(`Domain "${opts.domain}" not found at ${domainDir}`);
97
97
  }
98
98
 
99
- const apiFilePath = join(domainDir, 'api', `${opts.id}.ts`);
99
+ const apiFilePath = join(domainDir, 'actions', `${opts.id}.ts`);
100
100
 
101
101
  // Refuse if API already exists
102
102
  try {
@@ -124,7 +124,7 @@ export async function runAddApi(opts: AddApiOptions): Promise<void> {
124
124
  await writeFile(apiFilePath, apiContent);
125
125
 
126
126
  // Create fixture with the input schema's default shape as the example payload
127
- const apiTestDir = join(domainDir, 'api', '__tests__');
127
+ const apiTestDir = join(domainDir, 'actions', '__tests__');
128
128
  await mkdir(apiTestDir, { recursive: true });
129
129
  await writeFile(
130
130
  join(apiTestDir, `${opts.id}.fixture.json`),
@@ -51,18 +51,18 @@ export async function runAddDomain(opts: AddDomainOptions): Promise<void> {
51
51
  }
52
52
 
53
53
  // Create directory structure
54
- await mkdir(join(domainDir, 'api', '__tests__'), { recursive: true });
54
+ await mkdir(join(domainDir, 'actions', '__tests__'), { recursive: true });
55
55
  await mkdir(join(domainDir, 'subscribers'), { recursive: true });
56
56
  await mkdir(join(domainDir, 'publishes'), { recursive: true });
57
57
 
58
58
  // Write skeleton files
59
59
  await writeFile(join(domainDir, 'domain.config.ts'), domainConfigTemplate(opts.id, opts.tenancy));
60
60
  await writeFile(
61
- join(domainDir, 'api', 'example.ts'),
61
+ join(domainDir, 'actions', 'example.ts'),
62
62
  apiSkeletonTemplate(opts.id, 'example', opts.tenancy)
63
63
  );
64
64
  await writeFile(
65
- join(domainDir, 'api', '__tests__', 'example.fixture.json'),
65
+ join(domainDir, 'actions', '__tests__', 'example.fixture.json'),
66
66
  apiFixtureSkeleton(opts.id, 'example')
67
67
  );
68
68
  await writeFile(join(domainDir, 'publishes', 'events.ts'), eventsSkeletonTemplate(opts.id));
@@ -94,7 +94,7 @@ export async function runAddDomain(opts: AddDomainOptions): Promise<void> {
94
94
  cliLogger.info({ id: opts.id, dir: domainDir }, 'Domain scaffolded');
95
95
  // eslint-disable-next-line no-console
96
96
  console.log(
97
- `\n✓ Domain "${opts.id}" added at ${domainDir}\nNext: edit api/example.ts, ` +
97
+ `\n✓ Domain "${opts.id}" added at ${domainDir}\nNext: edit actions/example.ts, ` +
98
98
  `then run \`mc-domain-module build ${opts.id}\` to generate the registry.`
99
99
  );
100
100
  }
@@ -47,21 +47,20 @@ export async function runAddFixtureFactory(options: AddFixtureFactoryOptions): P
47
47
  const domain = options.domain;
48
48
  const apiId = options.apiId;
49
49
 
50
- const apiFile = join(projectRoot, 'domains', domain, 'api', `${apiId}.ts`);
50
+ const apiFile = join(projectRoot, 'domains', domain, 'actions', `${apiId}.ts`);
51
51
  await access(apiFile).catch(() => {
52
- throw new Error(`API file not found: ${apiFile}. Run add-api first.`);
52
+ throw new Error(`Action file not found: ${apiFile}. Run add-api first.`);
53
53
  });
54
54
 
55
- // Read the API file to extract the input type name
55
+ // Read the action file to extract the input type name
56
56
  const content = await readFile(apiFile, 'utf8');
57
- const outputTypeArg = content.match(/import\s+\{\s*[\w\s,]*\s*\}\s+from\s+['"]@mettlecast/);
58
57
 
59
- const factoryFile = join(projectRoot, 'domains', domain, 'api', '__tests__', `${apiId}.factory.ts`);
58
+ const factoryFile = join(projectRoot, 'domains', domain, 'actions', '__tests__', `${apiId}.factory.ts`);
60
59
  const factoryContent = FACTORY_TEMPLATE
61
60
  .replace(/\{domain\}/g, domain)
62
61
  .replace(/\{apiId\}/g, apiId);
63
62
 
64
- await mkdir(join(projectRoot, 'domains', domain, 'api', '__tests__'), { recursive: true });
63
+ await mkdir(join(projectRoot, 'domains', domain, 'actions', '__tests__'), { recursive: true });
65
64
  await writeFile(factoryFile, factoryContent, 'utf8');
66
65
 
67
66
  cliLogger.info({ factoryFile }, 'add-fixture-factory: factory written');
@@ -2,27 +2,18 @@ import { readFile, writeFile, readdir, mkdir } from 'node:fs/promises';
2
2
  import { join, resolve } from 'node:path';
3
3
  import { cliLogger } from '../utils/logger.js';
4
4
 
5
- /** A single API entry in the catalog. */
6
- export interface CatalogApi {
7
- id: string;
8
- domainId: string;
9
- path: string;
10
- method: string;
11
- authType: string;
12
- description?: string;
13
- requestSchema?: Record<string, unknown>;
14
- responseSchema?: Record<string, unknown>;
15
- versions?: Array<{ version: string; requestSchema?: Record<string, unknown>; responseSchema?: Record<string, unknown> }>;
16
- examples?: { request?: Record<string, unknown>; response?: Record<string, unknown> };
17
- }
18
-
19
5
  /** A single action entry in the catalog. */
20
6
  export interface CatalogAction {
21
7
  id: string;
22
8
  domainId: string;
23
- visibility: string;
9
+ backendAccess: string;
10
+ exposure: Record<string, unknown>;
11
+ exposureDeclared?: boolean;
24
12
  idempotent: boolean;
25
13
  description?: string;
14
+ handlerFile?: string;
15
+ deployment?: Record<string, unknown>;
16
+ outboundAccess?: string;
26
17
  inputSchema?: Record<string, unknown>;
27
18
  outputSchema?: Record<string, unknown>;
28
19
  }
@@ -83,7 +74,6 @@ export interface DomainCatalog {
83
74
  version: 2;
84
75
  generatedAt: string;
85
76
  domains: CatalogDomain[];
86
- apis: CatalogApi[];
87
77
  actions: CatalogAction[];
88
78
  events: CatalogEvent[];
89
79
  subscribers: CatalogSubscriber[];
@@ -95,30 +85,29 @@ export interface DomainCatalog {
95
85
  /**
96
86
  * Build the combined domain catalog from all per-domain registry files.
97
87
  * Reads .mc/{domain}-registry.json files and merges them into .mc/domain-registry.json.
98
- * @param tibDir - Path to the .tib directory. Defaults to .tib in cwd.
88
+ * @param registryDir - Path to the .mc registry directory. Defaults to .mc in cwd.
99
89
  * @returns The written catalog.
100
90
  */
101
- export async function runBuildCatalog(tibDir?: string): Promise<DomainCatalog> {
102
- const dir = tibDir ? resolve(tibDir) : join(process.cwd(), '.tib');
91
+ export async function runBuildCatalog(registryDir?: string): Promise<DomainCatalog> {
92
+ const dir = registryDir ? resolve(registryDir) : join(process.cwd(), '.mc');
103
93
 
104
94
  // Find all per-domain registry files
105
95
  let files: string[];
106
96
  try {
107
97
  files = await readdir(dir);
108
98
  } catch {
109
- throw new Error(`build-catalog: .tib directory not found at ${dir}. Run tib build first.`);
99
+ throw new Error(`build-catalog: .mc registry directory not found at ${dir}. Run mc-domain-module build first.`);
110
100
  }
111
101
 
112
102
  const registryFiles = files.filter(f => f.endsWith('-registry.json') && f !== 'domain-registry.json');
113
103
  if (registryFiles.length === 0) {
114
- throw new Error(`build-catalog: no domain registry files found in ${dir}. Run tib build <domain> first.`);
104
+ throw new Error(`build-catalog: no domain registry files found in ${dir}. Run mc-domain-module build <domain> first.`);
115
105
  }
116
106
 
117
107
  const catalog: DomainCatalog = {
118
108
  version: 2,
119
109
  generatedAt: new Date().toISOString(),
120
110
  domains: [],
121
- apis: [],
122
111
  actions: [],
123
112
  events: [],
124
113
  subscribers: [],
@@ -152,30 +141,19 @@ export async function runBuildCatalog(tibDir?: string): Promise<DomainCatalog> {
152
141
  description: domainEntry['description'] as string | undefined,
153
142
  });
154
143
 
155
- const apis = (registry['apis'] as Record<string, unknown>[] | undefined) ?? [];
156
- for (const api of apis) {
157
- catalog.apis.push({
158
- id: String(api['id']),
159
- domainId,
160
- path: String(api['path']),
161
- method: String(api['method'] ?? 'ANY'),
162
- authType: String(api['authType'] ?? 'jwt'),
163
- description: api['description'] as string | undefined,
164
- requestSchema: api['requestSchema'] as Record<string, unknown> | undefined,
165
- responseSchema: api['responseSchema'] as Record<string, unknown> | undefined,
166
- versions: api['versions'] as CatalogApi['versions'],
167
- examples: api['examples'] as CatalogApi['examples'],
168
- });
169
- }
170
-
171
144
  const actions = (registry['actions'] as Record<string, unknown>[] | undefined) ?? [];
172
145
  for (const action of actions) {
173
146
  catalog.actions.push({
174
147
  id: String(action['id']),
175
148
  domainId,
176
- visibility: String(action['visibility'] ?? 'private'),
149
+ backendAccess: String(action['backendAccess'] ?? 'private'),
150
+ exposure: (action['exposure'] as Record<string, unknown> | undefined) ?? { type: 'internal' },
151
+ exposureDeclared: action['exposureDeclared'] as boolean | undefined,
177
152
  idempotent: Boolean(action['idempotent'] ?? false),
178
153
  description: action['description'] as string | undefined,
154
+ handlerFile: action['handlerFile'] as string | undefined,
155
+ deployment: action['deployment'] as Record<string, unknown> | undefined,
156
+ outboundAccess: action['outboundAccess'] as string | undefined,
179
157
  inputSchema: action['inputSchema'] as Record<string, unknown> | undefined,
180
158
  outputSchema: action['outputSchema'] as Record<string, unknown> | undefined,
181
159
  });
@@ -241,7 +219,7 @@ export async function runBuildCatalog(tibDir?: string): Promise<DomainCatalog> {
241
219
  await writeFile(outPath, JSON.stringify(catalog), 'utf8');
242
220
 
243
221
  cliLogger.info(
244
- { outPath, domains: catalog.domains.length, apis: catalog.apis.length, actions: catalog.actions.length, events: catalog.events.length, jobs: catalog.jobs.length, schedules: catalog.schedules.length, integrations: catalog.integrations.length },
222
+ { outPath, domains: catalog.domains.length, actions: catalog.actions.length, events: catalog.events.length, jobs: catalog.jobs.length, schedules: catalog.schedules.length, integrations: catalog.integrations.length },
245
223
  'Domain catalog written'
246
224
  );
247
225
 
@@ -285,7 +285,7 @@ async function loadFlowsFromDir(dir: string, owningDomainOverride?: string): Pro
285
285
  */
286
286
  export async function runBuildFlows(options: BuildFlowsOptions): Promise<string> {
287
287
  const projectRoot = options.projectRoot ?? process.cwd();
288
- const outFile = options.outFile ?? join(projectRoot, '.tib', 'flows-registry.json');
288
+ const outFile = options.outFile ?? join(projectRoot, '.mc', 'flows-registry.json');
289
289
 
290
290
  cliLogger.info({ projectRoot }, 'Building flows registry');
291
291
 
@@ -1,6 +1,6 @@
1
1
  import { writeFile, mkdir, readdir, readFile } from 'node:fs/promises';
2
2
  import { join, resolve, basename } from 'node:path';
3
- import type { DomainRegistry } from '@mettlecast/domain-cdk-packer';
3
+ import type { DomainRegistry } from '../types.js';
4
4
  import { buildRegistry } from '../builder/build-registry.js';
5
5
  import { buildActionsTypes } from '../builder/build-types.js';
6
6
  import { cliLogger } from '../utils/logger.js';
@@ -27,7 +27,7 @@ export async function runBuild(options: BuildOptions): Promise<string> {
27
27
  const domainId = basename(domainRoot);
28
28
  const outFile = options.outFile
29
29
  ? resolve(options.outFile)
30
- : join(process.cwd(), '.tib', `${domainId}-registry.json`);
30
+ : join(process.cwd(), '.mc', `${domainId}-registry.json`);
31
31
 
32
32
  cliLogger.info({ domainRoot }, 'Building domain registry');
33
33
 
@@ -40,18 +40,19 @@ export async function runBuild(options: BuildOptions): Promise<string> {
40
40
  await mkdir(join(outFile, '..'), { recursive: true });
41
41
  await writeFile(outFile, JSON.stringify(registry), 'utf8');
42
42
 
43
- cliLogger.info({ outFile, apis: registry.apis.length, events: registry.events.length }, 'Registry written');
43
+ const apiExposedActions = registry.actions.filter(action => action.exposure.type === 'api').length;
44
+ cliLogger.info({ outFile, apiExposedActions, events: registry.events.length }, 'Registry written');
44
45
 
45
46
  // Discover all sibling registries and emit aggregated types
46
- const tibDir = join(process.cwd(), '.tib');
47
- const registryFiles = (await readdir(tibDir)).filter(f => f.endsWith('-registry.json'));
47
+ const registryDir = join(outFile, '..');
48
+ const registryFiles = (await readdir(registryDir)).filter(f => f.endsWith('-registry.json'));
48
49
  const allRegistries: DomainRegistry[] = [];
49
50
  for (const f of registryFiles) {
50
- const raw = await readFile(join(tibDir, f), 'utf8');
51
+ const raw = await readFile(join(registryDir, f), 'utf8');
51
52
  allRegistries.push(JSON.parse(raw) as DomainRegistry);
52
53
  }
53
54
  const typesContent = buildActionsTypes(allRegistries);
54
- const typesFile = join(tibDir, 'actions-types.d.ts');
55
+ const typesFile = join(registryDir, 'actions-types.d.ts');
55
56
  await writeFile(typesFile, typesContent);
56
57
 
57
58
  cliLogger.info({ outFile: typesFile }, 'Types file written');
@@ -57,14 +57,14 @@ interface ModulesHashesManifest {
57
57
  */
58
58
  export async function runCheckHashes(opts: CheckHashesOptions = {}): Promise<CheckHashesResult> {
59
59
  const root = opts.projectRoot ?? process.cwd();
60
- const manifestPath = join(root, '.tib', 'modules-hashes.json');
60
+ const manifestPath = join(root, '.mc', 'modules-hashes.json');
61
61
  const modulesDir = join(root, 'infra', 'modules');
62
62
 
63
63
  let manifest: ModulesHashesManifest;
64
64
  try {
65
65
  manifest = JSON.parse(await readFile(manifestPath, 'utf8')) as ModulesHashesManifest;
66
66
  } catch (err) {
67
- throw new Error(`tib check-hashes: cannot read ${manifestPath} (${(err as Error).message}). Re-scaffold to regenerate the manifest.`);
67
+ throw new Error(`mc-domain-module check-hashes: cannot read ${manifestPath} (${(err as Error).message}). Re-scaffold to regenerate the manifest.`);
68
68
  }
69
69
 
70
70
  const drifted: DriftedFile[] = [];
@@ -217,7 +217,7 @@ export async function runCreateProject(opts: CreateProjectOptions): Promise<void
217
217
 
218
218
  // 5. Prepare output directory
219
219
  await mkdir(outputDir, { recursive: true });
220
- await mkdir(join(outputDir, '.tib'), { recursive: true });
220
+ await mkdir(join(outputDir, '.mc'), { recursive: true });
221
221
 
222
222
  // 6. Get CLI version from package.json
223
223
  let cliVersion = '0.0.0';
@@ -55,7 +55,7 @@ export async function startDev(options: DevOptions): Promise<DevHandle> {
55
55
  }
56
56
 
57
57
  cliLogger.info(
58
- { domain: registry.domain.id, apis: registry.apis.length, port },
58
+ { domain: registry.domain.id, apiActions: registry.actions.filter(a => a.exposure?.type === 'api').length, port },
59
59
  'Starting local dev server'
60
60
  );
61
61