@friggframework/core 2.0.0--canary.652.b14f3b5.0 → 2.0.0--canary.654.be44e66.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.
Files changed (56) hide show
  1. package/CLAUDE.md +13 -12
  2. package/README.md +19 -22
  3. package/core/CLAUDE.md +25 -12
  4. package/core/Worker.js +37 -34
  5. package/core/create-handler.js +97 -197
  6. package/core/index.js +8 -1
  7. package/core/invocation-scope.js +224 -0
  8. package/core/secrets-to-env.js +7 -6
  9. package/database/config.js +0 -7
  10. package/database/encryption/README.md +5 -1
  11. package/database/encryption/encryption-schema-registry.js +3 -0
  12. package/errors/fetch-error.js +48 -65
  13. package/handlers/app-definition-loader.js +38 -2
  14. package/handlers/app-handler-helpers.js +15 -4
  15. package/handlers/database-migration-handler.js +10 -1
  16. package/handlers/routers/websocket.js +26 -6
  17. package/handlers/workers/db-migration.js +12 -1
  18. package/handlers/workers/dlq-processor.js +66 -38
  19. package/index.js +19 -1
  20. package/integrations/integration-base.js +56 -38
  21. package/integrations/integration-router.js +21 -20
  22. package/integrations/use-cases/delete-integration-for-user.js +24 -13
  23. package/jest.config.js +2 -0
  24. package/logs/.eslintrc.json +26 -1
  25. package/logs/__fixtures__/events.js +82 -0
  26. package/logs/__fixtures__/matchers.js +45 -0
  27. package/logs/__fixtures__/secrets.js +22 -0
  28. package/logs/__fixtures__/vectors.js +312 -0
  29. package/logs/__fixtures__/with-env.js +45 -0
  30. package/logs/context.js +92 -0
  31. package/logs/debug-shims.js +43 -0
  32. package/logs/index.js +14 -3
  33. package/logs/jest-logger-setup.js +25 -0
  34. package/logs/levels.js +71 -0
  35. package/logs/logger-runtime.js +142 -0
  36. package/logs/logger.js +132 -42
  37. package/logs/record.js +199 -0
  38. package/logs/redact.js +293 -0
  39. package/logs/serialize.js +322 -0
  40. package/logs/sinks.js +99 -0
  41. package/logs/summarize-event.js +110 -0
  42. package/modules/module.js +32 -19
  43. package/modules/requester/oauth-2.js +28 -22
  44. package/modules/requester/requester.js +69 -6
  45. package/package.json +5 -6
  46. package/queues/queuer-util.js +1 -15
  47. package/telemetry/bind-telemetry-context.js +4 -0
  48. package/telemetry/instrument-handler.js +19 -2
  49. package/telemetry/no-op-telemetry.js +4 -0
  50. package/telemetry/otel-telemetry.js +25 -6
  51. package/telemetry/telemetry-context.js +19 -8
  52. package/telemetry/telemetry-runtime.js +17 -0
  53. package/telemetry/telemetry-service.js +1 -0
  54. package/types/errors/index.d.ts +24 -10
  55. package/types/logs/index.d.ts +61 -2
  56. package/user/repositories/user-repository-documentdb.js +18 -42
package/CLAUDE.md CHANGED
@@ -437,22 +437,24 @@ if (!userId) {
437
437
 
438
438
  ### 9. Logging System (`/logs`)
439
439
 
440
- **Purpose**: Structured logging with debug capabilities.
440
+ **Purpose**: One redacted JSON record per line to stdout (ADR-048). See `docs/guides/LOGGING.md`.
441
441
 
442
- **Functions**:
443
- - `debug(message, data)` - Debug logging
444
- - `initDebugLog(eventName, event)` - Initialize debug context
445
- - `flushDebugLog(error)` - Flush logs on error
442
+ **Rules**:
443
+ - `logs/` is a leaf: it requires only Node built-ins and sibling `logs/` files.
444
+ - Integrations and API modules use `this.logger`; core uses one `getLogger('frigg.<area>')` per module.
445
+ - Fixed message text; ids and counts go into fields. `frigg.*` records at WARN and above need an `eventName` (`<logger name>.<action>`); tests fail without it.
446
+ - Log an error one time, at the boundary (`createHandler`, the express middleware, `Worker.run`, the DLQ processor). Inner code throws with `cause`.
447
+ - Use `statusCode`, not `status` (reserved key). `body`, `payload`, `response` are dropped at INFO and above.
448
+ - Tests assert on `createMemorySink()` records by `eventName`, not on console spies.
449
+ - `debug`, `initDebugLog`, `flushDebugLog` are deprecated shims.
446
450
 
447
451
  **Usage**:
448
452
 
449
453
  ```javascript
450
- const { debug, initDebugLog, flushDebugLog } = require('@friggframework/core');
454
+ const { getLogger } = require('../logs');
455
+ const log = getLogger('frigg.core.sync');
451
456
 
452
- initDebugLog('MyIntegration', event);
453
- debug('Processing request', { userId, action });
454
- // ... your code ...
455
- flushDebugLog(); // On error
457
+ log.warn('Sync skipped', { eventName: 'frigg.core.sync.skipped', processId });
456
458
  ```
457
459
 
458
460
  ### 10. Lambda Utilities (`/lambda`)
@@ -660,8 +662,7 @@ Use test doubles from `@friggframework/test` package for consistent mocking.
660
662
  ### Optional
661
663
 
662
664
  - `SECRET_ARN` - AWS Secrets Manager ARN for auto-injection
663
- - `DEBUG` - Debug logging pattern
664
- - `LOG_LEVEL` - Logging level (debug, info, warn, error)
665
+ - `FRIGG_LOG_LEVEL` - Minimum log level (`TRACE` … `FATAL`, default `INFO`; `DEBUG` on local runs)
665
666
 
666
667
  ## Version Information
667
668
 
package/README.md CHANGED
@@ -107,10 +107,6 @@ FRIGG_ENCRYPTION_KEY=your-256-bit-encryption-key
107
107
  AWS_REGION=us-east-1
108
108
  AWS_ACCESS_KEY_ID=your-access-key
109
109
  AWS_SECRET_ACCESS_KEY=your-secret-key
110
-
111
- # Logging
112
- DEBUG=frigg:*
113
- LOG_LEVEL=info
114
110
  ```
115
111
 
116
112
  ## Core Components
@@ -255,10 +251,12 @@ const {
255
251
  // Custom business logic error
256
252
  throw new RequiredPropertyError('userId is required');
257
253
 
258
- // API communication error
259
- throw new FetchError('Failed to fetch data from external API', {
260
- statusCode: 404,
261
- response: errorResponse
254
+ // API communication error. The message is built from the method, the
255
+ // sanitized URL and the status; the response body stays off the message.
256
+ throw await FetchError.create({
257
+ resource: 'https://api.example.com/contacts',
258
+ init: { method: 'GET' },
259
+ response,
262
260
  });
263
261
 
264
262
  // Base error with custom properties
@@ -270,25 +268,25 @@ throw new BaseError('Integration failed', {
270
268
 
271
269
  ### 6. Logging (`/logs`)
272
270
 
273
- Structured logging with debug capabilities.
271
+ One redacted JSON record per line to stdout, with the correlation ids of
272
+ ADR-011. See the [Logging guide](../../docs/guides/LOGGING.md).
274
273
 
275
274
  **Usage:**
276
275
  ```javascript
277
- const { debug, initDebugLog, flushDebugLog } = require('@friggframework/core');
278
-
279
- // Initialize debug logging
280
- initDebugLog('integration:slack');
281
-
282
- // Log debug information
283
- debug('Processing webhook payload', {
284
- eventType: 'contact.created',
285
- payload: webhookData
276
+ // In an integration or an API module
277
+ this.logger.info('Contact batch started', {
278
+ eventName: 'integration.hubspot.batch_started',
279
+ batchSize: contacts.length,
286
280
  });
287
281
 
288
- // Flush logs (useful in serverless environments)
289
- await flushDebugLog();
282
+ // In core code
283
+ const { getLogger } = require('@friggframework/core');
284
+ const log = getLogger('frigg.core.sync');
285
+ log.warn('Sync skipped', { eventName: 'frigg.core.sync.skipped', processId });
290
286
  ```
291
287
 
288
+ `debug`, `initDebugLog` and `flushDebugLog` are deprecated shims.
289
+
292
290
  ### 7. User Management (`/user`)
293
291
 
294
292
  Comprehensive user authentication and authorization system supporting both individual and organizational users.
@@ -1013,8 +1011,7 @@ const {
1013
1011
  | `MONGO_URI` | Yes | MongoDB connection string |
1014
1012
  | `FRIGG_ENCRYPTION_KEY` | Yes | 256-bit encryption key |
1015
1013
  | `AWS_REGION` | No | AWS region for services |
1016
- | `DEBUG` | No | Debug logging pattern |
1017
- | `LOG_LEVEL` | No | Logging level (debug, info, warn, error) |
1014
+ | `FRIGG_LOG_LEVEL` | No | Minimum log level: `TRACE`, `DEBUG`, `INFO` (default), `WARN`, `ERROR`, `FATAL`. Local runs default to `DEBUG` |
1018
1015
 
1019
1016
  ## License
1020
1017
 
package/core/CLAUDE.md CHANGED
@@ -21,13 +21,13 @@ This file provides guidance to Claude Code when working with the Frigg Framework
21
21
  - **Database Connection Management**: Automatic MongoDB connection with pooling
22
22
  - **Secrets Management**: AWS Secrets Manager integration via `SECRET_ARN` env var
23
23
  - **Error Sanitization**: Prevents internal details from leaking to end users
24
- - **Debug Logging**: Request/response logging with structured debug info
24
+ - **Invocation Scope**: `runInvocationScope` puts `requestId`, `route` and the redacted event summary on every record, then flushes usage, telemetry and sinks
25
25
  - **Connection Optimization**: `context.callbackWaitsForEmptyEventLoop = false` for reuse
26
26
 
27
27
  **Handler Configuration Options**:
28
28
  ```javascript
29
29
  const handler = createHandler({
30
- eventName: 'MyIntegration', // For logging/debugging
30
+ eventName: 'MyIntegration', // Logged as handlerName
31
31
  isUserFacingResponse: true, // true = sanitize errors, false = pass through
32
32
  method: async (event, context) => {}, // Your Lambda function logic
33
33
  shouldUseDatabase: true // false = skip MongoDB connection
@@ -129,7 +129,8 @@ class MyIntegration extends Delegate {
129
129
  ### Lambda Handler Lifecycle
130
130
  1. **Pre-Execution Setup**:
131
131
  ```javascript
132
- initDebugLog(eventName, event); // Debug logging setup
132
+ runInvocationScope({ requestId, handlerName, method, route, invocation }, …); // Logger scope for the invocation
133
+ log.info('Handler invoked', { eventName: 'frigg.handler.invoked' });
133
134
  await secretsToEnv(); // Secrets Manager injection
134
135
  await parametersToEnv(); // SSM Parameter Store fetch (only when SSM_PARAMETER_PREFIX + FRIGG_SSM_OFFLOADED_KEYS are set)
135
136
  context.callbackWaitsForEmptyEventLoop = false; // Connection pooling
@@ -149,12 +150,20 @@ class MyIntegration extends Delegate {
149
150
 
150
151
  4. **Error Handling & Cleanup**:
151
152
  ```javascript
152
- flushDebugLog(error); // Debug info flush on error
153
- // Sanitized error response for user-facing endpoints
153
+ // One record at the boundary: WARN frigg.handler.rejected (client-safe),
154
+ // ERROR frigg.handler.failed, or ERROR frigg.handler.halted (halt, no retry).
155
+ // Sanitized error response for user-facing endpoints; server-to-server
156
+ // errors are rethrown as a sanitized surrogate (toSanitizedSurrogate).
157
+ ```
158
+
159
+ 5. **Flush** (in `runInvocationScope`'s `finally`):
160
+ ```javascript
161
+ // Usage rollup first (unbounded), then telemetry + log sinks in parallel
162
+ // under one deadline: min(flushTimeoutMs, remaining time - 50 ms).
154
163
  ```
155
164
 
156
165
  ### SQS Job Processing Lifecycle
157
- 1. **Batch Processing**: Process all records in `event.Records` sequentially
166
+ 1. **Batch Processing**: Process all records in `event.Records` sequentially, each inside `runMessageScope` (adds `messageId`, `receiveCount`, `processId`, `integrationId`, `integrationEvent` to its records)
158
167
  2. **Message Parsing**: JSON.parse message body for parameters
159
168
  3. **Validation**: Run custom validation on parsed parameters
160
169
  4. **Execution**: Call `_run()` method with validated parameters
@@ -205,7 +214,9 @@ const handler = createHandler({
205
214
  - Logs full error details internally
206
215
 
207
216
  2. **Server-to-Server Errors**: `isUserFacingResponse: false`
208
- - Re-throws original error for AWS handling
217
+ - Logs one ERROR, then rethrows a sanitized surrogate for AWS handling:
218
+ a fresh `Error` with the sanitized `name`, `message`, `stack`, plus
219
+ `statusCode` and `code`. `instanceof` checks and custom properties are gone
209
220
  - Used for SQS, SNS, and internal API calls
210
221
  - Enables proper retry mechanisms
211
222
 
@@ -214,12 +225,14 @@ const handler = createHandler({
214
225
  - Prevents infinite retries for known issues
215
226
  - Used for graceful degradation scenarios
216
227
 
217
- ### Debug Logging Strategy
228
+ ### Logging Strategy
218
229
  ```javascript
219
- initDebugLog(eventName, event); // Start logging context
220
- // ... your code ...
221
- flushDebugLog(error); // Flush on error (includes full context)
230
+ const { getLogger } = require('../logs');
231
+ const log = getLogger('frigg.core.sync');
232
+ log.info('Sync started', { eventName: 'frigg.core.sync.started', processId });
233
+ // Throw with cause; the boundary logs the error one time.
222
234
  ```
235
+ See `docs/guides/LOGGING.md`.
223
236
 
224
237
  ## Integration Development Patterns
225
238
 
@@ -646,7 +659,7 @@ describe('Health Handler', () => {
646
659
  const { createHandler } = require('@friggframework/core/core');
647
660
 
648
661
  const testHandler = createHandler({
649
- isUserFacingResponse: false, // Get full errors in tests
662
+ isUserFacingResponse: false, // Rethrows a sanitized surrogate (name, message, statusCode, code; not the original instance)
650
663
  shouldUseDatabase: false, // Mock/skip DB in tests
651
664
  method: yourTestMethod
652
665
  });
package/core/Worker.js CHANGED
@@ -2,6 +2,7 @@ const { SQSClient, GetQueueUrlCommand, SendMessageCommand } = require('@aws-sdk/
2
2
  const _ = require('lodash');
3
3
  const { RequiredPropertyError } = require('../errors');
4
4
  const { get } = require('../assertions');
5
+ const { runMessageScope } = require('./invocation-scope');
5
6
 
6
7
  const sqs = new SQSClient({ region: process.env.AWS_REGION });
7
8
 
@@ -25,45 +26,47 @@ class Worker {
25
26
  );
26
27
 
27
28
  for (const record of records) {
28
- // Log record entry with SQS-provided attributes useful for tracing
29
- // delivery history (ApproximateReceiveCount for retries, etc.).
30
- let parsedEvent;
31
- try {
32
- parsedEvent = JSON.parse(record.body)?.event;
33
- } catch {
34
- parsedEvent = undefined;
35
- }
36
- console.log(`[Worker] record begin`, {
37
- messageId: record.messageId,
38
- event: parsedEvent,
39
- receiveCount: record.attributes?.ApproximateReceiveCount,
40
- });
41
-
42
- try {
43
- const runParams = JSON.parse(record.body);
44
- this._validateParams(runParams);
45
- await this._run(runParams, context);
46
- console.log(`[Worker] record success`, {
29
+ await runMessageScope(record, async () => {
30
+ // Log record entry with SQS-provided attributes useful for tracing
31
+ // delivery history (ApproximateReceiveCount for retries, etc.).
32
+ let parsedEvent;
33
+ try {
34
+ parsedEvent = JSON.parse(record.body)?.event;
35
+ } catch {
36
+ parsedEvent = undefined;
37
+ }
38
+ console.log(`[Worker] record begin`, {
47
39
  messageId: record.messageId,
48
- event: runParams?.event,
40
+ event: parsedEvent,
41
+ receiveCount: record.attributes?.ApproximateReceiveCount,
49
42
  });
50
- } catch (error) {
51
- if (error.isHaltError) {
52
- // HaltError means "discard this message, don't retry".
53
- // Treat as success so SQS deletes it from the queue.
54
- // Logged explicitly — silent discards made prod debugging
55
- // extremely hard; keep this visible.
56
- console.warn(`[Worker] record halted (discarded, no retry)`, {
43
+
44
+ try {
45
+ const runParams = JSON.parse(record.body);
46
+ this._validateParams(runParams);
47
+ await this._run(runParams, context);
48
+ console.log(`[Worker] record success`, {
57
49
  messageId: record.messageId,
58
- event: parsedEvent,
59
- reason: error.message,
60
- statusCode: error.statusCode,
50
+ event: runParams?.event,
61
51
  });
62
- continue;
52
+ } catch (error) {
53
+ if (error.isHaltError) {
54
+ // HaltError means "discard this message, don't retry".
55
+ // Treat as success so SQS deletes it from the queue.
56
+ // Logged explicitly — silent discards made prod debugging
57
+ // extremely hard; keep this visible.
58
+ console.warn(`[Worker] record halted (discarded, no retry)`, {
59
+ messageId: record.messageId,
60
+ event: parsedEvent,
61
+ reason: error.message,
62
+ statusCode: error.statusCode,
63
+ });
64
+ return;
65
+ }
66
+ console.error(`[Worker] Failed to process record ${record.messageId}:`, error);
67
+ batchItemFailures.push({ itemIdentifier: record.messageId });
63
68
  }
64
- console.error(`[Worker] Failed to process record ${record.messageId}:`, error);
65
- batchItemFailures.push({ itemIdentifier: record.messageId });
66
- }
69
+ });
67
70
  }
68
71
 
69
72
  if (batchItemFailures.length > 0) {
@@ -2,7 +2,15 @@
2
2
  // REMOVING FOR NOW UNTIL WE ADD WEBPACK BACK IN
3
3
  // require('source-map-support').install();
4
4
 
5
- const { initDebugLog, flushDebugLog } = require('../logs');
5
+ const { getLogger, toSanitizedSurrogate } = require('../logs');
6
+ const {
7
+ summarizeLambdaEvent,
8
+ toScopeInvocation,
9
+ } = require('../logs/summarize-event');
10
+ const {
11
+ runInvocationScope,
12
+ DEFAULT_FLUSH_TIMEOUT_MS,
13
+ } = require('./invocation-scope');
6
14
  const { secretsToEnv } = require('./secrets-to-env');
7
15
  const { parametersToEnv } = require('./parameters-to-env');
8
16
  const {
@@ -11,123 +19,7 @@ const {
11
19
  getPluginTelemetrySubscribers,
12
20
  } = require('../telemetry/telemetry-runtime');
13
21
 
14
- // Bounds the tail latency telemetry adds to every warm invocation. Kept low so
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
- };
22
+ const log = getLogger('frigg.handler');
131
23
 
132
24
  const createHandler = (optionByName = {}) => {
133
25
  const {
@@ -157,95 +49,103 @@ const createHandler = (optionByName = {}) => {
157
49
  // no-op after the first invocation.
158
50
  getPluginTelemetrySubscribers();
159
51
 
160
- try {
161
- console.info(`[createHandler] ${eventName}: handler entry`, {
162
- eventName,
163
- awsRequestId: context?.awsRequestId,
164
- ...eventSummary,
165
- });
166
-
167
- initDebugLog(eventName, event);
52
+ const scopeFields = {
53
+ requestId: context?.awsRequestId,
54
+ handlerName: eventName,
55
+ method: eventSummary.method,
56
+ route: eventSummary.route,
57
+ routeKey: eventSummary.routeKey,
58
+ invocation: toScopeInvocation(eventSummary),
59
+ };
168
60
 
169
- const requestMethod = event.httpMethod;
170
- const requestPath = event.path;
171
- if (requestMethod && requestPath) {
172
- console.info(`${requestMethod} ${requestPath}`);
173
- }
61
+ return runInvocationScope(
62
+ scopeFields,
63
+ async () => {
64
+ try {
65
+ log.info('Handler invoked', {
66
+ eventName: 'frigg.handler.invoked',
67
+ });
174
68
 
175
- // If enabled (i.e. if SECRET_ARN is set in process.env) Fetch secrets from AWS Secrets Manager, and set them as environment variables.
176
- await secretsToEnv();
69
+ // If enabled (i.e. if SECRET_ARN is set in process.env) Fetch secrets from AWS Secrets Manager, and set them as environment variables.
70
+ await secretsToEnv();
177
71
 
178
- // 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.
179
- await parametersToEnv();
72
+ // 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.
73
+ await parametersToEnv();
180
74
 
181
- // Lazy-required so DB-free handlers never load the Prisma client.
182
- if (shouldUseDatabase) {
183
- const { connectPrisma } = require('../database/prisma');
184
- await connectPrisma();
185
- }
75
+ // Lazy-required so DB-free handlers never load the Prisma client.
76
+ if (shouldUseDatabase) {
77
+ const { connectPrisma } = require('../database/prisma');
78
+ await connectPrisma();
79
+ }
186
80
 
187
- // Helps reuse the database connection. Lowers response times.
188
- context.callbackWaitsForEmptyEventLoop = false;
189
-
190
- // Run the Lambda
191
- return await method(event, context);
192
- } catch (error) {
193
- flushDebugLog(error);
194
-
195
- // Don't leak implementation details to end users.
196
- if (isUserFacingResponse) {
197
- // Allow client-safe errors to pass through with their actual message
198
- if (error.isClientSafe === true) {
199
- const statusCode = error.statusCode || 400;
200
- return {
201
- statusCode,
202
- body: JSON.stringify({
203
- error: error.message,
204
- }),
205
- };
206
- }
81
+ // Helps reuse the database connection. Lowers response times.
82
+ context.callbackWaitsForEmptyEventLoop = false;
83
+
84
+ // Run the Lambda
85
+ return await method(event, context);
86
+ } catch (error) {
87
+ // Don't leak implementation details to end users.
88
+ if (isUserFacingResponse) {
89
+ // Allow client-safe errors to pass through with their actual message
90
+ if (error.isClientSafe === true) {
91
+ const statusCode = error.statusCode || 400;
92
+ log.warn('Request rejected', {
93
+ eventName: 'frigg.handler.rejected',
94
+ statusCode,
95
+ error,
96
+ });
97
+ return {
98
+ statusCode,
99
+ body: JSON.stringify({
100
+ error: error.message,
101
+ }),
102
+ };
103
+ }
104
+
105
+ log.error('Handler failed', {
106
+ eventName: 'frigg.handler.failed',
107
+ error,
108
+ });
109
+
110
+ // Hide other errors with generic message
111
+ return {
112
+ statusCode: 500,
113
+ body: JSON.stringify({
114
+ error: 'An Internal Error Occurred',
115
+ }),
116
+ };
117
+ }
207
118
 
208
- // Hide other errors with generic message
209
- return {
210
- statusCode: 500,
211
- body: JSON.stringify({
212
- error: 'An Internal Error Occurred',
213
- }),
214
- };
215
- }
119
+ // Handle server-to-server responses.
216
120
 
217
- // Handle server-to-server responses.
218
-
219
- // Halt errors are logged but suceed and won't be retried.
220
- // Log explicitly — silent suppression here previously made stuck
221
- // messages invisible to observability tooling. Include
222
- // eventSummary so operators can correlate across concurrent
223
- // invocations (processId / messageIds / HTTP path).
224
- if (error.isHaltError === true) {
225
- console.warn(
226
- `[createHandler] ${eventName}: halt error suppressed (no retry)`,
227
- {
228
- eventName,
229
- errorName: error.name,
230
- errorMessage: error.message,
231
- statusCode: error.statusCode,
232
- ...eventSummary,
121
+ // Halt errors succeed and won't be retried, so the halt
122
+ // itself is the failure record.
123
+ if (error.isHaltError === true) {
124
+ log.error('Handler halted', {
125
+ eventName: 'frigg.handler.halted',
126
+ error,
127
+ });
128
+ return;
233
129
  }
234
- );
235
- return;
236
- }
237
130
 
238
- // Here we can just rethrow and let AWS build the response.
239
- throw error;
240
- } finally {
241
- // Flush telemetry + usage before the container freezes.
242
- await flushTelemetry(activeTelemetry, flushTimeoutMs);
243
- await flushUsageRollup(
244
- activeUsageRollup,
131
+ log.error('Handler failed', {
132
+ eventName: 'frigg.handler.failed',
133
+ error,
134
+ });
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 = { Delegate, Worker, loadInstalledModules, createHandler };
7
+ module.exports = {
8
+ Delegate,
9
+ Worker,
10
+ loadInstalledModules,
11
+ createHandler,
12
+ runInvocationScope,
13
+ };