gaoding-cli 1.0.0-alpha.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +38 -0
  3. package/contracts/operations/agent.send/input.schema.json +354 -0
  4. package/contracts/operations/agent.send/output.schema.json +271 -0
  5. package/contracts/operations/auth.status/input.schema.json +7 -0
  6. package/contracts/operations/auth.status/output.schema.json +132 -0
  7. package/contracts/operations/dam.delete/input.schema.json +23 -0
  8. package/contracts/operations/dam.delete/output.schema.json +6 -0
  9. package/contracts/operations/dam.get/input.schema.json +16 -0
  10. package/contracts/operations/dam.get/output.schema.json +46 -0
  11. package/contracts/operations/dam.list/input.schema.json +44 -0
  12. package/contracts/operations/dam.list/output.schema.json +71 -0
  13. package/contracts/operations/dam.search/input.schema.json +46 -0
  14. package/contracts/operations/dam.search/output.schema.json +71 -0
  15. package/contracts/operations/dam.upload/input.schema.json +31 -0
  16. package/contracts/operations/dam.upload/output.schema.json +46 -0
  17. package/contracts/operations/editor.apply/input.schema.json +134 -0
  18. package/contracts/operations/editor.apply/output.schema.json +15 -0
  19. package/contracts/operations/editor.connect/input.schema.json +15 -0
  20. package/contracts/operations/editor.connect/output.schema.json +17 -0
  21. package/contracts/operations/editor.disconnect/input.schema.json +8 -0
  22. package/contracts/operations/editor.disconnect/output.schema.json +14 -0
  23. package/contracts/operations/editor.save/input.schema.json +8 -0
  24. package/contracts/operations/editor.save/output.schema.json +23 -0
  25. package/contracts/operations/editor.screenshot/input.schema.json +8 -0
  26. package/contracts/operations/editor.screenshot/output.schema.json +16 -0
  27. package/contracts/operations/editor.snapshot/input.schema.json +8 -0
  28. package/contracts/operations/editor.snapshot/output.schema.json +113 -0
  29. package/contracts/operations/model.get/input.schema.json +18 -0
  30. package/contracts/operations/model.get/output.schema.json +190 -0
  31. package/contracts/operations/model.list/input.schema.json +15 -0
  32. package/contracts/operations/model.list/output.schema.json +111 -0
  33. package/contracts/operations/org.current/input.schema.json +7 -0
  34. package/contracts/operations/org.current/output.schema.json +49 -0
  35. package/contracts/operations/org.list/input.schema.json +7 -0
  36. package/contracts/operations/org.list/output.schema.json +71 -0
  37. package/contracts/operations/tool.call/input.schema.json +35 -0
  38. package/contracts/operations/tool.call/output.schema.json +129 -0
  39. package/contracts/operations/tool.list/input.schema.json +7 -0
  40. package/contracts/operations/tool.list/output.schema.json +45 -0
  41. package/dist/bin/gd-cli.js +38 -0
  42. package/dist/bin/postinstall.js +42 -0
  43. package/dist/src/bootstrap/create-cli.js +55 -0
  44. package/dist/src/bootstrap/create-runtime.js +222 -0
  45. package/dist/src/bootstrap/validators.js +118 -0
  46. package/dist/src/cli/action-binding.js +63 -0
  47. package/dist/src/cli/agent-commands.js +39 -0
  48. package/dist/src/cli/auth-commands.js +47 -0
  49. package/dist/src/cli/dam-commands.js +207 -0
  50. package/dist/src/cli/editor-commands.js +73 -0
  51. package/dist/src/cli/errors.js +90 -0
  52. package/dist/src/cli/model-commands.js +47 -0
  53. package/dist/src/cli/org-commands.js +52 -0
  54. package/dist/src/cli/presenter.js +80 -0
  55. package/dist/src/cli/prompt.js +30 -0
  56. package/dist/src/cli/tool-commands.js +65 -0
  57. package/dist/src/cli/update-command.js +12 -0
  58. package/dist/src/contracts/schema.js +4 -0
  59. package/dist/src/features/agent/creative-agent-adapter.js +31 -0
  60. package/dist/src/features/agent/creative-protocol.js +324 -0
  61. package/dist/src/features/agent/creative-stream.js +127 -0
  62. package/dist/src/features/agent/use-cases.js +113 -0
  63. package/dist/src/features/auth/access-policy.js +101 -0
  64. package/dist/src/features/auth/credential-store.js +62 -0
  65. package/dist/src/features/auth/sso-service.js +179 -0
  66. package/dist/src/features/auth/state.js +129 -0
  67. package/dist/src/features/auth/use-cases.js +106 -0
  68. package/dist/src/features/dam/asset-projection.js +270 -0
  69. package/dist/src/features/dam/dam-api-adapter.js +206 -0
  70. package/dist/src/features/dam/object-storage.js +191 -0
  71. package/dist/src/features/dam/registered-uploader.js +224 -0
  72. package/dist/src/features/dam/storage-upload.js +141 -0
  73. package/dist/src/features/dam/transient-uploader.js +17 -0
  74. package/dist/src/features/dam/use-cases.js +151 -0
  75. package/dist/src/features/editor/bridge-client.js +96 -0
  76. package/dist/src/features/editor/bridge-process.js +222 -0
  77. package/dist/src/features/editor/bridge-server.js +311 -0
  78. package/dist/src/features/editor/protocol.js +1 -0
  79. package/dist/src/features/editor/session-state.js +78 -0
  80. package/dist/src/features/editor/session.js +193 -0
  81. package/dist/src/features/editor/use-cases.js +61 -0
  82. package/dist/src/features/org/org-service.js +76 -0
  83. package/dist/src/features/org/use-cases.js +140 -0
  84. package/dist/src/features/skill/bundled-skills.js +55 -0
  85. package/dist/src/features/skill/installer.js +265 -0
  86. package/dist/src/features/tool/catalog.js +79 -0
  87. package/dist/src/features/tool/dynamic-schema.js +96 -0
  88. package/dist/src/features/tool/mns-catalog-adapter.js +262 -0
  89. package/dist/src/features/tool/tool-api-adapter.js +200 -0
  90. package/dist/src/features/tool/use-cases.js +126 -0
  91. package/dist/src/features/update/update-service.js +194 -0
  92. package/dist/src/platform/json-input.js +64 -0
  93. package/dist/src/platform/local-json-file.js +59 -0
  94. package/dist/src/platform/open-browser.js +8 -0
  95. package/dist/src/platform/redact.js +80 -0
  96. package/dist/src/platform/safe-upload-file.js +146 -0
  97. package/dist/src/platform/signature.js +19 -0
  98. package/dist/src/platform/signed-http-transport.js +109 -0
  99. package/dist/src/platform/url-safety.js +100 -0
  100. package/package.json +56 -0
  101. package/skills/gd-cli/SKILL.md +15 -0
  102. package/skills/gd-cli/references/auth-org.md +9 -0
  103. package/skills/gd-cli/references/creation.md +38 -0
  104. package/skills/gd-cli/references/dam.md +20 -0
  105. package/skills/gd-cli/references/editor.md +12 -0
  106. package/skills/gd-cli/references/errors.md +10 -0
  107. package/skills/gd-cli/references/update.md +11 -0
@@ -0,0 +1,194 @@
1
+ import { spawn } from "node:child_process";
2
+ import { realpathSync } from "node:fs";
3
+ import { resolve } from "node:path";
4
+ import { gt, valid } from "semver";
5
+ import { syncBundledSkills, verifyBundledSkills } from "../skill/installer.js";
6
+ const REGISTRY_URL = "https://registry.npmjs.org/gaoding-cli";
7
+ export class UpdateError extends Error {
8
+ code;
9
+ nextSteps;
10
+ constructor(code, message, nextSteps) {
11
+ super(message);
12
+ this.code = code;
13
+ this.nextSteps = nextSteps;
14
+ this.name = "UpdateError";
15
+ }
16
+ }
17
+ export function detectInstallSource(input) {
18
+ const binaryPath = normalizePath(input.binaryPath);
19
+ const realBinaryPath = normalizePath(input.realBinaryPath);
20
+ const packageRoot = normalizePath(input.packageRoot);
21
+ const cwd = normalizePath(resolve(input.cwd));
22
+ const userAgent = input.env.npm_config_user_agent ?? "";
23
+ const npmExecPath = normalizePath(input.env.npm_execpath ?? "");
24
+ if (userAgent.startsWith("yarn/") || npmExecPath.includes("/yarn/")) {
25
+ return unsupported("Yarn 安装不支持自动更新");
26
+ }
27
+ if (containsTemporaryRunner(binaryPath) || containsTemporaryRunner(packageRoot)) {
28
+ return unsupported("临时运行方式不支持自动更新");
29
+ }
30
+ if (binaryPath.includes("/node_modules/.bin/")
31
+ || packageRoot.startsWith(`${cwd}/node_modules/`)) {
32
+ return unsupported("项目依赖不支持自动更新");
33
+ }
34
+ if (!isWithin(realBinaryPath, packageRoot)) {
35
+ return unsupported("CLI binary 与 package 不匹配");
36
+ }
37
+ if (/\/global\/[^/]+\/\.pnpm\/gaoding-cli@[^/]+\/node_modules\/gaoding-cli$/u
38
+ .test(packageRoot)
39
+ || /\/v\d+\/[0-9a-f]+-[0-9a-f]+-[0-9a-f]{16}\/node_modules\/gaoding-cli$/u
40
+ .test(packageRoot)
41
+ || /\/v\d+\/[0-9a-f]+-[0-9a-f]+-[0-9a-f]{16}\/node_modules\/gaoding-cli\/dist\/bin\/gd-cli\.js$/u
42
+ .test(binaryPath)) {
43
+ return { kind: "global", manager: "pnpm", packageRoot: input.packageRoot };
44
+ }
45
+ if (packageRoot.endsWith("/lib/node_modules/gaoding-cli")
46
+ || packageRoot.endsWith("/npm/node_modules/gaoding-cli")) {
47
+ return { kind: "global", manager: "npm", packageRoot: input.packageRoot };
48
+ }
49
+ return unsupported("无法确认 npm 或 pnpm 全局安装");
50
+ }
51
+ export function createUpdateService(options) {
52
+ const fetchPackage = options.fetch ?? globalThis.fetch;
53
+ const runProcess = options.runProcess ?? spawnProcess;
54
+ const syncSkills = options.syncSkills ?? syncBundledSkills;
55
+ const verifySkills = options.verifySkills ?? verifyBundledSkills;
56
+ const env = options.env ?? process.env;
57
+ return {
58
+ async run(signal) {
59
+ signal.throwIfAborted();
60
+ const latest = await fetchLatest(fetchPackage, signal);
61
+ signal.throwIfAborted();
62
+ const skillOptions = {
63
+ env,
64
+ ...(options.homeDirectory ? { homeDirectory: options.homeDirectory } : {})
65
+ };
66
+ if (!gt(latest, options.currentVersion)) {
67
+ try {
68
+ await syncSkills(options.bundledSkills, skillOptions);
69
+ }
70
+ catch (error) {
71
+ rethrowAbort(signal);
72
+ throw new UpdateError("UPDATE_FAILED", `gd-cli ${options.currentVersion} 无需更新,但 Agent Skill 同步失败。`, ["请解决冲突后重新执行 gd-cli update。"]);
73
+ }
74
+ return { version: options.currentVersion, updated: false };
75
+ }
76
+ const binaryPath = options.binaryPath ?? process.argv[1] ?? "";
77
+ const source = detectInstallSource({
78
+ binaryPath,
79
+ realBinaryPath: safeRealpath(binaryPath),
80
+ packageRoot: options.packageRoot,
81
+ cwd: options.cwd ?? process.cwd(),
82
+ env
83
+ });
84
+ const nextSteps = manualCommands(latest);
85
+ if (source.kind === "unsupported") {
86
+ throw new UpdateError("UPDATE_FAILED", `当前安装方式不支持自动更新:${source.reason}。`, nextSteps);
87
+ }
88
+ const command = source.manager;
89
+ const args = source.manager === "npm"
90
+ ? ["install", "--global", `gaoding-cli@${latest}`]
91
+ : ["add", "--global", "--allow-build=gaoding-cli", `gaoding-cli@${latest}`];
92
+ try {
93
+ await runProcess(command, args, signal);
94
+ }
95
+ catch (error) {
96
+ rethrowAbort(signal);
97
+ throw new UpdateError("UPDATE_FAILED", `gd-cli 更新到 ${latest} 失败。`, nextSteps);
98
+ }
99
+ signal.throwIfAborted();
100
+ const targetSkills = options.bundledSkills.map((skill) => ({
101
+ ...skill,
102
+ version: latest
103
+ }));
104
+ try {
105
+ await verifySkills(targetSkills, skillOptions);
106
+ }
107
+ catch (error) {
108
+ rethrowAbort(signal);
109
+ throw new UpdateError("UPDATE_PARTIAL", `gd-cli 已更新到 ${latest},但 Agent Skill 验收失败。`, ["请重新执行 gd-cli update。"]);
110
+ }
111
+ return { version: latest, updated: true };
112
+ }
113
+ };
114
+ }
115
+ async function fetchLatest(fetchPackage, signal) {
116
+ try {
117
+ const response = await fetchPackage(REGISTRY_URL, {
118
+ headers: { Accept: "application/json" },
119
+ redirect: "error",
120
+ signal
121
+ });
122
+ if (!response.ok)
123
+ throw new Error(`Registry status ${response.status}`);
124
+ const metadata = await response.json();
125
+ if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
126
+ throw new Error("Registry metadata invalid");
127
+ }
128
+ const tags = metadata["dist-tags"];
129
+ if (!tags || typeof tags !== "object" || Array.isArray(tags)) {
130
+ throw new Error("Registry dist-tags missing");
131
+ }
132
+ const latest = tags.latest;
133
+ if (typeof latest !== "string" || valid(latest.trim()) === null) {
134
+ throw new Error("Registry latest missing");
135
+ }
136
+ return latest.trim();
137
+ }
138
+ catch (error) {
139
+ rethrowAbort(signal);
140
+ throw new UpdateError("UPDATE_FAILED", "无法获取 gaoding-cli 最新版本。", ["请稍后重新执行 gd-cli update。"]);
141
+ }
142
+ }
143
+ function spawnProcess(command, args, signal) {
144
+ return new Promise((resolvePromise, reject) => {
145
+ const child = spawn(command, args, {
146
+ stdio: "inherit",
147
+ shell: false,
148
+ signal
149
+ });
150
+ child.once("error", reject);
151
+ child.once("exit", (code, exitSignal) => {
152
+ if (code === 0) {
153
+ resolvePromise();
154
+ }
155
+ else {
156
+ reject(new Error(code === null
157
+ ? `package manager terminated by ${exitSignal ?? "unknown signal"}`
158
+ : `package manager exited with ${code}`));
159
+ }
160
+ });
161
+ });
162
+ }
163
+ function manualCommands(version) {
164
+ return [
165
+ `npm install --global gaoding-cli@${version}`,
166
+ `pnpm add --global --allow-build=gaoding-cli gaoding-cli@${version}`
167
+ ];
168
+ }
169
+ function containsTemporaryRunner(path) {
170
+ return path.includes("/.npm/_npx/")
171
+ || path.includes("/pnpm/dlx/")
172
+ || path.includes("/.pnpm/dlx/");
173
+ }
174
+ function isWithin(path, directory) {
175
+ return path === directory || path.startsWith(`${directory}/`);
176
+ }
177
+ function normalizePath(path) {
178
+ return path.replace(/\\/gu, "/").replace(/\/+$/u, "");
179
+ }
180
+ function safeRealpath(path) {
181
+ try {
182
+ return realpathSync(path);
183
+ }
184
+ catch {
185
+ return path;
186
+ }
187
+ }
188
+ function unsupported(reason) {
189
+ return { kind: "unsupported", reason };
190
+ }
191
+ function rethrowAbort(signal) {
192
+ if (signal.aborted)
193
+ signal.throwIfAborted();
194
+ }
@@ -0,0 +1,64 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { resolve } from "node:path";
3
+ export class JsonInputError extends Error {
4
+ constructor() {
5
+ super("无法读取有效的 JSON。");
6
+ this.name = "JsonInputError";
7
+ }
8
+ }
9
+ export function createJsonInputReader(options) {
10
+ return {
11
+ async read(source, signal) {
12
+ signal.throwIfAborted();
13
+ let text;
14
+ try {
15
+ text = source === "-"
16
+ ? await readStream(options.stdin, signal)
17
+ : await readFile(resolve(options.cwd, source), { encoding: "utf8", signal });
18
+ }
19
+ catch {
20
+ if (signal.aborted)
21
+ throw signal.reason;
22
+ throw new JsonInputError();
23
+ }
24
+ try {
25
+ return JSON.parse(text);
26
+ }
27
+ catch {
28
+ throw new JsonInputError();
29
+ }
30
+ }
31
+ };
32
+ }
33
+ function readStream(stream, signal) {
34
+ return new Promise((resolve, reject) => {
35
+ const chunks = [];
36
+ const onData = (chunk) => {
37
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
38
+ };
39
+ const cleanup = () => {
40
+ stream.removeListener("data", onData);
41
+ stream.removeListener("end", onEnd);
42
+ stream.removeListener("error", onError);
43
+ signal.removeEventListener("abort", onAbort);
44
+ };
45
+ const onEnd = () => {
46
+ cleanup();
47
+ resolve(Buffer.concat(chunks).toString("utf8"));
48
+ };
49
+ const onError = () => {
50
+ cleanup();
51
+ reject(new JsonInputError());
52
+ };
53
+ const onAbort = () => {
54
+ cleanup();
55
+ reject(signal.reason);
56
+ };
57
+ stream.on("data", onData);
58
+ stream.once("end", onEnd);
59
+ stream.once("error", onError);
60
+ signal.addEventListener("abort", onAbort, { once: true });
61
+ if (signal.aborted)
62
+ onAbort();
63
+ });
64
+ }
@@ -0,0 +1,59 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import * as nodeFileSystem from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ export class LocalFileError extends Error {
5
+ constructor(cause) {
6
+ super("无法访问本地状态文件。", { cause });
7
+ this.name = "LocalFileError";
8
+ }
9
+ }
10
+ export function createLocalJsonFile(options) {
11
+ const fileSystem = options.fileSystem ?? nodeFileSystem;
12
+ const path = join(options.directory, options.filename);
13
+ return {
14
+ async readText() {
15
+ try {
16
+ await fileSystem.chmod(options.directory, 0o700);
17
+ await fileSystem.chmod(path, 0o600);
18
+ return await fileSystem.readFile(path, "utf8");
19
+ }
20
+ catch (cause) {
21
+ if (isMissing(cause))
22
+ return null;
23
+ throw new LocalFileError(cause);
24
+ }
25
+ },
26
+ async writeTextAtomically(value) {
27
+ const temporaryPath = join(options.directory, `${options.filename}.tmp-${randomUUID()}`);
28
+ let handle;
29
+ try {
30
+ await fileSystem.mkdir(options.directory, { mode: 0o700, recursive: true });
31
+ await fileSystem.chmod(options.directory, 0o700);
32
+ handle = await fileSystem.open(temporaryPath, "wx", 0o600);
33
+ await handle.writeFile(value, "utf8");
34
+ await handle.sync();
35
+ await handle.close();
36
+ handle = undefined;
37
+ await fileSystem.chmod(temporaryPath, 0o600);
38
+ await fileSystem.rename(temporaryPath, path);
39
+ }
40
+ catch (cause) {
41
+ await handle?.close().catch(() => undefined);
42
+ await fileSystem.unlink(temporaryPath).catch(() => undefined);
43
+ throw new LocalFileError(cause);
44
+ }
45
+ },
46
+ async remove() {
47
+ try {
48
+ await fileSystem.unlink(path);
49
+ }
50
+ catch (cause) {
51
+ if (!isMissing(cause))
52
+ throw new LocalFileError(cause);
53
+ }
54
+ }
55
+ };
56
+ }
57
+ function isMissing(error) {
58
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
59
+ }
@@ -0,0 +1,8 @@
1
+ import open from "open";
2
+ export function createBrowserOpener(openUrl = open) {
3
+ return {
4
+ async open(url) {
5
+ await openUrl(url.href);
6
+ }
7
+ };
8
+ }
@@ -0,0 +1,80 @@
1
+ const REDACTED = "[REDACTED]";
2
+ const SENSITIVE_KEYS = new Set([
3
+ "ak",
4
+ "sk",
5
+ "accesskey",
6
+ "accesskeyid",
7
+ "accesskeysecret",
8
+ "authkey",
9
+ "authorization",
10
+ "authorizecode",
11
+ "authorizationcode",
12
+ "bearer",
13
+ "cookie",
14
+ "contentid",
15
+ "devicecode",
16
+ "difytaskid",
17
+ "orgid",
18
+ "password",
19
+ "secret",
20
+ "secretkey",
21
+ "signature",
22
+ "securitytoken",
23
+ "ststoken",
24
+ "taskid",
25
+ "token",
26
+ "accesstoken",
27
+ "refreshtoken"
28
+ ]);
29
+ export function redact(value) {
30
+ return redactValue(value, new WeakSet());
31
+ }
32
+ export function redactSensitiveText(value) {
33
+ const url = redactUrl(value);
34
+ if (url)
35
+ return url;
36
+ return value
37
+ .replace(/(authorization\s*:\s*bearer\s+)[^\s,;]+/gi, `$1${REDACTED}`)
38
+ .replace(/(cookie\s*:\s*)[^\r\n]+/gi, `$1${REDACTED}`)
39
+ .replace(/\b(ak|sk|access[_-]?key(?:[_-]?(?:id|secret))?|secret[_-]?key|auth[_-]?key|token|access[_-]?token|refresh[_-]?token|security[_-]?token|sts[_-]?token|signature|authorize[_-]?code|authorization[_-]?code|device[_-]?code|org[_-]?id|content[_-]?id|dify[_-]?task[_-]?id|task[_-]?id)\s*[:=]\s*[^\s,;}&]+/gi, `$1=${REDACTED}`);
40
+ }
41
+ function redactValue(value, seen) {
42
+ if (typeof value === "string")
43
+ return redactSensitiveText(value);
44
+ if (value === null || typeof value !== "object")
45
+ return value;
46
+ if (seen.has(value))
47
+ return "[Circular]";
48
+ seen.add(value);
49
+ if (value instanceof Error) {
50
+ return {
51
+ name: value.name,
52
+ message: redactSensitiveText(value.message)
53
+ };
54
+ }
55
+ if (Array.isArray(value))
56
+ return value.map((item) => redactValue(item, seen));
57
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [
58
+ key,
59
+ isSensitiveKey(key) ? REDACTED : redactValue(item, seen)
60
+ ]));
61
+ }
62
+ function redactUrl(value) {
63
+ let url;
64
+ try {
65
+ url = new URL(value);
66
+ }
67
+ catch {
68
+ return undefined;
69
+ }
70
+ if (url.protocol !== "http:" && url.protocol !== "https:")
71
+ return undefined;
72
+ for (const key of [...url.searchParams.keys()]) {
73
+ if (isSensitiveKey(key))
74
+ url.searchParams.set(key, REDACTED);
75
+ }
76
+ return url.href;
77
+ }
78
+ export function isSensitiveKey(key) {
79
+ return SENSITIVE_KEYS.has(key.toLowerCase().replaceAll(/[-_]/g, ""));
80
+ }
@@ -0,0 +1,146 @@
1
+ import { open, realpath, stat } from "node:fs/promises";
2
+ import { basename, extname, sep } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ export class LocalFileInputError extends Error {
5
+ constructor() {
6
+ super("本地文件不可用于上传。");
7
+ this.name = "LocalFileInputError";
8
+ }
9
+ }
10
+ const protectedSegments = new Set([
11
+ ".aws",
12
+ ".azure",
13
+ ".config",
14
+ ".docker",
15
+ ".gd",
16
+ ".gd-cli-dev",
17
+ ".git",
18
+ ".gnupg",
19
+ ".kube",
20
+ ".npm",
21
+ ".ssh"
22
+ ]);
23
+ const protectedFilenames = new Set([
24
+ ".env",
25
+ ".git-credentials",
26
+ ".gitconfig",
27
+ ".npmrc",
28
+ ".netrc",
29
+ ".pypirc",
30
+ "credentials.json",
31
+ "credential.json",
32
+ "id_ed25519",
33
+ "id_rsa",
34
+ "known_hosts"
35
+ ]);
36
+ const mediaTypes = {
37
+ avif: "image/avif",
38
+ bmp: "image/bmp",
39
+ gif: "image/gif",
40
+ heic: "image/heic",
41
+ heif: "image/heif",
42
+ jpeg: "image/jpeg",
43
+ m4v: "video/x-m4v",
44
+ mkv: "video/x-matroska",
45
+ mov: "video/quicktime",
46
+ mp4: "video/mp4",
47
+ png: "image/png",
48
+ svg: "image/svg+xml",
49
+ webm: "video/webm",
50
+ webp: "image/webp"
51
+ };
52
+ function reject() {
53
+ throw new LocalFileInputError();
54
+ }
55
+ function assertSafePath(filePath) {
56
+ const segments = filePath.split(sep).filter(Boolean).map((segment) => segment.toLowerCase());
57
+ const filename = segments.at(-1) ?? "";
58
+ if (protectedFilenames.has(filename) || /^\.env(?:\.|$)/.test(filename))
59
+ reject();
60
+ if (segments.some((segment) => segment.startsWith(".") || protectedSegments.has(segment))) {
61
+ reject();
62
+ }
63
+ }
64
+ function detectedFormat(header) {
65
+ if (header.length >= 3
66
+ && header[0] === 0xff
67
+ && header[1] === 0xd8
68
+ && header[2] === 0xff)
69
+ return "jpeg";
70
+ if (header.length >= 8
71
+ && header.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])))
72
+ return "png";
73
+ if (header.length >= 6) {
74
+ const signature = header.subarray(0, 6).toString("ascii");
75
+ if (signature === "GIF87a" || signature === "GIF89a")
76
+ return "gif";
77
+ }
78
+ if (header.length >= 12
79
+ && header.subarray(0, 4).toString("ascii") === "RIFF"
80
+ && header.subarray(8, 12).toString("ascii") === "WEBP")
81
+ return "webp";
82
+ if (header.length >= 12 && header.subarray(4, 8).toString("ascii") === "ftyp") {
83
+ const brand = header.subarray(8, 12).toString("ascii");
84
+ if (brand === "avif" || brand === "avis")
85
+ return "avif";
86
+ if (["heic", "heix", "hevc", "hevx"].includes(brand))
87
+ return "heic";
88
+ }
89
+ return undefined;
90
+ }
91
+ async function readHeader(filePath, signal) {
92
+ signal.throwIfAborted();
93
+ const handle = await open(filePath, "r");
94
+ try {
95
+ const buffer = Buffer.alloc(32);
96
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
97
+ signal.throwIfAborted();
98
+ return buffer.subarray(0, bytesRead);
99
+ }
100
+ finally {
101
+ await handle.close();
102
+ }
103
+ }
104
+ export async function inspectUploadFile(input) {
105
+ input.signal.throwIfAborted();
106
+ try {
107
+ const requestedPath = fileURLToPath(input.url);
108
+ if (input.allowSensitivePath !== true)
109
+ assertSafePath(requestedPath);
110
+ const filePath = await realpath(requestedPath);
111
+ if (input.allowSensitivePath !== true)
112
+ assertSafePath(filePath);
113
+ const details = await stat(filePath);
114
+ if (!details.isFile())
115
+ return reject();
116
+ const header = await readHeader(filePath, input.signal);
117
+ const detected = detectedFormat(header);
118
+ const extension = extname(requestedPath).slice(1).toLowerCase();
119
+ const format = detected ?? (extension === "jpg" ? "jpeg" : extension);
120
+ if (format === "")
121
+ return reject();
122
+ const detectedMediaType = detected === undefined ? undefined : mediaTypes[detected];
123
+ if (detectedMediaType !== undefined
124
+ && input.claimedMediaType !== undefined
125
+ && input.claimedMediaType.trim().toLowerCase() !== detectedMediaType) {
126
+ return reject();
127
+ }
128
+ return {
129
+ filePath,
130
+ filename: basename(requestedPath),
131
+ format,
132
+ mediaType: detectedMediaType
133
+ ?? input.claimedMediaType
134
+ ?? mediaTypes[format]
135
+ ?? `application/${format}`,
136
+ size: details.size
137
+ };
138
+ }
139
+ catch (error) {
140
+ if (input.signal.aborted)
141
+ throw input.signal.reason;
142
+ if (error instanceof LocalFileInputError)
143
+ throw error;
144
+ throw new LocalFileInputError();
145
+ }
146
+ }
@@ -0,0 +1,19 @@
1
+ import { createHmac } from "node:crypto";
2
+ export function canonicalizeQuery(entries) {
3
+ const sorted = [...entries].sort(([left], [right]) => left.localeCompare(right));
4
+ return new URLSearchParams(sorted.map(([key, value]) => [key, value]))
5
+ .toString()
6
+ .replaceAll("%2C", ",");
7
+ }
8
+ export function createSigningString(input) {
9
+ return `${input.method.toUpperCase()}@${input.pathname}@${input.canonicalQuery}@${input.timestamp}`;
10
+ }
11
+ export function signRequest(input) {
12
+ return hmac(input.secretKey, createSigningString(input));
13
+ }
14
+ export function signBindOrganization(input) {
15
+ return hmac(input.secretKey, `GET@/oauth/device/bind-org@ak=${input.accessKey}&orgId=${input.organizationId}@${input.timestamp}`);
16
+ }
17
+ function hmac(secret, value) {
18
+ return createHmac("sha256", secret).update(value).digest("hex");
19
+ }
@@ -0,0 +1,109 @@
1
+ import { canonicalizeQuery, signRequest } from "./signature.js";
2
+ export class RemoteRequestError extends Error {
3
+ status;
4
+ transient;
5
+ constructor(status, transient = false) {
6
+ super(status === undefined ? "无法连接稿定服务。" : `稿定服务请求失败(HTTP ${status})。`);
7
+ this.name = "RemoteRequestError";
8
+ this.status = status;
9
+ this.transient = transient;
10
+ }
11
+ }
12
+ const TRANSIENT_NETWORK_CODES = new Set([
13
+ "ECONNRESET",
14
+ "ECONNREFUSED",
15
+ "ETIMEDOUT",
16
+ "EPIPE",
17
+ "ENOTFOUND",
18
+ "EAI_AGAIN",
19
+ "UND_ERR_SOCKET",
20
+ "UND_ERR_CONNECT_TIMEOUT"
21
+ ]);
22
+ function isTransientNetworkFailure(error, seen = new WeakSet()) {
23
+ if (error === null || typeof error !== "object" || seen.has(error))
24
+ return false;
25
+ seen.add(error);
26
+ if ("code" in error
27
+ && typeof error.code === "string"
28
+ && TRANSIENT_NETWORK_CODES.has(error.code)) {
29
+ return true;
30
+ }
31
+ if (error instanceof AggregateError
32
+ && error.errors.some((item) => isTransientNetworkFailure(item, seen))) {
33
+ return true;
34
+ }
35
+ return "cause" in error && isTransientNetworkFailure(error.cause, seen);
36
+ }
37
+ export function createSignedHttpTransport(options) {
38
+ const baseUrl = new URL(options.baseUrl);
39
+ if (!baseUrl.pathname.endsWith("/"))
40
+ baseUrl.pathname += "/";
41
+ async function send(method, request, accept, body, timeout) {
42
+ request.signal.throwIfAborted();
43
+ const url = new URL(request.path.replace(/^\//, ""), baseUrl);
44
+ const query = canonicalizeQuery(Object.entries(request.query ?? {}));
45
+ url.search = query;
46
+ const timestamp = Math.floor(options.now().getTime() / 1000);
47
+ const headers = {
48
+ Accept: accept,
49
+ "X-AccessKey": request.credential.accessKey,
50
+ "X-Signature": signRequest({
51
+ method,
52
+ pathname: url.pathname,
53
+ canonicalQuery: query,
54
+ timestamp,
55
+ secretKey: request.credential.secretKey
56
+ }),
57
+ "X-Timestamp": String(timestamp)
58
+ };
59
+ if (options.channelId !== undefined)
60
+ headers["X-Channel-Id"] = options.channelId;
61
+ if (body !== undefined)
62
+ headers["Content-Type"] = "application/json";
63
+ if (request.organizationId !== undefined)
64
+ headers["X-Org-Id"] = request.organizationId;
65
+ const signal = timeout
66
+ ? AbortSignal.any([
67
+ request.signal,
68
+ AbortSignal.timeout(options.timeoutMs ?? 30_000)
69
+ ])
70
+ : request.signal;
71
+ try {
72
+ return await options.fetch(url, {
73
+ method,
74
+ headers,
75
+ ...(body === undefined ? {} : { body }),
76
+ signal
77
+ });
78
+ }
79
+ catch (error) {
80
+ if (request.signal.aborted)
81
+ throw request.signal.reason;
82
+ throw new RemoteRequestError(undefined, isTransientNetworkFailure(error));
83
+ }
84
+ }
85
+ async function json(response) {
86
+ if (!response.ok)
87
+ throw new RemoteRequestError(response.status);
88
+ try {
89
+ return await response.json();
90
+ }
91
+ catch {
92
+ throw new RemoteRequestError(response.status);
93
+ }
94
+ }
95
+ return {
96
+ async getJson(request) {
97
+ return json(await send("GET", request, "application/json", undefined, true));
98
+ },
99
+ async postJson(request) {
100
+ return json(await send("POST", request, "application/json", JSON.stringify(request.body), true));
101
+ },
102
+ async postStream(request) {
103
+ const response = await send("POST", request, request.accept, JSON.stringify(request.body), false);
104
+ if (!response.ok || !response.body)
105
+ throw new RemoteRequestError(response.status);
106
+ return response.body;
107
+ }
108
+ };
109
+ }