@mettlecast/domain-cdk-packer 0.2.0

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 (66) hide show
  1. package/README.md +83 -0
  2. package/dist/DomainStack.d.ts +68 -0
  3. package/dist/DomainStack.js +567 -0
  4. package/dist/__tests__/domain-stack.test.d.ts +1 -0
  5. package/dist/__tests__/domain-stack.test.js +223 -0
  6. package/dist/__tests__/lambda-factory.test.d.ts +1 -0
  7. package/dist/__tests__/lambda-factory.test.js +76 -0
  8. package/dist/__tests__/pack-flows.test.d.ts +1 -0
  9. package/dist/__tests__/pack-flows.test.js +283 -0
  10. package/dist/__tests__/registry.test.d.ts +1 -0
  11. package/dist/__tests__/registry.test.js +34 -0
  12. package/dist/__tests__/step-functions-codegen.test.d.ts +1 -0
  13. package/dist/__tests__/step-functions-codegen.test.js +159 -0
  14. package/dist/aspects/iam-boundaries-aspect.d.ts +11 -0
  15. package/dist/aspects/iam-boundaries-aspect.js +15 -0
  16. package/dist/aspects/index.d.ts +5 -0
  17. package/dist/aspects/index.js +3 -0
  18. package/dist/aspects/log-retention-aspect.d.ts +8 -0
  19. package/dist/aspects/log-retention-aspect.js +24 -0
  20. package/dist/aspects/tagging-aspect.d.ts +13 -0
  21. package/dist/aspects/tagging-aspect.js +17 -0
  22. package/dist/constructs/action-construct.d.ts +35 -0
  23. package/dist/constructs/action-construct.js +35 -0
  24. package/dist/constructs/alarm-construct.d.ts +19 -0
  25. package/dist/constructs/alarm-construct.js +58 -0
  26. package/dist/constructs/alarms-construct.d.ts +17 -0
  27. package/dist/constructs/alarms-construct.js +42 -0
  28. package/dist/constructs/api-construct.d.ts +30 -0
  29. package/dist/constructs/api-construct.js +64 -0
  30. package/dist/constructs/canary-construct.d.ts +13 -0
  31. package/dist/constructs/canary-construct.js +30 -0
  32. package/dist/constructs/cmk-construct.d.ts +9 -0
  33. package/dist/constructs/cmk-construct.js +20 -0
  34. package/dist/constructs/dashboard-construct.d.ts +15 -0
  35. package/dist/constructs/dashboard-construct.js +62 -0
  36. package/dist/constructs/health-construct.d.ts +11 -0
  37. package/dist/constructs/health-construct.js +29 -0
  38. package/dist/constructs/job-construct.d.ts +33 -0
  39. package/dist/constructs/job-construct.js +54 -0
  40. package/dist/constructs/schedule-construct.d.ts +27 -0
  41. package/dist/constructs/schedule-construct.js +54 -0
  42. package/dist/constructs/subscriber-construct.d.ts +30 -0
  43. package/dist/constructs/subscriber-construct.js +63 -0
  44. package/dist/constructs/waf-construct.d.ts +13 -0
  45. package/dist/constructs/waf-construct.js +75 -0
  46. package/dist/constructs/webhook-construct.d.ts +33 -0
  47. package/dist/constructs/webhook-construct.js +50 -0
  48. package/dist/flow-registry.d.ts +81 -0
  49. package/dist/flow-registry.js +2 -0
  50. package/dist/grouped-lambda-factory.d.ts +43 -0
  51. package/dist/grouped-lambda-factory.js +71 -0
  52. package/dist/iam/iam-policy-builder.d.ts +95 -0
  53. package/dist/iam/iam-policy-builder.js +232 -0
  54. package/dist/index.d.ts +36 -0
  55. package/dist/index.js +17 -0
  56. package/dist/lambda-factory.d.ts +46 -0
  57. package/dist/lambda-factory.js +80 -0
  58. package/dist/pack-domain.d.ts +35 -0
  59. package/dist/pack-domain.js +32 -0
  60. package/dist/pack-flows.d.ts +28 -0
  61. package/dist/pack-flows.js +154 -0
  62. package/dist/registry.d.ts +224 -0
  63. package/dist/registry.js +1 -0
  64. package/dist/step-functions-codegen.d.ts +65 -0
  65. package/dist/step-functions-codegen.js +55 -0
  66. package/package.json +36 -0
@@ -0,0 +1,223 @@
1
+ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
2
+ import * as cdk from 'aws-cdk-lib';
3
+ import { Template } from 'aws-cdk-lib/assertions';
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ import { DomainStack } from '../DomainStack.js';
7
+ const DOMAIN_ROOT = '/tmp/test-domain-cdk-packer';
8
+ const minimalRegistry = {
9
+ schemaVersion: '1',
10
+ domainRoot: DOMAIN_ROOT,
11
+ domain: { id: 'test-domain', kind: 'domain', name: 'Test Domain', tenancy: 'none' },
12
+ apis: [
13
+ {
14
+ id: 'get-users',
15
+ kind: 'api',
16
+ handlerFile: 'src/handlers/get-users.ts',
17
+ path: '/users',
18
+ method: 'GET',
19
+ authType: 'jwt',
20
+ },
21
+ ],
22
+ webhooks: [],
23
+ subscribers: [],
24
+ schedules: [],
25
+ jobs: [],
26
+ actions: [],
27
+ integrations: [],
28
+ events: [],
29
+ };
30
+ beforeAll(() => {
31
+ const handlerDir = path.join(DOMAIN_ROOT, 'src', 'handlers');
32
+ fs.mkdirSync(handlerDir, { recursive: true });
33
+ fs.writeFileSync(path.join(handlerDir, 'get-users.ts'), 'export const handler = async () => ({ statusCode: 200 });\n');
34
+ });
35
+ afterAll(() => {
36
+ fs.rmSync(DOMAIN_ROOT, { recursive: true, force: true });
37
+ });
38
+ describe('DomainStack', () => {
39
+ const eventBusArn = 'arn:aws:events:eu-north-1:123456789012:event-bus/tib-event-bus';
40
+ it('synthesises without error for minimal registry', () => {
41
+ const app = new cdk.App();
42
+ expect(() => new DomainStack(app, 'TestDomainStack', { registry: minimalRegistry, eventBusArn })).not.toThrow();
43
+ });
44
+ it('template contains HttpApi', () => {
45
+ const app = new cdk.App();
46
+ const stack = new DomainStack(app, 'TestDomainStack2', { registry: minimalRegistry, eventBusArn });
47
+ const template = Template.fromStack(stack);
48
+ template.resourceCountIs('AWS::ApiGatewayV2::Api', 1);
49
+ });
50
+ it('template contains one Lambda function for the api entry', () => {
51
+ const app = new cdk.App();
52
+ const stack = new DomainStack(app, 'TestDomainStack3', { registry: minimalRegistry, eventBusArn });
53
+ const template = Template.fromStack(stack);
54
+ template.resourceCountIs('AWS::Lambda::Function', 4);
55
+ });
56
+ it('attaches JWT authorizer to jwt-auth API routes when userPoolArn provided', () => {
57
+ const app = new cdk.App();
58
+ const userPoolArn = 'arn:aws:cognito-idp:eu-north-1:123456789012:userpool/eu-north-1_abc123xyz';
59
+ const userPoolClientId = 'test-client-id';
60
+ const stack = new DomainStack(app, 'TestDomainStackJwt', {
61
+ registry: minimalRegistry,
62
+ eventBusArn,
63
+ userPoolArn,
64
+ userPoolClientId,
65
+ });
66
+ const template = Template.fromStack(stack);
67
+ template.hasResourceProperties('AWS::ApiGatewayV2::Authorizer', {
68
+ AuthorizerType: 'JWT',
69
+ });
70
+ });
71
+ it('no authorizer attached when authType is none', () => {
72
+ const app = new cdk.App();
73
+ const registryNoAuth = {
74
+ ...minimalRegistry,
75
+ apis: [
76
+ {
77
+ ...minimalRegistry.apis[0],
78
+ authType: 'none',
79
+ },
80
+ ],
81
+ };
82
+ const stack = new DomainStack(app, 'TestDomainStackNoAuth', {
83
+ registry: registryNoAuth,
84
+ eventBusArn,
85
+ });
86
+ const template = Template.fromStack(stack);
87
+ template.resourceCountIs('AWS::ApiGatewayV2::Authorizer', 0);
88
+ });
89
+ it('DB_SECRET_ARN env var set on Lambdas when dbSecretArn provided', () => {
90
+ const app = new cdk.App();
91
+ const dbSecretArn = 'arn:aws:secretsmanager:eu-north-1:123456789012:secret:db-secret-abc123';
92
+ const stack = new DomainStack(app, 'TestDomainStackDbSecret', {
93
+ registry: minimalRegistry,
94
+ eventBusArn,
95
+ dbSecretArn,
96
+ });
97
+ const template = Template.fromStack(stack);
98
+ template.allResources('AWS::Lambda::Function', {
99
+ Environment: {
100
+ Variables: {
101
+ Match: {
102
+ stringLike: {
103
+ DB_SECRET_ARN: dbSecretArn,
104
+ },
105
+ },
106
+ },
107
+ },
108
+ });
109
+ });
110
+ it('secretsmanager:GetSecretValue grant added when dbSecretArn provided', () => {
111
+ const app = new cdk.App();
112
+ const dbSecretArn = 'arn:aws:secretsmanager:eu-north-1:123456789012:secret:db-secret-abc123';
113
+ const stack = new DomainStack(app, 'TestDomainStackSecretGrant', {
114
+ registry: minimalRegistry,
115
+ eventBusArn,
116
+ dbSecretArn,
117
+ });
118
+ const template = Template.fromStack(stack);
119
+ template.allResources('AWS::IAM::Policy', {
120
+ PolicyDocument: {
121
+ Match: {
122
+ objectLike: {
123
+ Statement: [
124
+ {
125
+ Match: {
126
+ objectLike: {
127
+ Action: ['secretsmanager:GetSecretValue'],
128
+ Resource: [dbSecretArn],
129
+ },
130
+ },
131
+ },
132
+ ],
133
+ },
134
+ },
135
+ },
136
+ });
137
+ });
138
+ describe('domain S3 bucket', () => {
139
+ const app = new cdk.App();
140
+ const stack = new DomainStack(app, 'TestDomainStackS3', { registry: minimalRegistry, eventBusArn });
141
+ const template = Template.fromStack(stack);
142
+ it('creates exactly one S3 bucket', () => {
143
+ template.resourceCountIs('AWS::S3::Bucket', 1);
144
+ });
145
+ it('enables versioning on the domain bucket', () => {
146
+ template.hasResourceProperties('AWS::S3::Bucket', {
147
+ VersioningConfiguration: {
148
+ Status: 'Enabled',
149
+ },
150
+ });
151
+ });
152
+ it('has CfnOutput for bucket name', () => {
153
+ template.hasOutput('DomainBucketName', {});
154
+ });
155
+ });
156
+ describe('tenant-scoped IAM role (defence in depth)', () => {
157
+ const app = new cdk.App();
158
+ const stack = new DomainStack(app, 'TestDomainStackTenant', { registry: minimalRegistry, eventBusArn });
159
+ const template = Template.fromStack(stack);
160
+ it('creates a tenant-scoped IAM role', () => {
161
+ template.hasResourceProperties('AWS::IAM::Role', {
162
+ RoleName: 'tib-test-domain-tenant-scoped',
163
+ });
164
+ });
165
+ it('sets DOMAIN_TENANT_ROLE_ARN env var on each Lambda', () => {
166
+ const lambdas = template.findResources('AWS::Lambda::Function');
167
+ for (const [id, res] of Object.entries(lambdas)) {
168
+ // Skip health/readiness Lambdas from HealthConstruct
169
+ if (id.includes('Health') || id.includes('Ready'))
170
+ continue;
171
+ expect(res.Properties.Environment.Variables).toHaveProperty('DOMAIN_TENANT_ROLE_ARN');
172
+ }
173
+ });
174
+ it('grants Lambda execution roles sts:AssumeRole + sts:TagSession on the tenant-scoped role', () => {
175
+ template.hasResourceProperties('AWS::IAM::Policy', {
176
+ PolicyDocument: {
177
+ Statement: expect.arrayContaining([
178
+ expect.objectContaining({
179
+ Action: expect.arrayContaining(['sts:AssumeRole', 'sts:TagSession']),
180
+ Effect: 'Allow',
181
+ }),
182
+ ]),
183
+ },
184
+ });
185
+ });
186
+ it('tenant-scoped role has trust policy with request-tag condition', () => {
187
+ template.hasResourceProperties('AWS::IAM::Role', {
188
+ AssumeRolePolicyDocument: {
189
+ Statement: expect.arrayContaining([
190
+ expect.objectContaining({
191
+ Action: expect.arrayContaining(['sts:AssumeRole', 'sts:TagSession']),
192
+ Effect: 'Allow',
193
+ Condition: expect.objectContaining({
194
+ StringLike: expect.objectContaining({
195
+ 'aws:RequestTag/tenantId': '*',
196
+ }),
197
+ }),
198
+ }),
199
+ ]),
200
+ },
201
+ });
202
+ });
203
+ it('tenant-scoped role policy uses PrincipalTag conditions', () => {
204
+ template.hasResourceProperties('AWS::IAM::Policy', {
205
+ Roles: expect.arrayContaining([expect.objectContaining({ Ref: expect.any(String) })]),
206
+ PolicyDocument: {
207
+ Statement: expect.arrayContaining([
208
+ expect.objectContaining({
209
+ Condition: expect.objectContaining({
210
+ 'ForAllValues:StringEquals': expect.objectContaining({
211
+ 'dynamodb:LeadingKeys': ['${aws:PrincipalTag/tenantId}'],
212
+ }),
213
+ }),
214
+ }),
215
+ ]),
216
+ },
217
+ });
218
+ });
219
+ it('has CfnOutput for tenant-scoped role ARN', () => {
220
+ template.hasOutput('DomainTenantScopedRoleArn', {});
221
+ });
222
+ });
223
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,76 @@
1
+ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
2
+ import * as cdk from 'aws-cdk-lib';
3
+ import { Template } from 'aws-cdk-lib/assertions';
4
+ import * as lambdaNode from 'aws-cdk-lib/aws-lambda-nodejs';
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+ import { LambdaFactory } from '../lambda-factory.js';
8
+ const DOMAIN_ROOT = '/tmp/test-domain-cdk-packer';
9
+ const minimalRegistry = {
10
+ schemaVersion: '1',
11
+ domainRoot: DOMAIN_ROOT,
12
+ domain: { id: 'test-domain', kind: 'domain', name: 'Test Domain', tenancy: 'none' },
13
+ apis: [], webhooks: [], subscribers: [], schedules: [], jobs: [], actions: [], integrations: [], events: [],
14
+ };
15
+ beforeAll(() => {
16
+ const handlerDir = path.join(DOMAIN_ROOT, 'src');
17
+ fs.mkdirSync(handlerDir, { recursive: true });
18
+ fs.writeFileSync(path.join(handlerDir, 'api.ts'), 'export const handler = async () => ({ statusCode: 200 });\n');
19
+ });
20
+ afterAll(() => {
21
+ fs.rmSync(DOMAIN_ROOT, { recursive: true, force: true });
22
+ });
23
+ describe('LambdaFactory', () => {
24
+ it('creates a NodejsFunction construct without throwing', () => {
25
+ const app = new cdk.App();
26
+ const stack = new cdk.Stack(app, 'TestStack');
27
+ const factory = new LambdaFactory({ scope: stack, registry: minimalRegistry, domainRoot: minimalRegistry.domainRoot });
28
+ expect(() => {
29
+ factory.createFunction({ id: 'test-api', kind: 'api', handlerFile: 'src/api.ts' });
30
+ }).not.toThrow();
31
+ });
32
+ describe('bundling configuration', () => {
33
+ it('template has Lambda with ARM_64 architecture', () => {
34
+ const app = new cdk.App();
35
+ const stack = new cdk.Stack(app, 'TestStackArch');
36
+ const factory = new LambdaFactory({ scope: stack, registry: minimalRegistry, domainRoot: minimalRegistry.domainRoot });
37
+ factory.createFunction({ id: 'test-api', kind: 'api', handlerFile: 'src/api.ts' });
38
+ const template = Template.fromStack(stack);
39
+ template.hasResourceProperties('AWS::Lambda::Function', {
40
+ Architectures: ['arm64'],
41
+ });
42
+ });
43
+ it('template has Lambda with NODEJS_22_X runtime', () => {
44
+ const app = new cdk.App();
45
+ const stack = new cdk.Stack(app, 'TestStackRuntime');
46
+ const factory = new LambdaFactory({ scope: stack, registry: minimalRegistry, domainRoot: minimalRegistry.domainRoot });
47
+ factory.createFunction({ id: 'test-api', kind: 'api', handlerFile: 'src/api.ts' });
48
+ const template = Template.fromStack(stack);
49
+ template.hasResourceProperties('AWS::Lambda::Function', {
50
+ Runtime: 'nodejs22.x',
51
+ });
52
+ });
53
+ it('template has Lambda with NODE_OPTIONS for source maps', () => {
54
+ const app = new cdk.App();
55
+ const stack = new cdk.Stack(app, 'TestStackSourceMaps');
56
+ const factory = new LambdaFactory({ scope: stack, registry: minimalRegistry, domainRoot: minimalRegistry.domainRoot });
57
+ factory.createFunction({ id: 'test-api', kind: 'api', handlerFile: 'src/api.ts' });
58
+ const template = Template.fromStack(stack);
59
+ template.hasResourceProperties('AWS::Lambda::Function', {
60
+ Environment: {
61
+ Variables: {
62
+ NODE_OPTIONS: '--enable-source-maps',
63
+ },
64
+ },
65
+ });
66
+ });
67
+ it('creates NodejsFunction with required bundling defaults (minify, INLINE sourceMap, treeShaking, external @aws-sdk/*)', () => {
68
+ const app = new cdk.App();
69
+ const stack = new cdk.Stack(app, 'TestStackDefaults');
70
+ const factory = new LambdaFactory({ scope: stack, registry: minimalRegistry, domainRoot: minimalRegistry.domainRoot });
71
+ const fn = factory.createFunction({ id: 'test-api', kind: 'api', handlerFile: 'src/api.ts' });
72
+ expect(fn).toBeInstanceOf(lambdaNode.NodejsFunction);
73
+ expect(fn.runtime?.name).toContain('nodejs22');
74
+ });
75
+ });
76
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,283 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { FlowsStack, packFlows } from '../pack-flows.js';
3
+ import * as cdk from 'aws-cdk-lib';
4
+ describe('packFlows', () => {
5
+ it('creates a FlowsStack from a registry', () => {
6
+ const app = new cdk.App();
7
+ const registry = {
8
+ schemaVersion: '1',
9
+ flows: [
10
+ {
11
+ id: 'test-flow',
12
+ name: 'Test Flow',
13
+ steps: [
14
+ {
15
+ type: 'flow-control',
16
+ control: 'succeed',
17
+ name: 'End',
18
+ },
19
+ ],
20
+ },
21
+ ],
22
+ };
23
+ const stack = packFlows(registry, app, 'test-flows', {
24
+ domainLambdaArns: {},
25
+ eventBusArn: 'arn:aws:events:us-east-1:123456789012:event-bus/test',
26
+ });
27
+ expect(stack).toBeInstanceOf(FlowsStack);
28
+ expect(stack.flowArnMap).toBeDefined();
29
+ });
30
+ it('throws when domain-action step references missing Lambda ARN', () => {
31
+ const app = new cdk.App();
32
+ const registry = {
33
+ schemaVersion: '1',
34
+ flows: [
35
+ {
36
+ id: 'action-flow',
37
+ name: 'Action Flow',
38
+ steps: [
39
+ {
40
+ type: 'domain-action',
41
+ name: 'RunAction',
42
+ domainId: 'example',
43
+ actionId: 'process',
44
+ },
45
+ ],
46
+ },
47
+ ],
48
+ };
49
+ expect(() => {
50
+ packFlows(registry, app, 'test-flows', {
51
+ domainLambdaArns: {},
52
+ eventBusArn: 'arn:aws:events:us-east-1:123456789012:event-bus/test',
53
+ });
54
+ }).toThrow(/no Lambda ARN for example-action/);
55
+ });
56
+ it('translates domain-action step with Lambda ARN', () => {
57
+ const app = new cdk.App();
58
+ const registry = {
59
+ schemaVersion: '1',
60
+ flows: [
61
+ {
62
+ id: 'action-flow',
63
+ name: 'Action Flow',
64
+ steps: [
65
+ {
66
+ type: 'domain-action',
67
+ name: 'RunAction',
68
+ domainId: 'example',
69
+ actionId: 'process',
70
+ },
71
+ ],
72
+ },
73
+ ],
74
+ };
75
+ const stack = packFlows(registry, app, 'test-flows', {
76
+ domainLambdaArns: {
77
+ 'example-action': 'arn:aws:lambda:us-east-1:123456789012:function:test',
78
+ },
79
+ eventBusArn: 'arn:aws:events:us-east-1:123456789012:event-bus/test',
80
+ });
81
+ expect(stack.flowArnMap['action-flow']).toBeDefined();
82
+ });
83
+ it('translates domain-event step to putEvents', () => {
84
+ const app = new cdk.App();
85
+ const registry = {
86
+ schemaVersion: '1',
87
+ flows: [
88
+ {
89
+ id: 'event-flow',
90
+ name: 'Event Flow',
91
+ steps: [
92
+ {
93
+ type: 'domain-event',
94
+ name: 'EmitEvent',
95
+ eventId: 'example.completed',
96
+ payload: { status: 'ok' },
97
+ version: 1,
98
+ },
99
+ ],
100
+ },
101
+ ],
102
+ };
103
+ const stack = packFlows(registry, app, 'test-flows', {
104
+ domainLambdaArns: {},
105
+ eventBusArn: 'arn:aws:events:us-east-1:123456789012:event-bus/test',
106
+ });
107
+ expect(stack.flowArnMap['event-flow']).toBeDefined();
108
+ });
109
+ it('handles empty flow registry gracefully', () => {
110
+ const app = new cdk.App();
111
+ const registry = {
112
+ schemaVersion: '1',
113
+ flows: [],
114
+ };
115
+ const stack = packFlows(registry, app, 'test-flows', {
116
+ domainLambdaArns: {},
117
+ eventBusArn: 'arn:aws:events:us-east-1:123456789012:event-bus/test',
118
+ });
119
+ expect(Object.keys(stack.flowArnMap)).toHaveLength(0);
120
+ });
121
+ it('creates EXPRESS state machine when flow type is express', () => {
122
+ const app = new cdk.App();
123
+ const registry = {
124
+ schemaVersion: '1',
125
+ flows: [
126
+ {
127
+ id: 'express-flow',
128
+ type: 'express',
129
+ name: 'Express Flow',
130
+ steps: [{ type: 'flow-control', control: 'succeed', name: 'End' }],
131
+ },
132
+ ],
133
+ };
134
+ const stack = packFlows(registry, app, 'test-flows-express', {
135
+ domainLambdaArns: {},
136
+ eventBusArn: 'arn:aws:events:us-east-1:123456789012:event-bus/test',
137
+ });
138
+ expect(stack.flowArnMap['express-flow']).toBeDefined();
139
+ });
140
+ it('creates STANDARD state machine when flow type is standard', () => {
141
+ const app = new cdk.App();
142
+ const registry = {
143
+ schemaVersion: '1',
144
+ flows: [
145
+ {
146
+ id: 'standard-flow',
147
+ type: 'standard',
148
+ name: 'Standard Flow',
149
+ steps: [{ type: 'flow-control', control: 'succeed', name: 'End' }],
150
+ },
151
+ ],
152
+ };
153
+ const stack = packFlows(registry, app, 'test-flows-standard', {
154
+ domainLambdaArns: {},
155
+ eventBusArn: 'arn:aws:events:us-east-1:123456789012:event-bus/test',
156
+ });
157
+ expect(stack.flowArnMap['standard-flow']).toBeDefined();
158
+ });
159
+ it('defaults to EXPRESS state machine when type is not specified', () => {
160
+ const app = new cdk.App();
161
+ const registry = {
162
+ schemaVersion: '1',
163
+ flows: [
164
+ {
165
+ id: 'default-flow',
166
+ name: 'Default Flow',
167
+ steps: [{ type: 'flow-control', control: 'succeed', name: 'End' }],
168
+ },
169
+ ],
170
+ };
171
+ const stack = packFlows(registry, app, 'test-flows-default', {
172
+ domainLambdaArns: {},
173
+ eventBusArn: 'arn:aws:events:us-east-1:123456789012:event-bus/test',
174
+ });
175
+ expect(stack.flowArnMap['default-flow']).toBeDefined();
176
+ });
177
+ it('adds IAM lambda:InvokeFunction grant when flow has domain-action steps', () => {
178
+ const app = new cdk.App();
179
+ const actionArn = 'arn:aws:lambda:eu-north-1:123456789012:function:example-process';
180
+ const registry = {
181
+ schemaVersion: '1',
182
+ flows: [
183
+ {
184
+ id: 'cross-domain-flow',
185
+ name: 'Cross Domain Flow',
186
+ steps: [
187
+ {
188
+ type: 'domain-action',
189
+ name: 'CallExternalDomain',
190
+ domainId: 'example',
191
+ actionId: 'process',
192
+ },
193
+ ],
194
+ },
195
+ ],
196
+ };
197
+ // Should not throw — ARN is provided and IAM grant added
198
+ const stack = packFlows(registry, app, 'test-cross-domain', {
199
+ domainLambdaArns: { 'example-action': actionArn },
200
+ eventBusArn: 'arn:aws:events:eu-north-1:123456789012:event-bus/test',
201
+ });
202
+ expect(stack.flowArnMap['cross-domain-flow']).toBeDefined();
203
+ });
204
+ it('skips IAM grant when domain-action step has no matching Lambda ARN', () => {
205
+ const app = new cdk.App();
206
+ const registry = {
207
+ schemaVersion: '1',
208
+ flows: [
209
+ {
210
+ id: 'no-arn-flow',
211
+ name: 'No ARN Flow',
212
+ steps: [
213
+ { type: 'flow-control', control: 'succeed', name: 'End' },
214
+ ],
215
+ },
216
+ ],
217
+ };
218
+ // No domain-action steps — no IAM grant needed
219
+ const stack = packFlows(registry, app, 'test-no-arn', {
220
+ domainLambdaArns: {},
221
+ eventBusArn: 'arn:aws:events:eu-north-1:123456789012:event-bus/test',
222
+ });
223
+ expect(stack.flowArnMap['no-arn-flow']).toBeDefined();
224
+ });
225
+ it('emits waitForTaskToken resource for domain-action step with waitForTaskToken=true', () => {
226
+ const app = new cdk.App();
227
+ const registry = {
228
+ schemaVersion: '1',
229
+ flows: [
230
+ {
231
+ id: 'wftt-flow',
232
+ type: 'standard',
233
+ name: 'Wait Flow',
234
+ steps: [
235
+ {
236
+ type: 'domain-action',
237
+ name: 'PauseStep',
238
+ domainId: 'example',
239
+ actionId: 'pause',
240
+ waitForTaskToken: true,
241
+ next: 'End',
242
+ },
243
+ { type: 'flow-control', control: 'succeed', name: 'End' },
244
+ ],
245
+ },
246
+ ],
247
+ };
248
+ const stack = packFlows(registry, app, 'test-wftt', {
249
+ domainLambdaArns: { 'example-action': 'arn:aws:lambda:eu-north-1:123:function:example-pause' },
250
+ eventBusArn: 'arn:aws:events:eu-north-1:123:event-bus/test',
251
+ });
252
+ expect(stack.flowArnMap['wftt-flow']).toBeDefined();
253
+ expect(stack.taskTokensTableName).toBeDefined();
254
+ });
255
+ it('creates the DDB task tokens table in the stack', () => {
256
+ const app = new cdk.App();
257
+ const registry = { schemaVersion: '1', flows: [] };
258
+ const stack = packFlows(registry, app, 'test-ddb', {
259
+ domainLambdaArns: {},
260
+ eventBusArn: 'arn:aws:events:eu-north-1:123:event-bus/test',
261
+ });
262
+ expect(stack.taskTokensTableName).toBeDefined();
263
+ expect(stack.taskTokensTableArn).toBeDefined();
264
+ });
265
+ });
266
+ describe('IamPolicyBuilder.forFlow', () => {
267
+ it('returns empty array when no action ARNs', async () => {
268
+ const { IamPolicyBuilder } = await import('../iam/iam-policy-builder.js');
269
+ const builder = new IamPolicyBuilder();
270
+ expect(builder.forFlow([])).toHaveLength(0);
271
+ });
272
+ it('returns lambda:InvokeFunction statement for provided ARNs', async () => {
273
+ const { IamPolicyBuilder } = await import('../iam/iam-policy-builder.js');
274
+ const builder = new IamPolicyBuilder();
275
+ const arns = [
276
+ 'arn:aws:lambda:eu-north-1:123:function:auth-verify-token',
277
+ 'arn:aws:lambda:eu-north-1:123:function:payments-charge-card',
278
+ ];
279
+ const statements = builder.forFlow(arns);
280
+ expect(statements).toHaveLength(1);
281
+ expect(JSON.stringify(statements[0]?.toStatementJson())).toContain('lambda:InvokeFunction');
282
+ });
283
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,34 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ describe('DomainRegistry type', () => {
3
+ it('accepts a valid minimal registry object', () => {
4
+ const r = {
5
+ schemaVersion: '1',
6
+ domainRoot: '/workspace/my-domain',
7
+ domain: { id: 'my-domain', kind: 'domain', name: 'My Domain', tenancy: 'required' },
8
+ apis: [],
9
+ webhooks: [],
10
+ subscribers: [],
11
+ schedules: [],
12
+ jobs: [],
13
+ actions: [],
14
+ integrations: [],
15
+ events: [],
16
+ };
17
+ expect(r.schemaVersion).toBe('1');
18
+ expect(r.domain.kind).toBe('domain');
19
+ });
20
+ it('discriminates RegistryEntry union by kind', () => {
21
+ const entries = [
22
+ { id: 'test-api', kind: 'api', handlerFile: 'src/api.ts', path: '/test', method: 'GET', authType: 'jwt' },
23
+ { id: 'test-job', kind: 'job', handlerFile: 'src/job.ts', maxRetries: 3, visibilityTimeoutSeconds: 30 },
24
+ ];
25
+ for (const entry of entries) {
26
+ if (entry.kind === 'api') {
27
+ expect(entry.path).toBeDefined();
28
+ }
29
+ if (entry.kind === 'job') {
30
+ expect(entry.maxRetries).toBeDefined();
31
+ }
32
+ }
33
+ });
34
+ });
@@ -0,0 +1 @@
1
+ export {};