latchkey 3.8.0 → 3.10.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.
Files changed (104) hide show
  1. package/README.md +3 -3
  2. package/dist/scripts/recordBrowserSession.js +4 -3
  3. package/dist/scripts/recordBrowserSession.js.map +1 -1
  4. package/dist/src/apiCredentials/base.d.ts +7 -1
  5. package/dist/src/apiCredentials/base.d.ts.map +1 -1
  6. package/dist/src/apiCredentials/base.js.map +1 -1
  7. package/dist/src/apiCredentials/store.d.ts +5 -0
  8. package/dist/src/apiCredentials/store.d.ts.map +1 -1
  9. package/dist/src/apiCredentials/store.js +7 -0
  10. package/dist/src/apiCredentials/store.js.map +1 -1
  11. package/dist/src/atomicWrite.d.ts +4 -1
  12. package/dist/src/atomicWrite.d.ts.map +1 -1
  13. package/dist/src/atomicWrite.js +34 -3
  14. package/dist/src/atomicWrite.js.map +1 -1
  15. package/dist/src/cli.js +0 -3
  16. package/dist/src/cli.js.map +1 -1
  17. package/dist/src/cliCommands.d.ts +8 -0
  18. package/dist/src/cliCommands.d.ts.map +1 -1
  19. package/dist/src/cliCommands.js +110 -17
  20. package/dist/src/cliCommands.js.map +1 -1
  21. package/dist/src/curl.d.ts +36 -0
  22. package/dist/src/curl.d.ts.map +1 -1
  23. package/dist/src/curl.js +42 -0
  24. package/dist/src/curl.js.map +1 -1
  25. package/dist/src/curlInjection.d.ts +5 -4
  26. package/dist/src/curlInjection.d.ts.map +1 -1
  27. package/dist/src/curlInjection.js +8 -7
  28. package/dist/src/curlInjection.js.map +1 -1
  29. package/dist/src/gateway/gatewayEndpoint.d.ts.map +1 -1
  30. package/dist/src/gateway/gatewayEndpoint.js +6 -3
  31. package/dist/src/gateway/gatewayEndpoint.js.map +1 -1
  32. package/dist/src/gateway/server.d.ts.map +1 -1
  33. package/dist/src/gateway/server.js +14 -3
  34. package/dist/src/gateway/server.js.map +1 -1
  35. package/dist/src/index.d.ts +1 -1
  36. package/dist/src/index.d.ts.map +1 -1
  37. package/dist/src/index.js +1 -1
  38. package/dist/src/index.js.map +1 -1
  39. package/dist/src/migrations.d.ts +6 -0
  40. package/dist/src/migrations.d.ts.map +1 -1
  41. package/dist/src/migrations.js +21 -8
  42. package/dist/src/migrations.js.map +1 -1
  43. package/dist/src/serviceRegistry.d.ts +14 -1
  44. package/dist/src/serviceRegistry.d.ts.map +1 -1
  45. package/dist/src/serviceRegistry.js +22 -2
  46. package/dist/src/serviceRegistry.js.map +1 -1
  47. package/dist/src/services/aws.d.ts +11 -2
  48. package/dist/src/services/aws.d.ts.map +1 -1
  49. package/dist/src/services/aws.js +72 -5
  50. package/dist/src/services/aws.js.map +1 -1
  51. package/dist/src/services/core/loginFlows/registry.d.ts.map +1 -1
  52. package/dist/src/services/core/loginFlows/registry.js +11 -6
  53. package/dist/src/services/core/loginFlows/registry.js.map +1 -1
  54. package/dist/src/services/core/loginFlows/tokenCapture.d.ts +74 -0
  55. package/dist/src/services/core/loginFlows/tokenCapture.d.ts.map +1 -0
  56. package/dist/src/services/core/loginFlows/tokenCapture.js +201 -0
  57. package/dist/src/services/core/loginFlows/tokenCapture.js.map +1 -0
  58. package/dist/src/services/index.d.ts +1 -0
  59. package/dist/src/services/index.d.ts.map +1 -1
  60. package/dist/src/services/index.js +1 -0
  61. package/dist/src/services/index.js.map +1 -1
  62. package/dist/src/version.d.ts +1 -1
  63. package/dist/src/version.d.ts.map +1 -1
  64. package/dist/src/version.js +1 -1
  65. package/dist/src/version.js.map +1 -1
  66. package/dist/tests/apiCredentialStore.test.js +8 -0
  67. package/dist/tests/apiCredentialStore.test.js.map +1 -1
  68. package/dist/tests/apiCredentials.test.js +84 -1
  69. package/dist/tests/apiCredentials.test.js.map +1 -1
  70. package/dist/tests/atomicWrite.test.js +42 -1
  71. package/dist/tests/atomicWrite.test.js.map +1 -1
  72. package/dist/tests/builtinServiceRegistry.d.ts +10 -0
  73. package/dist/tests/builtinServiceRegistry.d.ts.map +1 -0
  74. package/dist/tests/builtinServiceRegistry.js +10 -0
  75. package/dist/tests/builtinServiceRegistry.js.map +1 -0
  76. package/dist/tests/cli.test.js +273 -7
  77. package/dist/tests/cli.test.js.map +1 -1
  78. package/dist/tests/devShim.test.d.ts +2 -0
  79. package/dist/tests/devShim.test.d.ts.map +1 -0
  80. package/dist/tests/devShim.test.js +240 -0
  81. package/dist/tests/devShim.test.js.map +1 -0
  82. package/dist/tests/gateway.test.js +87 -2
  83. package/dist/tests/gateway.test.js.map +1 -1
  84. package/dist/tests/gatewayExtensions.test.js +2 -0
  85. package/dist/tests/gatewayExtensions.test.js.map +1 -1
  86. package/dist/tests/github.test.js +2 -2
  87. package/dist/tests/github.test.js.map +1 -1
  88. package/dist/tests/huggingface.test.js +2 -2
  89. package/dist/tests/huggingface.test.js.map +1 -1
  90. package/dist/tests/latchkeyEndpoint.test.js +9 -5
  91. package/dist/tests/latchkeyEndpoint.test.js.map +1 -1
  92. package/dist/tests/manualCredentialForms.test.js +2 -2
  93. package/dist/tests/manualCredentialForms.test.js.map +1 -1
  94. package/dist/tests/serviceRegistry.test.js +104 -20
  95. package/dist/tests/serviceRegistry.test.js.map +1 -1
  96. package/dist/tests/servicesAgainstRecordings.test.js +2 -2
  97. package/dist/tests/servicesAgainstRecordings.test.js.map +1 -1
  98. package/dist/tests/tailscale.test.js +2 -2
  99. package/dist/tests/tailscale.test.js.map +1 -1
  100. package/dist/tests/tokenCapture.test.d.ts +10 -0
  101. package/dist/tests/tokenCapture.test.d.ts.map +1 -0
  102. package/dist/tests/tokenCapture.test.js +136 -0
  103. package/dist/tests/tokenCapture.test.js.map +1 -0
  104. package/package.json +5 -2
@@ -0,0 +1,10 @@
1
+ /**
2
+ * A registry over the services latchkey ships with.
3
+ *
4
+ * The CLI and the gateway each build their own registry with
5
+ * `createServiceRegistry`, since theirs also carry whatever config.json holds.
6
+ * Tests that only care about the built-in services share this one.
7
+ */
8
+ import { BUILTIN_SERVICES, ServiceRegistry } from '../src/serviceRegistry.js';
9
+ export const BUILTIN_SERVICE_REGISTRY = new ServiceRegistry(BUILTIN_SERVICES);
10
+ //# sourceMappingURL=builtinServiceRegistry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"builtinServiceRegistry.js","sourceRoot":"","sources":["../../tests/builtinServiceRegistry.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAE9E,MAAM,CAAC,MAAM,wBAAwB,GAAG,IAAI,eAAe,CAAC,gBAAgB,CAAC,CAAC"}
@@ -5,12 +5,13 @@ import { tmpdir } from 'node:os';
5
5
  import { execSync } from 'node:child_process';
6
6
  import { Command } from 'commander';
7
7
  import { registerCommands } from '../src/cliCommands.js';
8
- import { CurlParseError, extractUrlFromCurlArguments } from '../src/curl.js';
8
+ import { CurlParseError, extractUrlFromCurlArguments, resolveCurlRequestBodySource, } from '../src/curl.js';
9
9
  import { hasGraphicalEnvironment } from '../src/playwrightUtils.js';
10
10
  import { EncryptedStorage } from '../src/encryptedStorage.js';
11
11
  import { Config } from '../src/config.js';
12
12
  import { ServiceRegistry } from '../src/serviceRegistry.js';
13
13
  import { ApiCredentialStatus } from '../src/apiCredentials/base.js';
14
+ import { LATEST_VERSION } from '../src/migrations.js';
14
15
  import { Service } from '../src/services/core/base.js';
15
16
  import { createMockService, MockService } from './mockService.js';
16
17
  import { RegisteredService } from '../src/services/core/registered.js';
@@ -183,6 +184,26 @@ describe('extractUrlFromCurlArguments', () => {
183
184
  expect(extractUrlFromCurlArguments(arguments_)).toBe('https://example.com/api?query=from%3Ame');
184
185
  });
185
186
  });
187
+ describe('resolveCurlRequestBodySource', () => {
188
+ it('should report an inline body when no file reference is present', () => {
189
+ expect(resolveCurlRequestBodySource(['-d', '{"a":1}', 'https://example.com'], '{"a":1}')).toEqual({ kind: 'inline', text: '{"a":1}' });
190
+ });
191
+ it('should report an inline body for --data-raw, which treats @ literally', () => {
192
+ expect(resolveCurlRequestBodySource(['--data-raw', '@literal', 'https://example.com'], '@literal')).toEqual({ kind: 'inline', text: '@literal' });
193
+ });
194
+ it('should report a file reference', () => {
195
+ expect(resolveCurlRequestBodySource(['--data-binary', '@/tmp/payload.json', 'https://example.com'], '@/tmp/payload.json')).toEqual({ kind: 'file', path: '/tmp/payload.json' });
196
+ });
197
+ it('should report stdin for @-', () => {
198
+ expect(resolveCurlRequestBodySource(['--data-binary', '@-', 'https://example.com'], '@-')).toEqual({ kind: 'stdin' });
199
+ });
200
+ it('should report an unreconstructable body for combined data arguments', () => {
201
+ expect(resolveCurlRequestBodySource(['-d', '@/tmp/payload.json', '-d', 'extra=1', 'https://example.com'], '@/tmp/payload.json&extra=1')).toEqual({ kind: 'unreconstructable' });
202
+ });
203
+ it('should report an unreconstructable body for url-encoded file data', () => {
204
+ expect(resolveCurlRequestBodySource(['--data-urlencode', '@/tmp/payload.json', 'https://example.com'], '@/tmp/payload.json')).toEqual({ kind: 'unreconstructable' });
205
+ });
206
+ });
186
207
  describe('hasGraphicalEnvironment', () => {
187
208
  const originalPlatform = process.platform;
188
209
  afterEach(() => {
@@ -345,6 +366,7 @@ describe('CLI commands with dependency injection', () => {
345
366
  const mockRegistry = new ServiceRegistry([mockSlackService]);
346
367
  return {
347
368
  registry: mockRegistry,
369
+ builtinServices: [mockSlackService],
348
370
  config: createMockConfig(),
349
371
  runCurl: (args) => {
350
372
  capturedArgs.push(...args);
@@ -973,11 +995,35 @@ describe('CLI commands with dependency injection', () => {
973
995
  function withStdinKey(key, overrides = {}) {
974
996
  return createMockDependencies({ readStdin: () => Promise.resolve(key), ...overrides });
975
997
  }
976
- // The credential store filename used by the mock config.
998
+ // The filenames used by the mock config.
977
999
  const STORE_FILENAME = 'credentials.json';
978
- function writeStore(contents) {
979
- writeSecureFile(join(tempDir, STORE_FILENAME), JSON.stringify(nestAccounts(contents)));
1000
+ const BROWSER_STATE_FILENAME = 'browser_state.json';
1001
+ function writeStore(contents, preparations = {}) {
1002
+ writeSecureFile(join(tempDir, STORE_FILENAME), JSON.stringify({ ...nestAccounts(contents), preparations }));
1003
+ }
1004
+ // Write an on-disk store with explicit account keys.
1005
+ function writeAccountStore(credentials, preparations = {}) {
1006
+ writeSecureFile(join(tempDir, STORE_FILENAME), JSON.stringify({ credentials, preparations }));
980
1007
  }
1008
+ function readAccountsWithKey(path, key) {
1009
+ const raw = JSON.parse(new EncryptedStorage(key).readFile(path) ?? '{}');
1010
+ return raw.credentials ?? {};
1011
+ }
1012
+ function readPreparationsWithKey(path, key) {
1013
+ const raw = JSON.parse(new EncryptedStorage(key).readFile(path) ?? '{}');
1014
+ return raw.preparations ?? {};
1015
+ }
1016
+ // A destination that already holds a store must be in the current data
1017
+ // format, which real Latchkey directories record in this file.
1018
+ function stampDataFormatVersion(directory) {
1019
+ mkdirSync(directory, { recursive: true });
1020
+ writeFileSync(join(directory, 'data-format-version'), String(LATEST_VERSION));
1021
+ }
1022
+ const GITLAB_PREPARATION = {
1023
+ objectType: 'oauth',
1024
+ clientId: 'gitlab-client-id',
1025
+ clientSecret: 'gitlab-client-secret',
1026
+ };
981
1027
  it('re-encrypts all stored credentials with the new key', async () => {
982
1028
  writeStore({
983
1029
  slack: { objectType: 'slack', token: 'tok', dCookie: 'cookie' },
@@ -1025,7 +1071,7 @@ describe('CLI commands with dependency injection', () => {
1025
1071
  const reEncrypted = readWithKey(join(destinationDirectory, STORE_FILENAME), NEW_ENCRYPTION_KEY);
1026
1072
  expect(Object.keys(reEncrypted)).toEqual(['slack']);
1027
1073
  });
1028
- it('errors when a requested service has no stored credentials', async () => {
1074
+ it('errors when a requested service has neither credentials nor a preparation', async () => {
1029
1075
  writeStore({ slack: { objectType: 'slack', token: 'tok', dCookie: 'cookie' } });
1030
1076
  const destinationDirectory = join(tempDir, 'export');
1031
1077
  const deps = withStdinKey(NEW_ENCRYPTION_KEY);
@@ -1033,6 +1079,170 @@ describe('CLI commands with dependency injection', () => {
1033
1079
  expect(exitCode).toBe(1);
1034
1080
  expect(existsSync(join(destinationDirectory, STORE_FILENAME))).toBe(false);
1035
1081
  });
1082
+ it('carries the preparations of the re-encrypted services', async () => {
1083
+ writeStore({ gitlab: { objectType: 'authorizationBearer', token: 'gitlab-token' } }, { gitlab: GITLAB_PREPARATION });
1084
+ const destinationDirectory = join(tempDir, 'export');
1085
+ const deps = withStdinKey(NEW_ENCRYPTION_KEY);
1086
+ await runCommand(['auth', 're-encrypt', destinationDirectory], deps);
1087
+ expect(exitCode).toBeNull();
1088
+ const destination = join(destinationDirectory, STORE_FILENAME);
1089
+ expect(readPreparationsWithKey(destination, NEW_ENCRYPTION_KEY)).toEqual({
1090
+ gitlab: GITLAB_PREPARATION,
1091
+ });
1092
+ });
1093
+ it('includes services that only have a preparation', async () => {
1094
+ writeStore({ slack: { objectType: 'slack', token: 'tok', dCookie: 'cookie' } }, { gitlab: GITLAB_PREPARATION });
1095
+ const destinationDirectory = join(tempDir, 'export');
1096
+ const deps = withStdinKey(NEW_ENCRYPTION_KEY);
1097
+ await runCommand(['auth', 're-encrypt', destinationDirectory], deps);
1098
+ expect(exitCode).toBeNull();
1099
+ const destination = join(destinationDirectory, STORE_FILENAME);
1100
+ expect(Object.keys(readWithKey(destination, NEW_ENCRYPTION_KEY))).toEqual(['slack']);
1101
+ expect(readPreparationsWithKey(destination, NEW_ENCRYPTION_KEY)).toEqual({
1102
+ gitlab: GITLAB_PREPARATION,
1103
+ });
1104
+ expect(logs.join('\n')).toContain('2 service(s)');
1105
+ });
1106
+ it('selects a service that only has a preparation with --services', async () => {
1107
+ writeStore({ slack: { objectType: 'slack', token: 'tok', dCookie: 'cookie' } }, { gitlab: GITLAB_PREPARATION });
1108
+ const destinationDirectory = join(tempDir, 'export');
1109
+ const deps = withStdinKey(NEW_ENCRYPTION_KEY);
1110
+ await runCommand(['auth', 're-encrypt', destinationDirectory, '--services', 'gitlab'], deps);
1111
+ expect(exitCode).toBeNull();
1112
+ const destination = join(destinationDirectory, STORE_FILENAME);
1113
+ expect(readWithKey(destination, NEW_ENCRYPTION_KEY)).toEqual({});
1114
+ expect(readPreparationsWithKey(destination, NEW_ENCRYPTION_KEY)).toEqual({
1115
+ gitlab: GITLAB_PREPARATION,
1116
+ });
1117
+ });
1118
+ it('replaces an existing preparation in the destination', async () => {
1119
+ writeStore({}, { gitlab: GITLAB_PREPARATION });
1120
+ const destinationDirectory = join(tempDir, 'export');
1121
+ const destination = join(destinationDirectory, STORE_FILENAME);
1122
+ stampDataFormatVersion(destinationDirectory);
1123
+ new EncryptedStorage(NEW_ENCRYPTION_KEY).writeFile(destination, JSON.stringify({
1124
+ credentials: {},
1125
+ preparations: {
1126
+ gitlab: { objectType: 'oauth', clientId: 'old-id', clientSecret: 'old-secret' },
1127
+ },
1128
+ }));
1129
+ const deps = withStdinKey(NEW_ENCRYPTION_KEY);
1130
+ await runCommand(['auth', 're-encrypt', destinationDirectory], deps);
1131
+ expect(exitCode).toBeNull();
1132
+ expect(readPreparationsWithKey(destination, NEW_ENCRYPTION_KEY)).toEqual({
1133
+ gitlab: GITLAB_PREPARATION,
1134
+ });
1135
+ });
1136
+ it('only includes the given account when using --account', async () => {
1137
+ writeAccountStore({
1138
+ slack: {
1139
+ 'work@example.com': { objectType: 'slack', token: 'work-tok', dCookie: 'work-cookie' },
1140
+ 'home@example.com': { objectType: 'slack', token: 'home-tok', dCookie: 'home-cookie' },
1141
+ },
1142
+ gitlab: {
1143
+ 'work@example.com': { objectType: 'authorizationBearer', token: 'gitlab-token' },
1144
+ },
1145
+ }, { gitlab: GITLAB_PREPARATION });
1146
+ const destinationDirectory = join(tempDir, 'export');
1147
+ const deps = withStdinKey(NEW_ENCRYPTION_KEY);
1148
+ await runCommand(['--account', 'work@example.com', 'auth', 're-encrypt', destinationDirectory], deps);
1149
+ expect(exitCode).toBeNull();
1150
+ const destination = join(destinationDirectory, STORE_FILENAME);
1151
+ expect(readAccountsWithKey(destination, NEW_ENCRYPTION_KEY)).toEqual({
1152
+ slack: {
1153
+ 'work@example.com': { objectType: 'slack', token: 'work-tok', dCookie: 'work-cookie' },
1154
+ },
1155
+ gitlab: {
1156
+ 'work@example.com': { objectType: 'authorizationBearer', token: 'gitlab-token' },
1157
+ },
1158
+ });
1159
+ // Preparations are not account-specific and are carried over as usual.
1160
+ expect(readPreparationsWithKey(destination, NEW_ENCRYPTION_KEY)).toEqual({
1161
+ gitlab: GITLAB_PREPARATION,
1162
+ });
1163
+ });
1164
+ it('keeps the other accounts of the destination when using --account', async () => {
1165
+ writeAccountStore({
1166
+ slack: {
1167
+ 'work@example.com': { objectType: 'slack', token: 'new-tok', dCookie: 'new-cookie' },
1168
+ },
1169
+ });
1170
+ const destinationDirectory = join(tempDir, 'export');
1171
+ const destination = join(destinationDirectory, STORE_FILENAME);
1172
+ stampDataFormatVersion(destinationDirectory);
1173
+ new EncryptedStorage(NEW_ENCRYPTION_KEY).writeFile(destination, JSON.stringify({
1174
+ credentials: {
1175
+ slack: {
1176
+ 'work@example.com': { objectType: 'slack', token: 'old-tok', dCookie: 'old-cookie' },
1177
+ 'home@example.com': {
1178
+ objectType: 'slack',
1179
+ token: 'home-tok',
1180
+ dCookie: 'home-cookie',
1181
+ },
1182
+ },
1183
+ },
1184
+ preparations: {},
1185
+ }));
1186
+ const deps = withStdinKey(NEW_ENCRYPTION_KEY);
1187
+ await runCommand(['--account', 'work@example.com', 'auth', 're-encrypt', destinationDirectory], deps);
1188
+ expect(exitCode).toBeNull();
1189
+ expect(readAccountsWithKey(destination, NEW_ENCRYPTION_KEY)).toEqual({
1190
+ slack: {
1191
+ 'work@example.com': { objectType: 'slack', token: 'new-tok', dCookie: 'new-cookie' },
1192
+ 'home@example.com': { objectType: 'slack', token: 'home-tok', dCookie: 'home-cookie' },
1193
+ },
1194
+ });
1195
+ });
1196
+ it('leaves the destination account alone when the source does not have it', async () => {
1197
+ writeAccountStore({
1198
+ slack: {
1199
+ 'work@example.com': { objectType: 'slack', token: 'work-tok', dCookie: 'work-cookie' },
1200
+ },
1201
+ }, { gitlab: GITLAB_PREPARATION });
1202
+ const destinationDirectory = join(tempDir, 'export');
1203
+ const destination = join(destinationDirectory, STORE_FILENAME);
1204
+ stampDataFormatVersion(destinationDirectory);
1205
+ new EncryptedStorage(NEW_ENCRYPTION_KEY).writeFile(destination, JSON.stringify({
1206
+ credentials: {
1207
+ // gitlab only has a preparation in the source, so its credentials
1208
+ // in the destination must survive.
1209
+ gitlab: {
1210
+ 'work@example.com': {
1211
+ objectType: 'authorizationBearer',
1212
+ token: 'destination-token',
1213
+ },
1214
+ },
1215
+ },
1216
+ preparations: {},
1217
+ }));
1218
+ const deps = withStdinKey(NEW_ENCRYPTION_KEY);
1219
+ await runCommand(['--account', 'work@example.com', 'auth', 're-encrypt', destinationDirectory], deps);
1220
+ expect(exitCode).toBeNull();
1221
+ expect(readAccountsWithKey(destination, NEW_ENCRYPTION_KEY)).toEqual({
1222
+ gitlab: {
1223
+ 'work@example.com': { objectType: 'authorizationBearer', token: 'destination-token' },
1224
+ },
1225
+ slack: {
1226
+ 'work@example.com': { objectType: 'slack', token: 'work-tok', dCookie: 'work-cookie' },
1227
+ },
1228
+ });
1229
+ expect(readPreparationsWithKey(destination, NEW_ENCRYPTION_KEY)).toEqual({
1230
+ gitlab: GITLAB_PREPARATION,
1231
+ });
1232
+ });
1233
+ it('errors when no stored credentials belong to the given account', async () => {
1234
+ writeAccountStore({
1235
+ slack: {
1236
+ 'work@example.com': { objectType: 'slack', token: 'work-tok', dCookie: 'work-cookie' },
1237
+ },
1238
+ });
1239
+ const destinationDirectory = join(tempDir, 'export');
1240
+ const deps = withStdinKey(NEW_ENCRYPTION_KEY);
1241
+ await runCommand(['--account', 'missing@example.com', 'auth', 're-encrypt', destinationDirectory], deps);
1242
+ expect(exitCode).toBe(1);
1243
+ expect(errorLogs.join('\n')).toContain('missing@example.com');
1244
+ expect(existsSync(join(destinationDirectory, STORE_FILENAME))).toBe(false);
1245
+ });
1036
1246
  it('errors for an invalid encryption key read from stdin', async () => {
1037
1247
  writeStore({});
1038
1248
  const destinationDirectory = join(tempDir, 'export');
@@ -1049,10 +1259,36 @@ describe('CLI commands with dependency injection', () => {
1049
1259
  expect(exitCode).toBeNull();
1050
1260
  expect(Object.keys(readWithKey(join(destinationDirectory, STORE_FILENAME), NEW_ENCRYPTION_KEY))).toEqual(['slack']);
1051
1261
  });
1052
- it('refuses to overwrite an existing credential store in the destination', async () => {
1262
+ it('adds the services into an existing credential store, overwriting by service', async () => {
1263
+ writeStore({
1264
+ slack: { objectType: 'slack', token: 'new-tok', dCookie: 'new-cookie' },
1265
+ });
1266
+ const destinationDirectory = join(tempDir, 'export');
1267
+ const destination = join(destinationDirectory, STORE_FILENAME);
1268
+ stampDataFormatVersion(destinationDirectory);
1269
+ new EncryptedStorage(NEW_ENCRYPTION_KEY).writeFile(destination, JSON.stringify(nestAccounts({
1270
+ slack: { objectType: 'slack', token: 'old-tok', dCookie: 'old-cookie' },
1271
+ discord: { objectType: 'authorizationBearer', token: 'discord-token' },
1272
+ })));
1273
+ const deps = withStdinKey(NEW_ENCRYPTION_KEY);
1274
+ await runCommand(['auth', 're-encrypt', destinationDirectory], deps);
1275
+ expect(exitCode).toBeNull();
1276
+ const reEncrypted = readWithKey(destination, NEW_ENCRYPTION_KEY);
1277
+ expect(Object.keys(reEncrypted).sort()).toEqual(['discord', 'slack']);
1278
+ expect(reEncrypted.slack).toEqual({
1279
+ objectType: 'slack',
1280
+ token: 'new-tok',
1281
+ dCookie: 'new-cookie',
1282
+ });
1283
+ expect(reEncrypted.discord).toEqual({
1284
+ objectType: 'authorizationBearer',
1285
+ token: 'discord-token',
1286
+ });
1287
+ });
1288
+ it('errors when the existing credential store cannot be read with the new key', async () => {
1053
1289
  writeStore({ slack: { objectType: 'slack', token: 'tok', dCookie: 'cookie' } });
1054
1290
  const destinationDirectory = join(tempDir, 'export');
1055
- mkdirSync(destinationDirectory);
1291
+ stampDataFormatVersion(destinationDirectory);
1056
1292
  const destination = join(destinationDirectory, STORE_FILENAME);
1057
1293
  writeFileSync(destination, 'existing');
1058
1294
  const deps = withStdinKey(NEW_ENCRYPTION_KEY);
@@ -1060,6 +1296,36 @@ describe('CLI commands with dependency injection', () => {
1060
1296
  expect(exitCode).toBe(1);
1061
1297
  expect(readFileSync(destination, 'utf-8')).toBe('existing');
1062
1298
  });
1299
+ it('re-encrypts the browser state with --browser-state', async () => {
1300
+ writeStore({ slack: { objectType: 'slack', token: 'tok', dCookie: 'cookie' } });
1301
+ new EncryptedStorage(TEST_ENCRYPTION_KEY).writeFile(join(tempDir, BROWSER_STATE_FILENAME), '{"cookies":[]}');
1302
+ const destinationDirectory = join(tempDir, 'export');
1303
+ const deps = withStdinKey(NEW_ENCRYPTION_KEY);
1304
+ await runCommand(['auth', 're-encrypt', destinationDirectory, '--browser-state'], deps);
1305
+ expect(exitCode).toBeNull();
1306
+ const browserStateDestination = join(destinationDirectory, BROWSER_STATE_FILENAME);
1307
+ expect(new EncryptedStorage(NEW_ENCRYPTION_KEY).readFile(browserStateDestination)).toBe('{"cookies":[]}');
1308
+ expect(Object.keys(readWithKey(join(destinationDirectory, STORE_FILENAME), NEW_ENCRYPTION_KEY))).toEqual(['slack']);
1309
+ });
1310
+ it('errors with --browser-state when there is no browser state', async () => {
1311
+ writeStore({ slack: { objectType: 'slack', token: 'tok', dCookie: 'cookie' } });
1312
+ const destinationDirectory = join(tempDir, 'export');
1313
+ const deps = withStdinKey(NEW_ENCRYPTION_KEY);
1314
+ await runCommand(['auth', 're-encrypt', destinationDirectory, '--browser-state'], deps);
1315
+ expect(exitCode).toBe(1);
1316
+ expect(existsSync(join(destinationDirectory, STORE_FILENAME))).toBe(false);
1317
+ });
1318
+ it('overwrites an existing browser state in the destination', async () => {
1319
+ writeStore({});
1320
+ new EncryptedStorage(TEST_ENCRYPTION_KEY).writeFile(join(tempDir, BROWSER_STATE_FILENAME), '{"cookies":["new"]}');
1321
+ const destinationDirectory = join(tempDir, 'export');
1322
+ const browserStateDestination = join(destinationDirectory, BROWSER_STATE_FILENAME);
1323
+ new EncryptedStorage(NEW_ENCRYPTION_KEY).writeFile(browserStateDestination, 'old');
1324
+ const deps = withStdinKey(NEW_ENCRYPTION_KEY);
1325
+ await runCommand(['auth', 're-encrypt', destinationDirectory, '--browser-state'], deps);
1326
+ expect(exitCode).toBeNull();
1327
+ expect(new EncryptedStorage(NEW_ENCRYPTION_KEY).readFile(browserStateDestination)).toBe('{"cookies":["new"]}');
1328
+ });
1063
1329
  it('errors when the destination is an existing file, not a directory', async () => {
1064
1330
  writeStore({ slack: { objectType: 'slack', token: 'tok', dCookie: 'cookie' } });
1065
1331
  const destinationDirectory = join(tempDir, 'export');