@world-engines/project-host 0.1.0-alpha.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/LICENSE +46 -0
- package/assets/gui/WELogo-title.svg +11 -0
- package/assets/gui/app.js +1326 -0
- package/assets/gui/index.html +118 -0
- package/assets/gui/local-preview-asset-urls.js +61 -0
- package/assets/gui/local-preview-bridge.js +393 -0
- package/assets/gui/spatial-template-panel.js +233 -0
- package/assets/gui/styles.css +115 -0
- package/assets/gui/terrain-render.js +120 -0
- package/assets/gui/workspace-status.css +17 -0
- package/assets/gui/workspace-status.js +255 -0
- package/assets/gui/world-map-globe.js +187 -0
- package/assets/gui/world-map-worker.js +62 -0
- package/assets/gui/world-map.css +30 -0
- package/assets/gui/world-map.js +2466 -0
- package/dist/account-model-proxy.d.ts +21 -0
- package/dist/account-model-proxy.js +470 -0
- package/dist/authoring-browser-runtime.d.ts +7 -0
- package/dist/authoring-browser-runtime.js +110 -0
- package/dist/authoring-ingestion.d.ts +38 -0
- package/dist/authoring-ingestion.js +410 -0
- package/dist/authoring-model-connection.d.ts +166 -0
- package/dist/authoring-model-connection.js +757 -0
- package/dist/chinese-lexical.d.ts +152 -0
- package/dist/chinese-lexical.js +555 -0
- package/dist/chromium-preview-browser-driver.d.ts +43 -0
- package/dist/chromium-preview-browser-driver.js +575 -0
- package/dist/cli/download.d.ts +12 -0
- package/dist/cli/download.js +50 -0
- package/dist/cli/dump.d.ts +32 -0
- package/dist/cli/dump.js +91 -0
- package/dist/cli/ingest.d.ts +49 -0
- package/dist/cli/ingest.js +225 -0
- package/dist/cli/review.d.ts +10 -0
- package/dist/cli/review.js +44 -0
- package/dist/cli/upload.d.ts +57 -0
- package/dist/cli/upload.js +112 -0
- package/dist/cli-command.d.ts +60 -0
- package/dist/cli-command.js +613 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +182 -0
- package/dist/desktop-authoring-runtime.d.ts +88 -0
- package/dist/desktop-authoring-runtime.js +1602 -0
- package/dist/desktop-sidecar.d.ts +11 -0
- package/dist/desktop-sidecar.js +143 -0
- package/dist/document-dump.d.ts +113 -0
- package/dist/document-dump.js +431 -0
- package/dist/document-graph-extractor.d.ts +125 -0
- package/dist/document-graph-extractor.js +588 -0
- package/dist/document-ingestion-journal.d.ts +68 -0
- package/dist/document-ingestion-journal.js +471 -0
- package/dist/document-ingestion.d.ts +183 -0
- package/dist/document-ingestion.js +976 -0
- package/dist/document-query-path-proof.d.ts +30 -0
- package/dist/document-query-path-proof.js +34 -0
- package/dist/document-query-path-renderer.d.ts +50 -0
- package/dist/document-query-path-renderer.js +75 -0
- package/dist/document-query-proof-paths.d.ts +22 -0
- package/dist/document-query-proof-paths.js +71 -0
- package/dist/document-query.d.ts +97 -0
- package/dist/document-query.js +1106 -0
- package/dist/document-relation-role.d.ts +14 -0
- package/dist/document-relation-role.js +86 -0
- package/dist/document-source.d.ts +107 -0
- package/dist/document-source.js +255 -0
- package/dist/embedding-cache.d.ts +35 -0
- package/dist/embedding-cache.js +317 -0
- package/dist/frozen-review-local-pipeline.d.ts +88 -0
- package/dist/frozen-review-local-pipeline.js +609 -0
- package/dist/frozen-review-source-capture.d.ts +15 -0
- package/dist/frozen-review-source-capture.js +551 -0
- package/dist/frozen-review-wasm-encryptor.d.ts +16 -0
- package/dist/frozen-review-wasm-encryptor.js +72 -0
- package/dist/frozen-review.d.ts +131 -0
- package/dist/frozen-review.js +802 -0
- package/dist/graph-traversal.d.ts +23 -0
- package/dist/graph-traversal.js +100 -0
- package/dist/gui/graph-layout-worker.js +1 -0
- package/dist/gui/graph-layout.wasm +0 -0
- package/dist/gui/graph.css +1 -0
- package/dist/gui/graph.js +76 -0
- package/dist/gui/plane-generation.js +112 -0
- package/dist/gui/plane-generator.js +310 -0
- package/dist/gui/plane-generator_bg.wasm +0 -0
- package/dist/gui/procedural-city.js +595 -0
- package/dist/gui/road-generation.js +129 -0
- package/dist/gui/road-parcels.js +1584 -0
- package/dist/gui/score.css +1 -0
- package/dist/gui/score.js +54 -0
- package/dist/gui/spatial-runtime.js +1231 -0
- package/dist/gui/spatial-templates.js +200 -0
- package/dist/gui/trigger.css +1 -0
- package/dist/gui/trigger.js +142 -0
- package/dist/harness-callback-transport.d.ts +17 -0
- package/dist/harness-callback-transport.js +128 -0
- package/dist/harness-document-ingestion.d.ts +77 -0
- package/dist/harness-document-ingestion.js +343 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.js +43 -0
- package/dist/ingestion-source-reconciliation.d.ts +88 -0
- package/dist/ingestion-source-reconciliation.js +253 -0
- package/dist/ladybug-runtime.d.ts +117 -0
- package/dist/ladybug-runtime.js +886 -0
- package/dist/local-account-embedding.d.ts +34 -0
- package/dist/local-account-embedding.js +294 -0
- package/dist/local-gallery.d.ts +35 -0
- package/dist/local-gallery.js +227 -0
- package/dist/local-gui-auth.d.ts +96 -0
- package/dist/local-gui-auth.js +703 -0
- package/dist/local-gui-contract.d.ts +53 -0
- package/dist/local-gui-contract.js +5 -0
- package/dist/local-gui-image-assets.d.ts +17 -0
- package/dist/local-gui-image-assets.js +96 -0
- package/dist/local-gui-preview-client.d.ts +3 -0
- package/dist/local-gui-preview-client.js +111 -0
- package/dist/local-gui-score.d.ts +72 -0
- package/dist/local-gui-score.js +184 -0
- package/dist/local-gui-server.d.ts +10 -0
- package/dist/local-gui-server.js +987 -0
- package/dist/local-gui-version.d.ts +17 -0
- package/dist/local-gui-version.js +147 -0
- package/dist/local-gui-workspace.d.ts +14 -0
- package/dist/local-gui-workspace.js +171 -0
- package/dist/local-model-settings.d.ts +28 -0
- package/dist/local-model-settings.js +124 -0
- package/dist/local-preview-assets.d.ts +28 -0
- package/dist/local-preview-assets.js +124 -0
- package/dist/local-preview-bound-resources.d.ts +12 -0
- package/dist/local-preview-bound-resources.js +80 -0
- package/dist/local-preview-message-store.d.ts +63 -0
- package/dist/local-preview-message-store.js +214 -0
- package/dist/local-preview-player-read-models.d.ts +47 -0
- package/dist/local-preview-player-read-models.js +90 -0
- package/dist/local-preview-projections.d.ts +61 -0
- package/dist/local-preview-projections.js +117 -0
- package/dist/local-preview-runtime.d.ts +105 -0
- package/dist/local-preview-runtime.js +553 -0
- package/dist/local-preview-session.d.ts +73 -0
- package/dist/local-preview-session.js +395 -0
- package/dist/local-preview-source.d.ts +40 -0
- package/dist/local-preview-source.js +192 -0
- package/dist/local-preview-view-events.d.ts +101 -0
- package/dist/local-preview-view-events.js +179 -0
- package/dist/local-preview-view-state-sqlite.d.ts +24 -0
- package/dist/local-preview-view-state-sqlite.js +263 -0
- package/dist/local-preview-view-state.d.ts +93 -0
- package/dist/local-preview-view-state.js +108 -0
- package/dist/local-preview-vite-config.d.ts +6 -0
- package/dist/local-preview-vite-config.js +122 -0
- package/dist/local-preview-world-effects.d.ts +9 -0
- package/dist/local-preview-world-effects.js +55 -0
- package/dist/local-preview-worldline-store.d.ts +47 -0
- package/dist/local-preview-worldline-store.js +282 -0
- package/dist/local-preview-worldlines.d.ts +95 -0
- package/dist/local-preview-worldlines.js +136 -0
- package/dist/mcp-async-dispatcher.d.ts +3 -0
- package/dist/mcp-async-dispatcher.js +129 -0
- package/dist/mcp-line-queue.d.ts +14 -0
- package/dist/mcp-line-queue.js +184 -0
- package/dist/mcp-pipe-internal.d.ts +6 -0
- package/dist/mcp-pipe-internal.js +151 -0
- package/dist/mcp-pipe-response.d.ts +14 -0
- package/dist/mcp-pipe-response.js +50 -0
- package/dist/mcp-stdio-response.d.ts +12 -0
- package/dist/mcp-stdio-response.js +27 -0
- package/dist/mcp.d.ts +27 -0
- package/dist/mcp.js +194 -0
- package/dist/prepared-embedding-cache.d.ts +38 -0
- package/dist/prepared-embedding-cache.js +238 -0
- package/dist/preview-browser-adapter.d.ts +53 -0
- package/dist/preview-browser-adapter.js +171 -0
- package/dist/preview-runtime.d.ts +67 -0
- package/dist/preview-runtime.js +460 -0
- package/dist/project-auth-transport.d.ts +27 -0
- package/dist/project-auth-transport.js +121 -0
- package/dist/project-auth.d.ts +51 -0
- package/dist/project-auth.js +108 -0
- package/dist/project-git-capture-recovery.d.ts +16 -0
- package/dist/project-git-capture-recovery.js +347 -0
- package/dist/project-git-cli.d.ts +5 -0
- package/dist/project-git-cli.js +40 -0
- package/dist/project-git-scenario-source.d.ts +10 -0
- package/dist/project-git-scenario-source.js +240 -0
- package/dist/project-git-view-source.d.ts +13 -0
- package/dist/project-git-view-source.js +134 -0
- package/dist/project-git.d.ts +97 -0
- package/dist/project-git.js +656 -0
- package/dist/project-host.d.ts +59 -0
- package/dist/project-host.js +471 -0
- package/dist/project-writer-lease.d.ts +18 -0
- package/dist/project-writer-lease.js +430 -0
- package/dist/public-error.d.ts +7 -0
- package/dist/public-error.js +61 -0
- package/dist/retrieval-benchmark.d.ts +46 -0
- package/dist/retrieval-benchmark.js +58 -0
- package/dist/safe-project-path.d.ts +8 -0
- package/dist/safe-project-path.js +93 -0
- package/dist/scenario-writer.d.ts +32 -0
- package/dist/scenario-writer.js +191 -0
- package/dist/transfer/download.d.ts +159 -0
- package/dist/transfer/download.js +598 -0
- package/dist/transfer/transport.d.ts +26 -0
- package/dist/transfer/transport.js +1 -0
- package/dist/transfer/upload.d.ts +162 -0
- package/dist/transfer/upload.js +354 -0
- package/dist/transfer-types.d.ts +95 -0
- package/dist/transfer-types.js +1 -0
- package/dist/transfer.d.ts +18 -0
- package/dist/transfer.js +273 -0
- package/dist/trigger-runtime.d.ts +121 -0
- package/dist/trigger-runtime.js +153 -0
- package/dist/vendor/wasm-core/asset-hashes.json +5 -0
- package/dist/vendor/wasm-core/wasm_core.d.ts +490 -0
- package/dist/vendor/wasm-core/wasm_core.js +1 -0
- package/dist/vendor/wasm-core/wasm_core_bg.wasm +0 -0
- package/dist/view-watcher.d.ts +9 -0
- package/dist/view-watcher.js +34 -0
- package/package.json +110 -0
|
@@ -0,0 +1,703 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import { AuthoringBridgeError } from "@world-engines/authoring-bridge";
|
|
3
|
+
import { MeResponse } from "@world-engines/protocol-ts/v1/rest/auth";
|
|
4
|
+
import { WalletResponse } from "@world-engines/protocol-ts/v1/rest/billing";
|
|
5
|
+
import { isKnownScenarioTag } from "@world-engines/authoring-ui/score-tags";
|
|
6
|
+
import { readFrozenReviewTargetBinding } from "./frozen-review-local-pipeline.js";
|
|
7
|
+
const API_ORIGIN = "https://api.late-frost.com";
|
|
8
|
+
const CALLBACK_PATH = "/desktop-login/callback";
|
|
9
|
+
const AUTHORIZATION_PATH = "/desktop-login";
|
|
10
|
+
const MAX_AUTH_RESPONSE_BYTES = 64 * 1024;
|
|
11
|
+
const MAX_TRANSFER_REQUEST_BYTES = 96 * 1024 * 1024;
|
|
12
|
+
const MAX_TRANSFER_RESPONSE_BYTES = 128 * 1024 * 1024;
|
|
13
|
+
const MAX_MODEL_RESPONSE_BYTES = 16 * 1024 * 1024;
|
|
14
|
+
/** server 的 CSP form-action 必须复用此精确授权 origin 列表。 */
|
|
15
|
+
export const LOCAL_GUI_AUTHORIZATION_ORIGINS = Object.freeze([
|
|
16
|
+
"https://late-frost.com",
|
|
17
|
+
"https://world-engines.org",
|
|
18
|
+
]);
|
|
19
|
+
const AUTHORIZATION_ORIGINS = new Set(LOCAL_GUI_AUTHORIZATION_ORIGINS);
|
|
20
|
+
const PROJECT_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/u;
|
|
21
|
+
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu;
|
|
22
|
+
const PKCE_PATTERN = /^[A-Za-z0-9_-]{43}$/u;
|
|
23
|
+
const SHA256_PATTERN = /^[0-9a-f]{64}$/u;
|
|
24
|
+
const ACCOUNT_MODEL_SESSION_PATH = "/v1/developer/account-models/session";
|
|
25
|
+
const ACCOUNT_MODEL_CHAT_CATALOG_PATH = "/v1/developer/account-models/chat/catalog";
|
|
26
|
+
const ACCOUNT_MODEL_EMBEDDING_DESCRIPTOR_PATH = "/v1/developer/account-models/embedding/descriptor";
|
|
27
|
+
const ACCOUNT_MODEL_EMBEDDING_PATH = "/v1/developer/account-models/openai/v1/embeddings";
|
|
28
|
+
const ACCOUNT_MODEL_CHAT_COMPLETIONS_PATH = "/v1/developer/account-models/openai/v1/chat/completions";
|
|
29
|
+
const IDEMPOTENCY_KEY_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/u;
|
|
30
|
+
const MODEL_SESSION_LEASES = new WeakMap();
|
|
31
|
+
function fail(code, message) {
|
|
32
|
+
throw new AuthoringBridgeError(code, message);
|
|
33
|
+
}
|
|
34
|
+
function normalizeLoopbackOrigin(value) {
|
|
35
|
+
let url;
|
|
36
|
+
try {
|
|
37
|
+
url = new URL(value);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
fail("E_SCHEMA_INVALID", "local GUI loopback origin 无效");
|
|
41
|
+
}
|
|
42
|
+
if (url.protocol !== "http:" || url.hostname !== "127.0.0.1" || url.port === ""
|
|
43
|
+
|| url.username !== "" || url.password !== "" || url.pathname !== "/"
|
|
44
|
+
|| url.search !== "" || url.hash !== "") {
|
|
45
|
+
fail("E_SCHEMA_INVALID", "local GUI loopback origin 必须是 127.0.0.1 的纯 HTTP origin");
|
|
46
|
+
}
|
|
47
|
+
return url;
|
|
48
|
+
}
|
|
49
|
+
function randomPkce() { return randomBytes(32).toString("base64url"); }
|
|
50
|
+
function sha256(value) { return createHash("sha256").update(value).digest("hex"); }
|
|
51
|
+
function object(value) {
|
|
52
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
53
|
+
? value
|
|
54
|
+
: undefined;
|
|
55
|
+
}
|
|
56
|
+
async function boundedBytes(response, limit, errorMessage) {
|
|
57
|
+
const declared = Number(response.headers.get("content-length"));
|
|
58
|
+
if (Number.isFinite(declared) && declared > limit)
|
|
59
|
+
fail("E_VALIDATION_FAILED", errorMessage);
|
|
60
|
+
if (response.body === null)
|
|
61
|
+
return new Uint8Array();
|
|
62
|
+
const reader = response.body.getReader();
|
|
63
|
+
const chunks = [];
|
|
64
|
+
let total = 0;
|
|
65
|
+
try {
|
|
66
|
+
while (true) {
|
|
67
|
+
const item = await reader.read();
|
|
68
|
+
if (item.done)
|
|
69
|
+
break;
|
|
70
|
+
total += item.value.byteLength;
|
|
71
|
+
if (!Number.isSafeInteger(total) || total > limit) {
|
|
72
|
+
await reader.cancel().catch(() => undefined);
|
|
73
|
+
fail("E_VALIDATION_FAILED", errorMessage);
|
|
74
|
+
}
|
|
75
|
+
chunks.push(item.value);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
if (error instanceof AuthoringBridgeError)
|
|
80
|
+
throw error;
|
|
81
|
+
fail("E_VALIDATION_FAILED", errorMessage);
|
|
82
|
+
}
|
|
83
|
+
const bytes = new Uint8Array(total);
|
|
84
|
+
let offset = 0;
|
|
85
|
+
for (const chunk of chunks) {
|
|
86
|
+
bytes.set(chunk, offset);
|
|
87
|
+
offset += chunk.byteLength;
|
|
88
|
+
}
|
|
89
|
+
return bytes;
|
|
90
|
+
}
|
|
91
|
+
async function boundedJson(response, limit, errorMessage) {
|
|
92
|
+
const bytes = await boundedBytes(response, limit, errorMessage);
|
|
93
|
+
try {
|
|
94
|
+
const value = object(JSON.parse(new TextDecoder().decode(bytes)));
|
|
95
|
+
if (value !== undefined)
|
|
96
|
+
return value;
|
|
97
|
+
}
|
|
98
|
+
catch { /* 统一映射为不包含远端 body 的公开错误。 */ }
|
|
99
|
+
fail("E_VALIDATION_FAILED", errorMessage);
|
|
100
|
+
}
|
|
101
|
+
function assertAuthorizationUrl(value, pending) {
|
|
102
|
+
if (typeof value !== "string" || value.length > 4096)
|
|
103
|
+
fail("E_VALIDATION_FAILED", "远端认证服务返回无效授权 URL");
|
|
104
|
+
let url;
|
|
105
|
+
try {
|
|
106
|
+
url = new URL(value);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
fail("E_VALIDATION_FAILED", "远端认证服务返回无效授权 URL");
|
|
110
|
+
}
|
|
111
|
+
const keys = [...url.searchParams.keys()];
|
|
112
|
+
if (!AUTHORIZATION_ORIGINS.has(url.origin) || url.pathname !== AUTHORIZATION_PATH
|
|
113
|
+
|| url.username !== "" || url.password !== "" || url.hash !== ""
|
|
114
|
+
|| keys.length !== 2 || new Set(keys).size !== 2
|
|
115
|
+
|| url.searchParams.get("request_id") !== pending.requestId
|
|
116
|
+
|| url.searchParams.get("state") !== pending.state) {
|
|
117
|
+
fail("E_VALIDATION_FAILED", "远端认证服务授权 URL 未通过 allowlist");
|
|
118
|
+
}
|
|
119
|
+
return url.toString();
|
|
120
|
+
}
|
|
121
|
+
function parseHttpOnlyCookie(header) {
|
|
122
|
+
if (header === null || !/(?:^|;)\s*HttpOnly(?:;|$)/iu.test(header)) {
|
|
123
|
+
fail("E_AUTH_REQUIRED", "远端认证服务未建立 HttpOnly session");
|
|
124
|
+
}
|
|
125
|
+
const first = header.split(";", 1)[0]?.trim() ?? "";
|
|
126
|
+
const separator = first.indexOf("=");
|
|
127
|
+
const name = separator < 0 ? "" : first.slice(0, separator);
|
|
128
|
+
const value = separator < 0 ? "" : first.slice(separator + 1);
|
|
129
|
+
if (!/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/u.test(name) || value.length === 0 || /[\x00-\x20\x7f;,]/u.test(value)) {
|
|
130
|
+
fail("E_AUTH_REQUIRED", "远端认证服务返回无效 HttpOnly session");
|
|
131
|
+
}
|
|
132
|
+
return Object.freeze({ pair: `${name}=${value}` });
|
|
133
|
+
}
|
|
134
|
+
function decodedStrictSegment(value) {
|
|
135
|
+
if (value.length === 0 || value.length > 768 || value.includes("/") || value.includes("\\"))
|
|
136
|
+
return undefined;
|
|
137
|
+
let decoded;
|
|
138
|
+
try {
|
|
139
|
+
decoded = decodeURIComponent(value);
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
return decoded.length <= 256 && /^[A-Za-z0-9_-]+$/u.test(decoded) ? decoded : undefined;
|
|
145
|
+
}
|
|
146
|
+
function operationSuffix(value, prefix) {
|
|
147
|
+
const parts = value.slice(prefix.length).split("/");
|
|
148
|
+
return decodedStrictSegment(parts[0] ?? "") !== undefined
|
|
149
|
+
&& (parts.length === 1 || (parts.length === 2 && ["reconcile", "retry", "undo"].includes(parts[1] ?? "")));
|
|
150
|
+
}
|
|
151
|
+
/** 为 npm GUI adapter 镜像 native broker 的 project-transfer route allowlist。 */
|
|
152
|
+
export function isLocalGuiTransferRouteAllowed(method, path) {
|
|
153
|
+
const prefix = "/v1/creator/project-transfer/";
|
|
154
|
+
if (!path.startsWith(prefix) || path.includes("?") || path.includes("//") || path.includes("\\"))
|
|
155
|
+
return false;
|
|
156
|
+
if ((method === "GET" && path === `${prefix}inventory`)
|
|
157
|
+
|| (method === "POST" && ["upload-plans", "upload/confirm", "download/plan", "download/confirm"].some((suffix) => path === `${prefix}${suffix}`)))
|
|
158
|
+
return true;
|
|
159
|
+
for (const [candidateMethod, candidatePrefix] of [
|
|
160
|
+
["PUT", `${prefix}upload-staging/`], ["PUT", `${prefix}staging/`],
|
|
161
|
+
["HEAD", `${prefix}upload-staging/`], ["HEAD", `${prefix}staging/`],
|
|
162
|
+
["GET", `${prefix}download/staging/`],
|
|
163
|
+
]) {
|
|
164
|
+
if (method === candidateMethod && path.startsWith(candidatePrefix))
|
|
165
|
+
return decodedStrictSegment(path.slice(candidatePrefix.length)) !== undefined;
|
|
166
|
+
}
|
|
167
|
+
for (const candidatePrefix of [`${prefix}operations/`, `${prefix}download/operations/`]) {
|
|
168
|
+
if ((method === "GET" || method === "POST") && path.startsWith(candidatePrefix))
|
|
169
|
+
return operationSuffix(path, candidatePrefix);
|
|
170
|
+
}
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
function redact(value) {
|
|
174
|
+
if (Array.isArray(value))
|
|
175
|
+
return value.map(redact);
|
|
176
|
+
const source = object(value);
|
|
177
|
+
if (source === undefined)
|
|
178
|
+
return value;
|
|
179
|
+
return Object.fromEntries(Object.entries(source).map(([key, item]) => [
|
|
180
|
+
key,
|
|
181
|
+
/cookie|token|authorization|session|secret/iu.test(key) ? "[redacted]" : redact(item),
|
|
182
|
+
]));
|
|
183
|
+
}
|
|
184
|
+
function projectModelResponse(kind, status, value) {
|
|
185
|
+
if (status < 200 || status >= 300) {
|
|
186
|
+
const error = object(value.error);
|
|
187
|
+
// embedding 和 chat 都可能在已扣费后丢失 provider 响应;必须保留同一
|
|
188
|
+
// 幂等键的持久终态,不能让本地代理把它降格为可换 key 的普通失败。
|
|
189
|
+
if ((kind === "chat_completions" || kind === "embedding") && error?.code === "E_OPERATION_UNCERTAIN") {
|
|
190
|
+
const projected = Object.fromEntries([
|
|
191
|
+
"operation_id", "status", "ledger_status", "provider_result_sha256", "retryable", "retry_policy",
|
|
192
|
+
].filter((key) => value[key] !== undefined).map((key) => [key, value[key]]));
|
|
193
|
+
return Object.freeze({ ...projected, error: Object.freeze({ code: "E_OPERATION_UNCERTAIN" }) });
|
|
194
|
+
}
|
|
195
|
+
return Object.freeze({});
|
|
196
|
+
}
|
|
197
|
+
if (kind === "session") {
|
|
198
|
+
return Object.freeze(Object.fromEntries(["principal_id", "project_id"]
|
|
199
|
+
.filter((key) => typeof value[key] === "string")
|
|
200
|
+
.map((key) => [key, value[key]])));
|
|
201
|
+
}
|
|
202
|
+
if (kind === "chat_catalog") {
|
|
203
|
+
const projectedModels = Array.isArray(value.models) ? value.models.map((item) => {
|
|
204
|
+
const source = object(item);
|
|
205
|
+
return source === undefined ? null : Object.freeze({
|
|
206
|
+
model: source.model,
|
|
207
|
+
max_output_tokens: source.max_output_tokens,
|
|
208
|
+
});
|
|
209
|
+
}) : value.models;
|
|
210
|
+
return Object.freeze({ ...Object.fromEntries([
|
|
211
|
+
"principal_id", "project_id", "catalog_digest",
|
|
212
|
+
].filter((key) => value[key] !== undefined).map((key) => [key, value[key]])), models: projectedModels });
|
|
213
|
+
}
|
|
214
|
+
if (kind === "embedding_descriptor") {
|
|
215
|
+
const inputKinds = object(value.input_kinds);
|
|
216
|
+
const projectedInputKinds = inputKinds === undefined ? value.input_kinds : Object.freeze(Object.fromEntries(["document", "query"].map((inputKind) => {
|
|
217
|
+
const source = object(inputKinds[inputKind]);
|
|
218
|
+
return [inputKind, source === undefined ? null : Object.freeze({
|
|
219
|
+
prompt_version: source.prompt_version,
|
|
220
|
+
descriptor_sha256: source.descriptor_sha256,
|
|
221
|
+
})];
|
|
222
|
+
})));
|
|
223
|
+
return Object.freeze({ ...Object.fromEntries([
|
|
224
|
+
"principal_id", "project_id", "provider", "model", "dimensions", "max_inputs", "max_input_chars", "descriptor_sha256",
|
|
225
|
+
].filter((key) => value[key] !== undefined).map((key) => [key, value[key]])), input_kinds: projectedInputKinds });
|
|
226
|
+
}
|
|
227
|
+
if (kind === "chat_completions") {
|
|
228
|
+
return Object.freeze(Object.fromEntries([
|
|
229
|
+
"id", "object", "created", "model", "provider", "choices", "usage", "operation_id", "principal_id", "project_id", "cost_status",
|
|
230
|
+
].filter((key) => value[key] !== undefined).map((key) => [key, value[key]])));
|
|
231
|
+
}
|
|
232
|
+
const projectedData = Array.isArray(value.data) ? value.data.map((item) => {
|
|
233
|
+
const source = object(item);
|
|
234
|
+
return source === undefined ? null : Object.freeze({ object: source.object, index: source.index, embedding: source.embedding });
|
|
235
|
+
}) : value.data;
|
|
236
|
+
const sourceUsage = object(value.usage);
|
|
237
|
+
const usage = sourceUsage === undefined ? value.usage : Object.freeze({
|
|
238
|
+
prompt_tokens: sourceUsage.prompt_tokens,
|
|
239
|
+
total_tokens: sourceUsage.total_tokens,
|
|
240
|
+
charged_units: sourceUsage.charged_units,
|
|
241
|
+
currency: sourceUsage.currency,
|
|
242
|
+
ledger_unit_scale: sourceUsage.ledger_unit_scale,
|
|
243
|
+
ledger_version: sourceUsage.ledger_version,
|
|
244
|
+
});
|
|
245
|
+
return Object.freeze({
|
|
246
|
+
object: value.object,
|
|
247
|
+
data: projectedData,
|
|
248
|
+
model: value.model,
|
|
249
|
+
provider: value.provider,
|
|
250
|
+
dimensions: value.dimensions,
|
|
251
|
+
input_kind: value.input_kind,
|
|
252
|
+
usage,
|
|
253
|
+
operation_id: value.operation_id,
|
|
254
|
+
principal_id: value.principal_id,
|
|
255
|
+
project_id: value.project_id,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
function requestBytes(body) {
|
|
259
|
+
if (body === undefined)
|
|
260
|
+
return new Uint8Array();
|
|
261
|
+
if (body instanceof Uint8Array)
|
|
262
|
+
return body;
|
|
263
|
+
let encoded;
|
|
264
|
+
try {
|
|
265
|
+
encoded = JSON.stringify(body);
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
fail("E_VALIDATION_FAILED", "transfer body 无法编码");
|
|
269
|
+
}
|
|
270
|
+
if (encoded === undefined)
|
|
271
|
+
fail("E_VALIDATION_FAILED", "transfer body 无法编码");
|
|
272
|
+
return new TextEncoder().encode(encoded);
|
|
273
|
+
}
|
|
274
|
+
function assertTransferRequest(request, projectId, bytes) {
|
|
275
|
+
if (request.project_id !== projectId)
|
|
276
|
+
fail("E_AUTH_REQUIRED", "transfer project identity 不匹配");
|
|
277
|
+
if (!isLocalGuiTransferRouteAllowed(request.method, request.path))
|
|
278
|
+
fail("E_AUTH_REQUIRED", "transfer route 未获授权");
|
|
279
|
+
if ((request.method === "GET" || request.method === "HEAD") && (request.body !== undefined || request.body_digest !== undefined || request.body_size !== undefined)) {
|
|
280
|
+
fail("E_VALIDATION_FAILED", "GET/HEAD transfer 请求不得携带 body identity");
|
|
281
|
+
}
|
|
282
|
+
if (bytes.byteLength > MAX_TRANSFER_REQUEST_BYTES)
|
|
283
|
+
fail("E_VALIDATION_FAILED", "transfer 请求过大");
|
|
284
|
+
if (request.body_size !== undefined && request.body_size !== bytes.byteLength)
|
|
285
|
+
fail("E_VALIDATION_FAILED", "transfer body size 不匹配");
|
|
286
|
+
if (request.body !== undefined && request.body_digest !== sha256(bytes))
|
|
287
|
+
fail("E_VALIDATION_FAILED", "transfer body digest 不匹配");
|
|
288
|
+
if (request.operation_id !== undefined && (request.operation_id.length === 0 || request.operation_id.length > 256))
|
|
289
|
+
fail("E_VALIDATION_FAILED", "transfer operation_id 无效");
|
|
290
|
+
const rawCount = Number(request.raw_body_digest !== undefined) + Number(request.raw_body_size !== undefined) + Number(request.body_codec !== undefined);
|
|
291
|
+
const staging = request.method === "PUT" && (/^\/v1\/creator\/project-transfer\/(?:upload-)?staging\//u.test(request.path));
|
|
292
|
+
if (rawCount !== 0 && (rawCount !== 3 || !staging || !SHA256_PATTERN.test(request.raw_body_digest ?? "")
|
|
293
|
+
|| request.raw_body_size === undefined || request.raw_body_size <= 0 || request.raw_body_size > 4 * 1024 * 1024
|
|
294
|
+
|| !["identity-v1", "gzip-member-v1"].includes(request.body_codec ?? ""))) {
|
|
295
|
+
fail("E_VALIDATION_FAILED", "transfer raw body identity 无效");
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
export function createLocalGuiAuth(options) {
|
|
299
|
+
if (!PROJECT_ID_PATTERN.test(options.projectId))
|
|
300
|
+
fail("E_SCHEMA_INVALID", "local GUI project_id 无效");
|
|
301
|
+
const loopback = normalizeLoopbackOrigin(options.loopbackOrigin);
|
|
302
|
+
const request = options.fetch ?? globalThis.fetch;
|
|
303
|
+
if (typeof request !== "function")
|
|
304
|
+
fail("E_SCHEMA_INVALID", "Node fetch 不可用");
|
|
305
|
+
let pending;
|
|
306
|
+
let pendingRemoteCancel;
|
|
307
|
+
let cookie;
|
|
308
|
+
let sessionEpoch = 0;
|
|
309
|
+
const authFetch = async (path, body) => {
|
|
310
|
+
try {
|
|
311
|
+
return await request(`${API_ORIGIN}${path}`, {
|
|
312
|
+
method: "POST", redirect: "manual", headers: { "content-type": "application/json" }, body: JSON.stringify(body),
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
fail("E_AUTH_REQUIRED", "远端认证服务不可用");
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
/**
|
|
320
|
+
* logout 绝不能因为远端暂时不可达而回滚本地 signed_out。失败只留下不可见的
|
|
321
|
+
* cancel identity,供本 auth instance 的下一次 logout 原样重试。
|
|
322
|
+
*/
|
|
323
|
+
const cancelRemoteChallenge = async (challenge) => {
|
|
324
|
+
let response;
|
|
325
|
+
try {
|
|
326
|
+
response = await request(`${API_ORIGIN}/v1/auth/desktop-handoff/cancel`, {
|
|
327
|
+
method: "POST", redirect: "manual", headers: { "content-type": "application/json" },
|
|
328
|
+
body: JSON.stringify({ requestId: challenge.requestId, projectId: options.projectId, state: challenge.state }),
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
catch {
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
await response.body?.cancel().catch(() => undefined);
|
|
335
|
+
return response.ok;
|
|
336
|
+
};
|
|
337
|
+
const status = () => {
|
|
338
|
+
if (pending !== undefined && pending.expiresAtMs <= Date.now())
|
|
339
|
+
pending = undefined;
|
|
340
|
+
return Object.freeze({
|
|
341
|
+
state: cookie !== undefined ? "signed_in" : pending !== undefined ? "challenge_pending" : "signed_out",
|
|
342
|
+
project_id: options.projectId,
|
|
343
|
+
...(pending === undefined ? {} : { expires_at_ms: pending.expiresAtMs }),
|
|
344
|
+
});
|
|
345
|
+
};
|
|
346
|
+
const consumeRemoteResponse = async (response, dispatchedEpoch) => {
|
|
347
|
+
if (response.status === 401) {
|
|
348
|
+
await response.body?.cancel().catch(() => undefined);
|
|
349
|
+
if (sessionEpoch === dispatchedEpoch) {
|
|
350
|
+
cookie = undefined;
|
|
351
|
+
sessionEpoch += 1;
|
|
352
|
+
}
|
|
353
|
+
fail("E_AUTH_REQUIRED", "远端 session 已失效");
|
|
354
|
+
}
|
|
355
|
+
if (response.status < 200 || response.status >= 300)
|
|
356
|
+
await response.body?.cancel().catch(() => undefined);
|
|
357
|
+
if (response.status === 403)
|
|
358
|
+
fail("E_REMOTE_FORBIDDEN", "当前账户无权读取绑定剧本评分");
|
|
359
|
+
if (response.status === 404)
|
|
360
|
+
fail("E_REFERENCE_MISSING", "绑定的远端剧本不存在");
|
|
361
|
+
if (response.status === 409)
|
|
362
|
+
fail("E_OPERATION_UNCERTAIN", "远端评分基准正在更新");
|
|
363
|
+
if (response.status < 200 || response.status >= 300)
|
|
364
|
+
fail("E_OPERATION_UNCERTAIN", "远端评分服务暂时不可用");
|
|
365
|
+
const responseBytes = await boundedBytes(response, MAX_TRANSFER_RESPONSE_BYTES, "远端响应过大或读取失败");
|
|
366
|
+
try {
|
|
367
|
+
return redact(JSON.parse(new TextDecoder().decode(responseBytes)));
|
|
368
|
+
}
|
|
369
|
+
catch {
|
|
370
|
+
fail("E_VALIDATION_FAILED", "远端响应不是有效 JSON");
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
const transport = Object.freeze({
|
|
374
|
+
async request(transferRequest) {
|
|
375
|
+
if (cookie === undefined)
|
|
376
|
+
fail("E_AUTH_REQUIRED", "local GUI 尚未建立远端 session");
|
|
377
|
+
const bytes = requestBytes(transferRequest.body);
|
|
378
|
+
assertTransferRequest(transferRequest, options.projectId, bytes);
|
|
379
|
+
const headers = new Headers({
|
|
380
|
+
cookie: cookie.pair,
|
|
381
|
+
"x-worldengine-project-id": options.projectId,
|
|
382
|
+
"x-worldengine-operation-id": transferRequest.operation_id ?? "",
|
|
383
|
+
});
|
|
384
|
+
if (transferRequest.content_type !== undefined)
|
|
385
|
+
headers.set("content-type", transferRequest.content_type);
|
|
386
|
+
else if (transferRequest.body !== undefined)
|
|
387
|
+
headers.set("content-type", transferRequest.body instanceof Uint8Array ? "application/octet-stream" : "application/json");
|
|
388
|
+
if (transferRequest.raw_body_digest !== undefined) {
|
|
389
|
+
headers.set("x-worldengine-raw-sha256", transferRequest.raw_body_digest);
|
|
390
|
+
headers.set("x-worldengine-raw-byte-length", String(transferRequest.raw_body_size));
|
|
391
|
+
headers.set("x-worldengine-chunk-codec", transferRequest.body_codec ?? "");
|
|
392
|
+
}
|
|
393
|
+
let response;
|
|
394
|
+
try {
|
|
395
|
+
response = await request(`${API_ORIGIN}${transferRequest.path}`, {
|
|
396
|
+
method: transferRequest.method, redirect: "manual", headers,
|
|
397
|
+
...(transferRequest.body === undefined ? {} : { body: bytes }),
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
catch {
|
|
401
|
+
fail("E_AUTH_REQUIRED", "远端 transfer 服务不可用");
|
|
402
|
+
}
|
|
403
|
+
if (response.status === 401)
|
|
404
|
+
cookie = undefined;
|
|
405
|
+
const responseBytes = await boundedBytes(response, MAX_TRANSFER_RESPONSE_BYTES, "transfer 响应过大或读取失败");
|
|
406
|
+
let body;
|
|
407
|
+
try {
|
|
408
|
+
body = redact(JSON.parse(new TextDecoder().decode(responseBytes)));
|
|
409
|
+
}
|
|
410
|
+
catch {
|
|
411
|
+
body = { contentBase64: Buffer.from(responseBytes).toString("base64") };
|
|
412
|
+
}
|
|
413
|
+
const headerDigest = response.headers.get("x-worldengine-sha256");
|
|
414
|
+
const headerSize = Number(response.headers.get("x-worldengine-byte-length") ?? response.headers.get("content-length"));
|
|
415
|
+
return {
|
|
416
|
+
status: response.status,
|
|
417
|
+
body,
|
|
418
|
+
readback: {
|
|
419
|
+
sha256: headerDigest !== null && SHA256_PATTERN.test(headerDigest) ? headerDigest : sha256(responseBytes),
|
|
420
|
+
byte_length: Number.isSafeInteger(headerSize) && headerSize >= 0 ? headerSize : responseBytes.byteLength,
|
|
421
|
+
},
|
|
422
|
+
};
|
|
423
|
+
},
|
|
424
|
+
});
|
|
425
|
+
return Object.freeze({
|
|
426
|
+
status,
|
|
427
|
+
async account() {
|
|
428
|
+
const dispatchedCookie = cookie;
|
|
429
|
+
const dispatchedEpoch = sessionEpoch;
|
|
430
|
+
if (dispatchedCookie === undefined)
|
|
431
|
+
return status();
|
|
432
|
+
const read = async (path, decoder) => {
|
|
433
|
+
const response = await request(`${API_ORIGIN}${path}`, {
|
|
434
|
+
method: "GET", redirect: "manual", signal: AbortSignal.timeout(10_000),
|
|
435
|
+
headers: { accept: "application/x-protobuf", cookie: dispatchedCookie.pair },
|
|
436
|
+
});
|
|
437
|
+
if (response.status === 401 && sessionEpoch === dispatchedEpoch && cookie === dispatchedCookie) {
|
|
438
|
+
cookie = undefined;
|
|
439
|
+
sessionEpoch += 1;
|
|
440
|
+
}
|
|
441
|
+
if (!response.ok || !/^application\/(?:x-)?protobuf(?:;|$)/iu.test(response.headers.get("content-type") ?? "")) {
|
|
442
|
+
await response.body?.cancel().catch(() => undefined);
|
|
443
|
+
fail("E_OPERATION_UNCERTAIN", "账户资料暂时不可用");
|
|
444
|
+
}
|
|
445
|
+
return decoder.decode(await boundedBytes(response, MAX_AUTH_RESPONSE_BYTES, "账户资料响应无效"));
|
|
446
|
+
};
|
|
447
|
+
const [profile, wallet] = await Promise.allSettled([
|
|
448
|
+
read("/v1/auth/me", MeResponse), read("/v1/wallet", WalletResponse),
|
|
449
|
+
]);
|
|
450
|
+
// 注销或切换后,旧请求既不能显示旧身份,也不能使新账户退出。
|
|
451
|
+
if (sessionEpoch !== dispatchedEpoch || cookie !== dispatchedCookie)
|
|
452
|
+
return status();
|
|
453
|
+
const identity = profile.status === "fulfilled" && profile.value.error === undefined ? profile.value.current_identity : undefined;
|
|
454
|
+
const displayName = typeof identity?.user_id === "string" && identity.user_id.length > 0
|
|
455
|
+
&& typeof identity.display_name === "string" && identity.display_name.trim().length > 0 ? identity.display_name : undefined;
|
|
456
|
+
const balance = wallet.status === "fulfilled" && wallet.value.error === undefined && wallet.value.currency === "CNY"
|
|
457
|
+
&& /^(?:0|[1-9]\d*)$/u.test(wallet.value.top_up_balance_units)
|
|
458
|
+
&& /^(?:0|[1-9]\d*)$/u.test(wallet.value.credit_balance_units)
|
|
459
|
+
? (BigInt(wallet.value.top_up_balance_units) + BigInt(wallet.value.credit_balance_units)).toString() : undefined;
|
|
460
|
+
return Object.freeze({ ...status(), account: Object.freeze({
|
|
461
|
+
profile_state: displayName === undefined ? "unavailable" : "ready",
|
|
462
|
+
wallet_state: balance === undefined ? "unavailable" : "ready",
|
|
463
|
+
...(displayName === undefined ? {} : { display_name: displayName }),
|
|
464
|
+
...(balance === undefined ? {} : { balance_units: balance }),
|
|
465
|
+
}) });
|
|
466
|
+
},
|
|
467
|
+
async start() {
|
|
468
|
+
if (cookie !== undefined)
|
|
469
|
+
fail("E_AUTH_REQUIRED", "local GUI 已登录");
|
|
470
|
+
if (pendingRemoteCancel !== undefined)
|
|
471
|
+
fail("E_AUTH_REQUIRED", "local GUI 注销正在等待远端确认");
|
|
472
|
+
if (pending !== undefined && pending.expiresAtMs > Date.now())
|
|
473
|
+
fail("E_AUTH_REQUIRED", "local GUI 已有待完成的认证 challenge");
|
|
474
|
+
const state = randomPkce();
|
|
475
|
+
const verifier = randomPkce();
|
|
476
|
+
const draft = { requestId: "", state, verifier, expiresAtMs: 0 };
|
|
477
|
+
const response = await authFetch("/v1/auth/desktop-handoff/start", {
|
|
478
|
+
state,
|
|
479
|
+
pkceChallenge: createHash("sha256").update(verifier).digest("base64url"),
|
|
480
|
+
loopbackPort: Number(loopback.port),
|
|
481
|
+
deviceName: "WorldEngine Project",
|
|
482
|
+
projectId: options.projectId,
|
|
483
|
+
});
|
|
484
|
+
if (response.status !== 200 && response.status !== 201)
|
|
485
|
+
fail("E_AUTH_REQUIRED", "远端认证服务拒绝 handoff start");
|
|
486
|
+
const value = await boundedJson(response, MAX_AUTH_RESPONSE_BYTES, "远端认证服务 start 响应无效");
|
|
487
|
+
if (typeof value.requestId !== "string" || !UUID_PATTERN.test(value.requestId)
|
|
488
|
+
|| value.projectId !== options.projectId || !Number.isSafeInteger(value.expiresAt)
|
|
489
|
+
|| value.expiresAt <= Date.now() || value.expiresAt > Date.now() + 10 * 60_000) {
|
|
490
|
+
fail("E_VALIDATION_FAILED", "远端认证服务 start 响应未绑定当前项目");
|
|
491
|
+
}
|
|
492
|
+
draft.requestId = value.requestId;
|
|
493
|
+
draft.expiresAtMs = value.expiresAt;
|
|
494
|
+
if (!PKCE_PATTERN.test(state) || !PKCE_PATTERN.test(verifier))
|
|
495
|
+
fail("E_VALIDATION_FAILED", "本地 PKCE 生成失败");
|
|
496
|
+
const authorizationUrl = assertAuthorizationUrl(value.authorizationUrl, draft);
|
|
497
|
+
pending = Object.freeze(draft);
|
|
498
|
+
if (options.openBrowser !== undefined) {
|
|
499
|
+
try {
|
|
500
|
+
await options.openBrowser(authorizationUrl);
|
|
501
|
+
}
|
|
502
|
+
catch {
|
|
503
|
+
fail("E_AUTH_REQUIRED", "系统浏览器无法打开认证页面");
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
return Object.freeze({ state: "challenge_pending", project_id: options.projectId, expires_at_ms: draft.expiresAtMs, authorization_url: authorizationUrl });
|
|
507
|
+
},
|
|
508
|
+
async handleCallback(callbackUrl) {
|
|
509
|
+
const challenge = pending;
|
|
510
|
+
if (challenge === undefined || challenge.expiresAtMs <= Date.now()) {
|
|
511
|
+
pending = undefined;
|
|
512
|
+
fail("E_AUTH_REQUIRED", "local GUI 没有有效的认证 challenge");
|
|
513
|
+
}
|
|
514
|
+
let callback;
|
|
515
|
+
try {
|
|
516
|
+
callback = callbackUrl instanceof URL ? new URL(callbackUrl.toString()) : new URL(callbackUrl);
|
|
517
|
+
}
|
|
518
|
+
catch {
|
|
519
|
+
fail("E_AUTH_REQUIRED", "认证 callback 无效");
|
|
520
|
+
}
|
|
521
|
+
const keys = [...callback.searchParams.keys()];
|
|
522
|
+
if (callback.origin !== loopback.origin || callback.pathname !== CALLBACK_PATH || callback.hash !== ""
|
|
523
|
+
|| keys.length !== 3 || new Set(keys).size !== 3
|
|
524
|
+
|| callback.searchParams.get("request_id") !== challenge.requestId
|
|
525
|
+
|| callback.searchParams.get("state") !== challenge.state
|
|
526
|
+
|| !/^\d{6}$/u.test(callback.searchParams.get("otp") ?? "")) {
|
|
527
|
+
fail("E_AUTH_REQUIRED", "认证 callback 未通过 state/loopback 校验");
|
|
528
|
+
}
|
|
529
|
+
const exchangeEpoch = sessionEpoch;
|
|
530
|
+
pending = undefined;
|
|
531
|
+
const response = await authFetch("/v1/auth/desktop-handoff/exchange", {
|
|
532
|
+
requestId: challenge.requestId,
|
|
533
|
+
projectId: options.projectId,
|
|
534
|
+
state: challenge.state,
|
|
535
|
+
codeVerifier: challenge.verifier,
|
|
536
|
+
otp: callback.searchParams.get("otp"),
|
|
537
|
+
});
|
|
538
|
+
if (!response.ok)
|
|
539
|
+
fail("E_AUTH_REQUIRED", "远端认证服务拒绝 handoff exchange");
|
|
540
|
+
const nextCookie = parseHttpOnlyCookie(response.headers.get("set-cookie"));
|
|
541
|
+
const value = await boundedJson(response, MAX_AUTH_RESPONSE_BYTES, "远端认证服务 exchange 响应无效");
|
|
542
|
+
const device = object(value.device);
|
|
543
|
+
if (value.requestId !== challenge.requestId || value.projectId !== options.projectId || value.status !== "authenticated"
|
|
544
|
+
|| typeof device?.id !== "string" || !UUID_PATTERN.test(device.id)) {
|
|
545
|
+
fail("E_VALIDATION_FAILED", "远端认证服务 exchange 响应未绑定当前项目");
|
|
546
|
+
}
|
|
547
|
+
if (sessionEpoch !== exchangeEpoch || pending !== undefined || cookie !== undefined) {
|
|
548
|
+
fail("E_AUTH_REQUIRED", "认证 exchange 返回时本地 session authority 已变化");
|
|
549
|
+
}
|
|
550
|
+
cookie = nextCookie;
|
|
551
|
+
sessionEpoch += 1;
|
|
552
|
+
return status();
|
|
553
|
+
},
|
|
554
|
+
async logout() {
|
|
555
|
+
const challenge = pendingRemoteCancel ?? pending;
|
|
556
|
+
pending = undefined;
|
|
557
|
+
cookie = undefined;
|
|
558
|
+
sessionEpoch += 1;
|
|
559
|
+
if (challenge === undefined || challenge.expiresAtMs <= Date.now()) {
|
|
560
|
+
pendingRemoteCancel = undefined;
|
|
561
|
+
return Object.freeze({ ...status(), remote_cancel: "not_required" });
|
|
562
|
+
}
|
|
563
|
+
if (await cancelRemoteChallenge(challenge)) {
|
|
564
|
+
pendingRemoteCancel = undefined;
|
|
565
|
+
return Object.freeze({ ...status(), remote_cancel: "confirmed" });
|
|
566
|
+
}
|
|
567
|
+
pendingRemoteCancel = challenge;
|
|
568
|
+
return Object.freeze({ ...status(), remote_cancel: "pending_retry" });
|
|
569
|
+
},
|
|
570
|
+
async requestModel(modelRequest) {
|
|
571
|
+
if (cookie === undefined)
|
|
572
|
+
fail("E_AUTH_REQUIRED", "local GUI 尚未建立远端 session");
|
|
573
|
+
if (typeof modelRequest !== "object" || modelRequest === null || Array.isArray(modelRequest)) {
|
|
574
|
+
fail("E_SCHEMA_INVALID", "账户模型请求无效");
|
|
575
|
+
}
|
|
576
|
+
const keys = Object.keys(modelRequest).sort().join("\0");
|
|
577
|
+
const sessionRequest = modelRequest.kind === "session" && keys === "kind";
|
|
578
|
+
const leaseReadRequest = (modelRequest.kind === "chat_catalog" || modelRequest.kind === "embedding_descriptor")
|
|
579
|
+
&& keys === "kind\0session";
|
|
580
|
+
const executionRequest = (modelRequest.kind === "embedding" || modelRequest.kind === "chat_completions")
|
|
581
|
+
&& keys === "body\0idempotency_key\0kind\0session"
|
|
582
|
+
&& IDEMPOTENCY_KEY_PATTERN.test(modelRequest.idempotency_key)
|
|
583
|
+
&& object(modelRequest.body) !== undefined;
|
|
584
|
+
if (!sessionRequest && !leaseReadRequest && !executionRequest)
|
|
585
|
+
fail("E_SCHEMA_INVALID", "账户模型请求字段无效");
|
|
586
|
+
let dispatchedCookie;
|
|
587
|
+
let dispatchedEpoch;
|
|
588
|
+
let issuedLease;
|
|
589
|
+
if (modelRequest.kind === "session") {
|
|
590
|
+
dispatchedCookie = cookie;
|
|
591
|
+
dispatchedEpoch = sessionEpoch;
|
|
592
|
+
}
|
|
593
|
+
else {
|
|
594
|
+
const binding = MODEL_SESSION_LEASES.get(modelRequest.session);
|
|
595
|
+
if (binding === undefined || binding.projectId !== options.projectId || binding.epoch !== sessionEpoch || binding.cookie !== cookie) {
|
|
596
|
+
fail("E_AUTH_REQUIRED", "账户模型 session lease 已失效");
|
|
597
|
+
}
|
|
598
|
+
dispatchedCookie = binding.cookie;
|
|
599
|
+
dispatchedEpoch = binding.epoch;
|
|
600
|
+
}
|
|
601
|
+
const headers = new Headers({
|
|
602
|
+
accept: "application/json",
|
|
603
|
+
cookie: dispatchedCookie.pair,
|
|
604
|
+
"x-worldengine-project-id": options.projectId,
|
|
605
|
+
});
|
|
606
|
+
let path;
|
|
607
|
+
let init;
|
|
608
|
+
if (modelRequest.kind === "session" || modelRequest.kind === "chat_catalog" || modelRequest.kind === "embedding_descriptor") {
|
|
609
|
+
path = modelRequest.kind === "session"
|
|
610
|
+
? ACCOUNT_MODEL_SESSION_PATH
|
|
611
|
+
: modelRequest.kind === "chat_catalog"
|
|
612
|
+
? ACCOUNT_MODEL_CHAT_CATALOG_PATH
|
|
613
|
+
: ACCOUNT_MODEL_EMBEDDING_DESCRIPTOR_PATH;
|
|
614
|
+
init = { method: "GET", redirect: "manual", headers };
|
|
615
|
+
}
|
|
616
|
+
else {
|
|
617
|
+
headers.set("content-type", "application/json");
|
|
618
|
+
headers.set("idempotency-key", modelRequest.idempotency_key);
|
|
619
|
+
path = modelRequest.kind === "embedding" ? ACCOUNT_MODEL_EMBEDDING_PATH : ACCOUNT_MODEL_CHAT_COMPLETIONS_PATH;
|
|
620
|
+
init = { method: "POST", redirect: "manual", headers, body: JSON.stringify(modelRequest.body) };
|
|
621
|
+
}
|
|
622
|
+
let response;
|
|
623
|
+
try {
|
|
624
|
+
response = await request(`${API_ORIGIN}${path}`, init);
|
|
625
|
+
}
|
|
626
|
+
catch {
|
|
627
|
+
fail("E_OPERATION_UNCERTAIN", "远端账户模型服务暂时不可用");
|
|
628
|
+
}
|
|
629
|
+
if (response.status === 401 && sessionEpoch === dispatchedEpoch) {
|
|
630
|
+
cookie = undefined;
|
|
631
|
+
sessionEpoch += 1;
|
|
632
|
+
}
|
|
633
|
+
let body = await boundedJson(response, MAX_MODEL_RESPONSE_BYTES, "远端账户模型响应无效");
|
|
634
|
+
const operationId = response.headers.get("x-world-engines-operation-id");
|
|
635
|
+
if (operationId !== null) {
|
|
636
|
+
if (!/^[A-Za-z0-9._:-]{1,256}$/u.test(operationId)
|
|
637
|
+
|| (body.operation_id !== undefined && body.operation_id !== operationId)) {
|
|
638
|
+
fail("E_VALIDATION_FAILED", "远端账户模型 operation identity 无效");
|
|
639
|
+
}
|
|
640
|
+
body = { ...body, operation_id: operationId };
|
|
641
|
+
}
|
|
642
|
+
if (sessionEpoch !== dispatchedEpoch || cookie !== dispatchedCookie)
|
|
643
|
+
fail("E_AUTH_REQUIRED", "账户模型 session 在请求期间已变化");
|
|
644
|
+
const projected = projectModelResponse(modelRequest.kind, response.status, body);
|
|
645
|
+
if (modelRequest.kind === "session" && response.status === 200) {
|
|
646
|
+
const principalId = projected.principal_id;
|
|
647
|
+
const projectId = projected.project_id;
|
|
648
|
+
if (typeof principalId !== "string" || typeof projectId !== "string" || projectId !== options.projectId)
|
|
649
|
+
fail("E_VALIDATION_FAILED", "账户模型 session 响应无效");
|
|
650
|
+
issuedLease = Object.freeze({});
|
|
651
|
+
MODEL_SESSION_LEASES.set(issuedLease, Object.freeze({ epoch: dispatchedEpoch, cookie: dispatchedCookie, principalId, projectId }));
|
|
652
|
+
}
|
|
653
|
+
return Object.freeze({ kind: modelRequest.kind, status: response.status, body: projected, ...(issuedLease === undefined ? {} : { session: issuedLease }) });
|
|
654
|
+
},
|
|
655
|
+
async requestScore(scoreRequest) {
|
|
656
|
+
if (cookie === undefined)
|
|
657
|
+
fail("E_AUTH_REQUIRED", "local GUI 尚未建立远端 session");
|
|
658
|
+
if (options.projectRoot === undefined)
|
|
659
|
+
fail("E_REFERENCE_MISSING", "local GUI 未配置项目根,无法读取远端评分绑定");
|
|
660
|
+
if (typeof scoreRequest !== "object" || scoreRequest === null || Array.isArray(scoreRequest))
|
|
661
|
+
fail("E_SCHEMA_INVALID", "评分请求无效");
|
|
662
|
+
const keys = Object.keys(scoreRequest).sort().join("\0");
|
|
663
|
+
if ((scoreRequest.kind === "score" && keys !== "kind")
|
|
664
|
+
|| (scoreRequest.kind === "tag-benchmark" && keys !== "kind\0tag")
|
|
665
|
+
|| (scoreRequest.kind !== "score" && scoreRequest.kind !== "tag-benchmark"))
|
|
666
|
+
fail("E_SCHEMA_INVALID", "评分请求字段无效");
|
|
667
|
+
if (scoreRequest.kind === "tag-benchmark" && !isKnownScenarioTag(scoreRequest.tag))
|
|
668
|
+
fail("E_SCHEMA_INVALID", "评分 tag 不在 canonical 字典");
|
|
669
|
+
let binding;
|
|
670
|
+
try {
|
|
671
|
+
binding = await readFrozenReviewTargetBinding(options.projectRoot, options.projectId);
|
|
672
|
+
}
|
|
673
|
+
catch (error) {
|
|
674
|
+
if (error instanceof AuthoringBridgeError && (error.code === "E_OPERATION_ID_CONFLICT" || error.code === "E_PATH_OUTSIDE_PROJECT"))
|
|
675
|
+
throw error;
|
|
676
|
+
fail("E_REFERENCE_MISSING", "当前项目尚未绑定远端评分剧本");
|
|
677
|
+
}
|
|
678
|
+
if (!/^[A-Za-z0-9_-]{1,256}$/u.test(binding.scenario_id))
|
|
679
|
+
fail("E_REFERENCE_MISSING", "远端评分剧本绑定无效");
|
|
680
|
+
const scenario = encodeURIComponent(binding.scenario_id);
|
|
681
|
+
const path = scoreRequest.kind === "score"
|
|
682
|
+
? `/v1/scenarios/${scenario}/score`
|
|
683
|
+
: `/v1/scenarios/${scenario}/score/tag-benchmark?${new URLSearchParams({ tag: scoreRequest.tag }).toString()}`;
|
|
684
|
+
let response;
|
|
685
|
+
const dispatchedEpoch = sessionEpoch;
|
|
686
|
+
try {
|
|
687
|
+
response = await request(`${API_ORIGIN}${path}`, {
|
|
688
|
+
method: "GET",
|
|
689
|
+
redirect: "manual",
|
|
690
|
+
headers: {
|
|
691
|
+
cookie: cookie.pair,
|
|
692
|
+
"x-worldengine-project-id": options.projectId,
|
|
693
|
+
},
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
catch {
|
|
697
|
+
fail("E_OPERATION_UNCERTAIN", "远端评分服务暂时不可用");
|
|
698
|
+
}
|
|
699
|
+
return Object.freeze({ kind: scoreRequest.kind, body: await consumeRemoteResponse(response, dispatchedEpoch) });
|
|
700
|
+
},
|
|
701
|
+
transport,
|
|
702
|
+
});
|
|
703
|
+
}
|