@mettlecast/domain-cdk-packer 0.2.102 → 0.2.104

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.
@@ -175,6 +175,11 @@ export class DomainStack extends cdk.Stack {
175
175
  const environment = {
176
176
  TIB_DOMAIN_ID: domainId,
177
177
  TIB_EVENT_BUS_ARN: eventBusArn,
178
+ // #5282: the runtime resolves the `ctx.publish` bus from
179
+ // EVENT_BUS_NAME (falling back to 'default'). Inject the shared bus
180
+ // name so published domain events land on the project-scoped bus.
181
+ // Prefer the supplied name; derive from the ARN when it is omitted.
182
+ EVENT_BUS_NAME: eventBusName ?? cdk.Fn.select(1, cdk.Fn.split('/', eventBusArn)),
178
183
  };
179
184
  if (databaseUrl) {
180
185
  environment.DATABASE_URL = databaseUrl;
@@ -248,6 +253,7 @@ export class DomainStack extends cdk.Stack {
248
253
  apiEntries,
249
254
  environment,
250
255
  eventBusArn,
256
+ eventBusName,
251
257
  reservedConcurrency,
252
258
  logRetentionDays: logRetentionDays ?? 30,
253
259
  lambdaGroupId: group.outboundAccess,
@@ -278,6 +284,7 @@ export class DomainStack extends cdk.Stack {
278
284
  }),
279
285
  environment,
280
286
  eventBusArn,
287
+ eventBusName,
281
288
  dedicated: internalDedicated,
282
289
  // Deterministic physical name so OTHER domain stacks can resolve
283
290
  // this Lambda's ARN for cross-domain `ctx.actions` dispatch.
@@ -319,6 +326,7 @@ export class DomainStack extends cdk.Stack {
319
326
  }),
320
327
  environment,
321
328
  eventBusArn,
329
+ eventBusName,
322
330
  dedicated,
323
331
  reservedConcurrency,
324
332
  logRetentionDays: logRetentionDays ?? 30,
@@ -620,6 +628,12 @@ export class DomainStack extends cdk.Stack {
620
628
  actionLambdas.forEach((fn) => {
621
629
  iamPolicies.forEach(statement => fn.addToRolePolicy(statement));
622
630
  });
631
+ // #5282: actions publish domain events via `ctx.publish`. The runtime
632
+ // targets the bus named by EVENT_BUS_NAME, so the execution role needs
633
+ // events:PutEvents scoped to the shared bus ARN.
634
+ actionLambdas.forEach((fn) => {
635
+ eventBus.grantPutEventsTo(fn);
636
+ });
623
637
  // Add dbSecretArn grant if provided
624
638
  if (dbSecretArn) {
625
639
  actionLambdas.forEach(fn => fn.addToRolePolicy(new iam.PolicyStatement({
@@ -91,6 +91,14 @@ describe('auth registry — bootstrap-invite registration (#5226)', () => {
91
91
  expect(bootstrapInviteSource).toContain("VALUES ($1, 'system', NULL, $2, 'sys_admin')");
92
92
  expect(bootstrapInviteSource).not.toContain("00000000-0000-0000-0000-000000000000");
93
93
  });
94
+ it('runs SQL through the raw pg client (ctx.db.client.query), not the Drizzle builder', () => {
95
+ // Regression for #5277: `DbContext.query` is the Drizzle query builder —
96
+ // an object, not a function. Calling it (`ctx.db.query(sql, params)`)
97
+ // throws `TypeError: e.db.query is not a function` at runtime. Raw SQL
98
+ // must go through the raw pg client exposed as `DbContext.client`.
99
+ expect(bootstrapInviteSource).toContain('ctx.db.client.query(');
100
+ expect(bootstrapInviteSource).not.toContain('ctx.db.query(');
101
+ });
94
102
  });
95
103
  describe('full documented build process — all current domains', () => {
96
104
  it('generates a registry file for EVERY domain under domains/ (auth, data-management, email, orgs)', () => {
@@ -568,4 +568,69 @@ describe('DomainStack', () => {
568
568
  expect(actionRoute.Properties.AuthorizerId).toBeDefined();
569
569
  });
570
570
  });
571
+ /**
572
+ * Issue #5282 — API action event publishing.
573
+ *
574
+ * Production evidence: the auth API-action Lambda inserted a bootstrap
575
+ * invitation but `ctx.publish('auth.member.invited')` failed with
576
+ * `AccessDeniedException: events:PutEvents` on the EventBridge default bus.
577
+ * The runtime resolves the `ctx.publish` bus from the `EVENT_BUS_NAME` env
578
+ * var (falling back to 'default') and the grouped API-action Lambda's
579
+ * execution role previously had no `events:PutEvents`. These synth tests
580
+ * pin both halves of the packer fix.
581
+ */
582
+ describe('action event publishing (#5282)', () => {
583
+ const busName = 'tib-event-bus';
584
+ it('injects EVENT_BUS_NAME into the grouped API-action Lambda', () => {
585
+ const app = new cdk.App();
586
+ const stack = new DomainStack(app, 'TestDomainStackEventBusName', {
587
+ registry: minimalRegistry,
588
+ eventBusArn,
589
+ eventBusName: busName,
590
+ projectId: 'Test',
591
+ envCode: 'Dev',
592
+ });
593
+ const template = Template.fromStack(stack);
594
+ const lambdas = template.findResources('AWS::Lambda::Function');
595
+ const actionApiLambdas = Object.values(lambdas).filter(l => l.Properties.Environment?.Variables?.POWERTOOLS_SERVICE_NAME === 'test-domain-action-api');
596
+ expect(actionApiLambdas.length).toBeGreaterThan(0);
597
+ for (const fn of actionApiLambdas) {
598
+ expect(fn.Properties.Environment?.Variables?.EVENT_BUS_NAME).toBe(busName);
599
+ }
600
+ });
601
+ it('derives EVENT_BUS_NAME from the bus ARN when eventBusName is omitted', () => {
602
+ const app = new cdk.App();
603
+ const stack = new DomainStack(app, 'TestDomainStackEventBusDerived', {
604
+ registry: minimalRegistry,
605
+ eventBusArn,
606
+ projectId: 'Test',
607
+ envCode: 'Dev',
608
+ });
609
+ const template = Template.fromStack(stack);
610
+ const lambdas = template.findResources('AWS::Lambda::Function');
611
+ const actionApiLambdas = Object.values(lambdas).filter(l => l.Properties.Environment?.Variables?.POWERTOOLS_SERVICE_NAME === 'test-domain-action-api');
612
+ expect(actionApiLambdas.length).toBeGreaterThan(0);
613
+ for (const fn of actionApiLambdas) {
614
+ // Without an explicit name the packer derives it from the supplied
615
+ // bus ARN tail (`...:event-bus/tib-event-bus` → `tib-event-bus`) so
616
+ // published events target the shared bus, not the 'default' one.
617
+ expect(fn.Properties.Environment?.Variables?.EVENT_BUS_NAME).toBe('tib-event-bus');
618
+ }
619
+ });
620
+ it('grants action Lambda roles events:PutEvents scoped to the shared bus', () => {
621
+ const app = new cdk.App();
622
+ const stack = new DomainStack(app, 'TestDomainStackPutEvents', {
623
+ registry: minimalRegistry,
624
+ eventBusArn,
625
+ eventBusName: busName,
626
+ projectId: 'Test',
627
+ envCode: 'Dev',
628
+ });
629
+ const template = Template.fromStack(stack);
630
+ const policies = Object.values(template.findResources('AWS::IAM::Policy'));
631
+ const hasScopedPutEvents = policies.some(resource => resource.Properties.PolicyDocument.Statement.some(statement => statement.Action?.includes('events:PutEvents')
632
+ && JSON.stringify(statement.Resource).includes('event-bus/tib-event-bus')));
633
+ expect(hasScopedPutEvents).toBe(true);
634
+ });
635
+ });
571
636
  });
@@ -45,6 +45,13 @@ export interface GroupedLambdaProps {
45
45
  environment: Record<string, string>;
46
46
  /** ARN of the project-scoped EventBridge bus. */
47
47
  eventBusArn: string;
48
+ /**
49
+ * Name of the project-scoped EventBridge bus. Injected as `EVENT_BUS_NAME`
50
+ * so the runtime's `ctx.publish` targets the shared bus instead of falling
51
+ * back to the AWS 'default' bus (#5282). When omitted, the name is derived
52
+ * from `eventBusArn` (e.g. `arn:...:event-bus/my-bus` → `my-bus`).
53
+ */
54
+ eventBusName?: string;
48
55
  /** VPC to place Lambdas in — required for Aurora connectivity. */
49
56
  vpc?: ec2.IVpc;
50
57
  /** Security groups to attach to the Lambda(s). */
@@ -200,6 +200,7 @@ export function createGroupedLambdas(scope, props) {
200
200
  POWERTOOLS_LOG_LEVEL: 'INFO',
201
201
  TIB_HANDLER_ID: entry.id,
202
202
  TIB_EVENT_BUS_ARN: props.eventBusArn,
203
+ EVENT_BUS_NAME: resolveEventBusName(props.eventBusName, props.eventBusArn),
203
204
  },
204
205
  timeout: cdk.Duration.seconds(30),
205
206
  memorySize: 256,
@@ -226,6 +227,7 @@ export function createGroupedLambdas(scope, props) {
226
227
  POWERTOOLS_LOG_LEVEL: 'INFO',
227
228
  TIB_HANDLER_MAP: JSON.stringify(props.handlerEntries.map(e => e.id)),
228
229
  TIB_EVENT_BUS_ARN: props.eventBusArn,
230
+ EVENT_BUS_NAME: resolveEventBusName(props.eventBusName, props.eventBusArn),
229
231
  },
230
232
  timeout: cdk.Duration.seconds(30),
231
233
  memorySize: 256,
@@ -235,6 +237,15 @@ export function createGroupedLambdas(scope, props) {
235
237
  }),
236
238
  ];
237
239
  }
240
+ /**
241
+ * Resolves the EventBridge bus name injected as `EVENT_BUS_NAME`.
242
+ * Prefers the explicitly supplied name (avoids ARN parsing when the ARN is a
243
+ * dynamic reference); falls back to deriving the name from the ARN tail
244
+ * (`arn:...:event-bus/{name}` → `{name}`).
245
+ */
246
+ function resolveEventBusName(eventBusName, eventBusArn) {
247
+ return eventBusName ?? cdk.Fn.select(1, cdk.Fn.split('/', eventBusArn));
248
+ }
238
249
  function toLogRetention(days) {
239
250
  const map = {
240
251
  30: logs.RetentionDays.ONE_MONTH,
@@ -329,6 +340,7 @@ export function createGroupedApiActionLambda(scope, props) {
329
340
  POWERTOOLS_SERVICE_NAME: serviceName,
330
341
  POWERTOOLS_LOG_LEVEL: 'INFO',
331
342
  TIB_EVENT_BUS_ARN: props.eventBusArn,
343
+ EVENT_BUS_NAME: resolveEventBusName(props.eventBusName, props.eventBusArn),
332
344
  },
333
345
  timeout: cdk.Duration.seconds(30),
334
346
  memorySize: 256,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mettlecast/domain-cdk-packer",
3
- "version": "0.2.102",
3
+ "version": "0.2.104",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "registry": "https://registry.npmjs.org",