@hulistmi/hulistmi 1.0.0

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/src/index.ts ADDED
@@ -0,0 +1,260 @@
1
+ import { StreamableHTTPTransport } from "@hono/mcp";
2
+ import type { Context } from "hono";
3
+ import { Hono } from "hono";
4
+ import { fetchHarmonyOSCatalog, renderCatalogMarkdown } from "./lib/catalog";
5
+ import {
6
+ assertRenderedMarkdownWithinLimit,
7
+ MAX_MCP_REQUEST_BYTES,
8
+ NotFoundError,
9
+ UpstreamPolicyError,
10
+ UpstreamSizeError,
11
+ } from "./lib/fetch";
12
+ import { fetchGuidePageData, renderGuideMarkdown } from "./lib/guides";
13
+ import { createMcpServer, MCP_SERVER_INFO } from "./lib/mcp";
14
+ import { enforceRateLimit } from "./lib/rate-limit";
15
+ import {
16
+ fetchReferencePageData,
17
+ renderReferenceMarkdown,
18
+ } from "./lib/reference";
19
+ import { renderSearchMarkdown, searchHarmonyOSDocs } from "./lib/search";
20
+ import {
21
+ createSkillIndex,
22
+ loadSkill,
23
+ SKILL_NAME,
24
+ skillHeaders,
25
+ skillIndexHeaders,
26
+ } from "./lib/skill";
27
+ import { buildWebMcpManifest } from "./lib/webmcp";
28
+
29
+ export interface Env {
30
+ ASSETS: Fetcher;
31
+ RATE_LIMITER?: {
32
+ limit(options: { key: string }): Promise<{ success: boolean }>;
33
+ };
34
+ }
35
+
36
+ const app = new Hono<{ Bindings: Env }>();
37
+ const ROBOTS_HEADER = "noindex, nofollow, noarchive";
38
+ const DOC_CACHE = "public, max-age=3600, s-maxage=86400";
39
+ const SHORT_CACHE = "public, max-age=300, s-maxage=600";
40
+
41
+ function origin(c: Context): string {
42
+ return new URL(c.req.url).origin;
43
+ }
44
+
45
+ function wantsJson(c: Context): boolean {
46
+ return c.req.header("Accept")?.includes("application/json") ?? false;
47
+ }
48
+
49
+ function setNoIndex(c: Context, cacheControl: string): void {
50
+ c.header("X-Robots-Tag", ROBOTS_HEADER);
51
+ c.header("Cache-Control", cacheControl);
52
+ c.header("Vary", "Accept");
53
+ }
54
+
55
+ async function sha256(text: string): Promise<string> {
56
+ const data = new TextEncoder().encode(text);
57
+ const digest = await crypto.subtle.digest("SHA-256", data);
58
+ return `"${Array.from(new Uint8Array(digest))
59
+ .map((byte) => byte.toString(16).padStart(2, "0"))
60
+ .join("")}"`;
61
+ }
62
+
63
+ async function assertMcpBodyWithinLimit(
64
+ request: Request,
65
+ ): Promise<Response | null> {
66
+ if (request.method === "GET" || request.method === "HEAD") return null;
67
+ const declared = Number(request.headers.get("Content-Length") ?? "NaN");
68
+ if (!Number.isFinite(declared) || declared > MAX_MCP_REQUEST_BYTES)
69
+ return bodyTooLarge();
70
+ const reader = request.clone().body?.getReader();
71
+ if (!reader) return null;
72
+ let total = 0;
73
+ while (true) {
74
+ const { done, value } = await reader.read();
75
+ if (done) break;
76
+ total += value.byteLength;
77
+ if (total > MAX_MCP_REQUEST_BYTES) {
78
+ await reader.cancel();
79
+ return bodyTooLarge();
80
+ }
81
+ }
82
+ return null;
83
+ }
84
+
85
+ function bodyTooLarge(): Response {
86
+ return new Response(JSON.stringify({ error: "Request body too large" }), {
87
+ status: 413,
88
+ headers: {
89
+ "Content-Type": "application/json",
90
+ "Cache-Control": "no-store",
91
+ },
92
+ });
93
+ }
94
+
95
+ async function renderDocument(
96
+ c: Context,
97
+ catalogName: "harmonyos-guides" | "harmonyos-references",
98
+ path: string,
99
+ ): Promise<Response> {
100
+ const data =
101
+ catalogName === "harmonyos-guides"
102
+ ? await fetchGuidePageData(path)
103
+ : await fetchReferencePageData(path);
104
+ const content =
105
+ catalogName === "harmonyos-guides"
106
+ ? renderGuideMarkdown(data, path)
107
+ : renderReferenceMarkdown(data, path);
108
+ const bounded = assertRenderedMarkdownWithinLimit(content);
109
+ const sourceUrl = `https://developer.huawei.com/consumer/en/doc/${catalogName}/${path}`;
110
+ setNoIndex(c, DOC_CACHE);
111
+ c.header("Content-Location", sourceUrl);
112
+ c.header("ETag", await sha256(bounded));
113
+ if (wantsJson(c)) return c.json({ url: sourceUrl, content: bounded });
114
+ return c.text(bounded, 200, {
115
+ "Content-Type": "text/markdown; charset=utf-8",
116
+ });
117
+ }
118
+
119
+ async function publicLimit(c: Context, next: () => Promise<void>) {
120
+ const blocked = await enforceRateLimit(c.req.raw, c.env);
121
+ if (blocked) return blocked;
122
+ await next();
123
+ }
124
+
125
+ app.use("/search", publicLimit);
126
+ app.use("/catalog", publicLimit);
127
+ app.use("/mcp", publicLimit);
128
+ app.use("/consumer/en/doc/harmonyos-guides/*", publicLimit);
129
+ app.use("/consumer/en/doc/harmonyos-references/*", publicLimit);
130
+
131
+ app.get("/", async (c) =>
132
+ c.env.ASSETS.fetch(new Request(new URL("/index.html", c.req.url))),
133
+ );
134
+
135
+ app.get("/bot", (c) =>
136
+ c.text(
137
+ "hulistmi.ai uses transparent, on-demand requests for HarmonyOS documentation and identifies itself with hulistmi-ai/1.0 (+https://hulistmi-ai.y6vd2dkjgb.workers.dev/#bot).",
138
+ 200,
139
+ {
140
+ "Content-Type": "text/plain; charset=utf-8",
141
+ "Cache-Control": SHORT_CACHE,
142
+ },
143
+ ),
144
+ );
145
+
146
+ app.get("/consumer/en/doc/harmonyos-guides/:path{.+}", async (c) =>
147
+ renderDocument(c, "harmonyos-guides", c.req.param("path")),
148
+ );
149
+ app.get("/consumer/en/doc/harmonyos-references/:path{.+}", async (c) =>
150
+ renderDocument(c, "harmonyos-references", c.req.param("path")),
151
+ );
152
+
153
+ app.get("/catalog", async (c) => {
154
+ const catalogName = c.req.query("catalogName") ?? "harmonyos-guides";
155
+ const language = c.req.query("language") ?? "en";
156
+ const depthRaw = c.req.query("depth");
157
+ const depth = depthRaw ? Number(depthRaw) : undefined;
158
+ if (
159
+ !["harmonyos-guides", "harmonyos-references"].includes(catalogName) ||
160
+ language !== "en" ||
161
+ (depth !== undefined && (!Number.isFinite(depth) || depth < 1))
162
+ )
163
+ return c.json({ error: "Unsupported catalog" }, 400);
164
+ const catalog = await fetchHarmonyOSCatalog(catalogName, language);
165
+ setNoIndex(c, SHORT_CACHE);
166
+ if (wantsJson(c)) return c.json(catalog);
167
+ return c.text(
168
+ assertRenderedMarkdownWithinLimit(renderCatalogMarkdown(catalog, depth)),
169
+ 200,
170
+ { "Content-Type": "text/markdown; charset=utf-8" },
171
+ );
172
+ });
173
+
174
+ app.get("/search", async (c) => {
175
+ const query = c.req.query("q") ?? "";
176
+ if (!query.trim() || query.length > 120)
177
+ return c.json({ error: "Invalid search query" }, 400);
178
+ const result = await searchHarmonyOSDocs(query);
179
+ setNoIndex(c, SHORT_CACHE);
180
+ if (wantsJson(c)) return c.json(result);
181
+ return c.text(
182
+ assertRenderedMarkdownWithinLimit(renderSearchMarkdown(result)),
183
+ 200,
184
+ { "Content-Type": "text/markdown; charset=utf-8" },
185
+ );
186
+ });
187
+
188
+ app.get("/.well-known/mcp/server-card.json", (c) =>
189
+ c.json({
190
+ serverInfo: MCP_SERVER_INFO,
191
+ endpoint: `${origin(c)}/mcp`,
192
+ transports: ["streamable-http"],
193
+ }),
194
+ );
195
+
196
+ app.get("/webmcp/manifest.json", (c) => c.json(buildWebMcpManifest(origin(c))));
197
+
198
+ app.get("/.well-known/api-catalog", (c) =>
199
+ c.json(
200
+ {
201
+ linkset: [
202
+ {
203
+ anchor: `${origin(c)}/mcp`,
204
+ item: [
205
+ {
206
+ href: `${origin(c)}/.well-known/mcp/server-card.json`,
207
+ rel: "service-desc",
208
+ },
209
+ { href: `${origin(c)}/webmcp/manifest.json`, rel: "manifest" },
210
+ ],
211
+ },
212
+ ],
213
+ },
214
+ 200,
215
+ {
216
+ "Content-Type": "application/linkset+json; charset=utf-8",
217
+ "Cache-Control": SHORT_CACHE,
218
+ },
219
+ ),
220
+ );
221
+
222
+ app.get("/.well-known/agent-skills/index.json", async (c) => {
223
+ const skill = await loadSkill(c.env.ASSETS, origin(c));
224
+ return c.json(await createSkillIndex(skill), 200, skillIndexHeaders);
225
+ });
226
+
227
+ app.get(`/.well-known/agent-skills/${SKILL_NAME}/SKILL.md`, async (c) => {
228
+ const skill = await loadSkill(c.env.ASSETS, origin(c));
229
+ return new Response(skill.bytes, { headers: skillHeaders });
230
+ });
231
+
232
+ app.all("/mcp", async (c) => {
233
+ const tooLarge = await assertMcpBodyWithinLimit(c.req.raw);
234
+ if (tooLarge) return tooLarge;
235
+ const mcpServer = createMcpServer();
236
+ const transport = new StreamableHTTPTransport();
237
+ await mcpServer.connect(transport);
238
+ return transport.handleRequest(c);
239
+ });
240
+
241
+ app.onError((err, c) => {
242
+ c.header("Cache-Control", "no-store");
243
+ c.header("X-Robots-Tag", ROBOTS_HEADER);
244
+ if (err instanceof NotFoundError) return c.json({ error: "Not found" }, 404);
245
+ if (err instanceof UpstreamPolicyError)
246
+ return c.json(
247
+ { error: "Upstream policy prevents rendering this content" },
248
+ 502,
249
+ );
250
+ if (err instanceof UpstreamSizeError)
251
+ return c.json({ error: "Upstream content is too large" }, 502);
252
+ if (
253
+ err instanceof Error &&
254
+ /invalid|unsupported|required|too long/i.test(err.message)
255
+ )
256
+ return c.json({ error: err.message }, 400);
257
+ return c.json({ error: "Unable to render HarmonyOS documentation" }, 502);
258
+ });
259
+
260
+ export default app;
@@ -0,0 +1,113 @@
1
+ import { fetchHuaweiJson, NotFoundError, UpstreamSizeError } from "./fetch";
2
+ import { UPSTREAM_CONTRACT } from "./upstream-contract";
3
+
4
+ const MAX_CATALOG_ITEMS = 20_000;
5
+ const MAX_CATALOG_UPSTREAM_BYTES = 5_000_000;
6
+ const SUPPORTED_CATALOGS = new Set([
7
+ "harmonyos-guides",
8
+ "harmonyos-references",
9
+ ]);
10
+
11
+ export interface HarmonyCatalogItem {
12
+ title: string;
13
+ path?: string;
14
+ children: HarmonyCatalogItem[];
15
+ }
16
+
17
+ export interface HarmonyCatalog {
18
+ catalogName: string;
19
+ language: "en";
20
+ items: HarmonyCatalogItem[];
21
+ }
22
+
23
+ export async function fetchHarmonyOSCatalog(
24
+ catalogName: string,
25
+ language = "en",
26
+ ): Promise<HarmonyCatalog> {
27
+ if (!SUPPORTED_CATALOGS.has(catalogName))
28
+ throw new NotFoundError("Unsupported HarmonyOS catalog");
29
+ if (language !== "en")
30
+ throw new Error("Unsupported HarmonyOS catalog language");
31
+ const request =
32
+ UPSTREAM_CONTRACT.catalogs[
33
+ catalogName as keyof typeof UPSTREAM_CONTRACT.catalogs
34
+ ].request;
35
+ const data = await fetchHuaweiJson<{
36
+ code: number | string;
37
+ value?: unknown;
38
+ }>(request, MAX_CATALOG_UPSTREAM_BYTES);
39
+ if (data.code !== 0 && data.code !== "0")
40
+ throw new Error("Huawei catalog response changed shape");
41
+ const items = normalizeCatalogItems(data.value);
42
+ if (countItems(items) > MAX_CATALOG_ITEMS)
43
+ throw new UpstreamSizeError("Huawei catalog exceeded maximum item count");
44
+ return { catalogName, language: "en", items };
45
+ }
46
+
47
+ function normalizeCatalogItems(value: unknown): HarmonyCatalogItem[] {
48
+ const source = Array.isArray(value)
49
+ ? value
50
+ : isRecord(value) && Array.isArray(value.catalogTreeList)
51
+ ? value.catalogTreeList
52
+ : [];
53
+ if (!Array.isArray(source)) return [];
54
+ return source.map((item) => {
55
+ const record = item as Record<string, unknown>;
56
+ return {
57
+ title: String(
58
+ record.nodeName ??
59
+ record.title ??
60
+ record.name ??
61
+ record.label ??
62
+ "Untitled",
63
+ ),
64
+ path:
65
+ typeof record.relateDocument === "string"
66
+ ? record.relateDocument
67
+ : typeof record.fileName === "string"
68
+ ? record.fileName
69
+ : undefined,
70
+ children: normalizeCatalogItems(record.children ?? record.childList),
71
+ };
72
+ });
73
+ }
74
+
75
+ function isRecord(value: unknown): value is Record<string, unknown> {
76
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
77
+ }
78
+
79
+ function countItems(items: HarmonyCatalogItem[]): number {
80
+ return items.reduce(
81
+ (count, item) => count + 1 + countItems(item.children),
82
+ 0,
83
+ );
84
+ }
85
+
86
+ function renderTreeItems(
87
+ items: HarmonyCatalogItem[],
88
+ depth: number,
89
+ maxDepth: number | undefined,
90
+ ): string[] {
91
+ if (!items.length || (maxDepth !== undefined && depth > maxDepth)) return [];
92
+ const lines: string[] = [];
93
+ for (const item of items) {
94
+ const indent = " ".repeat(depth - 1);
95
+ lines.push(`${indent}- ${item.title}${item.path ? `: ${item.path}` : ""}`);
96
+ if (item.children.length)
97
+ lines.push(...renderTreeItems(item.children, depth + 1, maxDepth));
98
+ }
99
+ return lines;
100
+ }
101
+
102
+ export function renderCatalogMarkdown(
103
+ catalog: HarmonyCatalog,
104
+ maxDepth?: number,
105
+ ): string {
106
+ const title =
107
+ catalog.catalogName === "harmonyos-guides"
108
+ ? "HarmonyOS Guides Catalog"
109
+ : "HarmonyOS References Catalog";
110
+ const lines = [`# ${title}`, ""];
111
+ lines.push(...renderTreeItems(catalog.items, 1, maxDepth));
112
+ return lines.join("\n");
113
+ }
@@ -0,0 +1,62 @@
1
+ import { huaweiUrlToPath, normalizeDocsPath } from "./url";
2
+
3
+ export type CliCommand =
4
+ | { command: "fetch"; input: string; json: boolean }
5
+ | { command: "search"; query: string; json: boolean }
6
+ | { command: "serve"; port?: number };
7
+
8
+ const CLI_DOC_PREFIX = "consumer/en/doc/";
9
+
10
+ export function resolveFetchEndpoint(input: string): string {
11
+ const trimmed = input.trim();
12
+ if (!trimmed) throw new Error("Fetch input cannot be empty");
13
+ let path: string;
14
+ if (/^https?:\/\//i.test(trimmed)) {
15
+ path = huaweiUrlToPath(trimmed);
16
+ } else {
17
+ path = normalizeDocsPath(trimmed);
18
+ }
19
+ // Strip the full prefix if already present
20
+ if (path.startsWith(CLI_DOC_PREFIX)) {
21
+ path = path.slice(CLI_DOC_PREFIX.length);
22
+ }
23
+ // Only add prefix for known HarmonyOS doc paths
24
+ if (
25
+ path.startsWith("harmonyos-guides/") ||
26
+ path.startsWith("harmonyos-references/")
27
+ ) {
28
+ return `/${CLI_DOC_PREFIX}${path}`;
29
+ }
30
+ return `/${path}`;
31
+ }
32
+
33
+ export function resolveSearchEndpoint(query: string): string {
34
+ const trimmed = query.trim();
35
+ if (!trimmed) throw new Error("Search query cannot be empty");
36
+ return `/search?q=${encodeURIComponent(trimmed)}`;
37
+ }
38
+
39
+ export function parseCliArgs(argv: string[]): CliCommand {
40
+ const [command, ...rest] = argv;
41
+ const json = rest.includes("--json");
42
+ const values = rest.filter((value) => value !== "--json");
43
+
44
+ if (command === "fetch") {
45
+ const input = values[0];
46
+ if (!input) throw new Error("Usage: hulistmi fetch <url-or-path> [--json]");
47
+ return { command, input, json };
48
+ }
49
+ if (command === "search") {
50
+ const query = values.join(" ");
51
+ if (!query) throw new Error("Usage: hulistmi search <query> [--json]");
52
+ return { command, query, json };
53
+ }
54
+ if (command === "serve") {
55
+ const portFlag = values.indexOf("--port");
56
+ const port = portFlag >= 0 ? Number(values[portFlag + 1]) : undefined;
57
+ return { command, port };
58
+ }
59
+ throw new Error(
60
+ "Usage: hulistmi fetch <url-or-path> [--json] | hulistmi search <query> [--json] | hulistmi serve [--port 8787]",
61
+ );
62
+ }
@@ -0,0 +1,164 @@
1
+ import {
2
+ fetchHuaweiJson,
3
+ NotFoundError,
4
+ type VerifiedHuaweiRequest,
5
+ } from "./fetch";
6
+ import type { HarmonyDocumentResponse, HarmonyDocumentValue } from "./types";
7
+ import { UPSTREAM_CONTRACT } from "./upstream-contract";
8
+
9
+ interface DocumentContractEntry {
10
+ checkCenterGrayUser: VerifiedHuaweiRequest;
11
+ getDocumentById: VerifiedHuaweiRequest;
12
+ getCenterRootNodeTree?: VerifiedHuaweiRequest;
13
+ getCenterDocument?: VerifiedHuaweiRequest;
14
+ }
15
+
16
+ interface GrayUserResponse {
17
+ code: number | string;
18
+ value?: {
19
+ isGrayUser?: number | boolean;
20
+ centerPrefix?: string;
21
+ level2NodeAlias?: string;
22
+ isApi?: number | boolean;
23
+ filename?: string;
24
+ };
25
+ }
26
+
27
+ const DOCUMENTS = UPSTREAM_CONTRACT.documents as Record<
28
+ string,
29
+ DocumentContractEntry
30
+ >;
31
+ const DOCUMENT_SLUG_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
32
+ const GRAY_ID = "11111111111111111111111111111111";
33
+ const CHECK_CENTER_GRAY_USER_URL =
34
+ "https://svc-drcn.developer.huawei.com/community/servlet/consumer/cn/documentPortal/checkCenterGrayUser";
35
+ const GET_DOCUMENT_BY_ID_URL =
36
+ "https://svc-drcn.developer.huawei.com/community/servlet/consumer/cn/documentPortal/getDocumentById";
37
+ const GET_CENTER_ROOT_NODE_TREE_URL =
38
+ "https://svc-drcn.developer.huawei.com/community/servlet/consumer/cn/documentPortal/getCenterRootNodeTree";
39
+ const GET_CENTER_DOCUMENT_URL =
40
+ "https://svc-drcn.developer.huawei.com/community/servlet/consumer/cn/documentPortal/getCenterDocument";
41
+
42
+ export async function fetchHarmonyDocumentPageData(
43
+ catalogName: "harmonyos-guides" | "harmonyos-references",
44
+ path: string,
45
+ ): Promise<HarmonyDocumentValue> {
46
+ const entry =
47
+ DOCUMENTS[documentKey(catalogName, path)] ?? buildEntry(catalogName, path);
48
+
49
+ const grayResponse = await fetchHuaweiJson<GrayUserResponse>(
50
+ entry.checkCenterGrayUser,
51
+ );
52
+ if (isCenterDocument(grayResponse)) {
53
+ const centerRequests =
54
+ entry.getCenterRootNodeTree && entry.getCenterDocument
55
+ ? {
56
+ getCenterRootNodeTree: entry.getCenterRootNodeTree,
57
+ getCenterDocument: entry.getCenterDocument,
58
+ }
59
+ : buildCenterRequests(grayResponse, path);
60
+ await fetchHuaweiJson(centerRequests.getCenterRootNodeTree);
61
+ return fetchAndValidateDocument(centerRequests.getCenterDocument);
62
+ }
63
+
64
+ return fetchAndValidateDocument(entry.getDocumentById);
65
+ }
66
+
67
+ function documentKey(
68
+ catalogName: "harmonyos-guides" | "harmonyos-references",
69
+ path: string,
70
+ ): string {
71
+ return `${catalogName}/${normalizeDocumentSlug(path)}`;
72
+ }
73
+
74
+ function buildEntry(
75
+ catalogName: "harmonyos-guides" | "harmonyos-references",
76
+ path: string,
77
+ ): DocumentContractEntry {
78
+ const slug = normalizeDocumentSlug(path);
79
+ if (!DOCUMENT_SLUG_PATTERN.test(slug))
80
+ throw new NotFoundError(`Unknown HarmonyOS document: ${path}`);
81
+
82
+ return {
83
+ checkCenterGrayUser: {
84
+ url: CHECK_CENTER_GRAY_USER_URL,
85
+ headers: {},
86
+ body: {
87
+ catalogName,
88
+ language: "en",
89
+ fileName: slug,
90
+ grayId: GRAY_ID,
91
+ },
92
+ },
93
+ getDocumentById: {
94
+ url: GET_DOCUMENT_BY_ID_URL,
95
+ headers: {},
96
+ body: {
97
+ objectId: slug,
98
+ nodeAlias: null,
99
+ catalogName,
100
+ language: "en",
101
+ },
102
+ },
103
+ };
104
+ }
105
+
106
+ function normalizeDocumentSlug(path: string): string {
107
+ return path.replace(/^\/+/, "").replace(/\/+$/, "").toLowerCase();
108
+ }
109
+
110
+ function isCenterDocument(response: GrayUserResponse): boolean {
111
+ return (
112
+ (response.code === 0 || response.code === "0") &&
113
+ Boolean(response.value?.isGrayUser)
114
+ );
115
+ }
116
+
117
+ function buildCenterRequests(
118
+ response: GrayUserResponse,
119
+ path: string,
120
+ ): Pick<DocumentContractEntry, "getCenterRootNodeTree" | "getCenterDocument"> {
121
+ const value = response.value;
122
+ const fileName = value?.filename ?? normalizeDocumentSlug(path);
123
+ if (
124
+ !value?.centerPrefix ||
125
+ !value.level2NodeAlias ||
126
+ !DOCUMENT_SLUG_PATTERN.test(fileName)
127
+ )
128
+ throw new Error("HarmonyOS center document response changed shape");
129
+
130
+ const body = {
131
+ centerPrefix: value.centerPrefix,
132
+ language: "en",
133
+ level2NodeAlias: value.level2NodeAlias,
134
+ isApi: value.isApi ? 1 : 0,
135
+ fileName,
136
+ };
137
+ return {
138
+ getCenterRootNodeTree: {
139
+ url: GET_CENTER_ROOT_NODE_TREE_URL,
140
+ headers: {},
141
+ body,
142
+ },
143
+ getCenterDocument: {
144
+ url: GET_CENTER_DOCUMENT_URL,
145
+ headers: {},
146
+ body,
147
+ },
148
+ };
149
+ }
150
+
151
+ async function fetchAndValidateDocument(
152
+ request: VerifiedHuaweiRequest,
153
+ ): Promise<HarmonyDocumentValue> {
154
+ const response = await fetchHuaweiJson<HarmonyDocumentResponse>(request);
155
+ if (response.code !== 0 && response.code !== "0")
156
+ throw new Error("HarmonyOS document response changed shape");
157
+ if (
158
+ !response.value ||
159
+ response.value.status !== "4" ||
160
+ !response.value.content?.content
161
+ )
162
+ throw new Error("HarmonyOS document content is unavailable");
163
+ return response.value;
164
+ }