@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.
- package/CLAUDE.md +13 -12
- package/README.md +19 -22
- package/core/CLAUDE.md +25 -12
- package/core/Worker.js +42 -34
- package/core/create-handler.js +99 -197
- package/core/index.js +8 -1
- package/core/invocation-scope.js +224 -0
- package/core/secrets-to-env.js +7 -6
- package/database/config.js +0 -7
- package/database/encryption/README.md +5 -1
- package/database/encryption/encryption-schema-registry.js +3 -0
- package/errors/fetch-error.js +48 -65
- package/handlers/app-definition-loader.js +53 -2
- package/handlers/app-handler-helpers.js +16 -4
- package/handlers/database-migration-handler.js +10 -1
- package/handlers/routers/websocket.js +26 -6
- package/handlers/workers/db-migration.js +12 -1
- package/handlers/workers/dlq-processor.js +66 -38
- package/index.js +19 -1
- package/integrations/integration-base.js +56 -38
- package/integrations/integration-router.js +21 -20
- package/integrations/use-cases/delete-integration-for-user.js +24 -13
- package/jest.config.js +2 -0
- package/logs/.eslintrc.json +26 -1
- package/logs/__fixtures__/events.js +82 -0
- package/logs/__fixtures__/matchers.js +45 -0
- package/logs/__fixtures__/secrets.js +22 -0
- package/logs/__fixtures__/vectors.js +329 -0
- package/logs/__fixtures__/with-env.js +45 -0
- package/logs/context.js +92 -0
- package/logs/debug-shims.js +49 -0
- package/logs/index.js +14 -3
- package/logs/jest-logger-setup.js +25 -0
- package/logs/levels.js +71 -0
- package/logs/logger-runtime.js +142 -0
- package/logs/logger.js +132 -42
- package/logs/record.js +199 -0
- package/logs/redact.js +325 -0
- package/logs/serialize.js +322 -0
- package/logs/sinks.js +99 -0
- package/logs/summarize-event.js +136 -0
- package/modules/module.js +32 -19
- package/modules/requester/oauth-2.js +28 -22
- package/modules/requester/requester.js +69 -6
- package/package.json +5 -6
- package/queues/queuer-util.js +1 -15
- package/telemetry/bind-telemetry-context.js +4 -0
- package/telemetry/instrument-handler.js +19 -2
- package/telemetry/no-op-telemetry.js +4 -0
- package/telemetry/otel-telemetry.js +25 -6
- package/telemetry/telemetry-context.js +19 -8
- package/telemetry/telemetry-runtime.js +17 -0
- package/telemetry/telemetry-service.js +1 -0
- package/types/errors/index.d.ts +24 -10
- package/types/logs/index.d.ts +61 -2
- package/user/repositories/user-repository-documentdb.js +18 -42
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
const { runInContext } = require('../logs/context');
|
|
2
|
+
const { summarizeMessageBody } = require('../logs/summarize-event');
|
|
3
|
+
const { flushSinks, hasFlushableSinks } = require('../logs/logger-runtime');
|
|
4
|
+
|
|
5
|
+
// Bounds the tail latency telemetry adds to every warm invocation. Kept low so
|
|
6
|
+
// an unreachable OTLP endpoint (e.g. a VPC Lambda with no NAT/egress) costs at
|
|
7
|
+
// most this, not multiple seconds. Override with OTEL_FLUSH_TIMEOUT_MS.
|
|
8
|
+
const DEFAULT_FLUSH_TIMEOUT_MS =
|
|
9
|
+
Number(process.env.OTEL_FLUSH_TIMEOUT_MS) || 500;
|
|
10
|
+
|
|
11
|
+
// Time kept back from the Lambda deadline, so a slow flush never becomes a
|
|
12
|
+
// Lambda timeout.
|
|
13
|
+
const FLUSH_MARGIN_MS = 50;
|
|
14
|
+
|
|
15
|
+
// setTimeout fires at once for a delay above this.
|
|
16
|
+
const MAX_TIMER_MS = 2 ** 31 - 1;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Fold the invocation's buffered usage counters into the durable store, then
|
|
20
|
+
* clear the buffer. On an SQS redelivery (ApproximateReceiveCount > 1) we
|
|
21
|
+
* DISCARD rather than flush — the prior delivery already counted, and the usage
|
|
22
|
+
* accuracy contract is "approximate, skip obvious redeliveries". Fully guarded.
|
|
23
|
+
*/
|
|
24
|
+
async function flushUsageRollup(subscriber, eventSummary, shouldUseDatabase) {
|
|
25
|
+
if (!subscriber) return;
|
|
26
|
+
try {
|
|
27
|
+
// Persisting usage requires a DB connection. DB-free handlers (e.g. the
|
|
28
|
+
// webhook-receipt route) never called connectPrisma, so drop the buffer
|
|
29
|
+
// instead of issuing a connectionless Prisma write.
|
|
30
|
+
if (!shouldUseDatabase) {
|
|
31
|
+
subscriber.discard();
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
// Discard only when EVERY record in the batch is a redelivery. The buffer
|
|
35
|
+
// is invocation-scoped (not per-message), so discarding on *any*
|
|
36
|
+
// redelivery would drop the fresh records' counts too (silent
|
|
37
|
+
// under-count). For a mixed batch we flush: preserving fresh counts and
|
|
38
|
+
// at worst re-counting the one redelivered record is strictly better than
|
|
39
|
+
// losing fresh data for an approximate store. (Integration queue workers
|
|
40
|
+
// are batchSize:1 today, so a batch is all-or-nothing; this keeps it
|
|
41
|
+
// correct if batchSize is ever raised.)
|
|
42
|
+
const records = Array.isArray(eventSummary?.records)
|
|
43
|
+
? eventSummary.records
|
|
44
|
+
: [];
|
|
45
|
+
const allRedelivered =
|
|
46
|
+
records.length > 0 &&
|
|
47
|
+
records.every((r) => Number(r.receiveCount) > 1);
|
|
48
|
+
if (allRedelivered) {
|
|
49
|
+
subscriber.discard();
|
|
50
|
+
} else {
|
|
51
|
+
await subscriber.flush();
|
|
52
|
+
}
|
|
53
|
+
} catch (_) {
|
|
54
|
+
// Usage rollup must never break the handler.
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isTelemetryEnabled(telemetry) {
|
|
59
|
+
try {
|
|
60
|
+
return Boolean(
|
|
61
|
+
telemetry &&
|
|
62
|
+
typeof telemetry.isEnabled === 'function' &&
|
|
63
|
+
telemetry.isEnabled()
|
|
64
|
+
);
|
|
65
|
+
} catch (_) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Flush telemetry before the Lambda container freezes. Because
|
|
72
|
+
* `callbackWaitsForEmptyEventLoop=false` stops the event loop the moment the
|
|
73
|
+
* handler returns, OTel's timer-driven batch processors would never fire — so
|
|
74
|
+
* spans/metrics must be flushed here. No timer of its own: the caller bounds it
|
|
75
|
+
* with `withDeadline`. Fully guarded, so a flush failure never breaks the
|
|
76
|
+
* handler.
|
|
77
|
+
*/
|
|
78
|
+
async function flushTelemetry(telemetry, { signal } = {}) {
|
|
79
|
+
if (!isTelemetryEnabled(telemetry) || signal?.aborted) return;
|
|
80
|
+
try {
|
|
81
|
+
await telemetry.forceFlush();
|
|
82
|
+
} catch (_) {
|
|
83
|
+
// Telemetry flush must never break the handler.
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Run `fn(signal)` for at most `ms`. Owns the only timer and clears it when the
|
|
89
|
+
* work settles. Aborts the signal at the deadline. Never rejects, and a late
|
|
90
|
+
* rejection of the work is swallowed.
|
|
91
|
+
*/
|
|
92
|
+
function withDeadline(ms, fn) {
|
|
93
|
+
const controller = new AbortController();
|
|
94
|
+
return new Promise((resolve) => {
|
|
95
|
+
let timer;
|
|
96
|
+
const finish = () => {
|
|
97
|
+
clearTimeout(timer);
|
|
98
|
+
resolve();
|
|
99
|
+
};
|
|
100
|
+
timer = setTimeout(() => {
|
|
101
|
+
controller.abort();
|
|
102
|
+
finish();
|
|
103
|
+
}, Math.min(ms, MAX_TIMER_MS));
|
|
104
|
+
let work;
|
|
105
|
+
try {
|
|
106
|
+
work = Promise.resolve(fn(controller.signal));
|
|
107
|
+
} catch (_) {
|
|
108
|
+
work = Promise.resolve();
|
|
109
|
+
}
|
|
110
|
+
work.then(finish, finish);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function remainingTimeMs(context) {
|
|
115
|
+
try {
|
|
116
|
+
const remaining = context?.getRemainingTimeInMillis?.();
|
|
117
|
+
return typeof remaining === 'number' ? remaining : Infinity;
|
|
118
|
+
} catch (_) {
|
|
119
|
+
return Infinity;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function flushInvocation({
|
|
124
|
+
telemetry,
|
|
125
|
+
usageRollup,
|
|
126
|
+
eventSummary,
|
|
127
|
+
shouldUseDatabase,
|
|
128
|
+
flushTimeoutMs,
|
|
129
|
+
context,
|
|
130
|
+
}) {
|
|
131
|
+
// ADR-048 §11: usage first and unbounded, then one deadline for the rest.
|
|
132
|
+
await flushUsageRollup(usageRollup, eventSummary, shouldUseDatabase);
|
|
133
|
+
|
|
134
|
+
const flushes = [];
|
|
135
|
+
if (isTelemetryEnabled(telemetry)) {
|
|
136
|
+
flushes.push((signal) => flushTelemetry(telemetry, { signal }));
|
|
137
|
+
}
|
|
138
|
+
if (hasFlushableSinks()) {
|
|
139
|
+
flushes.push((signal) => flushSinks({ signal }));
|
|
140
|
+
}
|
|
141
|
+
if (!flushes.length) return;
|
|
142
|
+
|
|
143
|
+
const deadline = Math.min(
|
|
144
|
+
flushTimeoutMs,
|
|
145
|
+
remainingTimeMs(context) - FLUSH_MARGIN_MS
|
|
146
|
+
);
|
|
147
|
+
if (!(deadline > 0)) return;
|
|
148
|
+
await withDeadline(deadline, (signal) =>
|
|
149
|
+
Promise.allSettled(flushes.map((flush) => flush(signal)))
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Open the logger scope for one invocation, run `fn` inside it, then flush.
|
|
155
|
+
* `fields` (requestId, handlerName, method, route, routeKey, invocation, ...)
|
|
156
|
+
* go into the scope's logger sub-object and never reach the telemetry bus.
|
|
157
|
+
*/
|
|
158
|
+
async function runInvocationScope(fields, fn, opts = {}) {
|
|
159
|
+
const {
|
|
160
|
+
telemetry,
|
|
161
|
+
usageRollup,
|
|
162
|
+
eventSummary,
|
|
163
|
+
shouldUseDatabase = true,
|
|
164
|
+
flushTimeoutMs = DEFAULT_FLUSH_TIMEOUT_MS,
|
|
165
|
+
context,
|
|
166
|
+
} = opts;
|
|
167
|
+
return runInContext({ log: { ...(fields || {}) } }, async () => {
|
|
168
|
+
try {
|
|
169
|
+
return await fn();
|
|
170
|
+
} finally {
|
|
171
|
+
try {
|
|
172
|
+
await flushInvocation({
|
|
173
|
+
telemetry,
|
|
174
|
+
usageRollup,
|
|
175
|
+
eventSummary,
|
|
176
|
+
shouldUseDatabase,
|
|
177
|
+
flushTimeoutMs,
|
|
178
|
+
context,
|
|
179
|
+
});
|
|
180
|
+
} catch (_) {
|
|
181
|
+
// A flush must never change the handler result.
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function toCount(value) {
|
|
188
|
+
const count = Number(value);
|
|
189
|
+
return value !== undefined && value !== null && Number.isFinite(count)
|
|
190
|
+
? count
|
|
191
|
+
: undefined;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Open the logger scope for one SQS message: messageId, receiveCount and the
|
|
196
|
+
* Frigg ids the body carries. No flush; the invocation scope owns that.
|
|
197
|
+
*/
|
|
198
|
+
function runMessageScope(record, fn) {
|
|
199
|
+
const { event, processId, integrationId } = summarizeMessageBody(
|
|
200
|
+
record?.body
|
|
201
|
+
);
|
|
202
|
+
return runInContext(
|
|
203
|
+
{
|
|
204
|
+
log: {
|
|
205
|
+
messageId: record?.messageId,
|
|
206
|
+
receiveCount: toCount(record?.attributes?.ApproximateReceiveCount),
|
|
207
|
+
integrationEvent: event,
|
|
208
|
+
processId,
|
|
209
|
+
integrationId,
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
fn
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
module.exports = {
|
|
217
|
+
runInvocationScope,
|
|
218
|
+
runMessageScope,
|
|
219
|
+
withDeadline,
|
|
220
|
+
flushTelemetry,
|
|
221
|
+
flushUsageRollup,
|
|
222
|
+
FLUSH_MARGIN_MS,
|
|
223
|
+
DEFAULT_FLUSH_TIMEOUT_MS,
|
|
224
|
+
};
|
package/core/secrets-to-env.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
const { getLogger } = require('../logs');
|
|
2
|
+
|
|
3
|
+
const log = getLogger('frigg.core.secrets');
|
|
4
|
+
|
|
1
5
|
const getSecretValue = async () => {
|
|
2
|
-
|
|
6
|
+
log.debug('Fetching secrets', { eventName: 'frigg.core.secrets.fetching' });
|
|
3
7
|
|
|
4
8
|
const httpPort = process.env.PARAMETERS_SECRETS_EXTENSION_HTTP_PORT || 2773;
|
|
5
9
|
const url = `http://localhost:${httpPort}/secretsmanager/get?secretId=${encodeURIComponent(
|
|
@@ -15,10 +19,7 @@ const getSecretValue = async () => {
|
|
|
15
19
|
const response = await fetch(url, options);
|
|
16
20
|
|
|
17
21
|
if (!response.ok) {
|
|
18
|
-
|
|
19
|
-
console.error('Invalid response - response:', JSON.stringify(response));
|
|
20
|
-
console.error('Invalid response - json:', json);
|
|
21
|
-
throw new Error(`Invalid ${response.status} response`);
|
|
22
|
+
throw new Error(`Secrets fetch failed with ${response.status}`);
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
const result = await response.json();
|
|
@@ -44,7 +45,7 @@ const secretsToEnv = async () => {
|
|
|
44
45
|
if (!process.env.SECRET_ARN) {
|
|
45
46
|
return;
|
|
46
47
|
}
|
|
47
|
-
|
|
48
|
+
log.debug('Secrets to env', { eventName: 'frigg.core.secrets.to_env' });
|
|
48
49
|
|
|
49
50
|
try {
|
|
50
51
|
const secrets = await getSecretValue();
|
package/database/config.js
CHANGED
|
@@ -125,16 +125,9 @@ let cachedDbType = null;
|
|
|
125
125
|
*/
|
|
126
126
|
const PRISMA_LOG_LEVEL = process.env.PRISMA_LOG_LEVEL || 'error,warn';
|
|
127
127
|
|
|
128
|
-
/**
|
|
129
|
-
* Enable Prisma query logging for performance monitoring
|
|
130
|
-
* @type {boolean}
|
|
131
|
-
*/
|
|
132
|
-
const PRISMA_QUERY_LOGGING = process.env.PRISMA_QUERY_LOGGING === 'true';
|
|
133
|
-
|
|
134
128
|
module.exports = {
|
|
135
129
|
getDatabaseType, // Export for testing and direct use
|
|
136
130
|
PRISMA_LOG_LEVEL,
|
|
137
|
-
PRISMA_QUERY_LOGGING,
|
|
138
131
|
};
|
|
139
132
|
|
|
140
133
|
/**
|
|
@@ -680,7 +680,11 @@ The encryption system uses **fail-fast error handling**:
|
|
|
680
680
|
|
|
681
681
|
### Logging Configuration
|
|
682
682
|
|
|
683
|
-
|
|
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)
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
const { logger } = require('./logger');
|
|
12
|
+
const { addDeniedKeys } = require('../../logs/redact');
|
|
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
|
+
addDeniedKeys(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
|
+
addDeniedKeys(fields);
|
|
152
155
|
return [...new Set(fields)];
|
|
153
156
|
}
|
|
154
157
|
|
package/errors/fetch-error.js
CHANGED
|
@@ -1,82 +1,65 @@
|
|
|
1
1
|
const { BaseError } = require('./base-error');
|
|
2
|
-
const {
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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 =
|
|
79
|
-
|
|
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,53 @@ 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 { addDeniedKeys } = require('../logs/redact');
|
|
6
|
+
const { getLogger } = require('../logs');
|
|
7
|
+
const {
|
|
8
|
+
extractCredentialFieldsFromModules,
|
|
9
|
+
} = require('../database/encryption/encryption-schema-registry');
|
|
10
|
+
const {
|
|
11
|
+
getModulesDefinitionFromIntegrationClasses,
|
|
12
|
+
} = require('../integrations/utils/map-integration-dto');
|
|
13
|
+
|
|
14
|
+
// Handlers without a database never load the encryption registry, so the
|
|
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
|
+
|
|
29
|
+
function registerCredentialLogKeys(appDefinition, integrations) {
|
|
30
|
+
try {
|
|
31
|
+
const fields = extractCredentialFieldsFromModules(
|
|
32
|
+
getModulesDefinitionFromIntegrationClasses(integrations)
|
|
33
|
+
);
|
|
34
|
+
warnIgnoredKeys(addDeniedKeys(fields));
|
|
35
|
+
} catch {
|
|
36
|
+
// An odd integration shape must not stop the app from loading.
|
|
37
|
+
}
|
|
38
|
+
const schema = appDefinition.encryption?.schema;
|
|
39
|
+
if (schema && typeof schema === 'object') {
|
|
40
|
+
for (const config of Object.values(schema)) {
|
|
41
|
+
warnIgnoredKeys(addDeniedKeys(config?.fields));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
5
45
|
|
|
6
46
|
/**
|
|
7
47
|
* Loads the App definition from the nearest backend package
|
|
8
48
|
* @function loadAppDefinition
|
|
9
49
|
* @description Searches for the nearest backend package.json, loads the corresponding index.js file,
|
|
10
50
|
* 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.
|
|
51
|
+
* @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
52
|
* @throws {Error} Throws error if backend package.json cannot be found.
|
|
13
53
|
* @throws {Error} Throws error if index.js file cannot be found in the backend directory.
|
|
14
54
|
* @example
|
|
@@ -36,8 +76,11 @@ function loadAppDefinition() {
|
|
|
36
76
|
adminScripts = [],
|
|
37
77
|
reports = [],
|
|
38
78
|
admin = {},
|
|
79
|
+
logging = null,
|
|
39
80
|
} = appDefinition;
|
|
40
81
|
|
|
82
|
+
registerCredentialLogKeys(appDefinition, integrations);
|
|
83
|
+
|
|
41
84
|
// Degrade consistently: an invalid telemetry block must never take down a
|
|
42
85
|
// router bundle that loads the app definition at module scope (telemetry is
|
|
43
86
|
// never allowed to break a handler). The singletons apply the same
|
|
@@ -59,7 +102,15 @@ function loadAppDefinition() {
|
|
|
59
102
|
};
|
|
60
103
|
}
|
|
61
104
|
|
|
62
|
-
return {
|
|
105
|
+
return {
|
|
106
|
+
integrations,
|
|
107
|
+
userConfig,
|
|
108
|
+
adminScripts,
|
|
109
|
+
reports,
|
|
110
|
+
admin,
|
|
111
|
+
telemetry,
|
|
112
|
+
logging,
|
|
113
|
+
};
|
|
63
114
|
}
|
|
64
115
|
|
|
65
116
|
module.exports = {
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
const { createHandler
|
|
1
|
+
const { createHandler } = require('@friggframework/core');
|
|
2
|
+
const { getLogger } = require('../logs');
|
|
2
3
|
const express = require('express');
|
|
3
4
|
const bodyParser = require('body-parser');
|
|
4
5
|
const cors = require('cors');
|
|
5
6
|
const Boom = require('@hapi/boom');
|
|
6
7
|
const serverlessHttp = require('serverless-http');
|
|
7
8
|
|
|
9
|
+
const log = getLogger('frigg.http');
|
|
10
|
+
|
|
8
11
|
const createApp = (applyMiddleware) => {
|
|
9
12
|
const app = express();
|
|
10
13
|
|
|
@@ -21,7 +24,7 @@ const createApp = (applyMiddleware) => {
|
|
|
21
24
|
|
|
22
25
|
if (applyMiddleware) applyMiddleware(app);
|
|
23
26
|
|
|
24
|
-
//
|
|
27
|
+
// The express boundary: send the error response and log it one time.
|
|
25
28
|
app.use((err, req, res, next) => {
|
|
26
29
|
const boomError = err.isBoom ? err : Boom.boomify(err);
|
|
27
30
|
const {
|
|
@@ -29,10 +32,19 @@ const createApp = (applyMiddleware) => {
|
|
|
29
32
|
} = boomError;
|
|
30
33
|
|
|
31
34
|
if (statusCode >= 500) {
|
|
32
|
-
|
|
35
|
+
log.error('Request failed', {
|
|
36
|
+
eventName: 'frigg.http.request_failed',
|
|
37
|
+
statusCode,
|
|
38
|
+
error: boomError,
|
|
39
|
+
});
|
|
33
40
|
res.status(statusCode).json({ error: 'Internal Server Error' });
|
|
34
41
|
} else {
|
|
35
|
-
|
|
42
|
+
// A client error needs no stack; the logger scrubs the reason.
|
|
43
|
+
log.warn('Request rejected', {
|
|
44
|
+
eventName: 'frigg.http.request_rejected',
|
|
45
|
+
statusCode,
|
|
46
|
+
reason: boomError.message,
|
|
47
|
+
});
|
|
36
48
|
res.status(statusCode).json({ error: err.message });
|
|
37
49
|
}
|
|
38
50
|
});
|
|
@@ -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
|
-
|
|
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,
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
const { createHandler } = require('@friggframework/core');
|
|
2
2
|
const { createWebsocketConnectionRepository } = require('../../database/websocket-connection-repository-factory');
|
|
3
|
+
const { getLogger } = require('../../logs');
|
|
4
|
+
|
|
5
|
+
const log = getLogger('frigg.websocket');
|
|
3
6
|
|
|
4
7
|
const websocketConnectionRepository = createWebsocketConnectionRepository();
|
|
5
8
|
|
|
@@ -11,10 +14,16 @@ const handleWebSocketConnection = async (event, context) => {
|
|
|
11
14
|
try {
|
|
12
15
|
const connectionId = event.requestContext.connectionId;
|
|
13
16
|
await websocketConnectionRepository.createConnection(connectionId);
|
|
14
|
-
|
|
17
|
+
log.info('Stored new connection', {
|
|
18
|
+
eventName: 'frigg.websocket.connected',
|
|
19
|
+
connectionId,
|
|
20
|
+
});
|
|
15
21
|
return { statusCode: 200, body: 'Connected.' };
|
|
16
22
|
} catch (error) {
|
|
17
|
-
|
|
23
|
+
log.error('Error storing connection', {
|
|
24
|
+
eventName: 'frigg.websocket.connect_failed',
|
|
25
|
+
error,
|
|
26
|
+
});
|
|
18
27
|
return { statusCode: 500, body: 'Error connecting.' };
|
|
19
28
|
}
|
|
20
29
|
|
|
@@ -23,17 +32,28 @@ const handleWebSocketConnection = async (event, context) => {
|
|
|
23
32
|
try {
|
|
24
33
|
const connectionId = event.requestContext.connectionId;
|
|
25
34
|
await websocketConnectionRepository.deleteConnection(connectionId);
|
|
26
|
-
|
|
35
|
+
log.info('Removed connection', {
|
|
36
|
+
eventName: 'frigg.websocket.disconnected',
|
|
37
|
+
connectionId,
|
|
38
|
+
});
|
|
27
39
|
return { statusCode: 200, body: 'Disconnected.' };
|
|
28
40
|
} catch (error) {
|
|
29
|
-
|
|
41
|
+
log.error('Error removing connection', {
|
|
42
|
+
eventName: 'frigg.websocket.disconnect_failed',
|
|
43
|
+
error,
|
|
44
|
+
});
|
|
30
45
|
return { statusCode: 500, body: 'Error disconnecting.' };
|
|
31
46
|
}
|
|
32
47
|
|
|
33
48
|
case 'MESSAGE':
|
|
34
49
|
// Handle incoming message
|
|
35
|
-
|
|
36
|
-
|
|
50
|
+
// Parsed only to keep the old failure on a body that is not JSON.
|
|
51
|
+
JSON.parse(event.body);
|
|
52
|
+
log.info('Received message', {
|
|
53
|
+
eventName: 'frigg.websocket.message_received',
|
|
54
|
+
connectionId: event.requestContext.connectionId,
|
|
55
|
+
bodyLength: event.body?.length ?? 0,
|
|
56
|
+
});
|
|
37
57
|
|
|
38
58
|
// Process the message and send a response
|
|
39
59
|
const responseMessage = { message: 'Message received' };
|