@friggframework/core 2.0.0-next.112 → 2.0.0-next.114
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/.eslintrc.json +18 -1
- package/CLAUDE.md +51 -12
- package/README.md +19 -22
- package/core/CLAUDE.md +28 -13
- package/core/Worker.js +32 -37
- package/core/create-handler.js +97 -197
- package/core/index.js +8 -1
- package/core/invocation-scope.js +225 -0
- package/core/secrets-to-env.js +7 -6
- package/database/config.js +0 -7
- package/database/encryption/README.md +5 -1
- package/database/encryption/encryption-schema-registry.js +3 -0
- package/database/encryption/logger.js +5 -2
- package/errors/fetch-error.js +58 -64
- package/handlers/app-definition-loader.js +38 -2
- package/handlers/app-handler-helpers.js +18 -4
- package/handlers/backend-utils.js +10 -13
- package/handlers/database-migration-handler.js +10 -1
- package/handlers/integration-event-dispatcher.js +2 -2
- package/handlers/routers/integration-webhook-routers.js +13 -6
- package/handlers/routers/websocket.js +27 -7
- package/handlers/workers/db-migration.js +12 -1
- package/handlers/workers/dlq-processor.js +64 -38
- package/index.js +19 -1
- package/integrations/integration-base.js +56 -38
- package/integrations/integration-router.js +21 -20
- package/integrations/use-cases/delete-integration-for-user.js +24 -13
- package/jest.config.js +2 -0
- package/logs/.eslintrc.json +26 -1
- package/logs/__fixtures__/events.js +82 -0
- package/logs/__fixtures__/matchers.js +45 -0
- package/logs/__fixtures__/secrets.js +59 -0
- package/logs/__fixtures__/vectors.js +329 -0
- package/logs/__fixtures__/with-env.js +45 -0
- package/logs/context.js +109 -0
- package/logs/contract-keys.js +67 -0
- package/logs/debug-shims.js +46 -0
- package/logs/denied-keys.js +21 -0
- package/logs/index.js +14 -3
- package/logs/jest-logger-setup.js +25 -0
- package/logs/levels.js +71 -0
- package/logs/logger-runtime.js +137 -0
- package/logs/logger.js +134 -42
- package/logs/record.js +276 -0
- package/logs/redact.js +328 -0
- package/logs/serialize.js +329 -0
- package/logs/sinks.js +93 -0
- package/logs/summarize-event.js +163 -0
- package/modules/module.js +32 -19
- package/modules/requester/oauth-2.js +28 -22
- package/modules/requester/requester.js +69 -6
- package/package.json +5 -6
- package/queues/queue-delivery.js +41 -0
- package/queues/queuer-util.js +1 -15
- package/telemetry/bind-telemetry-context.js +4 -0
- package/telemetry/instrument-handler.js +19 -2
- package/telemetry/no-op-telemetry.js +4 -0
- package/telemetry/otel-telemetry.js +25 -6
- package/telemetry/telemetry-context.js +19 -8
- package/telemetry/telemetry-runtime.js +17 -0
- package/telemetry/telemetry-service.js +1 -0
- package/types/errors/index.d.ts +24 -10
- package/types/logs/index.d.ts +61 -2
- package/user/repositories/user-repository-documentdb.js +10 -42
package/core/create-handler.js
CHANGED
|
@@ -2,7 +2,16 @@
|
|
|
2
2
|
// REMOVING FOR NOW UNTIL WE ADD WEBPACK BACK IN
|
|
3
3
|
// require('source-map-support').install();
|
|
4
4
|
|
|
5
|
-
const {
|
|
5
|
+
const { getLogger, toSanitizedSurrogate } = require('../logs');
|
|
6
|
+
const {
|
|
7
|
+
summarizeLambdaEvent,
|
|
8
|
+
toScopeInvocation,
|
|
9
|
+
toRequestDetails,
|
|
10
|
+
} = require('../logs/summarize-event');
|
|
11
|
+
const {
|
|
12
|
+
runInvocationScope,
|
|
13
|
+
DEFAULT_FLUSH_TIMEOUT_MS,
|
|
14
|
+
} = require('./invocation-scope');
|
|
6
15
|
const { secretsToEnv } = require('./secrets-to-env');
|
|
7
16
|
const { parametersToEnv } = require('./parameters-to-env');
|
|
8
17
|
const {
|
|
@@ -11,123 +20,7 @@ const {
|
|
|
11
20
|
getPluginTelemetrySubscribers,
|
|
12
21
|
} = require('../telemetry/telemetry-runtime');
|
|
13
22
|
|
|
14
|
-
|
|
15
|
-
// an unreachable OTLP endpoint (e.g. a VPC Lambda with no NAT/egress) costs at
|
|
16
|
-
// most this, not multiple seconds. Override with OTEL_FLUSH_TIMEOUT_MS.
|
|
17
|
-
const DEFAULT_FLUSH_TIMEOUT_MS =
|
|
18
|
-
Number(process.env.OTEL_FLUSH_TIMEOUT_MS) || 500;
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Fold the invocation's buffered usage counters into the durable store, then
|
|
22
|
-
* clear the buffer. On an SQS redelivery (ApproximateReceiveCount > 1) we
|
|
23
|
-
* DISCARD rather than flush — the prior delivery already counted, and the usage
|
|
24
|
-
* accuracy contract is "approximate, skip obvious redeliveries". Fully guarded.
|
|
25
|
-
*/
|
|
26
|
-
async function flushUsageRollup(subscriber, eventSummary, shouldUseDatabase) {
|
|
27
|
-
if (!subscriber) return;
|
|
28
|
-
try {
|
|
29
|
-
// Persisting usage requires a DB connection. DB-free handlers (e.g. the
|
|
30
|
-
// webhook-receipt route) never called connectPrisma, so drop the buffer
|
|
31
|
-
// instead of issuing a connectionless Prisma write.
|
|
32
|
-
if (!shouldUseDatabase) {
|
|
33
|
-
subscriber.discard();
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
// Discard only when EVERY record in the batch is a redelivery. The buffer
|
|
37
|
-
// is invocation-scoped (not per-message), so discarding on *any*
|
|
38
|
-
// redelivery would drop the fresh records' counts too (silent
|
|
39
|
-
// under-count). For a mixed batch we flush: preserving fresh counts and
|
|
40
|
-
// at worst re-counting the one redelivered record is strictly better than
|
|
41
|
-
// losing fresh data for an approximate store. (Integration queue workers
|
|
42
|
-
// are batchSize:1 today, so a batch is all-or-nothing; this keeps it
|
|
43
|
-
// correct if batchSize is ever raised.)
|
|
44
|
-
const records = Array.isArray(eventSummary?.records)
|
|
45
|
-
? eventSummary.records
|
|
46
|
-
: [];
|
|
47
|
-
const allRedelivered =
|
|
48
|
-
records.length > 0 &&
|
|
49
|
-
records.every((r) => Number(r.receiveCount) > 1);
|
|
50
|
-
if (allRedelivered) {
|
|
51
|
-
subscriber.discard();
|
|
52
|
-
} else {
|
|
53
|
-
await subscriber.flush();
|
|
54
|
-
}
|
|
55
|
-
} catch (_) {
|
|
56
|
-
// Usage rollup must never break the handler.
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Flush telemetry before the Lambda container freezes. Because
|
|
62
|
-
* `callbackWaitsForEmptyEventLoop=false` (below) stops the event loop the moment
|
|
63
|
-
* the handler returns, OTel's timer-driven batch processors would never fire —
|
|
64
|
-
* so spans/metrics must be flushed synchronously here. Bounded by a timeout so a
|
|
65
|
-
* stalled exporter can never block the response, and fully guarded so a flush
|
|
66
|
-
* failure never breaks the handler.
|
|
67
|
-
*/
|
|
68
|
-
async function flushTelemetry(telemetry, timeoutMs) {
|
|
69
|
-
try {
|
|
70
|
-
if (
|
|
71
|
-
!telemetry ||
|
|
72
|
-
typeof telemetry.isEnabled !== 'function' ||
|
|
73
|
-
!telemetry.isEnabled()
|
|
74
|
-
) {
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
let timer;
|
|
78
|
-
const deadline = new Promise((resolve) => {
|
|
79
|
-
timer = setTimeout(resolve, timeoutMs);
|
|
80
|
-
});
|
|
81
|
-
try {
|
|
82
|
-
await Promise.race([
|
|
83
|
-
Promise.resolve(telemetry.forceFlush()),
|
|
84
|
-
deadline,
|
|
85
|
-
]);
|
|
86
|
-
} finally {
|
|
87
|
-
clearTimeout(timer);
|
|
88
|
-
}
|
|
89
|
-
} catch (_) {
|
|
90
|
-
// Telemetry flush must never break the handler.
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Best-effort extraction of correlation identifiers from a Lambda event.
|
|
95
|
-
// For SQS: pulls messageIds + parsed event/processId/integrationId from each
|
|
96
|
-
// record body. For HTTP: pulls method+path. Never throws.
|
|
97
|
-
const summarizeLambdaEvent = (event) => {
|
|
98
|
-
if (!event) return {};
|
|
99
|
-
if (Array.isArray(event.Records)) {
|
|
100
|
-
return {
|
|
101
|
-
source: 'sqs',
|
|
102
|
-
records: event.Records.map((r) => {
|
|
103
|
-
let parsed = {};
|
|
104
|
-
try {
|
|
105
|
-
const body = JSON.parse(r.body);
|
|
106
|
-
parsed = {
|
|
107
|
-
event: body?.event,
|
|
108
|
-
processId: body?.data?.processId,
|
|
109
|
-
integrationId: body?.data?.integrationId,
|
|
110
|
-
};
|
|
111
|
-
} catch {
|
|
112
|
-
// ignore unparseable bodies
|
|
113
|
-
}
|
|
114
|
-
return {
|
|
115
|
-
messageId: r.messageId,
|
|
116
|
-
receiveCount: r.attributes?.ApproximateReceiveCount,
|
|
117
|
-
...parsed,
|
|
118
|
-
};
|
|
119
|
-
}),
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
if (event.httpMethod || event.requestContext?.http) {
|
|
123
|
-
return {
|
|
124
|
-
source: 'http',
|
|
125
|
-
method: event.httpMethod || event.requestContext?.http?.method,
|
|
126
|
-
path: event.path || event.rawPath,
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
return { source: 'other' };
|
|
130
|
-
};
|
|
23
|
+
const log = getLogger('frigg.handler');
|
|
131
24
|
|
|
132
25
|
const createHandler = (optionByName = {}) => {
|
|
133
26
|
const {
|
|
@@ -157,95 +50,102 @@ const createHandler = (optionByName = {}) => {
|
|
|
157
50
|
// no-op after the first invocation.
|
|
158
51
|
getPluginTelemetrySubscribers();
|
|
159
52
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
53
|
+
const scopeFields = {
|
|
54
|
+
requestId: context?.awsRequestId,
|
|
55
|
+
handlerName: eventName,
|
|
56
|
+
method: eventSummary.method,
|
|
57
|
+
route: eventSummary.route,
|
|
58
|
+
routeKey: eventSummary.routeKey,
|
|
59
|
+
invocation: toScopeInvocation(eventSummary),
|
|
60
|
+
};
|
|
61
|
+
// Path, query keys and header names go only on the entry and failure
|
|
62
|
+
// records; the logger adds them to the scope's invocation.
|
|
63
|
+
const requestDetails = toRequestDetails(eventSummary);
|
|
168
64
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
65
|
+
return runInvocationScope(
|
|
66
|
+
scopeFields,
|
|
67
|
+
async () => {
|
|
68
|
+
try {
|
|
69
|
+
log.info('Handler invoked', {
|
|
70
|
+
eventName: 'frigg.handler.invoked',
|
|
71
|
+
invocation: requestDetails,
|
|
72
|
+
});
|
|
174
73
|
|
|
175
|
-
|
|
176
|
-
|
|
74
|
+
// If enabled (i.e. if SECRET_ARN is set in process.env) Fetch secrets from AWS Secrets Manager, and set them as environment variables.
|
|
75
|
+
await secretsToEnv();
|
|
177
76
|
|
|
178
|
-
|
|
179
|
-
|
|
77
|
+
// 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.
|
|
78
|
+
await parametersToEnv();
|
|
180
79
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
80
|
+
// Lazy-required so DB-free handlers never load the Prisma client.
|
|
81
|
+
if (shouldUseDatabase) {
|
|
82
|
+
const { connectPrisma } = require('../database/prisma');
|
|
83
|
+
await connectPrisma();
|
|
84
|
+
}
|
|
186
85
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
86
|
+
// Helps reuse the database connection. Lowers response times.
|
|
87
|
+
context.callbackWaitsForEmptyEventLoop = false;
|
|
88
|
+
|
|
89
|
+
// Run the Lambda
|
|
90
|
+
return await method(event, context);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
// Allow client-safe errors to pass through with their actual message
|
|
93
|
+
if (isUserFacingResponse && error.isClientSafe === true) {
|
|
94
|
+
const statusCode = error.statusCode || 400;
|
|
95
|
+
log.warn('Request rejected', {
|
|
96
|
+
eventName: 'frigg.handler.rejected',
|
|
97
|
+
statusCode,
|
|
98
|
+
error,
|
|
99
|
+
});
|
|
100
|
+
return {
|
|
101
|
+
statusCode,
|
|
102
|
+
body: JSON.stringify({
|
|
103
|
+
error: error.message,
|
|
104
|
+
}),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
207
107
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
108
|
+
// Server-to-server: halt errors succeed and won't be
|
|
109
|
+
// retried, so the halt itself is the failure record.
|
|
110
|
+
if (!isUserFacingResponse && error.isHaltError === true) {
|
|
111
|
+
log.error('Handler halted', {
|
|
112
|
+
eventName: 'frigg.handler.halted',
|
|
113
|
+
invocation: requestDetails,
|
|
114
|
+
error,
|
|
115
|
+
});
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
216
118
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
statusCode: error.statusCode,
|
|
232
|
-
...eventSummary,
|
|
119
|
+
log.error('Handler failed', {
|
|
120
|
+
eventName: 'frigg.handler.failed',
|
|
121
|
+
invocation: requestDetails,
|
|
122
|
+
error,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Don't leak implementation details to end users.
|
|
126
|
+
if (isUserFacingResponse) {
|
|
127
|
+
return {
|
|
128
|
+
statusCode: 500,
|
|
129
|
+
body: JSON.stringify({
|
|
130
|
+
error: 'An Internal Error Occurred',
|
|
131
|
+
}),
|
|
132
|
+
};
|
|
233
133
|
}
|
|
234
|
-
);
|
|
235
|
-
return;
|
|
236
|
-
}
|
|
237
134
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
135
|
+
// The Lambda runtime writes a rethrown error itself, so
|
|
136
|
+
// rethrow a sanitized copy (ADR-048 §6 item 11).
|
|
137
|
+
throw toSanitizedSurrogate(error);
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
telemetry: activeTelemetry,
|
|
142
|
+
usageRollup: activeUsageRollup,
|
|
245
143
|
eventSummary,
|
|
246
|
-
shouldUseDatabase
|
|
247
|
-
|
|
248
|
-
|
|
144
|
+
shouldUseDatabase,
|
|
145
|
+
flushTimeoutMs,
|
|
146
|
+
context,
|
|
147
|
+
}
|
|
148
|
+
);
|
|
249
149
|
};
|
|
250
150
|
};
|
|
251
151
|
|
package/core/index.js
CHANGED
|
@@ -2,5 +2,12 @@ const { Delegate } = require('./Delegate');
|
|
|
2
2
|
const { Worker } = require('./Worker');
|
|
3
3
|
const { loadInstalledModules } = require('./load-installed-modules');
|
|
4
4
|
const { createHandler } = require('./create-handler');
|
|
5
|
+
const { runInvocationScope } = require('./invocation-scope');
|
|
5
6
|
|
|
6
|
-
module.exports = {
|
|
7
|
+
module.exports = {
|
|
8
|
+
Delegate,
|
|
9
|
+
Worker,
|
|
10
|
+
loadInstalledModules,
|
|
11
|
+
createHandler,
|
|
12
|
+
runInvocationScope,
|
|
13
|
+
};
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
const { runInContext, LOGGER_SCOPE_KEY } = require('../logs/context');
|
|
2
|
+
const { summarizeMessageBody } = require('../logs/summarize-event');
|
|
3
|
+
const { flushSinks, hasFlushableSinks } = require('../logs/logger-runtime');
|
|
4
|
+
|
|
5
|
+
// Bounds the tail latency telemetry adds to every warm invocation. Kept low so
|
|
6
|
+
// an unreachable OTLP endpoint (e.g. a VPC Lambda with no NAT/egress) costs at
|
|
7
|
+
// most this, not multiple seconds. Override with OTEL_FLUSH_TIMEOUT_MS.
|
|
8
|
+
const DEFAULT_FLUSH_TIMEOUT_MS =
|
|
9
|
+
Number(process.env.OTEL_FLUSH_TIMEOUT_MS) || 500;
|
|
10
|
+
|
|
11
|
+
// Time kept back from the Lambda deadline, so a slow flush never becomes a
|
|
12
|
+
// Lambda timeout.
|
|
13
|
+
const FLUSH_MARGIN_MS = 50;
|
|
14
|
+
|
|
15
|
+
// setTimeout fires at once for a delay above this.
|
|
16
|
+
const MAX_TIMER_MS = 2 ** 31 - 1;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Fold the invocation's buffered usage counters into the durable store, then
|
|
20
|
+
* clear the buffer. On an SQS redelivery (ApproximateReceiveCount > 1) we
|
|
21
|
+
* DISCARD rather than flush — the prior delivery already counted, and the usage
|
|
22
|
+
* accuracy contract is "approximate, skip obvious redeliveries". Fully guarded.
|
|
23
|
+
*/
|
|
24
|
+
async function flushUsageRollup(subscriber, eventSummary, shouldUseDatabase) {
|
|
25
|
+
if (!subscriber) return;
|
|
26
|
+
try {
|
|
27
|
+
// Persisting usage requires a DB connection. DB-free handlers (e.g. the
|
|
28
|
+
// webhook-receipt route) never called connectPrisma, so drop the buffer
|
|
29
|
+
// instead of issuing a connectionless Prisma write.
|
|
30
|
+
if (!shouldUseDatabase) {
|
|
31
|
+
subscriber.discard();
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
// Discard only when EVERY record in the batch is a redelivery. The buffer
|
|
35
|
+
// is invocation-scoped (not per-message), so discarding on *any*
|
|
36
|
+
// redelivery would drop the fresh records' counts too (silent
|
|
37
|
+
// under-count). For a mixed batch we flush: preserving fresh counts and
|
|
38
|
+
// at worst re-counting the one redelivered record is strictly better than
|
|
39
|
+
// losing fresh data for an approximate store. (Integration queue workers
|
|
40
|
+
// are batchSize:1 today, so a batch is all-or-nothing; this keeps it
|
|
41
|
+
// correct if batchSize is ever raised.)
|
|
42
|
+
const records = Array.isArray(eventSummary?.records)
|
|
43
|
+
? eventSummary.records
|
|
44
|
+
: [];
|
|
45
|
+
const allRedelivered =
|
|
46
|
+
records.length > 0 &&
|
|
47
|
+
records.every((r) => Number(r.receiveCount) > 1);
|
|
48
|
+
if (allRedelivered) {
|
|
49
|
+
subscriber.discard();
|
|
50
|
+
} else {
|
|
51
|
+
await subscriber.flush();
|
|
52
|
+
}
|
|
53
|
+
} catch (_) {
|
|
54
|
+
// Usage rollup must never break the handler.
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isTelemetryEnabled(telemetry) {
|
|
59
|
+
try {
|
|
60
|
+
return Boolean(
|
|
61
|
+
telemetry &&
|
|
62
|
+
typeof telemetry.isEnabled === 'function' &&
|
|
63
|
+
telemetry.isEnabled()
|
|
64
|
+
);
|
|
65
|
+
} catch (_) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Flush telemetry before the Lambda container freezes. Because
|
|
72
|
+
* `callbackWaitsForEmptyEventLoop=false` stops the event loop the moment the
|
|
73
|
+
* handler returns, OTel's timer-driven batch processors would never fire — so
|
|
74
|
+
* spans/metrics must be flushed here. No timer of its own: the caller bounds it
|
|
75
|
+
* with `withDeadline`. Fully guarded, so a flush failure never breaks the
|
|
76
|
+
* handler.
|
|
77
|
+
*/
|
|
78
|
+
async function flushTelemetry(telemetry, { signal } = {}) {
|
|
79
|
+
if (!isTelemetryEnabled(telemetry) || signal?.aborted) return;
|
|
80
|
+
try {
|
|
81
|
+
await telemetry.forceFlush();
|
|
82
|
+
} catch (_) {
|
|
83
|
+
// Telemetry flush must never break the handler.
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Run `fn(signal)` for at most `ms`. Owns the only timer and clears it when the
|
|
89
|
+
* work settles. Aborts the signal at the deadline. Never rejects, and a late
|
|
90
|
+
* rejection of the work is swallowed.
|
|
91
|
+
*/
|
|
92
|
+
function withDeadline(ms, fn) {
|
|
93
|
+
const controller = new AbortController();
|
|
94
|
+
return new Promise((resolve) => {
|
|
95
|
+
let timer;
|
|
96
|
+
const finish = () => {
|
|
97
|
+
clearTimeout(timer);
|
|
98
|
+
resolve();
|
|
99
|
+
};
|
|
100
|
+
timer = setTimeout(() => {
|
|
101
|
+
controller.abort();
|
|
102
|
+
finish();
|
|
103
|
+
}, Math.min(ms, MAX_TIMER_MS));
|
|
104
|
+
let work;
|
|
105
|
+
try {
|
|
106
|
+
work = Promise.resolve(fn(controller.signal));
|
|
107
|
+
} catch (_) {
|
|
108
|
+
work = Promise.resolve();
|
|
109
|
+
}
|
|
110
|
+
work.then(finish, finish);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function remainingTimeMs(context) {
|
|
115
|
+
try {
|
|
116
|
+
const remaining = context?.getRemainingTimeInMillis?.();
|
|
117
|
+
return typeof remaining === 'number' ? remaining : Infinity;
|
|
118
|
+
} catch (_) {
|
|
119
|
+
return Infinity;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function flushInvocation({
|
|
124
|
+
telemetry,
|
|
125
|
+
usageRollup,
|
|
126
|
+
eventSummary,
|
|
127
|
+
shouldUseDatabase,
|
|
128
|
+
flushTimeoutMs,
|
|
129
|
+
context,
|
|
130
|
+
}) {
|
|
131
|
+
// ADR-048 §11: the deadline is fixed at flush start. The usage rollup
|
|
132
|
+
// emits no telemetry, so it runs in parallel with no bound of its own.
|
|
133
|
+
const deadline = Math.min(
|
|
134
|
+
flushTimeoutMs,
|
|
135
|
+
remainingTimeMs(context) - FLUSH_MARGIN_MS
|
|
136
|
+
);
|
|
137
|
+
await Promise.all([
|
|
138
|
+
flushUsageRollup(usageRollup, eventSummary, shouldUseDatabase),
|
|
139
|
+
flushBounded(telemetry, deadline),
|
|
140
|
+
]);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function flushBounded(telemetry, deadline) {
|
|
144
|
+
const flushes = [];
|
|
145
|
+
if (isTelemetryEnabled(telemetry)) {
|
|
146
|
+
flushes.push((signal) => flushTelemetry(telemetry, { signal }));
|
|
147
|
+
}
|
|
148
|
+
if (hasFlushableSinks()) {
|
|
149
|
+
flushes.push((signal) => flushSinks({ signal }));
|
|
150
|
+
}
|
|
151
|
+
if (!flushes.length || !(deadline > 0)) return undefined;
|
|
152
|
+
return withDeadline(deadline, (signal) =>
|
|
153
|
+
Promise.allSettled(flushes.map((flush) => flush(signal)))
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Open the logger scope for one invocation, run `fn` inside it, then flush.
|
|
159
|
+
* `fields` (requestId, handlerName, method, route, routeKey, invocation, ...)
|
|
160
|
+
* go into the scope's logger sub-object and never reach the telemetry bus.
|
|
161
|
+
*/
|
|
162
|
+
async function runInvocationScope(fields, fn, opts = {}) {
|
|
163
|
+
const {
|
|
164
|
+
telemetry,
|
|
165
|
+
usageRollup,
|
|
166
|
+
eventSummary,
|
|
167
|
+
shouldUseDatabase = true,
|
|
168
|
+
flushTimeoutMs = DEFAULT_FLUSH_TIMEOUT_MS,
|
|
169
|
+
context,
|
|
170
|
+
} = opts;
|
|
171
|
+
return runInContext({ [LOGGER_SCOPE_KEY]: { ...(fields || {}) } }, async () => {
|
|
172
|
+
try {
|
|
173
|
+
return await fn();
|
|
174
|
+
} finally {
|
|
175
|
+
// Every step is guarded, so a flush never changes the result.
|
|
176
|
+
await flushInvocation({
|
|
177
|
+
telemetry,
|
|
178
|
+
usageRollup,
|
|
179
|
+
eventSummary,
|
|
180
|
+
shouldUseDatabase,
|
|
181
|
+
flushTimeoutMs,
|
|
182
|
+
context,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function toCount(value) {
|
|
189
|
+
const count = Number(value);
|
|
190
|
+
return value !== undefined && value !== null && Number.isFinite(count)
|
|
191
|
+
? count
|
|
192
|
+
: undefined;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Open the logger scope for one SQS message: messageId, receiveCount and the
|
|
197
|
+
* Frigg ids the body carries. No flush; the invocation scope owns that.
|
|
198
|
+
*/
|
|
199
|
+
function runMessageScope(record, fn) {
|
|
200
|
+
const { event, processId, integrationId } = summarizeMessageBody(
|
|
201
|
+
record?.body
|
|
202
|
+
);
|
|
203
|
+
return runInContext(
|
|
204
|
+
{
|
|
205
|
+
[LOGGER_SCOPE_KEY]: {
|
|
206
|
+
messageId: record?.messageId,
|
|
207
|
+
receiveCount: toCount(record?.attributes?.ApproximateReceiveCount),
|
|
208
|
+
integrationEvent: event,
|
|
209
|
+
processId,
|
|
210
|
+
integrationId,
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
fn
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
module.exports = {
|
|
218
|
+
runInvocationScope,
|
|
219
|
+
runMessageScope,
|
|
220
|
+
withDeadline,
|
|
221
|
+
flushTelemetry,
|
|
222
|
+
flushUsageRollup,
|
|
223
|
+
FLUSH_MARGIN_MS,
|
|
224
|
+
DEFAULT_FLUSH_TIMEOUT_MS,
|
|
225
|
+
};
|
package/core/secrets-to-env.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
const { getLogger } = require('../logs');
|
|
2
|
+
|
|
3
|
+
const log = getLogger('frigg.core.secrets');
|
|
4
|
+
|
|
1
5
|
const getSecretValue = async () => {
|
|
2
|
-
|
|
6
|
+
log.debug('Fetching secrets', { eventName: 'frigg.core.secrets.fetching' });
|
|
3
7
|
|
|
4
8
|
const httpPort = process.env.PARAMETERS_SECRETS_EXTENSION_HTTP_PORT || 2773;
|
|
5
9
|
const url = `http://localhost:${httpPort}/secretsmanager/get?secretId=${encodeURIComponent(
|
|
@@ -15,10 +19,7 @@ const getSecretValue = async () => {
|
|
|
15
19
|
const response = await fetch(url, options);
|
|
16
20
|
|
|
17
21
|
if (!response.ok) {
|
|
18
|
-
|
|
19
|
-
console.error('Invalid response - response:', JSON.stringify(response));
|
|
20
|
-
console.error('Invalid response - json:', json);
|
|
21
|
-
throw new Error(`Invalid ${response.status} response`);
|
|
22
|
+
throw new Error(`Secrets fetch failed with ${response.status}`);
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
const result = await response.json();
|
|
@@ -44,7 +45,7 @@ const secretsToEnv = async () => {
|
|
|
44
45
|
if (!process.env.SECRET_ARN) {
|
|
45
46
|
return;
|
|
46
47
|
}
|
|
47
|
-
|
|
48
|
+
log.debug('Secrets to env', { eventName: 'frigg.core.secrets.to_env' });
|
|
48
49
|
|
|
49
50
|
try {
|
|
50
51
|
const secrets = await getSecretValue();
|
package/database/config.js
CHANGED
|
@@ -125,16 +125,9 @@ let cachedDbType = null;
|
|
|
125
125
|
*/
|
|
126
126
|
const PRISMA_LOG_LEVEL = process.env.PRISMA_LOG_LEVEL || 'error,warn';
|
|
127
127
|
|
|
128
|
-
/**
|
|
129
|
-
* Enable Prisma query logging for performance monitoring
|
|
130
|
-
* @type {boolean}
|
|
131
|
-
*/
|
|
132
|
-
const PRISMA_QUERY_LOGGING = process.env.PRISMA_QUERY_LOGGING === 'true';
|
|
133
|
-
|
|
134
128
|
module.exports = {
|
|
135
129
|
getDatabaseType, // Export for testing and direct use
|
|
136
130
|
PRISMA_LOG_LEVEL,
|
|
137
|
-
PRISMA_QUERY_LOGGING,
|
|
138
131
|
};
|
|
139
132
|
|
|
140
133
|
/**
|
|
@@ -680,7 +680,11 @@ The encryption system uses **fail-fast error handling**:
|
|
|
680
680
|
|
|
681
681
|
### Logging Configuration
|
|
682
682
|
|
|
683
|
-
|
|
683
|
+
`FRIGG_LOG_LEVEL` is the one Frigg log level. The level rules, the record
|
|
684
|
+
fields and the redaction rules are in the
|
|
685
|
+
[Logging guide](../../../../docs/guides/LOGGING.md). The encryption logger
|
|
686
|
+
still writes text lines through `console` with the same level names until it
|
|
687
|
+
moves to the structured logger (ADR-048, incremental adoption).
|
|
684
688
|
|
|
685
689
|
```bash
|
|
686
690
|
# Production (minimal logging)
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
const { logger } = require('./logger');
|
|
12
|
+
const { registerDeniedKeys } = require('../../logs/denied-keys');
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Core encryption schema (immutable - cannot be overridden by custom schemas)
|
|
@@ -120,6 +121,7 @@ function registerCustomSchema(schema) {
|
|
|
120
121
|
}
|
|
121
122
|
|
|
122
123
|
customSchema = { ...schema };
|
|
124
|
+
registerDeniedKeys(Object.values(schema).flatMap((config) => config.fields));
|
|
123
125
|
logger.info(
|
|
124
126
|
`Registered custom encryption schema for models: ${Object.keys(customSchema).join(', ')}`
|
|
125
127
|
);
|
|
@@ -149,6 +151,7 @@ function extractCredentialFieldsFromModules(moduleDefinitions) {
|
|
|
149
151
|
}
|
|
150
152
|
}
|
|
151
153
|
|
|
154
|
+
registerDeniedKeys(fields);
|
|
152
155
|
return [...new Set(fields)];
|
|
153
156
|
}
|
|
154
157
|
|
|
@@ -17,9 +17,12 @@ class EncryptionLogger {
|
|
|
17
17
|
this.minLevel = this._getMinLevel();
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
// FRIGG_LOG_LEVEL also takes TRACE and FATAL (ADR-048); this logger has
|
|
21
|
+
// no such levels, so they map to its nearest ones.
|
|
20
22
|
_getMinLevel() {
|
|
21
|
-
const level = process.env.FRIGG_LOG_LEVEL || 'INFO';
|
|
22
|
-
|
|
23
|
+
const level = (process.env.FRIGG_LOG_LEVEL || 'INFO').trim().toUpperCase();
|
|
24
|
+
const mapped = { TRACE: 'DEBUG', FATAL: 'ERROR' }[level] ?? level;
|
|
25
|
+
return LOG_LEVELS[mapped] ?? LOG_LEVELS.INFO;
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
_shouldLog(level) {
|