@friggframework/core 2.0.0-next.112 → 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 +51 -12
- 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/encryption/README.md +5 -1
- package/database/encryption/encryption-schema-registry.js +3 -0
- package/database/encryption/logger.js +5 -2
- 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/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
|
@@ -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
|
|
|
@@ -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 };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Fake values, generated at load time so secret scanners find no literal in
|
|
2
|
+
// the source. A fixed seed per name keeps every run identical.
|
|
3
|
+
const MIXED = 'BCDFGHJKLMNPQRSTVWXZbcdfghjkmnpqrstvwxz23456789';
|
|
4
|
+
const HEX = '0123456789abcdef';
|
|
5
|
+
|
|
6
|
+
function seededRandom(name) {
|
|
7
|
+
let seed = [...name].reduce(
|
|
8
|
+
(h, c) => Math.imul(h ^ c.charCodeAt(0), 16777619),
|
|
9
|
+
2166136261
|
|
10
|
+
);
|
|
11
|
+
return () => {
|
|
12
|
+
seed = (seed + 0x6d2b79f5) | 0;
|
|
13
|
+
let t = Math.imul(seed ^ (seed >>> 15), seed | 1);
|
|
14
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
15
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function draw(name, alphabet, length) {
|
|
20
|
+
const next = seededRandom(name);
|
|
21
|
+
let out = '';
|
|
22
|
+
for (let i = 0; i < length; i += 1) {
|
|
23
|
+
out += alphabet[Math.floor(next() * alphabet.length)];
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// The fixed 'Zq' + 'B7k' start puts upper case, lower case and a digit in every token.
|
|
29
|
+
function fakeToken(name, length = 28) {
|
|
30
|
+
return `ZqB7k${draw(name, MIXED, length - 5)}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const base64url = (value) => Buffer.from(value).toString('base64url');
|
|
34
|
+
|
|
35
|
+
const SECRETS = Object.freeze({
|
|
36
|
+
bearer: fakeToken('bearer'),
|
|
37
|
+
apiKeyQuery: fakeToken('apiKeyQuery'),
|
|
38
|
+
friggApiKey: fakeToken('friggApiKey'),
|
|
39
|
+
cookie: fakeToken('cookie'),
|
|
40
|
+
oauthCode: fakeToken('oauthCode'),
|
|
41
|
+
codeVerifier: fakeToken('codeVerifier'),
|
|
42
|
+
password: fakeToken('password'),
|
|
43
|
+
clientSecret: fakeToken('clientSecret'),
|
|
44
|
+
accessToken: fakeToken('accessToken'),
|
|
45
|
+
refreshToken: fakeToken('refreshToken'),
|
|
46
|
+
idToken: fakeToken('idToken'),
|
|
47
|
+
hashword: fakeToken('hashword'),
|
|
48
|
+
dbPassword: fakeToken('dbPassword'),
|
|
49
|
+
signature: fakeToken('signature'),
|
|
50
|
+
jwt: [
|
|
51
|
+
base64url(JSON.stringify({ alg: 'HS256' })),
|
|
52
|
+
base64url(JSON.stringify({ sub: `fake-${draw('jwtSub', MIXED, 8)}` })),
|
|
53
|
+
base64url(draw('jwtSig', MIXED, 24)),
|
|
54
|
+
].join('.'),
|
|
55
|
+
hexToken: draw('hexToken', HEX, 40),
|
|
56
|
+
base64Run: Buffer.from(draw('base64Run', MIXED, 42)).toString('base64'),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
module.exports = { SECRETS, fakeToken };
|