@huaweidevtools/aishell-dsh-credentials-store 0.0.7 → 0.0.8
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/LICENSE +1 -1
- package/README.md +49 -10
- package/lib/accounts.d.ts +187 -0
- package/lib/accounts.d.ts.map +1 -0
- package/lib/accounts.js +298 -0
- package/lib/accounts.js.map +1 -0
- package/lib/index-JlyJE1E8.d.ts +219 -0
- package/lib/index-JlyJE1E8.d.ts.map +1 -0
- package/lib/index.d.ts +3 -218
- package/lib/index.js +55 -21
- package/lib/index.js.map +1 -1
- package/lib/paths-DtFRzop6.js +46 -0
- package/lib/paths-DtFRzop6.js.map +1 -0
- package/lib/paths.d.ts +56 -0
- package/lib/paths.d.ts.map +1 -0
- package/lib/paths.js +3 -0
- package/lib/scope-CFetnbG3.d.ts +71 -0
- package/lib/scope-CFetnbG3.d.ts.map +1 -0
- package/lib/scope-QmrHZaZt.js +96 -0
- package/lib/scope-QmrHZaZt.js.map +1 -0
- package/lib/scope.d.ts +2 -0
- package/lib/scope.js +3 -0
- package/lib/signer-BPR0UAF0.js +66 -0
- package/lib/signer-BPR0UAF0.js.map +1 -0
- package/lib/signer-Et0DIH6o.d.ts +40 -0
- package/lib/signer-Et0DIH6o.d.ts.map +1 -0
- package/lib/signer.d.ts +2 -40
- package/lib/signer.js +2 -65
- package/lib/types/accounts.d.ts +231 -0
- package/lib/types/accounts.d.ts.map +1 -0
- package/lib/types/accounts.js +384 -0
- package/lib/types/accounts.js.map +1 -0
- package/lib/types/backends/fingerprint.d.ts +41 -0
- package/lib/types/backends/fingerprint.d.ts.map +1 -0
- package/lib/types/backends/fingerprint.js +135 -0
- package/lib/types/backends/fingerprint.js.map +1 -0
- package/lib/types/backends/keyring.d.ts +69 -0
- package/lib/types/backends/keyring.d.ts.map +1 -0
- package/lib/types/backends/keyring.js +107 -0
- package/lib/types/backends/keyring.js.map +1 -0
- package/lib/types/backends/memory.d.ts +45 -0
- package/lib/types/backends/memory.d.ts.map +1 -0
- package/lib/types/backends/memory.js +89 -0
- package/lib/types/backends/memory.js.map +1 -0
- package/lib/types/failover.d.ts +136 -0
- package/lib/types/failover.d.ts.map +1 -0
- package/lib/types/failover.js +242 -0
- package/lib/types/failover.js.map +1 -0
- package/lib/types/index.d.ts +70 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/index.js +78 -0
- package/lib/types/index.js.map +1 -0
- package/lib/types/paths.d.ts +50 -0
- package/lib/types/paths.d.ts.map +1 -0
- package/lib/types/paths.js +59 -0
- package/lib/types/paths.js.map +1 -0
- package/lib/types/scope.d.ts +65 -0
- package/lib/types/scope.d.ts.map +1 -0
- package/lib/types/scope.js +102 -0
- package/lib/types/scope.js.map +1 -0
- package/lib/types/signer.d.ts +36 -0
- package/lib/types/signer.d.ts.map +1 -0
- package/lib/types/signer.js +81 -0
- package/lib/types/signer.js.map +1 -0
- package/lib/types/verify.d.ts +114 -0
- package/lib/types/verify.d.ts.map +1 -0
- package/lib/types/verify.js +240 -0
- package/lib/types/verify.js.map +1 -0
- package/lib/verify.d.ts +72 -0
- package/lib/verify.d.ts.map +1 -0
- package/lib/verify.js +172 -0
- package/lib/verify.js.map +1 -0
- package/package.json +20 -7
- package/lib/index.d.ts.map +0 -1
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Huawei Cloud credential vocabulary every plugin in this set shares.
|
|
3
|
+
*
|
|
4
|
+
* ## Why this module exists
|
|
5
|
+
*
|
|
6
|
+
* This repo once read the same keyring entries from four places, each declaring
|
|
7
|
+
* the account names itself and re-implementing "read AK and SK as a pair". The
|
|
8
|
+
* account names are a *persistence* contract, so a divergence would not fail a
|
|
9
|
+
* build — it would surface as one plugin working and another reporting "not
|
|
10
|
+
* authenticated" on a machine where the person had stored exactly one pair, and
|
|
11
|
+
* the cause would be invisible in the diff that introduced it.
|
|
12
|
+
*
|
|
13
|
+
* Everything now goes through this file.
|
|
14
|
+
*
|
|
15
|
+
* ## One entry, not three
|
|
16
|
+
*
|
|
17
|
+
* The store holds **one** record, `aishell-credential`, whose value is a single
|
|
18
|
+
* JSON bundle encoded as base64:
|
|
19
|
+
*
|
|
20
|
+
* ```json
|
|
21
|
+
* {
|
|
22
|
+
* "huaweicloud": { "access_key": "…", "secret_key": "…", "security_token": "…" },
|
|
23
|
+
* "aishell_e2b": { "api_key": "…", "expired_at": "…", "ak_fingerprint": "…" }
|
|
24
|
+
* }
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* The earlier layout was three separate entries (`HW_ACCESS_KEY`,
|
|
28
|
+
* `HW_SECRET_KEY`, `HW_SECURITY_TOKEN`). That had two problems this fixes:
|
|
29
|
+
*
|
|
30
|
+
* - **A save was four operations.** Clear, then write AK, then SK, then
|
|
31
|
+
* token. Every backend is per-entry, so a failure between them left a
|
|
32
|
+
* half-set behind, and nothing rolled it back.
|
|
33
|
+
* - **A read was three reads.** A concurrent write landing between them could
|
|
34
|
+
* hand back a new AK with an old SK — a pair that passes "both present" and
|
|
35
|
+
* then fails at the gateway with 401, pointing nowhere near the cause.
|
|
36
|
+
*
|
|
37
|
+
* One entry makes a write one atomic operation and a read one consistent
|
|
38
|
+
* snapshot.
|
|
39
|
+
*
|
|
40
|
+
* ## base64 is an encoding, not encryption
|
|
41
|
+
*
|
|
42
|
+
* It keeps the secret from sitting in a plaintext field a person or a log
|
|
43
|
+
* prints while poking at the store. It is **not** a security boundary: anyone
|
|
44
|
+
* who can read the entry can decode it. The actual protection is the backend —
|
|
45
|
+
* the OS keyring, or AES-256-GCM in the file backend.
|
|
46
|
+
*
|
|
47
|
+
* ## Read-modify-write is not atomic across processes
|
|
48
|
+
*
|
|
49
|
+
* {@link updateCredentials} reads the bundle, applies a change and writes it
|
|
50
|
+
* back. Two processes doing that concurrently can lose one of the changes. That
|
|
51
|
+
* is accepted rather than solved: the only writer is a person via the
|
|
52
|
+
* credential form, and both the e2b key and the AK/SK are refreshed by that
|
|
53
|
+
* same person. A lost update would be corrected by the next save. Solving it
|
|
54
|
+
* properly needs compare-and-swap, which the keyring API does not offer.
|
|
55
|
+
*
|
|
56
|
+
* @module @huaweidevtools/aishell-dsh-credentials-store/accounts
|
|
57
|
+
*/
|
|
58
|
+
import { createHash } from 'node:crypto';
|
|
59
|
+
/**
|
|
60
|
+
* The one store entry holding every credential this repo manages.
|
|
61
|
+
*
|
|
62
|
+
* Part of the persistence contract: renaming it orphans every stored entry
|
|
63
|
+
* while looking like a harmless constant change.
|
|
64
|
+
*/
|
|
65
|
+
export const CREDENTIAL_ACCOUNT = 'aishell-credential';
|
|
66
|
+
/**
|
|
67
|
+
* Entries written by the earlier three-key layout, in the order they are read
|
|
68
|
+
* during migration.
|
|
69
|
+
*
|
|
70
|
+
* Kept only so an existing install keeps working: see {@link readCredentials}.
|
|
71
|
+
* New writes never touch these.
|
|
72
|
+
*/
|
|
73
|
+
export const LEGACY_ACCOUNTS = ['HW_ACCESS_KEY', 'HW_SECRET_KEY', 'HW_SECURITY_TOKEN'];
|
|
74
|
+
/**
|
|
75
|
+
* The legacy entry holding an e2b API key on its own.
|
|
76
|
+
*
|
|
77
|
+
* Folded into the bundle by {@link readCredentials}. Kept as a constant because
|
|
78
|
+
* the migration has to name it.
|
|
79
|
+
*/
|
|
80
|
+
export const LEGACY_E2B_ACCOUNT = 'AISHELL_E2B_API_KEY';
|
|
81
|
+
/**
|
|
82
|
+
* Encode a bundle for storage.
|
|
83
|
+
*
|
|
84
|
+
* @param bundle - the bundle to store.
|
|
85
|
+
* @returns base64 of its JSON form.
|
|
86
|
+
*/
|
|
87
|
+
export function encodeBundle(bundle) {
|
|
88
|
+
return Buffer.from(JSON.stringify(bundle), 'utf8').toString('base64');
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Decode a stored value.
|
|
92
|
+
*
|
|
93
|
+
* Anything unreadable reads as an empty bundle rather than throwing: a corrupt
|
|
94
|
+
* entry should behave like "nothing stored" so the person is asked to
|
|
95
|
+
* authenticate again, not so every tool call fails on a parse error.
|
|
96
|
+
*
|
|
97
|
+
* A value that is not base64 is tried as raw JSON first — that is what the
|
|
98
|
+
* legacy e2b entry held, and migration reads it through here.
|
|
99
|
+
*
|
|
100
|
+
* @param raw - the stored string, if any.
|
|
101
|
+
* @returns the decoded bundle; empty when nothing usable is there.
|
|
102
|
+
*/
|
|
103
|
+
export function decodeBundle(raw) {
|
|
104
|
+
if (raw === undefined || raw === '')
|
|
105
|
+
return {};
|
|
106
|
+
const text = decodeText(raw);
|
|
107
|
+
if (text === undefined)
|
|
108
|
+
return {};
|
|
109
|
+
let parsed;
|
|
110
|
+
try {
|
|
111
|
+
parsed = JSON.parse(text);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return {};
|
|
115
|
+
}
|
|
116
|
+
if (parsed === null || typeof parsed !== 'object')
|
|
117
|
+
return {};
|
|
118
|
+
const candidate = parsed;
|
|
119
|
+
return {
|
|
120
|
+
...(isHuaweiCloudSection(candidate.huaweicloud)
|
|
121
|
+
? { huaweicloud: candidate.huaweicloud }
|
|
122
|
+
: {}),
|
|
123
|
+
...(isE2BSection(candidate.aishell_e2b)
|
|
124
|
+
? { aishell_e2b: candidate.aishell_e2b }
|
|
125
|
+
: {}),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
/** Decode base64, falling back to the raw string when it is already text. */
|
|
129
|
+
function decodeText(raw) {
|
|
130
|
+
try {
|
|
131
|
+
const decoded = Buffer.from(raw, 'base64');
|
|
132
|
+
// `Buffer.from` never throws on bad base64 — it drops the unusable tail. So
|
|
133
|
+
// the round-trip check is what distinguishes real base64 from legacy JSON.
|
|
134
|
+
if (decoded.toString('base64') === raw.replace(/\s/g, '')) {
|
|
135
|
+
const text = decoded.toString('utf8');
|
|
136
|
+
if (text.trimStart().startsWith('{'))
|
|
137
|
+
return text;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
// Fall through to raw.
|
|
142
|
+
}
|
|
143
|
+
return raw.trimStart().startsWith('{') ? raw : undefined;
|
|
144
|
+
}
|
|
145
|
+
/** Whether a decoded field is a usable Huawei Cloud section. */
|
|
146
|
+
function isHuaweiCloudSection(value) {
|
|
147
|
+
if (value === null || typeof value !== 'object')
|
|
148
|
+
return false;
|
|
149
|
+
const section = value;
|
|
150
|
+
return typeof section.access_key === 'string' && section.access_key !== ''
|
|
151
|
+
&& typeof section.secret_key === 'string' && section.secret_key !== ''
|
|
152
|
+
&& (section.security_token === undefined || typeof section.security_token === 'string');
|
|
153
|
+
}
|
|
154
|
+
/** Whether a decoded field is a usable e2b section. */
|
|
155
|
+
function isE2BSection(value) {
|
|
156
|
+
if (value === null || typeof value !== 'object')
|
|
157
|
+
return false;
|
|
158
|
+
const section = value;
|
|
159
|
+
return typeof section.api_key === 'string' && section.api_key !== ''
|
|
160
|
+
&& typeof section.expired_at === 'string' && section.expired_at !== ''
|
|
161
|
+
&& (section.ak_fingerprint === undefined || typeof section.ak_fingerprint === 'string');
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Read the whole bundle, migrating a legacy layout on the way.
|
|
165
|
+
*
|
|
166
|
+
* Migration is lazy and idempotent: the first read that finds no bundle but
|
|
167
|
+
* finds legacy entries merges them into one record, writes it, and removes the
|
|
168
|
+
* old entries. A failed write leaves the legacy entries alone, so the next read
|
|
169
|
+
* tries again — the person is never left with neither layout.
|
|
170
|
+
*
|
|
171
|
+
* @param store - the caller's credential store.
|
|
172
|
+
* @returns the bundle; empty when nothing is stored.
|
|
173
|
+
*/
|
|
174
|
+
export function readCredentials(store) {
|
|
175
|
+
const stored = store.get(CREDENTIAL_ACCOUNT);
|
|
176
|
+
if (stored !== undefined)
|
|
177
|
+
return decodeBundle(stored);
|
|
178
|
+
const migrated = migrateLegacy(store);
|
|
179
|
+
if (migrated === undefined)
|
|
180
|
+
return {};
|
|
181
|
+
return migrated;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Merge the legacy entries into one record and remove the old ones.
|
|
185
|
+
*
|
|
186
|
+
* @param store - the caller's credential store.
|
|
187
|
+
* @returns the migrated bundle, or `undefined` when there was nothing to move.
|
|
188
|
+
*/
|
|
189
|
+
function migrateLegacy(store) {
|
|
190
|
+
const ak = store.get(LEGACY_ACCOUNTS[0]);
|
|
191
|
+
const sk = store.get(LEGACY_ACCOUNTS[1]);
|
|
192
|
+
const token = store.get(LEGACY_ACCOUNTS[2]);
|
|
193
|
+
const legacyKey = store.get(LEGACY_E2B_ACCOUNT);
|
|
194
|
+
const huaweicloud = ak !== undefined && ak !== '' && sk !== undefined && sk !== ''
|
|
195
|
+
? {
|
|
196
|
+
access_key: ak,
|
|
197
|
+
secret_key: sk,
|
|
198
|
+
...(token === undefined || token === '' ? {} : { security_token: token }),
|
|
199
|
+
}
|
|
200
|
+
: undefined;
|
|
201
|
+
// The legacy e2b entry held `{ key, expiresAt, akFingerprint }`. Re-keyed to
|
|
202
|
+
// the bundle's snake-case spelling; an older bare key still reads as one that
|
|
203
|
+
// never expires, which is how the reader treated it.
|
|
204
|
+
const aisleE2B = legacyE2BSection(legacyKey);
|
|
205
|
+
if (huaweicloud === undefined && aisleE2B === undefined)
|
|
206
|
+
return undefined;
|
|
207
|
+
const bundle = {
|
|
208
|
+
...(huaweicloud === undefined ? {} : { huaweicloud }),
|
|
209
|
+
...(aisleE2B === undefined ? {} : { aishell_e2b: aisleE2B }),
|
|
210
|
+
};
|
|
211
|
+
try {
|
|
212
|
+
store.set(CREDENTIAL_ACCOUNT, encodeBundle(bundle));
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
// Could not write the merged record — leave the legacy entries in place so
|
|
216
|
+
// the next read retries rather than losing the credentials.
|
|
217
|
+
return bundle;
|
|
218
|
+
}
|
|
219
|
+
for (const account of LEGACY_ACCOUNTS)
|
|
220
|
+
store.delete(account);
|
|
221
|
+
store.delete(LEGACY_E2B_ACCOUNT);
|
|
222
|
+
return bundle;
|
|
223
|
+
}
|
|
224
|
+
/** Read one legacy e2b entry into its bundle section. */
|
|
225
|
+
function legacyE2BSection(raw) {
|
|
226
|
+
if (raw === undefined || raw === '')
|
|
227
|
+
return undefined;
|
|
228
|
+
let parsed;
|
|
229
|
+
try {
|
|
230
|
+
parsed = JSON.parse(raw);
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
// Not JSON at all: a bare key. Its expiry is unknown, which reads the same
|
|
234
|
+
// as "does not expire"; the gateway's 401 path covers the difference.
|
|
235
|
+
return { api_key: raw, expired_at: FOREVER_EXPIRY };
|
|
236
|
+
}
|
|
237
|
+
const candidate = parsed;
|
|
238
|
+
if (candidate === null || typeof candidate !== 'object')
|
|
239
|
+
return undefined;
|
|
240
|
+
if (typeof candidate.key !== 'string' || candidate.key === '')
|
|
241
|
+
return undefined;
|
|
242
|
+
return {
|
|
243
|
+
api_key: candidate.key,
|
|
244
|
+
expired_at: typeof candidate.expiresAt === 'string' ? candidate.expiresAt : FOREVER_EXPIRY,
|
|
245
|
+
...(typeof candidate.akFingerprint === 'string' && candidate.akFingerprint !== ''
|
|
246
|
+
? { ak_fingerprint: candidate.akFingerprint }
|
|
247
|
+
: {}),
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
/** The expiry recorded for a key that does not expire. */
|
|
251
|
+
export const FOREVER_EXPIRY = '9999-12-31T23:59:59.000Z';
|
|
252
|
+
/**
|
|
253
|
+
* Read the Huawei Cloud pair.
|
|
254
|
+
*
|
|
255
|
+
* AK and SK are read together and treated as one unit: half a pair reads as no
|
|
256
|
+
* credentials rather than an error, so a partially-configured machine gets
|
|
257
|
+
* "authenticate first" instead of a signing failure with no remedy. The
|
|
258
|
+
* SecurityToken is optional and is omitted entirely when absent — an empty
|
|
259
|
+
* string would sign as a third header the gateway then rejects.
|
|
260
|
+
*
|
|
261
|
+
* @param store - the caller's credential store.
|
|
262
|
+
* @returns the pair, or `undefined` when the AK/SK pair is incomplete.
|
|
263
|
+
*/
|
|
264
|
+
export function readStoredPair(store) {
|
|
265
|
+
const section = readCredentials(store).huaweicloud;
|
|
266
|
+
if (section === undefined)
|
|
267
|
+
return undefined;
|
|
268
|
+
return {
|
|
269
|
+
ak: section.access_key,
|
|
270
|
+
sk: section.secret_key,
|
|
271
|
+
...(section.security_token === undefined ? {} : { securityToken: section.security_token }),
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Whether a complete Huawei Cloud pair is stored.
|
|
276
|
+
*
|
|
277
|
+
* @param store - the caller's credential store.
|
|
278
|
+
*/
|
|
279
|
+
export function hasStoredPair(store) {
|
|
280
|
+
return readStoredPair(store) !== undefined;
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Read the stored e2b key.
|
|
284
|
+
*
|
|
285
|
+
* @param store - the caller's credential store.
|
|
286
|
+
* @returns the section, or `undefined` when no key is stored.
|
|
287
|
+
*/
|
|
288
|
+
export function readE2BKey(store) {
|
|
289
|
+
return readCredentials(store).aishell_e2b;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Apply one change to the bundle and write it back.
|
|
293
|
+
*
|
|
294
|
+
* The updater receives the current bundle and returns the next one. Returning
|
|
295
|
+
* `undefined` aborts the write, which is how a caller says "nothing changed" —
|
|
296
|
+
* that matters because every write is a full re-encode, and needlessly
|
|
297
|
+
* rewriting a record while another process reads it is how a spurious
|
|
298
|
+
* "credentials changed" is produced.
|
|
299
|
+
*
|
|
300
|
+
* @param store - the caller's credential store.
|
|
301
|
+
* @param update - computes the next bundle.
|
|
302
|
+
* @returns the bundle now stored.
|
|
303
|
+
*/
|
|
304
|
+
export function updateCredentials(store, update) {
|
|
305
|
+
const current = readCredentials(store);
|
|
306
|
+
const next = update(current);
|
|
307
|
+
if (next === undefined)
|
|
308
|
+
return current;
|
|
309
|
+
store.set(CREDENTIAL_ACCOUNT, encodeBundle(next));
|
|
310
|
+
return next;
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Store the Huawei Cloud pair, replacing any previous one.
|
|
314
|
+
*
|
|
315
|
+
* One write, so the pair can never be left half-replaced. A save without a
|
|
316
|
+
* token removes a previously stored one: a token belongs to the credential set
|
|
317
|
+
* that carried it, and inheriting a stale one would sign the next request with
|
|
318
|
+
* someone else's session.
|
|
319
|
+
*
|
|
320
|
+
* @param store - the caller's credential store.
|
|
321
|
+
* @param credentials - the verified pair.
|
|
322
|
+
*/
|
|
323
|
+
export function writeHuaweiCloud(store, credentials) {
|
|
324
|
+
updateCredentials(store, current => ({
|
|
325
|
+
...current,
|
|
326
|
+
huaweicloud: {
|
|
327
|
+
access_key: credentials.ak,
|
|
328
|
+
secret_key: credentials.sk,
|
|
329
|
+
...(credentials.securityToken === undefined ? {} : { security_token: credentials.securityToken }),
|
|
330
|
+
},
|
|
331
|
+
}));
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Store an e2b key.
|
|
335
|
+
*
|
|
336
|
+
* @param store - the caller's credential store.
|
|
337
|
+
* @param key - the section to store.
|
|
338
|
+
*/
|
|
339
|
+
export function writeE2BKey(store, key) {
|
|
340
|
+
updateCredentials(store, current => ({ ...current, aishell_e2b: key }));
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Remove the stored e2b key, leaving the Huawei Cloud pair alone.
|
|
344
|
+
*
|
|
345
|
+
* @param store - the caller's credential store.
|
|
346
|
+
*/
|
|
347
|
+
export function clearE2BKey(store) {
|
|
348
|
+
updateCredentials(store, current => {
|
|
349
|
+
if (current.aishell_e2b === undefined)
|
|
350
|
+
return undefined;
|
|
351
|
+
const { aishell_e2b: _removed, ...rest } = current;
|
|
352
|
+
return rest;
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Remove every credential this repo writes.
|
|
357
|
+
*
|
|
358
|
+
* The whole record goes, so a clear cannot leave a token or a key behind for
|
|
359
|
+
* the next pair to inherit. Deleting an absent entry is a no-op in every
|
|
360
|
+
* backend, and the legacy entries are removed too so a half-migrated store does
|
|
361
|
+
* not come back.
|
|
362
|
+
*
|
|
363
|
+
* @param store - the caller's credential store.
|
|
364
|
+
*/
|
|
365
|
+
export function clearCredentials(store) {
|
|
366
|
+
store.delete(CREDENTIAL_ACCOUNT);
|
|
367
|
+
for (const account of LEGACY_ACCOUNTS)
|
|
368
|
+
store.delete(account);
|
|
369
|
+
store.delete(LEGACY_E2B_ACCOUNT);
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* A short, stable digest of an AK, for recording which identity minted a key.
|
|
373
|
+
*
|
|
374
|
+
* The AK rather than the pair: the SK can be rotated independently while the
|
|
375
|
+
* account stays the same, and the tenant the gateway derives is the account. A
|
|
376
|
+
* digest rather than the AK itself keeps the bundle from carrying a second
|
|
377
|
+
* readable copy of a credential identifier.
|
|
378
|
+
*
|
|
379
|
+
* @param ak - the access key.
|
|
380
|
+
*/
|
|
381
|
+
export function fingerprint(ak) {
|
|
382
|
+
return createHash('sha256').update(ak, 'utf8').digest('hex');
|
|
383
|
+
}
|
|
384
|
+
//# sourceMappingURL=accounts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"accounts.js","sourceRoot":"","sources":["../../src/accounts.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Machine-fingerprint-bound encrypted file backend.
|
|
3
|
+
*
|
|
4
|
+
* When the OS keyring is unavailable (headless Linux, Docker, no keyring
|
|
5
|
+
* daemon), credentials are encrypted with a key derived from the machine UUID
|
|
6
|
+
* (node-machine-id) via scrypt, then AES-256-GCM. File format:
|
|
7
|
+
* [12B nonce][16B tag][ciphertext...].
|
|
8
|
+
*
|
|
9
|
+
* The key never touches disk; the ciphertext is useless on a different machine.
|
|
10
|
+
* Same-user-same-machine is trusted. VM cloning copies the machine UUID — a
|
|
11
|
+
* known limitation, not a security hole.
|
|
12
|
+
*/
|
|
13
|
+
import type { CredentialsStore, CredentialsStoreConfig } from '../index.ts';
|
|
14
|
+
/**
|
|
15
|
+
* Encrypted-file {@link CredentialsStore}. Each account is one entry in a
|
|
16
|
+
* single JSON map, encrypted as one file under the resolved credentials
|
|
17
|
+
* directory, named by the build's credential scope (see `../scope.ts`).
|
|
18
|
+
*
|
|
19
|
+
* Construction probes the machine ID + crypto — if `node-machine-id` cannot
|
|
20
|
+
* resolve a UUID, construction throws and the factory falls back to
|
|
21
|
+
* {@link MemoryBackend}.
|
|
22
|
+
*/
|
|
23
|
+
export declare class FingerprintBackend implements CredentialsStore {
|
|
24
|
+
private readonly filePath;
|
|
25
|
+
private readonly fileDir;
|
|
26
|
+
constructor(config?: CredentialsStoreConfig);
|
|
27
|
+
/**
|
|
28
|
+
* Load + decrypt the file into a fresh map. Not cached — credentials are
|
|
29
|
+
* read, used, and dropped so plaintext does not linger in memory between
|
|
30
|
+
* operations. The file I/O + scrypt decrypt per call is acceptable: this is
|
|
31
|
+
* not a hot path (once per chat request, not per token).
|
|
32
|
+
*/
|
|
33
|
+
private load;
|
|
34
|
+
/** Encrypt + write the map to disk with owner-only permissions. */
|
|
35
|
+
private persist;
|
|
36
|
+
get(account: string): string | undefined;
|
|
37
|
+
set(account: string, value: string): void;
|
|
38
|
+
delete(account: string): void;
|
|
39
|
+
has(account: string): boolean;
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=fingerprint.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fingerprint.d.ts","sourceRoot":"","sources":["../../../src/backends/fingerprint.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAQH,OAAO,KAAK,EAAE,gBAAgB,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAA;AA0D3E;;;;;;;;GAQG;AACH,qBAAa,kBAAmB,YAAW,gBAAgB;IACzD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAQ;IACjC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAQ;gBAEpB,MAAM,GAAE,sBAA2B;IAQ/C;;;;;OAKG;IACH,OAAO,CAAC,IAAI;IAYZ,mEAAmE;IACnE,OAAO,CAAC,OAAO;IAQf,GAAG,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAIxC,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IAOzC,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAO7B,GAAG,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO;CAG9B"}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Machine-fingerprint-bound encrypted file backend.
|
|
3
|
+
*
|
|
4
|
+
* When the OS keyring is unavailable (headless Linux, Docker, no keyring
|
|
5
|
+
* daemon), credentials are encrypted with a key derived from the machine UUID
|
|
6
|
+
* (node-machine-id) via scrypt, then AES-256-GCM. File format:
|
|
7
|
+
* [12B nonce][16B tag][ciphertext...].
|
|
8
|
+
*
|
|
9
|
+
* The key never touches disk; the ciphertext is useless on a different machine.
|
|
10
|
+
* Same-user-same-machine is trusted. VM cloning copies the machine UUID — a
|
|
11
|
+
* known limitation, not a security hole.
|
|
12
|
+
*/
|
|
13
|
+
import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from 'node:crypto';
|
|
14
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
import machineIdModule from 'node-machine-id';
|
|
17
|
+
import { CREDENTIALS_FILE_NAME } from "../scope.js";
|
|
18
|
+
import { credentialsDir } from "../paths.js";
|
|
19
|
+
const machineIdSync = machineIdModule.machineIdSync;
|
|
20
|
+
// Deliberately NOT the keyring service name: this is the scrypt salt for
|
|
21
|
+
// the encrypted file, and changing it would make every existing
|
|
22
|
+
// `credentials.enc` undecryptable — the file would read as empty and the
|
|
23
|
+
// credentials would look cleared. It is frozen.
|
|
24
|
+
const SALT = 'aishell-dsh/credentials/v1';
|
|
25
|
+
const KEY_LEN = 32; // AES-256
|
|
26
|
+
const NONCE_LEN = 12; // GCM standard (96-bit nonce)
|
|
27
|
+
const TAG_LEN = 16; // 128-bit auth tag
|
|
28
|
+
/** Derive a 32-byte key from the machine UUID via scrypt. */
|
|
29
|
+
function deriveKey() {
|
|
30
|
+
const machineId = machineIdSync();
|
|
31
|
+
return scryptSync(machineId, SALT, KEY_LEN);
|
|
32
|
+
}
|
|
33
|
+
/** Encrypt plaintext → [nonce][tag][ciphertext] as a Buffer. */
|
|
34
|
+
function fingerprintEncrypt(plaintext) {
|
|
35
|
+
const key = deriveKey();
|
|
36
|
+
const nonce = randomBytes(NONCE_LEN);
|
|
37
|
+
const cipher = createCipheriv('aes-256-gcm', key, nonce);
|
|
38
|
+
const ciphertext = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
|
|
39
|
+
const tag = cipher.getAuthTag();
|
|
40
|
+
return Buffer.concat([nonce, tag, ciphertext]);
|
|
41
|
+
}
|
|
42
|
+
/** Decrypt [nonce][tag][ciphertext] → plaintext. Throws on tamper/wrong machine. */
|
|
43
|
+
function fingerprintDecrypt(data) {
|
|
44
|
+
if (data.length < NONCE_LEN + TAG_LEN) {
|
|
45
|
+
throw new Error('credentials file corrupted (too short)');
|
|
46
|
+
}
|
|
47
|
+
const key = deriveKey();
|
|
48
|
+
const nonce = data.subarray(0, NONCE_LEN);
|
|
49
|
+
const tag = data.subarray(NONCE_LEN, NONCE_LEN + TAG_LEN);
|
|
50
|
+
const ciphertext = data.subarray(NONCE_LEN + TAG_LEN);
|
|
51
|
+
const decipher = createDecipheriv('aes-256-gcm', key, nonce);
|
|
52
|
+
decipher.setAuthTag(tag);
|
|
53
|
+
const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
54
|
+
return plaintext.toString('utf8');
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Resolve the directory holding the encrypted credentials file.
|
|
58
|
+
*
|
|
59
|
+
* An explicit `config.credentialsDir` wins; otherwise `<DSH_HOME>/credentials`,
|
|
60
|
+
* resolved by the shared `paths` module so the terraform cache in
|
|
61
|
+
* `aishell-deploy` cannot drift to a different home.
|
|
62
|
+
*
|
|
63
|
+
* Builds are separated by the *file name* — see `../scope.ts` — not by the
|
|
64
|
+
* directory.
|
|
65
|
+
*/
|
|
66
|
+
function resolveCredentialsDir(config) {
|
|
67
|
+
return config.credentialsDir ?? credentialsDir();
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Encrypted-file {@link CredentialsStore}. Each account is one entry in a
|
|
71
|
+
* single JSON map, encrypted as one file under the resolved credentials
|
|
72
|
+
* directory, named by the build's credential scope (see `../scope.ts`).
|
|
73
|
+
*
|
|
74
|
+
* Construction probes the machine ID + crypto — if `node-machine-id` cannot
|
|
75
|
+
* resolve a UUID, construction throws and the factory falls back to
|
|
76
|
+
* {@link MemoryBackend}.
|
|
77
|
+
*/
|
|
78
|
+
export class FingerprintBackend {
|
|
79
|
+
filePath;
|
|
80
|
+
fileDir;
|
|
81
|
+
constructor(config = {}) {
|
|
82
|
+
this.fileDir = resolveCredentialsDir(config);
|
|
83
|
+
this.filePath = join(this.fileDir, CREDENTIALS_FILE_NAME);
|
|
84
|
+
// Probe: deriveKey exercises node-machine-id. If it throws, the factory
|
|
85
|
+
// falls back to MemoryBackend.
|
|
86
|
+
deriveKey();
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Load + decrypt the file into a fresh map. Not cached — credentials are
|
|
90
|
+
* read, used, and dropped so plaintext does not linger in memory between
|
|
91
|
+
* operations. The file I/O + scrypt decrypt per call is acceptable: this is
|
|
92
|
+
* not a hot path (once per chat request, not per token).
|
|
93
|
+
*/
|
|
94
|
+
load() {
|
|
95
|
+
if (!existsSync(this.filePath))
|
|
96
|
+
return new Map();
|
|
97
|
+
try {
|
|
98
|
+
const encrypted = readFileSync(this.filePath);
|
|
99
|
+
const plaintext = fingerprintDecrypt(encrypted);
|
|
100
|
+
return new Map(Object.entries(JSON.parse(plaintext)));
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
// Corrupted or wrong machine — treat as empty.
|
|
104
|
+
return new Map();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/** Encrypt + write the map to disk with owner-only permissions. */
|
|
108
|
+
persist(entries) {
|
|
109
|
+
const plaintext = JSON.stringify(Object.fromEntries(entries));
|
|
110
|
+
mkdirSync(this.fileDir, { recursive: true, mode: 0o700 });
|
|
111
|
+
// Write owner-only (0600); on platforms that ignore the mode (Windows)
|
|
112
|
+
// the file is still encrypted, and ACLs inherit from the profile dir.
|
|
113
|
+
writeFileSync(this.filePath, fingerprintEncrypt(plaintext), { mode: 0o600 });
|
|
114
|
+
}
|
|
115
|
+
get(account) {
|
|
116
|
+
return this.load().get(account);
|
|
117
|
+
}
|
|
118
|
+
set(account, value) {
|
|
119
|
+
if (value.length === 0)
|
|
120
|
+
throw new Error('credential value must be non-empty');
|
|
121
|
+
const entries = this.load();
|
|
122
|
+
entries.set(account, value);
|
|
123
|
+
this.persist(entries);
|
|
124
|
+
}
|
|
125
|
+
delete(account) {
|
|
126
|
+
const entries = this.load();
|
|
127
|
+
if (entries.delete(account)) {
|
|
128
|
+
this.persist(entries);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
has(account) {
|
|
132
|
+
return this.load().has(account);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
//# sourceMappingURL=fingerprint.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fingerprint.js","sourceRoot":"","sources":["../../../src/backends/fingerprint.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OS-keyring backend — the durable, cross-process credential store.
|
|
3
|
+
*
|
|
4
|
+
* Wraps `@napi-rs/keyring` (macOS Keychain / Linux Secret Service / Windows
|
|
5
|
+
* Credential Manager). Values persist across process restarts and reboots
|
|
6
|
+
* because the OS owns the store.
|
|
7
|
+
*
|
|
8
|
+
* ## Why Linux pins the Secret Service store
|
|
9
|
+
*
|
|
10
|
+
* On Linux the binding picks a store itself: Secret Service (gnome-keyring,
|
|
11
|
+
* KWallet, KeePassXC) first, then — silently — the kernel keyutils keyring.
|
|
12
|
+
* That fallback is wrong for credentials, and the binding's README says so:
|
|
13
|
+
* the kernel keyring is "completely in-memory and will not persist across
|
|
14
|
+
* reboots". A machine with no Secret Service (a container, CI, a systemd unit
|
|
15
|
+
* with no session bus) would therefore accept credentials, report them stored,
|
|
16
|
+
* and lose them at the next reboot — with `credentialsStored: true` left in
|
|
17
|
+
* the profile pointing at nothing.
|
|
18
|
+
*
|
|
19
|
+
* Pinning `{ linux: { store: 'secret-service' } }` makes an unavailable
|
|
20
|
+
* service throw from the constructor probe instead, so
|
|
21
|
+
* {@link createCredentialsStore} falls through to the machine-fingerprint
|
|
22
|
+
* encrypted file: durable, and honest about where the value lives. Requiring
|
|
23
|
+
* the store is the only way to get that throw — the default selection reports
|
|
24
|
+
* success either way.
|
|
25
|
+
*
|
|
26
|
+
* Native keyring errors are caught: `get`/`has` return `undefined`/`false` so
|
|
27
|
+
* callers degrade gracefully; `set`/`delete` throw a descriptive error so the
|
|
28
|
+
* caller knows the store is unavailable.
|
|
29
|
+
*/
|
|
30
|
+
import type { CredentialsStore, CredentialsStoreConfig } from '../index.ts';
|
|
31
|
+
type KeyringEntry = {
|
|
32
|
+
getPassword: () => string | null;
|
|
33
|
+
setPassword: (value: string) => void;
|
|
34
|
+
deletePassword: () => void;
|
|
35
|
+
};
|
|
36
|
+
/** Entry options bag; only the Linux store pin is used (see the module note). */
|
|
37
|
+
type KeyringEntryOptions = {
|
|
38
|
+
linux?: {
|
|
39
|
+
store?: 'secret-service' | 'keyutils';
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
type KeyringModule = {
|
|
43
|
+
Entry: new (service: string, account: string, options?: KeyringEntryOptions) => KeyringEntry;
|
|
44
|
+
};
|
|
45
|
+
/** @internal Test hook: inject a mock keyring module (vi.mock cannot intercept createRequire). */
|
|
46
|
+
export declare function __setKeyringModule(mock: KeyringModule | undefined): void;
|
|
47
|
+
/**
|
|
48
|
+
* OS-keyring {@link CredentialsStore}. Constructed with a shared service name;
|
|
49
|
+
* every entry is stored under `new Entry(service, account, { linux: { store:
|
|
50
|
+
* 'secret-service' } })`.
|
|
51
|
+
*
|
|
52
|
+
* Construction probes the keyring once — if the native module cannot be loaded
|
|
53
|
+
* (no prebuilt binding for this platform), or the Linux Secret Service is
|
|
54
|
+
* unavailable, it throws and {@link createCredentialsStore} falls back to the
|
|
55
|
+
* next backend. Without the pin the absent-service case does not throw; see
|
|
56
|
+
* the module note.
|
|
57
|
+
*/
|
|
58
|
+
export declare class KeyringBackend implements CredentialsStore {
|
|
59
|
+
private readonly serviceName;
|
|
60
|
+
private readonly options;
|
|
61
|
+
private readonly Entry;
|
|
62
|
+
constructor(_config?: CredentialsStoreConfig);
|
|
63
|
+
get(account: string): string | undefined;
|
|
64
|
+
set(account: string, value: string): void;
|
|
65
|
+
delete(account: string): void;
|
|
66
|
+
has(account: string): boolean;
|
|
67
|
+
}
|
|
68
|
+
export {};
|
|
69
|
+
//# sourceMappingURL=keyring.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keyring.d.ts","sourceRoot":"","sources":["../../../src/backends/keyring.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAIH,OAAO,KAAK,EAAE,gBAAgB,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAA;AAO3E,KAAK,YAAY,GAAG;IAClB,WAAW,EAAE,MAAM,MAAM,GAAG,IAAI,CAAA;IAChC,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACpC,cAAc,EAAE,MAAM,IAAI,CAAA;CAC3B,CAAA;AACD,iFAAiF;AACjF,KAAK,mBAAmB,GAAG;IAAE,KAAK,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,gBAAgB,GAAG,UAAU,CAAA;KAAE,CAAA;CAAE,CAAA;AAChF,KAAK,aAAa,GAAG;IACnB,KAAK,EAAE,KAAK,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,mBAAmB,KAAK,YAAY,CAAA;CAC7F,CAAA;AAGD,kGAAkG;AAClG,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,aAAa,GAAG,SAAS,GAAG,IAAI,CAExE;AAOD;;;;;;;;;;GAUG;AACH,qBAAa,cAAe,YAAW,gBAAgB;IACrD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAQ;IACpC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqB;IAC7C,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAwB;gBAElC,OAAO,GAAE,sBAA2B;IAWhD,GAAG,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IASxC,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IAKzC,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAY7B,GAAG,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO;CAO9B"}
|