latchkey 2.7.3 → 2.9.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/README.md +55 -5
- package/dist/scripts/cryptFile.js +2 -2
- package/dist/scripts/cryptFile.js.map +1 -1
- package/dist/scripts/recordBrowserSession.js +3 -2
- package/dist/scripts/recordBrowserSession.js.map +1 -1
- package/dist/src/cli.js +5 -4
- package/dist/src/cli.js.map +1 -1
- package/dist/src/cliCommands.d.ts +1 -1
- package/dist/src/cliCommands.d.ts.map +1 -1
- package/dist/src/cliCommands.js +44 -6
- package/dist/src/cliCommands.js.map +1 -1
- package/dist/src/config.d.ts +34 -0
- package/dist/src/config.d.ts.map +1 -1
- package/dist/src/config.js +53 -0
- package/dist/src/config.js.map +1 -1
- package/dist/src/curlInjection.d.ts +1 -1
- package/dist/src/curlInjection.d.ts.map +1 -1
- package/dist/src/curlInjection.js +16 -1
- package/dist/src/curlInjection.js.map +1 -1
- package/dist/src/encryptedStorage.d.ts +9 -25
- package/dist/src/encryptedStorage.d.ts.map +1 -1
- package/dist/src/encryptedStorage.js +9 -52
- package/dist/src/encryptedStorage.js.map +1 -1
- package/dist/src/encryption.d.ts +45 -0
- package/dist/src/encryption.d.ts.map +1 -1
- package/dist/src/encryption.js +69 -0
- package/dist/src/encryption.js.map +1 -1
- package/dist/src/gateway/client.d.ts +12 -2
- package/dist/src/gateway/client.d.ts.map +1 -1
- package/dist/src/gateway/client.js +31 -4
- package/dist/src/gateway/client.js.map +1 -1
- package/dist/src/gateway/extensions.d.ts +59 -0
- package/dist/src/gateway/extensions.d.ts.map +1 -0
- package/dist/src/gateway/extensions.js +170 -0
- package/dist/src/gateway/extensions.js.map +1 -0
- package/dist/src/gateway/gatewayEndpoint.d.ts +22 -1
- package/dist/src/gateway/gatewayEndpoint.d.ts.map +1 -1
- package/dist/src/gateway/gatewayEndpoint.js +52 -15
- package/dist/src/gateway/gatewayEndpoint.js.map +1 -1
- package/dist/src/gateway/password.d.ts +16 -0
- package/dist/src/gateway/password.d.ts.map +1 -0
- package/dist/src/gateway/password.js +24 -0
- package/dist/src/gateway/password.js.map +1 -0
- package/dist/src/gateway/permissionsOverride.d.ts +65 -0
- package/dist/src/gateway/permissionsOverride.d.ts.map +1 -0
- package/dist/src/gateway/permissionsOverride.js +171 -0
- package/dist/src/gateway/permissionsOverride.js.map +1 -0
- package/dist/src/gateway/server.d.ts.map +1 -1
- package/dist/src/gateway/server.js +100 -15
- package/dist/src/gateway/server.js.map +1 -1
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/oauthUtils.d.ts +11 -2
- package/dist/src/oauthUtils.d.ts.map +1 -1
- package/dist/src/oauthUtils.js +25 -4
- package/dist/src/oauthUtils.js.map +1 -1
- package/dist/src/permissions.d.ts +3 -6
- package/dist/src/permissions.d.ts.map +1 -1
- package/dist/src/permissions.js +6 -13
- package/dist/src/permissions.js.map +1 -1
- package/dist/src/serviceRegistry.d.ts.map +1 -1
- package/dist/src/serviceRegistry.js +2 -1
- package/dist/src/serviceRegistry.js.map +1 -1
- package/dist/src/services/index.d.ts +1 -0
- package/dist/src/services/index.d.ts.map +1 -1
- package/dist/src/services/index.js +1 -0
- package/dist/src/services/index.js.map +1 -1
- package/dist/src/services/notion-mcp.d.ts +29 -0
- package/dist/src/services/notion-mcp.d.ts.map +1 -0
- package/dist/src/services/notion-mcp.js +156 -0
- package/dist/src/services/notion-mcp.js.map +1 -0
- package/dist/src/services/notion.d.ts.map +1 -1
- package/dist/src/services/notion.js +3 -2
- package/dist/src/services/notion.js.map +1 -1
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/dist/tests/apiCredentialStore.test.js +2 -2
- package/dist/tests/apiCredentialStore.test.js.map +1 -1
- package/dist/tests/cli.test.js +98 -53
- package/dist/tests/cli.test.js.map +1 -1
- package/dist/tests/config.test.js +37 -0
- package/dist/tests/config.test.js.map +1 -1
- package/dist/tests/encryptedStorage.test.js +19 -39
- package/dist/tests/encryptedStorage.test.js.map +1 -1
- package/dist/tests/gateway.test.js +184 -7
- package/dist/tests/gateway.test.js.map +1 -1
- package/dist/tests/gatewayClient.test.js +74 -0
- package/dist/tests/gatewayClient.test.js.map +1 -1
- package/dist/tests/gatewayExtensions.test.d.ts +2 -0
- package/dist/tests/gatewayExtensions.test.d.ts.map +1 -0
- package/dist/tests/gatewayExtensions.test.js +398 -0
- package/dist/tests/gatewayExtensions.test.js.map +1 -0
- package/dist/tests/latchkeyEndpoint.test.js +7 -6
- package/dist/tests/latchkeyEndpoint.test.js.map +1 -1
- package/dist/tests/migrations.test.js +2 -2
- package/dist/tests/migrations.test.js.map +1 -1
- package/dist/tests/oauthUtils.test.d.ts +2 -0
- package/dist/tests/oauthUtils.test.d.ts.map +1 -0
- package/dist/tests/oauthUtils.test.js +63 -0
- package/dist/tests/oauthUtils.test.js.map +1 -0
- package/dist/tests/permissions.test.js +14 -10
- package/dist/tests/permissions.test.js.map +1 -1
- package/dist/tests/permissionsOverride.test.d.ts +2 -0
- package/dist/tests/permissionsOverride.test.d.ts.map +1 -0
- package/dist/tests/permissionsOverride.test.js +136 -0
- package/dist/tests/permissionsOverride.test.js.map +1 -0
- package/dist/tests/resolveEncryptionKey.test.d.ts +2 -0
- package/dist/tests/resolveEncryptionKey.test.d.ts.map +1 -0
- package/dist/tests/resolveEncryptionKey.test.js +26 -0
- package/dist/tests/resolveEncryptionKey.test.js.map +1 -0
- package/dist/tests/sharedOperations.test.js +34 -50
- package/dist/tests/sharedOperations.test.js.map +1 -1
- package/package.json +2 -2
- package/dist/tests/encryptedStorageKeyGeneration.test.d.ts +0 -2
- package/dist/tests/encryptedStorageKeyGeneration.test.d.ts.map +0 -1
- package/dist/tests/encryptedStorageKeyGeneration.test.js +0 -22
- package/dist/tests/encryptedStorageKeyGeneration.test.js.map +0 -1
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
2
|
+
import { startOAuthCallbackServer, generateCodeVerifier, generateCodeChallenge, exchangeCodeForTokens, refreshAccessToken, OAuthTokenExchangeError, OAuthCallbackServerTimeoutError, } from '../src/oauthUtils.js';
|
|
3
|
+
import * as curl from '../src/curl.js';
|
|
4
|
+
afterEach(() => {
|
|
5
|
+
vi.restoreAllMocks();
|
|
6
|
+
});
|
|
7
|
+
void startOAuthCallbackServer;
|
|
8
|
+
void OAuthTokenExchangeError;
|
|
9
|
+
void OAuthCallbackServerTimeoutError;
|
|
10
|
+
describe('startOAuthCallbackServer', () => {
|
|
11
|
+
it.todo('add tests');
|
|
12
|
+
});
|
|
13
|
+
describe('generateCodeVerifier', () => {
|
|
14
|
+
it('just runs', () => {
|
|
15
|
+
generateCodeVerifier();
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
describe('generateCodeChallenge', () => {
|
|
19
|
+
it('just runs', () => {
|
|
20
|
+
generateCodeChallenge('dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk');
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
describe('exchangeCodeForTokens', () => {
|
|
24
|
+
it('with PKCE: includes code_verifier in body', () => {
|
|
25
|
+
const spy = vi.spyOn(curl, 'runCaptured').mockImplementation(() => {
|
|
26
|
+
throw new Error('STOP');
|
|
27
|
+
});
|
|
28
|
+
expect(() => exchangeCodeForTokens('https://api.notion.com/v1/oauth/token', 'auth-code-abc123', 'test-client-id', 'test-client-secret', 'http://localhost:12345/oauth2callback', 'dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk')).toThrow('STOP');
|
|
29
|
+
const args = spy.mock.calls[0][0];
|
|
30
|
+
const body = args[args.indexOf('-d') + 1];
|
|
31
|
+
expect(body).toBe('code=auth-code-abc123&client_id=test-client-id&redirect_uri=http%3A%2F%2Flocalhost%3A12345%2Foauth2callback&grant_type=authorization_code&client_secret=test-client-secret&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk');
|
|
32
|
+
});
|
|
33
|
+
it('without PKCE: omits code_verifier from body', () => {
|
|
34
|
+
const spy = vi.spyOn(curl, 'runCaptured').mockImplementation(() => {
|
|
35
|
+
throw new Error('STOP');
|
|
36
|
+
});
|
|
37
|
+
expect(() => exchangeCodeForTokens('https://api.notion.com/v1/oauth/token', 'auth-code-abc123', 'test-client-id', 'test-client-secret', 'http://localhost:12345/oauth2callback')).toThrow('STOP');
|
|
38
|
+
const args = spy.mock.calls[0][0];
|
|
39
|
+
const body = args[args.indexOf('-d') + 1];
|
|
40
|
+
expect(body).toBe('code=auth-code-abc123&client_id=test-client-id&redirect_uri=http%3A%2F%2Flocalhost%3A12345%2Foauth2callback&grant_type=authorization_code&client_secret=test-client-secret');
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
describe('refreshAccessToken', () => {
|
|
44
|
+
it('confidential client: includes client_secret in body', () => {
|
|
45
|
+
const spy = vi.spyOn(curl, 'runCaptured').mockImplementation(() => {
|
|
46
|
+
throw new Error('STOP');
|
|
47
|
+
});
|
|
48
|
+
expect(() => refreshAccessToken('https://api.notion.com/v1/oauth/token', 'refresh-token-xyz789', 'test-client-id', 'test-client-secret')).toThrow('STOP');
|
|
49
|
+
const args = spy.mock.calls[0][0];
|
|
50
|
+
const body = args[args.indexOf('-d') + 1];
|
|
51
|
+
expect(body).toBe('refresh_token=refresh-token-xyz789&client_id=test-client-id&grant_type=refresh_token&client_secret=test-client-secret');
|
|
52
|
+
});
|
|
53
|
+
it('public client: omits client_secret from body', () => {
|
|
54
|
+
const spy = vi.spyOn(curl, 'runCaptured').mockImplementation(() => {
|
|
55
|
+
throw new Error('STOP');
|
|
56
|
+
});
|
|
57
|
+
expect(() => refreshAccessToken('https://api.notion.com/v1/oauth/token', 'refresh-token-xyz789', 'test-client-id', '')).toThrow('STOP');
|
|
58
|
+
const args = spy.mock.calls[0][0];
|
|
59
|
+
const body = args[args.indexOf('-d') + 1];
|
|
60
|
+
expect(body).toBe('refresh_token=refresh-token-xyz789&client_id=test-client-id&grant_type=refresh_token');
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
//# sourceMappingURL=oauthUtils.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauthUtils.test.js","sourceRoot":"","sources":["../../tests/oauthUtils.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -2,7 +2,11 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
|
2
2
|
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
|
+
import { parseCurlArgs } from '@imbue-ai/detent';
|
|
5
6
|
import { checkPermission, PermissionCheckError } from '../src/permissions.js';
|
|
7
|
+
function requestFromCurl(args) {
|
|
8
|
+
return parseCurlArgs(args);
|
|
9
|
+
}
|
|
6
10
|
describe('checkPermission', () => {
|
|
7
11
|
let tempDir;
|
|
8
12
|
beforeEach(() => {
|
|
@@ -13,7 +17,7 @@ describe('checkPermission', () => {
|
|
|
13
17
|
});
|
|
14
18
|
it('should allow requests when no config file exists', async () => {
|
|
15
19
|
const configPath = join(tempDir, 'nonexistent', 'permissions.json');
|
|
16
|
-
const result = await checkPermission(['-X', 'GET', 'https://api.example.com/anything'], configPath);
|
|
20
|
+
const result = await checkPermission(requestFromCurl(['-X', 'GET', 'https://api.example.com/anything']), configPath);
|
|
17
21
|
expect(result).toBe(true);
|
|
18
22
|
});
|
|
19
23
|
it('should allow requests matching a permission rule', async () => {
|
|
@@ -35,7 +39,7 @@ describe('checkPermission', () => {
|
|
|
35
39
|
},
|
|
36
40
|
rules: [{ 'example-api': ['example-read'] }],
|
|
37
41
|
}));
|
|
38
|
-
const result = await checkPermission(['https://api.example.com/users'], configPath);
|
|
42
|
+
const result = await checkPermission(requestFromCurl(['https://api.example.com/users']), configPath);
|
|
39
43
|
expect(result).toBe(true);
|
|
40
44
|
});
|
|
41
45
|
it('should deny requests not matching any permission rule', async () => {
|
|
@@ -57,7 +61,7 @@ describe('checkPermission', () => {
|
|
|
57
61
|
},
|
|
58
62
|
rules: [{ 'example-api': ['example-read'] }],
|
|
59
63
|
}));
|
|
60
|
-
const result = await checkPermission(['-X', 'POST', 'https://api.example.com/users'], configPath);
|
|
64
|
+
const result = await checkPermission(requestFromCurl(['-X', 'POST', 'https://api.example.com/users']), configPath);
|
|
61
65
|
expect(result).toBe(false);
|
|
62
66
|
});
|
|
63
67
|
it('should deny requests to unrecognized domains when rules exist', async () => {
|
|
@@ -79,20 +83,20 @@ describe('checkPermission', () => {
|
|
|
79
83
|
},
|
|
80
84
|
rules: [{ 'example-api': ['example-read'] }],
|
|
81
85
|
}));
|
|
82
|
-
const result = await checkPermission(['https://api.other.com/something'], configPath);
|
|
86
|
+
const result = await checkPermission(requestFromCurl(['https://api.other.com/something']), configPath);
|
|
83
87
|
expect(result).toBe(false);
|
|
84
88
|
});
|
|
85
89
|
it('should throw PermissionCheckError for invalid config files', async () => {
|
|
86
90
|
const configPath = join(tempDir, 'permissions.json');
|
|
87
91
|
writeFileSync(configPath, 'not valid json');
|
|
88
|
-
await expect(checkPermission(['https://api.example.com/anything'], configPath)).rejects.toThrow(PermissionCheckError);
|
|
92
|
+
await expect(checkPermission(requestFromCurl(['https://api.example.com/anything']), configPath)).rejects.toThrow(PermissionCheckError);
|
|
89
93
|
});
|
|
90
94
|
it('should accept URLs without a scheme (defaulting to http://) when rules allow them', async () => {
|
|
91
95
|
const configPath = join(tempDir, 'permissions.json');
|
|
92
96
|
writeFileSync(configPath, JSON.stringify({
|
|
93
97
|
rules: [{ any: ['any'] }],
|
|
94
98
|
}));
|
|
95
|
-
const result = await checkPermission(['www.seznam.cz'], configPath);
|
|
99
|
+
const result = await checkPermission(requestFromCurl(['www.seznam.cz']), configPath);
|
|
96
100
|
expect(result).toBe(true);
|
|
97
101
|
});
|
|
98
102
|
it('should throw PermissionCheckError when a rule references an unknown schema', async () => {
|
|
@@ -100,15 +104,15 @@ describe('checkPermission', () => {
|
|
|
100
104
|
writeFileSync(configPath, JSON.stringify({
|
|
101
105
|
rules: [{ 'non-existent-schema': ['any'] }],
|
|
102
106
|
}));
|
|
103
|
-
await expect(checkPermission(['https://api.example.com/anything'], configPath, true)).rejects.toThrow(PermissionCheckError);
|
|
107
|
+
await expect(checkPermission(requestFromCurl(['https://api.example.com/anything']), configPath, true)).rejects.toThrow(PermissionCheckError);
|
|
104
108
|
});
|
|
105
109
|
it('should allow all requests with the any/any rule', async () => {
|
|
106
110
|
const configPath = join(tempDir, 'permissions.json');
|
|
107
111
|
writeFileSync(configPath, JSON.stringify({
|
|
108
112
|
rules: [{ any: ['any'] }],
|
|
109
113
|
}));
|
|
110
|
-
const resultGet = await checkPermission(['https://api.example.com/anything'], configPath);
|
|
111
|
-
const resultPost = await checkPermission(['-X', 'POST', '-d', '{"key":"value"}', 'https://api.other.com/resource'], configPath);
|
|
114
|
+
const resultGet = await checkPermission(requestFromCurl(['https://api.example.com/anything']), configPath);
|
|
115
|
+
const resultPost = await checkPermission(requestFromCurl(['-X', 'POST', '-d', '{"key":"value"}', 'https://api.other.com/resource']), configPath);
|
|
112
116
|
expect(resultGet).toBe(true);
|
|
113
117
|
expect(resultPost).toBe(true);
|
|
114
118
|
});
|
|
@@ -117,7 +121,7 @@ describe('checkPermission', () => {
|
|
|
117
121
|
writeFileSync(configPath, JSON.stringify({
|
|
118
122
|
rules: [],
|
|
119
123
|
}));
|
|
120
|
-
const result = await checkPermission(['https://api.example.com/anything'], configPath);
|
|
124
|
+
const result = await checkPermission(requestFromCurl(['https://api.example.com/anything']), configPath);
|
|
121
125
|
expect(result).toBe(false);
|
|
122
126
|
});
|
|
123
127
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"permissions.test.js","sourceRoot":"","sources":["../../tests/permissions.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AACrE,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAC7D,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,eAAe,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAE9E,QAAQ,CAAC,iBAAiB,EAAE,GAAG,EAAE;IAC/B,IAAI,OAAe,CAAC;IAEpB,UAAU,CAAC,GAAG,EAAE;QACd,OAAO,GAAG,WAAW,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,4BAA4B,CAAC,CAAC,CAAC;IACtE,CAAC,CAAC,CAAC;IAEH,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IACpD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,kDAAkD,EAAE,KAAK,IAAI,EAAE;QAChE,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,EAAE,aAAa,EAAE,kBAAkB,CAAC,CAAC;QAEpE,MAAM,MAAM,GAAG,MAAM,eAAe,CAClC,CAAC,IAAI,EAAE,KAAK,EAAE,kCAAkC,CAAC,
|
|
1
|
+
{"version":3,"file":"permissions.test.js","sourceRoot":"","sources":["../../tests/permissions.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"permissionsOverride.test.d.ts","sourceRoot":"","sources":["../../tests/permissionsOverride.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { createHmac } from 'node:crypto';
|
|
3
|
+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { createPermissionsOverrideJwt, derivePermissionsOverrideSigningKey, InvalidPermissionsOverrideError, PERMISSIONS_OVERRIDE_HEADER, PermissionsOverrideFileMissingError, resolvePermissionsOverride, verifyPermissionsOverrideJwt, } from '../src/gateway/permissionsOverride.js';
|
|
7
|
+
const TEST_ENCRYPTION_KEY = 'dGVzdGtleXRlc3RrZXl0ZXN0a2V5dGVzdGtleXRlc3Q=';
|
|
8
|
+
const OTHER_ENCRYPTION_KEY = 'b3RoZXJrZXlvdGhlcmtleW90aGVya2V5b3RoZXJrZXk=';
|
|
9
|
+
describe('PERMISSIONS_OVERRIDE_HEADER', () => {
|
|
10
|
+
it('is the lowercase header name', () => {
|
|
11
|
+
expect(PERMISSIONS_OVERRIDE_HEADER).toBe('x-latchkey-gateway-permissions-override');
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
describe('derivePermissionsOverrideSigningKey', () => {
|
|
15
|
+
it('produces a deterministic 32-byte key', () => {
|
|
16
|
+
const a = derivePermissionsOverrideSigningKey(TEST_ENCRYPTION_KEY);
|
|
17
|
+
const b = derivePermissionsOverrideSigningKey(TEST_ENCRYPTION_KEY);
|
|
18
|
+
expect(a.length).toBe(32);
|
|
19
|
+
expect(a.equals(b)).toBe(true);
|
|
20
|
+
});
|
|
21
|
+
it('produces different keys for different master keys', () => {
|
|
22
|
+
const a = derivePermissionsOverrideSigningKey(TEST_ENCRYPTION_KEY);
|
|
23
|
+
const b = derivePermissionsOverrideSigningKey(OTHER_ENCRYPTION_KEY);
|
|
24
|
+
expect(a.equals(b)).toBe(false);
|
|
25
|
+
});
|
|
26
|
+
it('does not equal the encryption key itself', () => {
|
|
27
|
+
const derived = derivePermissionsOverrideSigningKey(TEST_ENCRYPTION_KEY);
|
|
28
|
+
const master = Buffer.from(TEST_ENCRYPTION_KEY, 'base64');
|
|
29
|
+
expect(derived.equals(master)).toBe(false);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
describe('createPermissionsOverrideJwt / verifyPermissionsOverrideJwt', () => {
|
|
33
|
+
const signingKey = derivePermissionsOverrideSigningKey(TEST_ENCRYPTION_KEY);
|
|
34
|
+
it('round-trips an absolute path', () => {
|
|
35
|
+
const token = createPermissionsOverrideJwt('/etc/latchkey/permissions.json', signingKey);
|
|
36
|
+
const payload = verifyPermissionsOverrideJwt(token, signingKey);
|
|
37
|
+
expect(payload).toEqual({ permissionsConfig: '/etc/latchkey/permissions.json' });
|
|
38
|
+
});
|
|
39
|
+
it('produces a three-segment JWT', () => {
|
|
40
|
+
const token = createPermissionsOverrideJwt('/x.json', signingKey);
|
|
41
|
+
expect(token.split('.')).toHaveLength(3);
|
|
42
|
+
});
|
|
43
|
+
it('uses HS256/JWT in the header', () => {
|
|
44
|
+
const token = createPermissionsOverrideJwt('/x.json', signingKey);
|
|
45
|
+
const header = token.split('.')[0];
|
|
46
|
+
const json = Buffer.from(header, 'base64url').toString('utf-8');
|
|
47
|
+
expect(JSON.parse(json)).toEqual({ alg: 'HS256', typ: 'JWT' });
|
|
48
|
+
});
|
|
49
|
+
it('payload contains only the permissionsConfig field', () => {
|
|
50
|
+
const token = createPermissionsOverrideJwt('/x.json', signingKey);
|
|
51
|
+
const payload = token.split('.')[1];
|
|
52
|
+
const json = Buffer.from(payload, 'base64url').toString('utf-8');
|
|
53
|
+
expect(JSON.parse(json)).toEqual({ permissionsConfig: '/x.json' });
|
|
54
|
+
});
|
|
55
|
+
it('rejects creation for non-absolute paths', () => {
|
|
56
|
+
expect(() => createPermissionsOverrideJwt('relative/path.json', signingKey)).toThrow(InvalidPermissionsOverrideError);
|
|
57
|
+
});
|
|
58
|
+
it('rejects tokens signed with a different key', () => {
|
|
59
|
+
const otherKey = derivePermissionsOverrideSigningKey(OTHER_ENCRYPTION_KEY);
|
|
60
|
+
const token = createPermissionsOverrideJwt('/x.json', otherKey);
|
|
61
|
+
expect(() => verifyPermissionsOverrideJwt(token, signingKey)).toThrow(InvalidPermissionsOverrideError);
|
|
62
|
+
});
|
|
63
|
+
it('rejects tokens with a tampered payload', () => {
|
|
64
|
+
const token = createPermissionsOverrideJwt('/x.json', signingKey);
|
|
65
|
+
const [header, , signature] = token.split('.');
|
|
66
|
+
const tamperedPayload = Buffer.from(JSON.stringify({ permissionsConfig: '/y.json' }), 'utf-8').toString('base64url');
|
|
67
|
+
const tampered = `${header}.${tamperedPayload}.${signature}`;
|
|
68
|
+
expect(() => verifyPermissionsOverrideJwt(tampered, signingKey)).toThrow(InvalidPermissionsOverrideError);
|
|
69
|
+
});
|
|
70
|
+
it('rejects tokens that do not have three segments', () => {
|
|
71
|
+
expect(() => verifyPermissionsOverrideJwt('a.b', signingKey)).toThrow(InvalidPermissionsOverrideError);
|
|
72
|
+
expect(() => verifyPermissionsOverrideJwt('a.b.c.d', signingKey)).toThrow(InvalidPermissionsOverrideError);
|
|
73
|
+
});
|
|
74
|
+
function base64Url(value) {
|
|
75
|
+
return Buffer.from(value, 'utf-8').toString('base64url');
|
|
76
|
+
}
|
|
77
|
+
function buildSignedToken(headerJson, payloadJson) {
|
|
78
|
+
const headerSegment = base64Url(headerJson);
|
|
79
|
+
const payloadSegment = base64Url(payloadJson);
|
|
80
|
+
const signature = createHmac('sha256', signingKey)
|
|
81
|
+
.update(`${headerSegment}.${payloadSegment}`)
|
|
82
|
+
.digest('base64url');
|
|
83
|
+
return `${headerSegment}.${payloadSegment}.${signature}`;
|
|
84
|
+
}
|
|
85
|
+
it('rejects tokens whose payload is not valid JSON', () => {
|
|
86
|
+
// Sign a payload that is base64url of "not-json" so we hit the JSON parse
|
|
87
|
+
// error rather than the signature mismatch error first.
|
|
88
|
+
const headerSegment = base64Url(JSON.stringify({ alg: 'HS256', typ: 'JWT' }));
|
|
89
|
+
const payloadSegment = base64Url('not-json');
|
|
90
|
+
const signature = createHmac('sha256', signingKey)
|
|
91
|
+
.update(`${headerSegment}.${payloadSegment}`)
|
|
92
|
+
.digest('base64url');
|
|
93
|
+
const token = `${headerSegment}.${payloadSegment}.${signature}`;
|
|
94
|
+
expect(() => verifyPermissionsOverrideJwt(token, signingKey)).toThrow(InvalidPermissionsOverrideError);
|
|
95
|
+
});
|
|
96
|
+
it('rejects payloads without permissionsConfig', () => {
|
|
97
|
+
const token = buildSignedToken(JSON.stringify({ alg: 'HS256', typ: 'JWT' }), JSON.stringify({ other: '/x.json' }));
|
|
98
|
+
expect(() => verifyPermissionsOverrideJwt(token, signingKey)).toThrow(InvalidPermissionsOverrideError);
|
|
99
|
+
});
|
|
100
|
+
it('rejects payloads whose permissionsConfig is not absolute', () => {
|
|
101
|
+
const token = buildSignedToken(JSON.stringify({ alg: 'HS256', typ: 'JWT' }), JSON.stringify({ permissionsConfig: 'relative.json' }));
|
|
102
|
+
expect(() => verifyPermissionsOverrideJwt(token, signingKey)).toThrow(InvalidPermissionsOverrideError);
|
|
103
|
+
});
|
|
104
|
+
it('rejects headers with the wrong algorithm', () => {
|
|
105
|
+
const token = buildSignedToken(JSON.stringify({ alg: 'none', typ: 'JWT' }), JSON.stringify({ permissionsConfig: '/x.json' }));
|
|
106
|
+
expect(() => verifyPermissionsOverrideJwt(token, signingKey)).toThrow(InvalidPermissionsOverrideError);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
describe('resolvePermissionsOverride', () => {
|
|
110
|
+
const signingKey = derivePermissionsOverrideSigningKey(TEST_ENCRYPTION_KEY);
|
|
111
|
+
let tempDir;
|
|
112
|
+
beforeEach(() => {
|
|
113
|
+
tempDir = mkdtempSync(join(tmpdir(), 'latchkey-pp-test-'));
|
|
114
|
+
});
|
|
115
|
+
afterEach(() => {
|
|
116
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
117
|
+
});
|
|
118
|
+
it('returns the path when the file exists', () => {
|
|
119
|
+
const path = join(tempDir, 'permissions.json');
|
|
120
|
+
writeFileSync(path, '{}');
|
|
121
|
+
const token = createPermissionsOverrideJwt(path, signingKey);
|
|
122
|
+
expect(resolvePermissionsOverride(token, signingKey)).toBe(path);
|
|
123
|
+
});
|
|
124
|
+
it('throws PermissionsOverrideFileMissingError when the file is absent', () => {
|
|
125
|
+
const path = join(tempDir, 'does-not-exist.json');
|
|
126
|
+
const token = createPermissionsOverrideJwt(path, signingKey);
|
|
127
|
+
expect(() => resolvePermissionsOverride(token, signingKey)).toThrow(PermissionsOverrideFileMissingError);
|
|
128
|
+
});
|
|
129
|
+
it('throws PermissionsOverrideFileMissingError when the path is a directory', () => {
|
|
130
|
+
const path = join(tempDir, 'subdir');
|
|
131
|
+
mkdirSync(path);
|
|
132
|
+
const token = createPermissionsOverrideJwt(path, signingKey);
|
|
133
|
+
expect(() => resolvePermissionsOverride(token, signingKey)).toThrow(PermissionsOverrideFileMissingError);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
//# sourceMappingURL=permissionsOverride.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"permissionsOverride.test.js","sourceRoot":"","sources":["../../tests/permissionsOverride.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolveEncryptionKey.test.d.ts","sourceRoot":"","sources":["../../tests/resolveEncryptionKey.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { EncryptionKeyLostError, generateKey, resolveEncryptionKey } from '../src/encryption.js';
|
|
3
|
+
vi.mock('../src/keychain.js', async (importOriginal) => {
|
|
4
|
+
const original = await importOriginal();
|
|
5
|
+
return {
|
|
6
|
+
...original,
|
|
7
|
+
retrieveFromKeychain: () => Promise.resolve(null),
|
|
8
|
+
storeInKeychain: () => Promise.resolve(undefined),
|
|
9
|
+
};
|
|
10
|
+
});
|
|
11
|
+
describe('resolveEncryptionKey', () => {
|
|
12
|
+
it('returns the override verbatim and does not touch the keychain', async () => {
|
|
13
|
+
const override = generateKey();
|
|
14
|
+
await expect(resolveEncryptionKey({ encryptionKeyOverride: override })).resolves.toBe(override);
|
|
15
|
+
});
|
|
16
|
+
it('throws EncryptionKeyLostError when allowKeyGeneration is false and keychain has no key', async () => {
|
|
17
|
+
await expect(resolveEncryptionKey({ allowKeyGeneration: false })).rejects.toThrow(EncryptionKeyLostError);
|
|
18
|
+
});
|
|
19
|
+
it('generates a new key when allowKeyGeneration is true and keychain has no key', async () => {
|
|
20
|
+
await expect(resolveEncryptionKey({ allowKeyGeneration: true })).resolves.toMatch(/.+/);
|
|
21
|
+
});
|
|
22
|
+
it('generates a new key when allowKeyGeneration is unset and keychain has no key', async () => {
|
|
23
|
+
await expect(resolveEncryptionKey({})).resolves.toMatch(/.+/);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
//# sourceMappingURL=resolveEncryptionKey.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolveEncryptionKey.test.js","sourceRoot":"","sources":["../../tests/resolveEncryptionKey.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAClD,OAAO,EAAE,sBAAsB,EAAE,WAAW,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAEjG,EAAE,CAAC,IAAI,CAAC,oBAAoB,EAAE,KAAK,EAAE,cAAc,EAAE,EAAE;IACrD,MAAM,QAAQ,GAAG,MAAM,cAAc,EAAuC,CAAC;IAC7E,OAAO;QACL,GAAG,QAAQ;QACX,oBAAoB,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC;QACjD,eAAe,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,SAAS,CAAC;KAClD,CAAC;AACJ,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,sBAAsB,EAAE,GAAG,EAAE;IACpC,EAAE,CAAC,+DAA+D,EAAE,KAAK,IAAI,EAAE;QAC7E,MAAM,QAAQ,GAAG,WAAW,EAAE,CAAC;QAC/B,MAAM,MAAM,CAAC,oBAAoB,CAAC,EAAE,qBAAqB,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAClG,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,wFAAwF,EAAE,KAAK,IAAI,EAAE;QACtG,MAAM,MAAM,CAAC,oBAAoB,CAAC,EAAE,kBAAkB,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAC/E,sBAAsB,CACvB,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,6EAA6E,EAAE,KAAK,IAAI,EAAE;QAC3F,MAAM,MAAM,CAAC,oBAAoB,CAAC,EAAE,kBAAkB,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1F,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,8EAA8E,EAAE,KAAK,IAAI,EAAE;QAC5F,MAAM,MAAM,CAAC,oBAAoB,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IAChE,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -13,8 +13,8 @@ import { Config } from '../src/config.js';
|
|
|
13
13
|
import { servicesList, servicesInfo, authList, authBrowser, authBrowserPrepare, UnknownServiceError, PreparationRequiredError, } from '../src/sharedOperations.js';
|
|
14
14
|
import { BrowserFlowsNotSupportedError } from '../src/playwrightUtils.js';
|
|
15
15
|
const TEST_ENCRYPTION_KEY = 'dGVzdGtleXRlc3RrZXl0ZXN0a2V5dGVzdGtleXRlc3Q=';
|
|
16
|
-
|
|
17
|
-
const storage =
|
|
16
|
+
function writeSecureFile(path, content) {
|
|
17
|
+
const storage = new EncryptedStorage(TEST_ENCRYPTION_KEY);
|
|
18
18
|
storage.writeFile(path, content);
|
|
19
19
|
}
|
|
20
20
|
function createMockService(overrides = {}) {
|
|
@@ -51,49 +51,47 @@ describe('operations', () => {
|
|
|
51
51
|
afterEach(() => {
|
|
52
52
|
rmSync(tempDir, { recursive: true, force: true });
|
|
53
53
|
});
|
|
54
|
-
|
|
54
|
+
function createApiCredentialStore(credentialsData = {}) {
|
|
55
55
|
const storePath = join(tempDir, 'credentials.json');
|
|
56
|
-
|
|
57
|
-
const encryptedStorage =
|
|
58
|
-
encryptionKeyOverride: TEST_ENCRYPTION_KEY,
|
|
59
|
-
});
|
|
56
|
+
writeSecureFile(storePath, JSON.stringify(credentialsData));
|
|
57
|
+
const encryptedStorage = new EncryptedStorage(TEST_ENCRYPTION_KEY);
|
|
60
58
|
return new ApiCredentialStore(storePath, encryptedStorage);
|
|
61
59
|
}
|
|
62
60
|
describe('servicesList', () => {
|
|
63
|
-
it('should return sorted service names',
|
|
61
|
+
it('should return sorted service names', () => {
|
|
64
62
|
const serviceA = createMockService({ name: 'zzz-service' });
|
|
65
63
|
const serviceB = createMockService({ name: 'aaa-service' });
|
|
66
64
|
const registry = new ServiceRegistry([serviceA, serviceB]);
|
|
67
|
-
const store =
|
|
65
|
+
const store = createApiCredentialStore();
|
|
68
66
|
const config = createMockConfig();
|
|
69
67
|
const result = servicesList(registry, store, config, {});
|
|
70
68
|
expect(result).toEqual(['aaa-service', 'zzz-service']);
|
|
71
69
|
});
|
|
72
|
-
it('should filter to builtin services only',
|
|
70
|
+
it('should filter to builtin services only', () => {
|
|
73
71
|
const builtinService = createMockService({ name: 'slack' });
|
|
74
72
|
const registeredService = new RegisteredService('my-gitlab', 'https://gitlab.example.com');
|
|
75
73
|
const registry = new ServiceRegistry([builtinService]);
|
|
76
74
|
registry.addService(registeredService);
|
|
77
|
-
const store =
|
|
75
|
+
const store = createApiCredentialStore();
|
|
78
76
|
const config = createMockConfig();
|
|
79
77
|
const result = servicesList(registry, store, config, { builtin: true });
|
|
80
78
|
expect(result).toContain('slack');
|
|
81
79
|
expect(result).not.toContain('my-gitlab');
|
|
82
80
|
});
|
|
83
|
-
it('should filter to viable services with stored credentials',
|
|
81
|
+
it('should filter to viable services with stored credentials', () => {
|
|
84
82
|
const service = createMockService({ name: 'slack', getSession: undefined });
|
|
85
83
|
const registry = new ServiceRegistry([service]);
|
|
86
|
-
const store =
|
|
84
|
+
const store = createApiCredentialStore({
|
|
87
85
|
slack: { objectType: 'slack', token: 'test-token', dCookie: 'test-cookie' },
|
|
88
86
|
});
|
|
89
87
|
const config = createMockConfig({ browserDisabled: true });
|
|
90
88
|
const result = servicesList(registry, store, config, { viable: true });
|
|
91
89
|
expect(result).toContain('slack');
|
|
92
90
|
});
|
|
93
|
-
it('should exclude non-viable services without credentials or browser',
|
|
91
|
+
it('should exclude non-viable services without credentials or browser', () => {
|
|
94
92
|
const service = createMockService({ name: 'nologin', getSession: undefined });
|
|
95
93
|
const registry = new ServiceRegistry([service]);
|
|
96
|
-
const store =
|
|
94
|
+
const store = createApiCredentialStore({});
|
|
97
95
|
const config = createMockConfig();
|
|
98
96
|
const result = servicesList(registry, store, config, { viable: true });
|
|
99
97
|
expect(result).not.toContain('nologin');
|
|
@@ -103,7 +101,7 @@ describe('operations', () => {
|
|
|
103
101
|
it('should return service info', async () => {
|
|
104
102
|
const service = createMockService();
|
|
105
103
|
const registry = new ServiceRegistry([service]);
|
|
106
|
-
const store =
|
|
104
|
+
const store = createApiCredentialStore();
|
|
107
105
|
const config = createMockConfig();
|
|
108
106
|
const info = await servicesInfo(registry, store, config, 'slack');
|
|
109
107
|
expect(info.type).toBe('built-in');
|
|
@@ -115,14 +113,14 @@ describe('operations', () => {
|
|
|
115
113
|
});
|
|
116
114
|
it('should throw UnknownServiceError for unknown service', async () => {
|
|
117
115
|
const registry = new ServiceRegistry([]);
|
|
118
|
-
const store =
|
|
116
|
+
const store = createApiCredentialStore();
|
|
119
117
|
const config = createMockConfig();
|
|
120
118
|
await expect(servicesInfo(registry, store, config, 'unknown')).rejects.toThrow(UnknownServiceError);
|
|
121
119
|
});
|
|
122
120
|
it('should exclude browser from authOptions when browser disabled', async () => {
|
|
123
121
|
const service = createMockService();
|
|
124
122
|
const registry = new ServiceRegistry([service]);
|
|
125
|
-
const store =
|
|
123
|
+
const store = createApiCredentialStore();
|
|
126
124
|
const config = createMockConfig({ browserDisabled: true });
|
|
127
125
|
const info = await servicesInfo(registry, store, config, 'slack');
|
|
128
126
|
expect(info.authOptions).toEqual(['set']);
|
|
@@ -131,7 +129,7 @@ describe('operations', () => {
|
|
|
131
129
|
const registeredService = new RegisteredService('my-gitlab', 'https://gitlab.example.com');
|
|
132
130
|
const registry = new ServiceRegistry([]);
|
|
133
131
|
registry.addService(registeredService);
|
|
134
|
-
const store =
|
|
132
|
+
const store = createApiCredentialStore();
|
|
135
133
|
const config = createMockConfig();
|
|
136
134
|
const info = await servicesInfo(registry, store, config, 'my-gitlab');
|
|
137
135
|
expect(info.type).toBe('user-registered');
|
|
@@ -141,7 +139,7 @@ describe('operations', () => {
|
|
|
141
139
|
it('should return stored credentials with status', async () => {
|
|
142
140
|
const service = createMockService();
|
|
143
141
|
const registry = new ServiceRegistry([service]);
|
|
144
|
-
const store =
|
|
142
|
+
const store = createApiCredentialStore({
|
|
145
143
|
slack: { objectType: 'slack', token: 'test-token', dCookie: 'test-cookie' },
|
|
146
144
|
});
|
|
147
145
|
const result = await authList(registry, store);
|
|
@@ -152,13 +150,13 @@ describe('operations', () => {
|
|
|
152
150
|
});
|
|
153
151
|
it('should return empty object when no credentials stored', async () => {
|
|
154
152
|
const registry = new ServiceRegistry([]);
|
|
155
|
-
const store =
|
|
153
|
+
const store = createApiCredentialStore({});
|
|
156
154
|
const result = await authList(registry, store);
|
|
157
155
|
expect(Object.keys(result)).toHaveLength(0);
|
|
158
156
|
});
|
|
159
157
|
it('should treat unknown services as valid', async () => {
|
|
160
158
|
const registry = new ServiceRegistry([]);
|
|
161
|
-
const store =
|
|
159
|
+
const store = createApiCredentialStore({
|
|
162
160
|
unknown: { objectType: 'rawCurl', curlArguments: ['-H', 'X-Token: secret'] },
|
|
163
161
|
});
|
|
164
162
|
const result = await authList(registry, store);
|
|
@@ -171,20 +169,16 @@ describe('operations', () => {
|
|
|
171
169
|
describe('authBrowser', () => {
|
|
172
170
|
it('should throw UnknownServiceError for unknown service', async () => {
|
|
173
171
|
const registry = new ServiceRegistry([]);
|
|
174
|
-
const store =
|
|
175
|
-
const encryptedStorage =
|
|
176
|
-
encryptionKeyOverride: TEST_ENCRYPTION_KEY,
|
|
177
|
-
});
|
|
172
|
+
const store = createApiCredentialStore();
|
|
173
|
+
const encryptedStorage = new EncryptedStorage(TEST_ENCRYPTION_KEY);
|
|
178
174
|
const config = createMockConfig();
|
|
179
175
|
await expect(authBrowser(registry, store, encryptedStorage, config, 'unknown')).rejects.toThrow(UnknownServiceError);
|
|
180
176
|
});
|
|
181
177
|
it('should throw BrowserFlowsNotSupportedError when service has no browser support', async () => {
|
|
182
178
|
const service = createMockService({ getSession: undefined });
|
|
183
179
|
const registry = new ServiceRegistry([service]);
|
|
184
|
-
const store =
|
|
185
|
-
const encryptedStorage =
|
|
186
|
-
encryptionKeyOverride: TEST_ENCRYPTION_KEY,
|
|
187
|
-
});
|
|
180
|
+
const store = createApiCredentialStore();
|
|
181
|
+
const encryptedStorage = new EncryptedStorage(TEST_ENCRYPTION_KEY);
|
|
188
182
|
const config = createMockConfig();
|
|
189
183
|
await expect(authBrowser(registry, store, encryptedStorage, config, 'slack')).rejects.toThrow(BrowserFlowsNotSupportedError);
|
|
190
184
|
});
|
|
@@ -196,10 +190,8 @@ describe('operations', () => {
|
|
|
196
190
|
}),
|
|
197
191
|
});
|
|
198
192
|
const registry = new ServiceRegistry([service]);
|
|
199
|
-
const store =
|
|
200
|
-
const encryptedStorage =
|
|
201
|
-
encryptionKeyOverride: TEST_ENCRYPTION_KEY,
|
|
202
|
-
});
|
|
193
|
+
const store = createApiCredentialStore({});
|
|
194
|
+
const encryptedStorage = new EncryptedStorage(TEST_ENCRYPTION_KEY);
|
|
203
195
|
const config = createMockConfig();
|
|
204
196
|
await expect(authBrowser(registry, store, encryptedStorage, config, 'slack')).rejects.toThrow(PreparationRequiredError);
|
|
205
197
|
});
|
|
@@ -207,10 +199,8 @@ describe('operations', () => {
|
|
|
207
199
|
describe('authBrowserPrepare', () => {
|
|
208
200
|
it('should throw UnknownServiceError for unknown service', async () => {
|
|
209
201
|
const registry = new ServiceRegistry([]);
|
|
210
|
-
const store =
|
|
211
|
-
const encryptedStorage =
|
|
212
|
-
encryptionKeyOverride: TEST_ENCRYPTION_KEY,
|
|
213
|
-
});
|
|
202
|
+
const store = createApiCredentialStore();
|
|
203
|
+
const encryptedStorage = new EncryptedStorage(TEST_ENCRYPTION_KEY);
|
|
214
204
|
const config = createMockConfig();
|
|
215
205
|
await expect(authBrowserPrepare(registry, store, encryptedStorage, config, 'unknown')).rejects.toThrow(UnknownServiceError);
|
|
216
206
|
});
|
|
@@ -222,10 +212,8 @@ describe('operations', () => {
|
|
|
222
212
|
}),
|
|
223
213
|
});
|
|
224
214
|
const registry = new ServiceRegistry([service]);
|
|
225
|
-
const store =
|
|
226
|
-
const encryptedStorage =
|
|
227
|
-
encryptionKeyOverride: TEST_ENCRYPTION_KEY,
|
|
228
|
-
});
|
|
215
|
+
const store = createApiCredentialStore();
|
|
216
|
+
const encryptedStorage = new EncryptedStorage(TEST_ENCRYPTION_KEY);
|
|
229
217
|
const config = createMockConfig();
|
|
230
218
|
const result = await authBrowserPrepare(registry, store, encryptedStorage, config, 'slack');
|
|
231
219
|
expect(result.alreadyPrepared).toBe(true);
|
|
@@ -238,12 +226,10 @@ describe('operations', () => {
|
|
|
238
226
|
}),
|
|
239
227
|
});
|
|
240
228
|
const registry = new ServiceRegistry([service]);
|
|
241
|
-
const store =
|
|
229
|
+
const store = createApiCredentialStore({
|
|
242
230
|
slack: { objectType: 'slack', token: 'test-token', dCookie: 'test-cookie' },
|
|
243
231
|
});
|
|
244
|
-
const encryptedStorage =
|
|
245
|
-
encryptionKeyOverride: TEST_ENCRYPTION_KEY,
|
|
246
|
-
});
|
|
232
|
+
const encryptedStorage = new EncryptedStorage(TEST_ENCRYPTION_KEY);
|
|
247
233
|
const config = createMockConfig();
|
|
248
234
|
const result = await authBrowserPrepare(registry, store, encryptedStorage, config, 'slack');
|
|
249
235
|
expect(result.alreadyPrepared).toBe(true);
|
|
@@ -251,10 +237,8 @@ describe('operations', () => {
|
|
|
251
237
|
it('should return alreadyPrepared true when service has no getSession', async () => {
|
|
252
238
|
const service = createMockService({ getSession: undefined });
|
|
253
239
|
const registry = new ServiceRegistry([service]);
|
|
254
|
-
const store =
|
|
255
|
-
const encryptedStorage =
|
|
256
|
-
encryptionKeyOverride: TEST_ENCRYPTION_KEY,
|
|
257
|
-
});
|
|
240
|
+
const store = createApiCredentialStore();
|
|
241
|
+
const encryptedStorage = new EncryptedStorage(TEST_ENCRYPTION_KEY);
|
|
258
242
|
const config = createMockConfig();
|
|
259
243
|
const result = await authBrowserPrepare(registry, store, encryptedStorage, config, 'slack');
|
|
260
244
|
expect(result.alreadyPrepared).toBe(true);
|