@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.
Files changed (74) hide show
  1. package/.eslintrc.json +18 -1
  2. package/CLAUDE.md +101 -13
  3. package/README.md +19 -22
  4. package/core/CLAUDE.md +28 -13
  5. package/core/Worker.js +32 -37
  6. package/core/create-handler.js +97 -197
  7. package/core/index.js +8 -1
  8. package/core/invocation-scope.js +225 -0
  9. package/core/secrets-to-env.js +7 -6
  10. package/database/config.js +0 -7
  11. package/database/documentdb-encryption-service.js +6 -2
  12. package/database/documentdb-utils.js +2 -1
  13. package/database/encryption/README.md +25 -1
  14. package/database/encryption/encryption-schema-registry.js +3 -0
  15. package/database/encryption/integration-mapping-encryption.js +97 -0
  16. package/database/encryption/logger.js +5 -2
  17. package/database/encryption/prisma-encryption-extension.js +20 -9
  18. package/errors/fetch-error.js +48 -65
  19. package/handlers/app-definition-loader.js +38 -2
  20. package/handlers/app-handler-helpers.js +18 -4
  21. package/handlers/backend-utils.js +10 -13
  22. package/handlers/database-migration-handler.js +10 -1
  23. package/handlers/integration-event-dispatcher.js +2 -2
  24. package/handlers/routers/integration-webhook-routers.js +13 -6
  25. package/handlers/routers/websocket.js +27 -7
  26. package/handlers/workers/db-migration.js +12 -1
  27. package/handlers/workers/dlq-processor.js +64 -38
  28. package/index.js +19 -1
  29. package/integrations/integration-base.js +56 -38
  30. package/integrations/integration-router.js +21 -20
  31. package/integrations/repositories/integration-mapping-query-pipeline.js +152 -0
  32. package/integrations/repositories/integration-mapping-query.js +193 -0
  33. package/integrations/repositories/integration-mapping-repository-documentdb.js +53 -0
  34. package/integrations/repositories/integration-mapping-repository-interface.js +48 -0
  35. package/integrations/repositories/integration-mapping-repository-mongo.js +72 -0
  36. package/integrations/repositories/integration-mapping-repository-postgres.js +118 -0
  37. package/integrations/use-cases/delete-integration-for-user.js +24 -13
  38. package/jest.config.js +2 -0
  39. package/logs/.eslintrc.json +26 -1
  40. package/logs/__fixtures__/events.js +82 -0
  41. package/logs/__fixtures__/matchers.js +45 -0
  42. package/logs/__fixtures__/secrets.js +59 -0
  43. package/logs/__fixtures__/vectors.js +329 -0
  44. package/logs/__fixtures__/with-env.js +45 -0
  45. package/logs/context.js +109 -0
  46. package/logs/contract-keys.js +67 -0
  47. package/logs/debug-shims.js +46 -0
  48. package/logs/denied-keys.js +21 -0
  49. package/logs/index.js +14 -3
  50. package/logs/jest-logger-setup.js +25 -0
  51. package/logs/levels.js +71 -0
  52. package/logs/logger-runtime.js +137 -0
  53. package/logs/logger.js +134 -42
  54. package/logs/record.js +276 -0
  55. package/logs/redact.js +328 -0
  56. package/logs/serialize.js +329 -0
  57. package/logs/sinks.js +93 -0
  58. package/logs/summarize-event.js +163 -0
  59. package/modules/module.js +32 -19
  60. package/modules/requester/oauth-2.js +28 -22
  61. package/modules/requester/requester.js +69 -6
  62. package/package.json +5 -6
  63. package/queues/queue-delivery.js +41 -0
  64. package/queues/queuer-util.js +1 -15
  65. package/telemetry/bind-telemetry-context.js +4 -0
  66. package/telemetry/instrument-handler.js +19 -2
  67. package/telemetry/no-op-telemetry.js +4 -0
  68. package/telemetry/otel-telemetry.js +25 -6
  69. package/telemetry/telemetry-context.js +19 -8
  70. package/telemetry/telemetry-runtime.js +17 -0
  71. package/telemetry/telemetry-service.js +1 -0
  72. package/types/errors/index.d.ts +24 -10
  73. package/types/logs/index.d.ts +61 -2
  74. package/user/repositories/user-repository-documentdb.js +10 -42
@@ -680,7 +680,11 @@ The encryption system uses **fail-fast error handling**:
680
680
 
681
681
  ### Logging Configuration
682
682
 
683
- Configure log verbosity with `FRIGG_LOG_LEVEL`:
683
+ `FRIGG_LOG_LEVEL` is the one Frigg log level. The level rules, the record
684
+ fields and the redaction rules are in the
685
+ [Logging guide](../../../../docs/guides/LOGGING.md). The encryption logger
686
+ still writes text lines through `console` with the same level names until it
687
+ moves to the structured logger (ADR-048, incremental adoption).
684
688
 
685
689
  ```bash
686
690
  # Production (minimal logging)
@@ -814,6 +818,26 @@ export AES_KEY=$(openssl rand -hex 16) # Generate 32-char key
814
818
  ❌ Query on encrypted fields (not supported)
815
819
  ❌ Manually decrypt data (use extension)
816
820
 
821
+ The integration mapping repositories' `queryMappings()` (PostgreSQL, MongoDB
822
+ and DocumentDB) filters and sorts inside the `mapping` JSON, so it refuses to
823
+ run while field-level encryption still encrypts `IntegrationMapping.mapping` on
824
+ write. Opt the field out in the app definition, together with any nested
825
+ `mapping.*` path that `encryption.schema` encrypts; the Prisma extension and
826
+ `DocumentDBEncryptionService` both honor the opt-out:
827
+
828
+ ```javascript
829
+ encryption: {
830
+ disable: { IntegrationMapping: ['mapping'] },
831
+ }
832
+ ```
833
+
834
+ The opt-out applies to writes only. Rows written encrypted before it stay
835
+ readable, but `queryMappings()` does not match them until they are written
836
+ again. It returns the rows it matches decrypted, like
837
+ `findMappingsByIntegration()`: a nested `mapping.*` path written encrypted
838
+ before its opt-out reads plain, though conditions and ordering on that path
839
+ still see the ciphertext until the row is written again.
840
+
817
841
  ## Future Enhancements
818
842
 
819
843
  ### Planned
@@ -9,6 +9,7 @@
9
9
  */
10
10
 
11
11
  const { logger } = require('./logger');
12
+ const { registerDeniedKeys } = require('../../logs/denied-keys');
12
13
 
13
14
  /**
14
15
  * Core encryption schema (immutable - cannot be overridden by custom schemas)
@@ -120,6 +121,7 @@ function registerCustomSchema(schema) {
120
121
  }
121
122
 
122
123
  customSchema = { ...schema };
124
+ registerDeniedKeys(Object.values(schema).flatMap((config) => config.fields));
123
125
  logger.info(
124
126
  `Registered custom encryption schema for models: ${Object.keys(customSchema).join(', ')}`
125
127
  );
@@ -149,6 +151,7 @@ function extractCredentialFieldsFromModules(moduleDefinitions) {
149
151
  }
150
152
  }
151
153
 
154
+ registerDeniedKeys(fields);
152
155
  return [...new Set(fields)];
153
156
  }
154
157
 
@@ -0,0 +1,97 @@
1
+ const { getEncryptionConfig } = require('../prisma');
2
+ const { Cryptor } = require('../../encrypt/Cryptor');
3
+ const {
4
+ getFieldsToDecryptOnRead,
5
+ getFieldsToEncryptOnWrite,
6
+ loadCustomEncryptionSchema,
7
+ } = require('./encryption-schema-registry');
8
+ const {
9
+ createFieldEncryptionService,
10
+ } = require('./prisma-encryption-extension');
11
+
12
+ let plainMappingEncryption = null;
13
+
14
+ /**
15
+ * The `IntegrationMapping` fields, `mapping` itself or a nested `mapping.*`
16
+ * path, that field-level encryption encrypts on write. An empty result is
17
+ * kept for the life of the process.
18
+ *
19
+ * @returns {string[]} Empty when every mapping path is written as plain JSON
20
+ */
21
+ function getMappingFieldsEncryptedOnWrite() {
22
+ return mappingEncryption().encryptedOnWrite;
23
+ }
24
+
25
+ /**
26
+ * @throws {Error} When field-level encryption still encrypts `mapping`, or a
27
+ * nested `mapping.*` path, on write, naming the opt-out that lifts it
28
+ */
29
+ function assertMappingWrittenUnencrypted() {
30
+ const fields = getMappingFieldsEncryptedOnWrite();
31
+ if (fields.length === 0) return;
32
+
33
+ const encrypted = fields
34
+ .map((field) => `IntegrationMapping.${field}`)
35
+ .join(', ');
36
+ const optOut = fields.map((field) => `'${field}'`).join(', ');
37
+ throw new Error(
38
+ `queryMappings: field-level encryption still encrypts ${encrypted} on write, so it cannot be queried. Opt out by adding ${optOut} to appDefinition.encryption.disable.IntegrationMapping.`
39
+ );
40
+ }
41
+
42
+ /**
43
+ * Decrypts rows that `queryMappings` read around the Prisma encryption
44
+ * extension, the way reads through the extension do: every
45
+ * `IntegrationMapping` field the schema lists, opted-out paths included, so a
46
+ * path written encrypted before its opt-out comes back plain.
47
+ *
48
+ * @param {Object[]} rows - Rows whose `mapping` is a JSON object
49
+ * @returns {Promise<Object[]>} `rows` itself when encryption is off or the
50
+ * schema lists no field besides `mapping`
51
+ */
52
+ async function decryptQueriedMappings(rows) {
53
+ const { decryptor } = mappingEncryption();
54
+ if (!decryptor) return rows;
55
+ return decryptor.decryptFieldsInBulk('IntegrationMapping', rows);
56
+ }
57
+
58
+ function mappingEncryption() {
59
+ if (plainMappingEncryption) return plainMappingEncryption;
60
+
61
+ const encryption = currentMappingEncryption();
62
+ if (encryption.encryptedOnWrite.length === 0) {
63
+ plainMappingEncryption = encryption;
64
+ }
65
+ return encryption;
66
+ }
67
+
68
+ function currentMappingEncryption() {
69
+ const config = getEncryptionConfig();
70
+ if (!config.enabled) return { encryptedOnWrite: [], decryptor: null };
71
+
72
+ loadCustomEncryptionSchema();
73
+ const encryptedOnWrite = getFieldsToEncryptOnWrite(
74
+ 'IntegrationMapping'
75
+ ).filter((field) => field === 'mapping' || field.startsWith('mapping.'));
76
+ const decryptsBesidesMapping = getFieldsToDecryptOnRead(
77
+ 'IntegrationMapping'
78
+ ).some((field) => field !== 'mapping');
79
+ const decryptor = decryptsBesidesMapping
80
+ ? createFieldEncryptionService(
81
+ new Cryptor({ shouldUseAws: config.method === 'kms' })
82
+ )
83
+ : null;
84
+ return { encryptedOnWrite, decryptor };
85
+ }
86
+
87
+ /** Test helper: forget a kept result. */
88
+ function resetMappingEncryptionCheck() {
89
+ plainMappingEncryption = null;
90
+ }
91
+
92
+ module.exports = {
93
+ assertMappingWrittenUnencrypted,
94
+ decryptQueriedMappings,
95
+ getMappingFieldsEncryptedOnWrite,
96
+ resetMappingEncryptionCheck,
97
+ };
@@ -17,9 +17,12 @@ class EncryptionLogger {
17
17
  this.minLevel = this._getMinLevel();
18
18
  }
19
19
 
20
+ // FRIGG_LOG_LEVEL also takes TRACE and FATAL (ADR-048); this logger has
21
+ // no such levels, so they map to its nearest ones.
20
22
  _getMinLevel() {
21
- const level = process.env.FRIGG_LOG_LEVEL || 'INFO';
22
- return LOG_LEVELS[level.toUpperCase()] ?? LOG_LEVELS.INFO;
23
+ const level = (process.env.FRIGG_LOG_LEVEL || 'INFO').trim().toUpperCase();
24
+ const mapped = { TRACE: 'DEBUG', FATAL: 'ERROR' }[level] ?? level;
25
+ return LOG_LEVELS[mapped] ?? LOG_LEVELS.INFO;
23
26
  }
24
27
 
25
28
  _shouldLog(level) {
@@ -10,6 +10,24 @@ const {
10
10
  } = require('./encryption-schema-registry');
11
11
  const { FieldEncryptionService } = require('./field-encryption-service');
12
12
 
13
+ /**
14
+ * The FieldEncryptionService the extension runs, with field paths from the
15
+ * encryption schema registry.
16
+ *
17
+ * @param {import('../../encrypt/Cryptor').Cryptor} cryptor
18
+ * @returns {FieldEncryptionService}
19
+ */
20
+ function createFieldEncryptionService(cryptor) {
21
+ return new FieldEncryptionService({
22
+ cryptor,
23
+ schema: {
24
+ getEncryptedFields,
25
+ getFieldsToEncryptOnWrite,
26
+ getFieldsToDecryptOnRead,
27
+ },
28
+ });
29
+ }
30
+
13
31
  function createEncryptionExtension({ cryptor, enabled = true }) {
14
32
  if (!enabled) {
15
33
  return (client) => client;
@@ -21,14 +39,7 @@ function createEncryptionExtension({ cryptor, enabled = true }) {
21
39
  );
22
40
  }
23
41
 
24
- const encryptionService = new FieldEncryptionService({
25
- cryptor,
26
- schema: {
27
- getEncryptedFields,
28
- getFieldsToEncryptOnWrite,
29
- getFieldsToDecryptOnRead,
30
- },
31
- });
42
+ const encryptionService = createFieldEncryptionService(cryptor);
32
43
 
33
44
  return {
34
45
  name: 'frigg-field-encryption',
@@ -227,4 +238,4 @@ function createEncryptionExtension({ cryptor, enabled = true }) {
227
238
  };
228
239
  }
229
240
 
230
- module.exports = { createEncryptionExtension };
241
+ module.exports = { createEncryptionExtension, createFieldEncryptionService };
@@ -1,82 +1,65 @@
1
1
  const { BaseError } = require('./base-error');
2
- const { stripIndent } = require('common-tags');
3
-
4
- // TODO hide header values
2
+ const { redactUrl, scrubString } = require('../logs/redact');
5
3
 
6
4
  // Parameters names here are based on fetch. See:
7
5
  // https://developer.mozilla.org/en-US/docs/Web/API/fetch
8
6
 
9
- class FetchError extends BaseError {
10
- response = null;
11
-
12
- constructor(options = {}) {
13
- const { resource, init, response, responseBody } = options;
14
- const method = init?.method ?? 'GET';
15
- let initText = init
16
- ? init.body instanceof URLSearchParams
17
- ? (() => {
18
- init.body = init.body.toString();
19
- return JSON.stringify({ init }, null, 2);
20
- })()
21
- : JSON.stringify({ init }, null, 2)
22
- : '';
23
-
24
- let responseBodyText = '<response body is unavailable>';
25
- if (typeof responseBody === 'string') {
26
- responseBodyText = responseBody;
27
- } else if (responseBody) {
28
- responseBodyText = JSON.stringify(responseBody, null, 2);
29
- }
30
-
31
- const responseHeaders = {};
32
- if (response?.headers) {
33
- for (const [key, value] of response.headers) {
34
- responseHeaders[key] = value;
35
- }
36
- }
37
-
38
- let responseHeaderText = response
39
- ? JSON.stringify({ headers: responseHeaders }, null, 2)
40
- : '';
7
+ function resourceUrl(resource) {
8
+ if (resource === undefined || resource === null) return '';
9
+ if (typeof resource === 'object' && typeof resource.url === 'string') {
10
+ return redactUrl(resource.url);
11
+ }
12
+ return redactUrl(resource);
13
+ }
41
14
 
42
- // sanitize error reporting
43
- if (process.env.STAGE !== 'dev') {
44
- initText = false;
45
- responseHeaderText = false;
46
- responseBodyText = false;
47
- }
15
+ function causeLabel(cause) {
16
+ if (!cause || typeof cause !== 'object') return '';
17
+ const label = cause.code ?? cause.name;
18
+ return label === undefined || label === null ? '' : scrubString(String(label));
19
+ }
48
20
 
49
- const messageParts = [
50
- stripIndent`
51
- -----------------------------------------------------
52
- An error ocurred while fetching an external resource.
53
- -----------------------------------------------------
54
- >>> Request Details >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
55
- ${method} ${resource}
56
- `,
57
- initText,
58
- stripIndent`
59
- <<< Response Details <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
60
- ${response?.status} ${response?.statusText}
61
- `,
62
- responseHeaderText,
63
- responseBodyText,
64
- stripIndent`
65
- -----------------------------------------------------
66
- Stack Trace:
67
- `,
68
- ];
21
+ function buildMessage({ method, url, response, cause }) {
22
+ const outcome = response
23
+ ? response.status === undefined || response.status === null
24
+ ? ''
25
+ : String(response.status)
26
+ : causeLabel(cause);
27
+ return [method, url, outcome].filter(Boolean).join(' ');
28
+ }
69
29
 
70
- super(messageParts.filter(Boolean).join('\n'));
30
+ class FetchError extends BaseError {
31
+ constructor({ resource, init, response, cause, responseBody, body } = {}) {
32
+ const method = String(init?.method ?? 'GET').toUpperCase();
33
+ const url = resourceUrl(resource);
34
+ super(
35
+ buildMessage({ method, url, response, cause }),
36
+ cause ? { cause } : undefined
37
+ );
71
38
 
72
- this.response = response;
73
39
  this.statusCode = response?.status;
40
+ this.method = method;
41
+ this.url = url;
42
+ Object.defineProperty(this, 'response', {
43
+ value: response ?? null,
44
+ enumerable: false,
45
+ writable: true,
46
+ configurable: true,
47
+ });
48
+ Object.defineProperty(this, 'body', {
49
+ value: responseBody ?? body,
50
+ enumerable: false,
51
+ writable: true,
52
+ configurable: true,
53
+ });
74
54
  }
75
55
 
76
56
  static async create(options = {}) {
77
57
  const { response } = options;
78
- let responseBody = response?.bodyUsed ? null : await response?.text();
79
- if (!responseBody && options.body) responseBody = options.body;
58
+ let responseBody =
59
+ response && !response.bodyUsed && typeof response.text === 'function'
60
+ ? await response.text()
61
+ : null;
62
+ if (!responseBody) responseBody = options.responseBody ?? options.body;
80
63
  return new FetchError({ ...options, responseBody });
81
64
  }
82
65
  }
@@ -2,13 +2,38 @@ const { findNearestBackendPackageJson } = require('@friggframework/core/utils');
2
2
  const path = require('node:path');
3
3
  const fs = require('fs-extra');
4
4
  const { resolveTelemetryConfig } = require('../telemetry/telemetry-config');
5
+ const { registerDeniedKeys } = require('../logs/denied-keys');
6
+ const {
7
+ extractCredentialFieldsFromModules,
8
+ } = require('../database/encryption/encryption-schema-registry');
9
+ const {
10
+ getModulesDefinitionFromIntegrationClasses,
11
+ } = require('../integrations/utils/map-integration-dto');
12
+
13
+ // Handlers without a database never load the encryption registry, so the
14
+ // credential leaf keys are registered for redaction here too.
15
+ function registerCredentialLogKeys(appDefinition, integrations) {
16
+ try {
17
+ extractCredentialFieldsFromModules(
18
+ getModulesDefinitionFromIntegrationClasses(integrations)
19
+ );
20
+ } catch {
21
+ // An odd integration shape must not stop the app from loading.
22
+ }
23
+ const schema = appDefinition.encryption?.schema;
24
+ if (schema && typeof schema === 'object') {
25
+ for (const config of Object.values(schema)) {
26
+ registerDeniedKeys(config?.fields);
27
+ }
28
+ }
29
+ }
5
30
 
6
31
  /**
7
32
  * Loads the App definition from the nearest backend package
8
33
  * @function loadAppDefinition
9
34
  * @description Searches for the nearest backend package.json, loads the corresponding index.js file,
10
35
  * and extracts the application definition containing integrations and user configuration.
11
- * @returns {{integrations: Array<object>, userConfig: object | null, adminScripts: Array<object>, reports: Array<object>, admin: object, telemetry: object}} An object containing the application definition.
36
+ * @returns {{integrations: Array<object>, userConfig: object | null, adminScripts: Array<object>, reports: Array<object>, admin: object, telemetry: object, logging: object | null}} An object containing the application definition.
12
37
  * @throws {Error} Throws error if backend package.json cannot be found.
13
38
  * @throws {Error} Throws error if index.js file cannot be found in the backend directory.
14
39
  * @example
@@ -36,8 +61,11 @@ function loadAppDefinition() {
36
61
  adminScripts = [],
37
62
  reports = [],
38
63
  admin = {},
64
+ logging = null,
39
65
  } = appDefinition;
40
66
 
67
+ registerCredentialLogKeys(appDefinition, integrations);
68
+
41
69
  // Degrade consistently: an invalid telemetry block must never take down a
42
70
  // router bundle that loads the app definition at module scope (telemetry is
43
71
  // never allowed to break a handler). The singletons apply the same
@@ -59,7 +87,15 @@ function loadAppDefinition() {
59
87
  };
60
88
  }
61
89
 
62
- return { integrations, userConfig, adminScripts, reports, admin, telemetry };
90
+ return {
91
+ integrations,
92
+ userConfig,
93
+ adminScripts,
94
+ reports,
95
+ admin,
96
+ telemetry,
97
+ logging,
98
+ };
63
99
  }
64
100
 
65
101
  module.exports = {
@@ -1,10 +1,14 @@
1
- const { createHandler, flushDebugLog } = require('@friggframework/core');
1
+ const { createHandler } = require('@friggframework/core');
2
+ const { getLogger } = require('../logs');
3
+ const { summarizeExpressRequest } = require('../logs/summarize-event');
2
4
  const express = require('express');
3
5
  const bodyParser = require('body-parser');
4
6
  const cors = require('cors');
5
7
  const Boom = require('@hapi/boom');
6
8
  const serverlessHttp = require('serverless-http');
7
9
 
10
+ const log = getLogger('frigg.http');
11
+
8
12
  const createApp = (applyMiddleware) => {
9
13
  const app = express();
10
14
 
@@ -21,7 +25,7 @@ const createApp = (applyMiddleware) => {
21
25
 
22
26
  if (applyMiddleware) applyMiddleware(app);
23
27
 
24
- // Handle sending error response and logging server errors to console
28
+ // The express boundary: send the error response and log it one time.
25
29
  app.use((err, req, res, next) => {
26
30
  const boomError = err.isBoom ? err : Boom.boomify(err);
27
31
  const {
@@ -29,10 +33,20 @@ const createApp = (applyMiddleware) => {
29
33
  } = boomError;
30
34
 
31
35
  if (statusCode >= 500) {
32
- flushDebugLog(boomError);
36
+ log.error('Request failed', {
37
+ eventName: 'frigg.http.request_failed',
38
+ statusCode,
39
+ invocation: summarizeExpressRequest(req),
40
+ error: boomError,
41
+ });
33
42
  res.status(statusCode).json({ error: 'Internal Server Error' });
34
43
  } else {
35
- console.warn(`[Frigg] ${req.method} ${req.path} -> ${statusCode}: ${err.message}`);
44
+ // A client error needs no stack; the logger scrubs the reason.
45
+ log.warn('Request rejected', {
46
+ eventName: 'frigg.http.request_rejected',
47
+ statusCode,
48
+ reason: boomError.message,
49
+ });
36
50
  res.status(statusCode).json({ error: err.message });
37
51
  }
38
52
  });
@@ -1,4 +1,5 @@
1
1
  const { Router } = require('express');
2
+ const { runInContext, LOGGER_SCOPE_KEY } = require('../logs/context');
2
3
  const { Worker } = require('@friggframework/core');
3
4
  const {
4
5
  IntegrationEventDispatcher,
@@ -19,6 +20,7 @@ const {
19
20
  const {
20
21
  getModulesDefinitionFromIntegrationClasses,
21
22
  } = require('../integrations/utils/map-integration-dto');
23
+ const { processNotFound } = require('../integrations/use-cases/process-errors');
22
24
 
23
25
  const loadRouterFromObject = (IntegrationClass, routerObject) => {
24
26
  const router = Router();
@@ -37,12 +39,11 @@ const loadRouterFromObject = (IntegrationClass, routerObject) => {
37
39
  const dispatcher = new IntegrationEventDispatcher(
38
40
  integrationInstance
39
41
  );
40
- const result = await dispatcher.dispatchHttp({
41
- event,
42
- req,
43
- res,
44
- next,
45
- });
42
+ // Logs only: a route :integrationId is unauthenticated input.
43
+ const result = await runInContext(
44
+ { [LOGGER_SCOPE_KEY]: { integrationId: req.params?.integrationId } },
45
+ () => dispatcher.dispatchHttp({ event, req, res, next })
46
+ );
46
47
  res.json(result);
47
48
  } catch (error) {
48
49
  next(error);
@@ -149,7 +150,7 @@ const loadIntegrationForProcess = async (processId, integrationClass) => {
149
150
  const process = await processRepository.findById(processId);
150
151
 
151
152
  if (!process) {
152
- throw new Error(`Process not found: ${processId}`);
153
+ throw processNotFound(`Process not found: ${processId}`);
153
154
  }
154
155
 
155
156
  const instance = await getIntegrationInstance.execute(
@@ -164,7 +165,7 @@ const createQueueWorker = (integrationClass) => {
164
165
  const integrationName = integrationClass.Definition.name;
165
166
 
166
167
  class QueueWorker extends Worker {
167
- async _run(params, context) {
168
+ async _run(params, context, delivery) {
168
169
  const logCtx = {
169
170
  integration: integrationName,
170
171
  event: params.event,
@@ -254,6 +255,7 @@ const createQueueWorker = (integrationClass) => {
254
255
  event: params.event,
255
256
  data: params.data,
256
257
  context: context,
258
+ delivery,
257
259
  });
258
260
  console.log(
259
261
  `[QueueWorker] ${params.event} dispatched ok`,
@@ -273,11 +275,6 @@ const createQueueWorker = (integrationClass) => {
273
275
  return;
274
276
  }
275
277
 
276
- console.error(
277
- `Error in ${params.event} for ${integrationName}:`,
278
- error
279
- );
280
-
281
278
  // 4xx HTTP errors are permanent — the requester already
282
279
  // attempted token refresh (401) and backoff (429/5xx).
283
280
  // By the time a 4xx reaches here, retrying won't help.
@@ -24,6 +24,9 @@
24
24
 
25
25
  const { execFile } = require('child_process');
26
26
  const path = require('path');
27
+ const { getLogger } = require('../logs');
28
+
29
+ const log = getLogger('frigg.database.migration');
27
30
 
28
31
  /**
29
32
  * Execute Prisma migration command
@@ -153,7 +156,13 @@ exports.handler = async (event, context) => {
153
156
  console.log('='.repeat(60));
154
157
  console.log('Database Migration Handler');
155
158
  console.log('='.repeat(60));
156
- console.log('Event:', JSON.stringify(event, null, 2));
159
+ log.info('Database migration invoked', {
160
+ eventName: 'frigg.database.migration.invoked',
161
+ migrationId: event.migrationId,
162
+ dbType: process.env.DB_TYPE,
163
+ targetStage: process.env.STAGE,
164
+ action: event.command || 'deploy',
165
+ });
157
166
  console.log('Context:', JSON.stringify({
158
167
  functionName: context.functionName,
159
168
  functionVersion: context.functionVersion,
@@ -18,9 +18,9 @@ class IntegrationEventDispatcher {
18
18
  );
19
19
  }
20
20
 
21
- async dispatchJob({ event, data, context }) {
21
+ async dispatchJob({ event, data, context, delivery }) {
22
22
  return this._dispatch(event, (instance, handler) =>
23
- handler.call(instance, { data, context })
23
+ handler.call(instance, { data, context, delivery })
24
24
  );
25
25
  }
26
26
 
@@ -1,6 +1,7 @@
1
1
  const { createAppHandler } = require('./../app-handler-helpers');
2
2
  const { loadAppDefinition } = require('../app-definition-loader');
3
3
  const { Router } = require('express');
4
+ const { runInContext, LOGGER_SCOPE_KEY } = require('../../logs/context');
4
5
  const {
5
6
  IntegrationEventDispatcher,
6
7
  } = require('../integration-event-dispatcher');
@@ -55,12 +56,18 @@ for (const IntegrationClass of integrationClasses) {
55
56
  const dispatcher = new IntegrationEventDispatcher(
56
57
  integrationInstance
57
58
  );
58
- await dispatcher.dispatchHttp({
59
- event: 'WEBHOOK_RECEIVED',
60
- req,
61
- res,
62
- next,
63
- });
59
+ // Logs only: the id comes from an unauthenticated URL, so it must
60
+ // not reach the telemetry context that drives usage attribution.
61
+ await runInContext(
62
+ { [LOGGER_SCOPE_KEY]: { integrationId: req.params.integrationId } },
63
+ () =>
64
+ dispatcher.dispatchHttp({
65
+ event: 'WEBHOOK_RECEIVED',
66
+ req,
67
+ res,
68
+ next,
69
+ })
70
+ );
64
71
  } catch (error) {
65
72
  next(error);
66
73
  }