@friggframework/core 2.0.0--canary.653.8213092.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 (59) hide show
  1. package/CLAUDE.md +13 -50
  2. package/README.md +19 -22
  3. package/core/CLAUDE.md +26 -14
  4. package/core/Worker.js +37 -35
  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/backend-utils.js +1 -2
  16. package/handlers/database-migration-handler.js +10 -1
  17. package/handlers/integration-event-dispatcher.js +2 -2
  18. package/handlers/routers/websocket.js +26 -6
  19. package/handlers/workers/db-migration.js +12 -1
  20. package/handlers/workers/dlq-processor.js +66 -38
  21. package/index.js +19 -1
  22. package/integrations/integration-base.js +56 -38
  23. package/integrations/integration-router.js +21 -20
  24. package/integrations/use-cases/delete-integration-for-user.js +24 -13
  25. package/jest.config.js +2 -0
  26. package/logs/.eslintrc.json +26 -1
  27. package/logs/__fixtures__/events.js +82 -0
  28. package/logs/__fixtures__/matchers.js +45 -0
  29. package/logs/__fixtures__/secrets.js +22 -0
  30. package/logs/__fixtures__/vectors.js +312 -0
  31. package/logs/__fixtures__/with-env.js +45 -0
  32. package/logs/context.js +92 -0
  33. package/logs/debug-shims.js +43 -0
  34. package/logs/index.js +14 -3
  35. package/logs/jest-logger-setup.js +25 -0
  36. package/logs/levels.js +71 -0
  37. package/logs/logger-runtime.js +142 -0
  38. package/logs/logger.js +132 -42
  39. package/logs/record.js +199 -0
  40. package/logs/redact.js +293 -0
  41. package/logs/serialize.js +322 -0
  42. package/logs/sinks.js +99 -0
  43. package/logs/summarize-event.js +110 -0
  44. package/modules/module.js +32 -19
  45. package/modules/requester/oauth-2.js +28 -22
  46. package/modules/requester/requester.js +69 -6
  47. package/package.json +5 -6
  48. package/queues/queuer-util.js +1 -15
  49. package/telemetry/bind-telemetry-context.js +4 -0
  50. package/telemetry/instrument-handler.js +19 -2
  51. package/telemetry/no-op-telemetry.js +4 -0
  52. package/telemetry/otel-telemetry.js +25 -6
  53. package/telemetry/telemetry-context.js +19 -8
  54. package/telemetry/telemetry-runtime.js +17 -0
  55. package/telemetry/telemetry-service.js +1 -0
  56. package/types/errors/index.d.ts +24 -10
  57. package/types/logs/index.d.ts +61 -2
  58. package/user/repositories/user-repository-documentdb.js +18 -42
  59. package/queues/queue-delivery.js +0 -41
package/CLAUDE.md CHANGED
@@ -414,43 +414,6 @@ const decrypted = await cryptor.decrypt(encrypted);
414
414
  - `integration-event-dispatcher.js` - Routes events to integration handlers
415
415
  - Supports lifecycle events and user actions
416
416
 
417
- **Queue handler delivery**: a handler dispatched from the integration queue
418
- (any `this.events` entry reached through SQS, including `ON_WEBHOOK` and
419
- scheduled jobs) receives `{ data, context, delivery }`. HTTP-dispatched
420
- handlers (`{ req, res, next }`) and `this.on` events do not.
421
-
422
- | Field | Type | Value |
423
- |---|---|---|
424
- | `delivery.receiveCount` | `number \| undefined` | SQS `ApproximateReceiveCount` of this delivery |
425
- | `delivery.maxReceiveCount` | `number \| undefined` | Receives allowed before SQS moves the message to the DLQ (`FRIGG_QUEUE_MAX_RECEIVE_COUNT`) |
426
- | `delivery.isLastAttempt` | `boolean` | `true` only when both counts are known and `receiveCount >= maxReceiveCount` |
427
-
428
- `isLastAttempt` is `false` when either count is unknown: a local or non-SQS
429
- invocation, or a queue whose redrive policy the stack does not own
430
- (`ownership.queue: 'external'`). The value is information only: core still
431
- rethrows retryable errors and discards halt errors (4xx except 408/429).
432
-
433
- Use it to end a run or count lost work on the final try: when a retryable
434
- error (429, 5xx, network) is about to be rethrown and `isLastAttempt` is
435
- `true`, the message goes to the DLQ next, so mark the run failed or count the
436
- message's records as failed before rethrowing.
437
-
438
- ```javascript
439
- async processBatch({ data, delivery }) {
440
- try {
441
- await this.syncPage(data);
442
- } catch (error) {
443
- if (delivery?.isLastAttempt) await this.failRun(data.processId, error);
444
- throw error;
445
- }
446
- }
447
- ```
448
-
449
- The single source of the max receive count is
450
- `INTEGRATION_QUEUE_MAX_RECEIVE_COUNT` in `queues/queue-delivery.js`. The
451
- devtools integration builder uses it for the queue's `RedrivePolicy` and sets
452
- it as `FRIGG_QUEUE_MAX_RECEIVE_COUNT` on the queue worker function.
453
-
454
417
  ### 8. Error Handling (`/errors`)
455
418
 
456
419
  **Purpose**: Standardized error types with proper HTTP semantics.
@@ -474,22 +437,24 @@ if (!userId) {
474
437
 
475
438
  ### 9. Logging System (`/logs`)
476
439
 
477
- **Purpose**: Structured logging with debug capabilities.
440
+ **Purpose**: One redacted JSON record per line to stdout (ADR-048). See `docs/guides/LOGGING.md`.
478
441
 
479
- **Functions**:
480
- - `debug(message, data)` - Debug logging
481
- - `initDebugLog(eventName, event)` - Initialize debug context
482
- - `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.
483
450
 
484
451
  **Usage**:
485
452
 
486
453
  ```javascript
487
- const { debug, initDebugLog, flushDebugLog } = require('@friggframework/core');
454
+ const { getLogger } = require('../logs');
455
+ const log = getLogger('frigg.core.sync');
488
456
 
489
- initDebugLog('MyIntegration', event);
490
- debug('Processing request', { userId, action });
491
- // ... your code ...
492
- flushDebugLog(); // On error
457
+ log.warn('Sync skipped', { eventName: 'frigg.core.sync.skipped', processId });
493
458
  ```
494
459
 
495
460
  ### 10. Lambda Utilities (`/lambda`)
@@ -697,9 +662,7 @@ Use test doubles from `@friggframework/test` package for consistent mocking.
697
662
  ### Optional
698
663
 
699
664
  - `SECRET_ARN` - AWS Secrets Manager ARN for auto-injection
700
- - `DEBUG` - Debug logging pattern
701
- - `LOG_LEVEL` - Logging level (debug, info, warn, error)
702
- - `FRIGG_QUEUE_MAX_RECEIVE_COUNT` - Set by devtools on integration queue workers whose queue the stack owns; feeds `delivery.maxReceiveCount`
665
+ - `FRIGG_LOG_LEVEL` - Minimum log level (`TRACE` … `FATAL`, default `INFO`; `DEBUG` on local runs)
703
666
 
704
667
  ## Version Information
705
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
@@ -52,10 +52,9 @@ const handler = createHandler({
52
52
  **Usage Pattern**:
53
53
  ```javascript
54
54
  class MyWorker extends Worker {
55
- async _run(params, context = {}, delivery) {
55
+ async _run(params, context = {}) {
56
56
  // Your job processing logic here
57
57
  // params are already JSON.parsed from SQS message body
58
- // delivery is { receiveCount, maxReceiveCount, isLastAttempt }
59
58
  }
60
59
 
61
60
  _validateParams(params) {
@@ -130,7 +129,8 @@ class MyIntegration extends Delegate {
130
129
  ### Lambda Handler Lifecycle
131
130
  1. **Pre-Execution Setup**:
132
131
  ```javascript
133
- 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' });
134
134
  await secretsToEnv(); // Secrets Manager injection
135
135
  await parametersToEnv(); // SSM Parameter Store fetch (only when SSM_PARAMETER_PREFIX + FRIGG_SSM_OFFLOADED_KEYS are set)
136
136
  context.callbackWaitsForEmptyEventLoop = false; // Connection pooling
@@ -150,12 +150,20 @@ class MyIntegration extends Delegate {
150
150
 
151
151
  4. **Error Handling & Cleanup**:
152
152
  ```javascript
153
- flushDebugLog(error); // Debug info flush on error
154
- // 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).
155
163
  ```
156
164
 
157
165
  ### SQS Job Processing Lifecycle
158
- 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)
159
167
  2. **Message Parsing**: JSON.parse message body for parameters
160
168
  3. **Validation**: Run custom validation on parsed parameters
161
169
  4. **Execution**: Call `_run()` method with validated parameters
@@ -206,7 +214,9 @@ const handler = createHandler({
206
214
  - Logs full error details internally
207
215
 
208
216
  2. **Server-to-Server Errors**: `isUserFacingResponse: false`
209
- - 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
210
220
  - Used for SQS, SNS, and internal API calls
211
221
  - Enables proper retry mechanisms
212
222
 
@@ -215,12 +225,14 @@ const handler = createHandler({
215
225
  - Prevents infinite retries for known issues
216
226
  - Used for graceful degradation scenarios
217
227
 
218
- ### Debug Logging Strategy
228
+ ### Logging Strategy
219
229
  ```javascript
220
- initDebugLog(eventName, event); // Start logging context
221
- // ... your code ...
222
- 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.
223
234
  ```
235
+ See `docs/guides/LOGGING.md`.
224
236
 
225
237
  ## Integration Development Patterns
226
238
 
@@ -647,7 +659,7 @@ describe('Health Handler', () => {
647
659
  const { createHandler } = require('@friggframework/core/core');
648
660
 
649
661
  const testHandler = createHandler({
650
- isUserFacingResponse: false, // Get full errors in tests
662
+ isUserFacingResponse: false, // Rethrows a sanitized surrogate (name, message, statusCode, code; not the original instance)
651
663
  shouldUseDatabase: false, // Mock/skip DB in tests
652
664
  method: yourTestMethod
653
665
  });
package/core/Worker.js CHANGED
@@ -2,7 +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 { readQueueDelivery } = require('../queues/queue-delivery');
5
+ const { runMessageScope } = require('./invocation-scope');
6
6
 
7
7
  const sqs = new SQSClient({ region: process.env.AWS_REGION });
8
8
 
@@ -26,45 +26,47 @@ class Worker {
26
26
  );
27
27
 
28
28
  for (const record of records) {
29
- // Log record entry with SQS-provided attributes useful for tracing
30
- // delivery history (ApproximateReceiveCount for retries, etc.).
31
- let parsedEvent;
32
- try {
33
- parsedEvent = JSON.parse(record.body)?.event;
34
- } catch {
35
- parsedEvent = undefined;
36
- }
37
- console.log(`[Worker] record begin`, {
38
- messageId: record.messageId,
39
- event: parsedEvent,
40
- receiveCount: record.attributes?.ApproximateReceiveCount,
41
- });
42
-
43
- try {
44
- const runParams = JSON.parse(record.body);
45
- this._validateParams(runParams);
46
- await this._run(runParams, context, readQueueDelivery(record));
47
- 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`, {
48
39
  messageId: record.messageId,
49
- event: runParams?.event,
40
+ event: parsedEvent,
41
+ receiveCount: record.attributes?.ApproximateReceiveCount,
50
42
  });
51
- } catch (error) {
52
- if (error.isHaltError) {
53
- // HaltError means "discard this message, don't retry".
54
- // Treat as success so SQS deletes it from the queue.
55
- // Logged explicitly — silent discards made prod debugging
56
- // extremely hard; keep this visible.
57
- 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`, {
58
49
  messageId: record.messageId,
59
- event: parsedEvent,
60
- reason: error.message,
61
- statusCode: error.statusCode,
50
+ event: runParams?.event,
62
51
  });
63
- 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 });
64
68
  }
65
- console.error(`[Worker] Failed to process record ${record.messageId}:`, error);
66
- batchItemFailures.push({ itemIdentifier: record.messageId });
67
- }
69
+ });
68
70
  }
69
71
 
70
72
  if (batchItemFailures.length > 0) {