@huaweidevtools/aishell-dsh-credentials-store 0.0.6 → 0.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +49 -10
  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-JlyJE1E8.d.ts +219 -0
  8. package/lib/index-JlyJE1E8.d.ts.map +1 -0
  9. package/lib/index.d.ts +3 -218
  10. package/lib/index.js +55 -21
  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 +45 -0
  42. package/lib/types/backends/memory.d.ts.map +1 -0
  43. package/lib/types/backends/memory.js +89 -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 +78 -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 +20 -7
  74. package/lib/index.d.ts.map +0 -1
package/LICENSE CHANGED
@@ -186,7 +186,7 @@
186
186
  same "printed page" as the copyright notice for easier
187
187
  identification within third-party archives.
188
188
 
189
- Copyright 2024 Huawei Cloud AI Shell
189
+ Copyright 2026 Huawei Cloud AI Shell
190
190
 
191
191
  Licensed under the Apache License, Version 2.0 (the "License");
192
192
  you may not use this file except in compliance with the License.
package/README.md CHANGED
@@ -1,6 +1,16 @@
1
1
  # @huaweidevtools/aishell-dsh-credentials-store
2
2
 
3
- Credential storage abstraction for aishell DSH plugins. Upper layers call `createCredentialsStore()` and receive a `CredentialsStore` — they never learn which backend is serving. The factory probes backends in priority order and returns the first that answers.
3
+ Credential storage for aishell DSH plugins, plus the vocabulary they share: the
4
+ account names, the credential bundle, and IAM verification.
5
+
6
+ The store keeps **one** record — `aishell-credential` — holding the Huawei Cloud
7
+ pair and the e2b API key as one base64-encoded JSON bundle. One write, so a
8
+ credential set can never be left half-replaced; one read, so a new AK can never
9
+ pair with an old SK.
10
+
11
+ Upper layers call `createCredentialsStore()` and receive a `CredentialsStore` —
12
+ they never learn which backend is serving. The factory probes backends in
13
+ priority order and returns the first that answers.
4
14
 
5
15
  This is a **tool library**, not a DSH plugin — no `dsh.client`, no `cordis.patch.yml`, no `apply`.
6
16
 
@@ -12,7 +22,7 @@ This is a **tool library**, not a DSH plugin — no `dsh.client`, no `cordis.pat
12
22
  | 2 | `FingerprintBackend` | Yes (same-machine, survives reboot) | `node-machine-id` can resolve a machine UUID |
13
23
  | 3 | `MemoryBackend` | No (process-scoped) | None — always available, last resort |
14
24
 
15
- When the OS keyring is unavailable (headless Linux without D-Bus, keychain locked, container), the factory falls back to the fingerprint backend — a machine-fingerprint-bound AES-256-GCM encrypted file at `~/.<service>/credentials.enc`. The encryption key is derived from the machine UUID via scrypt; the ciphertext is useless on a different machine. If the machine UUID is also unavailable, the factory falls back to the in-memory backend so a single DSH session still works.
25
+ When the OS keyring is unavailable (headless Linux without D-Bus, keychain locked, container), the factory falls back to the fingerprint backend — a machine-fingerprint-bound AES-256-GCM encrypted file, `$DSH_HOME/credentials/<scope>.credentials.enc` (`<DSH_HOME>` falls back to `~/.aishell`). The encryption key is derived from the machine UUID via scrypt; the ciphertext is useless on a different machine. If the machine UUID is also unavailable, the factory falls back to the in-memory backend so a single DSH session still works.
16
26
 
17
27
  ### Linux: the store is required, not preferred
18
28
 
@@ -33,24 +43,53 @@ The construction probe can only detect a keyring that cannot be *read*. A GNOME
33
43
 
34
44
  The record lives in the fallback backend — the encrypted file in practice — so it survives the restart the failure mode requires: the keyring is locked *now*, and the read that would resurrect the credential happens in a later session, after it has unlocked. On a machine whose keyring has never failed the fallback has written nothing, and each read pays a sub-millisecond check against an absent file.
35
45
 
46
+ ## Credential scope
47
+
48
+ One build-time constant decides where credentials live, and it decides both
49
+ names at once:
50
+
51
+ | | default |
52
+ |---|---|
53
+ | keyring service | `aishell-dsh-plugins` |
54
+ | encrypted file | `plugins.credentials.enc` |
55
+
56
+ ```sh
57
+ AISHELL_CREDENTIAL_SCOPE=staging pnpm run build
58
+ # → keyring service `aishell-dsh-staging`, file `staging.credentials.enc`
59
+ ```
60
+
61
+ It is **not** a runtime setting, and not a plugin config option. A scope set on
62
+ a reader but not on the writer stores credentials in one namespace and looks for
63
+ them in another: the form says "saved" while every tool says "not
64
+ authenticated". A build-time constant cannot be set on one side only, and two
65
+ deployments that should not see each other's credentials differ by being built
66
+ differently — which is the honest place for a persistence boundary.
67
+
36
68
  ## Usage
37
69
 
38
70
  ```typescript
39
71
  import { createCredentialsStore } from '@huaweidevtools/aishell-dsh-credentials-store'
72
+ import { readStoredPair, writeHuaweiCloud } from '@huaweidevtools/aishell-dsh-credentials-store/accounts'
40
73
 
41
74
  const store = createCredentialsStore() // probes keyring → fingerprint → memory
42
- store.set('HW_ACCESS_KEY', 'LTAIxxxx') // durable if keyring or fingerprint is up
43
- const ak = store.get('HW_ACCESS_KEY') // 'LTAIxxxx'
44
- store.has('HW_ACCESS_KEY') // true
45
- store.delete('HW_ACCESS_KEY')
46
- ```
47
75
 
48
- A custom service name isolates entries from other applications sharing the same keyring:
76
+ // Read the pair: one snapshot of one record.
77
+ const pair = readStoredPair(store) // { ak, sk, securityToken? } | undefined
49
78
 
50
- ```typescript
51
- const store = createCredentialsStore({ service: 'my-app' })
79
+ // Write it: one atomic replacement. A save without a token drops the old one.
80
+ writeHuaweiCloud(store, { ak, sk })
52
81
  ```
53
82
 
83
+ Other entries:
84
+
85
+ | Subpath | What it is |
86
+ |---|---|
87
+ | `.` | The store factory and backend chain |
88
+ | `./accounts` | Account names, the credential bundle, the pair reader/writer |
89
+ | `./verify` | IAM verification (`GET /v3/projects`), one answer for one credential set |
90
+ | `./signer` | SDK-HMAC-SHA256 request signing |
91
+ | `./scope` | The build scope, and the two names derived from it |
92
+
54
93
  ## Security properties
55
94
 
56
95
  - **No plaintext files**: nothing on disk is readable with `cat`.
@@ -0,0 +1,187 @@
1
+ import "./scope-CFetnbG3.js";
2
+ import { CredentialsStore } from "./index-JlyJE1E8.js";
3
+ import { SignCredentials } from "./signer-Et0DIH6o.js";
4
+
5
+ //#region src/accounts.d.ts
6
+ /**
7
+ * The one store entry holding every credential this repo manages.
8
+ *
9
+ * Part of the persistence contract: renaming it orphans every stored entry
10
+ * while looking like a harmless constant change.
11
+ */
12
+
13
+ /**
14
+ * The one store entry holding every credential this repo manages.
15
+ *
16
+ * Part of the persistence contract: renaming it orphans every stored entry
17
+ * while looking like a harmless constant change.
18
+ */
19
+ declare const CREDENTIAL_ACCOUNT = "aishell-credential";
20
+ /**
21
+ * Entries written by the earlier three-key layout, in the order they are read
22
+ * during migration.
23
+ *
24
+ * Kept only so an existing install keeps working: see {@link readCredentials}.
25
+ * New writes never touch these.
26
+ */
27
+ declare const LEGACY_ACCOUNTS: readonly ["HW_ACCESS_KEY", "HW_SECRET_KEY", "HW_SECURITY_TOKEN"];
28
+ /**
29
+ * The legacy entry holding an e2b API key on its own.
30
+ *
31
+ * Folded into the bundle by {@link readCredentials}. Kept as a constant because
32
+ * the migration has to name it.
33
+ */
34
+ declare const LEGACY_E2B_ACCOUNT = "AISHELL_E2B_API_KEY";
35
+ /** The Huawei Cloud pair, as the bundle stores it. Snake-case on disk. */
36
+ interface HuaweiCloudSection {
37
+ readonly access_key: string;
38
+ readonly secret_key: string;
39
+ readonly security_token?: string;
40
+ }
41
+ /**
42
+ * An e2b API key, as the bundle stores it.
43
+ *
44
+ * `ak_fingerprint` is not part of the key the gateway issued — it is a digest
45
+ * of the AK that minted it, so a reader can tell whether the key still belongs
46
+ * to the credentials in hand. Without it, an AK rotation would leave the old
47
+ * key (and so the old tenant, with all its sandboxes) alive. It lives here
48
+ * rather than only in the reader's memory because it has to survive a restart.
49
+ */
50
+ interface E2BSection {
51
+ readonly api_key: string;
52
+ /** ISO 8601; `9999-12-31T23:59:59.000Z` when the key does not expire. */
53
+ readonly expired_at: string;
54
+ /** {@link fingerprint} of the AK that minted this key, when known. */
55
+ readonly ak_fingerprint?: string;
56
+ }
57
+ /** The whole bundle. Either section may be absent. */
58
+ interface CredentialBundle {
59
+ readonly huaweicloud?: HuaweiCloudSection;
60
+ readonly aishell_e2b?: E2BSection;
61
+ }
62
+ /**
63
+ * Encode a bundle for storage.
64
+ *
65
+ * @param bundle - the bundle to store.
66
+ * @returns base64 of its JSON form.
67
+ */
68
+ declare function encodeBundle(bundle: CredentialBundle): string;
69
+ /**
70
+ * Decode a stored value.
71
+ *
72
+ * Anything unreadable reads as an empty bundle rather than throwing: a corrupt
73
+ * entry should behave like "nothing stored" so the person is asked to
74
+ * authenticate again, not so every tool call fails on a parse error.
75
+ *
76
+ * A value that is not base64 is tried as raw JSON first — that is what the
77
+ * legacy e2b entry held, and migration reads it through here.
78
+ *
79
+ * @param raw - the stored string, if any.
80
+ * @returns the decoded bundle; empty when nothing usable is there.
81
+ */
82
+ declare function decodeBundle(raw: string | undefined): CredentialBundle;
83
+ /**
84
+ * Read the whole bundle, migrating a legacy layout on the way.
85
+ *
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
+ *
91
+ * @param store - the caller's credential store.
92
+ * @returns the bundle; empty when nothing is stored.
93
+ */
94
+ declare function readCredentials(store: CredentialsStore): CredentialBundle;
95
+ /** The expiry recorded for a key that does not expire. */
96
+ declare const FOREVER_EXPIRY = "9999-12-31T23:59:59.000Z";
97
+ /**
98
+ * Read the Huawei Cloud pair.
99
+ *
100
+ * AK and SK are read together and treated as one unit: half a pair reads as no
101
+ * credentials rather than an error, so a partially-configured machine gets
102
+ * "authenticate first" instead of a signing failure with no remedy. The
103
+ * SecurityToken is optional and is omitted entirely when absent — an empty
104
+ * string would sign as a third header the gateway then rejects.
105
+ *
106
+ * @param store - the caller's credential store.
107
+ * @returns the pair, or `undefined` when the AK/SK pair is incomplete.
108
+ */
109
+ declare function readStoredPair(store: CredentialsStore): SignCredentials | undefined;
110
+ /**
111
+ * Whether a complete Huawei Cloud pair is stored.
112
+ *
113
+ * @param store - the caller's credential store.
114
+ */
115
+ declare function hasStoredPair(store: CredentialsStore): boolean;
116
+ /**
117
+ * Read the stored e2b key.
118
+ *
119
+ * @param store - the caller's credential store.
120
+ * @returns the section, or `undefined` when no key is stored.
121
+ */
122
+ declare function readE2BKey(store: CredentialsStore): E2BSection | undefined;
123
+ /**
124
+ * Apply one change to the bundle and write it back.
125
+ *
126
+ * The updater receives the current bundle and returns the next one. Returning
127
+ * `undefined` aborts the write, which is how a caller says "nothing changed" —
128
+ * that matters because every write is a full re-encode, and needlessly
129
+ * rewriting a record while another process reads it is how a spurious
130
+ * "credentials changed" is produced.
131
+ *
132
+ * @param store - the caller's credential store.
133
+ * @param update - computes the next bundle.
134
+ * @returns the bundle now stored.
135
+ */
136
+ declare function updateCredentials(store: CredentialsStore, update: (current: CredentialBundle) => CredentialBundle | undefined): CredentialBundle;
137
+ /**
138
+ * Store the Huawei Cloud pair, replacing any previous one.
139
+ *
140
+ * 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
143
+ * someone else's session.
144
+ *
145
+ * @param store - the caller's credential store.
146
+ * @param credentials - the verified pair.
147
+ */
148
+ declare function writeHuaweiCloud(store: CredentialsStore, credentials: SignCredentials): void;
149
+ /**
150
+ * Store an e2b key.
151
+ *
152
+ * @param store - the caller's credential store.
153
+ * @param key - the section to store.
154
+ */
155
+ declare function writeE2BKey(store: CredentialsStore, key: E2BSection): void;
156
+ /**
157
+ * Remove the stored e2b key, leaving the Huawei Cloud pair alone.
158
+ *
159
+ * @param store - the caller's credential store.
160
+ */
161
+ declare function clearE2BKey(store: CredentialsStore): void;
162
+ /**
163
+ * Remove every credential this repo writes.
164
+ *
165
+ * The whole record goes, so a clear cannot leave a token or a key behind for
166
+ * 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.
169
+ *
170
+ * @param store - the caller's credential store.
171
+ */
172
+ declare function clearCredentials(store: CredentialsStore): void;
173
+ /**
174
+ * A short, stable digest of an AK, for recording which identity minted a key.
175
+ *
176
+ * The AK rather than the pair: the SK can be rotated independently while the
177
+ * account stays the same, and the tenant the gateway derives is the account. A
178
+ * digest rather than the AK itself keeps the bundle from carrying a second
179
+ * readable copy of a credential identifier.
180
+ *
181
+ * @param ak - the access key.
182
+ */
183
+ declare function fingerprint(ak: string): string; //#endregion
184
+
185
+ //# sourceMappingURL=accounts.d.ts.map
186
+ export { CREDENTIAL_ACCOUNT, CredentialBundle, E2BSection, FOREVER_EXPIRY, HuaweiCloudSection, LEGACY_ACCOUNTS, LEGACY_E2B_ACCOUNT, clearCredentials, clearE2BKey, decodeBundle, encodeBundle, fingerprint, hasStoredPair, readCredentials, readE2BKey, readStoredPair, updateCredentials, writeE2BKey, writeHuaweiCloud };
187
+ //# sourceMappingURL=accounts.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,298 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ //#region src/accounts.ts
4
+ /**
5
+ * The one store entry holding every credential this repo manages.
6
+ *
7
+ * Part of the persistence contract: renaming it orphans every stored entry
8
+ * while looking like a harmless constant change.
9
+ */
10
+ const CREDENTIAL_ACCOUNT = "aishell-credential";
11
+ /**
12
+ * Entries written by the earlier three-key layout, in the order they are read
13
+ * during migration.
14
+ *
15
+ * Kept only so an existing install keeps working: see {@link readCredentials}.
16
+ * New writes never touch these.
17
+ */
18
+ const LEGACY_ACCOUNTS = [
19
+ "HW_ACCESS_KEY",
20
+ "HW_SECRET_KEY",
21
+ "HW_SECURITY_TOKEN"
22
+ ];
23
+ /**
24
+ * The legacy entry holding an e2b API key on its own.
25
+ *
26
+ * Folded into the bundle by {@link readCredentials}. Kept as a constant because
27
+ * the migration has to name it.
28
+ */
29
+ const LEGACY_E2B_ACCOUNT = "AISHELL_E2B_API_KEY";
30
+ /**
31
+ * Encode a bundle for storage.
32
+ *
33
+ * @param bundle - the bundle to store.
34
+ * @returns base64 of its JSON form.
35
+ */
36
+ function encodeBundle(bundle) {
37
+ return Buffer.from(JSON.stringify(bundle), "utf8").toString("base64");
38
+ }
39
+ /**
40
+ * Decode a stored value.
41
+ *
42
+ * Anything unreadable reads as an empty bundle rather than throwing: a corrupt
43
+ * entry should behave like "nothing stored" so the person is asked to
44
+ * authenticate again, not so every tool call fails on a parse error.
45
+ *
46
+ * A value that is not base64 is tried as raw JSON first — that is what the
47
+ * legacy e2b entry held, and migration reads it through here.
48
+ *
49
+ * @param raw - the stored string, if any.
50
+ * @returns the decoded bundle; empty when nothing usable is there.
51
+ */
52
+ function decodeBundle(raw) {
53
+ if (raw === void 0 || raw === "") return {};
54
+ const text = decodeText(raw);
55
+ if (text === void 0) return {};
56
+ let parsed;
57
+ try {
58
+ parsed = JSON.parse(text);
59
+ } catch {
60
+ return {};
61
+ }
62
+ if (parsed === null || typeof parsed !== "object") return {};
63
+ const candidate = parsed;
64
+ return {
65
+ ...isHuaweiCloudSection(candidate.huaweicloud) ? { huaweicloud: candidate.huaweicloud } : {},
66
+ ...isE2BSection(candidate.aishell_e2b) ? { aishell_e2b: candidate.aishell_e2b } : {}
67
+ };
68
+ }
69
+ /** Decode base64, falling back to the raw string when it is already text. */
70
+ function decodeText(raw) {
71
+ try {
72
+ const decoded = Buffer.from(raw, "base64");
73
+ if (decoded.toString("base64") === raw.replace(/\s/g, "")) {
74
+ const text = decoded.toString("utf8");
75
+ if (text.trimStart().startsWith("{")) return text;
76
+ }
77
+ } catch {}
78
+ return raw.trimStart().startsWith("{") ? raw : void 0;
79
+ }
80
+ /** Whether a decoded field is a usable Huawei Cloud section. */
81
+ function isHuaweiCloudSection(value) {
82
+ if (value === null || typeof value !== "object") return false;
83
+ const section = value;
84
+ return typeof section.access_key === "string" && section.access_key !== "" && typeof section.secret_key === "string" && section.secret_key !== "" && (section.security_token === void 0 || typeof section.security_token === "string");
85
+ }
86
+ /** Whether a decoded field is a usable e2b section. */
87
+ function isE2BSection(value) {
88
+ if (value === null || typeof value !== "object") return false;
89
+ const section = value;
90
+ 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
+ }
92
+ /**
93
+ * Read the whole bundle, migrating a legacy layout on the way.
94
+ *
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
+ *
100
+ * @param store - the caller's credential store.
101
+ * @returns the bundle; empty when nothing is stored.
102
+ */
103
+ function readCredentials(store) {
104
+ const stored = store.get(CREDENTIAL_ACCOUNT);
105
+ if (stored !== void 0) return decodeBundle(stored);
106
+ const migrated = migrateLegacy(store);
107
+ if (migrated === void 0) return {};
108
+ return migrated;
109
+ }
110
+ /**
111
+ * Merge the legacy entries into one record and remove the old ones.
112
+ *
113
+ * @param store - the caller's credential store.
114
+ * @returns the migrated bundle, or `undefined` when there was nothing to move.
115
+ */
116
+ function migrateLegacy(store) {
117
+ const ak = store.get(LEGACY_ACCOUNTS[0]);
118
+ const sk = store.get(LEGACY_ACCOUNTS[1]);
119
+ const token = store.get(LEGACY_ACCOUNTS[2]);
120
+ const legacyKey = store.get(LEGACY_E2B_ACCOUNT);
121
+ const huaweicloud = ak !== void 0 && ak !== "" && sk !== void 0 && sk !== "" ? {
122
+ access_key: ak,
123
+ secret_key: sk,
124
+ ...token === void 0 || token === "" ? {} : { security_token: token }
125
+ } : void 0;
126
+ const aisleE2B = legacyE2BSection(legacyKey);
127
+ if (huaweicloud === void 0 && aisleE2B === void 0) return void 0;
128
+ const bundle = {
129
+ ...huaweicloud === void 0 ? {} : { huaweicloud },
130
+ ...aisleE2B === void 0 ? {} : { aishell_e2b: aisleE2B }
131
+ };
132
+ try {
133
+ store.set(CREDENTIAL_ACCOUNT, encodeBundle(bundle));
134
+ } catch {
135
+ return bundle;
136
+ }
137
+ for (const account of LEGACY_ACCOUNTS) store.delete(account);
138
+ store.delete(LEGACY_E2B_ACCOUNT);
139
+ return bundle;
140
+ }
141
+ /** Read one legacy e2b entry into its bundle section. */
142
+ function legacyE2BSection(raw) {
143
+ if (raw === void 0 || raw === "") return void 0;
144
+ let parsed;
145
+ try {
146
+ parsed = JSON.parse(raw);
147
+ } catch {
148
+ return {
149
+ api_key: raw,
150
+ expired_at: FOREVER_EXPIRY
151
+ };
152
+ }
153
+ const candidate = parsed;
154
+ if (candidate === null || typeof candidate !== "object") return void 0;
155
+ if (typeof candidate.key !== "string" || candidate.key === "") return void 0;
156
+ return {
157
+ api_key: candidate.key,
158
+ expired_at: typeof candidate.expiresAt === "string" ? candidate.expiresAt : FOREVER_EXPIRY,
159
+ ...typeof candidate.akFingerprint === "string" && candidate.akFingerprint !== "" ? { ak_fingerprint: candidate.akFingerprint } : {}
160
+ };
161
+ }
162
+ /** The expiry recorded for a key that does not expire. */
163
+ const FOREVER_EXPIRY = "9999-12-31T23:59:59.000Z";
164
+ /**
165
+ * Read the Huawei Cloud pair.
166
+ *
167
+ * AK and SK are read together and treated as one unit: half a pair reads as no
168
+ * credentials rather than an error, so a partially-configured machine gets
169
+ * "authenticate first" instead of a signing failure with no remedy. The
170
+ * SecurityToken is optional and is omitted entirely when absent — an empty
171
+ * string would sign as a third header the gateway then rejects.
172
+ *
173
+ * @param store - the caller's credential store.
174
+ * @returns the pair, or `undefined` when the AK/SK pair is incomplete.
175
+ */
176
+ function readStoredPair(store) {
177
+ const section = readCredentials(store).huaweicloud;
178
+ if (section === void 0) return void 0;
179
+ return {
180
+ ak: section.access_key,
181
+ sk: section.secret_key,
182
+ ...section.security_token === void 0 ? {} : { securityToken: section.security_token }
183
+ };
184
+ }
185
+ /**
186
+ * Whether a complete Huawei Cloud pair is stored.
187
+ *
188
+ * @param store - the caller's credential store.
189
+ */
190
+ function hasStoredPair(store) {
191
+ return readStoredPair(store) !== void 0;
192
+ }
193
+ /**
194
+ * Read the stored e2b key.
195
+ *
196
+ * @param store - the caller's credential store.
197
+ * @returns the section, or `undefined` when no key is stored.
198
+ */
199
+ function readE2BKey(store) {
200
+ return readCredentials(store).aishell_e2b;
201
+ }
202
+ /**
203
+ * Apply one change to the bundle and write it back.
204
+ *
205
+ * The updater receives the current bundle and returns the next one. Returning
206
+ * `undefined` aborts the write, which is how a caller says "nothing changed" —
207
+ * that matters because every write is a full re-encode, and needlessly
208
+ * rewriting a record while another process reads it is how a spurious
209
+ * "credentials changed" is produced.
210
+ *
211
+ * @param store - the caller's credential store.
212
+ * @param update - computes the next bundle.
213
+ * @returns the bundle now stored.
214
+ */
215
+ function updateCredentials(store, update) {
216
+ const current = readCredentials(store);
217
+ const next = update(current);
218
+ if (next === void 0) return current;
219
+ store.set(CREDENTIAL_ACCOUNT, encodeBundle(next));
220
+ return next;
221
+ }
222
+ /**
223
+ * Store the Huawei Cloud pair, replacing any previous one.
224
+ *
225
+ * 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
228
+ * someone else's session.
229
+ *
230
+ * @param store - the caller's credential store.
231
+ * @param credentials - the verified pair.
232
+ */
233
+ function writeHuaweiCloud(store, credentials) {
234
+ updateCredentials(store, (current) => ({
235
+ ...current,
236
+ huaweicloud: {
237
+ access_key: credentials.ak,
238
+ secret_key: credentials.sk,
239
+ ...credentials.securityToken === void 0 ? {} : { security_token: credentials.securityToken }
240
+ }
241
+ }));
242
+ }
243
+ /**
244
+ * Store an e2b key.
245
+ *
246
+ * @param store - the caller's credential store.
247
+ * @param key - the section to store.
248
+ */
249
+ function writeE2BKey(store, key) {
250
+ updateCredentials(store, (current) => ({
251
+ ...current,
252
+ aishell_e2b: key
253
+ }));
254
+ }
255
+ /**
256
+ * Remove the stored e2b key, leaving the Huawei Cloud pair alone.
257
+ *
258
+ * @param store - the caller's credential store.
259
+ */
260
+ function clearE2BKey(store) {
261
+ updateCredentials(store, (current) => {
262
+ if (current.aishell_e2b === void 0) return void 0;
263
+ const { aishell_e2b: _removed,...rest } = current;
264
+ return rest;
265
+ });
266
+ }
267
+ /**
268
+ * Remove every credential this repo writes.
269
+ *
270
+ * The whole record goes, so a clear cannot leave a token or a key behind for
271
+ * 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.
274
+ *
275
+ * @param store - the caller's credential store.
276
+ */
277
+ function clearCredentials(store) {
278
+ store.delete(CREDENTIAL_ACCOUNT);
279
+ for (const account of LEGACY_ACCOUNTS) store.delete(account);
280
+ store.delete(LEGACY_E2B_ACCOUNT);
281
+ }
282
+ /**
283
+ * A short, stable digest of an AK, for recording which identity minted a key.
284
+ *
285
+ * The AK rather than the pair: the SK can be rotated independently while the
286
+ * account stays the same, and the tenant the gateway derives is the account. A
287
+ * digest rather than the AK itself keeps the bundle from carrying a second
288
+ * readable copy of a credential identifier.
289
+ *
290
+ * @param ak - the access key.
291
+ */
292
+ function fingerprint(ak) {
293
+ return createHash("sha256").update(ak, "utf8").digest("hex");
294
+ }
295
+
296
+ //#endregion
297
+ export { CREDENTIAL_ACCOUNT, FOREVER_EXPIRY, LEGACY_ACCOUNTS, LEGACY_E2B_ACCOUNT, clearCredentials, clearE2BKey, decodeBundle, encodeBundle, fingerprint, hasStoredPair, readCredentials, readE2BKey, readStoredPair, updateCredentials, writeE2BKey, writeHuaweiCloud };
298
+ //# sourceMappingURL=accounts.js.map
@@ -0,0 +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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"}