@gaoding/cli 0.0.0 → 1.0.0-alpha.25
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 +21 -0
- package/README.md +40 -3
- package/contracts/operations/agent.send/input.schema.json +353 -0
- package/contracts/operations/agent.send/output.schema.json +271 -0
- package/contracts/operations/auth.status/input.schema.json +7 -0
- package/contracts/operations/auth.status/output.schema.json +132 -0
- package/contracts/operations/dam.copy/input.schema.json +19 -0
- package/contracts/operations/dam.copy/output.schema.json +19 -0
- package/contracts/operations/dam.delete/input.schema.json +23 -0
- package/contracts/operations/dam.delete/output.schema.json +6 -0
- package/contracts/operations/dam.download/input.schema.json +14 -0
- package/contracts/operations/dam.download/output.schema.json +25 -0
- package/contracts/operations/dam.folder.list/input.schema.json +15 -0
- package/contracts/operations/dam.folder.list/output.schema.json +37 -0
- package/contracts/operations/dam.get/input.schema.json +16 -0
- package/contracts/operations/dam.get/output.schema.json +46 -0
- package/contracts/operations/dam.list/input.schema.json +44 -0
- package/contracts/operations/dam.list/output.schema.json +71 -0
- package/contracts/operations/dam.move/input.schema.json +19 -0
- package/contracts/operations/dam.move/output.schema.json +18 -0
- package/contracts/operations/dam.recycle.list/input.schema.json +13 -0
- package/contracts/operations/dam.recycle.list/output.schema.json +34 -0
- package/contracts/operations/dam.recycle.restore/input.schema.json +14 -0
- package/contracts/operations/dam.recycle.restore/output.schema.json +6 -0
- package/contracts/operations/dam.rename/input.schema.json +18 -0
- package/contracts/operations/dam.rename/output.schema.json +18 -0
- package/contracts/operations/dam.repository.list/input.schema.json +8 -0
- package/contracts/operations/dam.repository.list/output.schema.json +52 -0
- package/contracts/operations/dam.search/input.schema.json +46 -0
- package/contracts/operations/dam.search/output.schema.json +71 -0
- package/contracts/operations/dam.tag.list/input.schema.json +12 -0
- package/contracts/operations/dam.tag.list/output.schema.json +26 -0
- package/contracts/operations/dam.upload/input.schema.json +31 -0
- package/contracts/operations/dam.upload/output.schema.json +46 -0
- package/contracts/operations/editor.apply/input.schema.json +134 -0
- package/contracts/operations/editor.apply/output.schema.json +15 -0
- package/contracts/operations/editor.connect/input.schema.json +15 -0
- package/contracts/operations/editor.connect/output.schema.json +17 -0
- package/contracts/operations/editor.disconnect/input.schema.json +8 -0
- package/contracts/operations/editor.disconnect/output.schema.json +14 -0
- package/contracts/operations/editor.save/input.schema.json +8 -0
- package/contracts/operations/editor.save/output.schema.json +23 -0
- package/contracts/operations/editor.screenshot/input.schema.json +8 -0
- package/contracts/operations/editor.screenshot/output.schema.json +16 -0
- package/contracts/operations/editor.snapshot/input.schema.json +8 -0
- package/contracts/operations/editor.snapshot/output.schema.json +113 -0
- package/contracts/operations/model.get/input.schema.json +18 -0
- package/contracts/operations/model.get/output.schema.json +191 -0
- package/contracts/operations/model.list/input.schema.json +15 -0
- package/contracts/operations/model.list/output.schema.json +111 -0
- package/contracts/operations/org.current/input.schema.json +7 -0
- package/contracts/operations/org.current/output.schema.json +49 -0
- package/contracts/operations/org.list/input.schema.json +7 -0
- package/contracts/operations/org.list/output.schema.json +71 -0
- package/contracts/operations/tool.call/input.schema.json +35 -0
- package/contracts/operations/tool.call/output.schema.json +129 -0
- package/contracts/operations/tool.list/input.schema.json +7 -0
- package/contracts/operations/tool.list/output.schema.json +45 -0
- package/dist/bin/gd-cli.js +60 -0
- package/dist/bin/postinstall.js +42 -0
- package/dist/src/bootstrap/create-cli.js +112 -0
- package/dist/src/bootstrap/create-runtime.js +290 -0
- package/dist/src/bootstrap/validators.js +157 -0
- package/dist/src/cli/action-binding.js +109 -0
- package/dist/src/cli/agent-commands.js +49 -0
- package/dist/src/cli/auth-commands.js +55 -0
- package/dist/src/cli/dam-commands.js +469 -0
- package/dist/src/cli/editor-commands.js +73 -0
- package/dist/src/cli/errors.js +157 -0
- package/dist/src/cli/model-commands.js +61 -0
- package/dist/src/cli/org-commands.js +58 -0
- package/dist/src/cli/presenter.js +105 -0
- package/dist/src/cli/prompt.js +30 -0
- package/dist/src/cli/tool-commands.js +77 -0
- package/dist/src/cli/update-command.js +14 -0
- package/dist/src/contracts/schema.js +4 -0
- package/dist/src/features/agent/creative-agent-adapter.js +36 -0
- package/dist/src/features/agent/creative-protocol.js +432 -0
- package/dist/src/features/agent/creative-stream.js +127 -0
- package/dist/src/features/agent/use-cases.js +150 -0
- package/dist/src/features/auth/access-policy.js +101 -0
- package/dist/src/features/auth/credential-store.js +62 -0
- package/dist/src/features/auth/sso-service.js +194 -0
- package/dist/src/features/auth/state.js +129 -0
- package/dist/src/features/auth/use-cases.js +136 -0
- package/dist/src/features/dam/asset-projection.js +278 -0
- package/dist/src/features/dam/dam-api-adapter.js +562 -0
- package/dist/src/features/dam/file-downloader.js +242 -0
- package/dist/src/features/dam/object-storage.js +193 -0
- package/dist/src/features/dam/operation-executor.js +72 -0
- package/dist/src/features/dam/registered-uploader.js +247 -0
- package/dist/src/features/dam/repository-catalog.js +127 -0
- package/dist/src/features/dam/storage-upload.js +141 -0
- package/dist/src/features/dam/transient-uploader.js +17 -0
- package/dist/src/features/dam/use-cases.js +470 -0
- package/dist/src/features/editor/bridge-client.js +109 -0
- package/dist/src/features/editor/bridge-process.js +222 -0
- package/dist/src/features/editor/bridge-server.js +337 -0
- package/dist/src/features/editor/protocol.js +1 -0
- package/dist/src/features/editor/public-url.js +36 -0
- package/dist/src/features/editor/session-state.js +78 -0
- package/dist/src/features/editor/session.js +207 -0
- package/dist/src/features/editor/use-cases.js +88 -0
- package/dist/src/features/org/org-service.js +76 -0
- package/dist/src/features/org/use-cases.js +156 -0
- package/dist/src/features/skill/bundled-skills.js +55 -0
- package/dist/src/features/skill/installer.js +271 -0
- package/dist/src/features/tool/catalog.js +83 -0
- package/dist/src/features/tool/dynamic-schema.js +128 -0
- package/dist/src/features/tool/mns-catalog-adapter.js +265 -0
- package/dist/src/features/tool/tool-api-adapter.js +352 -0
- package/dist/src/features/tool/use-cases.js +177 -0
- package/dist/src/features/update/latest-version.js +37 -0
- package/dist/src/features/update/update-notification.js +262 -0
- package/dist/src/features/update/update-service.js +183 -0
- package/dist/src/platform/json-input.js +67 -0
- package/dist/src/platform/local-json-file.js +59 -0
- package/dist/src/platform/open-browser.js +8 -0
- package/dist/src/platform/redact.js +80 -0
- package/dist/src/platform/remote-error-evidence.js +80 -0
- package/dist/src/platform/remote-protocol.js +10 -0
- package/dist/src/platform/safe-upload-file.js +146 -0
- package/dist/src/platform/signature.js +20 -0
- package/dist/src/platform/signed-http-transport.js +143 -0
- package/dist/src/platform/url-safety.js +100 -0
- package/dist/src/telemetry/invocation.js +156 -0
- package/dist/src/telemetry/sls-sink.js +31 -0
- package/package.json +54 -8
- package/skills/gd-cli/SKILL.md +15 -0
- package/skills/gd-cli/references/auth-org.md +9 -0
- package/skills/gd-cli/references/creation.md +115 -0
- package/skills/gd-cli/references/dam.md +52 -0
- package/skills/gd-cli/references/editor.md +254 -0
- package/skills/gd-cli/references/errors.md +13 -0
- package/skills/gd-cli/references/update.md +13 -0
- package/index.js +0 -3
|
@@ -0,0 +1,67 @@
|
|
|
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
|
+
const destroy = stream.destroy;
|
|
56
|
+
if (typeof destroy === "function")
|
|
57
|
+
destroy.call(stream);
|
|
58
|
+
reject(signal.reason);
|
|
59
|
+
};
|
|
60
|
+
stream.on("data", onData);
|
|
61
|
+
stream.once("end", onEnd);
|
|
62
|
+
stream.once("error", onError);
|
|
63
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
64
|
+
if (signal.aborted)
|
|
65
|
+
onAbort();
|
|
66
|
+
});
|
|
67
|
+
}
|
|
@@ -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,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,80 @@
|
|
|
1
|
+
import { redact, redactSensitiveText } from "./redact.js";
|
|
2
|
+
const maximumLength = 8_192;
|
|
3
|
+
const truncationMarker = "[TRUNCATED]";
|
|
4
|
+
export function remoteErrorEvidence(value) {
|
|
5
|
+
const normalized = parseJson(value);
|
|
6
|
+
const remoteCode = topLevelCode(normalized) ?? jsonRpcErrorCode(normalized);
|
|
7
|
+
const sanitized = sanitize(normalized);
|
|
8
|
+
if (sanitized === undefined) {
|
|
9
|
+
return remoteCode === undefined ? {} : { remoteCode };
|
|
10
|
+
}
|
|
11
|
+
const remoteBody = sanitized.length <= maximumLength
|
|
12
|
+
? sanitized
|
|
13
|
+
: `${sanitized.slice(0, maximumLength - truncationMarker.length)}${truncationMarker}`;
|
|
14
|
+
return {
|
|
15
|
+
...(remoteCode === undefined ? {} : { remoteCode }),
|
|
16
|
+
remoteBody
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function parseJson(value) {
|
|
20
|
+
if (typeof value !== "string")
|
|
21
|
+
return value;
|
|
22
|
+
try {
|
|
23
|
+
return JSON.parse(value);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function topLevelCode(value) {
|
|
30
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
31
|
+
return undefined;
|
|
32
|
+
if (!Object.prototype.hasOwnProperty.call(value, "code"))
|
|
33
|
+
return undefined;
|
|
34
|
+
return normalizeCode(value.code);
|
|
35
|
+
}
|
|
36
|
+
function jsonRpcErrorCode(value) {
|
|
37
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
38
|
+
return undefined;
|
|
39
|
+
const record = value;
|
|
40
|
+
if (record.jsonrpc !== "2.0"
|
|
41
|
+
|| record.error === null
|
|
42
|
+
|| typeof record.error !== "object"
|
|
43
|
+
|| Array.isArray(record.error)) {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
return normalizeCode(record.error.code);
|
|
47
|
+
}
|
|
48
|
+
function normalizeCode(code) {
|
|
49
|
+
if (typeof code === "string")
|
|
50
|
+
return redactRemoteText(code);
|
|
51
|
+
return typeof code === "number" ? String(code) : undefined;
|
|
52
|
+
}
|
|
53
|
+
function sanitize(value) {
|
|
54
|
+
if (typeof value === "string")
|
|
55
|
+
return redactRemoteText(value);
|
|
56
|
+
try {
|
|
57
|
+
return JSON.stringify(redactCookieAssignments(redact(value)));
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function redactRemoteText(value) {
|
|
64
|
+
return redactCookieAssignment(redactSensitiveText(value));
|
|
65
|
+
}
|
|
66
|
+
function redactCookieAssignments(value) {
|
|
67
|
+
if (typeof value === "string")
|
|
68
|
+
return redactCookieAssignment(value);
|
|
69
|
+
if (value === null || typeof value !== "object")
|
|
70
|
+
return value;
|
|
71
|
+
if (Array.isArray(value))
|
|
72
|
+
return value.map(redactCookieAssignments);
|
|
73
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
74
|
+
key,
|
|
75
|
+
redactCookieAssignments(item)
|
|
76
|
+
]));
|
|
77
|
+
}
|
|
78
|
+
function redactCookieAssignment(value) {
|
|
79
|
+
return value.replace(/\b(cookie)\s*=\s*[^\s,;}&]+/giu, "$1=[REDACTED]");
|
|
80
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export class RemoteProtocolError extends Error {
|
|
2
|
+
remoteCode;
|
|
3
|
+
remoteBody;
|
|
4
|
+
constructor(evidence = {}) {
|
|
5
|
+
super("稿定服务响应与当前 CLI 不兼容。");
|
|
6
|
+
this.name = "RemoteProtocolError";
|
|
7
|
+
this.remoteCode = evidence.remoteCode;
|
|
8
|
+
this.remoteBody = evidence.remoteBody;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -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,20 @@
|
|
|
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
|
+
.replaceAll("%3A", ":");
|
|
8
|
+
}
|
|
9
|
+
export function createSigningString(input) {
|
|
10
|
+
return `${input.method.toUpperCase()}@${input.pathname}@${input.canonicalQuery}@${input.timestamp}`;
|
|
11
|
+
}
|
|
12
|
+
export function signRequest(input) {
|
|
13
|
+
return hmac(input.secretKey, createSigningString(input));
|
|
14
|
+
}
|
|
15
|
+
export function signBindOrganization(input) {
|
|
16
|
+
return hmac(input.secretKey, `GET@/oauth/device/bind-org@ak=${input.accessKey}&orgId=${input.organizationId}@${input.timestamp}`);
|
|
17
|
+
}
|
|
18
|
+
function hmac(secret, value) {
|
|
19
|
+
return createHmac("sha256", secret).update(value).digest("hex");
|
|
20
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { remoteErrorEvidence } from "./remote-error-evidence.js";
|
|
2
|
+
import { RemoteProtocolError } from "./remote-protocol.js";
|
|
3
|
+
import { canonicalizeQuery, signRequest } from "./signature.js";
|
|
4
|
+
export class RemoteRequestError extends Error {
|
|
5
|
+
status;
|
|
6
|
+
transient;
|
|
7
|
+
remoteCode;
|
|
8
|
+
remoteBody;
|
|
9
|
+
constructor(status, transient = false, evidence = {}) {
|
|
10
|
+
super(status === undefined ? "无法连接稿定服务。" : `稿定服务请求失败(HTTP ${status})。`);
|
|
11
|
+
this.name = "RemoteRequestError";
|
|
12
|
+
this.status = status;
|
|
13
|
+
this.transient = transient;
|
|
14
|
+
this.remoteCode = evidence.remoteCode;
|
|
15
|
+
this.remoteBody = evidence.remoteBody;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
const TRANSIENT_NETWORK_CODES = new Set([
|
|
19
|
+
"ECONNRESET",
|
|
20
|
+
"ECONNREFUSED",
|
|
21
|
+
"ETIMEDOUT",
|
|
22
|
+
"EPIPE",
|
|
23
|
+
"ENOTFOUND",
|
|
24
|
+
"EAI_AGAIN",
|
|
25
|
+
"UND_ERR_SOCKET",
|
|
26
|
+
"UND_ERR_CONNECT_TIMEOUT"
|
|
27
|
+
]);
|
|
28
|
+
function isTransientNetworkFailure(error, seen = new WeakSet()) {
|
|
29
|
+
if (error === null || typeof error !== "object" || seen.has(error))
|
|
30
|
+
return false;
|
|
31
|
+
seen.add(error);
|
|
32
|
+
if ("code" in error
|
|
33
|
+
&& typeof error.code === "string"
|
|
34
|
+
&& TRANSIENT_NETWORK_CODES.has(error.code)) {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
if (error instanceof AggregateError
|
|
38
|
+
&& error.errors.some((item) => isTransientNetworkFailure(item, seen))) {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
return "cause" in error && isTransientNetworkFailure(error.cause, seen);
|
|
42
|
+
}
|
|
43
|
+
export function createSignedHttpTransport(options) {
|
|
44
|
+
const baseUrl = new URL(options.baseUrl);
|
|
45
|
+
if (!baseUrl.pathname.endsWith("/"))
|
|
46
|
+
baseUrl.pathname += "/";
|
|
47
|
+
async function send(method, request, accept, body, timeout) {
|
|
48
|
+
request.signal.throwIfAborted();
|
|
49
|
+
const url = new URL(request.path.replace(/^\//, ""), baseUrl);
|
|
50
|
+
const query = canonicalizeQuery(Object.entries(request.query ?? {}));
|
|
51
|
+
url.search = query;
|
|
52
|
+
const timestamp = Math.floor(options.now().getTime() / 1000);
|
|
53
|
+
const headers = {
|
|
54
|
+
Accept: accept,
|
|
55
|
+
"X-AccessKey": request.credential.accessKey,
|
|
56
|
+
"X-Signature": signRequest({
|
|
57
|
+
method,
|
|
58
|
+
pathname: url.pathname,
|
|
59
|
+
canonicalQuery: query,
|
|
60
|
+
timestamp,
|
|
61
|
+
secretKey: request.credential.secretKey
|
|
62
|
+
}),
|
|
63
|
+
"X-Timestamp": String(timestamp)
|
|
64
|
+
};
|
|
65
|
+
if (options.traceparent !== undefined)
|
|
66
|
+
headers.traceparent = options.traceparent();
|
|
67
|
+
if (options.channelId !== undefined)
|
|
68
|
+
headers["X-Channel-Id"] = options.channelId;
|
|
69
|
+
if (body !== undefined)
|
|
70
|
+
headers["Content-Type"] = "application/json";
|
|
71
|
+
if (request.organizationId !== undefined)
|
|
72
|
+
headers["X-Org-Id"] = request.organizationId;
|
|
73
|
+
const signal = timeout
|
|
74
|
+
? AbortSignal.any([
|
|
75
|
+
request.signal,
|
|
76
|
+
AbortSignal.timeout(options.timeoutMs ?? 30_000)
|
|
77
|
+
])
|
|
78
|
+
: request.signal;
|
|
79
|
+
try {
|
|
80
|
+
return await options.fetch(url, {
|
|
81
|
+
method,
|
|
82
|
+
headers,
|
|
83
|
+
...(body === undefined ? {} : { body }),
|
|
84
|
+
signal
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
if (request.signal.aborted)
|
|
89
|
+
throw request.signal.reason;
|
|
90
|
+
throw new RemoteRequestError(undefined, isTransientNetworkFailure(error));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async function json(response) {
|
|
94
|
+
let body;
|
|
95
|
+
try {
|
|
96
|
+
body = await response.text();
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
throw new RemoteRequestError(response.status);
|
|
100
|
+
}
|
|
101
|
+
const evidence = remoteErrorEvidence(body);
|
|
102
|
+
if (!response.ok)
|
|
103
|
+
throw new RemoteRequestError(response.status, false, evidence);
|
|
104
|
+
try {
|
|
105
|
+
return JSON.parse(body);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
throw new RemoteProtocolError(evidence);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
async function getJsonResponse(request) {
|
|
112
|
+
const response = await send("GET", request, "application/json", undefined, true);
|
|
113
|
+
return { body: await json(response), headers: response.headers };
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
async getJson(request) {
|
|
117
|
+
return (await getJsonResponse(request)).body;
|
|
118
|
+
},
|
|
119
|
+
getJsonResponse,
|
|
120
|
+
async postJson(request) {
|
|
121
|
+
return json(await send("POST", request, "application/json", JSON.stringify(request.body), true));
|
|
122
|
+
},
|
|
123
|
+
async putJson(request) {
|
|
124
|
+
return json(await send("PUT", request, "application/json", JSON.stringify(request.body), true));
|
|
125
|
+
},
|
|
126
|
+
async postStream(request) {
|
|
127
|
+
const response = await send("POST", request, request.accept, JSON.stringify(request.body), false);
|
|
128
|
+
if (!response.ok) {
|
|
129
|
+
let body;
|
|
130
|
+
try {
|
|
131
|
+
body = await response.text();
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
throw new RemoteRequestError(response.status);
|
|
135
|
+
}
|
|
136
|
+
throw new RemoteRequestError(response.status, false, remoteErrorEvidence(body));
|
|
137
|
+
}
|
|
138
|
+
if (!response.body)
|
|
139
|
+
throw new RemoteRequestError(response.status);
|
|
140
|
+
return response.body;
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
}
|