@friggframework/core 2.0.0--canary.652.b14f3b5.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.
Files changed (56) hide show
  1. package/CLAUDE.md +13 -12
  2. package/README.md +19 -22
  3. package/core/CLAUDE.md +25 -12
  4. package/core/Worker.js +42 -34
  5. package/core/create-handler.js +99 -197
  6. package/core/index.js +8 -1
  7. package/core/invocation-scope.js +224 -0
  8. package/core/secrets-to-env.js +7 -6
  9. package/database/config.js +0 -7
  10. package/database/encryption/README.md +5 -1
  11. package/database/encryption/encryption-schema-registry.js +3 -0
  12. package/errors/fetch-error.js +48 -65
  13. package/handlers/app-definition-loader.js +53 -2
  14. package/handlers/app-handler-helpers.js +16 -4
  15. package/handlers/database-migration-handler.js +10 -1
  16. package/handlers/routers/websocket.js +26 -6
  17. package/handlers/workers/db-migration.js +12 -1
  18. package/handlers/workers/dlq-processor.js +66 -38
  19. package/index.js +19 -1
  20. package/integrations/integration-base.js +56 -38
  21. package/integrations/integration-router.js +21 -20
  22. package/integrations/use-cases/delete-integration-for-user.js +24 -13
  23. package/jest.config.js +2 -0
  24. package/logs/.eslintrc.json +26 -1
  25. package/logs/__fixtures__/events.js +82 -0
  26. package/logs/__fixtures__/matchers.js +45 -0
  27. package/logs/__fixtures__/secrets.js +22 -0
  28. package/logs/__fixtures__/vectors.js +329 -0
  29. package/logs/__fixtures__/with-env.js +45 -0
  30. package/logs/context.js +92 -0
  31. package/logs/debug-shims.js +49 -0
  32. package/logs/index.js +14 -3
  33. package/logs/jest-logger-setup.js +25 -0
  34. package/logs/levels.js +71 -0
  35. package/logs/logger-runtime.js +142 -0
  36. package/logs/logger.js +132 -42
  37. package/logs/record.js +199 -0
  38. package/logs/redact.js +325 -0
  39. package/logs/serialize.js +322 -0
  40. package/logs/sinks.js +99 -0
  41. package/logs/summarize-event.js +136 -0
  42. package/modules/module.js +32 -19
  43. package/modules/requester/oauth-2.js +28 -22
  44. package/modules/requester/requester.js +69 -6
  45. package/package.json +5 -6
  46. package/queues/queuer-util.js +1 -15
  47. package/telemetry/bind-telemetry-context.js +4 -0
  48. package/telemetry/instrument-handler.js +19 -2
  49. package/telemetry/no-op-telemetry.js +4 -0
  50. package/telemetry/otel-telemetry.js +25 -6
  51. package/telemetry/telemetry-context.js +19 -8
  52. package/telemetry/telemetry-runtime.js +17 -0
  53. package/telemetry/telemetry-service.js +1 -0
  54. package/types/errors/index.d.ts +24 -10
  55. package/types/logs/index.d.ts +61 -2
  56. package/user/repositories/user-repository-documentdb.js +18 -42
@@ -0,0 +1,22 @@
1
+ // Fake values only. They avoid real provider prefixes so push protection stays quiet.
2
+ const SECRETS = Object.freeze({
3
+ bearer: 'ZqvbLv8tZrQ8kNM5Jq4SRrvrMpRC',
4
+ apiKeyQuery: 'ZqGkGLq6NLF7vXc7cJTWV4BwX3Ld',
5
+ friggApiKey: 'Zq4kPQhzWCfJXwXxq7KKSMcpt5bk',
6
+ cookie: 'ZqKBfkjbBx63hpkzVMfQnw8S3wNS',
7
+ oauthCode: 'ZqMNW4x97kwsvhwk48BnsXD3JCCB',
8
+ codeVerifier: 'Zq64cfH4zmrmJmxHpNRG6z8QTdDh',
9
+ password: 'ZqtkDW2Cbh5kH7SnbK4FGQLRxJLT',
10
+ clientSecret: 'ZqVbQLw3kDGwxHgrBW32MKLHJgtr',
11
+ accessToken: 'Zqnw6cnh5ZXpQLB5njSxZKQDPhJc',
12
+ refreshToken: 'ZqhDdt92NR5cvMsvQ6DFmM2qfJFS',
13
+ idToken: 'ZqCGFMSgJDmq8NKJ6ZG4vSjVd6Gw',
14
+ hashword: 'ZqvVPT8cjhxtt9cddR2NqrHSzP66',
15
+ dbPassword: 'ZqZkGTVwpk38sVnHhKZzWFg9djPW',
16
+ signature: 'Zq7b634PtgG9Ft4BJ9cjrMTZXjNX',
17
+ jwt: 'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJmYWtlLXVzZXItMTIzNDUifQ.ZmFrZVNpZ25hdHVyZUZvclRlc3RzT25seQ',
18
+ hexToken: '9f2c4e7a1b3d5f8e0a6c2b4d7e9f1a3c5b7d9e2f',
19
+ base64Run: 'QmFzZTY0RmFrZVNlY3JldFZhbHVlRm9yVGVzdHNPbmx5MTIzNDU2Nzg5MA==',
20
+ });
21
+
22
+ module.exports = { SECRETS };
@@ -0,0 +1,329 @@
1
+ // ADR-048 §14 redaction vectors. Each vector is one log call that carries
2
+ // secrets; the suite asserts that no 8-char window of them reaches a record.
3
+ const { SECRETS: S } = require('./secrets');
4
+ const { httpApiV2Event, restV1Event, sqsEvent } = require('./events');
5
+ const { summarizeLambdaEvent } = require('../summarize-event');
6
+
7
+ const secretUrl = `https://api.example.com/v1/items?api_key=${S.apiKeyQuery}`;
8
+ const SIGNATURE_VALUE = 'fakeSgnValue2Gh9Tc5Wp7LmQx3';
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
+
20
+ function errorWithMessage(message, name = 'Error') {
21
+ const error = new Error(message);
22
+ error.name = name;
23
+ return error;
24
+ }
25
+
26
+ function nodeFetchError() {
27
+ const { FetchError } = require('node-fetch');
28
+ return new FetchError(
29
+ `request to ${secretUrl} failed, reason: connect ECONNREFUSED Authorization: Bearer ${S.bearer}`,
30
+ 'system',
31
+ { code: 'ECONNREFUSED' }
32
+ );
33
+ }
34
+
35
+ function causeChain() {
36
+ const deepest = errorWithMessage(`token refresh failed: refresh_token=${S.refreshToken}`);
37
+ const third = new Error('third', { cause: deepest });
38
+ const second = new Error('second', { cause: third });
39
+ return new Error('top', { cause: second });
40
+ }
41
+
42
+ function cyclicObject() {
43
+ const node = { name: 'node', access_token: S.accessToken };
44
+ node.self = node;
45
+ node.list = [node, { password: S.password }];
46
+ return node;
47
+ }
48
+
49
+ function aggregateError() {
50
+ return new AggregateError(
51
+ [
52
+ errorWithMessage(`GET ${secretUrl} 401`),
53
+ errorWithMessage(`Authorization: Basic ${Buffer.from(`u:${S.password}`).toString('base64')}`),
54
+ ],
55
+ 'batch failed'
56
+ );
57
+ }
58
+
59
+ function prismaValidationError() {
60
+ const error = errorWithMessage(
61
+ [
62
+ 'Invalid `prisma.credential.create()` invocation:',
63
+ '',
64
+ `{ data: { access_token: "${S.accessToken}", refresh_token: "${S.refreshToken}" } }`,
65
+ '',
66
+ 'Argument `userId` is missing.',
67
+ ].join('\n'),
68
+ 'PrismaClientValidationError'
69
+ );
70
+ error.clientVersion = '6.19.3';
71
+ return error;
72
+ }
73
+
74
+ function vectors() {
75
+ return [
76
+ {
77
+ name: 'HTTP API v2 event through summarizeLambdaEvent',
78
+ level: 'info',
79
+ fields: () => ({ invocation: summarizeLambdaEvent(httpApiV2Event()) }),
80
+ secrets: [S.friggApiKey, S.cookie, S.oauthCode, S.bearer, S.apiKeyQuery, S.codeVerifier, S.clientSecret],
81
+ expectSanitized: (record) => {
82
+ expect(record.invocation.headerNames).toEqual(
83
+ expect.arrayContaining(['x-frigg-api-key', 'cookie', 'authorization'])
84
+ );
85
+ expect(record.invocation.queryKeys).toEqual(expect.arrayContaining(['code', 'api_key']));
86
+ },
87
+ },
88
+ {
89
+ name: 'HTTP API v2 event as a raw field',
90
+ level: 'debug',
91
+ fields: () => ({ event: httpApiV2Event() }),
92
+ secrets: [S.friggApiKey, S.cookie, S.oauthCode, S.bearer, S.apiKeyQuery, S.codeVerifier, S.clientSecret],
93
+ expectSanitized: (record) => {
94
+ expect(record.event.headers).toEqual(
95
+ expect.arrayContaining(['x-frigg-api-key', 'cookie'])
96
+ );
97
+ },
98
+ },
99
+ {
100
+ name: 'REST v1 event as a raw field',
101
+ level: 'debug',
102
+ fields: () => ({ event: restV1Event() }),
103
+ secrets: [S.friggApiKey, S.cookie, S.password, S.accessToken, S.refreshToken],
104
+ expectSanitized: (record) => {
105
+ expect(record.event.httpMethod).toBe('POST');
106
+ },
107
+ },
108
+ {
109
+ name: 'SQS event as a raw field',
110
+ level: 'debug',
111
+ fields: () => ({ event: sqsEvent() }),
112
+ secrets: [S.accessToken, S.clientSecret, S.bearer],
113
+ expectSanitized: (record) => {
114
+ expect(record.event.Records[0].messageId).toBe('msg-1');
115
+ },
116
+ },
117
+ {
118
+ name: 'node-fetch FetchError',
119
+ level: 'error',
120
+ fields: () => ({ error: nodeFetchError() }),
121
+ secrets: [S.apiKeyQuery, S.bearer],
122
+ expectSanitized: (record) => {
123
+ expect(record.error.message).toContain('api_key=REDACTED');
124
+ },
125
+ },
126
+ {
127
+ name: 'Error as the message',
128
+ level: 'error',
129
+ message: () => errorWithMessage(`GET ${secretUrl} Authorization: Bearer ${S.bearer}`),
130
+ secrets: [S.apiKeyQuery, S.bearer],
131
+ expectSanitized: (record) => {
132
+ expect(record.message).toContain('api_key=REDACTED');
133
+ },
134
+ },
135
+ {
136
+ name: 'cause chain with a secret at depth 3',
137
+ level: 'error',
138
+ fields: () => ({ error: causeChain() }),
139
+ secrets: [S.refreshToken],
140
+ expectSanitized: (record) => {
141
+ expect(record.error.cause.cause.message).toBe('third');
142
+ },
143
+ },
144
+ {
145
+ name: 'cyclic object',
146
+ level: 'warn',
147
+ fields: () => ({ node: cyclicObject() }),
148
+ secrets: [S.accessToken, S.password],
149
+ expectSanitized: (record) => {
150
+ expect(JSON.stringify(record)).toContain('[Circular]');
151
+ },
152
+ },
153
+ {
154
+ name: 'AggregateError',
155
+ level: 'error',
156
+ fields: () => ({ error: aggregateError() }),
157
+ secrets: [S.apiKeyQuery, S.password],
158
+ expectSanitized: (record) => {
159
+ expect(record.error.type).toBe('AggregateError');
160
+ },
161
+ },
162
+ {
163
+ name: 'Prisma validation error',
164
+ level: 'error',
165
+ fields: () => ({ error: prismaValidationError() }),
166
+ secrets: [S.accessToken, S.refreshToken],
167
+ expectSanitized: (record) => {
168
+ expect(record.error.message).toContain('Argument `userId` is missing.');
169
+ },
170
+ },
171
+ {
172
+ name: 'connection strings',
173
+ level: 'warn',
174
+ message: () =>
175
+ `connect failed postgresql://frigg:${S.dbPassword}@db.internal:5432/app and mongodb+srv://frigg:${S.dbPassword}@cluster0.example.net/app`,
176
+ secrets: [S.dbPassword],
177
+ expectSanitized: (record) => {
178
+ expect(record.message).toContain('db.internal');
179
+ },
180
+ },
181
+ {
182
+ name: 'JWT',
183
+ level: 'info',
184
+ fields: () => ({ note: `id token ${S.jwt} received` }),
185
+ secrets: [S.jwt],
186
+ },
187
+ {
188
+ name: 'Bearer and Basic credentials',
189
+ level: 'info',
190
+ fields: () => ({
191
+ note: `Authorization: Bearer ${S.bearer}; Authorization: Basic ${Buffer.from(`u:${S.password}`).toString('base64')}`,
192
+ }),
193
+ secrets: [S.bearer, S.password],
194
+ },
195
+ {
196
+ name: 'base64 run',
197
+ level: 'info',
198
+ fields: () => ({ note: `blob ${S.base64Run} end` }),
199
+ secrets: [S.base64Run],
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
+ },
208
+ {
209
+ name: '40-hex token',
210
+ level: 'error',
211
+ message: () => `lookup failed for key ${S.hexToken}`,
212
+ secrets: [S.hexToken],
213
+ },
214
+ {
215
+ name: 'client_secret, signature and code pairs',
216
+ level: 'info',
217
+ fields: () => ({
218
+ note: `client_secret=${S.clientSecret} signature=${SIGNATURE_VALUE} code=${S.oauthCode}`,
219
+ }),
220
+ secrets: [S.clientSecret, SIGNATURE_VALUE, S.oauthCode],
221
+ },
222
+ {
223
+ name: 'JSON string',
224
+ level: 'info',
225
+ fields: () => ({ note: JSON.stringify({ token: S.accessToken, id: 7 }) }),
226
+ secrets: [S.accessToken],
227
+ },
228
+ {
229
+ name: 'k=v& string',
230
+ level: 'info',
231
+ fields: () => ({ note: `user=alice&password=${S.password}&next=/home` }),
232
+ secrets: [S.password],
233
+ },
234
+ {
235
+ name: 'URLSearchParams body',
236
+ level: 'debug',
237
+ fields: () => ({
238
+ body: new URLSearchParams({
239
+ grant_type: 'authorization_code',
240
+ code: S.oauthCode,
241
+ code_verifier: S.codeVerifier,
242
+ client_secret: S.clientSecret,
243
+ }),
244
+ }),
245
+ secrets: [S.oauthCode, S.codeVerifier, S.clientSecret],
246
+ expectSanitized: (record) => {
247
+ expect(JSON.stringify(record.body)).toContain('grant_type');
248
+ },
249
+ },
250
+ {
251
+ name: 'Headers, URL and Buffer',
252
+ level: 'debug',
253
+ fields: () => ({
254
+ headers: new Headers({ authorization: `Bearer ${S.bearer}`, 'x-frigg-api-key': S.friggApiKey }),
255
+ url: new URL(`https://user:${S.password}@api.example.com/x?access_token=${S.accessToken}`),
256
+ raw: Buffer.from(`secret=${S.clientSecret}`),
257
+ }),
258
+ secrets: [S.bearer, S.friggApiKey, S.password, S.accessToken, S.clientSecret],
259
+ expectSanitized: (record) => {
260
+ expect(JSON.stringify(record.url)).toContain('api.example.com');
261
+ },
262
+ },
263
+ {
264
+ name: 'OAuth callback params',
265
+ level: 'debug',
266
+ fields: () => ({ params: { code: S.oauthCode, state: 'abc123', code_verifier: S.codeVerifier } }),
267
+ secrets: [S.oauthCode, S.codeVerifier],
268
+ expectSanitized: (record) => {
269
+ expect(Object.keys(record.params)).toEqual(
270
+ expect.arrayContaining(['code', 'state'])
271
+ );
272
+ },
273
+ },
274
+ {
275
+ name: 'hashword',
276
+ level: 'info',
277
+ fields: () => ({ user: { username: 'alice', hashword: S.hashword } }),
278
+ secrets: [S.hashword],
279
+ expectSanitized: (record) => {
280
+ expect(record.user.username).toBe('alice');
281
+ },
282
+ },
283
+ {
284
+ name: 'denied keys as bindings and top-level fields',
285
+ level: 'info',
286
+ bindings: () => ({ apiKey: S.friggApiKey }),
287
+ fields: () => ({ access_token: S.accessToken, id_token: S.idToken, cookies: [S.cookie] }),
288
+ secrets: [S.friggApiKey, S.accessToken, S.idToken, S.cookie],
289
+ },
290
+ ];
291
+ }
292
+
293
+ // A real Requester call that fails with 401. The URL carries a query key and
294
+ // the request an Authorization header.
295
+ async function requesterFetchError() {
296
+ const { Requester } = require('../../modules/requester/requester');
297
+ class TestRequester extends Requester {
298
+ async addAuthHeaders(headers) {
299
+ return { ...headers, Authorization: `Bearer ${S.bearer}` };
300
+ }
301
+ }
302
+ const response = {
303
+ status: 401,
304
+ ok: false,
305
+ bodyUsed: false,
306
+ headers: {
307
+ get: () => 'application/json',
308
+ raw: () => ({ 'content-type': ['application/json'] }),
309
+ [Symbol.iterator]: function* () {
310
+ yield ['content-type', 'application/json'];
311
+ },
312
+ },
313
+ json: async () => ({ access_token: S.accessToken }),
314
+ text: async () => `{"access_token":"${S.accessToken}"}`,
315
+ };
316
+ const requester = new TestRequester({
317
+ backOff: [],
318
+ requestTimeoutMs: 0,
319
+ fetch: async () => response,
320
+ });
321
+ return requester._get({ url: secretUrl }).then(
322
+ () => {
323
+ throw new Error('expected the request to fail');
324
+ },
325
+ (error) => error
326
+ );
327
+ }
328
+
329
+ module.exports = { vectors, requesterFetchError, secretUrl };
@@ -0,0 +1,45 @@
1
+ const { createMemorySink } = require('../sinks');
2
+ const { resetLoggerForTests } = require('../logger-runtime');
3
+
4
+ function applyEnv(vars) {
5
+ const saved = {};
6
+ for (const [key, value] of Object.entries(vars)) {
7
+ saved[key] = Object.prototype.hasOwnProperty.call(process.env, key)
8
+ ? process.env[key]
9
+ : undefined;
10
+ if (value === undefined) delete process.env[key];
11
+ else process.env[key] = value;
12
+ }
13
+ return () => {
14
+ for (const [key, value] of Object.entries(saved)) {
15
+ if (value === undefined) delete process.env[key];
16
+ else process.env[key] = value;
17
+ }
18
+ };
19
+ }
20
+
21
+ // Sets env keys (undefined deletes), resets the logger to read them, and
22
+ // passes a fresh memory sink to fn. Restores env and the test logger after.
23
+ function withEnv(vars, fn) {
24
+ const restore = applyEnv(vars);
25
+ const sink = createMemorySink({ install: false });
26
+ resetLoggerForTests({ sinks: [sink] });
27
+ const done = () => {
28
+ restore();
29
+ resetLoggerForTests({ level: 'TRACE', sinks: [createMemorySink({ install: false })] });
30
+ };
31
+ let result;
32
+ try {
33
+ result = fn(sink);
34
+ } catch (error) {
35
+ done();
36
+ throw error;
37
+ }
38
+ if (result && typeof result.then === 'function') {
39
+ return result.finally(done);
40
+ }
41
+ done();
42
+ return result;
43
+ }
44
+
45
+ module.exports = { withEnv, applyEnv };
@@ -0,0 +1,92 @@
1
+ const { AsyncLocalStorage } = require('node:async_hooks');
2
+
3
+ const LOGGER_SCOPE_KEY = 'log';
4
+ const INTEGRATION_KEYS = ['integrationId', 'integrationType', 'userId', 'version'];
5
+
6
+ // Shared across jest module registries and nested core copies.
7
+ const STORE_KEY = Symbol.for('@friggframework/core.logs.context');
8
+
9
+ function als() {
10
+ if (!globalThis[STORE_KEY]) {
11
+ globalThis[STORE_KEY] = new AsyncLocalStorage();
12
+ }
13
+ return globalThis[STORE_KEY];
14
+ }
15
+
16
+ function isPlainObject(value) {
17
+ if (value === null || typeof value !== 'object') return false;
18
+ const proto = Object.getPrototypeOf(value);
19
+ return proto === Object.prototype || proto === null;
20
+ }
21
+
22
+ function freezeCopy(value) {
23
+ if (Array.isArray(value)) {
24
+ return Object.freeze(value.map(freezeCopy));
25
+ }
26
+ if (isPlainObject(value)) {
27
+ const copy = {};
28
+ for (const key of Object.keys(value)) {
29
+ copy[key] = freezeCopy(value[key]);
30
+ }
31
+ return Object.freeze(copy);
32
+ }
33
+ return value;
34
+ }
35
+
36
+ function assignDefined(target, source) {
37
+ for (const key of Object.keys(source)) {
38
+ if (source[key] !== undefined) target[key] = source[key];
39
+ }
40
+ return target;
41
+ }
42
+
43
+ function mergeStores(outer, inner) {
44
+ const merged = { ...(outer || {}) };
45
+ if (!inner || typeof inner !== 'object') return freezeCopy(merged);
46
+ for (const key of Object.keys(inner)) {
47
+ const value = inner[key];
48
+ if (value === undefined) continue;
49
+ if (key === LOGGER_SCOPE_KEY && isPlainObject(value)) {
50
+ merged[key] = assignDefined({ ...(merged[key] || {}) }, value);
51
+ continue;
52
+ }
53
+ merged[key] = value;
54
+ }
55
+ return freezeCopy(merged);
56
+ }
57
+
58
+ function runInContext(context, fn) {
59
+ const storage = als();
60
+ return storage.run(mergeStores(storage.getStore(), context), fn);
61
+ }
62
+
63
+ function getContext() {
64
+ return als().getStore() ?? null;
65
+ }
66
+
67
+ function getLoggerScope() {
68
+ const store = getContext();
69
+ if (!store) return {};
70
+ const scope = {};
71
+ for (const key of INTEGRATION_KEYS) {
72
+ if (store[key] !== undefined) scope[key] = store[key];
73
+ }
74
+ if (isPlainObject(store[LOGGER_SCOPE_KEY])) {
75
+ Object.assign(scope, store[LOGGER_SCOPE_KEY]);
76
+ }
77
+ return scope;
78
+ }
79
+
80
+ function hasOnlyLoggerKeys(store) {
81
+ if (!store || typeof store !== 'object') return false;
82
+ const keys = Object.keys(store);
83
+ return keys.length > 0 && keys.every((key) => key === LOGGER_SCOPE_KEY);
84
+ }
85
+
86
+ module.exports = {
87
+ LOGGER_SCOPE_KEY,
88
+ runInContext,
89
+ getContext,
90
+ getLoggerScope,
91
+ hasOnlyLoggerKeys,
92
+ };
@@ -0,0 +1,49 @@
1
+ const util = require('node:util');
2
+ const { getLogger } = require('./logger');
3
+ const { getContext } = require('./context');
4
+ const {
5
+ summarizeLambdaEvent,
6
+ toScopeInvocation,
7
+ toRequestDetails,
8
+ } = require('./summarize-event');
9
+
10
+ // Deprecated. Kept for one major version; use getLogger or this.logger.
11
+ const LEGACY_LOGGER_NAME = 'frigg.legacy';
12
+ const FORMAT_DIRECTIVE = /%[sdifjoOc%]/;
13
+
14
+ const legacy = () => getLogger(LEGACY_LOGGER_NAME);
15
+
16
+ function debug(...messages) {
17
+ if (!messages.length) return;
18
+ const log = legacy();
19
+ if (!log.isLevelEnabled('DEBUG')) return;
20
+ const index = messages.findIndex((m) => typeof m === 'string');
21
+ if (index === 0 && FORMAT_DIRECTIVE.test(messages[0])) {
22
+ log.debug(util.format(...messages));
23
+ return;
24
+ }
25
+ const message = index === -1 ? '' : messages[index];
26
+ const args = messages.filter((_, i) => i !== index);
27
+ log.debug(message, args.length ? { args } : undefined);
28
+ }
29
+
30
+ function initDebugLog(...initMessages) {
31
+ if (getContext()?.log) return;
32
+ const log = legacy();
33
+ if (!log.isLevelEnabled('DEBUG')) return;
34
+ const event = initMessages.find((m) => m && typeof m === 'object');
35
+ const summary = summarizeLambdaEvent(event);
36
+ log.debug('Debug log initialized', {
37
+ invocation: toScopeInvocation(summary),
38
+ ...toRequestDetails(summary),
39
+ });
40
+ }
41
+
42
+ function flushDebugLog(error) {
43
+ legacy().error('Unhandled error', {
44
+ eventName: 'frigg.legacy.error',
45
+ error: error || new Error('flushDebugLog called with empty error'),
46
+ });
47
+ }
48
+
49
+ module.exports = { debug, initDebugLog, flushDebugLog };
package/logs/index.js CHANGED
@@ -1,7 +1,18 @@
1
- const {debug, initDebugLog, flushDebugLog} = require('./logger');
1
+ const { getLogger } = require('./logger');
2
+ const { createMemorySink } = require('./sinks');
3
+ const { resetLoggerForTests } = require('./logger-runtime');
4
+ const { serializeError, toSanitizedSurrogate } = require('./serialize');
5
+ const { redactValue } = require('./redact');
6
+ const { debug, initDebugLog, flushDebugLog } = require('./debug-shims');
2
7
 
3
8
  module.exports = {
9
+ getLogger,
10
+ createMemorySink,
11
+ resetLoggerForTests,
12
+ serializeError,
13
+ redactValue,
14
+ toSanitizedSurrogate,
4
15
  debug,
5
16
  initDebugLog,
6
- flushDebugLog
7
- }
17
+ flushDebugLog,
18
+ };
@@ -0,0 +1,25 @@
1
+ const { createMemorySink } = require('./sinks');
2
+ const { resetLoggerForTests, takeViolationsForTests } = require('./logger-runtime');
3
+ const { toContainNoSecretWindow } = require('./__fixtures__/matchers');
4
+
5
+ expect.extend({ toContainNoSecretWindow });
6
+
7
+ const resetToMemory = () =>
8
+ resetLoggerForTests({
9
+ level: 'TRACE',
10
+ sinks: [createMemorySink({ install: false })],
11
+ trackViolations: true,
12
+ });
13
+
14
+ // Also at load: records written at require time must not reach fd 1.
15
+ resetToMemory();
16
+
17
+ beforeEach(resetToMemory);
18
+
19
+ afterEach(() => {
20
+ const violations = takeViolationsForTests();
21
+ if (violations.length) {
22
+ const list = violations.map((v) => `${v.level} ${v.logger}: ${v.message}`).join('\n');
23
+ throw new Error(`frigg.* records at WARN or above need an eventName:\n${list}`);
24
+ }
25
+ });
package/logs/levels.js ADDED
@@ -0,0 +1,71 @@
1
+ const LEVELS = Object.freeze({
2
+ TRACE: 10,
3
+ DEBUG: 20,
4
+ INFO: 30,
5
+ WARN: 40,
6
+ ERROR: 50,
7
+ FATAL: 60,
8
+ });
9
+
10
+ const LEVEL_NAMES = Object.freeze(Object.keys(LEVELS));
11
+ const DEFAULT_LEVEL = 'INFO';
12
+ const MAX_ECHO_LENGTH = 32;
13
+
14
+ function parseLevel(value) {
15
+ if (typeof value !== 'string') return null;
16
+ const name = value.trim().toUpperCase();
17
+ return Object.prototype.hasOwnProperty.call(LEVELS, name) ? name : null;
18
+ }
19
+
20
+ function isSet(value) {
21
+ return typeof value === 'string' && value.trim() !== '';
22
+ }
23
+
24
+ function isLocalRun(env) {
25
+ if (env.STAGE === 'local') return true;
26
+ const offline = String(env.IS_OFFLINE ?? '').trim().toLowerCase();
27
+ return offline === 'true' || offline === '1';
28
+ }
29
+
30
+ function lessVerbose(a, b) {
31
+ return LEVELS[a] >= LEVELS[b] ? a : b;
32
+ }
33
+
34
+ function resolveLevel(env = {}) {
35
+ const warnings = [];
36
+ let level;
37
+
38
+ if (isSet(env.FRIGG_LOG_LEVEL)) {
39
+ level = parseLevel(env.FRIGG_LOG_LEVEL);
40
+ if (!level) {
41
+ level = DEFAULT_LEVEL;
42
+ warnings.push({
43
+ eventName: 'frigg.logger.invalid_level',
44
+ message: 'Unknown FRIGG_LOG_LEVEL, using INFO',
45
+ fields: {
46
+ received: env.FRIGG_LOG_LEVEL.trim().slice(0, MAX_ECHO_LENGTH),
47
+ allowed: LEVEL_NAMES,
48
+ },
49
+ });
50
+ }
51
+ } else if (env.DEBUG_VERBOSE === '1' || isLocalRun(env)) {
52
+ level = 'DEBUG';
53
+ } else {
54
+ level = DEFAULT_LEVEL;
55
+ }
56
+
57
+ const awsLevel = parseLevel(env.AWS_LAMBDA_LOG_LEVEL);
58
+ if (awsLevel && awsLevel !== level) {
59
+ const effective = lessVerbose(level, awsLevel);
60
+ warnings.push({
61
+ eventName: 'frigg.logger.level_conflict',
62
+ message: 'FRIGG_LOG_LEVEL and AWS_LAMBDA_LOG_LEVEL differ, using the less verbose level',
63
+ fields: { friggLevel: level, awsLevel, effectiveLevel: effective },
64
+ });
65
+ level = effective;
66
+ }
67
+
68
+ return { level, warnings };
69
+ }
70
+
71
+ module.exports = { LEVELS, LEVEL_NAMES, DEFAULT_LEVEL, parseLevel, resolveLevel };