@huaweidevtools/aishell-dsh-credentials-store 0.0.15 → 0.0.17
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/lib/accounts.d.ts +31 -20
- package/lib/accounts.d.ts.map +1 -1
- package/lib/accounts.js +57 -24
- package/lib/accounts.js.map +1 -1
- package/lib/{index-JlyJE1E8.d.ts → index-Cdsu1kDC.d.ts} +87 -12
- package/lib/index-Cdsu1kDC.d.ts.map +1 -0
- package/lib/index.d.ts +3 -3
- package/lib/index.js +165 -29
- package/lib/index.js.map +1 -1
- package/lib/{paths-DtFRzop6.js → paths-C0wxA5LO.js} +3 -3
- package/lib/paths-C0wxA5LO.js.map +1 -0
- package/lib/paths.d.ts +10 -9
- package/lib/paths.d.ts.map +1 -1
- package/lib/paths.js +1 -1
- package/lib/{scope-CFetnbG3.d.ts → scope-C7w2eTvE.d.ts} +12 -12
- package/lib/{scope-CFetnbG3.d.ts.map → scope-C7w2eTvE.d.ts.map} +1 -1
- package/lib/{scope-QmrHZaZt.js → scope-DJcK27Uc.js} +12 -12
- package/lib/scope-DJcK27Uc.js.map +1 -0
- package/lib/scope.d.ts +1 -1
- package/lib/scope.js +1 -1
- package/lib/signer-Bx88x7dI.d.ts +72 -0
- package/lib/signer-Bx88x7dI.d.ts.map +1 -0
- package/lib/{signer-BPR0UAF0.js → signer-CVOjpi6y.js} +39 -3
- package/lib/signer-CVOjpi6y.js.map +1 -0
- package/lib/signer.d.ts +2 -2
- package/lib/signer.js +2 -2
- package/lib/types/accounts.d.ts +61 -39
- package/lib/types/accounts.d.ts.map +1 -1
- package/lib/types/accounts.js +108 -52
- package/lib/types/accounts.js.map +1 -1
- package/lib/types/backends/fingerprint.d.ts +78 -0
- package/lib/types/backends/fingerprint.d.ts.map +1 -1
- package/lib/types/backends/fingerprint.js +155 -8
- package/lib/types/backends/fingerprint.js.map +1 -1
- package/lib/types/backends/keyring.d.ts +7 -7
- package/lib/types/backends/keyring.d.ts.map +1 -1
- package/lib/types/backends/keyring.js +15 -9
- package/lib/types/backends/keyring.js.map +1 -1
- package/lib/types/backends/memory.d.ts +4 -17
- package/lib/types/backends/memory.d.ts.map +1 -1
- package/lib/types/backends/memory.js +21 -32
- package/lib/types/backends/memory.js.map +1 -1
- package/lib/types/failover.d.ts +16 -18
- package/lib/types/failover.d.ts.map +1 -1
- package/lib/types/failover.js +19 -21
- package/lib/types/failover.js.map +1 -1
- package/lib/types/index.d.ts +4 -4
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +4 -4
- package/lib/types/index.js.map +1 -1
- package/lib/types/paths.d.ts +10 -9
- package/lib/types/paths.d.ts.map +1 -1
- package/lib/types/paths.js +10 -9
- package/lib/types/paths.js.map +1 -1
- package/lib/types/scope.d.ts +11 -11
- package/lib/types/scope.js +11 -11
- package/lib/types/signer.d.ts +32 -0
- package/lib/types/signer.d.ts.map +1 -1
- package/lib/types/signer.js +40 -5
- package/lib/types/signer.js.map +1 -1
- package/lib/types/verify.d.ts +7 -11
- package/lib/types/verify.d.ts.map +1 -1
- package/lib/types/verify.js +27 -11
- package/lib/types/verify.js.map +1 -1
- package/lib/verify.d.ts +1 -1
- package/lib/verify.d.ts.map +1 -1
- package/lib/verify.js +11 -1
- package/lib/verify.js.map +1 -1
- package/package.json +1 -1
- package/lib/index-JlyJE1E8.d.ts.map +0 -1
- package/lib/paths-DtFRzop6.js.map +0 -1
- package/lib/scope-QmrHZaZt.js.map +0 -1
- package/lib/signer-BPR0UAF0.js.map +0 -1
- package/lib/signer-Et0DIH6o.d.ts +0 -40
- package/lib/signer-Et0DIH6o.d.ts.map +0 -1
package/lib/types/accounts.js
CHANGED
|
@@ -1,18 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The Huawei Cloud credential vocabulary every plugin in this set shares.
|
|
3
3
|
*
|
|
4
|
-
* ## Why
|
|
4
|
+
* ## Why one module owns the account names
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* the cause would be invisible in the diff that introduced it.
|
|
6
|
+
* The account names and the "AK and SK are one unit" rule are a *persistence*
|
|
7
|
+
* contract. A second declaration of either does not fail a build: it surfaces
|
|
8
|
+
* as one plugin working and another reporting "not authenticated" on a machine
|
|
9
|
+
* where exactly one pair is stored, and the diff that introduced it looks
|
|
10
|
+
* harmless. Keeping them in one file makes the contract have one owner.
|
|
12
11
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* ## One entry, not three
|
|
12
|
+
* ## One record, holding the whole bundle
|
|
16
13
|
*
|
|
17
14
|
* The store holds **one** record, `aishell-credential`, whose value is a single
|
|
18
15
|
* JSON bundle encoded as base64:
|
|
@@ -24,18 +21,16 @@
|
|
|
24
21
|
* }
|
|
25
22
|
* ```
|
|
26
23
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
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.
|
|
24
|
+
* One record is what makes a save one operation and a read one consistent
|
|
25
|
+
* snapshot:
|
|
36
26
|
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
27
|
+
* - **A save is one write.** Splitting the pair across entries makes every
|
|
28
|
+
* save a sequence, and every backend is per-entry, so a failure between two
|
|
29
|
+
* of them leaves a half-set behind that nothing rolls back.
|
|
30
|
+
* - **A read is one read.** Reading the AK and the SK separately lets a
|
|
31
|
+
* concurrent write land between them: the caller gets a new AK with an old
|
|
32
|
+
* SK, which passes "both present" and then fails at the gateway with 401,
|
|
33
|
+
* pointing nowhere near the cause.
|
|
39
34
|
*
|
|
40
35
|
* ## base64 is an encoding, not encryption
|
|
41
36
|
*
|
|
@@ -50,9 +45,25 @@
|
|
|
50
45
|
* back. Two processes doing that concurrently can lose one of the changes. That
|
|
51
46
|
* is accepted rather than solved: the only writer is a person via the
|
|
52
47
|
* credential form, and both the e2b key and the AK/SK are refreshed by that
|
|
53
|
-
* same person
|
|
48
|
+
* same person, so a lost update is corrected by the next save. Solving it
|
|
54
49
|
* properly needs compare-and-swap, which the keyring API does not offer.
|
|
55
50
|
*
|
|
51
|
+
* ## One accepted consequence: a partial bundle can overwrite an unreadable one
|
|
52
|
+
*
|
|
53
|
+
* A read that cannot decrypt the file reports "nothing stored", so an
|
|
54
|
+
* {@link updateCredentials} built on it computes its next bundle from an empty
|
|
55
|
+
* one and writes that. For `clearE2BKey` that means the written record carries
|
|
56
|
+
* only whatever the updater added, and the section it could not read is no
|
|
57
|
+
* longer in the live file.
|
|
58
|
+
*
|
|
59
|
+
* That is not repaired here, because there is nothing to repair it *from* — the
|
|
60
|
+
* bytes are unreadable. What limits the damage is the file backend's write path:
|
|
61
|
+
* an unreadable file is moved to `<name>.unreadable-<timestamp>` rather than
|
|
62
|
+
* overwritten, so the old record survives on disk and the next successful save
|
|
63
|
+
* starts from an empty map. A reader who cannot decrypt the file has already
|
|
64
|
+
* lost those credentials; the guarantee is only that the file they were in is
|
|
65
|
+
* not destroyed.
|
|
66
|
+
*
|
|
56
67
|
* @module @huaweidevtools/aishell-dsh-credentials-store/accounts
|
|
57
68
|
*/
|
|
58
69
|
import { createHash } from 'node:crypto';
|
|
@@ -64,18 +75,22 @@ import { createHash } from 'node:crypto';
|
|
|
64
75
|
*/
|
|
65
76
|
export const CREDENTIAL_ACCOUNT = 'aishell-credential';
|
|
66
77
|
/**
|
|
67
|
-
*
|
|
68
|
-
*
|
|
78
|
+
* The per-field account names a store may still hold beside the bundle.
|
|
79
|
+
*
|
|
80
|
+
* These are the read-side names of a store written field-by-field instead of
|
|
81
|
+
* as one record. {@link readCredentials} folds them into
|
|
82
|
+
* {@link CREDENTIAL_ACCOUNT} on first read, so a machine that holds them keeps
|
|
83
|
+
* working; nothing writes them.
|
|
69
84
|
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
85
|
+
* The order is the order the migration reads them: access key, secret key,
|
|
86
|
+
* token.
|
|
72
87
|
*/
|
|
73
88
|
export const LEGACY_ACCOUNTS = ['HW_ACCESS_KEY', 'HW_SECRET_KEY', 'HW_SECURITY_TOKEN'];
|
|
74
89
|
/**
|
|
75
|
-
* The
|
|
90
|
+
* The standalone e2b key entry, beside the per-field names above.
|
|
76
91
|
*
|
|
77
|
-
* Folded into the bundle by {@link readCredentials}
|
|
78
|
-
* the
|
|
92
|
+
* Folded into the bundle by {@link readCredentials}, for the same reason: a
|
|
93
|
+
* store written before the bundle existed keeps working. Nothing writes it.
|
|
79
94
|
*/
|
|
80
95
|
export const LEGACY_E2B_ACCOUNT = 'AISHELL_E2B_API_KEY';
|
|
81
96
|
/**
|
|
@@ -161,12 +176,12 @@ function isE2BSection(value) {
|
|
|
161
176
|
&& (section.ak_fingerprint === undefined || typeof section.ak_fingerprint === 'string');
|
|
162
177
|
}
|
|
163
178
|
/**
|
|
164
|
-
* Read the whole bundle,
|
|
179
|
+
* Read the whole bundle, folding a per-field store into it on the way.
|
|
165
180
|
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
181
|
+
* The fold is lazy and idempotent: a read that finds no bundle but finds the
|
|
182
|
+
* per-field names merges them into one record, writes it, and removes the old
|
|
183
|
+
* entries. A failed write leaves them in place so the next read tries again —
|
|
184
|
+
* a store is never left with neither layout.
|
|
170
185
|
*
|
|
171
186
|
* @param store - the caller's credential store.
|
|
172
187
|
* @returns the bundle; empty when nothing is stored.
|
|
@@ -181,10 +196,10 @@ export function readCredentials(store) {
|
|
|
181
196
|
return migrated;
|
|
182
197
|
}
|
|
183
198
|
/**
|
|
184
|
-
* Merge the
|
|
199
|
+
* Merge the per-field entries into one record and remove the old ones.
|
|
185
200
|
*
|
|
186
201
|
* @param store - the caller's credential store.
|
|
187
|
-
* @returns the
|
|
202
|
+
* @returns the merged bundle, or `undefined` when there was nothing to move.
|
|
188
203
|
*/
|
|
189
204
|
function migrateLegacy(store) {
|
|
190
205
|
const ak = store.get(LEGACY_ACCOUNTS[0]);
|
|
@@ -198,9 +213,10 @@ function migrateLegacy(store) {
|
|
|
198
213
|
...(token === undefined || token === '' ? {} : { security_token: token }),
|
|
199
214
|
}
|
|
200
215
|
: undefined;
|
|
201
|
-
// The
|
|
202
|
-
// the bundle's snake-case spelling
|
|
203
|
-
// never expires, which is
|
|
216
|
+
// The standalone entry carries `{ key, expiresAt, akFingerprint }`; it is
|
|
217
|
+
// re-keyed to the bundle's snake-case spelling here. A bare key with no JSON
|
|
218
|
+
// around it reads as one that never expires, which is the reading the rest of
|
|
219
|
+
// the store gives an unknown expiry.
|
|
204
220
|
const aisleE2B = legacyE2BSection(legacyKey);
|
|
205
221
|
if (huaweicloud === undefined && aisleE2B === undefined)
|
|
206
222
|
return undefined;
|
|
@@ -212,16 +228,24 @@ function migrateLegacy(store) {
|
|
|
212
228
|
store.set(CREDENTIAL_ACCOUNT, encodeBundle(bundle));
|
|
213
229
|
}
|
|
214
230
|
catch {
|
|
215
|
-
// Could not write the merged record — leave the
|
|
216
|
-
// the next read retries rather than losing the credentials.
|
|
231
|
+
// Could not write the merged record — leave the per-field entries in place
|
|
232
|
+
// so the next read retries rather than losing the credentials.
|
|
217
233
|
return bundle;
|
|
218
234
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
235
|
+
// The merged record is durable, so the old entries are redundant now. Each
|
|
236
|
+
// delete is best-effort: a locked keyring rejects deletes as well as writes,
|
|
237
|
+
// and letting that propagate would make a **successful** migration look like
|
|
238
|
+
// a failed read to the caller. A leftover entry costs nothing — a read
|
|
239
|
+
// returns the bundle as soon as it exists, and `clearCredentials` removes
|
|
240
|
+
// these names too — whereas an exception here surfaces as "could not read
|
|
241
|
+
// credentials" on a store that is fine.
|
|
242
|
+
for (const account of LEGACY_ACCOUNTS) {
|
|
243
|
+
tryDelete(store, account);
|
|
244
|
+
}
|
|
245
|
+
tryDelete(store, LEGACY_E2B_ACCOUNT);
|
|
222
246
|
return bundle;
|
|
223
247
|
}
|
|
224
|
-
/** Read one
|
|
248
|
+
/** Read one standalone e2b entry into its bundle section. */
|
|
225
249
|
function legacyE2BSection(raw) {
|
|
226
250
|
if (raw === undefined || raw === '')
|
|
227
251
|
return undefined;
|
|
@@ -249,6 +273,31 @@ function legacyE2BSection(raw) {
|
|
|
249
273
|
}
|
|
250
274
|
/** The expiry recorded for a key that does not expire. */
|
|
251
275
|
export const FOREVER_EXPIRY = '9999-12-31T23:59:59.000Z';
|
|
276
|
+
/**
|
|
277
|
+
* Delete an entry, swallowing a backend refusal.
|
|
278
|
+
*
|
|
279
|
+
* For every caller whose success does not depend on the delete landing — the
|
|
280
|
+
* migration's cleanup above, and `clearCredentials` below it. The store is
|
|
281
|
+
* allowed to refuse (a locked keyring rejects deletes, a read-only backend has
|
|
282
|
+
* nothing to delete from), and in each of those cases the entry is either
|
|
283
|
+
* already gone or will be superseded; throwing would report a failure for work
|
|
284
|
+
* that is not needed.
|
|
285
|
+
*
|
|
286
|
+
* Not used where the delete *is* the operation — a save route clearing before
|
|
287
|
+
* it writes must know the clear happened, and that path calls `store.delete`
|
|
288
|
+
* directly.
|
|
289
|
+
*
|
|
290
|
+
* @param store - the caller's credential store.
|
|
291
|
+
* @param account - the entry to remove.
|
|
292
|
+
*/
|
|
293
|
+
function tryDelete(store, account) {
|
|
294
|
+
try {
|
|
295
|
+
store.delete(account);
|
|
296
|
+
}
|
|
297
|
+
catch {
|
|
298
|
+
// Best-effort: see the note above.
|
|
299
|
+
}
|
|
300
|
+
}
|
|
252
301
|
/**
|
|
253
302
|
* Read the Huawei Cloud pair.
|
|
254
303
|
*
|
|
@@ -310,11 +359,11 @@ export function updateCredentials(store, update) {
|
|
|
310
359
|
return next;
|
|
311
360
|
}
|
|
312
361
|
/**
|
|
313
|
-
* Store the Huawei Cloud pair, replacing
|
|
362
|
+
* Store the Huawei Cloud pair, replacing whatever is there.
|
|
314
363
|
*
|
|
315
364
|
* One write, so the pair can never be left half-replaced. A save without a
|
|
316
|
-
* token removes a
|
|
317
|
-
*
|
|
365
|
+
* token removes a stored one: a token belongs to the credential set that
|
|
366
|
+
* carried it, and inheriting a stale one would sign the next request with
|
|
318
367
|
* someone else's session.
|
|
319
368
|
*
|
|
320
369
|
* @param store - the caller's credential store.
|
|
@@ -357,16 +406,23 @@ export function clearE2BKey(store) {
|
|
|
357
406
|
*
|
|
358
407
|
* The whole record goes, so a clear cannot leave a token or a key behind for
|
|
359
408
|
* the next pair to inherit. Deleting an absent entry is a no-op in every
|
|
360
|
-
* backend
|
|
361
|
-
*
|
|
409
|
+
* backend.
|
|
410
|
+
*
|
|
411
|
+
* The per-field names are removed as well, so a store the fold never reached
|
|
412
|
+
* does not keep answering after a clear. Those deletes are best-effort and the
|
|
413
|
+
* main record's is not: a refusal on `CREDENTIAL_ACCOUNT` means the credentials
|
|
414
|
+
* are still there and the person has to be told, while the per-field names are
|
|
415
|
+
* leftovers whose removal nothing depends on. Swallowing the first would report
|
|
416
|
+
* a clear that did not happen.
|
|
362
417
|
*
|
|
363
418
|
* @param store - the caller's credential store.
|
|
419
|
+
* @throws when the backend refused to remove the main record.
|
|
364
420
|
*/
|
|
365
421
|
export function clearCredentials(store) {
|
|
366
422
|
store.delete(CREDENTIAL_ACCOUNT);
|
|
367
423
|
for (const account of LEGACY_ACCOUNTS)
|
|
368
|
-
store
|
|
369
|
-
store
|
|
424
|
+
tryDelete(store, account);
|
|
425
|
+
tryDelete(store, LEGACY_E2B_ACCOUNT);
|
|
370
426
|
}
|
|
371
427
|
/**
|
|
372
428
|
* A short, stable digest of an AK, for recording which identity minted a key.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"accounts.js","sourceRoot":"","sources":["../../src/accounts.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"accounts.js","sourceRoot":"","sources":["../../src/accounts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmEG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAIxC;;;;;GAKG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,oBAAoB,CAAA;AAEtD;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,eAAe,EAAE,eAAe,EAAE,mBAAmB,CAAU,CAAA;AAE/F;;;;;GAKG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,qBAAqB,CAAA;AAgCvD;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAAC,MAAwB;IACnD,OAAO,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAA;AACvE,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,YAAY,CAAC,GAAuB;IAClD,IAAI,GAAG,KAAK,SAAS,IAAI,GAAG,KAAK,EAAE;QAAE,OAAO,EAAE,CAAA;IAC9C,MAAM,IAAI,GAAG,UAAU,CAAC,GAAG,CAAC,CAAA;IAC5B,IAAI,IAAI,KAAK,SAAS;QAAE,OAAO,EAAE,CAAA;IACjC,IAAI,MAAe,CAAA;IACnB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;IAC3B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAA;IACX,CAAC;IACD,IAAI,MAAM,KAAK,IAAI,IAAI,OAAO,MAAM,KAAK,QAAQ;QAAE,OAAO,EAAE,CAAA;IAC5D,MAAM,SAAS,GAAG,MAA0D,CAAA;IAC5E,OAAO;QACL,GAAG,CAAC,oBAAoB,CAAC,SAAS,CAAC,WAAW,CAAC;YAC7C,CAAC,CAAC,EAAE,WAAW,EAAE,SAAS,CAAC,WAAW,EAAE;YACxC,CAAC,CAAC,EAAE,CAAC;QACP,GAAG,CAAC,YAAY,CAAC,SAAS,CAAC,WAAW,CAAC;YACrC,CAAC,CAAC,EAAE,WAAW,EAAE,SAAS,CAAC,WAAW,EAAE;YACxC,CAAC,CAAC,EAAE,CAAC;KACR,CAAA;AACH,CAAC;AAED,6EAA6E;AAC7E,SAAS,UAAU,CAAC,GAAW;IAC7B,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAA;QAC1C,4EAA4E;QAC5E,2EAA2E;QAC3E,IAAI,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,KAAK,GAAG,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,EAAE,CAAC;YAC1D,MAAM,IAAI,GAAG,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAA;YACrC,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC;gBAAE,OAAO,IAAI,CAAA;QACnD,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,uBAAuB;IACzB,CAAC;IACD,OAAO,GAAG,CAAC,SAAS,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAA;AAC1D,CAAC;AAED,gEAAgE;AAChE,SAAS,oBAAoB,CAAC,KAAc;IAC1C,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAA;IAC7D,MAAM,OAAO,GAAG,KAAgC,CAAA;IAChD,OAAO,OAAO,OAAO,CAAC,UAAU,KAAK,QAAQ,IAAI,OAAO,CAAC,UAAU,KAAK,EAAE;WACrE,OAAO,OAAO,CAAC,UAAU,KAAK,QAAQ,IAAI,OAAO,CAAC,UAAU,KAAK,EAAE;WACnE,CAAC,OAAO,CAAC,cAAc,KAAK,SAAS,IAAI,OAAO,OAAO,CAAC,cAAc,KAAK,QAAQ,CAAC,CAAA;AAC3F,CAAC;AAED,uDAAuD;AACvD,SAAS,YAAY,CAAC,KAAc;IAClC,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAA;IAC7D,MAAM,OAAO,GAAG,KAAgC,CAAA;IAChD,OAAO,OAAO,OAAO,CAAC,OAAO,KAAK,QAAQ,IAAI,OAAO,CAAC,OAAO,KAAK,EAAE;WAC/D,OAAO,OAAO,CAAC,UAAU,KAAK,QAAQ,IAAI,OAAO,CAAC,UAAU,KAAK,EAAE;WACnE,CAAC,OAAO,CAAC,cAAc,KAAK,SAAS,IAAI,OAAO,OAAO,CAAC,cAAc,KAAK,QAAQ,CAAC,CAAA;AAC3F,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,eAAe,CAAC,KAAuB;IACrD,MAAM,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAA;IAC5C,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,YAAY,CAAC,MAAM,CAAC,CAAA;IAErD,MAAM,QAAQ,GAAG,aAAa,CAAC,KAAK,CAAC,CAAA;IACrC,IAAI,QAAQ,KAAK,SAAS;QAAE,OAAO,EAAE,CAAA;IACrC,OAAO,QAAQ,CAAA;AACjB,CAAC;AAED;;;;;GAKG;AACH,SAAS,aAAa,CAAC,KAAuB;IAC5C,MAAM,EAAE,GAAG,KAAK,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,CAAA;IACxC,MAAM,EAAE,GAAG,KAAK,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,CAAA;IACxC,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,CAAA;IAC3C,MAAM,SAAS,GAAG,KAAK,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAA;IAE/C,MAAM,WAAW,GACf,EAAE,KAAK,SAAS,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,SAAS,IAAI,EAAE,KAAK,EAAE;QAC5D,CAAC,CAAC;YACA,UAAU,EAAE,EAAE;YACd,UAAU,EAAE,EAAE;YACd,GAAG,CAAC,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,KAAK,EAAE,CAAC;SAC1E;QACD,CAAC,CAAC,SAAS,CAAA;IAEf,0EAA0E;IAC1E,6EAA6E;IAC7E,8EAA8E;IAC9E,qCAAqC;IACrC,MAAM,QAAQ,GAAG,gBAAgB,CAAC,SAAS,CAAC,CAAA;IAE5C,IAAI,WAAW,KAAK,SAAS,IAAI,QAAQ,KAAK,SAAS;QAAE,OAAO,SAAS,CAAA;IAEzE,MAAM,MAAM,GAAqB;QAC/B,GAAG,CAAC,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC;QACrD,GAAG,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,QAAQ,EAAE,CAAC;KAC7D,CAAA;IAED,IAAI,CAAC;QACH,KAAK,CAAC,GAAG,CAAC,kBAAkB,EAAE,YAAY,CAAC,MAAM,CAAC,CAAC,CAAA;IACrD,CAAC;IAAC,MAAM,CAAC;QACP,2EAA2E;QAC3E,+DAA+D;QAC/D,OAAO,MAAM,CAAA;IACf,CAAC;IACD,2EAA2E;IAC3E,6EAA6E;IAC7E,6EAA6E;IAC7E,uEAAuE;IACvE,0EAA0E;IAC1E,0EAA0E;IAC1E,wCAAwC;IACxC,KAAK,MAAM,OAAO,IAAI,eAAe,EAAE,CAAC;QAAC,SAAS,CAAC,KAAK,EAAE,OAAO,CAAC,CAAA;IAAC,CAAC;IACpE,SAAS,CAAC,KAAK,EAAE,kBAAkB,CAAC,CAAA;IACpC,OAAO,MAAM,CAAA;AACf,CAAC;AAED,6DAA6D;AAC7D,SAAS,gBAAgB,CAAC,GAAuB;IAC/C,IAAI,GAAG,KAAK,SAAS,IAAI,GAAG,KAAK,EAAE;QAAE,OAAO,SAAS,CAAA;IACrD,IAAI,MAAe,CAAA;IACnB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;IAC1B,CAAC;IAAC,MAAM,CAAC;QACP,2EAA2E;QAC3E,sEAAsE;QACtE,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,UAAU,EAAE,cAAc,EAAE,CAAA;IACrD,CAAC;IACD,MAAM,SAAS,GAAG,MAAgF,CAAA;IAClG,IAAI,SAAS,KAAK,IAAI,IAAI,OAAO,SAAS,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAA;IACzE,IAAI,OAAO,SAAS,CAAC,GAAG,KAAK,QAAQ,IAAI,SAAS,CAAC,GAAG,KAAK,EAAE;QAAE,OAAO,SAAS,CAAA;IAC/E,OAAO;QACL,OAAO,EAAE,SAAS,CAAC,GAAG;QACtB,UAAU,EAAE,OAAO,SAAS,CAAC,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC,CAAC,cAAc;QAC1F,GAAG,CAAC,OAAO,SAAS,CAAC,aAAa,KAAK,QAAQ,IAAI,SAAS,CAAC,aAAa,KAAK,EAAE;YAC/E,CAAC,CAAC,EAAE,cAAc,EAAE,SAAS,CAAC,aAAa,EAAE;YAC7C,CAAC,CAAC,EAAE,CAAC;KACR,CAAA;AACH,CAAC;AAED,0DAA0D;AAC1D,MAAM,CAAC,MAAM,cAAc,GAAG,0BAA0B,CAAA;AAExD;;;;;;;;;;;;;;;;GAgBG;AACH,SAAS,SAAS,CAAC,KAAuB,EAAE,OAAe;IACzD,IAAI,CAAC;QACH,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;IACvB,CAAC;IAAC,MAAM,CAAC;QACP,mCAAmC;IACrC,CAAC;AACH,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,cAAc,CAAC,KAAuB;IACpD,MAAM,OAAO,GAAG,eAAe,CAAC,KAAK,CAAC,CAAC,WAAW,CAAA;IAClD,IAAI,OAAO,KAAK,SAAS;QAAE,OAAO,SAAS,CAAA;IAC3C,OAAO;QACL,EAAE,EAAE,OAAO,CAAC,UAAU;QACtB,EAAE,EAAE,OAAO,CAAC,UAAU;QACtB,GAAG,CAAC,OAAO,CAAC,cAAc,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,OAAO,CAAC,cAAc,EAAE,CAAC;KAC3F,CAAA;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,KAAuB;IACnD,OAAO,cAAc,CAAC,KAAK,CAAC,KAAK,SAAS,CAAA;AAC5C,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,UAAU,CAAC,KAAuB;IAChD,OAAO,eAAe,CAAC,KAAK,CAAC,CAAC,WAAW,CAAA;AAC3C,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,iBAAiB,CAC/B,KAAuB,EACvB,MAAmE;IAEnE,MAAM,OAAO,GAAG,eAAe,CAAC,KAAK,CAAC,CAAA;IACtC,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,CAAA;IAC5B,IAAI,IAAI,KAAK,SAAS;QAAE,OAAO,OAAO,CAAA;IACtC,KAAK,CAAC,GAAG,CAAC,kBAAkB,EAAE,YAAY,CAAC,IAAI,CAAC,CAAC,CAAA;IACjD,OAAO,IAAI,CAAA;AACb,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAAuB,EAAE,WAA4B;IACpF,iBAAiB,CAAC,KAAK,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC;QACnC,GAAG,OAAO;QACV,WAAW,EAAE;YACX,UAAU,EAAE,WAAW,CAAC,EAAE;YAC1B,UAAU,EAAE,WAAW,CAAC,EAAE;YAC1B,GAAG,CAAC,WAAW,CAAC,aAAa,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,WAAW,CAAC,aAAa,EAAE,CAAC;SAClG;KACF,CAAC,CAAC,CAAA;AACL,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAC,KAAuB,EAAE,GAAe;IAClE,iBAAiB,CAAC,KAAK,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC,EAAE,GAAG,OAAO,EAAE,WAAW,EAAE,GAAG,EAAE,CAAC,CAAC,CAAA;AACzE,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,WAAW,CAAC,KAAuB;IACjD,iBAAiB,CAAC,KAAK,EAAE,OAAO,CAAC,EAAE;QACjC,IAAI,OAAO,CAAC,WAAW,KAAK,SAAS;YAAE,OAAO,SAAS,CAAA;QACvD,MAAM,EAAE,WAAW,EAAE,QAAQ,EAAE,GAAG,IAAI,EAAE,GAAG,OAAO,CAAA;QAClD,OAAO,IAAI,CAAA;IACb,CAAC,CAAC,CAAA;AACJ,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAAuB;IACtD,KAAK,CAAC,MAAM,CAAC,kBAAkB,CAAC,CAAA;IAChC,KAAK,MAAM,OAAO,IAAI,eAAe;QAAE,SAAS,CAAC,KAAK,EAAE,OAAO,CAAC,CAAA;IAChE,SAAS,CAAC,KAAK,EAAE,kBAAkB,CAAC,CAAA;AACtC,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,WAAW,CAAC,EAAU;IACpC,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;AAC9D,CAAC"}
|
|
@@ -11,6 +11,23 @@
|
|
|
11
11
|
* known limitation, not a security hole.
|
|
12
12
|
*/
|
|
13
13
|
import type { CredentialsStore, CredentialsStoreConfig } from '../index.ts';
|
|
14
|
+
/**
|
|
15
|
+
* A credentials file that exists but cannot be decrypted or parsed.
|
|
16
|
+
*
|
|
17
|
+
* Its own type rather than a plain `Error` because the write path has to tell
|
|
18
|
+
* this case from every other failure: this is the one where overwriting the
|
|
19
|
+
* file would destroy credentials the person still has, so `set` moves it aside
|
|
20
|
+
* first. Anything else propagates unchanged.
|
|
21
|
+
*/
|
|
22
|
+
export declare class UnreadableCredentialsFile extends Error {
|
|
23
|
+
/** The file that could not be read. */
|
|
24
|
+
readonly filePath: string;
|
|
25
|
+
constructor(
|
|
26
|
+
/** The file that could not be read. */
|
|
27
|
+
filePath: string,
|
|
28
|
+
/** What decryption or parsing reported. */
|
|
29
|
+
cause: unknown);
|
|
30
|
+
}
|
|
14
31
|
/**
|
|
15
32
|
* Encrypted-file {@link CredentialsStore}. Each account is one entry in a
|
|
16
33
|
* single JSON map, encrypted as one file under the resolved credentials
|
|
@@ -29,12 +46,73 @@ export declare class FingerprintBackend implements CredentialsStore {
|
|
|
29
46
|
* read, used, and dropped so plaintext does not linger in memory between
|
|
30
47
|
* operations. The file I/O + scrypt decrypt per call is acceptable: this is
|
|
31
48
|
* not a hot path (once per chat request, not per token).
|
|
49
|
+
*
|
|
50
|
+
* @throws {@link UnreadableCredentialsFile} when the file exists but cannot be
|
|
51
|
+
* decrypted or parsed. Callers decide what that means; see the note on
|
|
52
|
+
* {@link FingerprintBackend.loadForWrite}.
|
|
32
53
|
*/
|
|
33
54
|
private load;
|
|
55
|
+
/**
|
|
56
|
+
* Load for a write, where an unreadable file must not be silently replaced.
|
|
57
|
+
*
|
|
58
|
+
* ## Why this is not just `load()` with the error swallowed
|
|
59
|
+
*
|
|
60
|
+
* `set` re-encrypts and rewrites the *whole* map. If load failed and returned
|
|
61
|
+
* empty, the next save would write a one-entry file over every other
|
|
62
|
+
* credential the person had stored — with no error, no backup and nothing to
|
|
63
|
+
* recover from. An unreadable file is not proof of an empty one: a truncated
|
|
64
|
+
* write, a machine-id that changed under the file, or a build whose salt
|
|
65
|
+
* moved all look identical from here.
|
|
66
|
+
*
|
|
67
|
+
* So the unreadable file is moved aside instead of overwritten. Saving
|
|
68
|
+
* therefore keeps working on a machine that inherited someone else's file —
|
|
69
|
+
* the documented VM-clone case — while whatever was in it stays on disk under
|
|
70
|
+
* a name that says what happened to it.
|
|
71
|
+
*
|
|
72
|
+
* @returns the current entries, or an empty map when the old file was moved
|
|
73
|
+
* aside.
|
|
74
|
+
*/
|
|
75
|
+
private loadForWrite;
|
|
76
|
+
/**
|
|
77
|
+
* Move an unreadable file out of the way.
|
|
78
|
+
*
|
|
79
|
+
* The suffix carries a timestamp and, if that name is taken, a counter — the
|
|
80
|
+
* quarantine must never itself throw over a name collision and must never
|
|
81
|
+
* overwrite a previous one, since the files it preserves are the only copy of
|
|
82
|
+
* what was there. The counter is bounded because an unbounded retry loop over
|
|
83
|
+
* a filesystem is a hang, not a fallback; reaching the bound means something
|
|
84
|
+
* outside this process is wrong and saying so is better than spinning.
|
|
85
|
+
*/
|
|
86
|
+
private quarantine;
|
|
34
87
|
/** Encrypt + write the map to disk with owner-only permissions. */
|
|
35
88
|
private persist;
|
|
89
|
+
/**
|
|
90
|
+
* Read one entry.
|
|
91
|
+
*
|
|
92
|
+
* An unreadable file reads as absent rather than throwing. The alternative —
|
|
93
|
+
* failing every tool call on a parse error — leaves the person with no
|
|
94
|
+
* working path; "not authenticated" at least routes them to the form that can
|
|
95
|
+
* fix it. The write path is where the difference is preserved; see
|
|
96
|
+
* {@link FingerprintBackend.loadForWrite}.
|
|
97
|
+
*/
|
|
36
98
|
get(account: string): string | undefined;
|
|
37
99
|
set(account: string, value: string): void;
|
|
100
|
+
/**
|
|
101
|
+
* Remove one entry.
|
|
102
|
+
*
|
|
103
|
+
* An unreadable file **throws** rather than being a silent no-op. There is
|
|
104
|
+
* nothing to remove from a map that cannot be read, and answering "done"
|
|
105
|
+
* would be a clear that did not clear: the file stays on disk, and the
|
|
106
|
+
* moment whatever made it unreadable is undone — a restored machine id, a
|
|
107
|
+
* repaired file — the credential reads back. `clearCredentials` relies on
|
|
108
|
+
* exactly this signal to tell the person the clear failed.
|
|
109
|
+
*
|
|
110
|
+
* Quarantining instead is not an option here: it would move the file out of
|
|
111
|
+
* the way, which is the destruction this method must not perform — a delete
|
|
112
|
+
* asked for one entry, not for every entry.
|
|
113
|
+
*
|
|
114
|
+
* @throws {@link UnreadableCredentialsFile} when the file cannot be read.
|
|
115
|
+
*/
|
|
38
116
|
delete(account: string): void;
|
|
39
117
|
has(account: string): boolean;
|
|
40
118
|
}
|
|
@@ -1 +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;
|
|
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;AAmF3E;;;;;;;GAOG;AACH,qBAAa,yBAA0B,SAAQ,KAAK;IAEhD,uCAAuC;IACvC,QAAQ,CAAC,QAAQ,EAAE,MAAM;;IADzB,uCAAuC;IAC9B,QAAQ,EAAE,MAAM;IACzB,2CAA2C;IAC3C,KAAK,EAAE,OAAO;CAQjB;AAED;;;;;;;;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;;;;;;;;;OASG;IACH,OAAO,CAAC,IAAI;IAWZ;;;;;;;;;;;;;;;;;;;OAmBG;IACH,OAAO,CAAC,YAAY;IAUpB;;;;;;;;;OASG;IACH,OAAO,CAAC,UAAU;IA0BlB,mEAAmE;IACnE,OAAO,CAAC,OAAO;IAQf;;;;;;;;OAQG;IACH,GAAG,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAQxC,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IAOzC;;;;;;;;;;;;;;;OAeG;IACH,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAO7B,GAAG,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO;CAO9B"}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* known limitation, not a security hole.
|
|
12
12
|
*/
|
|
13
13
|
import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from 'node:crypto';
|
|
14
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
14
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync } from 'node:fs';
|
|
15
15
|
import { join } from 'node:path';
|
|
16
16
|
import machineIdModule from 'node-machine-id';
|
|
17
17
|
import { CREDENTIALS_FILE_NAME } from "../scope.js";
|
|
@@ -25,10 +25,33 @@ const SALT = 'aishell-dsh/credentials/v1';
|
|
|
25
25
|
const KEY_LEN = 32; // AES-256
|
|
26
26
|
const NONCE_LEN = 12; // GCM standard (96-bit nonce)
|
|
27
27
|
const TAG_LEN = 16; // 128-bit auth tag
|
|
28
|
+
/**
|
|
29
|
+
* scrypt cost parameters, stated rather than inherited.
|
|
30
|
+
*
|
|
31
|
+
* Passing no options lets the runtime's defaults decide, and those are not a
|
|
32
|
+
* promise: Node documents the defaults but does not freeze them, so a runtime
|
|
33
|
+
* that changed them would silently derive a different key and make every
|
|
34
|
+
* existing `credentials.enc` undecryptable. They are part of the on-disk
|
|
35
|
+
* format for the same reason {@link SALT} is — **changing any of them is a
|
|
36
|
+
* breaking change to every stored file**, not a tuning knob.
|
|
37
|
+
*
|
|
38
|
+
* These are the values the format was defined with, so writing them down keeps
|
|
39
|
+
* every existing file readable while removing the runtime from the equation.
|
|
40
|
+
*/
|
|
41
|
+
const SCRYPT_PARAMS = { N: 16384, r: 8, p: 1 };
|
|
42
|
+
/**
|
|
43
|
+
* How many quarantine filenames to try before giving up.
|
|
44
|
+
*
|
|
45
|
+
* Collisions are only possible when several saves land in the same millisecond
|
|
46
|
+
* on a machine whose last few saves all quarantined — which is already an
|
|
47
|
+
* unhealthy machine. The bound exists so that case reports itself instead of
|
|
48
|
+
* looping.
|
|
49
|
+
*/
|
|
50
|
+
const QUARANTINE_NAME_ATTEMPTS = 16;
|
|
28
51
|
/** Derive a 32-byte key from the machine UUID via scrypt. */
|
|
29
52
|
function deriveKey() {
|
|
30
53
|
const machineId = machineIdSync();
|
|
31
|
-
return scryptSync(machineId, SALT, KEY_LEN);
|
|
54
|
+
return scryptSync(machineId, SALT, KEY_LEN, SCRYPT_PARAMS);
|
|
32
55
|
}
|
|
33
56
|
/** Encrypt plaintext → [nonce][tag][ciphertext] as a Buffer. */
|
|
34
57
|
function fingerprintEncrypt(plaintext) {
|
|
@@ -66,6 +89,27 @@ function fingerprintDecrypt(data) {
|
|
|
66
89
|
function resolveCredentialsDir(config) {
|
|
67
90
|
return config.credentialsDir ?? credentialsDir();
|
|
68
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* A credentials file that exists but cannot be decrypted or parsed.
|
|
94
|
+
*
|
|
95
|
+
* Its own type rather than a plain `Error` because the write path has to tell
|
|
96
|
+
* this case from every other failure: this is the one where overwriting the
|
|
97
|
+
* file would destroy credentials the person still has, so `set` moves it aside
|
|
98
|
+
* first. Anything else propagates unchanged.
|
|
99
|
+
*/
|
|
100
|
+
export class UnreadableCredentialsFile extends Error {
|
|
101
|
+
filePath;
|
|
102
|
+
constructor(
|
|
103
|
+
/** The file that could not be read. */
|
|
104
|
+
filePath,
|
|
105
|
+
/** What decryption or parsing reported. */
|
|
106
|
+
cause) {
|
|
107
|
+
super(`credentials file ${filePath} exists but could not be decrypted: `
|
|
108
|
+
+ (cause instanceof Error ? cause.message : String(cause)));
|
|
109
|
+
this.filePath = filePath;
|
|
110
|
+
this.name = 'UnreadableCredentialsFile';
|
|
111
|
+
}
|
|
112
|
+
}
|
|
69
113
|
/**
|
|
70
114
|
* Encrypted-file {@link CredentialsStore}. Each account is one entry in a
|
|
71
115
|
* single JSON map, encrypted as one file under the resolved credentials
|
|
@@ -90,20 +134,88 @@ export class FingerprintBackend {
|
|
|
90
134
|
* read, used, and dropped so plaintext does not linger in memory between
|
|
91
135
|
* operations. The file I/O + scrypt decrypt per call is acceptable: this is
|
|
92
136
|
* not a hot path (once per chat request, not per token).
|
|
137
|
+
*
|
|
138
|
+
* @throws {@link UnreadableCredentialsFile} when the file exists but cannot be
|
|
139
|
+
* decrypted or parsed. Callers decide what that means; see the note on
|
|
140
|
+
* {@link FingerprintBackend.loadForWrite}.
|
|
93
141
|
*/
|
|
94
142
|
load() {
|
|
95
143
|
if (!existsSync(this.filePath))
|
|
96
144
|
return new Map();
|
|
145
|
+
const encrypted = readFileSync(this.filePath);
|
|
97
146
|
try {
|
|
98
|
-
const encrypted = readFileSync(this.filePath);
|
|
99
147
|
const plaintext = fingerprintDecrypt(encrypted);
|
|
100
148
|
return new Map(Object.entries(JSON.parse(plaintext)));
|
|
101
149
|
}
|
|
102
|
-
catch {
|
|
103
|
-
|
|
150
|
+
catch (error) {
|
|
151
|
+
throw new UnreadableCredentialsFile(this.filePath, error);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Load for a write, where an unreadable file must not be silently replaced.
|
|
156
|
+
*
|
|
157
|
+
* ## Why this is not just `load()` with the error swallowed
|
|
158
|
+
*
|
|
159
|
+
* `set` re-encrypts and rewrites the *whole* map. If load failed and returned
|
|
160
|
+
* empty, the next save would write a one-entry file over every other
|
|
161
|
+
* credential the person had stored — with no error, no backup and nothing to
|
|
162
|
+
* recover from. An unreadable file is not proof of an empty one: a truncated
|
|
163
|
+
* write, a machine-id that changed under the file, or a build whose salt
|
|
164
|
+
* moved all look identical from here.
|
|
165
|
+
*
|
|
166
|
+
* So the unreadable file is moved aside instead of overwritten. Saving
|
|
167
|
+
* therefore keeps working on a machine that inherited someone else's file —
|
|
168
|
+
* the documented VM-clone case — while whatever was in it stays on disk under
|
|
169
|
+
* a name that says what happened to it.
|
|
170
|
+
*
|
|
171
|
+
* @returns the current entries, or an empty map when the old file was moved
|
|
172
|
+
* aside.
|
|
173
|
+
*/
|
|
174
|
+
loadForWrite() {
|
|
175
|
+
try {
|
|
176
|
+
return this.load();
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
if (!(error instanceof UnreadableCredentialsFile))
|
|
180
|
+
throw error;
|
|
181
|
+
this.quarantine(error);
|
|
104
182
|
return new Map();
|
|
105
183
|
}
|
|
106
184
|
}
|
|
185
|
+
/**
|
|
186
|
+
* Move an unreadable file out of the way.
|
|
187
|
+
*
|
|
188
|
+
* The suffix carries a timestamp and, if that name is taken, a counter — the
|
|
189
|
+
* quarantine must never itself throw over a name collision and must never
|
|
190
|
+
* overwrite a previous one, since the files it preserves are the only copy of
|
|
191
|
+
* what was there. The counter is bounded because an unbounded retry loop over
|
|
192
|
+
* a filesystem is a hang, not a fallback; reaching the bound means something
|
|
193
|
+
* outside this process is wrong and saying so is better than spinning.
|
|
194
|
+
*/
|
|
195
|
+
quarantine(cause) {
|
|
196
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
197
|
+
for (let attempt = 0; attempt < QUARANTINE_NAME_ATTEMPTS; attempt += 1) {
|
|
198
|
+
const suffix = attempt === 0 ? '' : `-${attempt}`;
|
|
199
|
+
const target = `${this.filePath}.unreadable-${stamp}${suffix}`;
|
|
200
|
+
if (existsSync(target))
|
|
201
|
+
continue;
|
|
202
|
+
try {
|
|
203
|
+
renameSync(this.filePath, target);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
// Another process quarantined it first: nothing left to move, and the
|
|
208
|
+
// write that follows is against a fresh file either way.
|
|
209
|
+
if (error instanceof Error && 'code' in error && error.code === 'ENOENT')
|
|
210
|
+
return;
|
|
211
|
+
throw new Error(`credentials file ${this.filePath} could not be read (${cause.message}), `
|
|
212
|
+
+ `and could not be moved aside to ${target} either: ${String(error)}`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
throw new Error(`credentials file ${this.filePath} could not be read (${cause.message}), and every `
|
|
216
|
+
+ `quarantine name is taken up to ${QUARANTINE_NAME_ATTEMPTS} attempts — `
|
|
217
|
+
+ 'move the file aside by hand before saving again.');
|
|
218
|
+
}
|
|
107
219
|
/** Encrypt + write the map to disk with owner-only permissions. */
|
|
108
220
|
persist(entries) {
|
|
109
221
|
const plaintext = JSON.stringify(Object.fromEntries(entries));
|
|
@@ -112,16 +224,46 @@ export class FingerprintBackend {
|
|
|
112
224
|
// the file is still encrypted, and ACLs inherit from the profile dir.
|
|
113
225
|
writeFileSync(this.filePath, fingerprintEncrypt(plaintext), { mode: 0o600 });
|
|
114
226
|
}
|
|
227
|
+
/**
|
|
228
|
+
* Read one entry.
|
|
229
|
+
*
|
|
230
|
+
* An unreadable file reads as absent rather than throwing. The alternative —
|
|
231
|
+
* failing every tool call on a parse error — leaves the person with no
|
|
232
|
+
* working path; "not authenticated" at least routes them to the form that can
|
|
233
|
+
* fix it. The write path is where the difference is preserved; see
|
|
234
|
+
* {@link FingerprintBackend.loadForWrite}.
|
|
235
|
+
*/
|
|
115
236
|
get(account) {
|
|
116
|
-
|
|
237
|
+
try {
|
|
238
|
+
return this.load().get(account);
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
return undefined;
|
|
242
|
+
}
|
|
117
243
|
}
|
|
118
244
|
set(account, value) {
|
|
119
245
|
if (value.length === 0)
|
|
120
246
|
throw new Error('credential value must be non-empty');
|
|
121
|
-
const entries = this.
|
|
247
|
+
const entries = this.loadForWrite();
|
|
122
248
|
entries.set(account, value);
|
|
123
249
|
this.persist(entries);
|
|
124
250
|
}
|
|
251
|
+
/**
|
|
252
|
+
* Remove one entry.
|
|
253
|
+
*
|
|
254
|
+
* An unreadable file **throws** rather than being a silent no-op. There is
|
|
255
|
+
* nothing to remove from a map that cannot be read, and answering "done"
|
|
256
|
+
* would be a clear that did not clear: the file stays on disk, and the
|
|
257
|
+
* moment whatever made it unreadable is undone — a restored machine id, a
|
|
258
|
+
* repaired file — the credential reads back. `clearCredentials` relies on
|
|
259
|
+
* exactly this signal to tell the person the clear failed.
|
|
260
|
+
*
|
|
261
|
+
* Quarantining instead is not an option here: it would move the file out of
|
|
262
|
+
* the way, which is the destruction this method must not perform — a delete
|
|
263
|
+
* asked for one entry, not for every entry.
|
|
264
|
+
*
|
|
265
|
+
* @throws {@link UnreadableCredentialsFile} when the file cannot be read.
|
|
266
|
+
*/
|
|
125
267
|
delete(account) {
|
|
126
268
|
const entries = this.load();
|
|
127
269
|
if (entries.delete(account)) {
|
|
@@ -129,7 +271,12 @@ export class FingerprintBackend {
|
|
|
129
271
|
}
|
|
130
272
|
}
|
|
131
273
|
has(account) {
|
|
132
|
-
|
|
274
|
+
try {
|
|
275
|
+
return this.load().has(account);
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
133
280
|
}
|
|
134
281
|
}
|
|
135
282
|
//# sourceMappingURL=fingerprint.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fingerprint.js","sourceRoot":"","sources":["../../../src/backends/fingerprint.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AACvF,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"fingerprint.js","sourceRoot":"","sources":["../../../src/backends/fingerprint.ts"],"names":[],"mappings":"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"}
|