Skip to content

Commit 336ae05

Browse files
committed
refactor credential manager as a singleton class instance
1 parent 3d3ea65 commit 336ae05

File tree

3 files changed

+124
-187
lines changed

3 files changed

+124
-187
lines changed

packages/code-infra/src/utils/changelog.mjs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Octokit } from '@octokit/rest';
22
import { $ } from 'execa';
33

4-
import { peristentAuthStrategy } from './github.mjs';
4+
import { persistentAuthStrategy } from './github.mjs';
55

66
/**
77
* @typedef {'team' | 'first_timer' | 'contributor'} AuthorAssociation
@@ -66,7 +66,7 @@ export async function fetchCommitsBetweenRefs({ org = 'mui', ...options }) {
6666
async function fetchCommitsRest({ token, repo, lastRelease, release, org }) {
6767
const octokit = token
6868
? new Octokit({ auth: token })
69-
: new Octokit({ authStrategy: peristentAuthStrategy });
69+
: new Octokit({ authStrategy: persistentAuthStrategy });
7070
/**
7171
* @typedef {import('@octokit/rest').Octokit} Octokit
7272
*/

packages/code-infra/src/utils/credentials.mjs

Lines changed: 57 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -10,129 +10,77 @@
1010

1111
import { AsyncEntry } from '@napi-rs/keyring';
1212

13-
const KEYRING_SERVICE = 'mui-code-infra';
14-
const REGISTRY_KEY = '__credential_registry__';
13+
export const KEYRING_SERVICE = 'mui-code-infra';
1514

1615
export const ERRORS = {
1716
NOT_FOUND: 'CREDENTIAL_NOT_FOUND',
1817
STORAGE_FAILURE: 'CREDENTIAL_STORAGE_FAILURE',
1918
};
2019

21-
/**
22-
* Gets all stored credentials from the registry
23-
* @returns {Promise<Record<string, string>>} Object containing all credentials
24-
*/
25-
export async function getCredentialData() {
26-
try {
27-
const registryEntry = new AsyncEntry(KEYRING_SERVICE, REGISTRY_KEY);
28-
const registryData = await registryEntry.getPassword();
29-
return registryData ? JSON.parse(registryData) : {};
30-
} catch (error) {
31-
return {}; // Registry doesn't exist yet
20+
class CredentialManager {
21+
/**
22+
* @type {Map<string, AsyncEntry>}
23+
*/
24+
#credentials = new Map();
25+
26+
/**
27+
* @type {Map<string, string>} In-memory cache for credentials that have been accessed.
28+
* This is used to avoid multiple prompts for the same credential during a single run.
29+
* Note: This cache is not persisted and will be cleared when the process exits
30+
*/
31+
#accessedCredentials = new Map();
32+
33+
constructor(serviceName = KEYRING_SERVICE) {
34+
this.serviceName = serviceName;
3235
}
33-
}
3436

35-
/**
36-
* Updates the registry with all credential data
37-
* @param {Record<string, string>} credentials - Object containing all credentials
38-
* @returns {Promise<void>}
39-
*/
40-
async function updateCredentialData(credentials) {
41-
try {
42-
const registryEntry = new AsyncEntry(KEYRING_SERVICE, REGISTRY_KEY);
43-
await registryEntry.setPassword(JSON.stringify(credentials));
44-
} catch (/** @type {any} */ error) {
45-
throw new Error(`Failed to update credential data: ${error.message}`);
46-
}
47-
}
48-
49-
/**
50-
* Gets a credential from the stored JSON data
51-
* @param {string} key - The credential key to retrieve
52-
* @returns {Promise<string>} The credential value
53-
* @throws {Error} If credential doesn't exist
54-
*/
55-
export async function getCredential(key) {
56-
try {
57-
const credentials = await getCredentialData();
58-
59-
if (!(key in credentials)) {
60-
throw new Error(ERRORS.NOT_FOUND);
37+
/**
38+
* @param {string} key
39+
* @returns {Promise<string | undefined>}
40+
*/
41+
async getPassword(key) {
42+
if (this.#accessedCredentials.has(key)) {
43+
return this.#accessedCredentials.get(key);
6144
}
62-
63-
return credentials[key];
64-
} catch (/** @type {any} */ error) {
65-
if (error.message === ERRORS.NOT_FOUND) {
66-
throw error;
45+
let credential = this.#credentials.get(key);
46+
if (!credential) {
47+
credential = new AsyncEntry(this.serviceName, key);
48+
this.#credentials.set(key, credential);
6749
}
68-
throw new Error(`Failed to get credential '${key}': ${error.message}`);
69-
}
70-
}
71-
72-
/**
73-
* Clears all stored credentials from keychain
74-
* @returns {Promise<void>}
75-
*/
76-
export async function clearCredentials() {
77-
try {
78-
const registryEntry = new AsyncEntry(KEYRING_SERVICE, REGISTRY_KEY);
79-
await registryEntry.deletePassword();
80-
} catch (error) {
81-
// Registry might not exist, ignore error
50+
const res = await credential.getPassword();
51+
if (res) {
52+
this.#accessedCredentials.set(key, res);
53+
}
54+
return res;
8255
}
83-
}
8456

85-
/**
86-
* Lists all stored credential keys (without showing the values)
87-
* @returns {Promise<string[]>} Array of credential keys
88-
*/
89-
export async function listCredentials() {
90-
try {
91-
const credentials = await getCredentialData();
92-
return Object.keys(credentials);
93-
} catch (/** @type {any} */ error) {
94-
throw new Error(`Failed to list credentials: ${error.message}`);
57+
/**
58+
* @param {string} key
59+
* @param {string} password
60+
* @returns {Promise<void>}
61+
*/
62+
async setPassword(key, password) {
63+
this.#accessedCredentials.set(key, password);
64+
let credential = this.#credentials.get(key);
65+
if (!credential) {
66+
credential = new AsyncEntry(this.serviceName, key);
67+
this.#credentials.set(key, credential);
68+
}
69+
await credential.setPassword(password);
9570
}
96-
}
97-
98-
/**
99-
* Removes a specific credential by key
100-
* @param {string[]} keys - The credential key to remove
101-
* @returns {Promise<void>} True if credential was found and removed, false if not found
102-
*/
103-
export async function removeCredential(...keys) {
104-
try {
105-
const credentials = await getCredentialData();
10671

107-
keys.forEach((key) => {
108-
if (key in credentials) {
109-
delete credentials[key];
110-
}
111-
});
112-
113-
// Update the stored data
114-
await updateCredentialData(credentials);
115-
} catch (/** @type {any} */ error) {
116-
throw new Error(`Failed to remove credential(s) '${keys.join(', ')}': ${error.message}`);
72+
/**
73+
* @param {string} key
74+
* @returns {Promise<void>}
75+
*/
76+
async deleteKey(key) {
77+
this.#accessedCredentials.delete(key);
78+
const credential = this.#credentials.get(key);
79+
if (credential) {
80+
await credential.deletePassword();
81+
this.#credentials.delete(key);
82+
}
11783
}
11884
}
11985

120-
/**
121-
* Stores a credential directly without prompting (for programmatic use)
122-
* @param {string} key - The credential key
123-
* @param {string} value - The credential value
124-
* @returns {Promise<void>}
125-
*/
126-
export async function setCredential(key, value) {
127-
try {
128-
const credentials = await getCredentialData();
129-
130-
// Update the credential in the object
131-
credentials[key] = value;
132-
133-
// Update the stored data
134-
await updateCredentialData(credentials);
135-
} catch (/** @type {any} */ error) {
136-
throw new Error(`Failed to set credential '${key}': ${error.message}`);
137-
}
138-
}
86+
export const credentialManager = new CredentialManager();

0 commit comments

Comments
 (0)