@friggframework/core 2.0.0--canary.622.eb44ae5.0 → 2.0.0--canary.625.27a7e7a.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/README.md +0 -40
- package/application/index.js +9 -10
- package/core/CLAUDE.md +10 -0
- package/core/create-handler.js +14 -112
- package/core/parameters-to-env.js +170 -0
- package/generated/prisma-mongodb/edge.js +4 -16
- package/generated/prisma-mongodb/index-browser.js +1 -13
- package/generated/prisma-mongodb/index.d.ts +96 -1494
- package/generated/prisma-mongodb/index.js +4 -16
- package/generated/prisma-mongodb/package.json +1 -1
- package/generated/prisma-mongodb/schema.prisma +0 -23
- package/generated/prisma-mongodb/wasm.js +4 -16
- package/generated/prisma-postgresql/edge.js +4 -16
- package/generated/prisma-postgresql/index-browser.js +1 -13
- package/generated/prisma-postgresql/index.d.ts +91 -1540
- package/generated/prisma-postgresql/index.js +4 -16
- package/generated/prisma-postgresql/package.json +1 -1
- package/generated/prisma-postgresql/schema.prisma +0 -22
- package/generated/prisma-postgresql/wasm.js +4 -16
- package/handlers/app-definition-loader.js +3 -26
- package/handlers/integration-event-dispatcher.js +15 -29
- package/handlers/routers/integration-webhook-routers.js +7 -20
- package/index.js +9 -16
- package/integrations/EXTENSIONS.md +2 -2
- package/integrations/integration-base.js +7 -64
- package/modules/requester/requester.js +5 -106
- package/package.json +6 -12
- package/prisma-mongodb/schema.prisma +0 -23
- package/prisma-postgresql/schema.prisma +0 -22
- package/reporting/README.md +1 -8
- package/reporting/reporting-router.js +1 -8
- package/reporting/use-cases/list-integrations-report.js +6 -53
- package/application/commands/usage-commands.js +0 -56
- package/prisma-postgresql/migrations/20260705000000_create_usage_counter/migration.sql +0 -26
- package/telemetry/README.md +0 -331
- package/telemetry/bind-telemetry-context.js +0 -73
- package/telemetry/canonical-counters.js +0 -52
- package/telemetry/exporters.js +0 -85
- package/telemetry/index.js +0 -26
- package/telemetry/instrument-handler.js +0 -87
- package/telemetry/no-op-telemetry.js +0 -67
- package/telemetry/north-star.js +0 -103
- package/telemetry/otel-telemetry.js +0 -213
- package/telemetry/plugin-subscribers.js +0 -77
- package/telemetry/telemetry-config.js +0 -120
- package/telemetry/telemetry-context.js +0 -40
- package/telemetry/telemetry-event-bus.js +0 -58
- package/telemetry/telemetry-runtime.js +0 -147
- package/telemetry/telemetry-service.js +0 -51
- package/telemetry/usage-rollup-subscriber.js +0 -116
- package/usage/README.md +0 -54
- package/usage/index.js +0 -17
- package/usage/repositories/usage-repository-documentdb.js +0 -194
- package/usage/repositories/usage-repository-factory.js +0 -25
- package/usage/repositories/usage-repository-interface.js +0 -37
- package/usage/repositories/usage-repository-prisma.js +0 -146
- package/usage/tracked-metrics.js +0 -38
- package/usage/usage-windows.js +0 -24
package/README.md
CHANGED
|
@@ -200,46 +200,6 @@ const secureData = cryptor.encrypt(JSON.stringify({
|
|
|
200
200
|
}));
|
|
201
201
|
```
|
|
202
202
|
|
|
203
|
-
### 4b. Telemetry & Usage Tracking (`/telemetry`, `/usage`)
|
|
204
|
-
|
|
205
|
-
Vendor-neutral OpenTelemetry observability plus durable, per-integration usage
|
|
206
|
-
counters (ADR-011). No-op by default (zero cold-start cost); framework seams are
|
|
207
|
-
auto-instrumented so integrations get handler/API-module/webhook metrics for free.
|
|
208
|
-
|
|
209
|
-
**Usage:**
|
|
210
|
-
```javascript
|
|
211
|
-
// App definition — turn on export + declare a North Star:
|
|
212
|
-
const Definition = {
|
|
213
|
-
name: 'my-app',
|
|
214
|
-
telemetry: {
|
|
215
|
-
exporter: { type: 'otlp', endpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT },
|
|
216
|
-
northStar: { default: { name: 'records.synced' } },
|
|
217
|
-
},
|
|
218
|
-
};
|
|
219
|
-
|
|
220
|
-
// Integration code — custom metrics/spans (this.telemetry is auto-tagged):
|
|
221
|
-
await this.telemetry.span('delta_sync', async () => {
|
|
222
|
-
this.telemetry.count('records.synced', batch.length, { entity: 'contact' });
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
// Declare which usage counters an integration reports (opts into reporting):
|
|
226
|
-
class HubSpotIntegration extends IntegrationBase {
|
|
227
|
-
static Definition = {
|
|
228
|
-
name: 'hubspot',
|
|
229
|
-
usage: { canonical: ['records.synced', 'api.requests'] },
|
|
230
|
-
};
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// Read the durable usage store (never an APM):
|
|
234
|
-
const frigg = createFriggCommands({ integrationClass: HubSpotIntegration });
|
|
235
|
-
await frigg.usage.getTotalsByDimension({ metric: 'records.synced', groupBy: 'integrationType' });
|
|
236
|
-
```
|
|
237
|
-
|
|
238
|
-
**See:** [`telemetry/README.md`](telemetry/README.md) for the full guide
|
|
239
|
-
(exporters, custom metrics, the Usage-Counter contract, North Star, the plugin
|
|
240
|
-
tap, cardinality rules, and caveats) and [`usage/README.md`](usage/README.md) for
|
|
241
|
-
the store internals.
|
|
242
|
-
|
|
243
203
|
### 5. Error Handling (`/errors`)
|
|
244
204
|
|
|
245
205
|
Standardized error types with proper HTTP status codes.
|
package/application/index.js
CHANGED
|
@@ -4,10 +4,15 @@ const {
|
|
|
4
4
|
} = require('./commands/integration-commands');
|
|
5
5
|
const { createUserCommands } = require('./commands/user-commands');
|
|
6
6
|
const { createEntityCommands } = require('./commands/entity-commands');
|
|
7
|
-
const {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const {
|
|
7
|
+
const {
|
|
8
|
+
createCredentialCommands,
|
|
9
|
+
} = require('./commands/credential-commands');
|
|
10
|
+
const {
|
|
11
|
+
createProcessCommands,
|
|
12
|
+
} = require('./commands/process-commands');
|
|
13
|
+
const {
|
|
14
|
+
createSchedulerCommands,
|
|
15
|
+
} = require('./commands/scheduler-commands');
|
|
11
16
|
|
|
12
17
|
/**
|
|
13
18
|
* Create a unified command factory with all CRUD operations
|
|
@@ -51,11 +56,6 @@ function createFriggCommands({ integrationClass }) {
|
|
|
51
56
|
|
|
52
57
|
// Process commands
|
|
53
58
|
...processCommands,
|
|
54
|
-
|
|
55
|
-
// Usage read/write — nested to match `frigg.usage.*`. The North Star
|
|
56
|
-
// read takes its config as a call argument, so nothing telemetry-specific
|
|
57
|
-
// is threaded through this general factory.
|
|
58
|
-
usage: createUsageCommands(),
|
|
59
59
|
};
|
|
60
60
|
}
|
|
61
61
|
|
|
@@ -70,7 +70,6 @@ module.exports = {
|
|
|
70
70
|
createCredentialCommands,
|
|
71
71
|
createProcessCommands,
|
|
72
72
|
createSchedulerCommands,
|
|
73
|
-
createUsageCommands,
|
|
74
73
|
|
|
75
74
|
// Legacy standalone function
|
|
76
75
|
findIntegrationContextByExternalEntityId,
|
package/core/CLAUDE.md
CHANGED
|
@@ -131,6 +131,7 @@ class MyIntegration extends Delegate {
|
|
|
131
131
|
```javascript
|
|
132
132
|
initDebugLog(eventName, event); // Debug logging setup
|
|
133
133
|
await secretsToEnv(); // Secrets Manager injection
|
|
134
|
+
await parametersToEnv(); // SSM Parameter Store fetch (only when SSM_PARAMETER_PREFIX + FRIGG_SSM_OFFLOADED_KEYS are set)
|
|
134
135
|
context.callbackWaitsForEmptyEventLoop = false; // Connection pooling
|
|
135
136
|
```
|
|
136
137
|
|
|
@@ -165,6 +166,15 @@ class MyIntegration extends Delegate {
|
|
|
165
166
|
- **Security**: No secrets logging or exposure in error messages
|
|
166
167
|
- **Caching**: Secrets cached for Lambda container lifetime
|
|
167
168
|
|
|
169
|
+
### SSM Parameter Store Loader (`parameters-to-env.js`)
|
|
170
|
+
Offloads env vars that would otherwise exceed Lambda's 4KB env limit. Runs right after `secretsToEnv()` and is a no-op unless both `SSM_PARAMETER_PREFIX` and `FRIGG_SSM_OFFLOADED_KEYS` (comma-separated env var names) are set.
|
|
171
|
+
|
|
172
|
+
- **Precedence**: real `process.env` > Secrets Manager > SSM. A key already present in `process.env` at load time is never fetched or overwritten (a documented local-debugging escape hatch); only keys the loader itself set are refreshed.
|
|
173
|
+
- **Fetch**: `GetParametersCommand` with `WithDecryption: true`, batched in groups of 10 (the GetParameters max). Parameter name for key `K` is `${SSM_PARAMETER_PREFIX}/${K}`.
|
|
174
|
+
- **TTL cache**: successful loads cache for `FRIGG_SSM_CACHE_TTL` seconds (default 300; `0` = forever). Concurrent callers share one in-flight promise. A failed initial load is never cached, so the next invocation retries; a failed TTL *refresh* keeps serving the stale values (warn + 30s backoff) instead of erroring a warm container. `ThrottlingException` is retried with short exponential backoff.
|
|
175
|
+
- **Fail-fast**: throws listing every missing parameter name (and the prefix) if a required key is absent from SSM. Keys already satisfied by real `process.env` never trigger a failure.
|
|
176
|
+
- **Security**: logs parameter names and versions only, never values.
|
|
177
|
+
|
|
168
178
|
## Database Connection Patterns
|
|
169
179
|
|
|
170
180
|
### Connection Pooling Strategy
|
package/core/create-handler.js
CHANGED
|
@@ -4,91 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
const { initDebugLog, flushDebugLog } = require('../logs');
|
|
6
6
|
const { secretsToEnv } = require('./secrets-to-env');
|
|
7
|
-
const {
|
|
8
|
-
getTelemetry,
|
|
9
|
-
getUsageRollupSubscriber,
|
|
10
|
-
getPluginTelemetrySubscribers,
|
|
11
|
-
} = require('../telemetry/telemetry-runtime');
|
|
12
|
-
|
|
13
|
-
// Bounds the tail latency telemetry adds to every warm invocation. Kept low so
|
|
14
|
-
// an unreachable OTLP endpoint (e.g. a VPC Lambda with no NAT/egress) costs at
|
|
15
|
-
// most this, not multiple seconds. Override with OTEL_FLUSH_TIMEOUT_MS.
|
|
16
|
-
const DEFAULT_FLUSH_TIMEOUT_MS =
|
|
17
|
-
Number(process.env.OTEL_FLUSH_TIMEOUT_MS) || 500;
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Fold the invocation's buffered usage counters into the durable store, then
|
|
21
|
-
* clear the buffer. On an SQS redelivery (ApproximateReceiveCount > 1) we
|
|
22
|
-
* DISCARD rather than flush — the prior delivery already counted, and the usage
|
|
23
|
-
* accuracy contract is "approximate, skip obvious redeliveries". Fully guarded.
|
|
24
|
-
*/
|
|
25
|
-
async function flushUsageRollup(subscriber, eventSummary, shouldUseDatabase) {
|
|
26
|
-
if (!subscriber) return;
|
|
27
|
-
try {
|
|
28
|
-
// Persisting usage requires a DB connection. DB-free handlers (e.g. the
|
|
29
|
-
// webhook-receipt route) never called connectPrisma, so drop the buffer
|
|
30
|
-
// instead of issuing a connectionless Prisma write.
|
|
31
|
-
if (!shouldUseDatabase) {
|
|
32
|
-
subscriber.discard();
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
// Discard only when EVERY record in the batch is a redelivery. The buffer
|
|
36
|
-
// is invocation-scoped (not per-message), so discarding on *any*
|
|
37
|
-
// redelivery would drop the fresh records' counts too (silent
|
|
38
|
-
// under-count). For a mixed batch we flush: preserving fresh counts and
|
|
39
|
-
// at worst re-counting the one redelivered record is strictly better than
|
|
40
|
-
// losing fresh data for an approximate store. (Integration queue workers
|
|
41
|
-
// are batchSize:1 today, so a batch is all-or-nothing; this keeps it
|
|
42
|
-
// correct if batchSize is ever raised.)
|
|
43
|
-
const records = Array.isArray(eventSummary?.records)
|
|
44
|
-
? eventSummary.records
|
|
45
|
-
: [];
|
|
46
|
-
const allRedelivered =
|
|
47
|
-
records.length > 0 &&
|
|
48
|
-
records.every((r) => Number(r.receiveCount) > 1);
|
|
49
|
-
if (allRedelivered) {
|
|
50
|
-
subscriber.discard();
|
|
51
|
-
} else {
|
|
52
|
-
await subscriber.flush();
|
|
53
|
-
}
|
|
54
|
-
} catch (_) {
|
|
55
|
-
// Usage rollup must never break the handler.
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Flush telemetry before the Lambda container freezes. Because
|
|
61
|
-
* `callbackWaitsForEmptyEventLoop=false` (below) stops the event loop the moment
|
|
62
|
-
* the handler returns, OTel's timer-driven batch processors would never fire —
|
|
63
|
-
* so spans/metrics must be flushed synchronously here. Bounded by a timeout so a
|
|
64
|
-
* stalled exporter can never block the response, and fully guarded so a flush
|
|
65
|
-
* failure never breaks the handler.
|
|
66
|
-
*/
|
|
67
|
-
async function flushTelemetry(telemetry, timeoutMs) {
|
|
68
|
-
try {
|
|
69
|
-
if (
|
|
70
|
-
!telemetry ||
|
|
71
|
-
typeof telemetry.isEnabled !== 'function' ||
|
|
72
|
-
!telemetry.isEnabled()
|
|
73
|
-
) {
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
let timer;
|
|
77
|
-
const deadline = new Promise((resolve) => {
|
|
78
|
-
timer = setTimeout(resolve, timeoutMs);
|
|
79
|
-
});
|
|
80
|
-
try {
|
|
81
|
-
await Promise.race([
|
|
82
|
-
Promise.resolve(telemetry.forceFlush()),
|
|
83
|
-
deadline,
|
|
84
|
-
]);
|
|
85
|
-
} finally {
|
|
86
|
-
clearTimeout(timer);
|
|
87
|
-
}
|
|
88
|
-
} catch (_) {
|
|
89
|
-
// Telemetry flush must never break the handler.
|
|
90
|
-
}
|
|
91
|
-
}
|
|
7
|
+
const { parametersToEnv } = require('./parameters-to-env');
|
|
92
8
|
|
|
93
9
|
// Best-effort extraction of correlation identifiers from a Lambda event.
|
|
94
10
|
// For SQS: pulls messageIds + parsed event/processId/integrationId from each
|
|
@@ -121,7 +37,8 @@ const summarizeLambdaEvent = (event) => {
|
|
|
121
37
|
if (event.httpMethod || event.requestContext?.http) {
|
|
122
38
|
return {
|
|
123
39
|
source: 'http',
|
|
124
|
-
method:
|
|
40
|
+
method:
|
|
41
|
+
event.httpMethod || event.requestContext?.http?.method,
|
|
125
42
|
path: event.path || event.rawPath,
|
|
126
43
|
};
|
|
127
44
|
}
|
|
@@ -134,9 +51,6 @@ const createHandler = (optionByName = {}) => {
|
|
|
134
51
|
isUserFacingResponse = true,
|
|
135
52
|
method,
|
|
136
53
|
shouldUseDatabase = true,
|
|
137
|
-
telemetry,
|
|
138
|
-
flushTimeoutMs = DEFAULT_FLUSH_TIMEOUT_MS,
|
|
139
|
-
usageRollup,
|
|
140
54
|
} = optionByName;
|
|
141
55
|
|
|
142
56
|
if (!method) {
|
|
@@ -145,23 +59,16 @@ const createHandler = (optionByName = {}) => {
|
|
|
145
59
|
|
|
146
60
|
return async (event, context) => {
|
|
147
61
|
const eventSummary = summarizeLambdaEvent(event);
|
|
148
|
-
const activeTelemetry = telemetry || getTelemetry();
|
|
149
|
-
const activeUsageRollup =
|
|
150
|
-
usageRollup !== undefined
|
|
151
|
-
? usageRollup
|
|
152
|
-
: getUsageRollupSubscriber();
|
|
153
|
-
|
|
154
|
-
// Wire adopter-declared telemetry subscribers once per cold start.
|
|
155
|
-
// Memoized in the singleton, so this is a cheap
|
|
156
|
-
// no-op after the first invocation.
|
|
157
|
-
getPluginTelemetrySubscribers();
|
|
158
62
|
|
|
159
63
|
try {
|
|
160
|
-
console.info(
|
|
161
|
-
eventName
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
64
|
+
console.info(
|
|
65
|
+
`[createHandler] ${eventName}: handler entry`,
|
|
66
|
+
{
|
|
67
|
+
eventName,
|
|
68
|
+
awsRequestId: context?.awsRequestId,
|
|
69
|
+
...eventSummary,
|
|
70
|
+
}
|
|
71
|
+
);
|
|
165
72
|
|
|
166
73
|
initDebugLog(eventName, event);
|
|
167
74
|
|
|
@@ -174,6 +81,9 @@ const createHandler = (optionByName = {}) => {
|
|
|
174
81
|
// If enabled (i.e. if SECRET_ARN is set in process.env) Fetch secrets from AWS Secrets Manager, and set them as environment variables.
|
|
175
82
|
await secretsToEnv();
|
|
176
83
|
|
|
84
|
+
// If enabled (i.e. if SSM_PARAMETER_PREFIX and FRIGG_SSM_OFFLOADED_KEYS are set) fetch offloaded params from SSM Parameter Store into process.env.
|
|
85
|
+
await parametersToEnv();
|
|
86
|
+
|
|
177
87
|
// Lazy-required so DB-free handlers never load the Prisma client.
|
|
178
88
|
if (shouldUseDatabase) {
|
|
179
89
|
const { connectPrisma } = require('../database/prisma');
|
|
@@ -233,14 +143,6 @@ const createHandler = (optionByName = {}) => {
|
|
|
233
143
|
|
|
234
144
|
// Here we can just rethrow and let AWS build the response.
|
|
235
145
|
throw error;
|
|
236
|
-
} finally {
|
|
237
|
-
// Flush telemetry + usage before the container freezes.
|
|
238
|
-
await flushTelemetry(activeTelemetry, flushTimeoutMs);
|
|
239
|
-
await flushUsageRollup(
|
|
240
|
-
activeUsageRollup,
|
|
241
|
-
eventSummary,
|
|
242
|
-
shouldUseDatabase
|
|
243
|
-
);
|
|
244
146
|
}
|
|
245
147
|
};
|
|
246
148
|
};
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// Runtime loader that hydrates process.env from SSM Parameter Store. Used to
|
|
2
|
+
// offload env vars that would otherwise blow past Lambda's 4KB env limit.
|
|
3
|
+
// Real process.env always wins over SSM (a documented local-debugging escape
|
|
4
|
+
// hatch); only keys this loader sets are ever refreshed.
|
|
5
|
+
|
|
6
|
+
const DEFAULT_CACHE_TTL_SECONDS = 300;
|
|
7
|
+
const BATCH_SIZE = 10;
|
|
8
|
+
const THROTTLE_BACKOFF_MS = [100, 200, 400];
|
|
9
|
+
const REFRESH_RETRY_MS = 30_000;
|
|
10
|
+
|
|
11
|
+
let client = null;
|
|
12
|
+
let cacheExpiresAt = null;
|
|
13
|
+
let inflight = null;
|
|
14
|
+
const ownedKeys = new Set();
|
|
15
|
+
|
|
16
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
17
|
+
|
|
18
|
+
const chunk = (items, size) => {
|
|
19
|
+
const batches = [];
|
|
20
|
+
for (let i = 0; i < items.length; i += size) {
|
|
21
|
+
batches.push(items.slice(i, i + size));
|
|
22
|
+
}
|
|
23
|
+
return batches;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const getCacheTtlSeconds = () => {
|
|
27
|
+
const raw = process.env.FRIGG_SSM_CACHE_TTL;
|
|
28
|
+
if (raw === undefined || raw === '') {
|
|
29
|
+
return DEFAULT_CACHE_TTL_SECONDS;
|
|
30
|
+
}
|
|
31
|
+
const parsed = Number(raw);
|
|
32
|
+
return Number.isNaN(parsed) ? DEFAULT_CACHE_TTL_SECONDS : parsed;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const parseKeys = (raw) =>
|
|
36
|
+
raw
|
|
37
|
+
.split(',')
|
|
38
|
+
.map((key) => key.trim())
|
|
39
|
+
.filter(Boolean);
|
|
40
|
+
|
|
41
|
+
const sendWithRetry = async (command) => {
|
|
42
|
+
let attempt = 0;
|
|
43
|
+
for (;;) {
|
|
44
|
+
try {
|
|
45
|
+
return await client.send(command);
|
|
46
|
+
} catch (err) {
|
|
47
|
+
if (
|
|
48
|
+
err.name === 'ThrottlingException' &&
|
|
49
|
+
attempt < THROTTLE_BACKOFF_MS.length
|
|
50
|
+
) {
|
|
51
|
+
await sleep(THROTTLE_BACKOFF_MS[attempt]);
|
|
52
|
+
attempt += 1;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
throw err;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const loadParameters = async (prefix, keys) => {
|
|
61
|
+
const { SSMClient, GetParametersCommand } = require('@aws-sdk/client-ssm');
|
|
62
|
+
if (!client) {
|
|
63
|
+
client = new SSMClient({ region: process.env.AWS_REGION });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// A key already living in process.env wins over SSM, except keys we set
|
|
67
|
+
// ourselves — those we refresh so later invocations pick up rotations.
|
|
68
|
+
const keysToFetch = keys.filter(
|
|
69
|
+
(key) => ownedKeys.has(key) || process.env[key] === undefined
|
|
70
|
+
);
|
|
71
|
+
const nameFor = (key) => `${prefix}/${key}`;
|
|
72
|
+
|
|
73
|
+
// A refresh means every key already has a served value; a warm container
|
|
74
|
+
// must keep serving stale values through an SSM blip instead of erroring.
|
|
75
|
+
const isRefresh =
|
|
76
|
+
keysToFetch.length > 0 &&
|
|
77
|
+
keysToFetch.every((key) => ownedKeys.has(key));
|
|
78
|
+
|
|
79
|
+
const found = new Map();
|
|
80
|
+
try {
|
|
81
|
+
for (const batch of chunk(keysToFetch, BATCH_SIZE)) {
|
|
82
|
+
const { Parameters = [] } = await sendWithRetry(
|
|
83
|
+
new GetParametersCommand({
|
|
84
|
+
Names: batch.map(nameFor),
|
|
85
|
+
WithDecryption: true,
|
|
86
|
+
})
|
|
87
|
+
);
|
|
88
|
+
for (const param of Parameters) {
|
|
89
|
+
found.set(param.Name, param);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const missing = keysToFetch
|
|
94
|
+
.map(nameFor)
|
|
95
|
+
.filter((name) => !found.has(name));
|
|
96
|
+
if (missing.length > 0) {
|
|
97
|
+
throw new Error(
|
|
98
|
+
`parametersToEnv: missing SSM parameters under ${prefix}: ${missing.join(
|
|
99
|
+
', '
|
|
100
|
+
)}`
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
} catch (err) {
|
|
104
|
+
if (!isRefresh) {
|
|
105
|
+
throw err;
|
|
106
|
+
}
|
|
107
|
+
console.warn(
|
|
108
|
+
`parametersToEnv: refresh failed, keeping cached values: ${err.message}`
|
|
109
|
+
);
|
|
110
|
+
cacheExpiresAt = Date.now() + REFRESH_RETRY_MS;
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const loaded = keysToFetch.map((key) => {
|
|
115
|
+
const param = found.get(nameFor(key));
|
|
116
|
+
process.env[key] = param.Value;
|
|
117
|
+
ownedKeys.add(key);
|
|
118
|
+
return `${param.Name} (v${param.Version})`;
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const ttlSeconds = getCacheTtlSeconds();
|
|
122
|
+
cacheExpiresAt =
|
|
123
|
+
ttlSeconds === 0 ? Infinity : Date.now() + ttlSeconds * 1000;
|
|
124
|
+
|
|
125
|
+
if (loaded.length > 0) {
|
|
126
|
+
console.log('parametersToEnv: loaded', loaded);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Hydrate process.env from SSM Parameter Store.
|
|
132
|
+
*
|
|
133
|
+
* No-op unless both SSM_PARAMETER_PREFIX and FRIGG_SSM_OFFLOADED_KEYS are set.
|
|
134
|
+
* Results are cached for FRIGG_SSM_CACHE_TTL seconds (default 300; 0 = forever).
|
|
135
|
+
* A failed fetch is never cached, so the next invocation retries.
|
|
136
|
+
*/
|
|
137
|
+
const parametersToEnv = async () => {
|
|
138
|
+
const prefix = process.env.SSM_PARAMETER_PREFIX;
|
|
139
|
+
const rawKeys = process.env.FRIGG_SSM_OFFLOADED_KEYS;
|
|
140
|
+
if (!prefix || !rawKeys) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const keys = parseKeys(rawKeys);
|
|
144
|
+
if (keys.length === 0) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (cacheExpiresAt !== null && Date.now() < cacheExpiresAt) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (!inflight) {
|
|
153
|
+
inflight = loadParameters(prefix, keys).finally(() => {
|
|
154
|
+
inflight = null;
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
return inflight;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const _resetCache = () => {
|
|
161
|
+
client = null;
|
|
162
|
+
cacheExpiresAt = null;
|
|
163
|
+
inflight = null;
|
|
164
|
+
ownedKeys.clear();
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
module.exports = {
|
|
168
|
+
parametersToEnv,
|
|
169
|
+
_resetCache,
|
|
170
|
+
};
|