@friggframework/core 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.
@@ -38,11 +38,11 @@ const parseKeys = (raw) =>
38
38
  .map((key) => key.trim())
39
39
  .filter(Boolean);
40
40
 
41
- const sendWithRetry = async (command) => {
41
+ const sendWithRetry = async (ssmClient, command) => {
42
42
  let attempt = 0;
43
43
  for (;;) {
44
44
  try {
45
- return await client.send(command);
45
+ return await ssmClient.send(command);
46
46
  } catch (err) {
47
47
  if (
48
48
  err.name === 'ThrottlingException' &&
@@ -57,6 +57,47 @@ const sendWithRetry = async (command) => {
57
57
  }
58
58
  };
59
59
 
60
+ /**
61
+ * Fetch offloaded parameters and return a plain { key: value } map. Fetch-only:
62
+ * no process.env mutation, no caching, no ownership tracking. Shared by the
63
+ * runtime loader and the INIT-phase preload (ssm-preload.js). Throws (listing
64
+ * every missing name) if any requested key is absent from SSM.
65
+ */
66
+ const fetchOffloadedParameters = async (prefix, keys, { region } = {}) => {
67
+ const { SSMClient, GetParametersCommand } = require('@aws-sdk/client-ssm');
68
+ const ssmClient = new SSMClient({
69
+ region: region || process.env.AWS_REGION,
70
+ });
71
+ const nameFor = (key) => `${prefix}/${key}`;
72
+
73
+ const found = new Map();
74
+ for (const batch of chunk(keys, BATCH_SIZE)) {
75
+ const { Parameters = [] } = await sendWithRetry(
76
+ ssmClient,
77
+ new GetParametersCommand({
78
+ Names: batch.map(nameFor),
79
+ WithDecryption: true,
80
+ })
81
+ );
82
+ for (const param of Parameters) {
83
+ found.set(param.Name, param);
84
+ }
85
+ }
86
+
87
+ const missing = keys.map(nameFor).filter((name) => !found.has(name));
88
+ if (missing.length > 0) {
89
+ throw new Error(
90
+ `missing SSM parameters under ${prefix}: ${missing.join(', ')}`
91
+ );
92
+ }
93
+
94
+ const values = {};
95
+ for (const key of keys) {
96
+ values[key] = found.get(nameFor(key)).Value;
97
+ }
98
+ return values;
99
+ };
100
+
60
101
  const loadParameters = async (prefix, keys) => {
61
102
  const { SSMClient, GetParametersCommand } = require('@aws-sdk/client-ssm');
62
103
  if (!client) {
@@ -80,6 +121,7 @@ const loadParameters = async (prefix, keys) => {
80
121
  try {
81
122
  for (const batch of chunk(keysToFetch, BATCH_SIZE)) {
82
123
  const { Parameters = [] } = await sendWithRetry(
124
+ client,
83
125
  new GetParametersCommand({
84
126
  Names: batch.map(nameFor),
85
127
  WithDecryption: true,
@@ -157,6 +199,23 @@ const parametersToEnv = async () => {
157
199
  return inflight;
158
200
  };
159
201
 
202
+ /**
203
+ * Adopt keys the INIT preload (ssm-preload.mjs) already populated into
204
+ * process.env, so the handler-time loader treats them as its own: it seeds
205
+ * the cache TTL and marks the keys owned so the TTL-refresh path re-fetches
206
+ * them. The preload runs in-process (NODE_OPTIONS=--import), sharing this
207
+ * module singleton. Pass ONLY the keys the preload actually set (not keys
208
+ * already present as real env vars) so the real-env-wins rule is preserved.
209
+ */
210
+ const adoptPreloadedKeys = (keys) => {
211
+ for (const key of keys) {
212
+ ownedKeys.add(key);
213
+ }
214
+ const ttlSeconds = getCacheTtlSeconds();
215
+ cacheExpiresAt =
216
+ ttlSeconds === 0 ? Infinity : Date.now() + ttlSeconds * 1000;
217
+ };
218
+
160
219
  const _resetCache = () => {
161
220
  client = null;
162
221
  cacheExpiresAt = null;
@@ -166,5 +225,7 @@ const _resetCache = () => {
166
225
 
167
226
  module.exports = {
168
227
  parametersToEnv,
228
+ fetchOffloadedParameters,
229
+ adoptPreloadedKeys,
169
230
  _resetCache,
170
231
  };
@@ -0,0 +1,47 @@
1
+ // INIT-phase SSM loader (ADR-027).
2
+ //
3
+ // Loaded via NODE_OPTIONS=--import BEFORE the Lambda handler and any api-module
4
+ // is required, so SSM-offloaded values are present in process.env at
5
+ // module-load time — when api-modules capture their OAuth client credentials
6
+ // in a top-level `const Definition = { env: { client_secret: process.env.X } }`.
7
+ // The runtime loader (parameters-to-env.js) runs inside the handler, which is
8
+ // too late for those module-load reads; this preload closes that gap.
9
+ //
10
+ // Top-level await here is awaited by Node before the entry module loads, so the
11
+ // fetch completes first. A fetch failure rejects the preload, failing Lambda
12
+ // INIT loudly (fail-fast) rather than starting with undefined credentials.
13
+
14
+ import { createRequire } from 'module';
15
+
16
+ const require = createRequire(import.meta.url);
17
+
18
+ const prefix = process.env.SSM_PARAMETER_PREFIX;
19
+ const rawKeys = process.env.FRIGG_SSM_OFFLOADED_KEYS;
20
+
21
+ if (prefix && rawKeys) {
22
+ const keys = rawKeys
23
+ .split(',')
24
+ .map((key) => key.trim())
25
+ .filter(Boolean);
26
+
27
+ if (keys.length > 0) {
28
+ const {
29
+ fetchOffloadedParameters,
30
+ adoptPreloadedKeys,
31
+ } = require('./parameters-to-env');
32
+ const values = await fetchOffloadedParameters(prefix, keys);
33
+
34
+ // Real env vars win over SSM (console-override debugging escape hatch).
35
+ const setKeys = [];
36
+ for (const [key, value] of Object.entries(values)) {
37
+ if (process.env[key] === undefined) {
38
+ process.env[key] = value;
39
+ setKeys.push(key);
40
+ }
41
+ }
42
+ // Hand the keys we set to the handler-time loader (same in-process
43
+ // module singleton) so its TTL-refresh path re-fetches them.
44
+ adoptPreloadedKeys(setKeys);
45
+ console.log(`frigg-ssm-preload: loaded ${setKeys.length} parameter(s) at INIT under ${prefix}`);
46
+ }
47
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@friggframework/core",
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
  "dependencies": {
6
6
  "@aws-sdk/client-apigatewaymanagementapi": "^3.588.0",
7
7
  "@aws-sdk/client-kms": "^3.588.0",
@@ -39,9 +39,9 @@
39
39
  }
40
40
  },
41
41
  "devDependencies": {
42
- "@friggframework/eslint-config": "2.0.0--canary.625.27a7e7a.0",
43
- "@friggframework/prettier-config": "2.0.0--canary.625.27a7e7a.0",
44
- "@friggframework/test": "2.0.0--canary.625.27a7e7a.0",
42
+ "@friggframework/eslint-config": "2.0.0--canary.625.3f500f2.0",
43
+ "@friggframework/prettier-config": "2.0.0--canary.625.3f500f2.0",
44
+ "@friggframework/test": "2.0.0--canary.625.3f500f2.0",
45
45
  "@prisma/client": "^6.19.3",
46
46
  "@types/lodash": "4.17.15",
47
47
  "@typescript-eslint/eslint-plugin": "^8.0.0",
@@ -81,5 +81,5 @@
81
81
  "publishConfig": {
82
82
  "access": "public"
83
83
  },
84
- "gitHead": "27a7e7ae02d6dc5789f5aba0fe5c4cf82320dc38"
84
+ "gitHead": "3f500f202c8dcf2210c07835c76abbab30e0d0ee"
85
85
  }