@friggframework/core 2.0.0--canary.654.be44e66.0 → 2.0.0--canary.654.42e42b2.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/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) {
@@ -55,15 +57,18 @@ class Worker {
55
57
  // Treat as success so SQS deletes it from the queue.
56
58
  // Logged explicitly — silent discards made prod debugging
57
59
  // 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,
60
+ log.error('Record halted (discarded, no retry)', {
61
+ eventName: 'frigg.worker.record_halted',
62
62
  statusCode: error.statusCode,
63
+ error,
63
64
  });
64
65
  return;
65
66
  }
66
- console.error(`[Worker] Failed to process record ${record.messageId}:`, error);
67
+ // The message goes back to SQS, so WARN (ADR-048 §4).
68
+ log.warn('Record failed, returned for retry', {
69
+ eventName: 'frigg.worker.record_failed',
70
+ error,
71
+ });
67
72
  batchItemFailures.push({ itemIdentifier: record.messageId });
68
73
  }
69
74
  });
@@ -6,6 +6,7 @@ const { getLogger, toSanitizedSurrogate } = require('../logs');
6
6
  const {
7
7
  summarizeLambdaEvent,
8
8
  toScopeInvocation,
9
+ toRequestDetails,
9
10
  } = require('../logs/summarize-event');
10
11
  const {
11
12
  runInvocationScope,
@@ -64,6 +65,7 @@ const createHandler = (optionByName = {}) => {
64
65
  try {
65
66
  log.info('Handler invoked', {
66
67
  eventName: 'frigg.handler.invoked',
68
+ ...toRequestDetails(eventSummary),
67
69
  });
68
70
 
69
71
  // If enabled (i.e. if SECRET_ARN is set in process.env) Fetch secrets from AWS Secrets Manager, and set them as environment variables.
@@ -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
  }
@@ -39,10 +39,11 @@ const createApp = (applyMiddleware) => {
39
39
  });
40
40
  res.status(statusCode).json({ error: 'Internal Server Error' });
41
41
  } else {
42
+ // A client error needs no stack; the logger scrubs the reason.
42
43
  log.warn('Request rejected', {
43
44
  eventName: 'frigg.http.request_rejected',
44
45
  statusCode,
45
- error: boomError,
46
+ reason: boomError.message,
46
47
  });
47
48
  res.status(statusCode).json({ error: err.message });
48
49
  }
@@ -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,11 @@
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
+ toScopeInvocation,
7
+ toRequestDetails,
8
+ } = require('./summarize-event');
5
9
 
6
10
  // Deprecated. Kept for one major version; use getLogger or this.logger.
7
11
  const LEGACY_LOGGER_NAME = 'frigg.legacy';
@@ -28,8 +32,10 @@ function initDebugLog(...initMessages) {
28
32
  const log = legacy();
29
33
  if (!log.isLevelEnabled('DEBUG')) return;
30
34
  const event = initMessages.find((m) => m && typeof m === 'object');
35
+ const summary = summarizeLambdaEvent(event);
31
36
  log.debug('Debug log initialized', {
32
- invocation: toScopeInvocation(summarizeLambdaEvent(event)),
37
+ invocation: toScopeInvocation(summary),
38
+ ...toRequestDetails(summary),
33
39
  });
34
40
  }
35
41
 
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
 
@@ -34,6 +57,8 @@ 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
59
  const ROOT_SLASH = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\/[^/?#]*\//;
60
+ const PREFIXED_TOKEN =
61
+ /(?<![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
62
  const JWT = /\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*/g;
38
63
  const AUTH_SCHEME = /\b(Bearer|bearer|BEARER|Basic|basic|BASIC|Token|token|TOKEN)(\s+)([A-Za-z0-9._~+/=-]{8,})/g;
39
64
  const AUTH_SCHEME_WORD = /^(bearer|basic|token)$/i;
@@ -62,7 +87,7 @@ function normalizeKey(key) {
62
87
 
63
88
  function isDeniedKey(key) {
64
89
  const normalized = normalizeKey(key);
65
- if (!normalized) return false;
90
+ if (!normalized || PROTECTED_KEYS.has(normalized)) return false;
66
91
  if (deniedKeys.has(normalized)) return true;
67
92
  return DENIED_SUFFIXES.some((suffix) => normalized.endsWith(suffix));
68
93
  }
@@ -80,13 +105,19 @@ function isPairKey(key, { withState = false } = {}) {
80
105
  );
81
106
  }
82
107
 
108
+ // Returns the leaves it skipped as record-contract keys, so a caller can warn.
83
109
  function addDeniedKeys(keys) {
84
- if (!Array.isArray(keys)) return;
110
+ const ignored = [];
111
+ if (!Array.isArray(keys)) return ignored;
85
112
  for (const key of keys) {
86
113
  if (typeof key !== 'string') continue;
87
- const leaf = normalizeKey(key.split('.').pop());
88
- if (leaf) deniedKeys.add(leaf);
114
+ const rawLeaf = key.split('.').pop();
115
+ const leaf = normalizeKey(rawLeaf);
116
+ if (!leaf) continue;
117
+ if (PROTECTED_KEYS.has(leaf)) ignored.push(rawLeaf);
118
+ else deniedKeys.add(leaf);
89
119
  }
120
+ return ignored;
90
121
  }
91
122
 
92
123
  function hasMixedClasses(run) {
@@ -107,7 +138,8 @@ function isRedactedValue(value) {
107
138
  }
108
139
 
109
140
  function scrubTokens(text, { allowHex = false, segment = false } = {}) {
110
- let out = text.replace(JWT, redacted);
141
+ let out = text.replace(PREFIXED_TOKEN, redacted);
142
+ out = out.replace(JWT, redacted);
111
143
  out = out.replace(AUTH_SCHEME, (match, scheme, space, credential) => {
112
144
  const classes = [/[A-Z]/, /[a-z]/, /[0-9]/, /[._~+/=-]/].filter((re) =>
113
145
  re.test(credential)
@@ -32,13 +32,26 @@ function unique(list) {
32
32
  return [...new Set(list)];
33
33
  }
34
34
 
35
+ // The route template keeps ids out of the field: REST v1 `resource`, or the
36
+ // path part of an HTTP API v2 `routeKey` such as `GET /api/{id}`.
37
+ function routeTemplate(event, path) {
38
+ if (typeof event.resource === 'string' && event.resource) {
39
+ return event.resource;
40
+ }
41
+ if (typeof event.routeKey === 'string') {
42
+ const space = event.routeKey.indexOf(' ');
43
+ if (space > 0) return event.routeKey.slice(space + 1);
44
+ }
45
+ return path;
46
+ }
47
+
35
48
  function summarizeHttp(event) {
36
49
  const path = event.path || event.rawPath;
37
50
  const summary = {
38
51
  source: 'http',
39
52
  method: event.httpMethod || event.requestContext?.http?.method,
40
53
  path,
41
- route: path,
54
+ route: routeTemplate(event, path),
42
55
  queryKeys: unique([
43
56
  ...objectKeys(event.queryStringParameters),
44
57
  ...objectKeys(event.multiValueQueryStringParameters),
@@ -98,8 +111,6 @@ function toScopeInvocation(summary) {
98
111
  source: 'http',
99
112
  method: summary.method,
100
113
  route: summary.route,
101
- queryKeys: summary.queryKeys ?? [],
102
- headerNames: summary.headerNames ?? [],
103
114
  };
104
115
  if (summary.routeKey) invocation.routeKey = summary.routeKey;
105
116
  return invocation;
@@ -107,4 +118,19 @@ function toScopeInvocation(summary) {
107
118
  return { source: summary.source };
108
119
  }
109
120
 
110
- module.exports = { summarizeLambdaEvent, summarizeMessageBody, toScopeInvocation };
121
+ // Per-request detail for the one entry record, never for the scope.
122
+ function toRequestDetails(summary) {
123
+ if (!summary || summary.source !== 'http') return {};
124
+ return {
125
+ path: summary.path,
126
+ queryKeys: summary.queryKeys ?? [],
127
+ headerNames: summary.headerNames ?? [],
128
+ };
129
+ }
130
+
131
+ module.exports = {
132
+ summarizeLambdaEvent,
133
+ summarizeMessageBody,
134
+ toScopeInvocation,
135
+ toRequestDetails,
136
+ };
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 ??
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.42e42b2.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.42e42b2.0",
51
+ "@friggframework/prettier-config": "2.0.0--canary.654.42e42b2.0",
52
+ "@friggframework/test": "2.0.0--canary.654.42e42b2.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": "42e42b2b48e90e9b46ffe6bcf009150a66a010cd"
93
93
  }