@mettlecast/domain-cdk-packer 0.2.109 → 0.2.111
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/dist/DomainStack.d.ts +6 -2
- package/dist/DomainStack.js +4 -2
- package/dist/__tests__/cross-domain-action-arn-naming.test.d.ts +1 -0
- package/dist/__tests__/cross-domain-action-arn-naming.test.js +180 -0
- package/dist/__tests__/deploy-entry-naming-parity.test.d.ts +1 -0
- package/dist/__tests__/deploy-entry-naming-parity.test.js +130 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/naming.d.ts +18 -0
- package/dist/naming.js +23 -0
- package/package.json +1 -1
package/dist/DomainStack.d.ts
CHANGED
|
@@ -89,8 +89,12 @@ export interface DomainStackProps extends cdk.StackProps {
|
|
|
89
89
|
* Injected into this domain's subscriber/action Lambdas (via the
|
|
90
90
|
* `TIB_ACTION_LAMBDA_ARNS` env var) so `ctx.actions.otherDomain.someAction()`
|
|
91
91
|
* dispatches cross-domain through the runtime's Lambda invoke envelope
|
|
92
|
-
* (#5226). ARNs
|
|
93
|
-
*
|
|
92
|
+
* (#5226). ARNs MUST be built with `domainActionLambdaArn(projectId,
|
|
93
|
+
* envCode, domainId)` — the single canonical source that matches the
|
|
94
|
+
* deterministic physical name this stack assigns to the grouped internal
|
|
95
|
+
* action Lambda (`${projectId}-${envCode}-${domainId}-action`, lowercased).
|
|
96
|
+
* Hand-assembled ARN strings drift from that name and make every
|
|
97
|
+
* cross-domain invoke target a phantom function (#5381).
|
|
94
98
|
*/
|
|
95
99
|
crossDomainActionArns?: Record<string, string>;
|
|
96
100
|
/**
|
package/dist/DomainStack.js
CHANGED
|
@@ -20,7 +20,7 @@ import { CmkConstruct } from './constructs/cmk-construct.js';
|
|
|
20
20
|
import { WafConstruct } from './constructs/waf-construct.js';
|
|
21
21
|
import { AlarmConstruct } from './constructs/alarm-construct.js';
|
|
22
22
|
import { CanaryConstruct } from './constructs/canary-construct.js';
|
|
23
|
-
import { domainResourceName } from './naming.js';
|
|
23
|
+
import { domainResourceName, domainActionLambdaName } from './naming.js';
|
|
24
24
|
import { SecurityAssertionAspect } from './aspects/security-assertion-aspect.js';
|
|
25
25
|
import { validateRegistry } from './validate-registry.js';
|
|
26
26
|
/** L1 helper: adds a CfnRoute + CfnIntegration to an existing HTTP API. */
|
|
@@ -259,7 +259,9 @@ export class DomainStack extends cdk.Stack {
|
|
|
259
259
|
}
|
|
260
260
|
// Deterministic physical name for THIS domain's internal-action grouped
|
|
261
261
|
// Lambda so other stacks can construct its ARN for cross-domain calls.
|
|
262
|
-
|
|
262
|
+
// Canonical deterministic name shared with every cross-domain ARN
|
|
263
|
+
// builder (naming.ts) — never hand-assemble this pattern elsewhere.
|
|
264
|
+
const actionLambdaName = domainActionLambdaName(projectId, envCode, domainId);
|
|
263
265
|
const resolveSubnetSelection = (access) => {
|
|
264
266
|
if (access === 'internet')
|
|
265
267
|
return internetSubnetSelection ?? internalSubnetSelection;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #5381 — the cross-domain action ARN map MUST resolve to the physical name of
|
|
3
|
+
* the internal-action grouped Lambda that DomainStack actually deploys.
|
|
4
|
+
*
|
|
5
|
+
* Production evidence (run 33969906049): auth's `on-member-invited`
|
|
6
|
+
* subscriber called `ctx.actions.email['send-template-email']` and the email
|
|
7
|
+
* send never started (no email_send_audit row). The map consumed by the
|
|
8
|
+
* subscriber Lambda had never been reconciled against the deployed function
|
|
9
|
+
* name: the generated app hand-assembled
|
|
10
|
+
* `${projectId}-${envCode}-domain-${other}-action` while DomainStack names
|
|
11
|
+
* the grouped internal-action Lambda
|
|
12
|
+
* `${projectId}-${envCode}-${domainId}-action` (naming.ts, lowercased). Every
|
|
13
|
+
* cross-domain invoke — and the IAM grant backing it — pointed at a phantom
|
|
14
|
+
* function. The existing cross-domain-wiring.test.ts could not catch this
|
|
15
|
+
* because it injected a synthetic ARN prop and asserted the SAME string came
|
|
16
|
+
* back out; it never compared the map against the deployed FunctionName.
|
|
17
|
+
*
|
|
18
|
+
* These tests close that loop with the REAL constructs:
|
|
19
|
+
* 1. synthesize an email DomainStack and read the physical FunctionName of
|
|
20
|
+
* its grouped internal-action Lambda out of the template;
|
|
21
|
+
* 2. synthesize an auth DomainStack whose crossDomainActionArns map is built
|
|
22
|
+
* exactly the way the generated deploy entries build it — through the
|
|
23
|
+
* shared `domainActionLambdaArn` helper — and assert the subscriber's
|
|
24
|
+
* TIB_ACTION_LAMBDA_ARNS value resolves to that same function name;
|
|
25
|
+
* 3. assert the subscriber's lambda:InvokeFunction grant targets the same
|
|
26
|
+
* function resource (a valid map with a missing grant is equally
|
|
27
|
+
* undeliverable).
|
|
28
|
+
*/
|
|
29
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
30
|
+
import * as cdk from 'aws-cdk-lib';
|
|
31
|
+
import { Template } from 'aws-cdk-lib/assertions';
|
|
32
|
+
import fs from 'node:fs';
|
|
33
|
+
import path from 'node:path';
|
|
34
|
+
import { DomainStack } from '../DomainStack.js';
|
|
35
|
+
import { domainActionLambdaName, domainActionLambdaArn } from '../naming.js';
|
|
36
|
+
const REPO_CWD = process.cwd();
|
|
37
|
+
const EVENT_BUS_ARN = 'arn:aws:events:eu-north-1:123456789012:event-bus/test-bus';
|
|
38
|
+
const PROJECT_ID = 'MTC';
|
|
39
|
+
const ENV_CODE = 'Dev';
|
|
40
|
+
function fixtureRegistry(domainId, opts) {
|
|
41
|
+
const root = path.join(REPO_CWD, `.test-arn-naming-${domainId}`);
|
|
42
|
+
return {
|
|
43
|
+
schemaVersion: '1',
|
|
44
|
+
domainRoot: root,
|
|
45
|
+
domain: { id: domainId, kind: 'domain', name: domainId, tenancy: 'none' },
|
|
46
|
+
webhooks: [],
|
|
47
|
+
subscribers: opts.subscribers
|
|
48
|
+
? [
|
|
49
|
+
{
|
|
50
|
+
id: 'on-member-invited',
|
|
51
|
+
kind: 'subscriber',
|
|
52
|
+
handlerFile: 'subscribers/on-member-invited.ts',
|
|
53
|
+
event: 'auth.member.invited',
|
|
54
|
+
semverRange: '^1',
|
|
55
|
+
outboundAccess: 'internal',
|
|
56
|
+
},
|
|
57
|
+
]
|
|
58
|
+
: [],
|
|
59
|
+
schedules: [],
|
|
60
|
+
jobs: [],
|
|
61
|
+
actions: opts.internalActions.map((id) => ({
|
|
62
|
+
id,
|
|
63
|
+
kind: 'action',
|
|
64
|
+
handlerFile: `actions/${id}.ts`,
|
|
65
|
+
backendAccess: 'domain',
|
|
66
|
+
exposure: { type: 'internal' },
|
|
67
|
+
idempotent: false,
|
|
68
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
69
|
+
outputSchema: { type: 'object', properties: {}, required: [] },
|
|
70
|
+
})),
|
|
71
|
+
integrations: [],
|
|
72
|
+
events: [],
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/** Create the handler stubs + grouped-Lambda asset dirs DomainStack expects. */
|
|
76
|
+
function materializeFixture(domainId, registry) {
|
|
77
|
+
const root = registry.domainRoot;
|
|
78
|
+
fs.mkdirSync(path.join(root, 'subscribers'), { recursive: true });
|
|
79
|
+
fs.mkdirSync(path.join(root, 'actions'), { recursive: true });
|
|
80
|
+
for (const sub of registry.subscribers) {
|
|
81
|
+
fs.writeFileSync(path.join(root, sub.handlerFile), `export const onMemberInvited = { id: "${sub.id}", event: "${sub.event}", semverRange: "^1", handler: async () => {} };\n`);
|
|
82
|
+
}
|
|
83
|
+
for (const action of registry.actions) {
|
|
84
|
+
const exportName = action.id.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
85
|
+
fs.writeFileSync(path.join(root, action.handlerFile), `export const ${exportName} = { id: "${action.id}", backendAccess: "domain", exposure: { type: "internal" }, input: { parse: (x) => x }, output: { parse: (x) => x }, idempotent: false, handler: async () => ({ ok: true }) };\n`);
|
|
86
|
+
}
|
|
87
|
+
for (const prim of ['subscriber', 'action']) {
|
|
88
|
+
fs.mkdirSync(path.join(REPO_CWD, 'dist', 'domains', domainId, prim), { recursive: true });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Resolve a (possibly tokenized) CFN value to a plain string, replacing
|
|
93
|
+
* pseudo-parameter refs with stable placeholders. The JSON map injected as
|
|
94
|
+
* TIB_ACTION_LAMBDA_ARNS synthesizes to an Fn::Join of literal segments and
|
|
95
|
+
* {Ref: AWS::Region}/{Ref: AWS::AccountId} tokens; the region/account do not
|
|
96
|
+
* affect the function-name segment we assert on.
|
|
97
|
+
*/
|
|
98
|
+
function flattenCfn(value) {
|
|
99
|
+
if (typeof value === 'string')
|
|
100
|
+
return value;
|
|
101
|
+
if (value && typeof value === 'object') {
|
|
102
|
+
const rec = value;
|
|
103
|
+
if (Array.isArray(rec['Fn::Join'])) {
|
|
104
|
+
const [delim, parts] = rec['Fn::Join'];
|
|
105
|
+
return parts.map(flattenCfn).join(delim);
|
|
106
|
+
}
|
|
107
|
+
if (typeof rec['Ref'] === 'string')
|
|
108
|
+
return `__${rec['Ref']}__`;
|
|
109
|
+
if (Array.isArray(value))
|
|
110
|
+
return value.map(flattenCfn).join('');
|
|
111
|
+
}
|
|
112
|
+
return String(value);
|
|
113
|
+
}
|
|
114
|
+
function findDomainLambdas(template) {
|
|
115
|
+
return template.findResources('AWS::Lambda::Function');
|
|
116
|
+
}
|
|
117
|
+
beforeAll(() => {
|
|
118
|
+
materializeFixture('email', fixtureRegistry('email', { subscribers: false, internalActions: ['send-template-email'] }));
|
|
119
|
+
materializeFixture('auth', fixtureRegistry('auth', { subscribers: true, internalActions: ['verify-token'] }));
|
|
120
|
+
});
|
|
121
|
+
afterAll(() => {
|
|
122
|
+
for (const d of ['email', 'auth']) {
|
|
123
|
+
fs.rmSync(path.join(REPO_CWD, `.test-arn-naming-${d}`), { recursive: true, force: true });
|
|
124
|
+
fs.rmSync(path.join(REPO_CWD, 'dist', 'domains', d), { recursive: true, force: true });
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
describe('cross-domain action ARN ⇔ deployed Lambda name (#5381)', () => {
|
|
128
|
+
it('the grouped internal-action Lambda carries the helper-derived physical name', () => {
|
|
129
|
+
const app = new cdk.App();
|
|
130
|
+
const registry = fixtureRegistry('email', { subscribers: false, internalActions: ['send-template-email'] });
|
|
131
|
+
const stack = new DomainStack(app, 'MTC-Dev-domain-email', {
|
|
132
|
+
registry,
|
|
133
|
+
eventBusArn: EVENT_BUS_ARN,
|
|
134
|
+
projectId: PROJECT_ID,
|
|
135
|
+
envCode: ENV_CODE,
|
|
136
|
+
});
|
|
137
|
+
const template = Template.fromStack(stack);
|
|
138
|
+
const lambdas = findDomainLambdas(template);
|
|
139
|
+
const physical = Object.values(lambdas).map((l) => l.Properties.FunctionName).filter(Boolean);
|
|
140
|
+
// The deployed name has NO 'domain' segment — this exact fact is what the
|
|
141
|
+
// old hand-assembled app.ts ARN got wrong.
|
|
142
|
+
expect(physical).toContain('mtc-dev-email-action');
|
|
143
|
+
});
|
|
144
|
+
it('a subscriber map built via domainActionLambdaArn resolves to the deployed email action function name', () => {
|
|
145
|
+
// Build the map the way BOTH generated deploy entries now build it.
|
|
146
|
+
const crossDomainActionArns = {
|
|
147
|
+
email: domainActionLambdaArn(PROJECT_ID, ENV_CODE, 'email'),
|
|
148
|
+
};
|
|
149
|
+
const app = new cdk.App();
|
|
150
|
+
const registry = fixtureRegistry('auth', { subscribers: true, internalActions: ['verify-token'] });
|
|
151
|
+
const stack = new DomainStack(app, 'MTC-Dev-domain-auth', {
|
|
152
|
+
registry,
|
|
153
|
+
eventBusArn: EVENT_BUS_ARN,
|
|
154
|
+
projectId: PROJECT_ID,
|
|
155
|
+
envCode: ENV_CODE,
|
|
156
|
+
crossDomainActionArns,
|
|
157
|
+
});
|
|
158
|
+
const template = Template.fromStack(stack);
|
|
159
|
+
// 1) Locate the grouped subscriber Lambda by its env-injected map.
|
|
160
|
+
const lambdas = findDomainLambdas(template);
|
|
161
|
+
const subscriberEntries = Object.entries(lambdas).filter(([id, l]) => /subscriber/i.test(id) && l.Properties.Environment?.Variables?.TIB_ACTION_LAMBDA_ARNS);
|
|
162
|
+
expect(subscriberEntries.length).toBeGreaterThanOrEqual(1);
|
|
163
|
+
for (const [, l] of subscriberEntries) {
|
|
164
|
+
const raw = l.Properties.Environment.Variables.TIB_ACTION_LAMBDA_ARNS;
|
|
165
|
+
const arns = JSON.parse(flattenCfn(raw));
|
|
166
|
+
const functionSegment = arns.email.split(':function:')[1];
|
|
167
|
+
// 2) THE invariant that was broken in production: the map's function
|
|
168
|
+
// segment must equal the physical name of the deployed email
|
|
169
|
+
// internal-action Lambda, not a hand-assembled approximation.
|
|
170
|
+
expect(functionSegment).toBe(domainActionLambdaName(PROJECT_ID, ENV_CODE, 'email'));
|
|
171
|
+
expect(functionSegment).toBe('mtc-dev-email-action');
|
|
172
|
+
}
|
|
173
|
+
// 3) The subscriber role must actually be granted invoke on THAT function
|
|
174
|
+
// (env correct but IAM phantom = still undeliverable).
|
|
175
|
+
const policies = Object.values(template.findResources('AWS::IAM::Policy'));
|
|
176
|
+
const hasInvokeOnRealName = policies.some((p) => p.Properties.PolicyDocument.Statement.some((s) => s.Action?.includes('lambda:InvokeFunction') &&
|
|
177
|
+
flattenCfn(s.Resource).includes(`:function:${domainActionLambdaName(PROJECT_ID, ENV_CODE, 'email')}`)));
|
|
178
|
+
expect(hasInvokeOnRealName).toBe(true);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #5385 — the COMMITTED generated deploy entries (infra/modules/app.ts and
|
|
3
|
+
* app-domain.ts) run in CI against the PUBLISHED @mettlecast/domain-cdk-packer:
|
|
4
|
+
* the deploy jobs `npm ci` in infra/modules and `npx tsx` loads the installed
|
|
5
|
+
* ESM tarball. Template v172 (#5384) imported `domainActionLambdaArn` — a
|
|
6
|
+
* member that exists only in this package's SOURCE — so every domain deploy
|
|
7
|
+
* died at link time with
|
|
8
|
+
* `SyntaxError: The requested module '@mettlecast/domain-cdk-packer' does
|
|
9
|
+
* not provide an export named 'domainActionLambdaArn'`
|
|
10
|
+
* (TIB Deploy run 34000848210) before the #5381 ARN repair could ship.
|
|
11
|
+
* Template v173 defines the ARN builder LOCALLY in the generated entries, on
|
|
12
|
+
* top of the published `domainResourceName` primitive.
|
|
13
|
+
*
|
|
14
|
+
* This test pins both halves of that contract:
|
|
15
|
+
* 1. the committed deploy entries value-import ONLY members verified to
|
|
16
|
+
* exist on the published packer export surface (npm tarballs 0.2.96 and
|
|
17
|
+
* 0.2.110 were checked: `packDomain`, `domainResourceName`);
|
|
18
|
+
* 2. the emitted local helper — extracted and evaluated with the REAL
|
|
19
|
+
* `domainResourceName` — produces the exact function name this package's
|
|
20
|
+
* canonical `domainActionLambdaName` assigns the deployed grouped
|
|
21
|
+
* internal-action Lambda. The local mirror must not fork away from
|
|
22
|
+
* naming.ts; if it drifts, this fails. (Region/account halves are CDK
|
|
23
|
+
* pseudo-parameters — asserted structurally, not by token identity.)
|
|
24
|
+
*/
|
|
25
|
+
import { describe, it, expect } from 'vitest';
|
|
26
|
+
import { readFileSync } from 'node:fs';
|
|
27
|
+
import { join, dirname } from 'node:path';
|
|
28
|
+
import { fileURLToPath } from 'node:url';
|
|
29
|
+
import { domainResourceName, domainActionLambdaName } from '../naming.js';
|
|
30
|
+
const repoRoot = join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..', '..');
|
|
31
|
+
/** Deploy entries the TIB Deploy jobs execute via `npx tsx`. */
|
|
32
|
+
const DEPLOY_ENTRIES = ['infra/modules/app.ts', 'infra/modules/app-domain.ts'];
|
|
33
|
+
/**
|
|
34
|
+
* Value exports VERIFIED present on every published packer the generated
|
|
35
|
+
* package.json range (^0.2.96) can resolve, including the lockfile-pinned
|
|
36
|
+
* 0.2.110. Widening this set requires first proving the member ships on the
|
|
37
|
+
* published tarball — that proof is exactly what #5385 lacked.
|
|
38
|
+
*/
|
|
39
|
+
const PUBLISHED_VALUE_EXPORTS = new Set(['packDomain', 'domainResourceName']);
|
|
40
|
+
/** Stub pseudo-parameter values for evaluating the emitted helper offline. */
|
|
41
|
+
const STUB_REGION = '__AWS_REGION__';
|
|
42
|
+
const STUB_ACCOUNT = '__AWS_ACCOUNT_ID__';
|
|
43
|
+
/**
|
|
44
|
+
* Extract the local `domainActionLambdaArn` function from a deploy entry and
|
|
45
|
+
* return it as a callable, with `cdk.Aws.*` stubbed and the REAL published
|
|
46
|
+
* `domainResourceName` primitive injected.
|
|
47
|
+
*/
|
|
48
|
+
function extractLocalArnHelper(src, label) {
|
|
49
|
+
const match = src.match(/function domainActionLambdaArn\([^)]*\)\s*:\s*string\s*\{[\s\S]*?\n\}/);
|
|
50
|
+
expect(match, `${label}: must define the local domainActionLambdaArn helper (template v173, #5385)`).toBeTruthy();
|
|
51
|
+
const stubCdk = { Aws: { REGION: STUB_REGION, ACCOUNT_ID: STUB_ACCOUNT } };
|
|
52
|
+
// The emitted helper is TypeScript; `new Function` parses plain JS. The
|
|
53
|
+
// function body contains no `: string` sequences, so stripping the four
|
|
54
|
+
// signature annotations is safe and keeps the executed logic verbatim.
|
|
55
|
+
const jsSource = match[0].replace(/: string/g, '');
|
|
56
|
+
// eslint-disable-next-line no-new-func -- executing the generated mirror IS the parity check
|
|
57
|
+
const factory = new Function('cdk', 'domainResourceName', `${jsSource}\nreturn domainActionLambdaArn;`);
|
|
58
|
+
return factory(stubCdk, domainResourceName);
|
|
59
|
+
}
|
|
60
|
+
/** Named value imports from '@mettlecast/domain-cdk-packer' (type imports erased). */
|
|
61
|
+
function packerValueImports(src) {
|
|
62
|
+
const names = [];
|
|
63
|
+
for (const line of src.split('\n')) {
|
|
64
|
+
const t = line.trim();
|
|
65
|
+
if (!t.startsWith('import') || !t.includes('@mettlecast/domain-cdk-packer'))
|
|
66
|
+
continue;
|
|
67
|
+
if (t.startsWith('import type'))
|
|
68
|
+
continue;
|
|
69
|
+
const braceOpen = t.indexOf('{');
|
|
70
|
+
const braceClose = t.indexOf('}');
|
|
71
|
+
if (braceOpen === -1 || braceClose === -1)
|
|
72
|
+
continue;
|
|
73
|
+
for (const raw of t.slice(braceOpen + 1, braceClose).split(',')) {
|
|
74
|
+
const name = raw.trim().split(/\s+as\s+/)[0].trim();
|
|
75
|
+
if (name)
|
|
76
|
+
names.push(name);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return names;
|
|
80
|
+
}
|
|
81
|
+
describe('deploy entries import only published packer members (#5385)', () => {
|
|
82
|
+
for (const rel of DEPLOY_ENTRIES) {
|
|
83
|
+
it(`${rel} value-imports nothing the installed packer lacks`, () => {
|
|
84
|
+
const src = readFileSync(join(repoRoot, rel), 'utf8');
|
|
85
|
+
const imported = packerValueImports(src);
|
|
86
|
+
// The regression itself: an unexported member crashes `npx tsx` at link
|
|
87
|
+
// time, so every deploy stack fails BEFORE synthesis. Fail loudly here.
|
|
88
|
+
expect(imported.length, `${rel} must import packDomain from the packer`).toBeGreaterThan(0);
|
|
89
|
+
for (const name of imported) {
|
|
90
|
+
expect(PUBLISHED_VALUE_EXPORTS.has(name), `${rel} imports { ${name} } from @mettlecast/domain-cdk-packer, but that member is not on the published export surface (SyntaxError at deploy time — #5385)`).toBe(true);
|
|
91
|
+
}
|
|
92
|
+
expect(imported).not.toContain('domainActionLambdaArn');
|
|
93
|
+
expect(imported).not.toContain('domainActionLambdaName');
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
describe('local ARN mirror ≡ canonical naming.ts (#5381/#5385)', () => {
|
|
98
|
+
it('naming.ts itself derives the action name from domainResourceName + "-action"', () => {
|
|
99
|
+
// The local mirror builds its ARN from the SAME published primitive; this
|
|
100
|
+
// pins the canonical contract the mirror is allowed to reproduce.
|
|
101
|
+
expect(domainActionLambdaName('MTC', 'Dev', 'email')).toBe(`${domainResourceName('MTC', 'Dev', 'email')}-action`);
|
|
102
|
+
expect(domainActionLambdaName('mtc', 'dev', 'email')).toBe('mtc-dev-email-action');
|
|
103
|
+
});
|
|
104
|
+
const fixtures = [
|
|
105
|
+
['MTC', 'Dev', 'email'],
|
|
106
|
+
['MTC', 'prod', 'data-management'],
|
|
107
|
+
['MyProj', 'Staging', 'billing'],
|
|
108
|
+
['MTC', 'Dev', 'auth'],
|
|
109
|
+
];
|
|
110
|
+
for (const rel of DEPLOY_ENTRIES) {
|
|
111
|
+
const src = readFileSync(join(repoRoot, rel), 'utf8');
|
|
112
|
+
const local = extractLocalArnHelper(src, rel);
|
|
113
|
+
for (const [projectId, envCode, domainId] of fixtures) {
|
|
114
|
+
it(`${rel}: domainActionLambdaArn('${projectId}', '${envCode}', '${domainId}') resolves to the deployed function name`, () => {
|
|
115
|
+
const generatedArn = local(projectId, envCode, domainId);
|
|
116
|
+
// Region/account halves are CFN pseudo-parameters (asserted by shape);
|
|
117
|
+
// the FUNCTION NAME is what drifted in production and what this guards.
|
|
118
|
+
expect(generatedArn.startsWith(`arn:aws:lambda:${STUB_REGION}:${STUB_ACCOUNT}:function:`)).toBe(true);
|
|
119
|
+
const functionSegment = generatedArn.split(':function:')[1];
|
|
120
|
+
expect(functionSegment).toBe(domainActionLambdaName(projectId, envCode, domainId));
|
|
121
|
+
// No phantom 'domain' segment (#5381's original bug shape).
|
|
122
|
+
expect(functionSegment).not.toContain('-domain-');
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
it(`${rel}: helper exists exactly once (no forked duplicates)`, () => {
|
|
126
|
+
const defs = src.match(/function domainActionLambdaArn\(/g) ?? [];
|
|
127
|
+
expect(defs.length).toBe(1);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
});
|
package/dist/index.d.ts
CHANGED
|
@@ -36,4 +36,4 @@ export type { DomainActionFlowNode, StepFunctionsTaskState } from './step-functi
|
|
|
36
36
|
export type { FlowRegistry, FlowRegistryEntry, SerialFlowStep, SerialDomainActionStep, SerialDomainApiStep, SerialDomainEventStep, SerialDomainQueryStep, SerialAwsServiceStep, SerialFlowControlStep } from './flow-registry.js';
|
|
37
37
|
export { FlowsStack, packFlows } from './pack-flows.js';
|
|
38
38
|
export type { PackFlowsOptions } from './pack-flows.js';
|
|
39
|
-
export { domainResourceName } from './naming.js';
|
|
39
|
+
export { domainResourceName, domainActionLambdaName, domainActionLambdaArn } from './naming.js';
|
package/dist/index.js
CHANGED
|
@@ -16,4 +16,4 @@ export { packDomain } from './pack-domain.js';
|
|
|
16
16
|
export { validateRegistry } from './validate-registry.js';
|
|
17
17
|
export { StepFunctionsCodegen } from './step-functions-codegen.js';
|
|
18
18
|
export { FlowsStack, packFlows } from './pack-flows.js';
|
|
19
|
-
export { domainResourceName } from './naming.js';
|
|
19
|
+
export { domainResourceName, domainActionLambdaName, domainActionLambdaArn } from './naming.js';
|
package/dist/naming.d.ts
CHANGED
|
@@ -9,3 +9,21 @@
|
|
|
9
9
|
* (pack-flows domain-query grants) so the two can never drift.
|
|
10
10
|
*/
|
|
11
11
|
export declare function domainResourceName(projectId: string, envCode: string, domainId: string): string;
|
|
12
|
+
/**
|
|
13
|
+
* Deterministic physical name of a domain's internal-action grouped Lambda —
|
|
14
|
+
* the invoke target for cross-domain `ctx.actions.<domain>.<action>()`
|
|
15
|
+
* dispatch (#5226). DomainStack assigns this exact name via `functionName`,
|
|
16
|
+
* and callers building cross-domain ARNs MUST derive them from here rather
|
|
17
|
+
* than hand-assembling the pattern: the generated app constructed
|
|
18
|
+
* `…-domain-<id>-action`, which never matched the deployed `…-<id>-action`,
|
|
19
|
+
* so every subscriber cross-domain invoke targeted a phantom function
|
|
20
|
+
* (bootstrap invitation email dispatch, #5381).
|
|
21
|
+
*/
|
|
22
|
+
export declare function domainActionLambdaName(projectId: string, envCode: string, domainId: string): string;
|
|
23
|
+
/**
|
|
24
|
+
* Cross-domain ARN for a domain's internal-action grouped Lambda, using
|
|
25
|
+
* account/region pseudo-parameters so it can be synthesized before the
|
|
26
|
+
* target stack exists. This is the canonical way to populate
|
|
27
|
+
* `PackDomainOptions.crossDomainActionArns`.
|
|
28
|
+
*/
|
|
29
|
+
export declare function domainActionLambdaArn(projectId: string, envCode: string, domainId: string): string;
|
package/dist/naming.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as cdk from 'aws-cdk-lib';
|
|
1
2
|
/**
|
|
2
3
|
* Canonical physical-name builder for per-domain AWS resources.
|
|
3
4
|
*
|
|
@@ -11,3 +12,25 @@
|
|
|
11
12
|
export function domainResourceName(projectId, envCode, domainId) {
|
|
12
13
|
return `${projectId}-${envCode}-${domainId}`.toLowerCase();
|
|
13
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* Deterministic physical name of a domain's internal-action grouped Lambda —
|
|
17
|
+
* the invoke target for cross-domain `ctx.actions.<domain>.<action>()`
|
|
18
|
+
* dispatch (#5226). DomainStack assigns this exact name via `functionName`,
|
|
19
|
+
* and callers building cross-domain ARNs MUST derive them from here rather
|
|
20
|
+
* than hand-assembling the pattern: the generated app constructed
|
|
21
|
+
* `…-domain-<id>-action`, which never matched the deployed `…-<id>-action`,
|
|
22
|
+
* so every subscriber cross-domain invoke targeted a phantom function
|
|
23
|
+
* (bootstrap invitation email dispatch, #5381).
|
|
24
|
+
*/
|
|
25
|
+
export function domainActionLambdaName(projectId, envCode, domainId) {
|
|
26
|
+
return `${domainResourceName(projectId, envCode, domainId)}-action`;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Cross-domain ARN for a domain's internal-action grouped Lambda, using
|
|
30
|
+
* account/region pseudo-parameters so it can be synthesized before the
|
|
31
|
+
* target stack exists. This is the canonical way to populate
|
|
32
|
+
* `PackDomainOptions.crossDomainActionArns`.
|
|
33
|
+
*/
|
|
34
|
+
export function domainActionLambdaArn(projectId, envCode, domainId) {
|
|
35
|
+
return `arn:aws:lambda:${cdk.Aws.REGION}:${cdk.Aws.ACCOUNT_ID}:function:${domainActionLambdaName(projectId, envCode, domainId)}`;
|
|
36
|
+
}
|