@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
|
@@ -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,66 @@ 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
|
+
return {
|
|
31
|
+
bodyLength: Buffer.byteLength(text),
|
|
32
|
+
bodySha256: crypto.createHash('sha256').update(text).digest('hex'),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isJson(body) {
|
|
16
37
|
try {
|
|
17
|
-
|
|
18
|
-
return
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
};
|
|
38
|
+
JSON.parse(body);
|
|
39
|
+
return true;
|
|
40
|
+
} catch {
|
|
41
|
+
return false;
|
|
32
42
|
}
|
|
33
43
|
}
|
|
34
44
|
|
|
35
|
-
|
|
45
|
+
function logRecord(record) {
|
|
46
|
+
const body = describeBody(record.body);
|
|
47
|
+
if (!isJson(record.body)) {
|
|
48
|
+
log.warn('DLQ message body is not JSON', {
|
|
49
|
+
eventName: 'frigg.queue.dlq.body_unparsed',
|
|
50
|
+
...body,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
// messageId, receiveCount and the body ids come from the message scope.
|
|
54
|
+
log.error('Message reached the DLQ', {
|
|
55
|
+
eventName: 'frigg.queue.dlq.message_failed',
|
|
56
|
+
sentTimestamp: record.attributes?.SentTimestamp,
|
|
57
|
+
sourceQueue: extractQueueName(record.eventSourceARN),
|
|
58
|
+
...body,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function dlqProcessor(event, context) {
|
|
36
63
|
if (!event?.Records?.length) return { batchItemFailures: [] };
|
|
37
64
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
65
|
+
await runInvocationScope(
|
|
66
|
+
{
|
|
67
|
+
requestId: context?.awsRequestId,
|
|
68
|
+
handlerName: 'dlqProcessor',
|
|
69
|
+
invocation: toScopeInvocation(summarizeLambdaEvent(event)),
|
|
70
|
+
},
|
|
71
|
+
async () => {
|
|
72
|
+
for (const record of event.Records) {
|
|
73
|
+
await runMessageScope(record, async () => {
|
|
74
|
+
try {
|
|
75
|
+
logRecord(record);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
log.error('DLQ record failed', {
|
|
78
|
+
eventName: 'frigg.queue.dlq.record_failed',
|
|
79
|
+
error,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
{ shouldUseDatabase: false, context }
|
|
86
|
+
);
|
|
59
87
|
|
|
60
88
|
return { batchItemFailures: [] };
|
|
61
89
|
}
|
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 "${handlers[eventName]}" for event "${eventName}" is ignored because this.events["${eventName}"] was 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
|
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
const Boom = require('@hapi/boom');
|
|
2
|
+
const { getLogger } = require('../../logs');
|
|
3
|
+
|
|
4
|
+
const log = getLogger('frigg.integrations');
|
|
2
5
|
// Removed Integration wrapper - using IntegrationBase directly
|
|
3
6
|
|
|
4
7
|
/**
|
|
@@ -65,17 +68,25 @@ class DeleteIntegrationForUser {
|
|
|
65
68
|
);
|
|
66
69
|
modules.push(moduleInstance);
|
|
67
70
|
} catch (error) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
71
|
+
log.warn('Failed to load module for deletion', {
|
|
72
|
+
eventName: 'frigg.integrations.delete_module_load_failed',
|
|
73
|
+
integrationId,
|
|
74
|
+
entityId,
|
|
75
|
+
error,
|
|
76
|
+
});
|
|
72
77
|
failedModuleLoads.push({ entityId, error: error.message });
|
|
73
78
|
}
|
|
74
79
|
}
|
|
75
80
|
|
|
76
81
|
if (failedModuleLoads.length > 0) {
|
|
77
|
-
|
|
78
|
-
|
|
82
|
+
log.warn(
|
|
83
|
+
'Some modules failed to load. Webhooks for these modules may require manual cleanup.',
|
|
84
|
+
{
|
|
85
|
+
eventName: 'frigg.integrations.delete_modules_incomplete',
|
|
86
|
+
integrationId,
|
|
87
|
+
failedCount: failedModuleLoads.length,
|
|
88
|
+
totalCount: integrationRecord.entitiesIds.length,
|
|
89
|
+
}
|
|
79
90
|
);
|
|
80
91
|
}
|
|
81
92
|
|
|
@@ -97,19 +108,19 @@ class DeleteIntegrationForUser {
|
|
|
97
108
|
try {
|
|
98
109
|
await integrationInstance.send('ON_DELETE');
|
|
99
110
|
} catch (error) {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
`[Integration Deletion] onDelete failed for integration ${integrationId}, leaving it IN_DELETION:`,
|
|
103
|
-
reason
|
|
104
|
-
);
|
|
111
|
+
// The error text can echo a request with its credentials and
|
|
112
|
+
// messages reach end users; the boundary logs the cause once.
|
|
105
113
|
await integrationInstance.updateIntegrationMessages.execute(
|
|
106
114
|
integrationId,
|
|
107
115
|
'errors',
|
|
108
116
|
'Integration Deletion Error',
|
|
109
|
-
|
|
117
|
+
'Deletion did not complete. Contact support.',
|
|
110
118
|
Date.now()
|
|
111
119
|
);
|
|
112
|
-
throw
|
|
120
|
+
throw new Error(
|
|
121
|
+
`Integration ${integrationId} deletion did not complete`,
|
|
122
|
+
{ cause: error }
|
|
123
|
+
);
|
|
113
124
|
}
|
|
114
125
|
|
|
115
126
|
await this.integrationRepository.deleteIntegrationById(integrationId);
|
package/jest.config.js
CHANGED
package/logs/.eslintrc.json
CHANGED
|
@@ -1,3 +1,28 @@
|
|
|
1
1
|
{
|
|
2
|
-
"extends": "@friggframework/eslint-config"
|
|
2
|
+
"extends": "@friggframework/eslint-config",
|
|
3
|
+
"rules": {
|
|
4
|
+
"no-console": "error",
|
|
5
|
+
"no-restricted-syntax": [
|
|
6
|
+
"error",
|
|
7
|
+
{
|
|
8
|
+
"selector": "CallExpression[callee.object.object.name='process'][callee.object.property.name=/^(stdout|stderr)$/][callee.property.name='write']",
|
|
9
|
+
"message": "Write through the logger; only logs/sinks.js writes to fd 1"
|
|
10
|
+
}
|
|
11
|
+
]
|
|
12
|
+
},
|
|
13
|
+
"overrides": [
|
|
14
|
+
{
|
|
15
|
+
"files": ["*.test.js", "__fixtures__/**", "jest-logger-setup.js"],
|
|
16
|
+
"rules": {
|
|
17
|
+
"no-console": "off",
|
|
18
|
+
"no-restricted-syntax": "off"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"files": ["sinks.js"],
|
|
23
|
+
"rules": {
|
|
24
|
+
"no-restricted-syntax": "off"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
]
|
|
3
28
|
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
const { SECRETS } = require('./secrets');
|
|
2
|
+
|
|
3
|
+
function httpApiV2Event() {
|
|
4
|
+
return {
|
|
5
|
+
version: '2.0',
|
|
6
|
+
routeKey: 'GET /api/authorize',
|
|
7
|
+
rawPath: '/api/authorize',
|
|
8
|
+
rawQueryString: `code=${SECRETS.oauthCode}&state=abc123&api_key=${SECRETS.apiKeyQuery}`,
|
|
9
|
+
cookies: [`session=${SECRETS.cookie}`],
|
|
10
|
+
headers: {
|
|
11
|
+
'x-frigg-api-key': SECRETS.friggApiKey,
|
|
12
|
+
authorization: `Bearer ${SECRETS.bearer}`,
|
|
13
|
+
cookie: `session=${SECRETS.cookie}`,
|
|
14
|
+
'content-type': 'application/json',
|
|
15
|
+
},
|
|
16
|
+
queryStringParameters: {
|
|
17
|
+
code: SECRETS.oauthCode,
|
|
18
|
+
state: 'abc123',
|
|
19
|
+
api_key: SECRETS.apiKeyQuery,
|
|
20
|
+
},
|
|
21
|
+
requestContext: {
|
|
22
|
+
http: { method: 'GET', path: '/api/authorize', sourceIp: '203.0.113.9' },
|
|
23
|
+
requestId: 'req-v2-1',
|
|
24
|
+
},
|
|
25
|
+
body: JSON.stringify({
|
|
26
|
+
code: SECRETS.oauthCode,
|
|
27
|
+
code_verifier: SECRETS.codeVerifier,
|
|
28
|
+
client_secret: SECRETS.clientSecret,
|
|
29
|
+
}),
|
|
30
|
+
isBase64Encoded: false,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function restV1Event() {
|
|
35
|
+
return {
|
|
36
|
+
resource: '/api/{proxy+}',
|
|
37
|
+
path: '/api/integrations',
|
|
38
|
+
httpMethod: 'POST',
|
|
39
|
+
headers: {
|
|
40
|
+
Authorization: `Basic ${Buffer.from(`user:${SECRETS.password}`).toString('base64')}`,
|
|
41
|
+
'X-Frigg-Api-Key': SECRETS.friggApiKey,
|
|
42
|
+
Cookie: `session=${SECRETS.cookie}`,
|
|
43
|
+
},
|
|
44
|
+
multiValueHeaders: {
|
|
45
|
+
Authorization: [`Basic ${Buffer.from(`user:${SECRETS.password}`).toString('base64')}`],
|
|
46
|
+
'Set-Cookie': [`session=${SECRETS.cookie}`],
|
|
47
|
+
},
|
|
48
|
+
queryStringParameters: { access_token: SECRETS.accessToken },
|
|
49
|
+
multiValueQueryStringParameters: { access_token: [SECRETS.accessToken] },
|
|
50
|
+
requestContext: { requestId: 'req-v1-1', stage: 'dev' },
|
|
51
|
+
body: JSON.stringify({ password: SECRETS.password, refresh_token: SECRETS.refreshToken }),
|
|
52
|
+
isBase64Encoded: false,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function sqsEvent() {
|
|
57
|
+
return {
|
|
58
|
+
Records: [
|
|
59
|
+
{
|
|
60
|
+
messageId: 'msg-1',
|
|
61
|
+
receiptHandle: 'receipt-1',
|
|
62
|
+
attributes: { ApproximateReceiveCount: '1' },
|
|
63
|
+
body: JSON.stringify({
|
|
64
|
+
event: 'PROCESS_BATCH',
|
|
65
|
+
data: {
|
|
66
|
+
processId: 'proc-1',
|
|
67
|
+
integrationId: 'int-1',
|
|
68
|
+
access_token: SECRETS.accessToken,
|
|
69
|
+
credentials: { client_secret: SECRETS.clientSecret },
|
|
70
|
+
},
|
|
71
|
+
}),
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
messageId: 'msg-2',
|
|
75
|
+
attributes: { ApproximateReceiveCount: '3' },
|
|
76
|
+
body: `not json Authorization: Bearer ${SECRETS.bearer}`,
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module.exports = { httpApiV2Event, restV1Event, sqsEvent };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const WINDOW = 8;
|
|
2
|
+
|
|
3
|
+
function forms(secret) {
|
|
4
|
+
return [
|
|
5
|
+
secret,
|
|
6
|
+
encodeURIComponent(secret),
|
|
7
|
+
Buffer.from(secret).toString('base64'),
|
|
8
|
+
];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function windows(value) {
|
|
12
|
+
if (value.length <= WINDOW) return [value];
|
|
13
|
+
const result = [];
|
|
14
|
+
for (let i = 0; i + WINDOW <= value.length; i += 1) {
|
|
15
|
+
result.push(value.slice(i, i + WINDOW));
|
|
16
|
+
}
|
|
17
|
+
return result;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function findSecretWindow(received, secrets) {
|
|
21
|
+
const haystack =
|
|
22
|
+
typeof received === 'string' ? received : JSON.stringify(received);
|
|
23
|
+
for (const secret of secrets) {
|
|
24
|
+
for (const form of forms(secret)) {
|
|
25
|
+
for (const w of windows(form)) {
|
|
26
|
+
if (haystack.includes(w)) return { secret, window: w };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function toContainNoSecretWindow(received, secrets) {
|
|
34
|
+
const list = Array.isArray(secrets) ? secrets : Object.values(secrets);
|
|
35
|
+
const hit = findSecretWindow(received, list);
|
|
36
|
+
return {
|
|
37
|
+
pass: hit === null,
|
|
38
|
+
message: () =>
|
|
39
|
+
hit
|
|
40
|
+
? `expected no ${WINDOW}-char window of a secret, found "${hit.window}" (from a secret of length ${hit.secret.length})`
|
|
41
|
+
: 'expected a secret window, found none',
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
module.exports = { toContainNoSecretWindow, findSecretWindow, WINDOW };
|