@venturekit/infra 0.0.0-dev.20260701100017 → 0.0.0-dev.20260704225856

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.
package/README.md CHANGED
@@ -29,13 +29,11 @@ Create a `vk.config.ts` at your project root:
29
29
  ```typescript
30
30
  import { defineVenture } from '@venturekit/infra';
31
31
  import { base } from './config/base';
32
- import { security } from './config/security';
33
32
  import { dev } from './config/dev';
34
33
  import { prod } from './config/prod';
35
34
 
36
35
  export default defineVenture({
37
36
  base,
38
- security,
39
37
  envs: { dev, prod },
40
38
  routesDir: 'src/routes',
41
39
  });
@@ -46,7 +44,6 @@ export default defineVenture({
46
44
  ```typescript
47
45
  export default defineVenture({
48
46
  base,
49
- security,
50
47
  envs: { dev, prod },
51
48
  routesDir: 'src/routes',
52
49
  infrastructure: {
@@ -213,12 +210,11 @@ VPC itself is not replaced and all other resources stay put.
213
210
  Creates a VentureKit application from a `VentureDefinition`:
214
211
 
215
212
  - `base` — `BaseConfig` (project identity)
216
- - `security` — `SecurityConfig` (scopes, clients)
217
213
  - `envs` — `{ dev?, stage?, prod? }` environment configurations
218
214
  - `routesDir` — path to file-based routes directory
219
215
  - `infrastructure?` — `VentureIntent` for declarative infrastructure
220
216
 
221
- ### `getResolvedConfig(base, security, envInput)`
217
+ ### `getResolvedConfig(base, envInput)`
222
218
 
223
219
  Returns the fully resolved `ResolvedConfig` for the current environment. Useful for accessing config values in handler code.
224
220
 
@@ -126,6 +126,21 @@ export interface AppStackImports {
126
126
  storageBucketNamesById?: Map<string, string>;
127
127
  /** SQS queue ARNs per `queues` intent — for grant + ESM source. */
128
128
  queueArnsById?: Map<string, string>;
129
+ /**
130
+ * EventBridge bus names per OWNED `events` intent. Injected into the
131
+ * Lambda environment as `EVENT_BUS_<ID>_NAME` (+ `VENTURE_EVENT_BUS_NAME`
132
+ * for the primary) so `@venturekit/runtime`'s `publishEvent()` resolves
133
+ * the bus without an SSM read. Subscriber-only intents (those with
134
+ * `external.busName`) are NOT in this map — they reference the bus by
135
+ * name on the rule side and never publish.
136
+ */
137
+ eventBusNamesById?: Map<string, string>;
138
+ /**
139
+ * EventBridge bus ARNs per OWNED `events` intent — the `events:PutEvents`
140
+ * IAM scoping target on the shared Lambda role, and the handle the app
141
+ * stack re-imports to attach owned-bus subscription rules.
142
+ */
143
+ eventBusArnsById?: Map<string, string>;
129
144
  /** SNS notify events topic ARNs per `notify` intent. */
130
145
  notifyEventsTopicArnsById?: Map<string, string>;
131
146
  /** CloudFront CDN domain per storage intent that has `cdn:true`. */
@@ -389,6 +404,79 @@ export declare class VentureAppStack extends cdk.Stack {
389
404
  * directly to the caller, where they belong.
390
405
  */
391
406
  private createStandaloneFunction;
407
+ /**
408
+ * Materialise every `events[]` intent's subscriptions as EventBridge
409
+ * `Rule`s on the target bus. Each rule routes events matching its
410
+ * pattern (source + detail-type) to ONE in-venture target — an SQS
411
+ * queue (delivered as a message its `queueBatchHandler` consumer
412
+ * reads, at-least-once) or a Lambda (invoked async).
413
+ *
414
+ * Bus resolution:
415
+ * - OWNED bus → re-imported from the messaging tier by ARN
416
+ * (`imports.eventBusArnsById`).
417
+ * - `external.busName` → imported by name (`fromEventBusName`);
418
+ * same account + region, owner venture deployed first. This is how
419
+ * a vertical venture attaches a rule to the control-plane
420
+ * venture's bus without either calling the other.
421
+ */
422
+ private createEventSubscriptions;
423
+ /**
424
+ * One EventBridge `Rule` on `bus` for subscription `sub`. The pattern
425
+ * defaults to `{ source: [busSource], detailType: sub.detailTypes }`,
426
+ * with `sub.sources` / `sub.pattern` overriding (the raw `pattern`
427
+ * escape hatch is shallow-merged LAST so it always wins). CDK's target
428
+ * constructs add the resource policy that lets EventBridge invoke the
429
+ * Lambda / send to the queue — for a cross-stack imported queue that
430
+ * is an `AWS::SQS::QueuePolicy` in this app stack, leaving the
431
+ * messaging-tier queue untouched.
432
+ */
433
+ private createEventRule;
434
+ /**
435
+ * Render declared `monitoring` intents into CloudWatch alarms
436
+ * (REQUIREMENTS §7.4). Until this existed a `MonitoringIntent` in
437
+ * vk.config.ts was a SILENT NO-OP — the intent reached the stack but
438
+ * nothing materialised a `cloudwatch.Alarm`.
439
+ *
440
+ * Each `AlarmIntent` becomes one `cloudwatch.Alarm` over a
441
+ * `cloudwatch.Metric` built verbatim from the intent (namespace /
442
+ * metric / statistic / period / threshold / comparison /
443
+ * evaluationPeriods / dimensions). When the parent `MonitoringIntent`
444
+ * carries a `notificationArn`, the alarm's ALARM action publishes to
445
+ * that (imported) SNS topic.
446
+ *
447
+ * # Dimension ergonomics
448
+ *
449
+ * Lambda function names and EventBridge rule names are clamped /
450
+ * prefixed at synth (`clampLambdaFunctionName`,
451
+ * `${project}-${stage}-cron-${name}`), so a consumer can't predict
452
+ * them in vk.config.ts. To keep the §7.4 Lambda-error / cron-failure
453
+ * alarms declarable, two dimension keys accept a LOGICAL handler name
454
+ * and are resolved here to the physical name:
455
+ * - `FunctionName` — matched against every Lambda this stack created
456
+ * (route slug, queue / cron / standalone handler name, notify
457
+ * dispatcher/bounce id) → the CDK `functionName` token.
458
+ * - `RuleName` — matched against declared cron handler names →
459
+ * `${project}-${stage}-cron-${name}`.
460
+ * Any other dimension value (an explicit physical name, an RDS
461
+ * `DBInstanceIdentifier`, a custom-metric dimension) passes through
462
+ * verbatim. (RDS identifiers live in the data stack, so they're not
463
+ * auto-resolvable here — supply the physical id, or see the
464
+ * cross-stack follow-up.)
465
+ */
466
+ private createMonitoring;
467
+ /**
468
+ * Resolve an AlarmIntent's dimensions, mapping the logical
469
+ * `FunctionName` / `RuleName` conventions to physical names (see
470
+ * `createMonitoring`). Returns a fresh object; never mutates input.
471
+ */
472
+ private resolveAlarmDimensions;
473
+ /**
474
+ * Find a Lambda this stack created by its LOGICAL name — a route
475
+ * slug, a queue / cron / standalone handler name, or a notify intent
476
+ * id. Returns undefined when nothing matches (the caller then treats
477
+ * the value as an explicit physical name).
478
+ */
479
+ private findLambdaByLogicalName;
392
480
  /**
393
481
  * Attach IAM grants to the shared Lambda role for every imported
394
482
  * resource. Called once during construction, before any Lambda is
@@ -1 +1 @@
1
- {"version":3,"file":"app-stack.d.ts","sourceRoot":"","sources":["../../src/cdk/app-stack.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"app-stack.d.ts","sourceRoot":"","sources":["../../src/cdk/app-stack.ts"],"names":[],"mappings":"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"}
@@ -79,6 +79,8 @@ import * as apigatewayv2 from 'aws-cdk-lib/aws-apigatewayv2';
79
79
  import * as apigatewayv2Integrations from 'aws-cdk-lib/aws-apigatewayv2-integrations';
80
80
  import * as sns from 'aws-cdk-lib/aws-sns';
81
81
  import * as snsSubscriptions from 'aws-cdk-lib/aws-sns-subscriptions';
82
+ import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch';
83
+ import * as cloudwatchActions from 'aws-cdk-lib/aws-cloudwatch-actions';
82
84
  import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager';
83
85
  import * as logs from 'aws-cdk-lib/aws-logs';
84
86
  import { DATA_SAFETY_CONFIG, } from '@venturekit/core';
@@ -88,6 +90,16 @@ import { bundleHandlerCodeWithEsbuild, inlineStubLambdaCode, toLambdaArchitectur
88
90
  import { discoverRouteFiles, } from './shared/route-discovery.js';
89
91
  import { discoverWsRoutes } from './shared/ws-discovery.js';
90
92
  import { attachWebSocketApi, } from './shared/websocket-construct.js';
93
+ /**
94
+ * Provider-agnostic `AlarmIntent.comparison` → CloudWatch operator.
95
+ * Defined once at module scope so `createMonitoring` stays a lookup.
96
+ */
97
+ const ALARM_COMPARISON = {
98
+ gt: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
99
+ gte: cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,
100
+ lt: cloudwatch.ComparisonOperator.LESS_THAN_THRESHOLD,
101
+ lte: cloudwatch.ComparisonOperator.LESS_THAN_OR_EQUAL_TO_THRESHOLD,
102
+ };
91
103
  /**
92
104
  * Convert an intent id (e.g. `user-uploads`) to upper-snake-case
93
105
  * (`USER_UPLOADS`) suitable for use in env-var names. Without this,
@@ -284,6 +296,19 @@ export class VentureAppStack extends cdk.Stack {
284
296
  // coordinated Lambda rollout anyway. RETAIN under strict isn't
285
297
  // appropriate; on a stack delete we're tearing down every Lambda
286
298
  // that uses the key, so retaining the key alone serves no purpose.
299
+ //
300
+ // # Rotation runbook (S5 — manual, dual-secret verify window)
301
+ // This key is symmetric, so it cannot use Secrets Manager's
302
+ // built-in (asymmetric DB-credential) rotation. Rotate WITHOUT
303
+ // downtime via a two-key overlap:
304
+ // 1. Add a SECOND secret (`…/internal-hmac-next`) and roll it to
305
+ // all functions as a fallback VERIFY key (sign with old, accept
306
+ // either) — a brief deploy where verification accepts both.
307
+ // 2. Flip signing to the new key; keep accepting the old one until
308
+ // every in-flight token (state tokens are short-lived) expires.
309
+ // 3. Remove the old key and the dual-verify fallback.
310
+ // Because issued tokens are short-lived, the overlap window can be
311
+ // minutes. Until tooling automates this, treat it as an ops task.
287
312
  this.internalHmacSecret = new secretsmanager.Secret(this, 'internalHmacSecret', {
288
313
  secretName: `venturekit/${projectName}/${stage}/internal-hmac`,
289
314
  description: 'VentureKit internal Lambda-to-Lambda HMAC signing key. Auto-generated.',
@@ -435,6 +460,18 @@ export class VentureAppStack extends cdk.Stack {
435
460
  }
436
461
  }
437
462
  }
463
+ // ─── Event-bus env injection (owned `events` intents) ──────
464
+ // `publishEvent()` resolves the destination bus from
465
+ // `EVENT_BUS_<ID>_NAME`, falling back to `VENTURE_EVENT_BUS_NAME`
466
+ // (the FIRST declared owned bus). Subscriber-only intents
467
+ // (`external.busName`) never publish and so are absent from the map.
468
+ if (imports.eventBusNamesById && imports.eventBusNamesById.size > 0) {
469
+ const [, primaryBusName] = imports.eventBusNamesById.entries().next().value;
470
+ this.baseEnvVars['VENTURE_EVENT_BUS_NAME'] = primaryBusName;
471
+ for (const [id, busName] of imports.eventBusNamesById) {
472
+ this.baseEnvVars[`EVENT_BUS_${upperSnake(id)}_NAME`] = busName;
473
+ }
474
+ }
438
475
  // ─── IAM grants on the shared Lambda role ──────────────────
439
476
  // Every grant attached to `this.lambdaRole` benefits every
440
477
  // Lambda in the stack (routes, queue consumers, schedules,
@@ -653,6 +690,24 @@ export class VentureAppStack extends cdk.Stack {
653
690
  this.provisionNotifyHandlers(infrastructure.notify, props.projectDir, imports);
654
691
  }
655
692
  }
693
+ // ─── EventBridge subscriptions (events[] rules → targets) ──
694
+ // Outside the `projectDir` guard above (like monitoring below) so a
695
+ // queue-target subscription — whose target ARN comes from imports,
696
+ // not handler discovery — still renders for projects that ship no
697
+ // handler code. Placed AFTER the guard so `{ function }` targets
698
+ // resolve to Lambdas discovered above. Owner-side bus creation lives
699
+ // in the messaging stack; here we only attach the rules + targets.
700
+ if (infrastructure.events && infrastructure.events.length > 0) {
701
+ this.createEventSubscriptions(infrastructure.events, imports);
702
+ }
703
+ // ─── CloudWatch alarms (monitoring) ───────────────────────
704
+ // Outside the `projectDir` guard above so data-only projects
705
+ // (custom-namespace or RDS alarms) still render; placed last so
706
+ // the FunctionName / RuleName dimension resolution can see every
707
+ // Lambda + cron rule created above.
708
+ if (infrastructure.monitoring && infrastructure.monitoring.length > 0) {
709
+ this.createMonitoring(infrastructure.monitoring);
710
+ }
656
711
  }
657
712
  /**
658
713
  * Build the Lambda code asset for a handler file. Production path:
@@ -913,6 +968,227 @@ export class VentureAppStack extends cdk.Stack {
913
968
  });
914
969
  this.functionLambdas.set(name, fn);
915
970
  }
971
+ /**
972
+ * Materialise every `events[]` intent's subscriptions as EventBridge
973
+ * `Rule`s on the target bus. Each rule routes events matching its
974
+ * pattern (source + detail-type) to ONE in-venture target — an SQS
975
+ * queue (delivered as a message its `queueBatchHandler` consumer
976
+ * reads, at-least-once) or a Lambda (invoked async).
977
+ *
978
+ * Bus resolution:
979
+ * - OWNED bus → re-imported from the messaging tier by ARN
980
+ * (`imports.eventBusArnsById`).
981
+ * - `external.busName` → imported by name (`fromEventBusName`);
982
+ * same account + region, owner venture deployed first. This is how
983
+ * a vertical venture attaches a rule to the control-plane
984
+ * venture's bus without either calling the other.
985
+ */
986
+ createEventSubscriptions(eventsIntents, imports) {
987
+ for (const intent of eventsIntents) {
988
+ const subs = intent.subscriptions ?? [];
989
+ if (subs.length === 0)
990
+ continue;
991
+ let bus;
992
+ if (intent.external?.busName) {
993
+ bus = events.EventBus.fromEventBusName(this, `events-${intent.id}-imported`, intent.external.busName);
994
+ }
995
+ else {
996
+ const busArn = imports.eventBusArnsById?.get(intent.id);
997
+ if (!busArn) {
998
+ throw new Error(`[venturekit] app stack: events intent '${intent.id}' has ` +
999
+ `subscriptions but no bus to attach them to. Either the ` +
1000
+ `messaging stack must create the bus (CLI passes its ARN via ` +
1001
+ `imports.eventBusArnsById) or the intent must set ` +
1002
+ `'external.busName' to attach rules to a bus owned by another ` +
1003
+ `venture.`);
1004
+ }
1005
+ bus = events.EventBus.fromEventBusArn(this, `events-${intent.id}-owned`, busArn);
1006
+ }
1007
+ const defaultSource = intent.source ?? this.projectName;
1008
+ for (const sub of subs) {
1009
+ this.createEventRule(intent, sub, bus, defaultSource, imports);
1010
+ }
1011
+ }
1012
+ }
1013
+ /**
1014
+ * One EventBridge `Rule` on `bus` for subscription `sub`. The pattern
1015
+ * defaults to `{ source: [busSource], detailType: sub.detailTypes }`,
1016
+ * with `sub.sources` / `sub.pattern` overriding (the raw `pattern`
1017
+ * escape hatch is shallow-merged LAST so it always wins). CDK's target
1018
+ * constructs add the resource policy that lets EventBridge invoke the
1019
+ * Lambda / send to the queue — for a cross-stack imported queue that
1020
+ * is an `AWS::SQS::QueuePolicy` in this app stack, leaving the
1021
+ * messaging-tier queue untouched.
1022
+ */
1023
+ createEventRule(intent, sub, bus, defaultSource, imports) {
1024
+ const resourceId = `events-${intent.id}-${sub.id}`;
1025
+ const pattern = {
1026
+ source: sub.sources ?? [defaultSource],
1027
+ ...(sub.detailTypes ? { detailType: sub.detailTypes } : {}),
1028
+ ...(sub.pattern ?? {}),
1029
+ };
1030
+ let target;
1031
+ if (sub.target?.queue) {
1032
+ const queueArn = imports.queueArnsById?.get(sub.target.queue);
1033
+ if (!queueArn) {
1034
+ throw new Error(`[venturekit] app stack: events subscription '${intent.id}/${sub.id}' ` +
1035
+ `targets queue '${sub.target.queue}' but no such queue is declared. ` +
1036
+ `Add a queues[] intent with id='${sub.target.queue}'.`);
1037
+ }
1038
+ const queue = sqs.Queue.fromQueueArn(this, `${resourceId}-queue`, queueArn);
1039
+ // Deliver ONLY the event `detail` as the SQS body (not the full
1040
+ // EventBridge envelope). This matches VentureKit's queue
1041
+ // convention — a `queueBatchHandler` consumer's `body` is the
1042
+ // payload — so subscribing a queue to an event is indistinguishable
1043
+ // from `sendMessage(queueId, detail)`. The publisher bakes
1044
+ // `__trace` INTO the detail, so trace propagation survives.
1045
+ target = new eventsTargets.SqsQueue(queue, {
1046
+ message: events.RuleTargetInput.fromEventPath('$.detail'),
1047
+ });
1048
+ }
1049
+ else if (sub.target?.function) {
1050
+ const fn = this.findLambdaByLogicalName(sub.target.function);
1051
+ if (!fn) {
1052
+ throw new Error(`[venturekit] app stack: events subscription '${intent.id}/${sub.id}' ` +
1053
+ `targets function '${sub.target.function}' but no such Lambda was ` +
1054
+ `created. Add a handler at src/functions/${sub.target.function}.ts.`);
1055
+ }
1056
+ target = new eventsTargets.LambdaFunction(fn);
1057
+ }
1058
+ else {
1059
+ throw new Error(`[venturekit] app stack: events subscription '${intent.id}/${sub.id}' ` +
1060
+ `must declare a target: { queue: '<id>' } or { function: '<id>' }.`);
1061
+ }
1062
+ new events.Rule(this, `${resourceId}-rule`, {
1063
+ ruleName: clampLambdaFunctionName(`${this.projectName}-${this.stage}-events-${intent.id}-${sub.id}`),
1064
+ eventBus: bus,
1065
+ eventPattern: pattern,
1066
+ targets: [target],
1067
+ });
1068
+ }
1069
+ /**
1070
+ * Render declared `monitoring` intents into CloudWatch alarms
1071
+ * (REQUIREMENTS §7.4). Until this existed a `MonitoringIntent` in
1072
+ * vk.config.ts was a SILENT NO-OP — the intent reached the stack but
1073
+ * nothing materialised a `cloudwatch.Alarm`.
1074
+ *
1075
+ * Each `AlarmIntent` becomes one `cloudwatch.Alarm` over a
1076
+ * `cloudwatch.Metric` built verbatim from the intent (namespace /
1077
+ * metric / statistic / period / threshold / comparison /
1078
+ * evaluationPeriods / dimensions). When the parent `MonitoringIntent`
1079
+ * carries a `notificationArn`, the alarm's ALARM action publishes to
1080
+ * that (imported) SNS topic.
1081
+ *
1082
+ * # Dimension ergonomics
1083
+ *
1084
+ * Lambda function names and EventBridge rule names are clamped /
1085
+ * prefixed at synth (`clampLambdaFunctionName`,
1086
+ * `${project}-${stage}-cron-${name}`), so a consumer can't predict
1087
+ * them in vk.config.ts. To keep the §7.4 Lambda-error / cron-failure
1088
+ * alarms declarable, two dimension keys accept a LOGICAL handler name
1089
+ * and are resolved here to the physical name:
1090
+ * - `FunctionName` — matched against every Lambda this stack created
1091
+ * (route slug, queue / cron / standalone handler name, notify
1092
+ * dispatcher/bounce id) → the CDK `functionName` token.
1093
+ * - `RuleName` — matched against declared cron handler names →
1094
+ * `${project}-${stage}-cron-${name}`.
1095
+ * Any other dimension value (an explicit physical name, an RDS
1096
+ * `DBInstanceIdentifier`, a custom-metric dimension) passes through
1097
+ * verbatim. (RDS identifiers live in the data stack, so they're not
1098
+ * auto-resolvable here — supply the physical id, or see the
1099
+ * cross-stack follow-up.)
1100
+ */
1101
+ createMonitoring(monitoring) {
1102
+ for (const intent of monitoring) {
1103
+ const alarms = intent.alarms ?? [];
1104
+ if (alarms.length === 0 && !intent.dashboard)
1105
+ continue;
1106
+ // Import the notification topic ONCE per intent — a per-alarm
1107
+ // import would collide on the construct id.
1108
+ const topic = intent.notificationArn
1109
+ ? sns.Topic.fromTopicArn(this, `alarm-topic-${intent.id}`, intent.notificationArn)
1110
+ : undefined;
1111
+ const createdAlarms = [];
1112
+ for (const a of alarms) {
1113
+ const alarm = new cloudwatch.Alarm(this, `alarm-${intent.id}-${a.name}`, {
1114
+ alarmName: `${this.projectName}-${this.stage}-${a.name}`,
1115
+ alarmDescription: a.description,
1116
+ metric: new cloudwatch.Metric({
1117
+ namespace: a.namespace ?? 'AWS/Lambda',
1118
+ metricName: a.metric,
1119
+ statistic: a.statistic ?? 'Average',
1120
+ period: cdk.Duration.seconds(a.periodSeconds ?? 300),
1121
+ dimensionsMap: this.resolveAlarmDimensions(a.dimensions),
1122
+ }),
1123
+ threshold: a.threshold,
1124
+ comparisonOperator: ALARM_COMPARISON[a.comparison] ??
1125
+ cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
1126
+ evaluationPeriods: a.evaluationPeriods ?? 1,
1127
+ // Failure/error alarms stay green when the metric simply
1128
+ // hasn't reported (no invocations = no errors).
1129
+ treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
1130
+ });
1131
+ if (topic) {
1132
+ alarm.addAlarmAction(new cloudwatchActions.SnsAction(topic));
1133
+ }
1134
+ createdAlarms.push(alarm);
1135
+ }
1136
+ if (intent.dashboard) {
1137
+ const dashboard = new cloudwatch.Dashboard(this, `dashboard-${intent.id}`, {
1138
+ dashboardName: `${this.projectName}-${this.stage}-${intent.id}`,
1139
+ });
1140
+ if (createdAlarms.length > 0) {
1141
+ dashboard.addWidgets(new cloudwatch.AlarmStatusWidget({
1142
+ title: `${intent.id} alarms`,
1143
+ alarms: createdAlarms,
1144
+ width: 24,
1145
+ }));
1146
+ }
1147
+ }
1148
+ }
1149
+ }
1150
+ /**
1151
+ * Resolve an AlarmIntent's dimensions, mapping the logical
1152
+ * `FunctionName` / `RuleName` conventions to physical names (see
1153
+ * `createMonitoring`). Returns a fresh object; never mutates input.
1154
+ */
1155
+ resolveAlarmDimensions(dimensions) {
1156
+ if (!dimensions)
1157
+ return {};
1158
+ const resolved = {};
1159
+ for (const [key, value] of Object.entries(dimensions)) {
1160
+ if (key === 'FunctionName') {
1161
+ const fn = this.findLambdaByLogicalName(value);
1162
+ // `fn.functionName` renders as an in-stack CFN `Ref`, but a
1163
+ // CloudWatch dimension needs the LITERAL name the metric is
1164
+ // published under. Every Lambda is built with an explicit
1165
+ // physical name (see `buildLambda`), so read it off the L1.
1166
+ const cfnFn = fn?.node.defaultChild;
1167
+ resolved[key] = cfnFn?.functionName ?? value;
1168
+ }
1169
+ else if (key === 'RuleName' && this.scheduleLambdas.has(value)) {
1170
+ resolved[key] = `${this.projectName}-${this.stage}-cron-${value}`;
1171
+ }
1172
+ else {
1173
+ resolved[key] = value;
1174
+ }
1175
+ }
1176
+ return resolved;
1177
+ }
1178
+ /**
1179
+ * Find a Lambda this stack created by its LOGICAL name — a route
1180
+ * slug, a queue / cron / standalone handler name, or a notify intent
1181
+ * id. Returns undefined when nothing matches (the caller then treats
1182
+ * the value as an explicit physical name).
1183
+ */
1184
+ findLambdaByLogicalName(name) {
1185
+ return (this.routeLambdas.get(name) ??
1186
+ this.scheduleLambdas.get(name) ??
1187
+ this.queueLambdas.get(name) ??
1188
+ this.functionLambdas.get(name) ??
1189
+ this.notifyDispatcherLambdas.get(name) ??
1190
+ this.notifyBounceLambdas.get(name));
1191
+ }
916
1192
  /**
917
1193
  * Attach IAM grants to the shared Lambda role for every imported
918
1194
  * resource. Called once during construction, before any Lambda is
@@ -1020,6 +1296,22 @@ export class VentureAppStack extends cdk.Stack {
1020
1296
  }));
1021
1297
  }
1022
1298
  }
1299
+ // ── EventBridge PutEvents on OWNED buses ─────────────────
1300
+ // Scoped to the ARNs of the buses this venture owns, so a handler
1301
+ // can `publishEvent()` to them but not to arbitrary buses. Pure
1302
+ // subscribers (external.busName) own no bus and get no grant — they
1303
+ // only RECEIVE via rules, which EventBridge delivers under its own
1304
+ // service principal, not the Lambda role.
1305
+ if (imports.eventBusArnsById) {
1306
+ const busArns = [...imports.eventBusArnsById.values()];
1307
+ if (busArns.length > 0) {
1308
+ this.lambdaRole.addToPrincipalPolicy(new iam.PolicyStatement({
1309
+ effect: iam.Effect.ALLOW,
1310
+ actions: ['events:PutEvents'],
1311
+ resources: busArns,
1312
+ }));
1313
+ }
1314
+ }
1023
1315
  }
1024
1316
  /**
1025
1317
  * Provision dispatcher + bounce-handler Lambdas per declared