@forge/cli-shared 9.10.0-next.1 → 9.10.0-next.1-experimental-1e95696

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/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # @forge/cli-shared
2
2
 
3
+ ## 9.10.0-next.1-experimental-1e95696
4
+
5
+ ### Major Changes
6
+
7
+ - ffd952f: Replace Keytar with a dedicated credential file protected by macOS and Linux permissions or Windows ACLs. This is a breaking authentication change: users with credentials stored in the OS keychain must run `forge login` once after upgrading. `FORGE_EMAIL` and `FORGE_API_TOKEN` continue to take precedence over saved credentials.
8
+
9
+ ### Minor Changes
10
+
11
+ - e865360: Bump @forge/util with latest aris and dependencies
12
+
13
+ ### Patch Changes
14
+
15
+ - Updated dependencies [e865360]
16
+ - Updated dependencies [5d8a857]
17
+ - @forge/util@3.1.0-next.0-experimental-1e95696
18
+ - @forge/manifest@13.8.1-next.0-experimental-1e95696
19
+
3
20
  ## 9.10.0-next.1
4
21
 
5
22
  ### Minor Changes
@@ -1 +1 @@
1
- {"version":3,"file":"get-app-owner.d.ts","sourceRoot":"","sources":["../../src/apps/get-app-owner.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAA4B,MAAM,YAAY,CAAC;AAErE,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAEjD,qBAAa,oBAAqB,SAAQ,KAAK;;CAI9C;AAED,eAAO,MAAM,aAAa;QAuC+sx2G,8CAAc;GA9Brvx2G,CAAC;AAEH,qBAAa,gBAAgB;IAEzB,OAAO,CAAC,QAAQ,CAAC,aAAa;IAC9B,OAAO,CAAC,QAAQ,CAAC,YAAY;gBADZ,aAAa,EAAE,aAAa,EAC5B,YAAY,EAAE,iBAAiB;IAGrC,OAAO;;;;;;;;;;;;;YAMN,WAAW;CAe1B"}
1
+ {"version":3,"file":"get-app-owner.d.ts","sourceRoot":"","sources":["../../src/apps/get-app-owner.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAA4B,MAAM,YAAY,CAAC;AAErE,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAEjD,qBAAa,oBAAqB,SAAQ,KAAK;;CAI9C;AAED,eAAO,MAAM,aAAa;QAuC0tz4G,8CAAc;GA9Bhwz4G,CAAC;AAEH,qBAAa,gBAAgB;IAEzB,OAAO,CAAC,QAAQ,CAAC,aAAa;IAC9B,OAAO,CAAC,QAAQ,CAAC,YAAY;gBADZ,aAAa,EAAE,aAAa,EAC5B,YAAY,EAAE,iBAAiB;IAGrC,OAAO;;;;;;;;;;;;;YAMN,WAAW;CAe1B"}
@@ -1,15 +1,8 @@
1
1
  import * as t from 'io-ts';
2
- import { CachedConf } from '../../cache';
3
2
  import { UserError } from '../../shared';
4
- import { Logger } from '../../ui';
5
3
  import { UserRepository } from './user-repository';
6
4
  export declare const EMAIL_KEY = "FORGE_EMAIL";
7
5
  export declare const API_TOKEN_KEY = "FORGE_API_TOKEN";
8
- export interface Keytar {
9
- getPassword(service: string, account: string): Promise<string | null>;
10
- setPassword(service: string, account: string, password: string): Promise<void>;
11
- deletePassword(service: string, account: string): Promise<void>;
12
- }
13
6
  export declare const personalApiCredentialsShape: t.TypeC<{
14
7
  email: t.StringC;
15
8
  token: t.StringC;
@@ -24,11 +17,9 @@ export type PersonalApiCredentialsValidated = t.TypeOf<typeof personalApiCredent
24
17
  export declare class NoTokenInStoreError extends UserError {
25
18
  constructor();
26
19
  }
27
- export declare class NoKeytarError extends UserError {
20
+ export declare class CredentialStoreSecurityError extends UserError {
28
21
  constructor();
29
22
  }
30
- export declare class KeytarAccessError extends UserError {
31
- }
32
23
  export interface CredentialGetter {
33
24
  getCredentials(): Promise<PersonalApiCredentialsValidated>;
34
25
  }
@@ -36,22 +27,25 @@ export interface CredentialStore extends CredentialGetter {
36
27
  setCredentials(credentials: PersonalApiCredentialsValidated): Promise<void>;
37
28
  deleteCredentials(): Promise<void>;
38
29
  }
39
- export declare class CredentialStoreImpl implements CredentialStore {
40
- private readonly logger;
41
- private readonly instructionsURL;
42
- private readonly keytar;
43
- private readonly cachedConfig;
30
+ export type CredentialStoreCommandRunner = (command: string, args: readonly string[]) => Promise<string>;
31
+ export declare function getCredentialsFilePath(currentPlatform?: NodeJS.Platform, environment?: NodeJS.ProcessEnv, userHome?: string): string;
32
+ export declare class FileCredentialStore implements CredentialStore {
44
33
  private readonly userRepository;
45
- private keytarAccount;
46
- constructor(logger: Logger, instructionsURL: string, keytar: Keytar | null, cachedConfig: CachedConf, userRepository: UserRepository);
47
- private wrapKeytarError;
48
- decodeCredentials(credentialsString: string | null): PersonalApiCredentialsValidated;
49
- getCredentialsKeytar(keytar: Keytar): Promise<PersonalApiCredentialsValidated | undefined>;
50
- popInsecurePlaintextCredentials(): PersonalApiCredentialsValidated | undefined;
51
- deleteInsecurePlaintextCredentials(): void;
34
+ private readonly account;
35
+ private readonly credentialsFilePath;
36
+ private readonly currentPlatform;
37
+ private readonly commandRunner;
38
+ private windowsUserSid;
39
+ constructor(userRepository: UserRepository, account: string, credentialsFilePath?: string, currentPlatform?: NodeJS.Platform, commandRunner?: CredentialStoreCommandRunner);
40
+ private getWindowsUserSid;
41
+ private applyWindowsAcl;
42
+ private secureWindowsStorage;
43
+ private securePosixStorage;
44
+ private secureStorage;
45
+ private readCredentialsFile;
52
46
  getCredentials(): Promise<PersonalApiCredentialsValidated>;
53
47
  setCredentials(credentials: PersonalApiCredentialsValidated): Promise<void>;
54
48
  deleteCredentials(): Promise<void>;
55
49
  }
56
- export declare function getCredentialStore(logger: Logger, userRepository: UserRepository): CredentialStore;
50
+ export declare function getCredentialStore(userRepository: UserRepository): CredentialStore;
57
51
  //# sourceMappingURL=credential-store.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"credential-store.d.ts","sourceRoot":"","sources":["../../../src/auth/personal/credential-store.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,CAAC,MAAM,OAAO,CAAC;AAC3B,OAAO,EAAuB,UAAU,EAAE,MAAM,aAAa,CAAC;AAG9D,OAAO,EAAE,SAAS,EAAa,MAAM,cAAc,CAAC;AACpD,OAAO,EAAE,MAAM,EAAQ,MAAM,UAAU,CAAC;AACxC,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAEnD,eAAO,MAAM,SAAS,gBAAgB,CAAC;AACvC,eAAO,MAAM,aAAa,oBAAoB,CAAC;AAa/C,MAAM,WAAW,MAAM;IACrB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAEtE,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE/E,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACjE;AAUD,eAAO,MAAM,2BAA2B;;;EAGtC,CAAC;AAEH,eAAO,MAAM,oCAAoC;;;;EAI/C,CAAC;AAEH,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,2BAA2B,CAAC,CAAC;AAClF,MAAM,MAAM,+BAA+B,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,oCAAoC,CAAC,CAAC;AAEpG,qBAAa,mBAAoB,SAAQ,SAAS;;CAIjD;AAED,qBAAa,aAAc,SAAQ,SAAS;;CAI3C;AAED,qBAAa,iBAAkB,SAAQ,SAAS;CAAG;AAEnD,MAAM,WAAW,gBAAgB;IAC/B,cAAc,IAAI,OAAO,CAAC,+BAA+B,CAAC,CAAC;CAC5D;AAED,MAAM,WAAW,eAAgB,SAAQ,gBAAgB;IACvD,cAAc,CAAC,WAAW,EAAE,+BAA+B,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE5E,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACpC;AAED,qBAAa,mBAAoB,YAAW,eAAe;IAIvD,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,eAAe;IAChC,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,YAAY;IAC7B,OAAO,CAAC,QAAQ,CAAC,cAAc;IAPjC,OAAO,CAAC,aAAa,CAAS;gBAGX,MAAM,EAAE,MAAM,EACd,eAAe,EAAE,MAAM,EACvB,MAAM,EAAE,MAAM,GAAG,IAAI,EACrB,YAAY,EAAE,UAAU,EACxB,cAAc,EAAE,cAAc;IAKjD,OAAO,CAAC,eAAe;IAoBvB,iBAAiB,CAAC,iBAAiB,EAAE,MAAM,GAAG,IAAI,GAAG,+BAA+B;IAoB9E,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,+BAA+B,GAAG,SAAS,CAAC;IAehG,+BAA+B,IAAI,+BAA+B,GAAG,SAAS;IAS9E,kCAAkC,IAAI,IAAI;IAIpC,cAAc,IAAI,OAAO,CAAC,+BAA+B,CAAC;IAgC1D,cAAc,CAAC,WAAW,EAAE,+BAA+B,GAAG,OAAO,CAAC,IAAI,CAAC;IAY3E,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC;CAWzC;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,cAAc,GAAG,eAAe,CAKlG"}
1
+ {"version":3,"file":"credential-store.d.ts","sourceRoot":"","sources":["../../../src/auth/personal/credential-store.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,CAAC,MAAM,OAAO,CAAC;AAE3B,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzC,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAEnD,eAAO,MAAM,SAAS,gBAAgB,CAAC;AACvC,eAAO,MAAM,aAAa,oBAAoB,CAAC;AAiB/C,eAAO,MAAM,2BAA2B;;;EAGtC,CAAC;AAEH,eAAO,MAAM,oCAAoC;;;;EAI/C,CAAC;AAEH,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,2BAA2B,CAAC,CAAC;AAClF,MAAM,MAAM,+BAA+B,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,oCAAoC,CAAC,CAAC;AAEpG,qBAAa,mBAAoB,SAAQ,SAAS;;CAIjD;AAED,qBAAa,4BAA6B,SAAQ,SAAS;;CAI1D;AAED,MAAM,WAAW,gBAAgB;IAC/B,cAAc,IAAI,OAAO,CAAC,+BAA+B,CAAC,CAAC;CAC5D;AAED,MAAM,WAAW,eAAgB,SAAQ,gBAAgB;IACvD,cAAc,CAAC,WAAW,EAAE,+BAA+B,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE5E,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACpC;AAID,MAAM,MAAM,4BAA4B,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;AAezG,wBAAgB,sBAAsB,CACpC,eAAe,kBAAa,EAC5B,WAAW,GAAE,MAAM,CAAC,UAAwB,EAC5C,QAAQ,SAAY,GACnB,MAAM,CAwBR;AAED,qBAAa,mBAAoB,YAAW,eAAe;IAIvD,OAAO,CAAC,QAAQ,CAAC,cAAc;IAC/B,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,mBAAmB;IACpC,OAAO,CAAC,QAAQ,CAAC,eAAe;IAChC,OAAO,CAAC,QAAQ,CAAC,aAAa;IAPhC,OAAO,CAAC,cAAc,CAA8B;gBAGjC,cAAc,EAAE,cAAc,EAC9B,OAAO,EAAE,MAAM,EACf,mBAAmB,SAA2B,EAC9C,eAAe,kBAAa,EAC5B,aAAa,GAAE,4BAAwD;YAG5E,iBAAiB;YAWjB,eAAe;YAaf,oBAAoB;YA0BpB,kBAAkB;YAiDlB,aAAa;YAab,mBAAmB;IA4B3B,cAAc,IAAI,OAAO,CAAC,+BAA+B,CAAC;IAkB1D,cAAc,CAAC,WAAW,EAAE,+BAA+B,GAAG,OAAO,CAAC,IAAI,CAAC;IAiB3E,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC;CAkBzC;AAED,wBAAgB,kBAAkB,CAAC,cAAc,EAAE,cAAc,GAAG,eAAe,CAElF"}
@@ -1,36 +1,35 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.CredentialStoreImpl = exports.KeytarAccessError = exports.NoKeytarError = exports.NoTokenInStoreError = exports.personalApiCredentialsValidatedShape = exports.personalApiCredentialsShape = exports.API_TOKEN_KEY = exports.EMAIL_KEY = void 0;
3
+ exports.FileCredentialStore = exports.CredentialStoreSecurityError = exports.NoTokenInStoreError = exports.personalApiCredentialsValidatedShape = exports.personalApiCredentialsShape = exports.API_TOKEN_KEY = exports.EMAIL_KEY = void 0;
4
+ exports.getCredentialsFilePath = getCredentialsFilePath;
4
5
  exports.getCredentialStore = getCredentialStore;
5
6
  const tslib_1 = require("tslib");
7
+ const child_process_1 = require("child_process");
8
+ const fs_1 = require("fs");
6
9
  const os_1 = require("os");
10
+ const path_1 = tslib_1.__importDefault(require("path"));
11
+ const util_1 = require("util");
7
12
  const Either_1 = require("fp-ts/lib/Either");
8
13
  const t = tslib_1.__importStar(require("io-ts"));
9
- const cache_1 = require("../../cache");
10
- const instruction_1 = require("../instruction");
11
14
  const graphql_1 = require("../../graphql");
12
15
  const shared_1 = require("../../shared");
13
16
  const ui_1 = require("../../ui");
14
17
  exports.EMAIL_KEY = 'FORGE_EMAIL';
15
18
  exports.API_TOKEN_KEY = 'FORGE_API_TOKEN';
16
- const KEYTAR_SERVICE = 'Atlassian';
17
- const CACHE_CREDENTIALS_KEY = 'forge-credentials';
18
- var KeytarAccount;
19
- (function (KeytarAccount) {
20
- KeytarAccount["DEV"] = "Ecosystem-staging";
21
- KeytarAccount["STG"] = "Ecosystem-staging";
22
- KeytarAccount["PROD"] = "Ecosystem";
23
- KeytarAccount["FEDRAMP_MODERATE_STAGING"] = "Ecosystem-fedm-staging";
24
- KeytarAccount["FEDRAMP_MODERATE_PROD"] = "Ecosystem-fedm-prod";
25
- })(KeytarAccount || (KeytarAccount = {}));
26
- function getKeytar() {
27
- try {
28
- return require('keytar');
29
- }
30
- catch {
31
- return null;
32
- }
33
- }
19
+ const CREDENTIALS_FILE_NAME = 'credentials.json';
20
+ const CREDENTIALS_DIRECTORY_MODE = 0o700;
21
+ const CREDENTIALS_FILE_MODE = 0o600;
22
+ const PERMISSION_BITS_MASK = 0o777;
23
+ const WINDOWS_SYSTEM_SID = 'S-1-5-18';
24
+ const WINDOWS_ADMINISTRATORS_SID = 'S-1-5-32-544';
25
+ var CredentialAccount;
26
+ (function (CredentialAccount) {
27
+ CredentialAccount["DEV"] = "Ecosystem-staging";
28
+ CredentialAccount["STG"] = "Ecosystem-staging";
29
+ CredentialAccount["PROD"] = "Ecosystem";
30
+ CredentialAccount["FEDRAMP_MODERATE_STAGING"] = "Ecosystem-fedm-staging";
31
+ CredentialAccount["FEDRAMP_MODERATE_PROD"] = "Ecosystem-fedm-prod";
32
+ })(CredentialAccount || (CredentialAccount = {}));
34
33
  exports.personalApiCredentialsShape = t.type({
35
34
  email: t.string,
36
35
  token: t.string
@@ -46,146 +45,223 @@ class NoTokenInStoreError extends shared_1.UserError {
46
45
  }
47
46
  }
48
47
  exports.NoTokenInStoreError = NoTokenInStoreError;
49
- class NoKeytarError extends shared_1.UserError {
48
+ class CredentialStoreSecurityError extends shared_1.UserError {
50
49
  constructor() {
51
- super(ui_1.Text.error.noKeytar);
50
+ super(ui_1.Text.error.credentialsFilePermissions);
52
51
  }
53
52
  }
54
- exports.NoKeytarError = NoKeytarError;
55
- class KeytarAccessError extends shared_1.UserError {
53
+ exports.CredentialStoreSecurityError = CredentialStoreSecurityError;
54
+ const execFileAsync = (0, util_1.promisify)(child_process_1.execFile);
55
+ const runCredentialStoreCommand = async (command, args) => {
56
+ const { stdout } = await execFileAsync(command, [...args], { encoding: 'utf8', windowsHide: true });
57
+ return stdout;
58
+ };
59
+ const credentialsByAccountShape = t.record(t.string, exports.personalApiCredentialsValidatedShape);
60
+ function isFileNotFoundError(error) {
61
+ return typeof error === 'object' && error !== null && 'code' in error && error.code === 'ENOENT';
56
62
  }
57
- exports.KeytarAccessError = KeytarAccessError;
58
- class CredentialStoreImpl {
59
- logger;
60
- instructionsURL;
61
- keytar;
62
- cachedConfig;
63
+ function getCredentialsFilePath(currentPlatform = (0, os_1.platform)(), environment = process.env, userHome = (0, os_1.homedir)()) {
64
+ const platformPath = currentPlatform === 'win32' ? path_1.default.win32 : path_1.default.posix;
65
+ let credentialsDirectory;
66
+ switch (currentPlatform) {
67
+ case 'darwin':
68
+ credentialsDirectory = platformPath.join(userHome, 'Library', 'Application Support', 'Atlassian', 'Forge');
69
+ break;
70
+ case 'win32':
71
+ credentialsDirectory = platformPath.join(environment.LOCALAPPDATA || platformPath.join(userHome, 'AppData', 'Local'), 'Atlassian', 'Forge');
72
+ break;
73
+ default:
74
+ credentialsDirectory = platformPath.join(environment.XDG_DATA_HOME || platformPath.join(userHome, '.local', 'share'), 'Atlassian', 'Forge');
75
+ }
76
+ return platformPath.join(credentialsDirectory, CREDENTIALS_FILE_NAME);
77
+ }
78
+ class FileCredentialStore {
63
79
  userRepository;
64
- keytarAccount;
65
- constructor(logger, instructionsURL, keytar, cachedConfig, userRepository) {
66
- this.logger = logger;
67
- this.instructionsURL = instructionsURL;
68
- this.keytar = keytar;
69
- this.cachedConfig = cachedConfig;
80
+ account;
81
+ credentialsFilePath;
82
+ currentPlatform;
83
+ commandRunner;
84
+ windowsUserSid;
85
+ constructor(userRepository, account, credentialsFilePath = getCredentialsFilePath(), currentPlatform = (0, os_1.platform)(), commandRunner = runCredentialStoreCommand) {
70
86
  this.userRepository = userRepository;
71
- this.keytarAccount = (0, graphql_1.getEnvironmentConfig)(KeytarAccount);
87
+ this.account = account;
88
+ this.credentialsFilePath = credentialsFilePath;
89
+ this.currentPlatform = currentPlatform;
90
+ this.commandRunner = commandRunner;
91
+ }
92
+ async getWindowsUserSid() {
93
+ this.windowsUserSid ??= this.commandRunner('whoami.exe', ['/user', '/fo', 'csv', '/nh']).then((output) => {
94
+ const sid = output.trim().match(/"?(S-\d(?:-\d+)+)"?$/)?.[1];
95
+ if (!sid) {
96
+ throw new Error('Could not determine the current Windows user SID');
97
+ }
98
+ return sid;
99
+ });
100
+ return this.windowsUserSid;
72
101
  }
73
- wrapKeytarError(e) {
74
- const message = (0, shared_1.wrapError)(e).message;
75
- let error;
76
- const currentPlatform = (0, os_1.platform)();
77
- switch (currentPlatform) {
78
- case 'darwin':
79
- error = ui_1.Text.error.keytarAccessError.mac(message);
80
- break;
81
- case 'linux':
82
- error = ui_1.Text.error.keytarAccessError.linux(message);
83
- break;
84
- case 'win32':
85
- error = ui_1.Text.error.keytarAccessError.windows(message);
86
- break;
87
- default:
88
- error = ui_1.Text.error.keytarAccessError.other(message);
89
- }
90
- throw new KeytarAccessError(error);
102
+ async applyWindowsAcl(targetPath, directory) {
103
+ const userSid = await this.getWindowsUserSid();
104
+ const permission = directory ? '(OI)(CI)F' : 'F';
105
+ await this.commandRunner('icacls.exe', [
106
+ targetPath,
107
+ '/inheritance:r',
108
+ '/grant:r',
109
+ `*${userSid}:${permission}`,
110
+ `*${WINDOWS_SYSTEM_SID}:${permission}`,
111
+ `*${WINDOWS_ADMINISTRATORS_SID}:${permission}`
112
+ ]);
91
113
  }
92
- decodeCredentials(credentialsString) {
93
- if (!credentialsString) {
94
- throw new NoTokenInStoreError();
95
- }
96
- let credentials;
114
+ async secureWindowsStorage(createFile) {
115
+ let fileExists = true;
97
116
  try {
98
- credentials = JSON.parse(credentialsString);
117
+ await fs_1.promises.access(this.credentialsFilePath);
99
118
  }
100
- catch {
101
- throw new NoTokenInStoreError();
119
+ catch (error) {
120
+ if (!isFileNotFoundError(error)) {
121
+ throw error;
122
+ }
123
+ fileExists = false;
102
124
  }
103
- const decodeResult = exports.personalApiCredentialsValidatedShape.decode(credentials);
104
- if ((0, Either_1.isLeft)(decodeResult)) {
105
- throw new NoTokenInStoreError();
125
+ if (!fileExists && !createFile) {
126
+ return;
106
127
  }
107
- return decodeResult.right;
128
+ const credentialsDirectory = path_1.default.dirname(this.credentialsFilePath);
129
+ await fs_1.promises.mkdir(credentialsDirectory, { recursive: true });
130
+ await this.applyWindowsAcl(credentialsDirectory, true);
131
+ if (!fileExists) {
132
+ await fs_1.promises.writeFile(this.credentialsFilePath, '', { flag: 'wx' });
133
+ }
134
+ await this.applyWindowsAcl(this.credentialsFilePath, false);
108
135
  }
109
- async getCredentialsKeytar(keytar) {
110
- let stringifiedPersonalApiCredentials;
136
+ async securePosixStorage(createFile) {
137
+ let fileExists = true;
111
138
  try {
112
- stringifiedPersonalApiCredentials = await keytar.getPassword(KEYTAR_SERVICE, this.keytarAccount);
139
+ await fs_1.promises.access(this.credentialsFilePath);
113
140
  }
114
- catch (e) {
115
- this.wrapKeytarError(e);
141
+ catch (error) {
142
+ if (!isFileNotFoundError(error)) {
143
+ throw error;
144
+ }
145
+ fileExists = false;
116
146
  }
117
- if (!stringifiedPersonalApiCredentials) {
118
- return undefined;
147
+ if (!fileExists && !createFile) {
148
+ return;
119
149
  }
120
- return this.decodeCredentials(stringifiedPersonalApiCredentials);
121
- }
122
- popInsecurePlaintextCredentials() {
123
- const result = this.cachedConfig.get(CACHE_CREDENTIALS_KEY);
124
- if (result) {
125
- this.logger.warn(ui_1.Text.warning.plaintextCredentialsFound(this.instructionsURL));
126
- this.deleteInsecurePlaintextCredentials();
150
+ const credentialsDirectory = path_1.default.dirname(this.credentialsFilePath);
151
+ await fs_1.promises.mkdir(credentialsDirectory, {
152
+ recursive: true,
153
+ mode: CREDENTIALS_DIRECTORY_MODE
154
+ });
155
+ await fs_1.promises.chmod(credentialsDirectory, CREDENTIALS_DIRECTORY_MODE);
156
+ const directoryStat = await fs_1.promises.stat(credentialsDirectory);
157
+ const currentUserId = process.getuid?.();
158
+ if (currentUserId === undefined ||
159
+ !directoryStat.isDirectory() ||
160
+ directoryStat.uid !== currentUserId ||
161
+ (directoryStat.mode & PERMISSION_BITS_MASK) !== CREDENTIALS_DIRECTORY_MODE) {
162
+ throw new Error('The credentials directory is not owned exclusively by the current user');
163
+ }
164
+ if (!fileExists) {
165
+ const file = await fs_1.promises.open(this.credentialsFilePath, 'wx', CREDENTIALS_FILE_MODE);
166
+ await file.close();
167
+ }
168
+ await fs_1.promises.chmod(this.credentialsFilePath, CREDENTIALS_FILE_MODE);
169
+ const fileStat = await fs_1.promises.stat(this.credentialsFilePath);
170
+ if (!fileStat.isFile() ||
171
+ fileStat.uid !== currentUserId ||
172
+ (fileStat.mode & PERMISSION_BITS_MASK) !== CREDENTIALS_FILE_MODE) {
173
+ throw new Error('Could not restrict access to the credentials file');
127
174
  }
128
- return result;
129
- }
130
- deleteInsecurePlaintextCredentials() {
131
- this.cachedConfig.delete(CACHE_CREDENTIALS_KEY);
132
175
  }
133
- async getCredentials() {
134
- let result;
135
- const plaintextCredentials = this.popInsecurePlaintextCredentials();
136
- if (plaintextCredentials) {
137
- if (this.keytar) {
138
- await this.setCredentials(plaintextCredentials);
139
- this.logger.warn(ui_1.Text.warning.plaintextCredentialsMigrated);
176
+ async secureStorage(createFile) {
177
+ try {
178
+ if (this.currentPlatform === 'win32') {
179
+ await this.secureWindowsStorage(createFile);
140
180
  }
141
181
  else {
142
- this.logger.warn(ui_1.Text.warning.plaintextCredentialsNotMigrated);
182
+ await this.securePosixStorage(createFile);
183
+ }
184
+ }
185
+ catch {
186
+ await fs_1.promises.rm(this.credentialsFilePath, { force: true }).catch(() => undefined);
187
+ throw new CredentialStoreSecurityError();
188
+ }
189
+ }
190
+ async readCredentialsFile() {
191
+ let contents;
192
+ try {
193
+ await this.secureStorage(false);
194
+ contents = await fs_1.promises.readFile(this.credentialsFilePath, 'utf8');
195
+ }
196
+ catch (error) {
197
+ if (isFileNotFoundError(error)) {
198
+ return {};
143
199
  }
200
+ throw error;
201
+ }
202
+ let parsed;
203
+ try {
204
+ parsed = JSON.parse(contents);
205
+ }
206
+ catch {
207
+ throw new NoTokenInStoreError();
208
+ }
209
+ const decodeResult = credentialsByAccountShape.decode(parsed);
210
+ if ((0, Either_1.isLeft)(decodeResult)) {
211
+ throw new NoTokenInStoreError();
144
212
  }
213
+ return decodeResult.right;
214
+ }
215
+ async getCredentials() {
145
216
  if (process.env[exports.API_TOKEN_KEY] && process.env[exports.EMAIL_KEY]) {
146
217
  const credentials = {
147
218
  email: process.env[exports.EMAIL_KEY],
148
219
  token: process.env[exports.API_TOKEN_KEY]
149
220
  };
150
221
  const { accountId } = await this.userRepository.getUser(credentials);
151
- result = { ...credentials, accountId };
152
- }
153
- else if (this.keytar) {
154
- result = await this.getCredentialsKeytar(this.keytar);
222
+ return { ...credentials, accountId };
155
223
  }
156
- if (result) {
157
- return result;
224
+ const credentials = (await this.readCredentialsFile())[this.account];
225
+ if (!credentials) {
226
+ throw new NoTokenInStoreError();
158
227
  }
159
- throw new NoTokenInStoreError();
228
+ return credentials;
160
229
  }
161
230
  async setCredentials(credentials) {
162
- if (this.keytar) {
163
- try {
164
- await this.keytar.setPassword(KEYTAR_SERVICE, this.keytarAccount, JSON.stringify(credentials));
165
- }
166
- catch (e) {
167
- throw new NoKeytarError();
168
- }
231
+ let credentialsByAccount;
232
+ try {
233
+ credentialsByAccount = await this.readCredentialsFile();
169
234
  }
170
- else {
171
- throw new NoKeytarError();
235
+ catch (error) {
236
+ if (!(error instanceof NoTokenInStoreError)) {
237
+ throw error;
238
+ }
239
+ credentialsByAccount = {};
172
240
  }
241
+ credentialsByAccount[this.account] = credentials;
242
+ await this.secureStorage(true);
243
+ await fs_1.promises.writeFile(this.credentialsFilePath, `${JSON.stringify(credentialsByAccount, null, 2)}\n`, 'utf8');
244
+ await this.secureStorage(false);
173
245
  }
174
246
  async deleteCredentials() {
175
- if (this.keytar) {
247
+ const credentialsByAccount = await this.readCredentialsFile();
248
+ delete credentialsByAccount[this.account];
249
+ if (Object.keys(credentialsByAccount).length === 0) {
176
250
  try {
177
- await this.keytar.deletePassword(KEYTAR_SERVICE, this.keytarAccount);
251
+ await fs_1.promises.unlink(this.credentialsFilePath);
178
252
  }
179
- catch (e) {
253
+ catch (error) {
254
+ if (!isFileNotFoundError(error)) {
255
+ throw error;
256
+ }
180
257
  }
258
+ return;
181
259
  }
182
- this.deleteInsecurePlaintextCredentials();
260
+ await fs_1.promises.writeFile(this.credentialsFilePath, `${JSON.stringify(credentialsByAccount, null, 2)}\n`, 'utf8');
261
+ await this.secureStorage(false);
183
262
  }
184
263
  }
185
- exports.CredentialStoreImpl = CredentialStoreImpl;
186
- function getCredentialStore(logger, userRepository) {
187
- const keytar = getKeytar();
188
- const cachedConfig = cache_1.CachedConf.getCache(cache_1.CONFIG_PROJECT_NAME);
189
- const instructionsUrl = (0, instruction_1.getInstructionsUrl)();
190
- return new CredentialStoreImpl(logger, instructionsUrl, keytar, cachedConfig, userRepository);
264
+ exports.FileCredentialStore = FileCredentialStore;
265
+ function getCredentialStore(userRepository) {
266
+ return new FileCredentialStore(userRepository, (0, graphql_1.getEnvironmentConfig)(CredentialAccount));
191
267
  }
@@ -19,7 +19,7 @@ class PersonalTokenAuthenticator {
19
19
  if (process.env[credential_store_1.API_TOKEN_KEY] && process.env[credential_store_1.EMAIL_KEY]) {
20
20
  return ui_1.Text.error.invalidTokenInEnv(instructionsUrl, credential_store_1.EMAIL_KEY, credential_store_1.API_TOKEN_KEY);
21
21
  }
22
- return ui_1.Text.error.invalidTokenInKeytar;
22
+ return ui_1.Text.error.invalidTokenInCredentialFile;
23
23
  }
24
24
  }
25
25
  exports.PersonalTokenAuthenticator = PersonalTokenAuthenticator;