@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.
- package/core/parameters-to-env.js +63 -2
- package/core/ssm-preload.mjs +47 -0
- package/package.json +5 -5
|
@@ -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
|
|
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.
|
|
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.
|
|
43
|
-
"@friggframework/prettier-config": "2.0.0--canary.625.
|
|
44
|
-
"@friggframework/test": "2.0.0--canary.625.
|
|
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": "
|
|
84
|
+
"gitHead": "3f500f202c8dcf2210c07835c76abbab30e0d0ee"
|
|
85
85
|
}
|