@huaweidevtools/aishell-dsh-credentials-store 0.0.7 → 0.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +49 -10
- package/lib/accounts.d.ts +187 -0
- package/lib/accounts.d.ts.map +1 -0
- package/lib/accounts.js +298 -0
- package/lib/accounts.js.map +1 -0
- package/lib/index-JlyJE1E8.d.ts +219 -0
- package/lib/index-JlyJE1E8.d.ts.map +1 -0
- package/lib/index.d.ts +3 -218
- package/lib/index.js +55 -21
- package/lib/index.js.map +1 -1
- package/lib/paths-DtFRzop6.js +46 -0
- package/lib/paths-DtFRzop6.js.map +1 -0
- package/lib/paths.d.ts +56 -0
- package/lib/paths.d.ts.map +1 -0
- package/lib/paths.js +3 -0
- package/lib/scope-CFetnbG3.d.ts +71 -0
- package/lib/scope-CFetnbG3.d.ts.map +1 -0
- package/lib/scope-QmrHZaZt.js +96 -0
- package/lib/scope-QmrHZaZt.js.map +1 -0
- package/lib/scope.d.ts +2 -0
- package/lib/scope.js +3 -0
- package/lib/signer-BPR0UAF0.js +66 -0
- package/lib/signer-BPR0UAF0.js.map +1 -0
- package/lib/signer-Et0DIH6o.d.ts +40 -0
- package/lib/signer-Et0DIH6o.d.ts.map +1 -0
- package/lib/signer.d.ts +2 -40
- package/lib/signer.js +2 -65
- package/lib/types/accounts.d.ts +231 -0
- package/lib/types/accounts.d.ts.map +1 -0
- package/lib/types/accounts.js +384 -0
- package/lib/types/accounts.js.map +1 -0
- package/lib/types/backends/fingerprint.d.ts +41 -0
- package/lib/types/backends/fingerprint.d.ts.map +1 -0
- package/lib/types/backends/fingerprint.js +135 -0
- package/lib/types/backends/fingerprint.js.map +1 -0
- package/lib/types/backends/keyring.d.ts +69 -0
- package/lib/types/backends/keyring.d.ts.map +1 -0
- package/lib/types/backends/keyring.js +107 -0
- package/lib/types/backends/keyring.js.map +1 -0
- package/lib/types/backends/memory.d.ts +45 -0
- package/lib/types/backends/memory.d.ts.map +1 -0
- package/lib/types/backends/memory.js +89 -0
- package/lib/types/backends/memory.js.map +1 -0
- package/lib/types/failover.d.ts +136 -0
- package/lib/types/failover.d.ts.map +1 -0
- package/lib/types/failover.js +242 -0
- package/lib/types/failover.js.map +1 -0
- package/lib/types/index.d.ts +70 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/index.js +78 -0
- package/lib/types/index.js.map +1 -0
- package/lib/types/paths.d.ts +50 -0
- package/lib/types/paths.d.ts.map +1 -0
- package/lib/types/paths.js +59 -0
- package/lib/types/paths.js.map +1 -0
- package/lib/types/scope.d.ts +65 -0
- package/lib/types/scope.d.ts.map +1 -0
- package/lib/types/scope.js +102 -0
- package/lib/types/scope.js.map +1 -0
- package/lib/types/signer.d.ts +36 -0
- package/lib/types/signer.d.ts.map +1 -0
- package/lib/types/signer.js +81 -0
- package/lib/types/signer.js.map +1 -0
- package/lib/types/verify.d.ts +114 -0
- package/lib/types/verify.d.ts.map +1 -0
- package/lib/types/verify.js +240 -0
- package/lib/types/verify.js.map +1 -0
- package/lib/verify.d.ts +72 -0
- package/lib/verify.d.ts.map +1 -0
- package/lib/verify.js +172 -0
- package/lib/verify.js.map +1 -0
- package/package.json +20 -7
- package/lib/index.d.ts.map +0 -1
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
|
|
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
|
|
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
|
|
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
|
-
|
|
76
|
+
// Read the pair: one snapshot of one record.
|
|
77
|
+
const pair = readStoredPair(store) // { ak, sk, securityToken? } | undefined
|
|
49
78
|
|
|
50
|
-
|
|
51
|
-
|
|
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"}
|
package/lib/accounts.js
ADDED
|
@@ -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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"}
|