@huaweidevtools/aishell-dsh-credentials-store 0.0.15 → 0.0.16

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 (75) hide show
  1. package/lib/accounts.d.ts +31 -20
  2. package/lib/accounts.d.ts.map +1 -1
  3. package/lib/accounts.js +57 -24
  4. package/lib/accounts.js.map +1 -1
  5. package/lib/{index-JlyJE1E8.d.ts → index-Cdsu1kDC.d.ts} +87 -12
  6. package/lib/index-Cdsu1kDC.d.ts.map +1 -0
  7. package/lib/index.d.ts +3 -3
  8. package/lib/index.js +165 -29
  9. package/lib/index.js.map +1 -1
  10. package/lib/{paths-DtFRzop6.js → paths-C0wxA5LO.js} +3 -3
  11. package/lib/paths-C0wxA5LO.js.map +1 -0
  12. package/lib/paths.d.ts +10 -9
  13. package/lib/paths.d.ts.map +1 -1
  14. package/lib/paths.js +1 -1
  15. package/lib/{scope-CFetnbG3.d.ts → scope-C7w2eTvE.d.ts} +12 -12
  16. package/lib/{scope-CFetnbG3.d.ts.map → scope-C7w2eTvE.d.ts.map} +1 -1
  17. package/lib/{scope-QmrHZaZt.js → scope-DJcK27Uc.js} +12 -12
  18. package/lib/scope-DJcK27Uc.js.map +1 -0
  19. package/lib/scope.d.ts +1 -1
  20. package/lib/scope.js +1 -1
  21. package/lib/signer-Bx88x7dI.d.ts +72 -0
  22. package/lib/signer-Bx88x7dI.d.ts.map +1 -0
  23. package/lib/{signer-BPR0UAF0.js → signer-CVOjpi6y.js} +39 -3
  24. package/lib/signer-CVOjpi6y.js.map +1 -0
  25. package/lib/signer.d.ts +2 -2
  26. package/lib/signer.js +2 -2
  27. package/lib/types/accounts.d.ts +61 -39
  28. package/lib/types/accounts.d.ts.map +1 -1
  29. package/lib/types/accounts.js +108 -52
  30. package/lib/types/accounts.js.map +1 -1
  31. package/lib/types/backends/fingerprint.d.ts +78 -0
  32. package/lib/types/backends/fingerprint.d.ts.map +1 -1
  33. package/lib/types/backends/fingerprint.js +155 -8
  34. package/lib/types/backends/fingerprint.js.map +1 -1
  35. package/lib/types/backends/keyring.d.ts +7 -7
  36. package/lib/types/backends/keyring.d.ts.map +1 -1
  37. package/lib/types/backends/keyring.js +15 -9
  38. package/lib/types/backends/keyring.js.map +1 -1
  39. package/lib/types/backends/memory.d.ts +4 -17
  40. package/lib/types/backends/memory.d.ts.map +1 -1
  41. package/lib/types/backends/memory.js +21 -32
  42. package/lib/types/backends/memory.js.map +1 -1
  43. package/lib/types/failover.d.ts +16 -18
  44. package/lib/types/failover.d.ts.map +1 -1
  45. package/lib/types/failover.js +19 -21
  46. package/lib/types/failover.js.map +1 -1
  47. package/lib/types/index.d.ts +4 -4
  48. package/lib/types/index.d.ts.map +1 -1
  49. package/lib/types/index.js +4 -4
  50. package/lib/types/index.js.map +1 -1
  51. package/lib/types/paths.d.ts +10 -9
  52. package/lib/types/paths.d.ts.map +1 -1
  53. package/lib/types/paths.js +10 -9
  54. package/lib/types/paths.js.map +1 -1
  55. package/lib/types/scope.d.ts +11 -11
  56. package/lib/types/scope.js +11 -11
  57. package/lib/types/signer.d.ts +32 -0
  58. package/lib/types/signer.d.ts.map +1 -1
  59. package/lib/types/signer.js +40 -5
  60. package/lib/types/signer.js.map +1 -1
  61. package/lib/types/verify.d.ts +7 -11
  62. package/lib/types/verify.d.ts.map +1 -1
  63. package/lib/types/verify.js +27 -11
  64. package/lib/types/verify.js.map +1 -1
  65. package/lib/verify.d.ts +1 -1
  66. package/lib/verify.d.ts.map +1 -1
  67. package/lib/verify.js +11 -1
  68. package/lib/verify.js.map +1 -1
  69. package/package.json +1 -1
  70. package/lib/index-JlyJE1E8.d.ts.map +0 -1
  71. package/lib/paths-DtFRzop6.js.map +0 -1
  72. package/lib/scope-QmrHZaZt.js.map +0 -1
  73. package/lib/signer-BPR0UAF0.js.map +0 -1
  74. package/lib/signer-Et0DIH6o.d.ts +0 -40
  75. package/lib/signer-Et0DIH6o.d.ts.map +0 -1
package/lib/accounts.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import "./scope-CFetnbG3.js";
2
- import { CredentialsStore } from "./index-JlyJE1E8.js";
3
- import { SignCredentials } from "./signer-Et0DIH6o.js";
1
+ import "./scope-C7w2eTvE.js";
2
+ import { CredentialsStore } from "./index-Cdsu1kDC.js";
3
+ import { SignCredentials } from "./signer-Bx88x7dI.js";
4
4
 
5
5
  //#region src/accounts.d.ts
6
6
  /**
@@ -18,18 +18,22 @@ import { SignCredentials } from "./signer-Et0DIH6o.js";
18
18
  */
19
19
  declare const CREDENTIAL_ACCOUNT = "aishell-credential";
20
20
  /**
21
- * Entries written by the earlier three-key layout, in the order they are read
22
- * during migration.
21
+ * The per-field account names a store may still hold beside the bundle.
23
22
  *
24
- * Kept only so an existing install keeps working: see {@link readCredentials}.
25
- * New writes never touch these.
23
+ * These are the read-side names of a store written field-by-field instead of
24
+ * as one record. {@link readCredentials} folds them into
25
+ * {@link CREDENTIAL_ACCOUNT} on first read, so a machine that holds them keeps
26
+ * working; nothing writes them.
27
+ *
28
+ * The order is the order the migration reads them: access key, secret key,
29
+ * token.
26
30
  */
27
31
  declare const LEGACY_ACCOUNTS: readonly ["HW_ACCESS_KEY", "HW_SECRET_KEY", "HW_SECURITY_TOKEN"];
28
32
  /**
29
- * The legacy entry holding an e2b API key on its own.
33
+ * The standalone e2b key entry, beside the per-field names above.
30
34
  *
31
- * Folded into the bundle by {@link readCredentials}. Kept as a constant because
32
- * the migration has to name it.
35
+ * Folded into the bundle by {@link readCredentials}, for the same reason: a
36
+ * store written before the bundle existed keeps working. Nothing writes it.
33
37
  */
34
38
  declare const LEGACY_E2B_ACCOUNT = "AISHELL_E2B_API_KEY";
35
39
  /** The Huawei Cloud pair, as the bundle stores it. Snake-case on disk. */
@@ -81,12 +85,12 @@ declare function encodeBundle(bundle: CredentialBundle): string;
81
85
  */
82
86
  declare function decodeBundle(raw: string | undefined): CredentialBundle;
83
87
  /**
84
- * Read the whole bundle, migrating a legacy layout on the way.
88
+ * Read the whole bundle, folding a per-field store into it on the way.
85
89
  *
86
- * Migration is lazy and idempotent: the first read that finds no bundle but
87
- * finds legacy entries merges them into one record, writes it, and removes the
88
- * old entries. A failed write leaves the legacy entries alone, so the next read
89
- * tries again — the person is never left with neither layout.
90
+ * The fold is lazy and idempotent: a read that finds no bundle but finds the
91
+ * per-field names merges them into one record, writes it, and removes the old
92
+ * entries. A failed write leaves them in place so the next read tries again —
93
+ * a store is never left with neither layout.
90
94
  *
91
95
  * @param store - the caller's credential store.
92
96
  * @returns the bundle; empty when nothing is stored.
@@ -135,11 +139,11 @@ declare function readE2BKey(store: CredentialsStore): E2BSection | undefined;
135
139
  */
136
140
  declare function updateCredentials(store: CredentialsStore, update: (current: CredentialBundle) => CredentialBundle | undefined): CredentialBundle;
137
141
  /**
138
- * Store the Huawei Cloud pair, replacing any previous one.
142
+ * Store the Huawei Cloud pair, replacing whatever is there.
139
143
  *
140
144
  * One write, so the pair can never be left half-replaced. A save without a
141
- * token removes a previously stored one: a token belongs to the credential set
142
- * that carried it, and inheriting a stale one would sign the next request with
145
+ * token removes a stored one: a token belongs to the credential set that
146
+ * carried it, and inheriting a stale one would sign the next request with
143
147
  * someone else's session.
144
148
  *
145
149
  * @param store - the caller's credential store.
@@ -164,10 +168,17 @@ declare function clearE2BKey(store: CredentialsStore): void;
164
168
  *
165
169
  * The whole record goes, so a clear cannot leave a token or a key behind for
166
170
  * the next pair to inherit. Deleting an absent entry is a no-op in every
167
- * backend, and the legacy entries are removed too so a half-migrated store does
168
- * not come back.
171
+ * backend.
172
+ *
173
+ * The per-field names are removed as well, so a store the fold never reached
174
+ * does not keep answering after a clear. Those deletes are best-effort and the
175
+ * main record's is not: a refusal on `CREDENTIAL_ACCOUNT` means the credentials
176
+ * are still there and the person has to be told, while the per-field names are
177
+ * leftovers whose removal nothing depends on. Swallowing the first would report
178
+ * a clear that did not happen.
169
179
  *
170
180
  * @param store - the caller's credential store.
181
+ * @throws when the backend refused to remove the main record.
171
182
  */
172
183
  declare function clearCredentials(store: CredentialsStore): void;
173
184
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"accounts.d.ts","names":[],"sources":["../../src/accounts.ts"],"sourcesContent":null,"mappings":";;;;;;;;;;;;;;;;;;AA4IgB,cAxEH,kBAAA,GAwE0C,oBAAgB;;;;;;;;cA/D1D;;;;AAkIb;;;AAA0D,cA1H7C,kBAAA,GA0H6C,qBAAA;AAAgB;UAvHzD,kBAAA;EAqMJ,SAAA,UAAc,EAAA,MAAA;;;;;;;;;;;;;AAcX,UApMC,UAAA,CAoMa;EAAA,SAAA,OAAA,EAAA,MAAA;EAAA;EAAwB,SAAG,UAAA,EAAA,MAAA;EAAe;;;;UA3LvD,gBAAA;yBACQ;EAyMT,SAAA,WAAa,CAAA,EAxMJ,UAwMY;;;;;;;AAUrC;AAA0B,iBAzMV,YAAA,CAyMU,MAAA,EAzMW,gBAyMX,CAAA,EAAA,MAAA;;;AAAqC;;;;;;;;;;;iBAxL/C,YAAA,2BAAuC;;;AAyMvD;;;;;;AAGmB;;;iBAzIH,eAAA,QAAuB,mBAAmB;;cA8E7C,cAAA;;;;;;;AA8Eb;;;;AAAsF;;iBAhEtE,cAAA,QAAsB,mBAAmB;;;;;AAiFzD;AAA2B,iBAlEX,aAAA,CAkEW,KAAA,EAlEU,gBAkEV,CAAA,EAAA,OAAA;;;AAAyC;;;;iBAxDpD,UAAA,QAAkB,mBAAmB;;AAiErD;;;;;;;;;;;AAkBA;iBAlEgB,iBAAA,QACP,oCACW,qBAAqB,+BACtC;;;;;;;;;;AA+EH;;iBA5DgB,gBAAA,QAAwB,+BAA+B;;;;;;;iBAiBvD,WAAA,QAAmB,uBAAuB;;;;;;iBAS1C,WAAA,QAAmB;;;;;;;;;;;iBAkBnB,gBAAA,QAAwB;;;;;;;;;;;iBAgBxB,WAAA"}
1
+ {"version":3,"file":"accounts.d.ts","names":[],"sources":["../../src/accounts.ts"],"sourcesContent":null,"mappings":";;;;;;;;;;;;;;;;;;cA+Ea,kBAAA;;;;;AA+Ib;;;;AAA0E;;AAsF1E;cAxNa;;;;;;;cAQA,kBAAA;;UAGI,kBAAA;;;EAoPD,SAAA,cAAc,CAAA,EAAA,MAAA;;;;AAA0C;;;;;;AAexE;UApPiB,UAAA;;;;;;AA8PjB;;AAAkC,UArPjB,gBAAA,CAqPiB;EAAgB,SAAG,WAAA,CAAA,EApP5B,kBAoP4B;EAAU,SAAA,WAAA,CAAA,EAnPtC,UAmPsC;;;;;;;;iBA1O/C,YAAA,SAAqB;;;;;;AA2PrC;;;;;;AAGmB;;iBA7OH,YAAA,2BAAuC;;;;;;;;;;AAgQvD;;AAAwC,iBA7LxB,eAAA,CA6LwB,KAAA,EA7LD,gBA6LC,CAAA,EA7LkB,gBA6LlB;;AAA8C,cAvGzE,cAAA,GAuGyE,0BAAA;;;;;;;AAiBtF;;;;AAAoE;;iBAjFpD,cAAA,QAAsB,mBAAmB;;;;AA0FzD;;iBA3EgB,aAAA,QAAqB;;;;;;;iBAUrB,UAAA,QAAkB,mBAAmB;;;;;;;;;AA0FrD;;;;;iBAzEgB,iBAAA,QACP,oCACW,qBAAqB,+BACtC;;;;;;AAsFH;;;;;;iBAnEgB,gBAAA,QAAwB,+BAA+B;;;;;;;iBAiBvD,WAAA,QAAmB,uBAAuB;;;;;;iBAS1C,WAAA,QAAmB;;;;;;;;;;;;;;;;;;iBAyBnB,gBAAA,QAAwB;;;;;;;;;;;iBAgBxB,WAAA"}
package/lib/accounts.js CHANGED
@@ -9,11 +9,15 @@ import { createHash } from "node:crypto";
9
9
  */
10
10
  const CREDENTIAL_ACCOUNT = "aishell-credential";
11
11
  /**
12
- * Entries written by the earlier three-key layout, in the order they are read
13
- * during migration.
12
+ * The per-field account names a store may still hold beside the bundle.
14
13
  *
15
- * Kept only so an existing install keeps working: see {@link readCredentials}.
16
- * New writes never touch these.
14
+ * These are the read-side names of a store written field-by-field instead of
15
+ * as one record. {@link readCredentials} folds them into
16
+ * {@link CREDENTIAL_ACCOUNT} on first read, so a machine that holds them keeps
17
+ * working; nothing writes them.
18
+ *
19
+ * The order is the order the migration reads them: access key, secret key,
20
+ * token.
17
21
  */
18
22
  const LEGACY_ACCOUNTS = [
19
23
  "HW_ACCESS_KEY",
@@ -21,10 +25,10 @@ const LEGACY_ACCOUNTS = [
21
25
  "HW_SECURITY_TOKEN"
22
26
  ];
23
27
  /**
24
- * The legacy entry holding an e2b API key on its own.
28
+ * The standalone e2b key entry, beside the per-field names above.
25
29
  *
26
- * Folded into the bundle by {@link readCredentials}. Kept as a constant because
27
- * the migration has to name it.
30
+ * Folded into the bundle by {@link readCredentials}, for the same reason: a
31
+ * store written before the bundle existed keeps working. Nothing writes it.
28
32
  */
29
33
  const LEGACY_E2B_ACCOUNT = "AISHELL_E2B_API_KEY";
30
34
  /**
@@ -90,12 +94,12 @@ function isE2BSection(value) {
90
94
  return typeof section.api_key === "string" && section.api_key !== "" && typeof section.expired_at === "string" && section.expired_at !== "" && (section.ak_fingerprint === void 0 || typeof section.ak_fingerprint === "string");
91
95
  }
92
96
  /**
93
- * Read the whole bundle, migrating a legacy layout on the way.
97
+ * Read the whole bundle, folding a per-field store into it on the way.
94
98
  *
95
- * Migration is lazy and idempotent: the first read that finds no bundle but
96
- * finds legacy entries merges them into one record, writes it, and removes the
97
- * old entries. A failed write leaves the legacy entries alone, so the next read
98
- * tries again — the person is never left with neither layout.
99
+ * The fold is lazy and idempotent: a read that finds no bundle but finds the
100
+ * per-field names merges them into one record, writes it, and removes the old
101
+ * entries. A failed write leaves them in place so the next read tries again —
102
+ * a store is never left with neither layout.
99
103
  *
100
104
  * @param store - the caller's credential store.
101
105
  * @returns the bundle; empty when nothing is stored.
@@ -108,10 +112,10 @@ function readCredentials(store) {
108
112
  return migrated;
109
113
  }
110
114
  /**
111
- * Merge the legacy entries into one record and remove the old ones.
115
+ * Merge the per-field entries into one record and remove the old ones.
112
116
  *
113
117
  * @param store - the caller's credential store.
114
- * @returns the migrated bundle, or `undefined` when there was nothing to move.
118
+ * @returns the merged bundle, or `undefined` when there was nothing to move.
115
119
  */
116
120
  function migrateLegacy(store) {
117
121
  const ak = store.get(LEGACY_ACCOUNTS[0]);
@@ -134,11 +138,11 @@ function migrateLegacy(store) {
134
138
  } catch {
135
139
  return bundle;
136
140
  }
137
- for (const account of LEGACY_ACCOUNTS) store.delete(account);
138
- store.delete(LEGACY_E2B_ACCOUNT);
141
+ for (const account of LEGACY_ACCOUNTS) tryDelete(store, account);
142
+ tryDelete(store, LEGACY_E2B_ACCOUNT);
139
143
  return bundle;
140
144
  }
141
- /** Read one legacy e2b entry into its bundle section. */
145
+ /** Read one standalone e2b entry into its bundle section. */
142
146
  function legacyE2BSection(raw) {
143
147
  if (raw === void 0 || raw === "") return void 0;
144
148
  let parsed;
@@ -162,6 +166,28 @@ function legacyE2BSection(raw) {
162
166
  /** The expiry recorded for a key that does not expire. */
163
167
  const FOREVER_EXPIRY = "9999-12-31T23:59:59.000Z";
164
168
  /**
169
+ * Delete an entry, swallowing a backend refusal.
170
+ *
171
+ * For every caller whose success does not depend on the delete landing — the
172
+ * migration's cleanup above, and `clearCredentials` below it. The store is
173
+ * allowed to refuse (a locked keyring rejects deletes, a read-only backend has
174
+ * nothing to delete from), and in each of those cases the entry is either
175
+ * already gone or will be superseded; throwing would report a failure for work
176
+ * that is not needed.
177
+ *
178
+ * Not used where the delete *is* the operation — a save route clearing before
179
+ * it writes must know the clear happened, and that path calls `store.delete`
180
+ * directly.
181
+ *
182
+ * @param store - the caller's credential store.
183
+ * @param account - the entry to remove.
184
+ */
185
+ function tryDelete(store, account) {
186
+ try {
187
+ store.delete(account);
188
+ } catch {}
189
+ }
190
+ /**
165
191
  * Read the Huawei Cloud pair.
166
192
  *
167
193
  * AK and SK are read together and treated as one unit: half a pair reads as no
@@ -220,11 +246,11 @@ function updateCredentials(store, update) {
220
246
  return next;
221
247
  }
222
248
  /**
223
- * Store the Huawei Cloud pair, replacing any previous one.
249
+ * Store the Huawei Cloud pair, replacing whatever is there.
224
250
  *
225
251
  * One write, so the pair can never be left half-replaced. A save without a
226
- * token removes a previously stored one: a token belongs to the credential set
227
- * that carried it, and inheriting a stale one would sign the next request with
252
+ * token removes a stored one: a token belongs to the credential set that
253
+ * carried it, and inheriting a stale one would sign the next request with
228
254
  * someone else's session.
229
255
  *
230
256
  * @param store - the caller's credential store.
@@ -269,15 +295,22 @@ function clearE2BKey(store) {
269
295
  *
270
296
  * The whole record goes, so a clear cannot leave a token or a key behind for
271
297
  * the next pair to inherit. Deleting an absent entry is a no-op in every
272
- * backend, and the legacy entries are removed too so a half-migrated store does
273
- * not come back.
298
+ * backend.
299
+ *
300
+ * The per-field names are removed as well, so a store the fold never reached
301
+ * does not keep answering after a clear. Those deletes are best-effort and the
302
+ * main record's is not: a refusal on `CREDENTIAL_ACCOUNT` means the credentials
303
+ * are still there and the person has to be told, while the per-field names are
304
+ * leftovers whose removal nothing depends on. Swallowing the first would report
305
+ * a clear that did not happen.
274
306
  *
275
307
  * @param store - the caller's credential store.
308
+ * @throws when the backend refused to remove the main record.
276
309
  */
277
310
  function clearCredentials(store) {
278
311
  store.delete(CREDENTIAL_ACCOUNT);
279
- for (const account of LEGACY_ACCOUNTS) store.delete(account);
280
- store.delete(LEGACY_E2B_ACCOUNT);
312
+ for (const account of LEGACY_ACCOUNTS) tryDelete(store, account);
313
+ tryDelete(store, LEGACY_E2B_ACCOUNT);
281
314
  }
282
315
  /**
283
316
  * A short, stable digest of an AK, for recording which identity minted a key.
@@ -1 +1 @@
1
- {"version":3,"file":"accounts.js","names":["bundle: CredentialBundle","raw: string | undefined","parsed: unknown","raw: string","value: unknown","store: CredentialsStore","huaweicloud: HuaweiCloudSection | undefined","update: (current: CredentialBundle) => CredentialBundle | undefined","credentials: SignCredentials","key: E2BSection","ak: string"],"sources":["../src/accounts.ts"],"sourcesContent":["/**\n * The Huawei Cloud credential vocabulary every plugin in this set shares.\n *\n * ## Why this module exists\n *\n * This repo once read the same keyring entries from four places, each declaring\n * the account names itself and re-implementing \"read AK and SK as a pair\". The\n * account names are a *persistence* contract, so a divergence would not fail a\n * build — it would surface as one plugin working and another reporting \"not\n * authenticated\" on a machine where the person had stored exactly one pair, and\n * the cause would be invisible in the diff that introduced it.\n *\n * Everything now goes through this file.\n *\n * ## One entry, not three\n *\n * The store holds **one** record, `aishell-credential`, whose value is a single\n * JSON bundle encoded as base64:\n *\n * ```json\n * {\n * \"huaweicloud\": { \"access_key\": \"…\", \"secret_key\": \"…\", \"security_token\": \"…\" },\n * \"aishell_e2b\": { \"api_key\": \"…\", \"expired_at\": \"…\", \"ak_fingerprint\": \"…\" }\n * }\n * ```\n *\n * The earlier layout was three separate entries (`HW_ACCESS_KEY`,\n * `HW_SECRET_KEY`, `HW_SECURITY_TOKEN`). That had two problems this fixes:\n *\n * - **A save was four operations.** Clear, then write AK, then SK, then\n * token. Every backend is per-entry, so a failure between them left a\n * half-set behind, and nothing rolled it back.\n * - **A read was three reads.** A concurrent write landing between them could\n * hand back a new AK with an old SK — a pair that passes \"both present\" and\n * then fails at the gateway with 401, pointing nowhere near the cause.\n *\n * One entry makes a write one atomic operation and a read one consistent\n * snapshot.\n *\n * ## base64 is an encoding, not encryption\n *\n * It keeps the secret from sitting in a plaintext field a person or a log\n * prints while poking at the store. It is **not** a security boundary: anyone\n * who can read the entry can decode it. The actual protection is the backend —\n * the OS keyring, or AES-256-GCM in the file backend.\n *\n * ## Read-modify-write is not atomic across processes\n *\n * {@link updateCredentials} reads the bundle, applies a change and writes it\n * back. Two processes doing that concurrently can lose one of the changes. That\n * is accepted rather than solved: the only writer is a person via the\n * credential form, and both the e2b key and the AK/SK are refreshed by that\n * same person. A lost update would be corrected by the next save. Solving it\n * properly needs compare-and-swap, which the keyring API does not offer.\n *\n * @module @huaweidevtools/aishell-dsh-credentials-store/accounts\n */\n\nimport { createHash } from 'node:crypto'\nimport type { CredentialsStore } from './index.ts'\nimport type { SignCredentials } from './signer.ts'\n\n/**\n * The one store entry holding every credential this repo manages.\n *\n * Part of the persistence contract: renaming it orphans every stored entry\n * while looking like a harmless constant change.\n */\nexport const CREDENTIAL_ACCOUNT = 'aishell-credential'\n\n/**\n * Entries written by the earlier three-key layout, in the order they are read\n * during migration.\n *\n * Kept only so an existing install keeps working: see {@link readCredentials}.\n * New writes never touch these.\n */\nexport const LEGACY_ACCOUNTS = ['HW_ACCESS_KEY', 'HW_SECRET_KEY', 'HW_SECURITY_TOKEN'] as const\n\n/**\n * The legacy entry holding an e2b API key on its own.\n *\n * Folded into the bundle by {@link readCredentials}. Kept as a constant because\n * the migration has to name it.\n */\nexport const LEGACY_E2B_ACCOUNT = 'AISHELL_E2B_API_KEY'\n\n/** The Huawei Cloud pair, as the bundle stores it. Snake-case on disk. */\nexport interface HuaweiCloudSection {\n readonly access_key: string\n readonly secret_key: string\n readonly security_token?: string\n}\n\n/**\n * An e2b API key, as the bundle stores it.\n *\n * `ak_fingerprint` is not part of the key the gateway issued — it is a digest\n * of the AK that minted it, so a reader can tell whether the key still belongs\n * to the credentials in hand. Without it, an AK rotation would leave the old\n * key (and so the old tenant, with all its sandboxes) alive. It lives here\n * rather than only in the reader's memory because it has to survive a restart.\n */\nexport interface E2BSection {\n readonly api_key: string\n /** ISO 8601; `9999-12-31T23:59:59.000Z` when the key does not expire. */\n readonly expired_at: string\n /** {@link fingerprint} of the AK that minted this key, when known. */\n readonly ak_fingerprint?: string\n}\n\n/** The whole bundle. Either section may be absent. */\nexport interface CredentialBundle {\n readonly huaweicloud?: HuaweiCloudSection\n readonly aishell_e2b?: E2BSection\n}\n\n/**\n * Encode a bundle for storage.\n *\n * @param bundle - the bundle to store.\n * @returns base64 of its JSON form.\n */\nexport function encodeBundle(bundle: CredentialBundle): string {\n return Buffer.from(JSON.stringify(bundle), 'utf8').toString('base64')\n}\n\n/**\n * Decode a stored value.\n *\n * Anything unreadable reads as an empty bundle rather than throwing: a corrupt\n * entry should behave like \"nothing stored\" so the person is asked to\n * authenticate again, not so every tool call fails on a parse error.\n *\n * A value that is not base64 is tried as raw JSON first — that is what the\n * legacy e2b entry held, and migration reads it through here.\n *\n * @param raw - the stored string, if any.\n * @returns the decoded bundle; empty when nothing usable is there.\n */\nexport function decodeBundle(raw: string | undefined): CredentialBundle {\n if (raw === undefined || raw === '') return {}\n const text = decodeText(raw)\n if (text === undefined) return {}\n let parsed: unknown\n try {\n parsed = JSON.parse(text)\n } catch {\n return {}\n }\n if (parsed === null || typeof parsed !== 'object') return {}\n const candidate = parsed as { huaweicloud?: unknown, aishell_e2b?: unknown }\n return {\n ...(isHuaweiCloudSection(candidate.huaweicloud)\n ? { huaweicloud: candidate.huaweicloud }\n : {}),\n ...(isE2BSection(candidate.aishell_e2b)\n ? { aishell_e2b: candidate.aishell_e2b }\n : {}),\n }\n}\n\n/** Decode base64, falling back to the raw string when it is already text. */\nfunction decodeText(raw: string): string | undefined {\n try {\n const decoded = Buffer.from(raw, 'base64')\n // `Buffer.from` never throws on bad base64 — it drops the unusable tail. So\n // the round-trip check is what distinguishes real base64 from legacy JSON.\n if (decoded.toString('base64') === raw.replace(/\\s/g, '')) {\n const text = decoded.toString('utf8')\n if (text.trimStart().startsWith('{')) return text\n }\n } catch {\n // Fall through to raw.\n }\n return raw.trimStart().startsWith('{') ? raw : undefined\n}\n\n/** Whether a decoded field is a usable Huawei Cloud section. */\nfunction isHuaweiCloudSection(value: unknown): value is HuaweiCloudSection {\n if (value === null || typeof value !== 'object') return false\n const section = value as Record<string, unknown>\n return typeof section.access_key === 'string' && section.access_key !== ''\n && typeof section.secret_key === 'string' && section.secret_key !== ''\n && (section.security_token === undefined || typeof section.security_token === 'string')\n}\n\n/** Whether a decoded field is a usable e2b section. */\nfunction isE2BSection(value: unknown): value is E2BSection {\n if (value === null || typeof value !== 'object') return false\n const section = value as Record<string, unknown>\n return typeof section.api_key === 'string' && section.api_key !== ''\n && typeof section.expired_at === 'string' && section.expired_at !== ''\n && (section.ak_fingerprint === undefined || typeof section.ak_fingerprint === 'string')\n}\n\n/**\n * Read the whole bundle, migrating a legacy layout on the way.\n *\n * Migration is lazy and idempotent: the first read that finds no bundle but\n * finds legacy entries merges them into one record, writes it, and removes the\n * old entries. A failed write leaves the legacy entries alone, so the next read\n * tries again — the person is never left with neither layout.\n *\n * @param store - the caller's credential store.\n * @returns the bundle; empty when nothing is stored.\n */\nexport function readCredentials(store: CredentialsStore): CredentialBundle {\n const stored = store.get(CREDENTIAL_ACCOUNT)\n if (stored !== undefined) return decodeBundle(stored)\n\n const migrated = migrateLegacy(store)\n if (migrated === undefined) return {}\n return migrated\n}\n\n/**\n * Merge the legacy entries into one record and remove the old ones.\n *\n * @param store - the caller's credential store.\n * @returns the migrated bundle, or `undefined` when there was nothing to move.\n */\nfunction migrateLegacy(store: CredentialsStore): CredentialBundle | undefined {\n const ak = store.get(LEGACY_ACCOUNTS[0])\n const sk = store.get(LEGACY_ACCOUNTS[1])\n const token = store.get(LEGACY_ACCOUNTS[2])\n const legacyKey = store.get(LEGACY_E2B_ACCOUNT)\n\n const huaweicloud: HuaweiCloudSection | undefined =\n ak !== undefined && ak !== '' && sk !== undefined && sk !== ''\n ? {\n access_key: ak,\n secret_key: sk,\n ...(token === undefined || token === '' ? {} : { security_token: token }),\n }\n : undefined\n\n // The legacy e2b entry held `{ key, expiresAt, akFingerprint }`. Re-keyed to\n // the bundle's snake-case spelling; an older bare key still reads as one that\n // never expires, which is how the reader treated it.\n const aisleE2B = legacyE2BSection(legacyKey)\n\n if (huaweicloud === undefined && aisleE2B === undefined) return undefined\n\n const bundle: CredentialBundle = {\n ...(huaweicloud === undefined ? {} : { huaweicloud }),\n ...(aisleE2B === undefined ? {} : { aishell_e2b: aisleE2B }),\n }\n\n try {\n store.set(CREDENTIAL_ACCOUNT, encodeBundle(bundle))\n } catch {\n // Could not write the merged record — leave the legacy entries in place so\n // the next read retries rather than losing the credentials.\n return bundle\n }\n for (const account of LEGACY_ACCOUNTS) store.delete(account)\n store.delete(LEGACY_E2B_ACCOUNT)\n return bundle\n}\n\n/** Read one legacy e2b entry into its bundle section. */\nfunction legacyE2BSection(raw: string | undefined): E2BSection | undefined {\n if (raw === undefined || raw === '') return undefined\n let parsed: unknown\n try {\n parsed = JSON.parse(raw)\n } catch {\n // Not JSON at all: a bare key. Its expiry is unknown, which reads the same\n // as \"does not expire\"; the gateway's 401 path covers the difference.\n return { api_key: raw, expired_at: FOREVER_EXPIRY }\n }\n const candidate = parsed as { key?: unknown, expiresAt?: unknown, akFingerprint?: unknown } | null\n if (candidate === null || typeof candidate !== 'object') return undefined\n if (typeof candidate.key !== 'string' || candidate.key === '') return undefined\n return {\n api_key: candidate.key,\n expired_at: typeof candidate.expiresAt === 'string' ? candidate.expiresAt : FOREVER_EXPIRY,\n ...(typeof candidate.akFingerprint === 'string' && candidate.akFingerprint !== ''\n ? { ak_fingerprint: candidate.akFingerprint }\n : {}),\n }\n}\n\n/** The expiry recorded for a key that does not expire. */\nexport const FOREVER_EXPIRY = '9999-12-31T23:59:59.000Z'\n\n/**\n * Read the Huawei Cloud pair.\n *\n * AK and SK are read together and treated as one unit: half a pair reads as no\n * credentials rather than an error, so a partially-configured machine gets\n * \"authenticate first\" instead of a signing failure with no remedy. The\n * SecurityToken is optional and is omitted entirely when absent — an empty\n * string would sign as a third header the gateway then rejects.\n *\n * @param store - the caller's credential store.\n * @returns the pair, or `undefined` when the AK/SK pair is incomplete.\n */\nexport function readStoredPair(store: CredentialsStore): SignCredentials | undefined {\n const section = readCredentials(store).huaweicloud\n if (section === undefined) return undefined\n return {\n ak: section.access_key,\n sk: section.secret_key,\n ...(section.security_token === undefined ? {} : { securityToken: section.security_token }),\n }\n}\n\n/**\n * Whether a complete Huawei Cloud pair is stored.\n *\n * @param store - the caller's credential store.\n */\nexport function hasStoredPair(store: CredentialsStore): boolean {\n return readStoredPair(store) !== undefined\n}\n\n/**\n * Read the stored e2b key.\n *\n * @param store - the caller's credential store.\n * @returns the section, or `undefined` when no key is stored.\n */\nexport function readE2BKey(store: CredentialsStore): E2BSection | undefined {\n return readCredentials(store).aishell_e2b\n}\n\n/**\n * Apply one change to the bundle and write it back.\n *\n * The updater receives the current bundle and returns the next one. Returning\n * `undefined` aborts the write, which is how a caller says \"nothing changed\" —\n * that matters because every write is a full re-encode, and needlessly\n * rewriting a record while another process reads it is how a spurious\n * \"credentials changed\" is produced.\n *\n * @param store - the caller's credential store.\n * @param update - computes the next bundle.\n * @returns the bundle now stored.\n */\nexport function updateCredentials(\n store: CredentialsStore,\n update: (current: CredentialBundle) => CredentialBundle | undefined,\n): CredentialBundle {\n const current = readCredentials(store)\n const next = update(current)\n if (next === undefined) return current\n store.set(CREDENTIAL_ACCOUNT, encodeBundle(next))\n return next\n}\n\n/**\n * Store the Huawei Cloud pair, replacing any previous one.\n *\n * One write, so the pair can never be left half-replaced. A save without a\n * token removes a previously stored one: a token belongs to the credential set\n * that carried it, and inheriting a stale one would sign the next request with\n * someone else's session.\n *\n * @param store - the caller's credential store.\n * @param credentials - the verified pair.\n */\nexport function writeHuaweiCloud(store: CredentialsStore, credentials: SignCredentials): void {\n updateCredentials(store, current => ({\n ...current,\n huaweicloud: {\n access_key: credentials.ak,\n secret_key: credentials.sk,\n ...(credentials.securityToken === undefined ? {} : { security_token: credentials.securityToken }),\n },\n }))\n}\n\n/**\n * Store an e2b key.\n *\n * @param store - the caller's credential store.\n * @param key - the section to store.\n */\nexport function writeE2BKey(store: CredentialsStore, key: E2BSection): void {\n updateCredentials(store, current => ({ ...current, aishell_e2b: key }))\n}\n\n/**\n * Remove the stored e2b key, leaving the Huawei Cloud pair alone.\n *\n * @param store - the caller's credential store.\n */\nexport function clearE2BKey(store: CredentialsStore): void {\n updateCredentials(store, current => {\n if (current.aishell_e2b === undefined) return undefined\n const { aishell_e2b: _removed, ...rest } = current\n return rest\n })\n}\n\n/**\n * Remove every credential this repo writes.\n *\n * The whole record goes, so a clear cannot leave a token or a key behind for\n * the next pair to inherit. Deleting an absent entry is a no-op in every\n * backend, and the legacy entries are removed too so a half-migrated store does\n * not come back.\n *\n * @param store - the caller's credential store.\n */\nexport function clearCredentials(store: CredentialsStore): void {\n store.delete(CREDENTIAL_ACCOUNT)\n for (const account of LEGACY_ACCOUNTS) store.delete(account)\n store.delete(LEGACY_E2B_ACCOUNT)\n}\n\n/**\n * A short, stable digest of an AK, for recording which identity minted a key.\n *\n * The AK rather than the pair: the SK can be rotated independently while the\n * account stays the same, and the tenant the gateway derives is the account. A\n * digest rather than the AK itself keeps the bundle from carrying a second\n * readable copy of a credential identifier.\n *\n * @param ak - the access key.\n */\nexport function fingerprint(ak: string): string {\n return createHash('sha256').update(ak, 'utf8').digest('hex')\n}"],"mappings":"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"}
1
+ {"version":3,"file":"accounts.js","names":["bundle: CredentialBundle","raw: string | undefined","parsed: unknown","raw: string","value: unknown","store: CredentialsStore","huaweicloud: HuaweiCloudSection | undefined","account: string","update: (current: CredentialBundle) => CredentialBundle | undefined","credentials: SignCredentials","key: E2BSection","ak: string"],"sources":["../src/accounts.ts"],"sourcesContent":["/**\n * The Huawei Cloud credential vocabulary every plugin in this set shares.\n *\n * ## Why one module owns the account names\n *\n * The account names and the \"AK and SK are one unit\" rule are a *persistence*\n * contract. A second declaration of either does not fail a build: it surfaces\n * as one plugin working and another reporting \"not authenticated\" on a machine\n * where exactly one pair is stored, and the diff that introduced it looks\n * harmless. Keeping them in one file makes the contract have one owner.\n *\n * ## One record, holding the whole bundle\n *\n * The store holds **one** record, `aishell-credential`, whose value is a single\n * JSON bundle encoded as base64:\n *\n * ```json\n * {\n * \"huaweicloud\": { \"access_key\": \"…\", \"secret_key\": \"…\", \"security_token\": \"…\" },\n * \"aishell_e2b\": { \"api_key\": \"…\", \"expired_at\": \"…\", \"ak_fingerprint\": \"…\" }\n * }\n * ```\n *\n * One record is what makes a save one operation and a read one consistent\n * snapshot:\n *\n * - **A save is one write.** Splitting the pair across entries makes every\n * save a sequence, and every backend is per-entry, so a failure between two\n * of them leaves a half-set behind that nothing rolls back.\n * - **A read is one read.** Reading the AK and the SK separately lets a\n * concurrent write land between them: the caller gets a new AK with an old\n * SK, which passes \"both present\" and then fails at the gateway with 401,\n * pointing nowhere near the cause.\n *\n * ## base64 is an encoding, not encryption\n *\n * It keeps the secret from sitting in a plaintext field a person or a log\n * prints while poking at the store. It is **not** a security boundary: anyone\n * who can read the entry can decode it. The actual protection is the backend —\n * the OS keyring, or AES-256-GCM in the file backend.\n *\n * ## Read-modify-write is not atomic across processes\n *\n * {@link updateCredentials} reads the bundle, applies a change and writes it\n * back. Two processes doing that concurrently can lose one of the changes. That\n * is accepted rather than solved: the only writer is a person via the\n * credential form, and both the e2b key and the AK/SK are refreshed by that\n * same person, so a lost update is corrected by the next save. Solving it\n * properly needs compare-and-swap, which the keyring API does not offer.\n *\n * ## One accepted consequence: a partial bundle can overwrite an unreadable one\n *\n * A read that cannot decrypt the file reports \"nothing stored\", so an\n * {@link updateCredentials} built on it computes its next bundle from an empty\n * one and writes that. For `clearE2BKey` that means the written record carries\n * only whatever the updater added, and the section it could not read is no\n * longer in the live file.\n *\n * That is not repaired here, because there is nothing to repair it *from* — the\n * bytes are unreadable. What limits the damage is the file backend's write path:\n * an unreadable file is moved to `<name>.unreadable-<timestamp>` rather than\n * overwritten, so the old record survives on disk and the next successful save\n * starts from an empty map. A reader who cannot decrypt the file has already\n * lost those credentials; the guarantee is only that the file they were in is\n * not destroyed.\n *\n * @module @huaweidevtools/aishell-dsh-credentials-store/accounts\n */\n\nimport { createHash } from 'node:crypto'\nimport type { CredentialsStore } from './index.ts'\nimport type { SignCredentials } from './signer.ts'\n\n/**\n * The one store entry holding every credential this repo manages.\n *\n * Part of the persistence contract: renaming it orphans every stored entry\n * while looking like a harmless constant change.\n */\nexport const CREDENTIAL_ACCOUNT = 'aishell-credential'\n\n/**\n * The per-field account names a store may still hold beside the bundle.\n *\n * These are the read-side names of a store written field-by-field instead of\n * as one record. {@link readCredentials} folds them into\n * {@link CREDENTIAL_ACCOUNT} on first read, so a machine that holds them keeps\n * working; nothing writes them.\n *\n * The order is the order the migration reads them: access key, secret key,\n * token.\n */\nexport const LEGACY_ACCOUNTS = ['HW_ACCESS_KEY', 'HW_SECRET_KEY', 'HW_SECURITY_TOKEN'] as const\n\n/**\n * The standalone e2b key entry, beside the per-field names above.\n *\n * Folded into the bundle by {@link readCredentials}, for the same reason: a\n * store written before the bundle existed keeps working. Nothing writes it.\n */\nexport const LEGACY_E2B_ACCOUNT = 'AISHELL_E2B_API_KEY'\n\n/** The Huawei Cloud pair, as the bundle stores it. Snake-case on disk. */\nexport interface HuaweiCloudSection {\n readonly access_key: string\n readonly secret_key: string\n readonly security_token?: string\n}\n\n/**\n * An e2b API key, as the bundle stores it.\n *\n * `ak_fingerprint` is not part of the key the gateway issued — it is a digest\n * of the AK that minted it, so a reader can tell whether the key still belongs\n * to the credentials in hand. Without it, an AK rotation would leave the old\n * key (and so the old tenant, with all its sandboxes) alive. It lives here\n * rather than only in the reader's memory because it has to survive a restart.\n */\nexport interface E2BSection {\n readonly api_key: string\n /** ISO 8601; `9999-12-31T23:59:59.000Z` when the key does not expire. */\n readonly expired_at: string\n /** {@link fingerprint} of the AK that minted this key, when known. */\n readonly ak_fingerprint?: string\n}\n\n/** The whole bundle. Either section may be absent. */\nexport interface CredentialBundle {\n readonly huaweicloud?: HuaweiCloudSection\n readonly aishell_e2b?: E2BSection\n}\n\n/**\n * Encode a bundle for storage.\n *\n * @param bundle - the bundle to store.\n * @returns base64 of its JSON form.\n */\nexport function encodeBundle(bundle: CredentialBundle): string {\n return Buffer.from(JSON.stringify(bundle), 'utf8').toString('base64')\n}\n\n/**\n * Decode a stored value.\n *\n * Anything unreadable reads as an empty bundle rather than throwing: a corrupt\n * entry should behave like \"nothing stored\" so the person is asked to\n * authenticate again, not so every tool call fails on a parse error.\n *\n * A value that is not base64 is tried as raw JSON first — that is what the\n * legacy e2b entry held, and migration reads it through here.\n *\n * @param raw - the stored string, if any.\n * @returns the decoded bundle; empty when nothing usable is there.\n */\nexport function decodeBundle(raw: string | undefined): CredentialBundle {\n if (raw === undefined || raw === '') return {}\n const text = decodeText(raw)\n if (text === undefined) return {}\n let parsed: unknown\n try {\n parsed = JSON.parse(text)\n } catch {\n return {}\n }\n if (parsed === null || typeof parsed !== 'object') return {}\n const candidate = parsed as { huaweicloud?: unknown, aishell_e2b?: unknown }\n return {\n ...(isHuaweiCloudSection(candidate.huaweicloud)\n ? { huaweicloud: candidate.huaweicloud }\n : {}),\n ...(isE2BSection(candidate.aishell_e2b)\n ? { aishell_e2b: candidate.aishell_e2b }\n : {}),\n }\n}\n\n/** Decode base64, falling back to the raw string when it is already text. */\nfunction decodeText(raw: string): string | undefined {\n try {\n const decoded = Buffer.from(raw, 'base64')\n // `Buffer.from` never throws on bad base64 — it drops the unusable tail. So\n // the round-trip check is what distinguishes real base64 from legacy JSON.\n if (decoded.toString('base64') === raw.replace(/\\s/g, '')) {\n const text = decoded.toString('utf8')\n if (text.trimStart().startsWith('{')) return text\n }\n } catch {\n // Fall through to raw.\n }\n return raw.trimStart().startsWith('{') ? raw : undefined\n}\n\n/** Whether a decoded field is a usable Huawei Cloud section. */\nfunction isHuaweiCloudSection(value: unknown): value is HuaweiCloudSection {\n if (value === null || typeof value !== 'object') return false\n const section = value as Record<string, unknown>\n return typeof section.access_key === 'string' && section.access_key !== ''\n && typeof section.secret_key === 'string' && section.secret_key !== ''\n && (section.security_token === undefined || typeof section.security_token === 'string')\n}\n\n/** Whether a decoded field is a usable e2b section. */\nfunction isE2BSection(value: unknown): value is E2BSection {\n if (value === null || typeof value !== 'object') return false\n const section = value as Record<string, unknown>\n return typeof section.api_key === 'string' && section.api_key !== ''\n && typeof section.expired_at === 'string' && section.expired_at !== ''\n && (section.ak_fingerprint === undefined || typeof section.ak_fingerprint === 'string')\n}\n\n/**\n * Read the whole bundle, folding a per-field store into it on the way.\n *\n * The fold is lazy and idempotent: a read that finds no bundle but finds the\n * per-field names merges them into one record, writes it, and removes the old\n * entries. A failed write leaves them in place so the next read tries again —\n * a store is never left with neither layout.\n *\n * @param store - the caller's credential store.\n * @returns the bundle; empty when nothing is stored.\n */\nexport function readCredentials(store: CredentialsStore): CredentialBundle {\n const stored = store.get(CREDENTIAL_ACCOUNT)\n if (stored !== undefined) return decodeBundle(stored)\n\n const migrated = migrateLegacy(store)\n if (migrated === undefined) return {}\n return migrated\n}\n\n/**\n * Merge the per-field entries into one record and remove the old ones.\n *\n * @param store - the caller's credential store.\n * @returns the merged bundle, or `undefined` when there was nothing to move.\n */\nfunction migrateLegacy(store: CredentialsStore): CredentialBundle | undefined {\n const ak = store.get(LEGACY_ACCOUNTS[0])\n const sk = store.get(LEGACY_ACCOUNTS[1])\n const token = store.get(LEGACY_ACCOUNTS[2])\n const legacyKey = store.get(LEGACY_E2B_ACCOUNT)\n\n const huaweicloud: HuaweiCloudSection | undefined =\n ak !== undefined && ak !== '' && sk !== undefined && sk !== ''\n ? {\n access_key: ak,\n secret_key: sk,\n ...(token === undefined || token === '' ? {} : { security_token: token }),\n }\n : undefined\n\n // The standalone entry carries `{ key, expiresAt, akFingerprint }`; it is\n // re-keyed to the bundle's snake-case spelling here. A bare key with no JSON\n // around it reads as one that never expires, which is the reading the rest of\n // the store gives an unknown expiry.\n const aisleE2B = legacyE2BSection(legacyKey)\n\n if (huaweicloud === undefined && aisleE2B === undefined) return undefined\n\n const bundle: CredentialBundle = {\n ...(huaweicloud === undefined ? {} : { huaweicloud }),\n ...(aisleE2B === undefined ? {} : { aishell_e2b: aisleE2B }),\n }\n\n try {\n store.set(CREDENTIAL_ACCOUNT, encodeBundle(bundle))\n } catch {\n // Could not write the merged record — leave the per-field entries in place\n // so the next read retries rather than losing the credentials.\n return bundle\n }\n // The merged record is durable, so the old entries are redundant now. Each\n // delete is best-effort: a locked keyring rejects deletes as well as writes,\n // and letting that propagate would make a **successful** migration look like\n // a failed read to the caller. A leftover entry costs nothing — a read\n // returns the bundle as soon as it exists, and `clearCredentials` removes\n // these names too — whereas an exception here surfaces as \"could not read\n // credentials\" on a store that is fine.\n for (const account of LEGACY_ACCOUNTS) { tryDelete(store, account) }\n tryDelete(store, LEGACY_E2B_ACCOUNT)\n return bundle\n}\n\n/** Read one standalone e2b entry into its bundle section. */\nfunction legacyE2BSection(raw: string | undefined): E2BSection | undefined {\n if (raw === undefined || raw === '') return undefined\n let parsed: unknown\n try {\n parsed = JSON.parse(raw)\n } catch {\n // Not JSON at all: a bare key. Its expiry is unknown, which reads the same\n // as \"does not expire\"; the gateway's 401 path covers the difference.\n return { api_key: raw, expired_at: FOREVER_EXPIRY }\n }\n const candidate = parsed as { key?: unknown, expiresAt?: unknown, akFingerprint?: unknown } | null\n if (candidate === null || typeof candidate !== 'object') return undefined\n if (typeof candidate.key !== 'string' || candidate.key === '') return undefined\n return {\n api_key: candidate.key,\n expired_at: typeof candidate.expiresAt === 'string' ? candidate.expiresAt : FOREVER_EXPIRY,\n ...(typeof candidate.akFingerprint === 'string' && candidate.akFingerprint !== ''\n ? { ak_fingerprint: candidate.akFingerprint }\n : {}),\n }\n}\n\n/** The expiry recorded for a key that does not expire. */\nexport const FOREVER_EXPIRY = '9999-12-31T23:59:59.000Z'\n\n/**\n * Delete an entry, swallowing a backend refusal.\n *\n * For every caller whose success does not depend on the delete landing — the\n * migration's cleanup above, and `clearCredentials` below it. The store is\n * allowed to refuse (a locked keyring rejects deletes, a read-only backend has\n * nothing to delete from), and in each of those cases the entry is either\n * already gone or will be superseded; throwing would report a failure for work\n * that is not needed.\n *\n * Not used where the delete *is* the operation — a save route clearing before\n * it writes must know the clear happened, and that path calls `store.delete`\n * directly.\n *\n * @param store - the caller's credential store.\n * @param account - the entry to remove.\n */\nfunction tryDelete(store: CredentialsStore, account: string): void {\n try {\n store.delete(account)\n } catch {\n // Best-effort: see the note above.\n }\n}\n\n/**\n * Read the Huawei Cloud pair.\n *\n * AK and SK are read together and treated as one unit: half a pair reads as no\n * credentials rather than an error, so a partially-configured machine gets\n * \"authenticate first\" instead of a signing failure with no remedy. The\n * SecurityToken is optional and is omitted entirely when absent — an empty\n * string would sign as a third header the gateway then rejects.\n *\n * @param store - the caller's credential store.\n * @returns the pair, or `undefined` when the AK/SK pair is incomplete.\n */\nexport function readStoredPair(store: CredentialsStore): SignCredentials | undefined {\n const section = readCredentials(store).huaweicloud\n if (section === undefined) return undefined\n return {\n ak: section.access_key,\n sk: section.secret_key,\n ...(section.security_token === undefined ? {} : { securityToken: section.security_token }),\n }\n}\n\n/**\n * Whether a complete Huawei Cloud pair is stored.\n *\n * @param store - the caller's credential store.\n */\nexport function hasStoredPair(store: CredentialsStore): boolean {\n return readStoredPair(store) !== undefined\n}\n\n/**\n * Read the stored e2b key.\n *\n * @param store - the caller's credential store.\n * @returns the section, or `undefined` when no key is stored.\n */\nexport function readE2BKey(store: CredentialsStore): E2BSection | undefined {\n return readCredentials(store).aishell_e2b\n}\n\n/**\n * Apply one change to the bundle and write it back.\n *\n * The updater receives the current bundle and returns the next one. Returning\n * `undefined` aborts the write, which is how a caller says \"nothing changed\" —\n * that matters because every write is a full re-encode, and needlessly\n * rewriting a record while another process reads it is how a spurious\n * \"credentials changed\" is produced.\n *\n * @param store - the caller's credential store.\n * @param update - computes the next bundle.\n * @returns the bundle now stored.\n */\nexport function updateCredentials(\n store: CredentialsStore,\n update: (current: CredentialBundle) => CredentialBundle | undefined,\n): CredentialBundle {\n const current = readCredentials(store)\n const next = update(current)\n if (next === undefined) return current\n store.set(CREDENTIAL_ACCOUNT, encodeBundle(next))\n return next\n}\n\n/**\n * Store the Huawei Cloud pair, replacing whatever is there.\n *\n * One write, so the pair can never be left half-replaced. A save without a\n * token removes a stored one: a token belongs to the credential set that\n * carried it, and inheriting a stale one would sign the next request with\n * someone else's session.\n *\n * @param store - the caller's credential store.\n * @param credentials - the verified pair.\n */\nexport function writeHuaweiCloud(store: CredentialsStore, credentials: SignCredentials): void {\n updateCredentials(store, current => ({\n ...current,\n huaweicloud: {\n access_key: credentials.ak,\n secret_key: credentials.sk,\n ...(credentials.securityToken === undefined ? {} : { security_token: credentials.securityToken }),\n },\n }))\n}\n\n/**\n * Store an e2b key.\n *\n * @param store - the caller's credential store.\n * @param key - the section to store.\n */\nexport function writeE2BKey(store: CredentialsStore, key: E2BSection): void {\n updateCredentials(store, current => ({ ...current, aishell_e2b: key }))\n}\n\n/**\n * Remove the stored e2b key, leaving the Huawei Cloud pair alone.\n *\n * @param store - the caller's credential store.\n */\nexport function clearE2BKey(store: CredentialsStore): void {\n updateCredentials(store, current => {\n if (current.aishell_e2b === undefined) return undefined\n const { aishell_e2b: _removed, ...rest } = current\n return rest\n })\n}\n\n/**\n * Remove every credential this repo writes.\n *\n * The whole record goes, so a clear cannot leave a token or a key behind for\n * the next pair to inherit. Deleting an absent entry is a no-op in every\n * backend.\n *\n * The per-field names are removed as well, so a store the fold never reached\n * does not keep answering after a clear. Those deletes are best-effort and the\n * main record's is not: a refusal on `CREDENTIAL_ACCOUNT` means the credentials\n * are still there and the person has to be told, while the per-field names are\n * leftovers whose removal nothing depends on. Swallowing the first would report\n * a clear that did not happen.\n *\n * @param store - the caller's credential store.\n * @throws when the backend refused to remove the main record.\n */\nexport function clearCredentials(store: CredentialsStore): void {\n store.delete(CREDENTIAL_ACCOUNT)\n for (const account of LEGACY_ACCOUNTS) tryDelete(store, account)\n tryDelete(store, LEGACY_E2B_ACCOUNT)\n}\n\n/**\n * A short, stable digest of an AK, for recording which identity minted a key.\n *\n * The AK rather than the pair: the SK can be rotated independently while the\n * account stays the same, and the tenant the gateway derives is the account. A\n * digest rather than the AK itself keeps the bundle from carrying a second\n * readable copy of a credential identifier.\n *\n * @param ak - the access key.\n */\nexport function fingerprint(ak: string): string {\n return createHash('sha256').update(ak, 'utf8').digest('hex')\n}"],"mappings":"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"}
@@ -24,8 +24,8 @@ declare function __setKeyringModule(mock: KeyringModule | undefined): void;
24
24
  * Construction probes the keyring once — if the native module cannot be loaded
25
25
  * (no prebuilt binding for this platform), or the Linux Secret Service is
26
26
  * unavailable, it throws and {@link createCredentialsStore} falls back to the
27
- * next backend. Without the pin the absent-service case does not throw; see
28
- * the module note.
27
+ * next backend. The pin is what makes the second case throw; see the module
28
+ * note.
29
29
  */
30
30
  declare class KeyringBackend implements CredentialsStore {
31
31
  private readonly serviceName;
@@ -38,6 +38,21 @@ declare class KeyringBackend implements CredentialsStore {
38
38
  has(account: string): boolean;
39
39
  } //#endregion
40
40
  //#region src/backends/fingerprint.d.ts
41
+ /**
42
+ * A credentials file that exists but cannot be decrypted or parsed.
43
+ *
44
+ * Its own type rather than a plain `Error` because the write path has to tell
45
+ * this case from every other failure: this is the one where overwriting the
46
+ * file would destroy credentials the person still has, so `set` moves it aside
47
+ * first. Anything else propagates unchanged.
48
+ */
49
+ declare class UnreadableCredentialsFile extends Error {
50
+ /** The file that could not be read. */
51
+ readonly filePath: string;
52
+ constructor(/** The file that could not be read. */
53
+ filePath: string, /** What decryption or parsing reported. */
54
+ cause: unknown);
55
+ }
41
56
  /**
42
57
  * Encrypted-file {@link CredentialsStore}. Each account is one entry in a
43
58
  * single JSON map, encrypted as one file under the resolved credentials
@@ -56,18 +71,78 @@ declare class FingerprintBackend implements CredentialsStore {
56
71
  * read, used, and dropped so plaintext does not linger in memory between
57
72
  * operations. The file I/O + scrypt decrypt per call is acceptable: this is
58
73
  * not a hot path (once per chat request, not per token).
74
+ *
75
+ * @throws {@link UnreadableCredentialsFile} when the file exists but cannot be
76
+ * decrypted or parsed. Callers decide what that means; see the note on
77
+ * {@link FingerprintBackend.loadForWrite}.
59
78
  */
60
79
  private load;
80
+ /**
81
+ * Load for a write, where an unreadable file must not be silently replaced.
82
+ *
83
+ * ## Why this is not just `load()` with the error swallowed
84
+ *
85
+ * `set` re-encrypts and rewrites the *whole* map. If load failed and returned
86
+ * empty, the next save would write a one-entry file over every other
87
+ * credential the person had stored — with no error, no backup and nothing to
88
+ * recover from. An unreadable file is not proof of an empty one: a truncated
89
+ * write, a machine-id that changed under the file, or a build whose salt
90
+ * moved all look identical from here.
91
+ *
92
+ * So the unreadable file is moved aside instead of overwritten. Saving
93
+ * therefore keeps working on a machine that inherited someone else's file —
94
+ * the documented VM-clone case — while whatever was in it stays on disk under
95
+ * a name that says what happened to it.
96
+ *
97
+ * @returns the current entries, or an empty map when the old file was moved
98
+ * aside.
99
+ */
100
+ private loadForWrite;
101
+ /**
102
+ * Move an unreadable file out of the way.
103
+ *
104
+ * The suffix carries a timestamp and, if that name is taken, a counter — the
105
+ * quarantine must never itself throw over a name collision and must never
106
+ * overwrite a previous one, since the files it preserves are the only copy of
107
+ * what was there. The counter is bounded because an unbounded retry loop over
108
+ * a filesystem is a hang, not a fallback; reaching the bound means something
109
+ * outside this process is wrong and saying so is better than spinning.
110
+ */
111
+ private quarantine;
61
112
  /** Encrypt + write the map to disk with owner-only permissions. */
62
113
  private persist;
114
+ /**
115
+ * Read one entry.
116
+ *
117
+ * An unreadable file reads as absent rather than throwing. The alternative —
118
+ * failing every tool call on a parse error — leaves the person with no
119
+ * working path; "not authenticated" at least routes them to the form that can
120
+ * fix it. The write path is where the difference is preserved; see
121
+ * {@link FingerprintBackend.loadForWrite}.
122
+ */
63
123
  get(account: string): string | undefined;
64
124
  set(account: string, value: string): void;
125
+ /**
126
+ * Remove one entry.
127
+ *
128
+ * An unreadable file **throws** rather than being a silent no-op. There is
129
+ * nothing to remove from a map that cannot be read, and answering "done"
130
+ * would be a clear that did not clear: the file stays on disk, and the
131
+ * moment whatever made it unreadable is undone — a restored machine id, a
132
+ * repaired file — the credential reads back. `clearCredentials` relies on
133
+ * exactly this signal to tell the person the clear failed.
134
+ *
135
+ * Quarantining instead is not an option here: it would move the file out of
136
+ * the way, which is the destruction this method must not perform — a delete
137
+ * asked for one entry, not for every entry.
138
+ *
139
+ * @throws {@link UnreadableCredentialsFile} when the file cannot be read.
140
+ */
65
141
  delete(account: string): void;
66
142
  has(account: string): boolean;
67
- }
68
-
69
- //#endregion
143
+ } //#endregion
70
144
  //#region src/backends/memory.d.ts
145
+
71
146
  //# sourceMappingURL=fingerprint.d.ts.map
72
147
  /** Process-scoped {@link CredentialsStore}; entries vanish on exit. */
73
148
  declare class MemoryBackend implements CredentialsStore {
@@ -108,9 +183,9 @@ interface FailoverStoreOptions {
108
183
  *
109
184
  * Stickiness is per instance, not per process: each plugin that consumes
110
185
  * credentials builds its own store, so one instance failing over does not stop
111
- * another from trying the primary. That is intended — the flag only exists to
186
+ * another from trying the primary. That is intentional — the flag exists to
112
187
  * avoid re-hitting a primary already known to be unwritable from *this* store,
113
- * and a second instance's attempt is one probe rather than a policy decision.
188
+ * so a second instance's attempt is one probe rather than a policy decision.
114
189
  * Correctness across instances comes from the persisted record, not the flag.
115
190
  *
116
191
  * Constructed by {@link createCredentialsStore} around a KeyringBackend whose
@@ -156,9 +231,9 @@ declare class FailoverStore implements CredentialsStore {
156
231
  * call {@link createCredentialsStore} and get a {@link CredentialsStore} —
157
232
  * they never learn which backend is serving. The factory probes backends in
158
233
  * priority order (OS keyring first, process memory last) and returns the
159
- * first that answers. New backends (fingerprint + encrypted file, etc.) slot
160
- * into the chain without touching callers. A keyring that answers the probe
161
- * but later rejects writes fails over to the next backend on first failure.
234
+ * first that answers, so a backend can be added to the chain without any
235
+ * caller changing. A keyring that answers the probe but later rejects writes
236
+ * fails over to the next backend on first failure.
162
237
  *
163
238
  * This is a tool library, not a DSH plugin — no `dsh.client`, no
164
239
  * `cordis.patch.yml`, no `apply`. DSH's own `ctx.credentials` is untouched;
@@ -215,5 +290,5 @@ interface CredentialsStore {
215
290
  declare function createCredentialsStore(config?: CredentialsStoreConfig): CredentialsStore;
216
291
 
217
292
  //#endregion
218
- export { CredentialsStore, CredentialsStoreConfig, FailoverStore, FingerprintBackend, KeyringBackend, MemoryBackend, __resetSharedMemoryBackend, __setKeyringModule, createCredentialsStore, sharedMemoryBackend };
219
- //# sourceMappingURL=index-JlyJE1E8.d.ts.map
293
+ export { CredentialsStore, CredentialsStoreConfig, FailoverStore, FingerprintBackend, KeyringBackend, MemoryBackend, UnreadableCredentialsFile, __resetSharedMemoryBackend, __setKeyringModule, createCredentialsStore, sharedMemoryBackend };
294
+ //# sourceMappingURL=index-Cdsu1kDC.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index-Cdsu1kDC.d.ts","names":[],"sources":["../../src/backends/keyring.ts","../../src/backends/fingerprint.ts","../../src/backends/memory.ts","../../src/failover.ts","../../src/index.ts"],"sourcesContent":null,"mappings":";;KAuCK,YAAA;;;;;;KAMA,mBAAA;ECiEQ,KAAA,CAAA,EAAA;;;;KDhER,aAAA;0DACqD,wBAAwB;;;iBAKlE,kBAAA,OAAyB;;ACkFzC;;;;AAA2D;;;;;ACrH3D;AAA2B,cFuDd,cAAA,YAA0B,gBEvDZ,CAAA;EAAA,iBAGJ,WAAA;EAA2B,iBAHZ,OAAA;EAAgB,iBAAA,KAAA;EAwDtC,WAAA,CAAA,OAAmB,CAAnB,EFIO,sBEJY;EAAA,GAAA,CAAA,OAAA,EAAA,MAAA,CAAA,EAAA,MAAA,GAAA,SAAA;EAAA,GAAS,CAAA,OAAA,EAAA,MAAA,EAAA,KAAA,EAAA,MAAA,CAAA,EAAA,IAAA;EAA2B,MAAG,CAAA,OAAA,EAAA,MAAA,CAAA,EAAA,IAAA;EAAa,GAAA,CAAA,OAAA,EAAA,MAAA,CAAA,EAAA,OAAA;;;;;;;;;;;AFD1E,cCsCA,yBAAA,SAAkC,KAAK,CDtCxB;EAAA;EAAA,SAKL,QAAA,EAAA,MAAA;EAA2B,WALX,CAAgB;;;;;;;;;;;ACsCvD;;cAwBa,kBAAA,YAA8B;;;uBAIrB;;;;;AAJtB;;;;AAA2D;;;;;ACrH3D;;;;AAAsD;AAwDtD;;;;AAAuF;;;;;;;AAWvF;;;;;ACcA;;;;;;;;;;;;;;;;AAyBA;;;;EAOsE,GAPhC,CAAA,OAAA,EAAA,MAAA,CAAA,EAAA,MAAA,GAAA,SAAA;EAAgB,GAAA,CAAA,OAAA,EAAA,MAAA,EAAA,KAAA,EAAA,MAAA,CAAA,EAAA,IAAA;;;;;;;;;;;;;;;;;;;;;;;;cD1GzC,aAAA,YAAyB;;wBAGf;;;;EFoDV,GAAA,CAAA,OAAA,EAAA,MAAe,CAAA,EAAA,OAAA;;AAKL,iBEJP,mBAAA,CFIO,MAAA,CAAA,EEJqB,sBFIrB,CAAA,EEJmD,aFInD;;AALgC;;;;;iBEYvC,0BAAA,CAAA;;;;;;;UCcC,oBAAA;;;;;;;iCAOgB;;;;;;;AAkBjC;;;;;AAAsD;;;;;cAAzC,aAAA,YAAyB;;;;;;uBAOf,2BAA2B;;;;;;;;;;;EC1GjC,GAAA,CAAA,OAAA,EAAA,MAAA,CAAA,EAAA,MAAsB,GAAA,SAAA;;;;;;;;AAiBvC;;;;;;;;;AJT2E;AAO1D;AAMO;;;;AAEsE;;AAK9F;;;;;;;;;UI5BiB,sBAAA;;;AJgDjB;;;;EAAuD,SAAA,cAAA,CAAA,EAAA,MAAA;;;;;;;;;UI/BtC,gBAAA;;;EHqEJ;;;;;;;;;;AAwBb;;;;AAA2D;;;;;ACrH3D;;;;AAAsD;AAwDtD;;AAA4C,iBEF5B,sBAAA,CFE4B,MAAA,CAAA,EEFG,sBFEH,CAAA,EEFiC,gBFEjC"}
package/lib/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- import { CREDENTIALS_FILE_NAME$1 as CREDENTIALS_FILE_NAME, CREDENTIAL_SCOPE$1 as CREDENTIAL_SCOPE, DEFAULT_SCOPE$1 as DEFAULT_SCOPE, KEYRING_SERVICE$1 as KEYRING_SERVICE } from "./scope-CFetnbG3.js";
2
- import { CredentialsStore, CredentialsStoreConfig, FailoverStore, FingerprintBackend, KeyringBackend, MemoryBackend, __resetSharedMemoryBackend, __setKeyringModule, createCredentialsStore, sharedMemoryBackend } from "./index-JlyJE1E8.js";
3
- export { CREDENTIALS_FILE_NAME, CREDENTIAL_SCOPE, CredentialsStore, CredentialsStoreConfig, DEFAULT_SCOPE, FailoverStore, FingerprintBackend, KEYRING_SERVICE, KeyringBackend, MemoryBackend, __resetSharedMemoryBackend, __setKeyringModule, createCredentialsStore, sharedMemoryBackend };
1
+ import { CREDENTIALS_FILE_NAME$1 as CREDENTIALS_FILE_NAME, CREDENTIAL_SCOPE$1 as CREDENTIAL_SCOPE, DEFAULT_SCOPE$1 as DEFAULT_SCOPE, KEYRING_SERVICE$1 as KEYRING_SERVICE } from "./scope-C7w2eTvE.js";
2
+ import { CredentialsStore, CredentialsStoreConfig, FailoverStore, FingerprintBackend, KeyringBackend, MemoryBackend, UnreadableCredentialsFile, __resetSharedMemoryBackend, __setKeyringModule, createCredentialsStore, sharedMemoryBackend } from "./index-Cdsu1kDC.js";
3
+ export { CREDENTIALS_FILE_NAME, CREDENTIAL_SCOPE, CredentialsStore, CredentialsStoreConfig, DEFAULT_SCOPE, FailoverStore, FingerprintBackend, KEYRING_SERVICE, KeyringBackend, MemoryBackend, UnreadableCredentialsFile, __resetSharedMemoryBackend, __setKeyringModule, createCredentialsStore, sharedMemoryBackend };