@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.
@@ -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
- // Populate offloaded values into process.env during Lambda INIT,
102
- // BEFORE app modules load. api-modules capture OAuth client
103
- // credentials at module-require time (top-level Definition.env), so
104
- // the handler-time loader (parametersToEnv) is too late for them.
105
- // NODE_OPTIONS=--import runs this ESM preload (top-level await) before
106
- // the entry module. Shipped in @friggframework/core (packaged into
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('sets NODE_OPTIONS to --import the INIT preload (appended to any app value)', async () => {
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
- expect(result.environment.NODE_OPTIONS).toContain('--import');
246
- expect(result.environment.NODE_OPTIONS).toContain(
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.8f60019.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.8f60019.0",
30
- "@friggframework/schemas": "2.0.0--canary.625.8f60019.0",
31
- "@friggframework/test": "2.0.0--canary.625.8f60019.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.8f60019.0",
60
- "@friggframework/prettier-config": "2.0.0--canary.625.8f60019.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": "8f600193664963446e2c59c4b05ee076b0fbdc2a"
92
+ "gitHead": "3f500f202c8dcf2210c07835c76abbab30e0d0ee"
93
93
  }