@friggframework/core 2.0.0-next.111 → 2.0.0-next.113
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.json +18 -1
- package/CLAUDE.md +101 -13
- package/README.md +19 -22
- package/core/CLAUDE.md +28 -13
- package/core/Worker.js +32 -37
- package/core/create-handler.js +97 -197
- package/core/index.js +8 -1
- package/core/invocation-scope.js +225 -0
- package/core/secrets-to-env.js +7 -6
- package/database/config.js +0 -7
- package/database/documentdb-encryption-service.js +6 -2
- package/database/documentdb-utils.js +2 -1
- package/database/encryption/README.md +25 -1
- package/database/encryption/encryption-schema-registry.js +3 -0
- package/database/encryption/integration-mapping-encryption.js +97 -0
- package/database/encryption/logger.js +5 -2
- package/database/encryption/prisma-encryption-extension.js +20 -9
- package/errors/fetch-error.js +48 -65
- package/handlers/app-definition-loader.js +38 -2
- package/handlers/app-handler-helpers.js +18 -4
- package/handlers/backend-utils.js +10 -13
- package/handlers/database-migration-handler.js +10 -1
- package/handlers/integration-event-dispatcher.js +2 -2
- package/handlers/routers/integration-webhook-routers.js +13 -6
- package/handlers/routers/websocket.js +27 -7
- package/handlers/workers/db-migration.js +12 -1
- package/handlers/workers/dlq-processor.js +64 -38
- package/index.js +19 -1
- package/integrations/integration-base.js +56 -38
- package/integrations/integration-router.js +21 -20
- package/integrations/repositories/integration-mapping-query-pipeline.js +152 -0
- package/integrations/repositories/integration-mapping-query.js +193 -0
- package/integrations/repositories/integration-mapping-repository-documentdb.js +53 -0
- package/integrations/repositories/integration-mapping-repository-interface.js +48 -0
- package/integrations/repositories/integration-mapping-repository-mongo.js +72 -0
- package/integrations/repositories/integration-mapping-repository-postgres.js +118 -0
- 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 +59 -0
- package/logs/__fixtures__/vectors.js +329 -0
- package/logs/__fixtures__/with-env.js +45 -0
- package/logs/context.js +109 -0
- package/logs/contract-keys.js +67 -0
- package/logs/debug-shims.js +46 -0
- package/logs/denied-keys.js +21 -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 +137 -0
- package/logs/logger.js +134 -42
- package/logs/record.js +276 -0
- package/logs/redact.js +328 -0
- package/logs/serialize.js +329 -0
- package/logs/sinks.js +93 -0
- package/logs/summarize-event.js +163 -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/queue-delivery.js +41 -0
- 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 +10 -42
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
const { createHandler } = require('@friggframework/core');
|
|
2
|
-
const { createWebsocketConnectionRepository } = require('../../
|
|
2
|
+
const { createWebsocketConnectionRepository } = require('../../websocket/repositories/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' };
|
|
@@ -55,6 +55,9 @@ const {
|
|
|
55
55
|
|
|
56
56
|
// Inject prisma-runner as dependency
|
|
57
57
|
const prismaRunner = require('../../database/utils/prisma-runner');
|
|
58
|
+
const { getLogger } = require('../../logs');
|
|
59
|
+
|
|
60
|
+
const log = getLogger('frigg.database.migration');
|
|
58
61
|
|
|
59
62
|
// Use S3 repository for migration status tracking (no User table dependency)
|
|
60
63
|
const bucketName = process.env.S3_BUCKET_NAME || process.env.MIGRATION_STATUS_BUCKET;
|
|
@@ -144,7 +147,6 @@ exports.handler = async (event, context) => {
|
|
|
144
147
|
console.log('========================================');
|
|
145
148
|
console.log('Database Migration Lambda Started');
|
|
146
149
|
console.log('========================================');
|
|
147
|
-
console.log('Event:', JSON.stringify(event, null, 2));
|
|
148
150
|
console.log('Context:', JSON.stringify({
|
|
149
151
|
requestId: context.requestId,
|
|
150
152
|
functionName: context.functionName,
|
|
@@ -157,6 +159,15 @@ exports.handler = async (event, context) => {
|
|
|
157
159
|
// Check for action parameter (direct invocation for status checks)
|
|
158
160
|
const action = event.action || 'migrate'; // Default to migration
|
|
159
161
|
|
|
162
|
+
// targetStage, not stage: the record's own stage field would win.
|
|
163
|
+
log.info('Database migration invoked', {
|
|
164
|
+
eventName: 'frigg.database.migration.invoked',
|
|
165
|
+
migrationId,
|
|
166
|
+
dbType,
|
|
167
|
+
targetStage: stage,
|
|
168
|
+
action,
|
|
169
|
+
});
|
|
170
|
+
|
|
160
171
|
// Handle checkStatus action
|
|
161
172
|
if (action === 'checkStatus') {
|
|
162
173
|
console.log(`\n========================================`);
|
|
@@ -5,6 +5,18 @@
|
|
|
5
5
|
* This handler MUST NOT throw. If it throws, the message goes back to
|
|
6
6
|
* the DLQ and creates an infinite loop. All errors are caught and logged.
|
|
7
7
|
*/
|
|
8
|
+
const crypto = require('node:crypto');
|
|
9
|
+
const { getLogger } = require('../../logs');
|
|
10
|
+
const {
|
|
11
|
+
summarizeLambdaEvent,
|
|
12
|
+
toScopeInvocation,
|
|
13
|
+
} = require('../../logs/summarize-event');
|
|
14
|
+
const {
|
|
15
|
+
runInvocationScope,
|
|
16
|
+
runMessageScope,
|
|
17
|
+
} = require('../../core/invocation-scope');
|
|
18
|
+
|
|
19
|
+
const log = getLogger('frigg.queue.dlq');
|
|
8
20
|
|
|
9
21
|
function extractQueueName(eventSourceARN) {
|
|
10
22
|
if (!eventSourceARN) return 'UNKNOWN';
|
|
@@ -12,50 +24,64 @@ function extractQueueName(eventSourceARN) {
|
|
|
12
24
|
return parts[parts.length - 1] || 'UNKNOWN';
|
|
13
25
|
}
|
|
14
26
|
|
|
15
|
-
|
|
27
|
+
// The body can hold credentials, so only its size and digest are logged.
|
|
28
|
+
function describeBody(body) {
|
|
29
|
+
const text = typeof body === 'string' ? body : String(body ?? '');
|
|
30
|
+
let parsed = true;
|
|
16
31
|
try {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
integrationId: parsed.data?.integrationId || null,
|
|
21
|
-
processId: parsed.data?.processId || null,
|
|
22
|
-
data: parsed.data,
|
|
23
|
-
};
|
|
24
|
-
} catch (error) {
|
|
25
|
-
console.warn('[DLQ] Failed to parse message body', { error: error.message, body });
|
|
26
|
-
return {
|
|
27
|
-
event: 'UNKNOWN',
|
|
28
|
-
integrationId: null,
|
|
29
|
-
processId: null,
|
|
30
|
-
rawBody: body,
|
|
31
|
-
};
|
|
32
|
+
JSON.parse(text);
|
|
33
|
+
} catch {
|
|
34
|
+
parsed = false;
|
|
32
35
|
}
|
|
36
|
+
return {
|
|
37
|
+
parsed,
|
|
38
|
+
bodyLength: Buffer.byteLength(text),
|
|
39
|
+
bodySha256: crypto.createHash('sha256').update(text).digest('hex'),
|
|
40
|
+
};
|
|
33
41
|
}
|
|
34
42
|
|
|
35
|
-
|
|
43
|
+
function logRecord(record) {
|
|
44
|
+
const { parsed, ...body } = describeBody(record.body);
|
|
45
|
+
if (!parsed) {
|
|
46
|
+
log.warn('DLQ message body is not JSON', {
|
|
47
|
+
eventName: 'frigg.queue.dlq.body_unparsed',
|
|
48
|
+
...body,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
// messageId, receiveCount and the body ids come from the message scope.
|
|
52
|
+
log.error('Message reached the DLQ', {
|
|
53
|
+
eventName: 'frigg.queue.dlq.message_failed',
|
|
54
|
+
sentTimestamp: record.attributes?.SentTimestamp,
|
|
55
|
+
sourceQueue: extractQueueName(record.eventSourceARN),
|
|
56
|
+
...body,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function dlqProcessor(event, context) {
|
|
36
61
|
if (!event?.Records?.length) return { batchItemFailures: [] };
|
|
37
62
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
63
|
+
await runInvocationScope(
|
|
64
|
+
{
|
|
65
|
+
requestId: context?.awsRequestId,
|
|
66
|
+
handlerName: 'dlqProcessor',
|
|
67
|
+
invocation: toScopeInvocation(summarizeLambdaEvent(event)),
|
|
68
|
+
},
|
|
69
|
+
async () => {
|
|
70
|
+
for (const record of event.Records) {
|
|
71
|
+
await runMessageScope(record, async () => {
|
|
72
|
+
try {
|
|
73
|
+
logRecord(record);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
log.error('DLQ record failed', {
|
|
76
|
+
eventName: 'frigg.queue.dlq.record_failed',
|
|
77
|
+
error,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
{ shouldUseDatabase: false, context }
|
|
84
|
+
);
|
|
59
85
|
|
|
60
86
|
return { batchItemFailures: [] };
|
|
61
87
|
}
|
package/index.js
CHANGED
|
@@ -11,6 +11,7 @@ const {
|
|
|
11
11
|
Worker,
|
|
12
12
|
loadInstalledModules,
|
|
13
13
|
createHandler,
|
|
14
|
+
runInvocationScope,
|
|
14
15
|
} = require('./core/index');
|
|
15
16
|
const {
|
|
16
17
|
prisma,
|
|
@@ -78,7 +79,17 @@ const {
|
|
|
78
79
|
} = require('./telemetry/index');
|
|
79
80
|
const { createUsageRepository } = require('./usage/index');
|
|
80
81
|
const { TimeoutCatcher } = require('./lambda/index');
|
|
81
|
-
const {
|
|
82
|
+
const {
|
|
83
|
+
getLogger,
|
|
84
|
+
createMemorySink,
|
|
85
|
+
resetLoggerForTests,
|
|
86
|
+
serializeError,
|
|
87
|
+
redactValue,
|
|
88
|
+
toSanitizedSurrogate,
|
|
89
|
+
debug,
|
|
90
|
+
initDebugLog,
|
|
91
|
+
flushDebugLog,
|
|
92
|
+
} = require('./logs/index');
|
|
82
93
|
const {
|
|
83
94
|
Credential,
|
|
84
95
|
Entity,
|
|
@@ -108,6 +119,7 @@ module.exports = {
|
|
|
108
119
|
Worker,
|
|
109
120
|
loadInstalledModules,
|
|
110
121
|
createHandler,
|
|
122
|
+
runInvocationScope,
|
|
111
123
|
|
|
112
124
|
// database
|
|
113
125
|
prisma,
|
|
@@ -175,6 +187,12 @@ module.exports = {
|
|
|
175
187
|
TimeoutCatcher,
|
|
176
188
|
|
|
177
189
|
// logs
|
|
190
|
+
getLogger,
|
|
191
|
+
createMemorySink,
|
|
192
|
+
resetLoggerForTests,
|
|
193
|
+
serializeError,
|
|
194
|
+
redactValue,
|
|
195
|
+
toSanitizedSurrogate,
|
|
178
196
|
debug,
|
|
179
197
|
initDebugLog,
|
|
180
198
|
flushDebugLog,
|
|
@@ -21,6 +21,7 @@ const { validateExtensionBinding } = require('./extension');
|
|
|
21
21
|
const { getTelemetry } = require('../telemetry/telemetry-runtime');
|
|
22
22
|
const { instrumentHandler } = require('../telemetry/instrument-handler');
|
|
23
23
|
const { bindTelemetryContext } = require('../telemetry/bind-telemetry-context');
|
|
24
|
+
const { getLogger } = require('../logs');
|
|
24
25
|
|
|
25
26
|
const constantsToBeMigrated = {
|
|
26
27
|
defaultEvents: {
|
|
@@ -64,6 +65,10 @@ class IntegrationBase {
|
|
|
64
65
|
this.getTelemetryContext()
|
|
65
66
|
);
|
|
66
67
|
|
|
68
|
+
logger = getLogger(
|
|
69
|
+
`integration.${this.constructor.Definition?.name ?? 'unknown'}`
|
|
70
|
+
).child(() => this.getLoggerBindings());
|
|
71
|
+
|
|
67
72
|
static getOptionDetails() {
|
|
68
73
|
const options = new Options({
|
|
69
74
|
module: Object.values(this.Definition.modules)[0], // This is a placeholder until we revamp the frontend
|
|
@@ -254,6 +259,14 @@ class IntegrationBase {
|
|
|
254
259
|
};
|
|
255
260
|
}
|
|
256
261
|
|
|
262
|
+
// Stage and appName are record resource fields; binding them would
|
|
263
|
+
// only land in droppedKeys.
|
|
264
|
+
getLoggerBindings() {
|
|
265
|
+
const { integrationId, integrationType, userId, version } =
|
|
266
|
+
this.getTelemetryContext();
|
|
267
|
+
return { integrationId, integrationType, userId, version };
|
|
268
|
+
}
|
|
269
|
+
|
|
257
270
|
assertHydrated(message = 'Integration instance is not hydrated') {
|
|
258
271
|
if (!this.isHydrated) {
|
|
259
272
|
throw new Error(message);
|
|
@@ -411,15 +424,15 @@ class IntegrationBase {
|
|
|
411
424
|
*/
|
|
412
425
|
async reconcileAuthStatus(authPassed) {
|
|
413
426
|
if (!authPassed) {
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
);
|
|
427
|
+
this.logger.warn('Integration failed to authenticate', {
|
|
428
|
+
eventName: `${this.logger.name}.auth_failed`,
|
|
429
|
+
});
|
|
417
430
|
await this.persistStatus('ERROR');
|
|
418
431
|
}
|
|
419
432
|
if (authPassed && this.status === 'ERROR') {
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
);
|
|
433
|
+
this.logger.info('Auth confirmed, clearing ERROR', {
|
|
434
|
+
eventName: `${this.logger.name}.auth_confirmed`,
|
|
435
|
+
});
|
|
423
436
|
await this.persistStatus('ENABLED');
|
|
424
437
|
}
|
|
425
438
|
}
|
|
@@ -615,12 +628,22 @@ class IntegrationBase {
|
|
|
615
628
|
this[key] = module;
|
|
616
629
|
}
|
|
617
630
|
|
|
631
|
+
// The raw error can echo a request with its credentials, and messages
|
|
632
|
+
// reach end users, so only a fixed text is stored.
|
|
618
633
|
addError(error) {
|
|
619
634
|
if (!this.messages.errors) {
|
|
620
635
|
this.messages.errors = [];
|
|
621
636
|
}
|
|
622
|
-
this.messages.errors.push(
|
|
637
|
+
this.messages.errors.push({
|
|
638
|
+
title: 'Integration Error',
|
|
639
|
+
message: `Integration ${this.id} hit an error. Contact support.`,
|
|
640
|
+
timestamp: Date.now(),
|
|
641
|
+
});
|
|
623
642
|
this.status = 'ERROR';
|
|
643
|
+
this.logger.error('Integration error recorded', {
|
|
644
|
+
eventName: `${this.logger.name}.error_recorded`,
|
|
645
|
+
error,
|
|
646
|
+
});
|
|
624
647
|
}
|
|
625
648
|
|
|
626
649
|
addWarning(warning) {
|
|
@@ -639,9 +662,10 @@ class IntegrationBase {
|
|
|
639
662
|
async persistStatus(status) {
|
|
640
663
|
await this.updateIntegrationStatus.execute(this.id, status);
|
|
641
664
|
this.status = status;
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
665
|
+
this.logger.info('Integration status changed', {
|
|
666
|
+
eventName: `${this.logger.name}.status_changed`,
|
|
667
|
+
integrationStatus: status,
|
|
668
|
+
});
|
|
645
669
|
}
|
|
646
670
|
|
|
647
671
|
/**
|
|
@@ -753,10 +777,14 @@ class IntegrationBase {
|
|
|
753
777
|
// it wins. Warn if the binding tried to wire an override that's now ignored.
|
|
754
778
|
if (this.events[eventName]) {
|
|
755
779
|
if (typeof handlers[eventName] === 'string') {
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
780
|
+
this.logger.warn(
|
|
781
|
+
'Binding handler is ignored: the event is already set',
|
|
782
|
+
{
|
|
783
|
+
eventName: `${this.logger.name}.extension_handler_shadowed`,
|
|
784
|
+
bindingName,
|
|
785
|
+
handler: handlers[eventName],
|
|
786
|
+
event: eventName,
|
|
787
|
+
}
|
|
760
788
|
);
|
|
761
789
|
}
|
|
762
790
|
continue;
|
|
@@ -858,19 +886,12 @@ class IntegrationBase {
|
|
|
858
886
|
if (this.status === 'ERROR') return;
|
|
859
887
|
|
|
860
888
|
const moduleName = notifier?.name;
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
console.log(
|
|
868
|
-
`[Frigg] Module ${
|
|
869
|
-
moduleName || '?'
|
|
870
|
-
} reported invalid credentials for integration ${
|
|
871
|
-
this.id
|
|
872
|
-
} — marking ERROR${detail}`
|
|
873
|
-
);
|
|
889
|
+
this.logger.warn('Module reported invalid credentials, marking ERROR', {
|
|
890
|
+
eventName: `${this.logger.name}.credentials_invalidated`,
|
|
891
|
+
moduleName,
|
|
892
|
+
statusCode: object?.statusCode,
|
|
893
|
+
reason: object?.reason,
|
|
894
|
+
});
|
|
874
895
|
await this._recordCredentialRejection(
|
|
875
896
|
moduleName,
|
|
876
897
|
object?.statusCode
|
|
@@ -881,13 +902,10 @@ class IntegrationBase {
|
|
|
881
902
|
|
|
882
903
|
if (delegateString === 'CREDENTIAL_VALIDATED') {
|
|
883
904
|
if (this.status !== 'ERROR') return;
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
this.id
|
|
889
|
-
} — clearing ERROR → ENABLED`
|
|
890
|
-
);
|
|
905
|
+
this.logger.info('Module reported valid credentials, clearing ERROR', {
|
|
906
|
+
eventName: `${this.logger.name}.credentials_validated`,
|
|
907
|
+
moduleName: notifier?.name,
|
|
908
|
+
});
|
|
891
909
|
await this.persistStatus('ENABLED');
|
|
892
910
|
}
|
|
893
911
|
}
|
|
@@ -909,10 +927,10 @@ class IntegrationBase {
|
|
|
909
927
|
Date.now()
|
|
910
928
|
);
|
|
911
929
|
} catch (error) {
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
error
|
|
915
|
-
);
|
|
930
|
+
this.logger.error('Failed to record credential rejection', {
|
|
931
|
+
eventName: `${this.logger.name}.credential_rejection_record_failed`,
|
|
932
|
+
error,
|
|
933
|
+
});
|
|
916
934
|
}
|
|
917
935
|
}
|
|
918
936
|
}
|
|
@@ -2,6 +2,7 @@ const express = require('express');
|
|
|
2
2
|
const { get } = require('../assertions');
|
|
3
3
|
const Boom = require('@hapi/boom');
|
|
4
4
|
const catchAsyncError = require('express-async-handler');
|
|
5
|
+
const { getLogger } = require('../logs');
|
|
5
6
|
const {
|
|
6
7
|
createIntegrationRepository,
|
|
7
8
|
} = require('./repositories/integration-repository-factory');
|
|
@@ -70,6 +71,8 @@ const {
|
|
|
70
71
|
ProcessAuthorizationCallback,
|
|
71
72
|
} = require('../modules/use-cases/process-authorization-callback');
|
|
72
73
|
|
|
74
|
+
const log = getLogger('frigg.integrations');
|
|
75
|
+
|
|
73
76
|
function createIntegrationRouter() {
|
|
74
77
|
const { integrations: integrationClasses, userConfig } =
|
|
75
78
|
loadAppDefinition();
|
|
@@ -536,29 +539,27 @@ function setEntityRoutes(router, authenticateUser, useCases) {
|
|
|
536
539
|
params.data && typeof params.data === 'object'
|
|
537
540
|
? Object.keys(params.data)
|
|
538
541
|
: [];
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
+
log.debug('Authorize requested', {
|
|
543
|
+
userId,
|
|
544
|
+
entityType: params.entityType,
|
|
545
|
+
dataKeys,
|
|
546
|
+
});
|
|
542
547
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
params.data
|
|
549
|
-
);
|
|
548
|
+
const entityDetails = await processAuthorizationCallback.execute(
|
|
549
|
+
userId,
|
|
550
|
+
params.entityType,
|
|
551
|
+
params.data
|
|
552
|
+
);
|
|
550
553
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
+
log.info('Entity authorized', {
|
|
555
|
+
eventName: 'frigg.integrations.authorized',
|
|
556
|
+
userId,
|
|
557
|
+
entityType: params.entityType,
|
|
558
|
+
credentialId: entityDetails?.credential_id,
|
|
559
|
+
entityId: entityDetails?.entity_id,
|
|
560
|
+
});
|
|
554
561
|
|
|
555
|
-
|
|
556
|
-
} catch (err) {
|
|
557
|
-
console.error(
|
|
558
|
-
`[Frigg] POST /api/authorize failed userId=${userId} entityType=${params.entityType} error=${err?.message || err}`
|
|
559
|
-
);
|
|
560
|
-
throw err;
|
|
561
|
-
}
|
|
562
|
+
res.json(entityDetails);
|
|
562
563
|
})
|
|
563
564
|
);
|
|
564
565
|
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
const SORT_KEY = '__sortKey';
|
|
2
|
+
const MONGO_DIRECTIONS = { asc: 1, desc: -1 };
|
|
3
|
+
const SORT_TYPE_RANKS = [
|
|
4
|
+
['string'],
|
|
5
|
+
['int', 'long', 'double', 'decimal'],
|
|
6
|
+
['bool'],
|
|
7
|
+
['array'],
|
|
8
|
+
['object'],
|
|
9
|
+
];
|
|
10
|
+
const SCALAR_TYPES = SORT_TYPE_RANKS.slice(0, 3).flat();
|
|
11
|
+
|
|
12
|
+
const isType = (expression, type) => ({ $eq: [{ $type: expression }, type] });
|
|
13
|
+
const isAbsent = (expression) => ({
|
|
14
|
+
$eq: [{ $ifNull: [expression, null] }, null],
|
|
15
|
+
});
|
|
16
|
+
const mappingField = (segments) => `$mapping.${segments.join('.')}`;
|
|
17
|
+
|
|
18
|
+
const CONDITION_EXPRESSIONS = {
|
|
19
|
+
exists: (operand) => ({ $ne: [{ $ifNull: [operand, null] }, null] }),
|
|
20
|
+
notExists: (operand) => isAbsent(operand),
|
|
21
|
+
in: (operand, values) => ({
|
|
22
|
+
$and: [
|
|
23
|
+
isType(operand, 'string'),
|
|
24
|
+
{ $in: [operand, { $literal: values }] },
|
|
25
|
+
],
|
|
26
|
+
}),
|
|
27
|
+
notStartsWith: (operand, prefix) => ({
|
|
28
|
+
$cond: [
|
|
29
|
+
isType(operand, 'string'),
|
|
30
|
+
{
|
|
31
|
+
$ne: [
|
|
32
|
+
{ $substrCP: [operand, 0, [...prefix].length] },
|
|
33
|
+
{ $literal: prefix },
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
true,
|
|
37
|
+
],
|
|
38
|
+
}),
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Aggregation stages that answer a validated queryMappings query on the
|
|
43
|
+
* MongoDB wire protocol.
|
|
44
|
+
*
|
|
45
|
+
* @param {*} integrationId - The value IntegrationMapping.integrationId is stored as
|
|
46
|
+
* @param {ReturnType<import('./integration-mapping-query').validateMappingQuery>} query
|
|
47
|
+
* @returns {{match: Object, page: Object[], sort: Object}} `match` selects
|
|
48
|
+
* the matching documents; `page` sorts, skips and limits them; `sort` is
|
|
49
|
+
* the $sort stage inside `page`
|
|
50
|
+
*/
|
|
51
|
+
function buildMappingQueryStages(
|
|
52
|
+
integrationId,
|
|
53
|
+
{ where, orderBy, skip, take, omit }
|
|
54
|
+
) {
|
|
55
|
+
const match = {
|
|
56
|
+
$match: {
|
|
57
|
+
integrationId,
|
|
58
|
+
$expr: {
|
|
59
|
+
$and: [
|
|
60
|
+
isType('$mapping', 'object'),
|
|
61
|
+
...where.map(entryExpression),
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
const sort = { $sort: sortSpec(orderBy) };
|
|
67
|
+
const page = [
|
|
68
|
+
...(orderBy
|
|
69
|
+
? [{ $addFields: { [SORT_KEY]: sortKey(orderBy.path) } }]
|
|
70
|
+
: []),
|
|
71
|
+
...(omit.length > 0 ? [{ $project: omitProjection(omit) }] : []),
|
|
72
|
+
sort,
|
|
73
|
+
...(skip > 0 ? [{ $skip: skip }] : []),
|
|
74
|
+
{ $limit: take },
|
|
75
|
+
];
|
|
76
|
+
return { match, page, sort };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function entryExpression(entry) {
|
|
80
|
+
if (!entry.anyOf) return conditionExpression(entry);
|
|
81
|
+
return { $or: entry.anyOf.map(conditionExpression) };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function conditionExpression({ field, path, op, value }) {
|
|
85
|
+
const operand = field === 'sourceId' ? '$sourceId' : resolvePath(path);
|
|
86
|
+
return CONDITION_EXPRESSIONS[op](operand, value);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The value at a mapping path; null when an enclosing value is not an object.
|
|
91
|
+
*/
|
|
92
|
+
function resolvePath(segments) {
|
|
93
|
+
const enclosing = segments
|
|
94
|
+
.slice(0, -1)
|
|
95
|
+
.map((_, i) =>
|
|
96
|
+
isType(mappingField(segments.slice(0, i + 1)), 'object')
|
|
97
|
+
);
|
|
98
|
+
if (enclosing.length === 0) return mappingField(segments);
|
|
99
|
+
return {
|
|
100
|
+
$cond: [
|
|
101
|
+
enclosing.length === 1 ? enclosing[0] : { $and: enclosing },
|
|
102
|
+
mappingField(segments),
|
|
103
|
+
null,
|
|
104
|
+
],
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Orders values like jsonb: strings < numbers < booleans < arrays < objects,
|
|
110
|
+
* scalars by value within their type. Null and absent values sort last in
|
|
111
|
+
* both directions.
|
|
112
|
+
*/
|
|
113
|
+
function sortKey(path) {
|
|
114
|
+
const type = { $type: '$$value' };
|
|
115
|
+
return {
|
|
116
|
+
$let: {
|
|
117
|
+
vars: { value: resolvePath(path) },
|
|
118
|
+
in: {
|
|
119
|
+
missing: isAbsent('$$value'),
|
|
120
|
+
rank: {
|
|
121
|
+
$switch: {
|
|
122
|
+
branches: SORT_TYPE_RANKS.map((types, i) => ({
|
|
123
|
+
case: { $in: [type, types] },
|
|
124
|
+
then: i + 1,
|
|
125
|
+
})),
|
|
126
|
+
default: 0,
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
value: {
|
|
130
|
+
$cond: [{ $in: [type, SCALAR_TYPES] }, '$$value', null],
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function sortSpec(orderBy) {
|
|
138
|
+
if (!orderBy) return { _id: 1 };
|
|
139
|
+
const direction = MONGO_DIRECTIONS[orderBy.direction];
|
|
140
|
+
return {
|
|
141
|
+
[`${SORT_KEY}.missing`]: 1,
|
|
142
|
+
[`${SORT_KEY}.rank`]: direction,
|
|
143
|
+
[`${SORT_KEY}.value`]: direction,
|
|
144
|
+
_id: direction,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function omitProjection(omit) {
|
|
149
|
+
return Object.fromEntries(omit.map((key) => [`mapping.${key}`, 0]));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
module.exports = { buildMappingQueryStages };
|