@friggframework/devtools 2.0.0--canary.625.27a7e7a.0 → 2.0.0--canary.625.3f500f2.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.
- package/infrastructure/__tests__/ssm-preload-node-options.test.js +79 -0
- package/infrastructure/domains/parameters/offload-utils.js +15 -0
- package/infrastructure/domains/parameters/ssm-builder.js +7 -0
- package/infrastructure/domains/parameters/ssm-builder.test.js +13 -0
- package/infrastructure/infrastructure-composer.js +25 -0
- package/package.json +7 -7
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The SSM INIT preload (NODE_OPTIONS=--import) must be scoped to skipEsbuild
|
|
3
|
+
* functions only. esbuild-bundled functions (e.g. defaultWebsocket) do not
|
|
4
|
+
* package the preload .mjs, and a missing --import target fatally aborts Node
|
|
5
|
+
* startup — so a global NODE_OPTIONS would brick them (ADR-027 review finding).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { composeServerlessDefinition } = require('../infrastructure-composer');
|
|
9
|
+
|
|
10
|
+
jest.mock('../domains/shared/resource-discovery', () => {
|
|
11
|
+
const original = jest.requireActual('../domains/shared/resource-discovery');
|
|
12
|
+
return {
|
|
13
|
+
...original,
|
|
14
|
+
gatherDiscoveredResources: jest.fn().mockResolvedValue({
|
|
15
|
+
defaultVpcId: 'vpc-123456',
|
|
16
|
+
defaultSecurityGroupId: 'sg-123456',
|
|
17
|
+
privateSubnetId1: 'subnet-1',
|
|
18
|
+
privateSubnetId2: 'subnet-2',
|
|
19
|
+
defaultKmsKeyId:
|
|
20
|
+
'arn:aws:kms:us-east-1:123456789012:key/abc-123',
|
|
21
|
+
auroraClusterEndpoint: 'c.cluster-x.us-east-1.rds.amazonaws.com',
|
|
22
|
+
auroraPort: 5432,
|
|
23
|
+
}),
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const buildApp = (overrides = {}) => ({
|
|
28
|
+
name: 'preload-test',
|
|
29
|
+
provider: 'aws',
|
|
30
|
+
usePrismaLambdaLayer: false,
|
|
31
|
+
encryption: { fieldLevelEncryptionMethod: 'aes' },
|
|
32
|
+
vpc: { enable: false },
|
|
33
|
+
database: { postgres: { enable: true } },
|
|
34
|
+
websockets: { enable: true },
|
|
35
|
+
integrations: [{ Definition: { name: 'hubspot' } }],
|
|
36
|
+
...overrides,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const IMPORT_FRAGMENT = '--import file:///var/task/node_modules/@friggframework/core/core/ssm-preload.mjs';
|
|
40
|
+
|
|
41
|
+
describe('SSM preload NODE_OPTIONS scoping', () => {
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
process.argv = ['node', 'test'];
|
|
44
|
+
delete process.env.FRIGG_SKIP_AWS_DISCOVERY;
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('sets NODE_OPTIONS --import on skipEsbuild functions and NOT on esbuild-bundled ones when offload is active', async () => {
|
|
48
|
+
const definition = await composeServerlessDefinition(
|
|
49
|
+
buildApp({
|
|
50
|
+
ssm: { enable: true },
|
|
51
|
+
environment: { HUBSPOT_SECRET: 'ssm' },
|
|
52
|
+
})
|
|
53
|
+
);
|
|
54
|
+
const fns = definition.functions;
|
|
55
|
+
|
|
56
|
+
// skipEsbuild handlers (auth/user/health + integration) package the
|
|
57
|
+
// preload → they get the flag.
|
|
58
|
+
expect(fns.auth.skipEsbuild).toBe(true);
|
|
59
|
+
expect(fns.auth.environment.NODE_OPTIONS).toBe(IMPORT_FRAGMENT);
|
|
60
|
+
expect(fns.hubspot.environment.NODE_OPTIONS).toBe(IMPORT_FRAGMENT);
|
|
61
|
+
|
|
62
|
+
// defaultWebsocket is esbuild-bundled (no skipEsbuild) → MUST NOT get it.
|
|
63
|
+
expect(fns.defaultWebsocket.skipEsbuild).toBeFalsy();
|
|
64
|
+
expect(fns.defaultWebsocket.environment?.NODE_OPTIONS).toBeUndefined();
|
|
65
|
+
|
|
66
|
+
// Never global.
|
|
67
|
+
expect(definition.provider.environment.NODE_OPTIONS).toBeUndefined();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('sets no NODE_OPTIONS anywhere when offload is inactive', async () => {
|
|
71
|
+
const definition = await composeServerlessDefinition(
|
|
72
|
+
buildApp({ ssm: { enable: true } }) // enabled, but no 'ssm' keys
|
|
73
|
+
);
|
|
74
|
+
for (const fn of Object.values(definition.functions)) {
|
|
75
|
+
expect(fn.environment?.NODE_OPTIONS).toBeUndefined();
|
|
76
|
+
}
|
|
77
|
+
expect(definition.provider.environment.NODE_OPTIONS).toBeUndefined();
|
|
78
|
+
});
|
|
79
|
+
});
|
|
@@ -16,6 +16,19 @@ const ENV_NAME_PATTERN = /^[A-Z][A-Z0-9_]*$/;
|
|
|
16
16
|
const DEFAULT_PARAMETER_PREFIX =
|
|
17
17
|
'/frigg/${self:service}/${self:provider.stage}';
|
|
18
18
|
|
|
19
|
+
// INIT-phase preload shipped in @friggframework/core, present at this path in
|
|
20
|
+
// every skipEsbuild handler (which packages the full node_modules tree).
|
|
21
|
+
// LAMBDA_TASK_ROOT = /var/task.
|
|
22
|
+
const SSM_PRELOAD_PATH =
|
|
23
|
+
'/var/task/node_modules/@friggframework/core/core/ssm-preload.mjs';
|
|
24
|
+
|
|
25
|
+
// NODE_OPTIONS value that loads the INIT preload. Set ONLY on skipEsbuild
|
|
26
|
+
// functions (esbuild-bundled functions do not package the .mjs, and a missing
|
|
27
|
+
// --import target is a fatal Node startup error). No `${env:NODE_OPTIONS}`
|
|
28
|
+
// prefix: that source resolves the deploy host's shell at package time, which
|
|
29
|
+
// would leak an unrelated value into every Lambda.
|
|
30
|
+
const SSM_PRELOAD_NODE_OPTIONS = `--import file://${SSM_PRELOAD_PATH}`;
|
|
31
|
+
|
|
19
32
|
// Keys the framework itself sets (from discovered resources or the base
|
|
20
33
|
// template) or that the loader/bypass handlers depend on before SSM values
|
|
21
34
|
// are available. Never offloadable.
|
|
@@ -172,4 +185,6 @@ module.exports = {
|
|
|
172
185
|
validateOffloadConfig,
|
|
173
186
|
FRAMEWORK_ENV_BLOCKLIST,
|
|
174
187
|
DEFAULT_PARAMETER_PREFIX,
|
|
188
|
+
SSM_PRELOAD_PATH,
|
|
189
|
+
SSM_PRELOAD_NODE_OPTIONS,
|
|
175
190
|
};
|
|
@@ -93,6 +93,13 @@ class SsmBuilder extends InfrastructureBuilder {
|
|
|
93
93
|
result.environment.SSM_PARAMETER_PREFIX = prefix;
|
|
94
94
|
result.environment.FRIGG_SSM_OFFLOADED_KEYS = offloadedKeys.join(',');
|
|
95
95
|
|
|
96
|
+
// The INIT-phase preload (NODE_OPTIONS=--import) that populates
|
|
97
|
+
// process.env before app modules load is set per-function on
|
|
98
|
+
// skipEsbuild handlers only — see infrastructure-composer.js. It
|
|
99
|
+
// cannot go here (provider.environment is global) because a missing
|
|
100
|
+
// --import target fatally aborts Node startup, and esbuild-bundled
|
|
101
|
+
// functions do not package the preload file.
|
|
102
|
+
|
|
96
103
|
// Prefix-scoped read grant. Built as a plain serverless string (not
|
|
97
104
|
// Fn::Sub) because the prefix contains serverless variables like
|
|
98
105
|
// ${self:service} that Fn::Sub would reject as bad substitution keys.
|
|
@@ -234,6 +234,19 @@ describe('SsmBuilder', () => {
|
|
|
234
234
|
expect(result.environment.FRIGG_SSM_OFFLOADED_KEYS).toBe('BAR,FOO');
|
|
235
235
|
});
|
|
236
236
|
|
|
237
|
+
it('does not set NODE_OPTIONS globally (the composer scopes it to skipEsbuild functions)', async () => {
|
|
238
|
+
const appDefinition = {
|
|
239
|
+
ssm: { enable: true },
|
|
240
|
+
environment: { FOO: 'ssm' },
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const result = await ssmBuilder.build(appDefinition, {});
|
|
244
|
+
|
|
245
|
+
// NODE_OPTIONS=--import would fatally abort esbuild-bundled functions
|
|
246
|
+
// that lack the preload file, so it must never be provider-global.
|
|
247
|
+
expect(result.environment.NODE_OPTIONS).toBeUndefined();
|
|
248
|
+
});
|
|
249
|
+
|
|
237
250
|
it('should add a prefix-scoped read statement while retaining the broad grant by default', async () => {
|
|
238
251
|
const appDefinition = {
|
|
239
252
|
ssm: { enable: true },
|
|
@@ -21,6 +21,30 @@ const { AdminScriptBuilder } = require('./domains/admin-scripts/admin-script-bui
|
|
|
21
21
|
|
|
22
22
|
// Utilities
|
|
23
23
|
const { applyFunctionEnvironments } = require('./domains/shared/function-environments');
|
|
24
|
+
const {
|
|
25
|
+
isSsmOffloadActive,
|
|
26
|
+
SSM_PRELOAD_NODE_OPTIONS,
|
|
27
|
+
} = require('./domains/parameters/offload-utils');
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Load the SSM INIT preload (NODE_OPTIONS=--import) on skipEsbuild handlers
|
|
31
|
+
* only. Those package the full node_modules tree, so the preload .mjs is
|
|
32
|
+
* present at /var/task. esbuild-bundled functions (e.g. defaultWebsocket,
|
|
33
|
+
* adopter custom functions) do NOT ship it — and a missing --import target is a
|
|
34
|
+
* fatal Node startup error — so they are left with the handler-time loader
|
|
35
|
+
* fallback instead. Set at function scope (function env wins over provider env).
|
|
36
|
+
*/
|
|
37
|
+
function applySsmPreloadNodeOptions(appDefinition, functions) {
|
|
38
|
+
if (!isSsmOffloadActive(appDefinition)) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
for (const fn of Object.values(functions)) {
|
|
42
|
+
if (fn.skipEsbuild) {
|
|
43
|
+
fn.environment = fn.environment || {};
|
|
44
|
+
fn.environment.NODE_OPTIONS = SSM_PRELOAD_NODE_OPTIONS;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
24
48
|
const { modifyHandlerPaths } = require('./domains/shared/utilities/handler-path-resolver');
|
|
25
49
|
const { createBaseDefinition } = require('./domains/shared/utilities/base-definition-factory');
|
|
26
50
|
const { ensurePrismaLayerExists } = require('./domains/shared/utilities/prisma-layer-manager');
|
|
@@ -80,6 +104,7 @@ const composeServerlessDefinition = async (AppDefinition) => {
|
|
|
80
104
|
definition.functions,
|
|
81
105
|
merged.functionEnvironments
|
|
82
106
|
);
|
|
107
|
+
applySsmPreloadNodeOptions(AppDefinition, definition.functions);
|
|
83
108
|
|
|
84
109
|
if (merged.vpcConfig) {
|
|
85
110
|
definition.provider.vpc = merged.vpcConfig;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@friggframework/devtools",
|
|
3
3
|
"prettier": "@friggframework/prettier-config",
|
|
4
|
-
"version": "2.0.0--canary.625.
|
|
4
|
+
"version": "2.0.0--canary.625.3f500f2.0",
|
|
5
5
|
"bin": {
|
|
6
6
|
"frigg": "./frigg-cli/index.js"
|
|
7
7
|
},
|
|
@@ -26,9 +26,9 @@
|
|
|
26
26
|
"@babel/eslint-parser": "^7.18.9",
|
|
27
27
|
"@babel/parser": "^7.25.3",
|
|
28
28
|
"@babel/traverse": "^7.25.3",
|
|
29
|
-
"@friggframework/core": "2.0.0--canary.625.
|
|
30
|
-
"@friggframework/schemas": "2.0.0--canary.625.
|
|
31
|
-
"@friggframework/test": "2.0.0--canary.625.
|
|
29
|
+
"@friggframework/core": "2.0.0--canary.625.3f500f2.0",
|
|
30
|
+
"@friggframework/schemas": "2.0.0--canary.625.3f500f2.0",
|
|
31
|
+
"@friggframework/test": "2.0.0--canary.625.3f500f2.0",
|
|
32
32
|
"@hapi/boom": "^10.0.1",
|
|
33
33
|
"@inquirer/prompts": "^5.3.8",
|
|
34
34
|
"axios": "^1.18.0",
|
|
@@ -56,8 +56,8 @@
|
|
|
56
56
|
"validate-npm-package-name": "^5.0.0"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
|
-
"@friggframework/eslint-config": "2.0.0--canary.625.
|
|
60
|
-
"@friggframework/prettier-config": "2.0.0--canary.625.
|
|
59
|
+
"@friggframework/eslint-config": "2.0.0--canary.625.3f500f2.0",
|
|
60
|
+
"@friggframework/prettier-config": "2.0.0--canary.625.3f500f2.0",
|
|
61
61
|
"aws-sdk-client-mock": "^4.1.0",
|
|
62
62
|
"aws-sdk-client-mock-jest": "^4.1.0",
|
|
63
63
|
"jest": "^30.1.3",
|
|
@@ -89,5 +89,5 @@
|
|
|
89
89
|
"publishConfig": {
|
|
90
90
|
"access": "public"
|
|
91
91
|
},
|
|
92
|
-
"gitHead": "
|
|
92
|
+
"gitHead": "3f500f202c8dcf2210c07835c76abbab30e0d0ee"
|
|
93
93
|
}
|