@friggframework/core 2.0.0--canary.654.3cd4278.0 → 2.0.0--canary.651.dadf4d6.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 (68) hide show
  1. package/.eslintrc.json +1 -18
  2. package/CLAUDE.md +62 -14
  3. package/README.md +22 -19
  4. package/core/CLAUDE.md +12 -25
  5. package/core/Worker.js +37 -31
  6. package/core/create-handler.js +197 -96
  7. package/core/index.js +1 -8
  8. package/core/secrets-to-env.js +6 -7
  9. package/database/config.js +7 -0
  10. package/database/documentdb-encryption-service.js +6 -2
  11. package/database/documentdb-utils.js +2 -1
  12. package/database/encryption/README.md +21 -5
  13. package/database/encryption/encryption-schema-registry.js +0 -3
  14. package/database/encryption/integration-mapping-encryption.js +97 -0
  15. package/database/encryption/prisma-encryption-extension.js +20 -9
  16. package/errors/fetch-error.js +65 -48
  17. package/handlers/app-definition-loader.js +2 -53
  18. package/handlers/app-handler-helpers.js +4 -18
  19. package/handlers/backend-utils.js +5 -0
  20. package/handlers/database-migration-handler.js +1 -10
  21. package/handlers/routers/websocket.js +6 -26
  22. package/handlers/workers/db-migration.js +1 -12
  23. package/handlers/workers/dlq-processor.js +38 -64
  24. package/index.js +1 -19
  25. package/integrations/integration-base.js +38 -56
  26. package/integrations/integration-router.js +20 -21
  27. package/integrations/repositories/integration-mapping-query-pipeline.js +152 -0
  28. package/integrations/repositories/integration-mapping-query.js +193 -0
  29. package/integrations/repositories/integration-mapping-repository-documentdb.js +53 -0
  30. package/integrations/repositories/integration-mapping-repository-interface.js +48 -0
  31. package/integrations/repositories/integration-mapping-repository-mongo.js +72 -0
  32. package/integrations/repositories/integration-mapping-repository-postgres.js +118 -0
  33. package/integrations/use-cases/delete-integration-for-user.js +13 -24
  34. package/jest.config.js +0 -2
  35. package/logs/.eslintrc.json +1 -26
  36. package/logs/index.js +3 -14
  37. package/logs/logger.js +42 -132
  38. package/modules/module.js +19 -32
  39. package/modules/requester/oauth-2.js +22 -28
  40. package/modules/requester/requester.js +6 -69
  41. package/package.json +6 -5
  42. package/queues/queuer-util.js +15 -1
  43. package/telemetry/bind-telemetry-context.js +0 -4
  44. package/telemetry/instrument-handler.js +2 -19
  45. package/telemetry/no-op-telemetry.js +0 -4
  46. package/telemetry/otel-telemetry.js +6 -25
  47. package/telemetry/telemetry-context.js +8 -19
  48. package/telemetry/telemetry-runtime.js +0 -17
  49. package/telemetry/telemetry-service.js +0 -1
  50. package/types/errors/index.d.ts +10 -24
  51. package/types/logs/index.d.ts +2 -61
  52. package/user/repositories/user-repository-documentdb.js +40 -8
  53. package/core/invocation-scope.js +0 -224
  54. package/logs/__fixtures__/events.js +0 -82
  55. package/logs/__fixtures__/matchers.js +0 -45
  56. package/logs/__fixtures__/secrets.js +0 -59
  57. package/logs/__fixtures__/vectors.js +0 -329
  58. package/logs/__fixtures__/with-env.js +0 -45
  59. package/logs/context.js +0 -92
  60. package/logs/debug-shims.js +0 -46
  61. package/logs/jest-logger-setup.js +0 -25
  62. package/logs/levels.js +0 -71
  63. package/logs/logger-runtime.js +0 -142
  64. package/logs/record.js +0 -231
  65. package/logs/redact.js +0 -333
  66. package/logs/serialize.js +0 -322
  67. package/logs/sinks.js +0 -99
  68. package/logs/summarize-event.js +0 -162
package/.eslintrc.json CHANGED
@@ -1,20 +1,3 @@
1
1
  {
2
- "extends": "@friggframework/eslint-config",
3
- "rules": {
4
- "no-restricted-syntax": [
5
- "warn",
6
- {
7
- "selector": "CallExpression[callee.object.object.name='process'][callee.object.property.name=/^(stdout|stderr)$/][callee.property.name='write']",
8
- "message": "Write through the logger; only logs/sinks.js writes to fd 1 (ADR-048 §14)"
9
- }
10
- ]
11
- },
12
- "overrides": [
13
- {
14
- "files": ["*.test.js", "database/utils/prisma-runner.js"],
15
- "rules": {
16
- "no-restricted-syntax": "off"
17
- }
18
- }
19
- ]
2
+ "extends": "@friggframework/eslint-config"
20
3
  }
package/CLAUDE.md CHANGED
@@ -211,7 +211,56 @@ packages/core/
211
211
  - `integration-repository-factory.js` - Creates database-specific repositories
212
212
  - `integration-repository-mongo.js` - MongoDB implementation
213
213
  - `integration-repository-postgres.js` - PostgreSQL implementation
214
- - `integration-mapping-repository-*.js` - Mapping data persistence
214
+ - `integration-mapping-repository-*.js` - Mapping data persistence.
215
+ `queryMappings(integrationId, { where, orderBy, skip, take, omit })`
216
+ returns `{ mappings, total }`: one filtered, ordered page of an
217
+ integration's mappings, for callers that cannot load every row through
218
+ `findMappingsByIntegration`. `where` is an array of ANDed conditions
219
+ (at most 20); an entry may be `{ anyOf: [...] }`, ORed, one level deep.
220
+ Conditions are `{ path: 'mapping.<segment>...', op: 'exists' | 'notExists' }`
221
+ (JSON null counts as absent), `{ path: 'mapping.…', op: 'in', value: string[] }`
222
+ (1–500 strings) and `{ path: 'sourceId', op: 'notStartsWith', value }`
223
+ (a NULL sourceId matches). `orderBy` is `{ path: 'mapping.…', direction:
224
+ 'asc' | 'desc' }`, nulls last, ties broken by id in the same direction;
225
+ without it rows come in id order. `take` is 1–500. `omit` lists top-level
226
+ mapping keys to leave out of the rows; never write such rows back. Path
227
+ segments must match `^[A-Za-z_][A-Za-z0-9_]*$`. The PostgreSQL, MongoDB and
228
+ DocumentDB adapters give the same pages and totals;
229
+ `integration-mapping-repository-query-parity.test.js` checks that against
230
+ real databases when `QUERY_MAPPINGS_PARITY_MONGO_URL` /
231
+ `QUERY_MAPPINGS_PARITY_POSTGRES_URL` are set. Two orderings still differ:
232
+ strings compare by the database collation on PostgreSQL and by code point
233
+ on MongoDB and DocumentDB, and arrays or objects at the sort path order
234
+ among themselves only on PostgreSQL (the other adapters fall back to id).
235
+ The legacy `IntegrationMappingRepository` inherits the port's
236
+ "not supported by this database adapter yet" error. Every adapter refuses
237
+ to run while field-level encryption still encrypts
238
+ `IntegrationMapping.mapping` on write (see `database/encryption/README.md`),
239
+ and returns rows decrypted like `findMappingsByIntegration`. PostgreSQL and
240
+ MongoDB decrypt through `decryptQueriedMappings`
241
+ (`database/encryption/integration-mapping-encryption.js`), a no-op while
242
+ encryption is off or lists no `IntegrationMapping` field besides `mapping`.
243
+ Validation lives in `integration-mapping-query.js`; a new operator is one
244
+ entry in its `OPERATORS` table, one in the Postgres adapter's
245
+ `CONDITION_SQL` and one in `CONDITION_EXPRESSIONS` in
246
+ `integration-mapping-query-pipeline.js`, the aggregation stages both
247
+ MongoDB-protocol adapters share. Those stages may only use what Amazon
248
+ DocumentDB 4.0 and 5.0 support (no `$facet`, `$getField`, `$set` or
249
+ `$unset`).
250
+ **Cost**: PostgreSQL answers in one SQL statement, which reads every row
251
+ of the integration and evaluates the JSON paths per row, because no JSON
252
+ index exists. With ~4 KB mappings on PostgreSQL 16 that is about 0.3 s per
253
+ call at 10⁴ rows per integration and 2.5–3 s at 10⁵. MongoDB answers in one
254
+ aggregate: the `integrationId` index narrows `$match` to the integration,
255
+ the `$expr` is then evaluated per document, and `$facet` returns the page
256
+ with its `$count`, so a page (after `omit`) must fit the 16 MB document
257
+ limit. DocumentDB has no `$facet`: the page and the count are two
258
+ concurrent aggregates, so the total does not come from the same snapshot
259
+ as the page. Both sort in memory on a computed key (`allowDiskUse`). With
260
+ ~4 KB mappings on MongoDB 7 that is about 0.05–0.1 s per call at 10⁴ rows
261
+ per integration and 0.5–0.7 s at 10⁵ (the DocumentDB pipeline, run on
262
+ MongoDB, 0.6–0.8 s). On every adapter a deep offset or an empty page past
263
+ the end costs about the same as the first page.
215
264
  - `process-repository-*.js` - Process (long-running job) persistence.
216
265
  Implements `applyProcessUpdate(processId, ops)` — a race-safe alternative
217
266
  to `update(id, patch)` that routes increments, sets, and bounded-array
@@ -437,24 +486,22 @@ if (!userId) {
437
486
 
438
487
  ### 9. Logging System (`/logs`)
439
488
 
440
- **Purpose**: One redacted JSON record per line to stdout (ADR-048). See `docs/guides/LOGGING.md`.
489
+ **Purpose**: Structured logging with debug capabilities.
441
490
 
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.
491
+ **Functions**:
492
+ - `debug(message, data)` - Debug logging
493
+ - `initDebugLog(eventName, event)` - Initialize debug context
494
+ - `flushDebugLog(error)` - Flush logs on error
450
495
 
451
496
  **Usage**:
452
497
 
453
498
  ```javascript
454
- const { getLogger } = require('../logs');
455
- const log = getLogger('frigg.core.sync');
499
+ const { debug, initDebugLog, flushDebugLog } = require('@friggframework/core');
456
500
 
457
- log.warn('Sync skipped', { eventName: 'frigg.core.sync.skipped', processId });
501
+ initDebugLog('MyIntegration', event);
502
+ debug('Processing request', { userId, action });
503
+ // ... your code ...
504
+ flushDebugLog(); // On error
458
505
  ```
459
506
 
460
507
  ### 10. Lambda Utilities (`/lambda`)
@@ -662,7 +709,8 @@ Use test doubles from `@friggframework/test` package for consistent mocking.
662
709
  ### Optional
663
710
 
664
711
  - `SECRET_ARN` - AWS Secrets Manager ARN for auto-injection
665
- - `FRIGG_LOG_LEVEL` - Minimum log level (`TRACE` … `FATAL`, default `INFO`; `DEBUG` on local runs)
712
+ - `DEBUG` - Debug logging pattern
713
+ - `LOG_LEVEL` - Logging level (debug, info, warn, error)
666
714
 
667
715
  ## Version Information
668
716
 
package/README.md CHANGED
@@ -107,6 +107,10 @@ 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
110
114
  ```
111
115
 
112
116
  ## Core Components
@@ -251,12 +255,10 @@ const {
251
255
  // Custom business logic error
252
256
  throw new RequiredPropertyError('userId is required');
253
257
 
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,
258
+ // API communication error
259
+ throw new FetchError('Failed to fetch data from external API', {
260
+ statusCode: 404,
261
+ response: errorResponse
260
262
  });
261
263
 
262
264
  // Base error with custom properties
@@ -268,25 +270,25 @@ throw new BaseError('Integration failed', {
268
270
 
269
271
  ### 6. Logging (`/logs`)
270
272
 
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).
273
+ Structured logging with debug capabilities.
273
274
 
274
275
  **Usage:**
275
276
  ```javascript
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,
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
280
286
  });
281
287
 
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 });
288
+ // Flush logs (useful in serverless environments)
289
+ await flushDebugLog();
286
290
  ```
287
291
 
288
- `debug`, `initDebugLog` and `flushDebugLog` are deprecated shims.
289
-
290
292
  ### 7. User Management (`/user`)
291
293
 
292
294
  Comprehensive user authentication and authorization system supporting both individual and organizational users.
@@ -1011,7 +1013,8 @@ const {
1011
1013
  | `MONGO_URI` | Yes | MongoDB connection string |
1012
1014
  | `FRIGG_ENCRYPTION_KEY` | Yes | 256-bit encryption key |
1013
1015
  | `AWS_REGION` | No | AWS region for services |
1014
- | `FRIGG_LOG_LEVEL` | No | Minimum log level: `TRACE`, `DEBUG`, `INFO` (default), `WARN`, `ERROR`, `FATAL`. Local runs default to `DEBUG` |
1016
+ | `DEBUG` | No | Debug logging pattern |
1017
+ | `LOG_LEVEL` | No | Logging level (debug, info, warn, error) |
1015
1018
 
1016
1019
  ## License
1017
1020
 
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
- - **Invocation Scope**: `runInvocationScope` puts `requestId`, `route` and the redacted event summary on every record, then flushes usage, telemetry and sinks
24
+ - **Debug Logging**: Request/response logging with structured debug info
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', // Logged as handlerName
30
+ eventName: 'MyIntegration', // For logging/debugging
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,8 +129,7 @@ class MyIntegration extends Delegate {
129
129
  ### Lambda Handler Lifecycle
130
130
  1. **Pre-Execution Setup**:
131
131
  ```javascript
132
- runInvocationScope({ requestId, handlerName, method, route, invocation }, …); // Logger scope for the invocation
133
- log.info('Handler invoked', { eventName: 'frigg.handler.invoked' });
132
+ initDebugLog(eventName, event); // Debug logging setup
134
133
  await secretsToEnv(); // Secrets Manager injection
135
134
  await parametersToEnv(); // SSM Parameter Store fetch (only when SSM_PARAMETER_PREFIX + FRIGG_SSM_OFFLOADED_KEYS are set)
136
135
  context.callbackWaitsForEmptyEventLoop = false; // Connection pooling
@@ -150,20 +149,12 @@ class MyIntegration extends Delegate {
150
149
 
151
150
  4. **Error Handling & Cleanup**:
152
151
  ```javascript
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).
152
+ flushDebugLog(error); // Debug info flush on error
153
+ // Sanitized error response for user-facing endpoints
163
154
  ```
164
155
 
165
156
  ### SQS Job Processing Lifecycle
166
- 1. **Batch Processing**: Process all records in `event.Records` sequentially, each inside `runMessageScope` (adds `messageId`, `receiveCount`, `processId`, `integrationId`, `integrationEvent` to its records)
157
+ 1. **Batch Processing**: Process all records in `event.Records` sequentially
167
158
  2. **Message Parsing**: JSON.parse message body for parameters
168
159
  3. **Validation**: Run custom validation on parsed parameters
169
160
  4. **Execution**: Call `_run()` method with validated parameters
@@ -214,9 +205,7 @@ const handler = createHandler({
214
205
  - Logs full error details internally
215
206
 
216
207
  2. **Server-to-Server Errors**: `isUserFacingResponse: false`
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
208
+ - Re-throws original error for AWS handling
220
209
  - Used for SQS, SNS, and internal API calls
221
210
  - Enables proper retry mechanisms
222
211
 
@@ -225,14 +214,12 @@ const handler = createHandler({
225
214
  - Prevents infinite retries for known issues
226
215
  - Used for graceful degradation scenarios
227
216
 
228
- ### Logging Strategy
217
+ ### Debug Logging Strategy
229
218
  ```javascript
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.
219
+ initDebugLog(eventName, event); // Start logging context
220
+ // ... your code ...
221
+ flushDebugLog(error); // Flush on error (includes full context)
234
222
  ```
235
- See `docs/guides/LOGGING.md`.
236
223
 
237
224
  ## Integration Development Patterns
238
225
 
@@ -659,7 +646,7 @@ describe('Health Handler', () => {
659
646
  const { createHandler } = require('@friggframework/core/core');
660
647
 
661
648
  const testHandler = createHandler({
662
- isUserFacingResponse: false, // Rethrows a sanitized surrogate (name, message, statusCode, code; not the original instance)
649
+ isUserFacingResponse: false, // Get full errors in tests
663
650
  shouldUseDatabase: false, // Mock/skip DB in tests
664
651
  method: yourTestMethod
665
652
  });
package/core/Worker.js CHANGED
@@ -2,11 +2,8 @@ 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');
6
- const { getLogger } = require('../logs');
7
5
 
8
6
  const sqs = new SQSClient({ region: process.env.AWS_REGION });
9
- const log = getLogger('frigg.worker');
10
7
 
11
8
  class Worker {
12
9
  async getQueueURL(params) {
@@ -28,36 +25,45 @@ class Worker {
28
25
  );
29
26
 
30
27
  for (const record of records) {
31
- await runMessageScope(record, async () => {
32
- // messageId, receiveCount and the event come from the scope.
33
- log.debug('Record started', { eventName: 'frigg.worker.record_started' });
34
-
35
- try {
36
- const runParams = JSON.parse(record.body);
37
- this._validateParams(runParams);
38
- await this._run(runParams, context);
39
- log.debug('Record succeeded', { eventName: 'frigg.worker.record_succeeded' });
40
- } catch (error) {
41
- if (error.isHaltError) {
42
- // HaltError means "discard this message, don't retry".
43
- // Treat as success so SQS deletes it from the queue.
44
- // Logged explicitly — silent discards made prod debugging
45
- // extremely hard; keep this visible.
46
- log.error('Record halted (discarded, no retry)', {
47
- eventName: 'frigg.worker.record_halted',
48
- statusCode: error.statusCode,
49
- error,
50
- });
51
- return;
52
- }
53
- // The message goes back to SQS, so WARN (ADR-048 §4).
54
- log.warn('Record failed, returned for retry', {
55
- eventName: 'frigg.worker.record_failed',
56
- error,
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`, {
47
+ messageId: record.messageId,
48
+ event: runParams?.event,
49
+ });
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)`, {
57
+ messageId: record.messageId,
58
+ event: parsedEvent,
59
+ reason: error.message,
60
+ statusCode: error.statusCode,
57
61
  });
58
- batchItemFailures.push({ itemIdentifier: record.messageId });
62
+ continue;
59
63
  }
60
- });
64
+ console.error(`[Worker] Failed to process record ${record.messageId}:`, error);
65
+ batchItemFailures.push({ itemIdentifier: record.messageId });
66
+ }
61
67
  }
62
68
 
63
69
  if (batchItemFailures.length > 0) {