@mettlecast/domain-cdk-packer 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 (30) hide show
  1. package/dist/DomainStack.d.ts +5 -0
  2. package/dist/DomainStack.js +145 -67
  3. package/dist/__tests__/action-construct.test.d.ts +1 -0
  4. package/dist/__tests__/action-construct.test.js +159 -0
  5. package/dist/__tests__/domain-stack.test.js +315 -15
  6. package/dist/__tests__/grouped-lambda-factory-action-wrapper.test.d.ts +11 -0
  7. package/dist/__tests__/grouped-lambda-factory-action-wrapper.test.js +47 -0
  8. package/dist/__tests__/grouped-lambda-factory.test.d.ts +1 -0
  9. package/dist/__tests__/grouped-lambda-factory.test.js +146 -0
  10. package/dist/__tests__/lambda-factory.test.js +7 -7
  11. package/dist/__tests__/registry.test.js +111 -4
  12. package/dist/__tests__/security-assertion-aspect.test.d.ts +1 -0
  13. package/dist/__tests__/security-assertion-aspect.test.js +222 -0
  14. package/dist/aspects/index.d.ts +4 -0
  15. package/dist/aspects/index.js +2 -0
  16. package/dist/aspects/security-assertion-aspect.d.ts +161 -0
  17. package/dist/aspects/security-assertion-aspect.js +226 -0
  18. package/dist/constructs/action-construct.d.ts +16 -6
  19. package/dist/constructs/action-construct.js +10 -11
  20. package/dist/constructs/api-construct.d.ts +10 -2
  21. package/dist/constructs/api-construct.js +25 -5
  22. package/dist/grouped-lambda-factory.d.ts +69 -1
  23. package/dist/grouped-lambda-factory.js +98 -10
  24. package/dist/iam/iam-policy-builder.js +0 -2
  25. package/dist/index.d.ts +2 -2
  26. package/dist/index.js +1 -1
  27. package/dist/pack-domain.d.ts +2 -0
  28. package/dist/pack-domain.js +9 -3
  29. package/dist/registry.d.ts +98 -47
  30. package/package.json +1 -1
@@ -5,7 +5,6 @@ describe('DomainRegistry type', () => {
5
5
  schemaVersion: '1',
6
6
  domainRoot: '/workspace/my-domain',
7
7
  domain: { id: 'my-domain', kind: 'domain', name: 'My Domain', tenancy: 'required' },
8
- apis: [],
9
8
  webhooks: [],
10
9
  subscribers: [],
11
10
  schedules: [],
@@ -19,12 +18,12 @@ describe('DomainRegistry type', () => {
19
18
  });
20
19
  it('discriminates RegistryEntry union by kind', () => {
21
20
  const entries = [
22
- { id: 'test-api', kind: 'api', handlerFile: 'src/api.ts', path: '/test', method: 'GET', authType: 'jwt' },
21
+ { id: 'test-action', kind: 'action', handlerFile: 'src/action.ts', backendAccess: 'domain', exposure: { type: 'api', path: '/test', method: 'GET', auth: 'required', tenancy: 'none' }, idempotent: true },
23
22
  { id: 'test-job', kind: 'job', handlerFile: 'src/job.ts', maxRetries: 3, visibilityTimeoutSeconds: 30 },
24
23
  ];
25
24
  for (const entry of entries) {
26
- if (entry.kind === 'api') {
27
- expect(entry.path).toBeDefined();
25
+ if (entry.kind === 'action') {
26
+ expect(entry.exposure).toBeDefined();
28
27
  }
29
28
  if (entry.kind === 'job') {
30
29
  expect(entry.maxRetries).toBeDefined();
@@ -32,3 +31,111 @@ describe('DomainRegistry type', () => {
32
31
  }
33
32
  });
34
33
  });
34
+ /**
35
+ * Action-first migration (#4619, Wave 1 Task 1.2):
36
+ *
37
+ * ActionRegistryEntry must surface `backendAccess` and `exposure` so the
38
+ * registry builder can carry them through to CDK/contract generation.
39
+ * The legacy `visibility` field stays optional during the migration window.
40
+ */
41
+ describe('ActionRegistryEntry type (action-first migration)', () => {
42
+ it('requires backendAccess and exposure on every action entry', () => {
43
+ const entry = {
44
+ id: 'charge-card',
45
+ kind: 'action',
46
+ handlerFile: 'src/actions/charge-card.ts',
47
+ backendAccess: 'domain',
48
+ exposure: { type: 'internal' },
49
+ idempotent: true,
50
+ };
51
+ expect(entry.backendAccess).toBe('domain');
52
+ expect(entry.exposure).toEqual({ type: 'internal' });
53
+ });
54
+ it('accepts the full backendAccess scope union', () => {
55
+ const scopes = ['private', 'domain', 'platform'];
56
+ for (const backendAccess of scopes) {
57
+ const entry = {
58
+ id: `a-${backendAccess}`,
59
+ kind: 'action',
60
+ handlerFile: 'a.ts',
61
+ backendAccess,
62
+ exposure: { type: 'internal' },
63
+ idempotent: false,
64
+ };
65
+ expect(entry.backendAccess).toBe(backendAccess);
66
+ }
67
+ });
68
+ it('accepts an api exposure with all optional metadata', () => {
69
+ const exposure = {
70
+ type: 'api',
71
+ path: '/v1/tenants/{tenantId}/billing/invoices',
72
+ method: 'POST',
73
+ auth: 'required',
74
+ tenancy: 'required',
75
+ roles: ['billing-admin'],
76
+ securityException: { reason: 'bootstrap path before tenant exists' },
77
+ };
78
+ const entry = {
79
+ id: 'create-invoice',
80
+ kind: 'action',
81
+ handlerFile: 'src/actions/create-invoice.ts',
82
+ backendAccess: 'domain',
83
+ exposure,
84
+ idempotent: true,
85
+ };
86
+ expect(entry.exposure.type).toBe('api');
87
+ if (entry.exposure.type === 'api') {
88
+ expect(entry.exposure.path).toContain('{tenantId}');
89
+ expect(entry.exposure.method).toBe('POST');
90
+ expect(entry.exposure.auth).toBe('required');
91
+ expect(entry.exposure.tenancy).toBe('required');
92
+ expect(entry.exposure.roles).toEqual(['billing-admin']);
93
+ expect(entry.exposure.securityException?.reason).toMatch(/bootstrap/);
94
+ }
95
+ });
96
+ it('discriminates the ActionExposure union by type', () => {
97
+ const samples = [
98
+ { type: 'internal' },
99
+ { type: 'api', path: '/v1/x', method: 'GET', auth: 'none', tenancy: 'none' },
100
+ ];
101
+ for (const exposure of samples) {
102
+ if (exposure.type === 'api') {
103
+ expect(exposure.path).toBeDefined();
104
+ expect(exposure.method).toBe('GET');
105
+ }
106
+ else {
107
+ expect(exposure.type).toBe('internal');
108
+ }
109
+ }
110
+ });
111
+ it('keeps legacy visibility as an optional field during the migration', () => {
112
+ // The legacy field must remain present-but-optional on the type so
113
+ // existing CDK constructs (e.g. action-construct.ts) keep compiling
114
+ // until they are updated to consume backendAccess directly.
115
+ const entry = {
116
+ id: 'legacy',
117
+ kind: 'action',
118
+ handlerFile: 'src/actions/legacy.ts',
119
+ backendAccess: 'private',
120
+ exposure: { type: 'internal' },
121
+ visibility: 'workspace',
122
+ idempotent: false,
123
+ };
124
+ expect(entry.visibility).toBe('workspace');
125
+ expect(entry.backendAccess).toBe('private');
126
+ });
127
+ it('preserves input/output schema snapshot fields', () => {
128
+ const entry = {
129
+ id: 'with-schemas',
130
+ kind: 'action',
131
+ handlerFile: 'src/actions/with-schemas.ts',
132
+ backendAccess: 'domain',
133
+ exposure: { type: 'internal' },
134
+ idempotent: false,
135
+ inputSchema: { type: 'object', properties: { amount: { type: 'number' } } },
136
+ outputSchema: { type: 'object', properties: { id: { type: 'string' } } },
137
+ };
138
+ expect(entry.inputSchema).toBeDefined();
139
+ expect(entry.outputSchema).toBeDefined();
140
+ });
141
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,222 @@
1
+ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
2
+ import * as cdk from 'aws-cdk-lib';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { DomainStack } from '../DomainStack.js';
6
+ const DOMAIN_ROOT = path.join(process.cwd(), '.test-security-aspect');
7
+ const baseRegistry = {
8
+ schemaVersion: '1',
9
+ domainRoot: DOMAIN_ROOT,
10
+ domain: { id: 'sec-domain', kind: 'domain', name: 'Sec Domain', tenancy: 'none' },
11
+ webhooks: [],
12
+ subscribers: [],
13
+ schedules: [],
14
+ jobs: [],
15
+ actions: [],
16
+ integrations: [],
17
+ events: [],
18
+ };
19
+ /**
20
+ * Build a stack, force synth, and return its error annotations so each
21
+ * test can assert specifically on the codes emitted by
22
+ * `SecurityAssertionAspect`. CDK aspects only run during synth, so the
23
+ * helper must call `app.synth()`. Annotations attached by aspects to
24
+ * child constructs (e.g. a `CfnRoute`) live on the child's metadata,
25
+ * not the stack's — we walk the construct tree to aggregate them.
26
+ */
27
+ function buildStackAndGetErrors(registry) {
28
+ const app = new cdk.App();
29
+ const stack = new DomainStack(app, 'SecAspectStack', {
30
+ registry,
31
+ eventBusArn: 'arn:aws:events:eu-north-1:123456789012:event-bus/tib-event-bus',
32
+ });
33
+ // Force synthesis so the aspect's visit() fires.
34
+ try {
35
+ app.synth();
36
+ }
37
+ catch {
38
+ // Synth may throw when the aspect emits a hard error; we still want
39
+ // to read the metadata so the assertion can find the error code.
40
+ }
41
+ const errorMessages = [];
42
+ const collect = (construct) => {
43
+ for (const m of construct.node.metadata) {
44
+ if (m.type === 'aws:cdk:error') {
45
+ errorMessages.push(typeof m.data === 'string' ? m.data : JSON.stringify(m.data));
46
+ }
47
+ }
48
+ for (const child of construct.node.children) {
49
+ collect(child);
50
+ }
51
+ };
52
+ collect(stack);
53
+ return { stack, errorMessages };
54
+ }
55
+ beforeAll(() => {
56
+ const handlerDir = path.join(DOMAIN_ROOT, 'src', 'handlers');
57
+ fs.mkdirSync(handlerDir, { recursive: true });
58
+ // The CDK grouped-lambda-factory derives the named export from the
59
+ // entry id (`list-users` -> `listUsers`). We provide every named export
60
+ // the security-aspect test uses so each test can pick the same handler
61
+ // file regardless of which entry id it instantiates.
62
+ //
63
+ // Issue #4689: defineApi was removed; HTTP endpoints are now declared
64
+ // as actions with `exposure.type === 'api'`. Each named export below
65
+ // matches one of the action ids used in the test fixtures.
66
+ fs.writeFileSync(path.join(handlerDir, 'noop.ts'), [
67
+ // Match the grouped-lambda-factory's export-name conversion so the
68
+ // bundler can resolve the import for every test fixture.
69
+ 'export const listUsers = { id: "list-users", backendAccess: "domain", exposure: { type: "api", path: "/v1/tenants/{tenantId}/users", method: "GET", auth: "required", tenancy: "required" }, input: { parse: (x) => x }, output: { parse: (x) => x }, idempotent: false, handler: async () => ({ ok: true }) };',
70
+ 'export const listAll = { id: "list-all", backendAccess: "domain", exposure: { type: "api", path: "/v1/tenants/{tenantId}/all", method: "GET", auth: "required", tenancy: "required" }, input: { parse: (x) => x }, output: { parse: (x) => x }, idempotent: false, handler: async () => ({ ok: true }) };',
71
+ 'export const noopAction = { id: "noop-action", backendAccess: "private", exposure: { type: "internal" }, input: { parse: (x) => x }, output: { parse: (x) => x }, idempotent: false, handler: async () => ({ ok: true }) };',
72
+ ].join('\n') + '\n');
73
+ });
74
+ afterAll(() => {
75
+ fs.rmSync(DOMAIN_ROOT, { recursive: true, force: true });
76
+ });
77
+ /**
78
+ * Issue #4662 Task D — deployment-time assertions. The CDK aspect is the
79
+ * last line of defence: it walks the synthesised tree and verifies the
80
+ * invariants the CLI validator already enforces. These tests pin each
81
+ * invariant independently so a regression in any single check is caught.
82
+ */
83
+ describe('SecurityAssertionAspect (#4662 Task D, #4689)', () => {
84
+ it('does not emit errors for an empty registry', () => {
85
+ const { errorMessages } = buildStackAndGetErrors(baseRegistry);
86
+ // The base stack emits zero routes / zero Function URLs, so the aspect
87
+ // should have nothing to complain about.
88
+ const aspectCodes = ['SECURITY_MISSING_JWT_AUTHORIZER', 'SECURITY_MISSING_TENANT_PATH', 'SECURITY_ACTION_FUNCTION_URL', 'SECURITY_MISSING_SECURITY_EXCEPTION'];
89
+ expect(errorMessages.filter(m => aspectCodes.some(c => m.includes(c)))).toHaveLength(0);
90
+ });
91
+ it('emits SECURITY_MISSING_TENANT_PATH when an action API path lacks the tenant placeholder', () => {
92
+ // Issue #4689: the canonical HTTP endpoint surface is the action
93
+ // array with `exposure.type === 'api'`. The aspect now reads from
94
+ // `actions[]` rather than `apis[]`.
95
+ const registry = {
96
+ ...baseRegistry,
97
+ actions: [
98
+ {
99
+ id: 'list-users', kind: 'action', handlerFile: 'src/handlers/noop.ts',
100
+ backendAccess: 'domain',
101
+ exposure: { type: 'api', path: '/users', method: 'GET', auth: 'required', tenancy: 'required' },
102
+ idempotent: false,
103
+ },
104
+ ],
105
+ };
106
+ const { errorMessages } = buildStackAndGetErrors(registry);
107
+ expect(errorMessages.some(m => m.includes('SECURITY_MISSING_TENANT_PATH') && m.includes('list-users'))).toBe(true);
108
+ });
109
+ it('does NOT emit SECURITY_MISSING_TENANT_PATH when the action API path includes the placeholder', () => {
110
+ const registry = {
111
+ ...baseRegistry,
112
+ actions: [
113
+ {
114
+ id: 'list-users', kind: 'action', handlerFile: 'src/handlers/noop.ts',
115
+ backendAccess: 'domain',
116
+ exposure: { type: 'api', path: '/v1/tenants/{tenantId}/users', method: 'GET', auth: 'required', tenancy: 'required' },
117
+ idempotent: false,
118
+ },
119
+ ],
120
+ };
121
+ // We expect the JWT authorizer error (because no Cognito is provided),
122
+ // but NOT the tenant path error.
123
+ const { errorMessages } = buildStackAndGetErrors(registry);
124
+ expect(errorMessages.some(m => m.includes('SECURITY_MISSING_TENANT_PATH'))).toBe(false);
125
+ });
126
+ it('emits SECURITY_MISSING_JWT_AUTHORIZER when an action API is declared without Cognito config', () => {
127
+ const registry = {
128
+ ...baseRegistry,
129
+ actions: [
130
+ {
131
+ id: 'list-users', kind: 'action', handlerFile: 'src/handlers/noop.ts',
132
+ backendAccess: 'domain',
133
+ exposure: { type: 'api', path: '/v1/tenants/{tenantId}/users', method: 'GET', auth: 'required', tenancy: 'required' },
134
+ idempotent: false,
135
+ },
136
+ ],
137
+ };
138
+ const { errorMessages } = buildStackAndGetErrors(registry);
139
+ expect(errorMessages.some(m => m.includes('SECURITY_MISSING_JWT_AUTHORIZER') || m.includes('JWT-protected routes'))).toBe(true);
140
+ });
141
+ it('does NOT emit SECURITY_MISSING_SECURITY_EXCEPTION when authType is none but the route is the health probe', () => {
142
+ // The DomainStack installs the health/ready probes as `authType: 'none'` routes.
143
+ // Because they are special-cased in the aspect (and CLI validator
144
+ // ignores them), the aspect must NOT fire for them.
145
+ const registry = {
146
+ ...baseRegistry,
147
+ };
148
+ const { errorMessages } = buildStackAndGetErrors(registry);
149
+ expect(errorMessages.some(m => m.includes('SECURITY_MISSING_SECURITY_EXCEPTION'))).toBe(false);
150
+ });
151
+ it('does NOT emit Function URL error for the existing health/ready probes', () => {
152
+ // HealthConstruct attaches `Lambda::Url` to its functions. The aspect
153
+ // exempts them via fn-name regex; we must not regress that exemption.
154
+ const registry = { ...baseRegistry };
155
+ const { errorMessages } = buildStackAndGetErrors(registry);
156
+ // HealthConstruct uses NodejsFunction which DOES NOT create Lambda::Url
157
+ // (URLs are only created when fn.addFunctionUrl is called). So there
158
+ // should be no Function URL errors at all on the base stack.
159
+ expect(errorMessages.some(m => m.includes('SECURITY_ACTION_FUNCTION_URL'))).toBe(false);
160
+ });
161
+ it('emits SECURITY_MISSING_TENANT_PATH for an action exposure with tenancy=required but bad path', () => {
162
+ const registry = {
163
+ ...baseRegistry,
164
+ actions: [
165
+ {
166
+ id: 'list-all',
167
+ kind: 'action',
168
+ handlerFile: 'src/handlers/noop.ts',
169
+ backendAccess: 'domain',
170
+ exposure: { type: 'api', path: '/v1/admin/users', method: 'GET', auth: 'required', tenancy: 'required' },
171
+ idempotent: false,
172
+ },
173
+ ],
174
+ };
175
+ const { errorMessages } = buildStackAndGetErrors(registry);
176
+ expect(errorMessages.some(m => m.includes('SECURITY_MISSING_TENANT_PATH') && m.includes('list-all'))).toBe(true);
177
+ });
178
+ it('emits SECURITY_MISSING_JWT_AUTHORIZER for an action exposure with auth=required but no Cognito', () => {
179
+ const registry = {
180
+ ...baseRegistry,
181
+ actions: [
182
+ {
183
+ id: 'list-users',
184
+ kind: 'action',
185
+ handlerFile: 'src/handlers/noop.ts',
186
+ backendAccess: 'domain',
187
+ exposure: { type: 'api', path: '/v1/tenants/{tenantId}/users', method: 'GET', auth: 'required', tenancy: 'required' },
188
+ idempotent: false,
189
+ },
190
+ ],
191
+ };
192
+ const { errorMessages } = buildStackAndGetErrors(registry);
193
+ expect(errorMessages.some(m => m.includes('SECURITY_MISSING_JWT_AUTHORIZER') && m.includes('list-users'))).toBe(true);
194
+ });
195
+ it('does NOT emit SECURITY_MISSING_SECURITY_EXCEPTION for an action with auth=none AND securityException.reason', () => {
196
+ const registry = {
197
+ ...baseRegistry,
198
+ actions: [
199
+ {
200
+ id: 'noop-action',
201
+ kind: 'action',
202
+ handlerFile: 'src/handlers/noop.ts',
203
+ backendAccess: 'platform',
204
+ exposure: {
205
+ type: 'api',
206
+ path: '/v1/health',
207
+ method: 'GET',
208
+ auth: 'none',
209
+ tenancy: 'none',
210
+ securityException: { reason: 'public liveness probe; ticket OPS-123' },
211
+ },
212
+ idempotent: false,
213
+ },
214
+ ],
215
+ };
216
+ const { errorMessages } = buildStackAndGetErrors(registry);
217
+ // Note: the CLI validator's AUTH_NONE_REQUIRES_EXCEPTION rule mirrors
218
+ // this; we only check that the *aspect* does not block a well-formed
219
+ // action.
220
+ expect(errorMessages.some(m => m.includes('SECURITY_MISSING_SECURITY_EXCEPTION'))).toBe(false);
221
+ });
222
+ });
@@ -3,3 +3,7 @@ export type { TibTaggingAspectProps } from './tagging-aspect.js';
3
3
  export { LogRetentionAspect } from './log-retention-aspect.js';
4
4
  export { IamBoundariesAspect } from './iam-boundaries-aspect.js';
5
5
  export type { IamBoundariesAspectProps } from './iam-boundaries-aspect.js';
6
+ export { SecurityAssertionAspect } from './security-assertion-aspect.js';
7
+ export type { SecurityAssertionAspectProps } from './security-assertion-aspect.js';
8
+ export { SECURITY_ASSERTION_CODES } from './security-assertion-aspect.js';
9
+ export type { SecurityAssertionCode } from './security-assertion-aspect.js';
@@ -1,3 +1,5 @@
1
1
  export { TibTaggingAspect } from './tagging-aspect.js';
2
2
  export { LogRetentionAspect } from './log-retention-aspect.js';
3
3
  export { IamBoundariesAspect } from './iam-boundaries-aspect.js';
4
+ export { SecurityAssertionAspect } from './security-assertion-aspect.js';
5
+ export { SECURITY_ASSERTION_CODES } from './security-assertion-aspect.js';
@@ -0,0 +1,161 @@
1
+ import * as cdk from 'aws-cdk-lib';
2
+ import { IConstruct } from 'constructs';
3
+ import type { DomainRegistry } from '../registry.js';
4
+ /**
5
+ * Properties for {@link SecurityAssertionAspect}.
6
+ *
7
+ * The aspect reads the `DomainRegistry` exactly once at synthesis time and
8
+ * then walks every CDK construct for violations. All checks are read-only —
9
+ * the aspect never modifies the synthesized graph, only emits annotations.
10
+ *
11
+ * Issue #4662 Task D — deployment-time assertions. The CLI validator at
12
+ * `packages/domain-cli` already enforces the same rules against the
13
+ * `DomainRegistry` before synth, but the aspect is the last line of defence:
14
+ * it runs even if the CLI was skipped, and it can catch regressions in the
15
+ * synthesized graph itself (e.g. a route accidentally wired without an
16
+ * authorizer, or a Lambda exposed via Function URL).
17
+ */
18
+ export interface SecurityAssertionAspectProps {
19
+ /** The compiled domain registry — source of truth for what was declared. */
20
+ registry: DomainRegistry;
21
+ /**
22
+ * When true, missing JWT authorizer on a JWT-required route is reported
23
+ * as a synth-blocking error rather than a warning. Default: true.
24
+ *
25
+ * Set to false during the Wave 4 Task 4.1 roll-out so we could ship
26
+ * fix-ups one stack at a time. New deployments should leave it on.
27
+ */
28
+ failOnMissingAuthorizer?: boolean;
29
+ /**
30
+ * When true, an API-exposed action whose handler Lambda is reachable
31
+ * through a `Lambda::Url` resource is a synth-blocking error rather
32
+ * than a warning. Default: true — Function URLs create public, IAM-
33
+ * or NONE-authenticated endpoints that bypass API Gateway entirely.
34
+ */
35
+ failOnFunctionUrl?: boolean;
36
+ }
37
+ /**
38
+ * Result codes emitted by the aspect. Mirrored in the CLI validator so
39
+ * downstream tooling can correlate synth-time annotations with build-time
40
+ * validation errors. Each code corresponds to a single, narrowly-scoped
41
+ * invariant — see the per-method docstrings for the exact contract.
42
+ */
43
+ export declare const SECURITY_ASSERTION_CODES: {
44
+ /**
45
+ * A JWT-required route (api.authType='jwt' or action.exposure.auth='required')
46
+ * was synthesised without an attached `CfnAuthorizer` ID. Synth blocks.
47
+ */
48
+ readonly MISSING_JWT_AUTHORIZER: "SECURITY_MISSING_JWT_AUTHORIZER";
49
+ /**
50
+ * A tenancy-required route (api requiring a tenant, or action.exposure.tenancy='required')
51
+ * does not include the canonical tenant placeholder
52
+ * (`/v1/tenants/{tenantId}/`) in its path. The runtime cannot bind
53
+ * `ctx.tenant.id` from a path that does not carry the placeholder, so
54
+ * this would silently produce a tenant-less route. Synth blocks.
55
+ */
56
+ readonly MISSING_TENANT_PATH: "SECURITY_MISSING_TENANT_PATH";
57
+ /**
58
+ * An action Lambda is reachable through a `Lambda::Url` resource. This
59
+ * creates a public-or-IAM endpoint that bypasses API Gateway, the JWT
60
+ * authorizer, and the registry's exposure metadata. Synth blocks.
61
+ */
62
+ readonly ACTION_FUNCTION_URL: "SECURITY_ACTION_FUNCTION_URL";
63
+ /**
64
+ * An API route is declared `auth: 'none'` or `authType: 'none'` without
65
+ * a `securityException.reason`. Public routes must document the
66
+ * exception so security reviewers can audit the relaxation. Synth
67
+ * blocks — anonymous routes are the exception, not the default.
68
+ */
69
+ readonly MISSING_SECURITY_EXCEPTION: "SECURITY_MISSING_SECURITY_EXCEPTION";
70
+ };
71
+ export type SecurityAssertionCode = typeof SECURITY_ASSERTION_CODES[keyof typeof SECURITY_ASSERTION_CODES];
72
+ /**
73
+ * CDK Aspect that performs deployment-time security assertions against
74
+ * the synthesised {@link DomainStack}.
75
+ *
76
+ * The aspect complements the CLI validator at `packages/domain-cli`. Both
77
+ * layers run from the same source of truth (`DomainRegistry`), but they
78
+ * fail at different points in the pipeline:
79
+ *
80
+ * - The CLI validator fails the build BEFORE synth, so the developer
81
+ * gets a structured error code in their terminal and CI fails on
82
+ * `mc-domain-module validate`.
83
+ * - The aspect fails synth AFTER the stack has been assembled. It can
84
+ * detect runtime regressions (e.g. a route accidentally synthesised
85
+ * without an authorizer because a feature flag silently skipped the
86
+ * wiring) that the CLI cannot see.
87
+ *
88
+ * Adding the aspect to a stack is a one-liner:
89
+ *
90
+ * ```ts
91
+ * cdk.Aspects.of(stack).add(new SecurityAssertionAspect({ registry }));
92
+ * ```
93
+ *
94
+ * The {@link DomainStack} constructor already wires this aspect in for
95
+ * every domain, so most code does not need to interact with the class
96
+ * directly.
97
+ */
98
+ export declare class SecurityAssertionAspect implements cdk.IAspect {
99
+ private readonly props;
100
+ constructor(props: SecurityAssertionAspectProps);
101
+ /**
102
+ * CDK calls this for every node in the construct tree. The aspect is
103
+ * intentionally narrow: it inspects the synthesised graph for the
104
+ * specific invariants listed above. It does NOT validate the registry
105
+ * itself — the CLI owns that responsibility and emitting duplicate
106
+ * errors here would be noisy.
107
+ *
108
+ * @param node - The CDK construct being visited.
109
+ */
110
+ visit(node: IConstruct): void;
111
+ /**
112
+ * Walk every `CfnRoute` in the construct tree and verify the security
113
+ * invariants in one pass:
114
+ *
115
+ * 1. Routes declared as JWT-protected actually carry an `AuthorizerId`.
116
+ * Covers action API exposures (`exposure.auth === 'required'`).
117
+ * 2. Routes declared as tenant-required include the canonical
118
+ * `/v1/tenants/{tenantId}/` placeholder in their path.
119
+ *
120
+ * Both checks fail synth by default; the authorizer check can be
121
+ * downgraded to a warning via {@link SecurityAssertionAspectProps.failOnMissingAuthorizer}
122
+ * for incremental rollouts.
123
+ */
124
+ private assertRouteSecurity;
125
+ /**
126
+ * Walk every `CfnUrl` in the construct tree and verify no action
127
+ * Lambda is reachable through a `Lambda::Url`. Wave 4 Task 4.2 (#4619)
128
+ * explicitly removed Function URL exposure because the resulting
129
+ * endpoints bypass API Gateway, the JWT authorizer, and the
130
+ * `backendAccess` / `exposure` policy gates.
131
+ *
132
+ * Health and readiness probes are explicitly exempted — they use
133
+ * `Lambda::Url` for cheap internal polling and the runtime never
134
+ * routes tenant traffic through them.
135
+ */
136
+ private assertNoFunctionUrls;
137
+ /**
138
+ * Walk every `CfnRoute` whose action registry counterpart declares
139
+ * `auth: 'none'` and verify that the action exposure carries a
140
+ * `securityException.reason`. Public routes must document the exception
141
+ * so security reviewers can audit the relaxation.
142
+ */
143
+ private assertAnonymousRoutesHaveException;
144
+ /**
145
+ * Find the registry action whose `exposure.path` matches a synthesised
146
+ * route for action API exposures.
147
+ */
148
+ private findActionRegistryEntryForRoute;
149
+ /**
150
+ * Predicate — does the action registry entry carry a securityException
151
+ * on its exposure block?
152
+ */
153
+ private hasActionSecurityException;
154
+ /**
155
+ * Predicate — does the path contain `/v1/tenants/{tenantId}/`?
156
+ *
157
+ * We do not use a regex because the placeholder is a literal string
158
+ * and the runtime extractor performs an identical literal match.
159
+ */
160
+ private hasTenantPlaceholder;
161
+ }