@mettlecast/domain-cdk-packer 0.2.60 → 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.
@@ -10,7 +10,60 @@ function camelCase(str) {
10
10
  function toPascalCase(str) {
11
11
  return str.charAt(0).toUpperCase() + camelCase(str).slice(1);
12
12
  }
13
- function generateDedicatedEntry(handlerImportPath, entry, primitiveType) {
13
+ /**
14
+ * Per-primitive-type default adapter exported from `@mettlecast/domain-runtime`.
15
+ *
16
+ * Exported (Wave 7 Task 7.3, #4619) so unit tests can pin the adapter
17
+ * selection logic without instantiating CDK or esbuild. The mapping here
18
+ * is the source of truth for the default wrap used by
19
+ * `buildDedicatedEntryContent`; it does NOT cover the per-entry override
20
+ * used by API-exposed actions (see `EXPOSED_ACTION_ADAPTER`).
21
+ */
22
+ export const ADAPTER_BY_PRIMITIVE = {
23
+ api: 'createApiLambdaHandler',
24
+ subscriber: 'createSubscriberLambdaHandler',
25
+ job: 'createJobLambdaHandler',
26
+ webhook: 'createWebhookLambdaHandler',
27
+ action: 'createActionLambdaHandler',
28
+ };
29
+ /**
30
+ * Adapter used for API-exposed actions (Wave 4 Task 4.1, #4619).
31
+ *
32
+ * API-exposed actions receive raw HTTP API v2 events, so they must wrap
33
+ * with `createExposedActionApiHandler` instead of the envelope-based
34
+ * `createActionLambdaHandler` that internal actions use.
35
+ *
36
+ * Exported (Wave 7 Task 7.3) so tests can assert the override behaviour
37
+ * without depending on the published runtime's barrel.
38
+ */
39
+ export const EXPOSED_ACTION_ADAPTER = 'createExposedActionApiHandler';
40
+ /**
41
+ * Pure, side-effect-free entry-content generator for a single dedicated-mode
42
+ * Lambda handler.
43
+ *
44
+ * Returns the TypeScript source string that `NodejsFunction` will bundle.
45
+ * Exported (Wave 7 Task 7.3, #4619) so unit tests can assert adapter
46
+ * selection and import wiring without paying the cost of esbuild bundling
47
+ * on every `DomainStack` synth.
48
+ *
49
+ * Note: Wave 7 Task 7.1 may extend this signature with `domainId` (and
50
+ * rewrite the action-adapter wrappers) so the runtime can plumb
51
+ * `definingDomain` / `domainId` into the dedicated Lambda. The narrow
52
+ * tests in `grouped-lambda-factory.test.ts` deliberately target only the
53
+ * parts that are stable across that change so they keep passing through
54
+ * the merge.
55
+ *
56
+ * @param handlerImportPath Relative path from the temp entry directory
57
+ * to the handler file. Must already be normalised
58
+ * to start with `./` or `../`.
59
+ * @param entry The handler entry whose `id` and `adapter`
60
+ * drive the export name and adapter choice.
61
+ * @param primitiveType The primitive type — selects the default
62
+ * adapter when `entry.adapter` is unset, and
63
+ * switches to the `hydrateCtx` envelope for
64
+ * schedules.
65
+ */
66
+ export function buildDedicatedEntryContent(handlerImportPath, entry, primitiveType, domainId) {
14
67
  const exportName = camelCase(entry.id);
15
68
  if (primitiveType === 'schedule') {
16
69
  return [
@@ -26,14 +79,50 @@ function generateDedicatedEntry(handlerImportPath, entry, primitiveType) {
26
79
  `};`,
27
80
  ].join('\n');
28
81
  }
29
- const adapterMap = {
30
- api: 'createApiLambdaHandler',
31
- subscriber: 'createSubscriberLambdaHandler',
32
- job: 'createJobLambdaHandler',
33
- webhook: 'createWebhookLambdaHandler',
34
- action: 'createActionLambdaHandler',
35
- };
36
- const adapter = adapterMap[primitiveType] ?? 'createApiLambdaHandler';
82
+ // Per-handler adapter override (Wave 4 Task 4.1) takes precedence over the
83
+ // primitive-type default. Used so API-exposed actions wrap with
84
+ // `createExposedActionApiHandler` while internal actions in the same domain
85
+ // continue to use `createActionLambdaHandler`.
86
+ const adapter = entry.adapter ?? ADAPTER_BY_PRIMITIVE[primitiveType] ?? 'createApiLambdaHandler';
87
+ // Wave 7 Task 7.1 (#4619): the action adapters take a (registry, options)
88
+ // pair / (action, options) pair, NOT a bare handler export. Generating
89
+ // `${adapter}(${exportName})` would produce invalid JS for actions — the
90
+ // bundles were silently broken (esbuild succeeded but the runtime crashed
91
+ // on first invocation). We now emit the correct wrapper for each adapter:
92
+ // * internal action → `createActionLambdaHandler(registry, options)`
93
+ // * api-exposed action → `createExposedActionApiHandler(action, options)`
94
+ if (primitiveType === 'action' && adapter !== 'createExposedActionApiHandler') {
95
+ return [
96
+ `import { createActionLambdaHandler } from '@mettlecast/domain-runtime';`,
97
+ `import { ${exportName} } from '${handlerImportPath}';`,
98
+ '',
99
+ `const registry = { ${JSON.stringify(domainId)}: { ${JSON.stringify(entry.id)}: ${exportName} } };`,
100
+ '',
101
+ `export const handler = createActionLambdaHandler(registry, {`,
102
+ ` domainId: ${JSON.stringify(domainId)},`,
103
+ ` databaseUrl: process.env.DATABASE_URL,`,
104
+ ` eventBusName: process.env.EVENT_BUS_NAME,`,
105
+ ` idempotencyTableName: process.env.IDEMPOTENCY_TABLE,`,
106
+ `});`,
107
+ ].join('\n');
108
+ }
109
+ if (primitiveType === 'action' && adapter === 'createExposedActionApiHandler') {
110
+ return [
111
+ `import { createExposedActionApiHandler } from '@mettlecast/domain-runtime';`,
112
+ `import { ${exportName} } from '${handlerImportPath}';`,
113
+ '',
114
+ // Build a single-action registry so the public handler can call its
115
+ // own action via `ctx.actions[domainId][actionId]` (Wave 7 Task 7.1).
116
+ `const actionRegistry = { ${JSON.stringify(domainId)}: { ${JSON.stringify(entry.id)}: ${exportName} } };`,
117
+ '',
118
+ `export const handler = createExposedActionApiHandler(${exportName}, {`,
119
+ ` definingDomain: ${JSON.stringify(domainId)},`,
120
+ ` domainId: ${JSON.stringify(domainId)},`,
121
+ ` actionRegistry,`,
122
+ ` callerDomainId: ${JSON.stringify(domainId)},`,
123
+ `});`,
124
+ ].join('\n');
125
+ }
37
126
  return [
38
127
  `import { ${adapter} } from '@mettlecast/domain-runtime';`,
39
128
  `import { ${exportName} } from '${handlerImportPath}';`,
@@ -76,7 +165,7 @@ export function createGroupedLambdas(scope, props) {
76
165
  const entryDir = createTempEntryDir();
77
166
  const entryPath = join(entryDir, 'entry.ts');
78
167
  const handlerImportPath = relative(entryDir, join(domainRoot, entry.handlerFile)).replace(/\\/g, '/');
79
- const entryContent = generateDedicatedEntry(handlerImportPath.startsWith('.') ? handlerImportPath : `./${handlerImportPath}`, entry, props.primitiveType);
168
+ const entryContent = buildDedicatedEntryContent(handlerImportPath.startsWith('.') ? handlerImportPath : `./${handlerImportPath}`, entry, props.primitiveType, props.domainId);
80
169
  writeFileSync(entryPath, entryContent, 'utf8');
81
170
  return new lambdaNode.NodejsFunction(scope, `${props.domainId}-${props.primitiveType}${groupSuffix}-${entry.id}`, {
82
171
  runtime: lambda.Runtime.NODEJS_22_X,
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- export type { DomainRegistry, RegistryEntry, RegistryEntryKind, BaseRegistryEntry, SerialDeploymentConfig, ApiRegistryEntry, ApiVersionSnapshot, WebhookRegistryEntry, SubscriberRegistryEntry, ScheduleRegistryEntry, JobRegistryEntry, ActionRegistryEntry, IntegrationRegistryEntry, EventRegistryEntry, DomainRegistryEntry, SchemaSnapshot, } from './registry.js';
1
+ export type { DomainRegistry, RegistryEntry, RegistryEntryKind, BaseRegistryEntry, SerialDeploymentConfig, ApiRegistryEntry, ApiVersionSnapshot, WebhookRegistryEntry, SubscriberRegistryEntry, ScheduleRegistryEntry, JobRegistryEntry, ActionRegistryEntry, ActionBackendAccess, ActionExposure, ActionApiExposure, IntegrationRegistryEntry, EventRegistryEntry, DomainRegistryEntry, SchemaSnapshot, } from './registry.js';
2
2
  export { LambdaFactory } from './lambda-factory.js';
3
3
  export type { LambdaFactoryProps } from './lambda-factory.js';
4
- export { createGroupedLambdas } from './grouped-lambda-factory.js';
4
+ export { createGroupedLambdas, buildDedicatedEntryContent, ADAPTER_BY_PRIMITIVE, EXPOSED_ACTION_ADAPTER } from './grouped-lambda-factory.js';
5
5
  export type { PrimitiveType, HandlerEntry, GroupedLambdaProps } from './grouped-lambda-factory.js';
6
6
  export { IamPolicyBuilder } from './iam/iam-policy-builder.js';
7
7
  export type { WebhookPolicyParams, QueuePolicyParams } from './iam/iam-policy-builder.js';
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export { LambdaFactory } from './lambda-factory.js';
2
- export { createGroupedLambdas } from './grouped-lambda-factory.js';
2
+ export { createGroupedLambdas, buildDedicatedEntryContent, ADAPTER_BY_PRIMITIVE, EXPOSED_ACTION_ADAPTER } from './grouped-lambda-factory.js';
3
3
  export { IamPolicyBuilder } from './iam/iam-policy-builder.js';
4
4
  export { ApiConstruct } from './constructs/api-construct.js';
5
5
  export { WebhookConstruct } from './constructs/webhook-construct.js';
@@ -63,6 +63,20 @@ export interface ApiRegistryEntry extends BaseRegistryEntry {
63
63
  method: string;
64
64
  /** Authentication type. */
65
65
  authType: 'jwt' | 'api-key' | 'none';
66
+ /**
67
+ * Required for `authType: 'none'` (anonymous) APIs. Carries a free-text
68
+ * reason that documents the security exception so reviewers can audit
69
+ * the relaxation. The CDK synth-time aspect and the CLI validator both
70
+ * refuse to synthesize anonymous APIs without this field.
71
+ *
72
+ * Added in #4662 Task D — the legacy `defineApi` surface did not
73
+ * require a reason because early projects had only a handful of
74
+ * public routes; that did not scale and the runtime needs the
75
+ * documentation alongside the route.
76
+ */
77
+ securityException?: {
78
+ reason: string;
79
+ };
66
80
  /** Optional deployment overrides. */
67
81
  deployment?: SerialDeploymentConfig;
68
82
  /** Whether this handler may use NAT-backed public internet egress. */
@@ -153,16 +167,110 @@ export interface JobRegistryEntry extends BaseRegistryEntry {
153
167
  /** Whether this handler may use NAT-backed public internet egress. */
154
168
  outboundAccess?: RegistryOutboundAccess;
155
169
  }
170
+ /**
171
+ * Backend invocation permission for an action.
172
+ *
173
+ * Controls who can call the action through `ctx.actions` (cross-domain
174
+ * Lambda invocation or runtime action calls).
175
+ *
176
+ * - `private` = only same-domain/internal generated adapters
177
+ * - `domain` = callable by other domains through ctx.actions, subject to policy
178
+ * - `platform` = callable by system jobs, flows, admin tooling
179
+ */
180
+ export type ActionBackendAccess = 'private' | 'domain' | 'platform';
181
+ /**
182
+ * API-exposure metadata for an action.
183
+ *
184
+ * Present only when `exposure.type === 'api'`. Carries the route, method,
185
+ * auth, tenancy, and any required role or documented security exception
186
+ * that downstream CDK/contract generation needs to wire the route safely.
187
+ *
188
+ * The optional `authDeclared` and `tenancyDeclared` flags are populated by
189
+ * the registry builder to record whether the source code explicitly declared
190
+ * each field, or whether the builder fell back to the safe default. They are
191
+ * consumed by the domain CLI's validate command (Wave 6 Task 6.1) to enforce
192
+ * the action-first security model (#4619).
193
+ */
194
+ export interface ActionApiExposure {
195
+ /** Discriminant. */
196
+ type: 'api';
197
+ /** HTTP route path, e.g. '/v1/tenants/{tenantId}/billing/invoices'. */
198
+ path: string;
199
+ /** HTTP method. One of: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS. */
200
+ method: string;
201
+ /** Authentication requirement for the route. */
202
+ auth: 'required' | 'none' | 'service';
203
+ /** Tenancy requirement for the route. */
204
+ tenancy: 'required' | 'none' | 'system';
205
+ /** Required role(s) for `auth: 'required'` routes (system tenancy requires this). */
206
+ roles?: string[];
207
+ /** Documented exception when auth or tenancy is intentionally not required. */
208
+ securityException?: {
209
+ reason: string;
210
+ };
211
+ /**
212
+ * True if the source code explicitly declared `auth`. False if the
213
+ * builder fell back to the safe default (`'required'`). Validation-only.
214
+ */
215
+ authDeclared?: boolean;
216
+ /**
217
+ * True if the source code explicitly declared `tenancy`. False if the
218
+ * builder fell back to the safe default (`'required'`). Validation-only.
219
+ */
220
+ tenancyDeclared?: boolean;
221
+ }
222
+ /**
223
+ * Discriminated union of all supported action exposure shapes.
224
+ *
225
+ * `internal` actions are not exposed externally; they are reachable only
226
+ * through `ctx.actions`. `api` actions are exposed via API Gateway and
227
+ * carry the route + auth metadata needed for safe route generation.
228
+ */
229
+ export type ActionExposure = ActionApiExposure | {
230
+ type: 'internal';
231
+ };
156
232
  /**
157
233
  * Action registry entry for callable domain actions.
234
+ *
235
+ * `backendAccess` is the canonical permission field for backend invocation
236
+ * and replaces the legacy `visibility` field during the
237
+ * action-first migration (#4619). `visibility` is preserved as an optional
238
+ * deprecated field so existing CDK constructs continue to compile until
239
+ * they are updated to consume `backendAccess` directly.
158
240
  */
159
241
  export interface ActionRegistryEntry extends BaseRegistryEntry {
160
242
  /** Discriminant. */
161
243
  kind: 'action';
162
244
  /** Handler file path (required for action). */
163
245
  handlerFile: string;
164
- /** Visibility scope: 'private', 'domain', or 'workspace'. */
165
- visibility: 'private' | 'domain' | 'workspace';
246
+ /**
247
+ * Backend invocation permission for the action.
248
+ * Replaces legacy `visibility` for the action-first migration.
249
+ */
250
+ backendAccess: ActionBackendAccess;
251
+ /**
252
+ * External exposure shape for the action. New-style actions must declare
253
+ * an exposure; legacy actions default to `{ type: 'internal' }` during
254
+ * migration to preserve existing behavior.
255
+ */
256
+ exposure: ActionExposure;
257
+ /**
258
+ * True if the source code explicitly declared `exposure`. False (or
259
+ * omitted) if the builder fell back to the default `{ type: 'internal' }`
260
+ * because the source did not opt in. Consumed by the domain CLI's
261
+ * validate command (Wave 6 Task 6.1) to enforce `ACTION_EXPOSURE_REQUIRED`.
262
+ */
263
+ exposureDeclared?: boolean;
264
+ /**
265
+ * Legacy visibility scope. Retained during the action-first migration so
266
+ * CDK constructs that still read this field (e.g. action-construct.ts)
267
+ * keep compiling. New source code should set `backendAccess` instead;
268
+ * the registry builder maps legacy `visibility` -> `backendAccess` with
269
+ * `workspace` collapsing to `domain` per the migration spec.
270
+ *
271
+ * @deprecated Use `backendAccess` instead.
272
+ */
273
+ visibility?: 'private' | 'domain' | 'workspace';
166
274
  /** Whether the action enforces idempotency. */
167
275
  idempotent: boolean;
168
276
  /** Optional deployment overrides. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mettlecast/domain-cdk-packer",
3
- "version": "0.2.60",
3
+ "version": "0.2.61",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "registry": "https://registry.npmjs.org",