@friggframework/core 2.0.0--canary.654.be44e66.0 → 2.0.0--canary.654.ebad223.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.
package/.eslintrc.json CHANGED
@@ -1,3 +1,20 @@
1
1
  {
2
- "extends": "@friggframework/eslint-config"
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
+ ]
3
20
  }
package/core/Worker.js CHANGED
@@ -3,8 +3,10 @@ const _ = require('lodash');
3
3
  const { RequiredPropertyError } = require('../errors');
4
4
  const { get } = require('../assertions');
5
5
  const { runMessageScope } = require('./invocation-scope');
6
+ const { getLogger } = require('../logs');
6
7
 
7
8
  const sqs = new SQSClient({ region: process.env.AWS_REGION });
9
+ const log = getLogger('frigg.worker');
8
10
 
9
11
  class Worker {
10
12
  async getQueueURL(params) {
@@ -27,43 +29,32 @@ class Worker {
27
29
 
28
30
  for (const record of records) {
29
31
  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`, {
39
- messageId: record.messageId,
40
- event: parsedEvent,
41
- receiveCount: record.attributes?.ApproximateReceiveCount,
42
- });
32
+ // messageId, receiveCount and the event come from the scope.
33
+ log.debug('Record started', { eventName: 'frigg.worker.record_started' });
43
34
 
44
35
  try {
45
36
  const runParams = JSON.parse(record.body);
46
37
  this._validateParams(runParams);
47
38
  await this._run(runParams, context);
48
- console.log(`[Worker] record success`, {
49
- messageId: record.messageId,
50
- event: runParams?.event,
51
- });
39
+ log.debug('Record succeeded', { eventName: 'frigg.worker.record_succeeded' });
52
40
  } catch (error) {
53
41
  if (error.isHaltError) {
54
42
  // HaltError means "discard this message, don't retry".
55
43
  // Treat as success so SQS deletes it from the queue.
56
44
  // Logged explicitly — silent discards made prod debugging
57
45
  // 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,
46
+ log.error('Record halted (discarded, no retry)', {
47
+ eventName: 'frigg.worker.record_halted',
62
48
  statusCode: error.statusCode,
49
+ error,
63
50
  });
64
51
  return;
65
52
  }
66
- console.error(`[Worker] Failed to process record ${record.messageId}:`, error);
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,
57
+ });
67
58
  batchItemFailures.push({ itemIdentifier: record.messageId });
68
59
  }
69
60
  });
@@ -6,6 +6,7 @@ const { getLogger, toSanitizedSurrogate } = require('../logs');
6
6
  const {
7
7
  summarizeLambdaEvent,
8
8
  toScopeInvocation,
9
+ toRequestInvocation,
9
10
  } = require('../logs/summarize-event');
10
11
  const {
11
12
  runInvocationScope,
@@ -57,6 +58,8 @@ const createHandler = (optionByName = {}) => {
57
58
  routeKey: eventSummary.routeKey,
58
59
  invocation: toScopeInvocation(eventSummary),
59
60
  };
61
+ // The full redacted summary goes only on the entry and failure records.
62
+ const requestInvocation = toRequestInvocation(eventSummary);
60
63
 
61
64
  return runInvocationScope(
62
65
  scopeFields,
@@ -64,6 +67,7 @@ const createHandler = (optionByName = {}) => {
64
67
  try {
65
68
  log.info('Handler invoked', {
66
69
  eventName: 'frigg.handler.invoked',
70
+ invocation: requestInvocation,
67
71
  });
68
72
 
69
73
  // If enabled (i.e. if SECRET_ARN is set in process.env) Fetch secrets from AWS Secrets Manager, and set them as environment variables.
@@ -84,45 +88,28 @@ const createHandler = (optionByName = {}) => {
84
88
  // Run the Lambda
85
89
  return await method(event, context);
86
90
  } 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',
91
+ // Allow client-safe errors to pass through with their actual message
92
+ if (isUserFacingResponse && error.isClientSafe === true) {
93
+ const statusCode = error.statusCode || 400;
94
+ log.warn('Request rejected', {
95
+ eventName: 'frigg.handler.rejected',
96
+ statusCode,
107
97
  error,
108
98
  });
109
-
110
- // Hide other errors with generic message
111
99
  return {
112
- statusCode: 500,
100
+ statusCode,
113
101
  body: JSON.stringify({
114
- error: 'An Internal Error Occurred',
102
+ error: error.message,
115
103
  }),
116
104
  };
117
105
  }
118
106
 
119
- // Handle server-to-server responses.
120
-
121
- // Halt errors succeed and won't be retried, so the halt
122
- // itself is the failure record.
123
- if (error.isHaltError === true) {
107
+ // Server-to-server: halt errors succeed and won't be
108
+ // retried, so the halt itself is the failure record.
109
+ if (!isUserFacingResponse && error.isHaltError === true) {
124
110
  log.error('Handler halted', {
125
111
  eventName: 'frigg.handler.halted',
112
+ invocation: requestInvocation,
126
113
  error,
127
114
  });
128
115
  return;
@@ -130,8 +117,20 @@ const createHandler = (optionByName = {}) => {
130
117
 
131
118
  log.error('Handler failed', {
132
119
  eventName: 'frigg.handler.failed',
120
+ invocation: requestInvocation,
133
121
  error,
134
122
  });
123
+
124
+ // Don't leak implementation details to end users.
125
+ if (isUserFacingResponse) {
126
+ return {
127
+ statusCode: 500,
128
+ body: JSON.stringify({
129
+ error: 'An Internal Error Occurred',
130
+ }),
131
+ };
132
+ }
133
+
135
134
  // The Lambda runtime writes a rethrown error itself, so
136
135
  // rethrow a sanitized copy (ADR-048 §6 item 11).
137
136
  throw toSanitizedSurrogate(error);
@@ -3,6 +3,7 @@ const path = require('node:path');
3
3
  const fs = require('fs-extra');
4
4
  const { resolveTelemetryConfig } = require('../telemetry/telemetry-config');
5
5
  const { addDeniedKeys } = require('../logs/redact');
6
+ const { getLogger } = require('../logs');
6
7
  const {
7
8
  extractCredentialFieldsFromModules,
8
9
  } = require('../database/encryption/encryption-schema-registry');
@@ -12,18 +13,32 @@ const {
12
13
 
13
14
  // Handlers without a database never load the encryption registry, so the
14
15
  // credential leaf keys are registered for redaction here too.
16
+ const warnedIgnoredKeys = new Set();
17
+
18
+ function warnIgnoredKeys(ignored) {
19
+ for (const key of ignored) {
20
+ if (warnedIgnoredKeys.has(key)) continue;
21
+ warnedIgnoredKeys.add(key);
22
+ getLogger('frigg.logger').warn(
23
+ 'Credential field name is a record field; it stays visible in logs',
24
+ { eventName: 'frigg.logger.denied_key_ignored', key }
25
+ );
26
+ }
27
+ }
28
+
15
29
  function registerCredentialLogKeys(appDefinition, integrations) {
16
30
  try {
17
- extractCredentialFieldsFromModules(
31
+ const fields = extractCredentialFieldsFromModules(
18
32
  getModulesDefinitionFromIntegrationClasses(integrations)
19
33
  );
34
+ warnIgnoredKeys(addDeniedKeys(fields));
20
35
  } catch {
21
36
  // An odd integration shape must not stop the app from loading.
22
37
  }
23
38
  const schema = appDefinition.encryption?.schema;
24
39
  if (schema && typeof schema === 'object') {
25
40
  for (const config of Object.values(schema)) {
26
- addDeniedKeys(config?.fields);
41
+ warnIgnoredKeys(addDeniedKeys(config?.fields));
27
42
  }
28
43
  }
29
44
  }
@@ -1,5 +1,6 @@
1
1
  const { createHandler } = require('@friggframework/core');
2
2
  const { getLogger } = require('../logs');
3
+ const { summarizeExpressRequest } = require('../logs/summarize-event');
3
4
  const express = require('express');
4
5
  const bodyParser = require('body-parser');
5
6
  const cors = require('cors');
@@ -35,14 +36,16 @@ const createApp = (applyMiddleware) => {
35
36
  log.error('Request failed', {
36
37
  eventName: 'frigg.http.request_failed',
37
38
  statusCode,
39
+ invocation: summarizeExpressRequest(req),
38
40
  error: boomError,
39
41
  });
40
42
  res.status(statusCode).json({ error: 'Internal Server Error' });
41
43
  } else {
44
+ // A client error needs no stack; the logger scrubs the reason.
42
45
  log.warn('Request rejected', {
43
46
  eventName: 'frigg.http.request_rejected',
44
47
  statusCode,
45
- error: boomError,
48
+ reason: boomError.message,
46
49
  });
47
50
  res.status(statusCode).json({ error: err.message });
48
51
  }
@@ -273,11 +273,6 @@ const createQueueWorker = (integrationClass) => {
273
273
  return;
274
274
  }
275
275
 
276
- console.error(
277
- `Error in ${params.event} for ${integrationName}:`,
278
- error
279
- );
280
-
281
276
  // 4xx HTTP errors are permanent — the requester already
282
277
  // attempted token refresh (401) and backoff (429/5xx).
283
278
  // By the time a 4xx reaches here, retrying won't help.
@@ -27,24 +27,22 @@ function extractQueueName(eventSourceARN) {
27
27
  // The body can hold credentials, so only its size and digest are logged.
28
28
  function describeBody(body) {
29
29
  const text = typeof body === 'string' ? body : String(body ?? '');
30
+ let parsed = true;
31
+ try {
32
+ JSON.parse(text);
33
+ } catch {
34
+ parsed = false;
35
+ }
30
36
  return {
37
+ parsed,
31
38
  bodyLength: Buffer.byteLength(text),
32
39
  bodySha256: crypto.createHash('sha256').update(text).digest('hex'),
33
40
  };
34
41
  }
35
42
 
36
- function isJson(body) {
37
- try {
38
- JSON.parse(body);
39
- return true;
40
- } catch {
41
- return false;
42
- }
43
- }
44
-
45
43
  function logRecord(record) {
46
- const body = describeBody(record.body);
47
- if (!isJson(record.body)) {
44
+ const { parsed, ...body } = describeBody(record.body);
45
+ if (!parsed) {
48
46
  log.warn('DLQ message body is not JSON', {
49
47
  eventName: 'frigg.queue.dlq.body_unparsed',
50
48
  ...body,
@@ -778,7 +778,7 @@ class IntegrationBase {
778
778
  if (this.events[eventName]) {
779
779
  if (typeof handlers[eventName] === 'string') {
780
780
  this.logger.warn(
781
- `Binding handler "${handlers[eventName]}" for event "${eventName}" is ignored because this.events["${eventName}"] was already set`,
781
+ 'Binding handler is ignored: the event is already set',
782
782
  {
783
783
  eventName: `${this.logger.name}.extension_handler_shadowed`,
784
784
  bindingName,
@@ -7,6 +7,16 @@ const { summarizeLambdaEvent } = require('../summarize-event');
7
7
  const secretUrl = `https://api.example.com/v1/items?api_key=${S.apiKeyQuery}`;
8
8
  const SIGNATURE_VALUE = 'fakeSgnValue2Gh9Tc5Wp7LmQx3';
9
9
 
10
+ // Built by concatenation so push protection does not match the prefixes.
11
+ const TOKEN_TAIL = 'ZqPr3fixTail8Hn2Kd8Ws1Yc6';
12
+ const PREFIXED_TOKENS = [
13
+ 'sk_' + 'live_' + TOKEN_TAIL,
14
+ 'whsec' + '_' + TOKEN_TAIL,
15
+ 'xox' + 'b-' + TOKEN_TAIL,
16
+ 'gh' + 'p_' + TOKEN_TAIL,
17
+ 'AK' + 'IA' + 'ZQPR3FIXTAIL8HN2',
18
+ ];
19
+
10
20
  function errorWithMessage(message, name = 'Error') {
11
21
  const error = new Error(message);
12
22
  error.name = name;
@@ -188,6 +198,13 @@ function vectors() {
188
198
  fields: () => ({ note: `blob ${S.base64Run} end` }),
189
199
  secrets: [S.base64Run],
190
200
  },
201
+ {
202
+ name: 'provider token prefixes',
203
+ level: 'warn',
204
+ message: () => `provider rejected ${PREFIXED_TOKENS.join(' and ')}`,
205
+ fields: () => ({ note: PREFIXED_TOKENS.join(',') }),
206
+ secrets: PREFIXED_TOKENS,
207
+ },
191
208
  {
192
209
  name: '40-hex token',
193
210
  level: 'error',
@@ -1,7 +1,10 @@
1
1
  const util = require('node:util');
2
2
  const { getLogger } = require('./logger');
3
3
  const { getContext } = require('./context');
4
- const { summarizeLambdaEvent, toScopeInvocation } = require('./summarize-event');
4
+ const {
5
+ summarizeLambdaEvent,
6
+ toRequestInvocation,
7
+ } = require('./summarize-event');
5
8
 
6
9
  // Deprecated. Kept for one major version; use getLogger or this.logger.
7
10
  const LEGACY_LOGGER_NAME = 'frigg.legacy';
@@ -29,7 +32,7 @@ function initDebugLog(...initMessages) {
29
32
  if (!log.isLevelEnabled('DEBUG')) return;
30
33
  const event = initMessages.find((m) => m && typeof m === 'object');
31
34
  log.debug('Debug log initialized', {
32
- invocation: toScopeInvocation(summarizeLambdaEvent(event)),
35
+ invocation: toRequestInvocation(summarizeLambdaEvent(event)),
33
36
  });
34
37
  }
35
38
 
package/logs/record.js CHANGED
@@ -78,6 +78,29 @@ function buildMessage(message, callSite) {
78
78
  return '[non-string message]';
79
79
  }
80
80
 
81
+ const INVOCATION_KEY = 'invocation';
82
+
83
+ function isPlainObject(value) {
84
+ return (
85
+ value !== null &&
86
+ typeof value === 'object' &&
87
+ Object.getPrototypeOf(value) === Object.prototype
88
+ );
89
+ }
90
+
91
+ // A boundary adds request detail to the scope's invocation; scope keys win.
92
+ function extendInvocation(scoped, detail, dropped) {
93
+ const merged = { ...scoped };
94
+ for (const [key, value] of Object.entries(detail)) {
95
+ if (!Object.prototype.hasOwnProperty.call(scoped, key)) {
96
+ merged[key] = value;
97
+ } else if (JSON.stringify(scoped[key]) !== JSON.stringify(value)) {
98
+ dropped.add(`${INVOCATION_KEY}.${key}`);
99
+ }
100
+ }
101
+ return merged;
102
+ }
103
+
81
104
  function traceFields(spanContext) {
82
105
  if (
83
106
  !spanContext ||
@@ -135,7 +158,8 @@ function buildRecord({
135
158
  OWNED_KEYS.has(key) ||
136
159
  RESERVED_KEYS.has(key) ||
137
160
  (dropPayloads && PAYLOAD_KEYS.has(key)) ||
138
- Object.prototype.hasOwnProperty.call(record, key)
161
+ (Object.prototype.hasOwnProperty.call(record, key) &&
162
+ !canExtendInvocation(key, value))
139
163
  ) {
140
164
  dropped.add(key);
141
165
  continue;
@@ -149,10 +173,18 @@ function buildRecord({
149
173
  const value =
150
174
  key === 'error' ? serializeError(accepted[key]) : safe?.[key];
151
175
  if (value === undefined || value === null) continue;
176
+ if (key === INVOCATION_KEY && isPlainObject(record[key])) {
177
+ record[key] = extendInvocation(record[key], value, dropped);
178
+ continue;
179
+ }
152
180
  record[key] = value;
153
181
  if (isCallSite) callSiteKeys.push(key);
154
182
  }
155
183
  };
184
+ const canExtendInvocation = (key, value) =>
185
+ key === INVOCATION_KEY &&
186
+ isPlainObject(record[key]) &&
187
+ isPlainObject(value);
156
188
  place(readFields(scope), false);
157
189
  place(readFields(bindings), false);
158
190
  place(callSite, true);
package/logs/redact.js CHANGED
@@ -14,8 +14,8 @@ const DENIED_SUFFIXES = [
14
14
  ];
15
15
 
16
16
  // Core encryption registry leaves and exact names the suffix rule does not
17
- // cover. `header` is Node's raw `_header` request text. `domain` stays
18
- // allowed: it is too common a key.
17
+ // cover. `header` is Node's raw `_header` request text. `domain` is not
18
+ // seeded (too common); a module can still register it as a credential field.
19
19
  const deniedKeys = new Set([
20
20
  'hashword',
21
21
  'cookies',
@@ -25,6 +25,29 @@ const deniedKeys = new Set([
25
25
  'header',
26
26
  ]);
27
27
 
28
+ // Record-contract fields. A credential field with one of these names must not
29
+ // blank the field in every record.
30
+ const PROTECTED_KEYS = new Set(
31
+ [
32
+ 'integrationId',
33
+ 'integrationType',
34
+ 'userId',
35
+ 'version',
36
+ 'entityId',
37
+ 'credentialId',
38
+ 'requestId',
39
+ 'messageId',
40
+ 'processId',
41
+ 'integrationEvent',
42
+ 'eventName',
43
+ 'handlerName',
44
+ 'method',
45
+ 'route',
46
+ 'routeKey',
47
+ 'statusCode',
48
+ ].map((key) => key.toLowerCase())
49
+ );
50
+
28
51
  const PAIR_KEYS = new Set(['code', 'codeverifier']);
29
52
  const DIGEST_KEY = /(sha(1|256)|hash|digest|checksum)$/;
30
53
 
@@ -33,7 +56,11 @@ const MAX_SCRUB_INPUT = 65536;
33
56
  const URL_IN_TEXT = /\b[a-zA-Z][a-zA-Z0-9+.-]*:\/\/[^\s"'<>`]+/g;
34
57
  const URL_TRAILING_PUNCT = /[.,;:!?)\]}]+$/;
35
58
  const ABSOLUTE_URL = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//;
36
- const ROOT_SLASH = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\/[^/?#]*\//;
59
+ const RAW_ABSOLUTE_PATH = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\/[^/?#]*([^?#]*)/;
60
+ const RAW_RELATIVE_PATH = /^[^?#]*/;
61
+ const PATH_TOKEN_SEGMENT = /^[A-Za-z0-9_.~-]{20,}$/;
62
+ const PREFIXED_TOKEN =
63
+ /(?<![A-Za-z0-9_-])(?:(?:sk|rk)_(?:live|test)_|whsec_|xox[abprs]-|gh[opsu]_|github_pat_|shp(?:at|ss)_|glpat-|npm_)[A-Za-z0-9_-]{16,}|\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/g;
37
64
  const JWT = /\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*/g;
38
65
  const AUTH_SCHEME = /\b(Bearer|bearer|BEARER|Basic|basic|BASIC|Token|token|TOKEN)(\s+)([A-Za-z0-9._~+/=-]{8,})/g;
39
66
  const AUTH_SCHEME_WORD = /^(bearer|basic|token)$/i;
@@ -62,7 +89,7 @@ function normalizeKey(key) {
62
89
 
63
90
  function isDeniedKey(key) {
64
91
  const normalized = normalizeKey(key);
65
- if (!normalized) return false;
92
+ if (!normalized || PROTECTED_KEYS.has(normalized)) return false;
66
93
  if (deniedKeys.has(normalized)) return true;
67
94
  return DENIED_SUFFIXES.some((suffix) => normalized.endsWith(suffix));
68
95
  }
@@ -80,13 +107,19 @@ function isPairKey(key, { withState = false } = {}) {
80
107
  );
81
108
  }
82
109
 
110
+ // Returns the leaves it skipped as record-contract keys, so a caller can warn.
83
111
  function addDeniedKeys(keys) {
84
- if (!Array.isArray(keys)) return;
112
+ const ignored = [];
113
+ if (!Array.isArray(keys)) return ignored;
85
114
  for (const key of keys) {
86
115
  if (typeof key !== 'string') continue;
87
- const leaf = normalizeKey(key.split('.').pop());
88
- if (leaf) deniedKeys.add(leaf);
116
+ const rawLeaf = key.split('.').pop();
117
+ const leaf = normalizeKey(rawLeaf);
118
+ if (!leaf) continue;
119
+ if (PROTECTED_KEYS.has(leaf)) ignored.push(rawLeaf);
120
+ else deniedKeys.add(leaf);
89
121
  }
122
+ return ignored;
90
123
  }
91
124
 
92
125
  function hasMixedClasses(run) {
@@ -107,7 +140,8 @@ function isRedactedValue(value) {
107
140
  }
108
141
 
109
142
  function scrubTokens(text, { allowHex = false, segment = false } = {}) {
110
- let out = text.replace(JWT, redacted);
143
+ let out = text.replace(PREFIXED_TOKEN, redacted);
144
+ out = out.replace(JWT, redacted);
111
145
  out = out.replace(AUTH_SCHEME, (match, scheme, space, credential) => {
112
146
  const classes = [/[A-Z]/, /[a-z]/, /[0-9]/, /[._~+/=-]/].filter((re) =>
113
147
  re.test(credential)
@@ -164,11 +198,18 @@ function redactHash(hash) {
164
198
  return scrubTokens(hash);
165
199
  }
166
200
 
201
+ // A long segment mixing upper case, lower case and digits is an opaque
202
+ // token (webhook secrets live in paths); hex ids and UUIDs lack one class.
203
+ function redactPathSegment(segment) {
204
+ if (PATH_TOKEN_SEGMENT.test(segment) && hasMixedClasses(segment)) {
205
+ return redacted(segment);
206
+ }
207
+ return scrubTokens(segment, { segment: true });
208
+ }
209
+
210
+ // The raw path, not URL#pathname, which percent-encodes `{proxy+}`.
167
211
  function redactPath(pathname) {
168
- return pathname
169
- .split('/')
170
- .map((segment) => scrubTokens(segment, { segment: true }))
171
- .join('/');
212
+ return pathname.split('/').map(redactPathSegment).join('/');
172
213
  }
173
214
 
174
215
  function redactUrlFallback(raw) {
@@ -189,8 +230,7 @@ function redactUrl(url) {
189
230
  try {
190
231
  if (ABSOLUTE_URL.test(raw)) {
191
232
  const u = new URL(raw);
192
- const path =
193
- u.pathname === '/' && !ROOT_SLASH.test(raw) ? '' : u.pathname;
233
+ const path = raw.match(RAW_ABSOLUTE_PATH)[1];
194
234
  return `${u.protocol}//${u.host}${redactPath(path)}${redactParams(
195
235
  u.search,
196
236
  '?'
@@ -198,7 +238,7 @@ function redactUrl(url) {
198
238
  }
199
239
  if (raw.startsWith('/')) {
200
240
  const u = new URL(raw, 'http://relative.invalid');
201
- return `${redactPath(u.pathname)}${redactParams(
241
+ return `${redactPath(raw.match(RAW_RELATIVE_PATH)[0])}${redactParams(
202
242
  u.search,
203
243
  '?'
204
244
  )}${redactHash(u.hash)}`;
@@ -1,3 +1,5 @@
1
+ const { redactUrl } = require('./redact');
2
+
1
3
  // Best-effort extraction of the logical event/processId/integrationId from a
2
4
  // JSON message body. Used only for log correlation. Never throws.
3
5
  function summarizeMessageBody(bodyStr) {
@@ -32,13 +34,26 @@ function unique(list) {
32
34
  return [...new Set(list)];
33
35
  }
34
36
 
37
+ // The route template keeps ids out of the field: REST v1 `resource`, or the
38
+ // path part of an HTTP API v2 `routeKey` such as `GET /api/{id}`.
39
+ function routeTemplate(event, path) {
40
+ if (typeof event.resource === 'string' && event.resource) {
41
+ return event.resource;
42
+ }
43
+ if (typeof event.routeKey === 'string') {
44
+ const space = event.routeKey.indexOf(' ');
45
+ if (space > 0) return event.routeKey.slice(space + 1);
46
+ }
47
+ return path;
48
+ }
49
+
35
50
  function summarizeHttp(event) {
36
51
  const path = event.path || event.rawPath;
37
52
  const summary = {
38
53
  source: 'http',
39
54
  method: event.httpMethod || event.requestContext?.http?.method,
40
55
  path,
41
- route: path,
56
+ route: routeTemplate(event, path),
42
57
  queryKeys: unique([
43
58
  ...objectKeys(event.queryStringParameters),
44
59
  ...objectKeys(event.multiValueQueryStringParameters),
@@ -98,8 +113,6 @@ function toScopeInvocation(summary) {
98
113
  source: 'http',
99
114
  method: summary.method,
100
115
  route: summary.route,
101
- queryKeys: summary.queryKeys ?? [],
102
- headerNames: summary.headerNames ?? [],
103
116
  };
104
117
  if (summary.routeKey) invocation.routeKey = summary.routeKey;
105
118
  return invocation;
@@ -107,4 +120,43 @@ function toScopeInvocation(summary) {
107
120
  return { source: summary.source };
108
121
  }
109
122
 
110
- module.exports = { summarizeLambdaEvent, summarizeMessageBody, toScopeInvocation };
123
+ // Per-request detail for the entry and failure records, never for the
124
+ // scope. A raw path can hold a token, so it goes through redactUrl.
125
+ function toRequestDetails(summary) {
126
+ if (!summary || summary.source !== 'http') return {};
127
+ return {
128
+ path: redactUrl(summary.path ?? ''),
129
+ queryKeys: summary.queryKeys ?? [],
130
+ headerNames: summary.headerNames ?? [],
131
+ };
132
+ }
133
+
134
+ // The full redacted request summary (ADR-048 §6: it goes under `invocation`).
135
+ function toRequestInvocation(summary) {
136
+ return { ...toScopeInvocation(summary), ...toRequestDetails(summary) };
137
+ }
138
+
139
+ // The same shape from an express request, for the express error boundary.
140
+ function summarizeExpressRequest(req) {
141
+ try {
142
+ if (!req || typeof req !== 'object') return { source: 'http' };
143
+ return {
144
+ source: 'http',
145
+ method: req.method,
146
+ path: redactUrl(req.path ?? ''),
147
+ queryKeys: objectKeys(req.query),
148
+ headerNames: unique(objectKeys(req.headers).map((name) => name.toLowerCase())),
149
+ };
150
+ } catch {
151
+ return { source: 'http' };
152
+ }
153
+ }
154
+
155
+ module.exports = {
156
+ summarizeLambdaEvent,
157
+ summarizeMessageBody,
158
+ toScopeInvocation,
159
+ toRequestDetails,
160
+ toRequestInvocation,
161
+ summarizeExpressRequest,
162
+ };
package/modules/module.js CHANGED
@@ -32,7 +32,7 @@ class Module extends Delegate {
32
32
  this.credential = entityObj?.credential;
33
33
  this.definition = definition;
34
34
  this.name = this.definition.moduleName;
35
- this.logger = getLogger(`module.${this.name}`).child(() => ({
35
+ this.logger = getLogger(`module.${this.name ?? 'unknown'}`).child(() => ({
36
36
  entityId: this.entity?.id,
37
37
  credentialId:
38
38
  this.credential?.id ??
@@ -357,7 +357,7 @@ class OAuth2Requester extends Requester {
357
357
  eventName: `${this.logger.name}.token_refresh_failed`,
358
358
  statusCode:
359
359
  error?.statusCode ?? error?.status ?? error?.response?.status,
360
- error,
360
+ reason: error?.message,
361
361
  });
362
362
 
363
363
  if (!this._isDefinitiveAuthRejection(error)) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@friggframework/core",
3
3
  "prettier": "@friggframework/prettier-config",
4
- "version": "2.0.0--canary.654.be44e66.0",
4
+ "version": "2.0.0--canary.654.ebad223.0",
5
5
  "dependencies": {
6
6
  "@aws-sdk/client-apigatewaymanagementapi": "^3.588.0",
7
7
  "@aws-sdk/client-kms": "^3.588.0",
@@ -47,9 +47,9 @@
47
47
  }
48
48
  },
49
49
  "devDependencies": {
50
- "@friggframework/eslint-config": "2.0.0--canary.654.be44e66.0",
51
- "@friggframework/prettier-config": "2.0.0--canary.654.be44e66.0",
52
- "@friggframework/test": "2.0.0--canary.654.be44e66.0",
50
+ "@friggframework/eslint-config": "2.0.0--canary.654.ebad223.0",
51
+ "@friggframework/prettier-config": "2.0.0--canary.654.ebad223.0",
52
+ "@friggframework/test": "2.0.0--canary.654.ebad223.0",
53
53
  "@prisma/client": "^6.19.3",
54
54
  "@types/lodash": "4.17.15",
55
55
  "@typescript-eslint/eslint-plugin": "^8.0.0",
@@ -89,5 +89,5 @@
89
89
  "publishConfig": {
90
90
  "access": "public"
91
91
  },
92
- "gitHead": "be44e66226ec1db051c264a5f8cf02998f3aa14c"
92
+ "gitHead": "ebad2239696f6fbfd7918a327bdbd45c737dc7e0"
93
93
  }
@@ -148,13 +148,8 @@ class UserRepositoryDocumentDB extends UserRepositoryInterface {
148
148
 
149
149
  // Defensive check: verify document was found after insert
150
150
  if (!created) {
151
- log.error('User not found after insert', {
152
- eventName: 'frigg.user.not_found_after_insert',
153
- insertedId: fromObjectId(insertedId),
154
- paramKeys: Object.keys(params),
155
- });
156
151
  throw new Error(
157
- 'Failed to create individual user: Document not found after insert. ' +
152
+ `Failed to create individual user: Document not found after insert (insertedId ${fromObjectId(insertedId)}). ` +
158
153
  'This indicates a database consistency issue.'
159
154
  );
160
155
  }
@@ -192,13 +187,8 @@ class UserRepositoryDocumentDB extends UserRepositoryInterface {
192
187
 
193
188
  // Defensive check: verify document was found after insert
194
189
  if (!created) {
195
- log.error('Organization user not found after insert', {
196
- eventName: 'frigg.user.organization_not_found_after_insert',
197
- insertedId: fromObjectId(insertedId),
198
- paramKeys: Object.keys(params),
199
- });
200
190
  throw new Error(
201
- 'Failed to create organization user: Document not found after insert. ' +
191
+ `Failed to create organization user: Document not found after insert (insertedId ${fromObjectId(insertedId)}). ` +
202
192
  'This indicates a database consistency issue.'
203
193
  );
204
194
  }
@@ -284,7 +274,8 @@ class UserRepositoryDocumentDB extends UserRepositoryInterface {
284
274
  // Defensive check: verify document was found after update
285
275
  if (!updated) {
286
276
  throw new Error(
287
- `Failed to update user ${userId}: not found after update`
277
+ 'Failed to update individual user: Document not found after update. ' +
278
+ 'This indicates a database consistency issue.'
288
279
  );
289
280
  }
290
281
 
@@ -317,7 +308,8 @@ class UserRepositoryDocumentDB extends UserRepositoryInterface {
317
308
 
318
309
  if (!updated) {
319
310
  throw new Error(
320
- `Failed to update user ${userId}: not found after update`
311
+ 'Failed to update organization user: Document not found after update. ' +
312
+ 'This indicates a database consistency issue.'
321
313
  );
322
314
  }
323
315