@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.
@@ -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.27a7e7a.0",
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.27a7e7a.0",
30
- "@friggframework/schemas": "2.0.0--canary.625.27a7e7a.0",
31
- "@friggframework/test": "2.0.0--canary.625.27a7e7a.0",
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.27a7e7a.0",
60
- "@friggframework/prettier-config": "2.0.0--canary.625.27a7e7a.0",
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": "27a7e7ae02d6dc5789f5aba0fe5c4cf82320dc38"
92
+ "gitHead": "3f500f202c8dcf2210c07835c76abbab30e0d0ee"
93
93
  }