@friggframework/core 2.0.0-next.111 → 2.0.0-next.113
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 +101 -13
- 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/documentdb-encryption-service.js +6 -2
- package/database/documentdb-utils.js +2 -1
- package/database/encryption/README.md +25 -1
- package/database/encryption/encryption-schema-registry.js +3 -0
- package/database/encryption/integration-mapping-encryption.js +97 -0
- package/database/encryption/logger.js +5 -2
- package/database/encryption/prisma-encryption-extension.js +20 -9
- package/errors/fetch-error.js +48 -65
- 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/repositories/integration-mapping-query-pipeline.js +152 -0
- package/integrations/repositories/integration-mapping-query.js +193 -0
- package/integrations/repositories/integration-mapping-repository-documentdb.js +53 -0
- package/integrations/repositories/integration-mapping-repository-interface.js +48 -0
- package/integrations/repositories/integration-mapping-repository-mongo.js +72 -0
- package/integrations/repositories/integration-mapping-repository-postgres.js +118 -0
- 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
|
/**
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
const { Cryptor } = require('../encrypt/Cryptor');
|
|
2
|
-
const {
|
|
2
|
+
const {
|
|
3
|
+
getEncryptedFields,
|
|
4
|
+
getFieldsToEncryptOnWrite,
|
|
5
|
+
loadCustomEncryptionSchema,
|
|
6
|
+
} = require('./encryption/encryption-schema-registry');
|
|
3
7
|
|
|
4
8
|
/**
|
|
5
9
|
* Encryption service specifically for DocumentDB repositories
|
|
@@ -107,7 +111,7 @@ class DocumentDBEncryptionService {
|
|
|
107
111
|
}
|
|
108
112
|
|
|
109
113
|
// Get encrypted fields from registry
|
|
110
|
-
const encryptedFieldsConfig =
|
|
114
|
+
const encryptedFieldsConfig = getFieldsToEncryptOnWrite(modelName);
|
|
111
115
|
if (!encryptedFieldsConfig || encryptedFieldsConfig.length === 0) {
|
|
112
116
|
return document;
|
|
113
117
|
}
|
|
@@ -166,11 +166,12 @@ async function findManyDrained(client, collection, filter = {}, options = {}) {
|
|
|
166
166
|
return drainCursor(client, collection, first);
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
-
async function aggregateDrained(client, collection, pipeline) {
|
|
169
|
+
async function aggregateDrained(client, collection, pipeline, options = {}) {
|
|
170
170
|
const first = await client.$runCommandRaw({
|
|
171
171
|
aggregate: collection,
|
|
172
172
|
pipeline,
|
|
173
173
|
cursor: { batchSize: DRAIN_BATCH_SIZE },
|
|
174
|
+
...options,
|
|
174
175
|
});
|
|
175
176
|
return drainCursor(client, collection, first);
|
|
176
177
|
}
|