@huaweidevtools/aishell-dsh-credentials-store 0.0.1-beta.2 → 0.0.1-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +62 -14
- 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-CsXaUaRH.d.ts +211 -0
- package/lib/index-CsXaUaRH.d.ts.map +1 -0
- package/lib/index.d.ts +3 -208
- package/lib/index.js +26 -28
- 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 +22 -0
- package/lib/types/backends/memory.d.ts.map +1 -0
- package/lib/types/backends/memory.js +35 -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 +73 -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 +21 -8
- package/lib/index.d.ts.map +0 -1
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Huawei Cloud IAM credential verification — one implementation, shared.
|
|
3
|
+
*
|
|
4
|
+
* ## Why it moved here
|
|
5
|
+
*
|
|
6
|
+
* Two packages verified credentials independently: `aishell-credentials`' Host
|
|
7
|
+
* route, with `GET /v3/auth/domains` (it only wanted the account name), and
|
|
8
|
+
* the copied ops tool `auth`, with `GET /v3/projects` (it also wanted the
|
|
9
|
+
* account id and the project list). Same question — "are these credentials
|
|
10
|
+
* valid?" — two endpoints, two error-message vocabularies, two chances to
|
|
11
|
+
* disagree about what a 403 means.
|
|
12
|
+
*
|
|
13
|
+
* This module is the one answer. It is storage-adjacent rather than
|
|
14
|
+
* credential-form-specific, so it lives with the store and the signer, and
|
|
15
|
+
* every caller gets the same verdict in the same shape.
|
|
16
|
+
*
|
|
17
|
+
* ## Why `/v3/projects` is the endpoint
|
|
18
|
+
*
|
|
19
|
+
* It is a superset of the domains call:
|
|
20
|
+
*
|
|
21
|
+
* - A 200 proves the AK/SK (and STS token) work, signed exactly the way every
|
|
22
|
+
* other call is.
|
|
23
|
+
* - Each project carries `domain_id`, which **is** the account id (Huawei
|
|
24
|
+
* Cloud's "domain" and "account" are the same object).
|
|
25
|
+
* - Each project's `name` is a region name, so a caller that needs a
|
|
26
|
+
* `project_id` for a region-scoped API has it without a second request.
|
|
27
|
+
*
|
|
28
|
+
* The account *name* is the one thing projects does not carry, so it is read
|
|
29
|
+
* from `/v3/auth/domains` **best-effort** — some AK/SK scopes do not grant it,
|
|
30
|
+
* and a missing name is diagnostic-only. A failure there never fails
|
|
31
|
+
* verification.
|
|
32
|
+
*
|
|
33
|
+
* ## Region semantics, which surprise people
|
|
34
|
+
*
|
|
35
|
+
* IAM is a global service. The region in the URL only routes to a regional
|
|
36
|
+
* replica; it does not scope the result. So a wrong region still answers 200
|
|
37
|
+
* with the same account, and the real mismatch surfaces at the first
|
|
38
|
+
* region-scoped call. Verification therefore validates the credentials and
|
|
39
|
+
* **not** the region — conflating them would report "invalid credentials" for
|
|
40
|
+
* a typo'd region, sending the person to check the wrong thing.
|
|
41
|
+
*
|
|
42
|
+
* ## Result shape
|
|
43
|
+
*
|
|
44
|
+
* A discriminated union rather than a throw: a rejected credential is an
|
|
45
|
+
* expected outcome of asking someone for credentials, not an exception. The
|
|
46
|
+
* caller decides whether to show, log or retry it.
|
|
47
|
+
*
|
|
48
|
+
* @module @huaweidevtools/aishell-dsh-credentials-store/verify
|
|
49
|
+
*/
|
|
50
|
+
import { sign } from "./signer.js";
|
|
51
|
+
/** Default per-request timeout; IAM answers in well under a second when healthy. */
|
|
52
|
+
const DEFAULT_TIMEOUT_MS = 15_000;
|
|
53
|
+
/** IAM hostname for a region (regional replica of a global service). */
|
|
54
|
+
function iamUrl(region) {
|
|
55
|
+
const host = /^\d+\.\d+\.\d+\.\d+$/.test(region) || region === 'global'
|
|
56
|
+
? 'iam.myhuaweicloud.com'
|
|
57
|
+
: `iam.${region}.myhuaweicloud.com`;
|
|
58
|
+
return `https://${host}`;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Sign and GET one IAM path, returning the status and body.
|
|
62
|
+
*
|
|
63
|
+
* @param base - IAM origin.
|
|
64
|
+
* @param path - the request path.
|
|
65
|
+
* @param credentials - the pair to sign with.
|
|
66
|
+
* @param options - fetch implementation and timeout.
|
|
67
|
+
*/
|
|
68
|
+
async function signedGet(base, path, credentials, options) {
|
|
69
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
70
|
+
const url = new URL(`${base}${path}`);
|
|
71
|
+
const headers = sign('GET', url.host, url.pathname, {}, '', credentials);
|
|
72
|
+
const signal = AbortSignal.timeout(options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
73
|
+
const response = await fetchImpl(url, {
|
|
74
|
+
method: 'GET',
|
|
75
|
+
headers: {
|
|
76
|
+
host: headers.host,
|
|
77
|
+
'x-sdk-date': headers['x-sdk-date'],
|
|
78
|
+
Authorization: headers.Authorization,
|
|
79
|
+
...(headers['x-security-token'] === undefined
|
|
80
|
+
? {}
|
|
81
|
+
: { 'x-security-token': headers['x-security-token'] }),
|
|
82
|
+
},
|
|
83
|
+
signal,
|
|
84
|
+
});
|
|
85
|
+
return { status: response.status, body: await response.text() };
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Verify a credential pair against Huawei Cloud IAM.
|
|
89
|
+
*
|
|
90
|
+
* The pair is signed exactly as every other request is, so a 200 proves the
|
|
91
|
+
* same thing a successful tool call would prove — no separate verification
|
|
92
|
+
* scheme that could drift from the one that actually runs.
|
|
93
|
+
*
|
|
94
|
+
* @param credentials - the AK/SK (and optional STS token) to verify.
|
|
95
|
+
* @param options - region and transport details.
|
|
96
|
+
* @returns the account on success, or a failure naming what IAM said.
|
|
97
|
+
*/
|
|
98
|
+
export async function verifyCredentials(credentials, options) {
|
|
99
|
+
const base = iamUrl(options.region);
|
|
100
|
+
let status;
|
|
101
|
+
let body;
|
|
102
|
+
try {
|
|
103
|
+
({ status, body } = await signedGet(base, '/v3/projects', credentials, options));
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
return {
|
|
107
|
+
ok: false,
|
|
108
|
+
status: 0,
|
|
109
|
+
message: `Could not reach Huawei Cloud IAM: ${error instanceof Error ? error.message : String(error)}`,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
if (status !== 200) {
|
|
113
|
+
return { ok: false, status, message: describeIamFailure(status, body) };
|
|
114
|
+
}
|
|
115
|
+
const parsedProjects = parseProjects(body);
|
|
116
|
+
if (parsedProjects === undefined) {
|
|
117
|
+
return { ok: false, status, message: 'Huawei Cloud IAM answered with a body that is not JSON.' };
|
|
118
|
+
}
|
|
119
|
+
const first = parsedProjects[0];
|
|
120
|
+
if (first === undefined) {
|
|
121
|
+
// Authenticated but sees no projects. IAM is global, so this is not a
|
|
122
|
+
// region problem: the AK/SK lacks IAM read scope, or belongs to a
|
|
123
|
+
// sub-account whose parent has not granted project membership. Terraform
|
|
124
|
+
// needs a project, so this is a failure rather than an empty success.
|
|
125
|
+
return {
|
|
126
|
+
ok: false,
|
|
127
|
+
status,
|
|
128
|
+
message: 'The credentials are valid but no project is visible to them. Confirm the AK/SK may '
|
|
129
|
+
+ 'read IAM, or ask the main account to grant project membership. '
|
|
130
|
+
+ '(This is unrelated to the region — IAM is a global service.)',
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
const accountId = first.domainId;
|
|
134
|
+
if (accountId === undefined) {
|
|
135
|
+
return { ok: false, status, message: 'Huawei Cloud IAM returned no domain_id, so the account is unknown.' };
|
|
136
|
+
}
|
|
137
|
+
const name = await readAccountName(base, accountId, credentials, options) ?? accountId;
|
|
138
|
+
return {
|
|
139
|
+
ok: true,
|
|
140
|
+
account: { accountId, name, projects: parsedProjects },
|
|
141
|
+
region: options.region,
|
|
142
|
+
sts: credentials.securityToken !== undefined,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Parse `/v3/projects` into the project list.
|
|
147
|
+
*
|
|
148
|
+
* @returns the projects (possibly empty), or `undefined` when the body is not
|
|
149
|
+
* JSON at all — a different failure from "valid JSON, no projects".
|
|
150
|
+
*/
|
|
151
|
+
function parseProjects(body) {
|
|
152
|
+
let parsed;
|
|
153
|
+
try {
|
|
154
|
+
parsed = JSON.parse(body);
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
const raw = parsed?.projects;
|
|
160
|
+
if (!Array.isArray(raw))
|
|
161
|
+
return [];
|
|
162
|
+
const projects = [];
|
|
163
|
+
for (const entry of raw) {
|
|
164
|
+
const project = entry;
|
|
165
|
+
if (project === null || typeof project !== 'object')
|
|
166
|
+
continue;
|
|
167
|
+
if (typeof project.id !== 'string' || project.id === '')
|
|
168
|
+
continue;
|
|
169
|
+
projects.push({
|
|
170
|
+
id: project.id,
|
|
171
|
+
name: typeof project.name === 'string' ? project.name : '',
|
|
172
|
+
...(typeof project.description === 'string' && project.description !== ''
|
|
173
|
+
? { description: project.description }
|
|
174
|
+
: {}),
|
|
175
|
+
// Kept beside the public fields because the caller derives the account id
|
|
176
|
+
// from it without a second pass over the raw body.
|
|
177
|
+
...(typeof project.domain_id === 'string' && project.domain_id !== ''
|
|
178
|
+
? { domainId: project.domain_id }
|
|
179
|
+
: {}),
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
return projects;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Read the account (domain) name, best-effort.
|
|
186
|
+
*
|
|
187
|
+
* @returns the name, or `undefined` — never an error. Some AK/SK scopes do not
|
|
188
|
+
* grant `/v3/auth/domains`, and the account name is diagnostic only.
|
|
189
|
+
*/
|
|
190
|
+
async function readAccountName(base, accountId, credentials, options) {
|
|
191
|
+
let status;
|
|
192
|
+
let body;
|
|
193
|
+
try {
|
|
194
|
+
({ status, body } = await signedGet(base, '/v3/auth/domains', credentials, options));
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
return undefined;
|
|
198
|
+
}
|
|
199
|
+
if (status !== 200)
|
|
200
|
+
return undefined;
|
|
201
|
+
try {
|
|
202
|
+
const parsed = JSON.parse(body);
|
|
203
|
+
const match = parsed.domains?.find(domain => domain.id === accountId);
|
|
204
|
+
return typeof match?.name === 'string' && match.name !== '' ? match.name : undefined;
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
return undefined;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Render an IAM rejection into one actionable sentence.
|
|
212
|
+
*
|
|
213
|
+
* @param status - the HTTP status.
|
|
214
|
+
* @param body - the response body, which may carry Huawei's own error text.
|
|
215
|
+
*/
|
|
216
|
+
function describeIamFailure(status, body) {
|
|
217
|
+
try {
|
|
218
|
+
const parsed = JSON.parse(body);
|
|
219
|
+
const detail = typeof parsed.error_msg === 'string'
|
|
220
|
+
? parsed.error_msg
|
|
221
|
+
: typeof parsed.message === 'string' ? parsed.message : undefined;
|
|
222
|
+
if (detail !== undefined) {
|
|
223
|
+
const code = typeof parsed.error_code === 'string' ? `, ${parsed.error_code}` : '';
|
|
224
|
+
return `Huawei Cloud rejected the credentials (HTTP ${status}${code}): ${detail}`;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
// Non-JSON body — an ingress page or a proxy error; nothing to read.
|
|
229
|
+
}
|
|
230
|
+
if (status === 401) {
|
|
231
|
+
return 'Huawei Cloud rejected the credentials (HTTP 401): the Access Key, Secret Key, '
|
|
232
|
+
+ 'or Security Token was refused.';
|
|
233
|
+
}
|
|
234
|
+
if (status === 403) {
|
|
235
|
+
return 'The credentials are valid but may not read IAM (HTTP 403). '
|
|
236
|
+
+ 'Confirm the Access Key has IAM read permission.';
|
|
237
|
+
}
|
|
238
|
+
return `Huawei Cloud IAM failed with HTTP ${status}: ${body.slice(0, 200)}`;
|
|
239
|
+
}
|
|
240
|
+
//# sourceMappingURL=verify.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verify.js","sourceRoot":"","sources":["../../src/verify.ts"],"names":[],"mappings":"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"}
|
package/lib/verify.d.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { SignCredentials } from "./signer-Et0DIH6o.js";
|
|
2
|
+
|
|
3
|
+
//#region src/verify.d.ts
|
|
4
|
+
/** One project visible to the credentials; its `name` is a region name. */
|
|
5
|
+
|
|
6
|
+
/** One project visible to the credentials; its `name` is a region name. */
|
|
7
|
+
interface VerifiedProject {
|
|
8
|
+
/** The `project_id` a region-scoped API takes. */
|
|
9
|
+
readonly id: string;
|
|
10
|
+
/** The project name — the region name, e.g. `cn-north-4`. */
|
|
11
|
+
readonly name: string;
|
|
12
|
+
readonly description?: string;
|
|
13
|
+
}
|
|
14
|
+
/** The account the credentials belong to. */
|
|
15
|
+
interface VerifiedAccount {
|
|
16
|
+
/** The account id, equal to IAM's `domain_id`. */
|
|
17
|
+
readonly accountId: string;
|
|
18
|
+
/** The account (domain) name; falls back to {@link accountId} when unreadable. */
|
|
19
|
+
readonly name: string;
|
|
20
|
+
/**
|
|
21
|
+
* Every project visible to these credentials.
|
|
22
|
+
*
|
|
23
|
+
* More than one project may share a region name (enterprise projects), which
|
|
24
|
+
* is why this is not reduced to a region→project map: picking one would be a
|
|
25
|
+
* deployment decision this module has no basis for.
|
|
26
|
+
*/
|
|
27
|
+
readonly projects: readonly VerifiedProject[];
|
|
28
|
+
}
|
|
29
|
+
/** A successful verification. */
|
|
30
|
+
interface VerifyOk {
|
|
31
|
+
readonly ok: true;
|
|
32
|
+
readonly account: VerifiedAccount;
|
|
33
|
+
/** The region the call was routed through; see the module note — not verified. */
|
|
34
|
+
readonly region: string;
|
|
35
|
+
/** Whether the credentials were temporary (STS). */
|
|
36
|
+
readonly sts: boolean;
|
|
37
|
+
}
|
|
38
|
+
/** A failed verification. */
|
|
39
|
+
interface VerifyFailed {
|
|
40
|
+
readonly ok: false;
|
|
41
|
+
/** HTTP status from IAM, or `0` for a network/signing failure. */
|
|
42
|
+
readonly status: number;
|
|
43
|
+
/** A sentence the person or model can act on. */
|
|
44
|
+
readonly message: string;
|
|
45
|
+
}
|
|
46
|
+
/** The outcome of one verification. */
|
|
47
|
+
type VerifyResult = VerifyOk | VerifyFailed;
|
|
48
|
+
/** Options for {@link verifyCredentials}. */
|
|
49
|
+
interface VerifyOptions {
|
|
50
|
+
/** Region used to route the call. Does not scope the result; see the module note. */
|
|
51
|
+
readonly region: string;
|
|
52
|
+
/** Fetch implementation; defaults to the global. Injected so tests need no network. */
|
|
53
|
+
readonly fetchImpl?: typeof fetch;
|
|
54
|
+
/** Per-request timeout in milliseconds; defaults to 15 s. */
|
|
55
|
+
readonly timeoutMs?: number;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Verify a credential pair against Huawei Cloud IAM.
|
|
59
|
+
*
|
|
60
|
+
* The pair is signed exactly as every other request is, so a 200 proves the
|
|
61
|
+
* same thing a successful tool call would prove — no separate verification
|
|
62
|
+
* scheme that could drift from the one that actually runs.
|
|
63
|
+
*
|
|
64
|
+
* @param credentials - the AK/SK (and optional STS token) to verify.
|
|
65
|
+
* @param options - region and transport details.
|
|
66
|
+
* @returns the account on success, or a failure naming what IAM said.
|
|
67
|
+
*/
|
|
68
|
+
declare function verifyCredentials(credentials: SignCredentials, options: VerifyOptions): Promise<VerifyResult>; //#endregion
|
|
69
|
+
|
|
70
|
+
//# sourceMappingURL=verify.d.ts.map
|
|
71
|
+
export { VerifiedAccount, VerifiedProject, VerifyFailed, VerifyOk, VerifyOptions, VerifyResult, verifyCredentials };
|
|
72
|
+
//# sourceMappingURL=verify.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verify.d.ts","names":[],"sources":["../../src/verify.ts"],"sourcesContent":null,"mappings":";;;;;;UAqDiB,eAAA;;;;;;;;UASA,eAAA;;;;;;;;;;;;8BAYa;;;UAIb,QAAA;;oBAEG;;;;;;;UAQH,YAAA;;;;;;;;KASL,YAAA,GAAe,WAAW;;UAGrB,aAAA;;;;8BAIa;;;;;;;;;;;;;;;iBA4DR,iBAAA,cACP,0BACJ,gBACR,QAAQ"}
|
package/lib/verify.js
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { sign } from "./signer-BPR0UAF0.js";
|
|
2
|
+
|
|
3
|
+
//#region src/verify.ts
|
|
4
|
+
/** Default per-request timeout; IAM answers in well under a second when healthy. */
|
|
5
|
+
const DEFAULT_TIMEOUT_MS = 15e3;
|
|
6
|
+
/** IAM hostname for a region (regional replica of a global service). */
|
|
7
|
+
function iamUrl(region) {
|
|
8
|
+
const host = /^\d+\.\d+\.\d+\.\d+$/.test(region) || region === "global" ? "iam.myhuaweicloud.com" : `iam.${region}.myhuaweicloud.com`;
|
|
9
|
+
return `https://${host}`;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Sign and GET one IAM path, returning the status and body.
|
|
13
|
+
*
|
|
14
|
+
* @param base - IAM origin.
|
|
15
|
+
* @param path - the request path.
|
|
16
|
+
* @param credentials - the pair to sign with.
|
|
17
|
+
* @param options - fetch implementation and timeout.
|
|
18
|
+
*/
|
|
19
|
+
async function signedGet(base, path, credentials, options) {
|
|
20
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
21
|
+
const url = new URL(`${base}${path}`);
|
|
22
|
+
const headers = sign("GET", url.host, url.pathname, {}, "", credentials);
|
|
23
|
+
const signal = AbortSignal.timeout(options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
24
|
+
const response = await fetchImpl(url, {
|
|
25
|
+
method: "GET",
|
|
26
|
+
headers: {
|
|
27
|
+
host: headers.host,
|
|
28
|
+
"x-sdk-date": headers["x-sdk-date"],
|
|
29
|
+
Authorization: headers.Authorization,
|
|
30
|
+
...headers["x-security-token"] === void 0 ? {} : { "x-security-token": headers["x-security-token"] }
|
|
31
|
+
},
|
|
32
|
+
signal
|
|
33
|
+
});
|
|
34
|
+
return {
|
|
35
|
+
status: response.status,
|
|
36
|
+
body: await response.text()
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Verify a credential pair against Huawei Cloud IAM.
|
|
41
|
+
*
|
|
42
|
+
* The pair is signed exactly as every other request is, so a 200 proves the
|
|
43
|
+
* same thing a successful tool call would prove — no separate verification
|
|
44
|
+
* scheme that could drift from the one that actually runs.
|
|
45
|
+
*
|
|
46
|
+
* @param credentials - the AK/SK (and optional STS token) to verify.
|
|
47
|
+
* @param options - region and transport details.
|
|
48
|
+
* @returns the account on success, or a failure naming what IAM said.
|
|
49
|
+
*/
|
|
50
|
+
async function verifyCredentials(credentials, options) {
|
|
51
|
+
const base = iamUrl(options.region);
|
|
52
|
+
let status;
|
|
53
|
+
let body;
|
|
54
|
+
try {
|
|
55
|
+
({status, body} = await signedGet(base, "/v3/projects", credentials, options));
|
|
56
|
+
} catch (error) {
|
|
57
|
+
return {
|
|
58
|
+
ok: false,
|
|
59
|
+
status: 0,
|
|
60
|
+
message: `Could not reach Huawei Cloud IAM: ${error instanceof Error ? error.message : String(error)}`
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
if (status !== 200) return {
|
|
64
|
+
ok: false,
|
|
65
|
+
status,
|
|
66
|
+
message: describeIamFailure(status, body)
|
|
67
|
+
};
|
|
68
|
+
const parsedProjects = parseProjects(body);
|
|
69
|
+
if (parsedProjects === void 0) return {
|
|
70
|
+
ok: false,
|
|
71
|
+
status,
|
|
72
|
+
message: "Huawei Cloud IAM answered with a body that is not JSON."
|
|
73
|
+
};
|
|
74
|
+
const first = parsedProjects[0];
|
|
75
|
+
if (first === void 0) return {
|
|
76
|
+
ok: false,
|
|
77
|
+
status,
|
|
78
|
+
message: "The credentials are valid but no project is visible to them. Confirm the AK/SK may read IAM, or ask the main account to grant project membership. (This is unrelated to the region — IAM is a global service.)"
|
|
79
|
+
};
|
|
80
|
+
const accountId = first.domainId;
|
|
81
|
+
if (accountId === void 0) return {
|
|
82
|
+
ok: false,
|
|
83
|
+
status,
|
|
84
|
+
message: "Huawei Cloud IAM returned no domain_id, so the account is unknown."
|
|
85
|
+
};
|
|
86
|
+
const name = await readAccountName(base, accountId, credentials, options) ?? accountId;
|
|
87
|
+
return {
|
|
88
|
+
ok: true,
|
|
89
|
+
account: {
|
|
90
|
+
accountId,
|
|
91
|
+
name,
|
|
92
|
+
projects: parsedProjects
|
|
93
|
+
},
|
|
94
|
+
region: options.region,
|
|
95
|
+
sts: credentials.securityToken !== void 0
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Parse `/v3/projects` into the project list.
|
|
100
|
+
*
|
|
101
|
+
* @returns the projects (possibly empty), or `undefined` when the body is not
|
|
102
|
+
* JSON at all — a different failure from "valid JSON, no projects".
|
|
103
|
+
*/
|
|
104
|
+
function parseProjects(body) {
|
|
105
|
+
let parsed;
|
|
106
|
+
try {
|
|
107
|
+
parsed = JSON.parse(body);
|
|
108
|
+
} catch {
|
|
109
|
+
return void 0;
|
|
110
|
+
}
|
|
111
|
+
const raw = parsed?.projects;
|
|
112
|
+
if (!Array.isArray(raw)) return [];
|
|
113
|
+
const projects = [];
|
|
114
|
+
for (const entry of raw) {
|
|
115
|
+
const project = entry;
|
|
116
|
+
if (project === null || typeof project !== "object") continue;
|
|
117
|
+
if (typeof project.id !== "string" || project.id === "") continue;
|
|
118
|
+
projects.push({
|
|
119
|
+
id: project.id,
|
|
120
|
+
name: typeof project.name === "string" ? project.name : "",
|
|
121
|
+
...typeof project.description === "string" && project.description !== "" ? { description: project.description } : {},
|
|
122
|
+
...typeof project.domain_id === "string" && project.domain_id !== "" ? { domainId: project.domain_id } : {}
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
return projects;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Read the account (domain) name, best-effort.
|
|
129
|
+
*
|
|
130
|
+
* @returns the name, or `undefined` — never an error. Some AK/SK scopes do not
|
|
131
|
+
* grant `/v3/auth/domains`, and the account name is diagnostic only.
|
|
132
|
+
*/
|
|
133
|
+
async function readAccountName(base, accountId, credentials, options) {
|
|
134
|
+
let status;
|
|
135
|
+
let body;
|
|
136
|
+
try {
|
|
137
|
+
({status, body} = await signedGet(base, "/v3/auth/domains", credentials, options));
|
|
138
|
+
} catch {
|
|
139
|
+
return void 0;
|
|
140
|
+
}
|
|
141
|
+
if (status !== 200) return void 0;
|
|
142
|
+
try {
|
|
143
|
+
const parsed = JSON.parse(body);
|
|
144
|
+
const match = parsed.domains?.find((domain) => domain.id === accountId);
|
|
145
|
+
return typeof match?.name === "string" && match.name !== "" ? match.name : void 0;
|
|
146
|
+
} catch {
|
|
147
|
+
return void 0;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Render an IAM rejection into one actionable sentence.
|
|
152
|
+
*
|
|
153
|
+
* @param status - the HTTP status.
|
|
154
|
+
* @param body - the response body, which may carry Huawei's own error text.
|
|
155
|
+
*/
|
|
156
|
+
function describeIamFailure(status, body) {
|
|
157
|
+
try {
|
|
158
|
+
const parsed = JSON.parse(body);
|
|
159
|
+
const detail = typeof parsed.error_msg === "string" ? parsed.error_msg : typeof parsed.message === "string" ? parsed.message : void 0;
|
|
160
|
+
if (detail !== void 0) {
|
|
161
|
+
const code = typeof parsed.error_code === "string" ? `, ${parsed.error_code}` : "";
|
|
162
|
+
return `Huawei Cloud rejected the credentials (HTTP ${status}${code}): ${detail}`;
|
|
163
|
+
}
|
|
164
|
+
} catch {}
|
|
165
|
+
if (status === 401) return "Huawei Cloud rejected the credentials (HTTP 401): the Access Key, Secret Key, or Security Token was refused.";
|
|
166
|
+
if (status === 403) return "The credentials are valid but may not read IAM (HTTP 403). Confirm the Access Key has IAM read permission.";
|
|
167
|
+
return `Huawei Cloud IAM failed with HTTP ${status}: ${body.slice(0, 200)}`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
//#endregion
|
|
171
|
+
export { verifyCredentials };
|
|
172
|
+
//# sourceMappingURL=verify.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verify.js","names":["region: string","base: string","path: string","credentials: SignCredentials","options: VerifyOptions","status: number","body: string","parsed: unknown","projects: Array<VerifiedProject & { readonly domainId?: string }>","accountId: string"],"sources":["../src/verify.ts"],"sourcesContent":["/**\n * Huawei Cloud IAM credential verification — one implementation, shared.\n *\n * ## Why it moved here\n *\n * Two packages verified credentials independently: `aishell-credentials`' Host\n * route, with `GET /v3/auth/domains` (it only wanted the account name), and\n * the copied ops tool `auth`, with `GET /v3/projects` (it also wanted the\n * account id and the project list). Same question — \"are these credentials\n * valid?\" — two endpoints, two error-message vocabularies, two chances to\n * disagree about what a 403 means.\n *\n * This module is the one answer. It is storage-adjacent rather than\n * credential-form-specific, so it lives with the store and the signer, and\n * every caller gets the same verdict in the same shape.\n *\n * ## Why `/v3/projects` is the endpoint\n *\n * It is a superset of the domains call:\n *\n * - A 200 proves the AK/SK (and STS token) work, signed exactly the way every\n * other call is.\n * - Each project carries `domain_id`, which **is** the account id (Huawei\n * Cloud's \"domain\" and \"account\" are the same object).\n * - Each project's `name` is a region name, so a caller that needs a\n * `project_id` for a region-scoped API has it without a second request.\n *\n * The account *name* is the one thing projects does not carry, so it is read\n * from `/v3/auth/domains` **best-effort** — some AK/SK scopes do not grant it,\n * and a missing name is diagnostic-only. A failure there never fails\n * verification.\n *\n * ## Region semantics, which surprise people\n *\n * IAM is a global service. The region in the URL only routes to a regional\n * replica; it does not scope the result. So a wrong region still answers 200\n * with the same account, and the real mismatch surfaces at the first\n * region-scoped call. Verification therefore validates the credentials and\n * **not** the region — conflating them would report \"invalid credentials\" for\n * a typo'd region, sending the person to check the wrong thing.\n *\n * ## Result shape\n *\n * A discriminated union rather than a throw: a rejected credential is an\n * expected outcome of asking someone for credentials, not an exception. The\n * caller decides whether to show, log or retry it.\n *\n * @module @huaweidevtools/aishell-dsh-credentials-store/verify\n */\n\nimport { sign, type SignCredentials } from './signer.ts'\n\n/** One project visible to the credentials; its `name` is a region name. */\nexport interface VerifiedProject {\n /** The `project_id` a region-scoped API takes. */\n readonly id: string\n /** The project name — the region name, e.g. `cn-north-4`. */\n readonly name: string\n readonly description?: string\n}\n\n/** The account the credentials belong to. */\nexport interface VerifiedAccount {\n /** The account id, equal to IAM's `domain_id`. */\n readonly accountId: string\n /** The account (domain) name; falls back to {@link accountId} when unreadable. */\n readonly name: string\n /**\n * Every project visible to these credentials.\n *\n * More than one project may share a region name (enterprise projects), which\n * is why this is not reduced to a region→project map: picking one would be a\n * deployment decision this module has no basis for.\n */\n readonly projects: readonly VerifiedProject[]\n}\n\n/** A successful verification. */\nexport interface VerifyOk {\n readonly ok: true\n readonly account: VerifiedAccount\n /** The region the call was routed through; see the module note — not verified. */\n readonly region: string\n /** Whether the credentials were temporary (STS). */\n readonly sts: boolean\n}\n\n/** A failed verification. */\nexport interface VerifyFailed {\n readonly ok: false\n /** HTTP status from IAM, or `0` for a network/signing failure. */\n readonly status: number\n /** A sentence the person or model can act on. */\n readonly message: string\n}\n\n/** The outcome of one verification. */\nexport type VerifyResult = VerifyOk | VerifyFailed\n\n/** Options for {@link verifyCredentials}. */\nexport interface VerifyOptions {\n /** Region used to route the call. Does not scope the result; see the module note. */\n readonly region: string\n /** Fetch implementation; defaults to the global. Injected so tests need no network. */\n readonly fetchImpl?: typeof fetch\n /** Per-request timeout in milliseconds; defaults to 15 s. */\n readonly timeoutMs?: number\n}\n\n/** Default per-request timeout; IAM answers in well under a second when healthy. */\nconst DEFAULT_TIMEOUT_MS = 15_000\n\n/** IAM hostname for a region (regional replica of a global service). */\nfunction iamUrl(region: string): string {\n const host = /^\\d+\\.\\d+\\.\\d+\\.\\d+$/.test(region) || region === 'global'\n ? 'iam.myhuaweicloud.com'\n : `iam.${region}.myhuaweicloud.com`\n return `https://${host}`\n}\n\n/**\n * Sign and GET one IAM path, returning the status and body.\n *\n * @param base - IAM origin.\n * @param path - the request path.\n * @param credentials - the pair to sign with.\n * @param options - fetch implementation and timeout.\n */\nasync function signedGet(\n base: string,\n path: string,\n credentials: SignCredentials,\n options: VerifyOptions,\n): Promise<{ status: number, body: string }> {\n const fetchImpl = options.fetchImpl ?? fetch\n const url = new URL(`${base}${path}`)\n const headers = sign('GET', url.host, url.pathname, {}, '', credentials)\n const signal = AbortSignal.timeout(options.timeoutMs ?? DEFAULT_TIMEOUT_MS)\n const response = await fetchImpl(url, {\n method: 'GET',\n headers: {\n host: headers.host,\n 'x-sdk-date': headers['x-sdk-date'],\n Authorization: headers.Authorization,\n ...(headers['x-security-token'] === undefined\n ? {}\n : { 'x-security-token': headers['x-security-token'] }),\n },\n signal,\n })\n return { status: response.status, body: await response.text() }\n}\n\n/**\n * Verify a credential pair against Huawei Cloud IAM.\n *\n * The pair is signed exactly as every other request is, so a 200 proves the\n * same thing a successful tool call would prove — no separate verification\n * scheme that could drift from the one that actually runs.\n *\n * @param credentials - the AK/SK (and optional STS token) to verify.\n * @param options - region and transport details.\n * @returns the account on success, or a failure naming what IAM said.\n */\nexport async function verifyCredentials(\n credentials: SignCredentials,\n options: VerifyOptions,\n): Promise<VerifyResult> {\n const base = iamUrl(options.region)\n\n let status: number\n let body: string\n try {\n ({ status, body } = await signedGet(base, '/v3/projects', credentials, options))\n } catch (error) {\n return {\n ok: false,\n status: 0,\n message: `Could not reach Huawei Cloud IAM: ${error instanceof Error ? error.message : String(error)}`,\n }\n }\n\n if (status !== 200) {\n return { ok: false, status, message: describeIamFailure(status, body) }\n }\n\n const parsedProjects = parseProjects(body)\n if (parsedProjects === undefined) {\n return { ok: false, status, message: 'Huawei Cloud IAM answered with a body that is not JSON.' }\n }\n\n const first = parsedProjects[0]\n if (first === undefined) {\n // Authenticated but sees no projects. IAM is global, so this is not a\n // region problem: the AK/SK lacks IAM read scope, or belongs to a\n // sub-account whose parent has not granted project membership. Terraform\n // needs a project, so this is a failure rather than an empty success.\n return {\n ok: false,\n status,\n message:\n 'The credentials are valid but no project is visible to them. Confirm the AK/SK may '\n + 'read IAM, or ask the main account to grant project membership. '\n + '(This is unrelated to the region — IAM is a global service.)',\n }\n }\n\n const accountId = first.domainId\n if (accountId === undefined) {\n return { ok: false, status, message: 'Huawei Cloud IAM returned no domain_id, so the account is unknown.' }\n }\n\n const name = await readAccountName(base, accountId, credentials, options) ?? accountId\n\n return {\n ok: true,\n account: { accountId, name, projects: parsedProjects },\n region: options.region,\n sts: credentials.securityToken !== undefined,\n }\n}\n\n/**\n * Parse `/v3/projects` into the project list.\n *\n * @returns the projects (possibly empty), or `undefined` when the body is not\n * JSON at all — a different failure from \"valid JSON, no projects\".\n */\nfunction parseProjects(body: string): Array<VerifiedProject & { readonly domainId?: string }> | undefined {\n let parsed: unknown\n try {\n parsed = JSON.parse(body)\n } catch {\n return undefined\n }\n const raw = (parsed as { projects?: unknown } | null)?.projects\n if (!Array.isArray(raw)) return []\n\n const projects: Array<VerifiedProject & { readonly domainId?: string }> = []\n for (const entry of raw) {\n const project = entry as Record<string, unknown> | null\n if (project === null || typeof project !== 'object') continue\n if (typeof project.id !== 'string' || project.id === '') continue\n projects.push({\n id: project.id,\n name: typeof project.name === 'string' ? project.name : '',\n ...(typeof project.description === 'string' && project.description !== ''\n ? { description: project.description }\n : {}),\n // Kept beside the public fields because the caller derives the account id\n // from it without a second pass over the raw body.\n ...(typeof project.domain_id === 'string' && project.domain_id !== ''\n ? { domainId: project.domain_id }\n : {}),\n })\n }\n return projects\n}\n\n/**\n * Read the account (domain) name, best-effort.\n *\n * @returns the name, or `undefined` — never an error. Some AK/SK scopes do not\n * grant `/v3/auth/domains`, and the account name is diagnostic only.\n */\nasync function readAccountName(\n base: string,\n accountId: string,\n credentials: SignCredentials,\n options: VerifyOptions,\n): Promise<string | undefined> {\n let status: number\n let body: string\n try {\n ({ status, body } = await signedGet(base, '/v3/auth/domains', credentials, options))\n } catch {\n return undefined\n }\n if (status !== 200) return undefined\n try {\n const parsed = JSON.parse(body) as { domains?: Array<{ id?: unknown, name?: unknown }> }\n const match = parsed.domains?.find(domain => domain.id === accountId)\n return typeof match?.name === 'string' && match.name !== '' ? match.name : undefined\n } catch {\n return undefined\n }\n}\n\n/**\n * Render an IAM rejection into one actionable sentence.\n *\n * @param status - the HTTP status.\n * @param body - the response body, which may carry Huawei's own error text.\n */\nfunction describeIamFailure(status: number, body: string): string {\n try {\n const parsed = JSON.parse(body) as { error_msg?: unknown, error_code?: unknown, message?: unknown }\n const detail = typeof parsed.error_msg === 'string'\n ? parsed.error_msg\n : typeof parsed.message === 'string' ? parsed.message : undefined\n if (detail !== undefined) {\n const code = typeof parsed.error_code === 'string' ? `, ${parsed.error_code}` : ''\n return `Huawei Cloud rejected the credentials (HTTP ${status}${code}): ${detail}`\n }\n } catch {\n // Non-JSON body — an ingress page or a proxy error; nothing to read.\n }\n if (status === 401) {\n return 'Huawei Cloud rejected the credentials (HTTP 401): the Access Key, Secret Key, '\n + 'or Security Token was refused.'\n }\n if (status === 403) {\n return 'The credentials are valid but may not read IAM (HTTP 403). '\n + 'Confirm the Access Key has IAM read permission.'\n }\n return `Huawei Cloud IAM failed with HTTP ${status}: ${body.slice(0, 200)}`\n}"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@huaweidevtools/aishell-dsh-credentials-store",
|
|
3
3
|
"description": "Credential storage + SDK-HMAC-SHA256 signing for aishell DSH plugins",
|
|
4
|
-
"version": "0.0.1-beta.
|
|
4
|
+
"version": "0.0.1-beta.3",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -21,19 +21,32 @@
|
|
|
21
21
|
"./signer": {
|
|
22
22
|
"types": "./lib/signer.d.ts",
|
|
23
23
|
"default": "./lib/signer.js"
|
|
24
|
+
},
|
|
25
|
+
"./accounts": {
|
|
26
|
+
"types": "./lib/accounts.d.ts",
|
|
27
|
+
"default": "./lib/accounts.js"
|
|
28
|
+
},
|
|
29
|
+
"./verify": {
|
|
30
|
+
"types": "./lib/verify.d.ts",
|
|
31
|
+
"default": "./lib/verify.js"
|
|
32
|
+
},
|
|
33
|
+
"./scope": {
|
|
34
|
+
"types": "./lib/scope.d.ts",
|
|
35
|
+
"default": "./lib/scope.js"
|
|
36
|
+
},
|
|
37
|
+
"./paths": {
|
|
38
|
+
"types": "./lib/paths.d.ts",
|
|
39
|
+
"default": "./lib/paths.js"
|
|
24
40
|
}
|
|
25
41
|
},
|
|
26
42
|
"files": [
|
|
27
|
-
"lib
|
|
28
|
-
"lib
|
|
29
|
-
"lib
|
|
30
|
-
"lib/signer.d.ts",
|
|
31
|
-
"lib/index.js.map",
|
|
32
|
-
"lib/index.d.ts.map"
|
|
43
|
+
"lib/**/*.js",
|
|
44
|
+
"lib/**/*.d.ts",
|
|
45
|
+
"lib/**/*.map"
|
|
33
46
|
],
|
|
34
47
|
"license": "Apache-2.0",
|
|
35
48
|
"dependencies": {
|
|
36
|
-
"@napi-rs/keyring": "^2.
|
|
49
|
+
"@napi-rs/keyring": "^2.1.1",
|
|
37
50
|
"node-machine-id": "^1.1.12"
|
|
38
51
|
},
|
|
39
52
|
"scripts": {
|
package/lib/index.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/backends/keyring.ts","../../src/backends/fingerprint.ts","../../src/backends/memory.ts","../../src/failover.ts","../../src/index.ts"],"sourcesContent":null,"mappings":";;AA+CA,KA1BK,YAAA,GA0BQ;EAAe,WAAA,EAAA,GAAA,GAAA,MAAA,GAAA,IAAA;EAAA,WAIN,EAAA,CAAA,KAAA,EAAA,MAAA,EAAA,GAAA,IAAA;EAA2B,cAJV,EAAA,GAAA,GAAA,IAAA;AAAgB,CAAA;KArBlD,aAAA;mDAAmE;;;iBAIxD,kBAAA,OAAyB;;;;;;;;AC2DzC;AAAgC,cD1CnB,cAAA,YAA0B,gBC0CP,CAAA;EAAA,iBAIV,WAAA;EAA2B,iBAJN,KAAA;EAAgB,WAAA,CAAA,MAAA,CAAA,EDtCrC,sBCsCqC;;;;;AC1E3D,CAAA;;;;;AFgCuD;;;;;;;cC0C1C,kBAAA,YAA8B;;;uBAIrB;;;AAJtB;;;;EAA2D,QAAA,IAAA;;;;;EC1E9C,MAAA,CAAA,OAAA,EAAc,MAAA,CAAA,EAAA,IAAA;EAAA,GAAA,CAAA,OAAA,EAAA,MAAA,CAAA,EAAA,OAAA;CAAA;;;;;AFgCd,cEhCA,aAAA,YAAyB,gBFgCV,CAAA;EAAA,iBAAA,OAAA;EAAA,WAIN,CAAA,OAAA,CAAA,EEjCC,sBFiCD;EAA2B,GAJV,CAAA,OAAA,EAAA,MAAA,CAAA,EAAA,MAAA,GAAA,SAAA;EAAgB,GAAA,CAAA,OAAA,EAAA,MAAA,EAAA,KAAA,EAAA,MAAA,CAAA,EAAA,IAAA;;;;;;;;;;UGqDtC,oBAAA;;;;;;;iCAOgB;;;ACnFjC;;;;;;;;AAuBA;;;;;;cD8Ea,aAAA,YAAyB;;;;;;uBAOf,2BAA2B;;;;;;;;;ACtDlD;EAAsC,QAAA,cAAA;EAAA,GAAS,CAAA,OAAA,EAAA,MAAA,CAAA,EAAA,MAAA,GAAA,SAAA;EAA2B,GAAG,CAAA,OAAA,EAAA,MAAA,CAAA,EAAA,OAAA;EAAgB,GAAA,CAAA,OAAA,EAAA,MAAA,EAAA,KAAA,EAAA,MAAA,CAAA,EAAA,IAAA;;;;;;;;;;;;;;;AJhElB;AAO1D;;AASjB;;;;;;;;;AAiBA;;;;AAAuD;UIvBtC,sBAAA;;;;;;;;;;;;AHiEjB;EAAgC,SAAA,cAAA,CAAA,EAAA,MAAA;;;AAA2B;;;;;AC1E3D;AAA2B,UEgCV,gBAAA,CFhCU;EAAA;EAGuB,GAHZ,CAAA,OAAA,EAAA,MAAA,CAAA,EAAA,MAAA,GAAA,SAAA;EAAgB;;;;;ECqFrC,GAAA,CAAA,OAAA,EAAA,MAAA,CAAA,EAAA,OAAoB;;;;;;;;;;;;;;;;AAyBrC;;;;;AAAsD;iBC/CtC,sBAAA,UAA+B,yBAA8B"}
|