@friggframework/devtools 2.0.0--canary.625.8f60019.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 +6 -14
- package/infrastructure/domains/parameters/ssm-builder.test.js +3 -13
- 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
|
};
|
|
@@ -16,11 +16,6 @@ const {
|
|
|
16
16
|
validateOffloadConfig,
|
|
17
17
|
} = require('./offload-utils');
|
|
18
18
|
|
|
19
|
-
// INIT-phase preload shipped in @friggframework/core, packaged into every
|
|
20
|
-
// skipEsbuild handler at this stable path (LAMBDA_TASK_ROOT = /var/task).
|
|
21
|
-
const SSM_PRELOAD_PATH =
|
|
22
|
-
'/var/task/node_modules/@friggframework/core/core/ssm-preload.mjs';
|
|
23
|
-
|
|
24
19
|
class SsmBuilder extends InfrastructureBuilder {
|
|
25
20
|
constructor() {
|
|
26
21
|
super();
|
|
@@ -98,15 +93,12 @@ class SsmBuilder extends InfrastructureBuilder {
|
|
|
98
93
|
result.environment.SSM_PARAMETER_PREFIX = prefix;
|
|
99
94
|
result.environment.FRIGG_SSM_OFFLOADED_KEYS = offloadedKeys.join(',');
|
|
100
95
|
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
// every skipEsbuild handler at a stable path). Appended to any
|
|
108
|
-
// app-provided NODE_OPTIONS.
|
|
109
|
-
result.environment.NODE_OPTIONS = `\${env:NODE_OPTIONS, ''} --import file://${SSM_PRELOAD_PATH}`;
|
|
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.
|
|
110
102
|
|
|
111
103
|
// Prefix-scoped read grant. Built as a plain serverless string (not
|
|
112
104
|
// Fn::Sub) because the prefix contains serverless variables like
|
|
@@ -234,7 +234,7 @@ describe('SsmBuilder', () => {
|
|
|
234
234
|
expect(result.environment.FRIGG_SSM_OFFLOADED_KEYS).toBe('BAR,FOO');
|
|
235
235
|
});
|
|
236
236
|
|
|
237
|
-
it('
|
|
237
|
+
it('does not set NODE_OPTIONS globally (the composer scopes it to skipEsbuild functions)', async () => {
|
|
238
238
|
const appDefinition = {
|
|
239
239
|
ssm: { enable: true },
|
|
240
240
|
environment: { FOO: 'ssm' },
|
|
@@ -242,18 +242,8 @@ describe('SsmBuilder', () => {
|
|
|
242
242
|
|
|
243
243
|
const result = await ssmBuilder.build(appDefinition, {});
|
|
244
244
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
'@friggframework/core/core/ssm-preload.mjs'
|
|
248
|
-
);
|
|
249
|
-
// Appends to any app-provided NODE_OPTIONS rather than clobbering it.
|
|
250
|
-
expect(result.environment.NODE_OPTIONS).toContain(
|
|
251
|
-
"${env:NODE_OPTIONS, ''}"
|
|
252
|
-
);
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
it('does not set NODE_OPTIONS when no keys are offloaded', async () => {
|
|
256
|
-
const result = await ssmBuilder.build({ ssm: { enable: true } }, {});
|
|
245
|
+
// NODE_OPTIONS=--import would fatally abort esbuild-bundled functions
|
|
246
|
+
// that lack the preload file, so it must never be provider-global.
|
|
257
247
|
expect(result.environment.NODE_OPTIONS).toBeUndefined();
|
|
258
248
|
});
|
|
259
249
|
|
|
@@ -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
|
}
|