@huaweidevtools/aishell-dsh-credentials-store 0.0.1-beta.1 → 0.0.1-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +62 -14
  3. package/lib/accounts.d.ts +187 -0
  4. package/lib/accounts.d.ts.map +1 -0
  5. package/lib/accounts.js +298 -0
  6. package/lib/accounts.js.map +1 -0
  7. package/lib/index-CsXaUaRH.d.ts +211 -0
  8. package/lib/index-CsXaUaRH.d.ts.map +1 -0
  9. package/lib/index.d.ts +3 -208
  10. package/lib/index.js +26 -28
  11. package/lib/index.js.map +1 -1
  12. package/lib/paths-DtFRzop6.js +46 -0
  13. package/lib/paths-DtFRzop6.js.map +1 -0
  14. package/lib/paths.d.ts +56 -0
  15. package/lib/paths.d.ts.map +1 -0
  16. package/lib/paths.js +3 -0
  17. package/lib/scope-CFetnbG3.d.ts +71 -0
  18. package/lib/scope-CFetnbG3.d.ts.map +1 -0
  19. package/lib/scope-QmrHZaZt.js +96 -0
  20. package/lib/scope-QmrHZaZt.js.map +1 -0
  21. package/lib/scope.d.ts +2 -0
  22. package/lib/scope.js +3 -0
  23. package/lib/signer-BPR0UAF0.js +66 -0
  24. package/lib/signer-BPR0UAF0.js.map +1 -0
  25. package/lib/signer-Et0DIH6o.d.ts +40 -0
  26. package/lib/signer-Et0DIH6o.d.ts.map +1 -0
  27. package/lib/signer.d.ts +2 -40
  28. package/lib/signer.js +2 -65
  29. package/lib/types/accounts.d.ts +231 -0
  30. package/lib/types/accounts.d.ts.map +1 -0
  31. package/lib/types/accounts.js +384 -0
  32. package/lib/types/accounts.js.map +1 -0
  33. package/lib/types/backends/fingerprint.d.ts +41 -0
  34. package/lib/types/backends/fingerprint.d.ts.map +1 -0
  35. package/lib/types/backends/fingerprint.js +135 -0
  36. package/lib/types/backends/fingerprint.js.map +1 -0
  37. package/lib/types/backends/keyring.d.ts +69 -0
  38. package/lib/types/backends/keyring.d.ts.map +1 -0
  39. package/lib/types/backends/keyring.js +107 -0
  40. package/lib/types/backends/keyring.js.map +1 -0
  41. package/lib/types/backends/memory.d.ts +22 -0
  42. package/lib/types/backends/memory.d.ts.map +1 -0
  43. package/lib/types/backends/memory.js +35 -0
  44. package/lib/types/backends/memory.js.map +1 -0
  45. package/lib/types/failover.d.ts +136 -0
  46. package/lib/types/failover.d.ts.map +1 -0
  47. package/lib/types/failover.js +242 -0
  48. package/lib/types/failover.js.map +1 -0
  49. package/lib/types/index.d.ts +70 -0
  50. package/lib/types/index.d.ts.map +1 -0
  51. package/lib/types/index.js +73 -0
  52. package/lib/types/index.js.map +1 -0
  53. package/lib/types/paths.d.ts +50 -0
  54. package/lib/types/paths.d.ts.map +1 -0
  55. package/lib/types/paths.js +59 -0
  56. package/lib/types/paths.js.map +1 -0
  57. package/lib/types/scope.d.ts +65 -0
  58. package/lib/types/scope.d.ts.map +1 -0
  59. package/lib/types/scope.js +102 -0
  60. package/lib/types/scope.js.map +1 -0
  61. package/lib/types/signer.d.ts +36 -0
  62. package/lib/types/signer.d.ts.map +1 -0
  63. package/lib/types/signer.js +81 -0
  64. package/lib/types/signer.js.map +1 -0
  65. package/lib/types/verify.d.ts +114 -0
  66. package/lib/types/verify.d.ts.map +1 -0
  67. package/lib/types/verify.js +240 -0
  68. package/lib/types/verify.js.map +1 -0
  69. package/lib/verify.d.ts +72 -0
  70. package/lib/verify.d.ts.map +1 -0
  71. package/lib/verify.js +172 -0
  72. package/lib/verify.js.map +1 -0
  73. package/package.json +21 -8
  74. 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"}