@fa-software/semi2-dsh 0.0.10-preview.20260920042705
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/cordis.patch.yml +5 -0
- package/lib/client.js +351 -0
- package/lib/client.js.map +1 -0
- package/lib/codegraph/cli.js +142 -0
- package/lib/codegraph/config.js +22 -0
- package/lib/codegraph/ensure.js +25 -0
- package/lib/codegraph/guide.js +21 -0
- package/lib/codegraph/index.js +97 -0
- package/lib/codegraph/tools.js +227 -0
- package/lib/codegraph/workspace.js +52 -0
- package/lib/configuration/compile.js +177 -0
- package/lib/configuration/credentials.js +104 -0
- package/lib/configuration/generated/graphql.js +1 -0
- package/lib/configuration/generated/schema.js +95 -0
- package/lib/configuration/graphql.js +193 -0
- package/lib/configuration/index.js +29 -0
- package/lib/configuration/login.js +96 -0
- package/lib/configuration/runtime.js +1100 -0
- package/lib/configuration/skill-resources.js +218 -0
- package/lib/configuration/skills.js +185 -0
- package/lib/configuration/status.js +57 -0
- package/lib/configuration/workspace.js +55 -0
- package/lib/configuration/workspaces.js +169 -0
- package/lib/contract.js +42 -0
- package/lib/index.js +14 -0
- package/lib/types/codegraph/cli.d.ts +20 -0
- package/lib/types/codegraph/config.d.ts +10 -0
- package/lib/types/codegraph/ensure.d.ts +3 -0
- package/lib/types/codegraph/guide.d.ts +4 -0
- package/lib/types/codegraph/index.d.ts +5 -0
- package/lib/types/codegraph/tools.d.ts +17 -0
- package/lib/types/codegraph/workspace.d.ts +8 -0
- package/lib/types/configuration/compile.d.ts +81 -0
- package/lib/types/configuration/credentials.d.ts +35 -0
- package/lib/types/configuration/generated/graphql.d.ts +100 -0
- package/lib/types/configuration/generated/schema.d.ts +1003 -0
- package/lib/types/configuration/graphql.d.ts +46 -0
- package/lib/types/configuration/index.d.ts +9 -0
- package/lib/types/configuration/login.d.ts +22 -0
- package/lib/types/configuration/runtime.d.ts +320 -0
- package/lib/types/configuration/skill-resources.d.ts +14 -0
- package/lib/types/configuration/skills.d.ts +8 -0
- package/lib/types/configuration/status.d.ts +9 -0
- package/lib/types/configuration/workspace.d.ts +7 -0
- package/lib/types/configuration/workspaces.d.ts +96 -0
- package/lib/types/contract.d.ts +32 -0
- package/lib/types/index.d.ts +11 -0
- package/lib/types/web/index.d.ts +2 -0
- package/lib/types/web/login.d.ts +2 -0
- package/lib/types/web/loopback-bridge.d.ts +8 -0
- package/lib/types/web/preview-proxy.d.ts +17 -0
- package/lib/types/web/runtime.d.ts +8 -0
- package/lib/types/web/welcome-ack.d.ts +33 -0
- package/lib/web/index.js +38 -0
- package/lib/web/login.js +79 -0
- package/lib/web/loopback-bridge.js +12 -0
- package/lib/web/preview-proxy.js +201 -0
- package/lib/web/runtime.js +19 -0
- package/lib/web/welcome-ack.js +51 -0
- package/package.json +81 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/** 账户的 CNB 连接态。AWAITING_BINDING = 账户由上游目录预建,还没绑定本人令牌。 */
|
|
2
|
+
export var CnbConnectionState;
|
|
3
|
+
(function (CnbConnectionState) {
|
|
4
|
+
CnbConnectionState["AwaitingBinding"] = "AWAITING_BINDING";
|
|
5
|
+
CnbConnectionState["Connected"] = "CONNECTED";
|
|
6
|
+
CnbConnectionState["ReconnectRequired"] = "RECONNECT_REQUIRED";
|
|
7
|
+
})(CnbConnectionState || (CnbConnectionState = {}));
|
|
8
|
+
export var DiscoveryErrorCode;
|
|
9
|
+
(function (DiscoveryErrorCode) {
|
|
10
|
+
DiscoveryErrorCode["CredentialsNotConfigured"] = "CREDENTIALS_NOT_CONFIGURED";
|
|
11
|
+
DiscoveryErrorCode["DiscoveryUnsupported"] = "DISCOVERY_UNSUPPORTED";
|
|
12
|
+
DiscoveryErrorCode["InvalidModelCatalog"] = "INVALID_MODEL_CATALOG";
|
|
13
|
+
DiscoveryErrorCode["InvalidModelUrl"] = "INVALID_MODEL_URL";
|
|
14
|
+
DiscoveryErrorCode["ModelCatalogTooLarge"] = "MODEL_CATALOG_TOO_LARGE";
|
|
15
|
+
DiscoveryErrorCode["ModelUrlForbidden"] = "MODEL_URL_FORBIDDEN";
|
|
16
|
+
DiscoveryErrorCode["UpstreamCredentialsRejected"] = "UPSTREAM_CREDENTIALS_REJECTED";
|
|
17
|
+
DiscoveryErrorCode["UpstreamPaginationError"] = "UPSTREAM_PAGINATION_ERROR";
|
|
18
|
+
DiscoveryErrorCode["UpstreamRedirectForbidden"] = "UPSTREAM_REDIRECT_FORBIDDEN";
|
|
19
|
+
DiscoveryErrorCode["UpstreamResponseTooLarge"] = "UPSTREAM_RESPONSE_TOO_LARGE";
|
|
20
|
+
DiscoveryErrorCode["UpstreamTimeout"] = "UPSTREAM_TIMEOUT";
|
|
21
|
+
DiscoveryErrorCode["UpstreamUnavailable"] = "UPSTREAM_UNAVAILABLE";
|
|
22
|
+
})(DiscoveryErrorCode || (DiscoveryErrorCode = {}));
|
|
23
|
+
export var DiscoveryStatus;
|
|
24
|
+
(function (DiscoveryStatus) {
|
|
25
|
+
DiscoveryStatus["Failed"] = "FAILED";
|
|
26
|
+
DiscoveryStatus["Never"] = "NEVER";
|
|
27
|
+
DiscoveryStatus["Stale"] = "STALE";
|
|
28
|
+
DiscoveryStatus["Success"] = "SUCCESS";
|
|
29
|
+
})(DiscoveryStatus || (DiscoveryStatus = {}));
|
|
30
|
+
export var InputModality;
|
|
31
|
+
(function (InputModality) {
|
|
32
|
+
InputModality["Image"] = "IMAGE";
|
|
33
|
+
InputModality["Text"] = "TEXT";
|
|
34
|
+
})(InputModality || (InputModality = {}));
|
|
35
|
+
export var MetadataField;
|
|
36
|
+
(function (MetadataField) {
|
|
37
|
+
MetadataField["ContextWindow"] = "CONTEXT_WINDOW";
|
|
38
|
+
MetadataField["Input"] = "INPUT";
|
|
39
|
+
MetadataField["MaxTokens"] = "MAX_TOKENS";
|
|
40
|
+
MetadataField["ReasoningLevels"] = "REASONING_LEVELS";
|
|
41
|
+
})(MetadataField || (MetadataField = {}));
|
|
42
|
+
/** 模型元数据来源的优先级:手工 > 目录(内置或上游发现)> dsh 默认。 */
|
|
43
|
+
export var MetadataSource;
|
|
44
|
+
(function (MetadataSource) {
|
|
45
|
+
MetadataSource["Builtin"] = "BUILTIN";
|
|
46
|
+
MetadataSource["Discovered"] = "DISCOVERED";
|
|
47
|
+
MetadataSource["DshDefault"] = "DSH_DEFAULT";
|
|
48
|
+
MetadataSource["Manual"] = "MANUAL";
|
|
49
|
+
})(MetadataSource || (MetadataSource = {}));
|
|
50
|
+
export var ModelProtocol;
|
|
51
|
+
(function (ModelProtocol) {
|
|
52
|
+
ModelProtocol["Anthropic"] = "ANTHROPIC";
|
|
53
|
+
ModelProtocol["Deepseek"] = "DEEPSEEK";
|
|
54
|
+
ModelProtocol["OpenaiChat"] = "OPENAI_CHAT";
|
|
55
|
+
ModelProtocol["OpenaiResponses"] = "OPENAI_RESPONSES";
|
|
56
|
+
})(ModelProtocol || (ModelProtocol = {}));
|
|
57
|
+
export var ProjectKind;
|
|
58
|
+
(function (ProjectKind) {
|
|
59
|
+
ProjectKind["Cnb"] = "CNB";
|
|
60
|
+
ProjectKind["Local"] = "LOCAL";
|
|
61
|
+
})(ProjectKind || (ProjectKind = {}));
|
|
62
|
+
export var ReasoningLevel;
|
|
63
|
+
(function (ReasoningLevel) {
|
|
64
|
+
ReasoningLevel["High"] = "HIGH";
|
|
65
|
+
ReasoningLevel["Low"] = "LOW";
|
|
66
|
+
ReasoningLevel["Max"] = "MAX";
|
|
67
|
+
ReasoningLevel["Medium"] = "MEDIUM";
|
|
68
|
+
ReasoningLevel["Minimal"] = "MINIMAL";
|
|
69
|
+
ReasoningLevel["Off"] = "OFF";
|
|
70
|
+
ReasoningLevel["Xhigh"] = "XHIGH";
|
|
71
|
+
})(ReasoningLevel || (ReasoningLevel = {}));
|
|
72
|
+
export var Role;
|
|
73
|
+
(function (Role) {
|
|
74
|
+
Role["Admin"] = "ADMIN";
|
|
75
|
+
Role["User"] = "USER";
|
|
76
|
+
})(Role || (Role = {}));
|
|
77
|
+
/** 技能附件内容的编码:文本类用 UTF8,其余(图片 / 二进制)用 BASE64。 */
|
|
78
|
+
export var SkillFileEncoding;
|
|
79
|
+
(function (SkillFileEncoding) {
|
|
80
|
+
SkillFileEncoding["Base64"] = "BASE64";
|
|
81
|
+
SkillFileEncoding["Utf8"] = "UTF8";
|
|
82
|
+
})(SkillFileEncoding || (SkillFileEncoding = {}));
|
|
83
|
+
export var UpstreamStatus;
|
|
84
|
+
(function (UpstreamStatus) {
|
|
85
|
+
UpstreamStatus["Active"] = "ACTIVE";
|
|
86
|
+
UpstreamStatus["Archived"] = "ARCHIVED";
|
|
87
|
+
UpstreamStatus["Forking"] = "FORKING";
|
|
88
|
+
UpstreamStatus["Unavailable"] = "UNAVAILABLE";
|
|
89
|
+
})(UpstreamStatus || (UpstreamStatus = {}));
|
|
90
|
+
export var UserStatus;
|
|
91
|
+
(function (UserStatus) {
|
|
92
|
+
UserStatus["Active"] = "ACTIVE";
|
|
93
|
+
UserStatus["Disabled"] = "DISABLED";
|
|
94
|
+
UserStatus["Pending"] = "PENDING";
|
|
95
|
+
})(UserStatus || (UserStatus = {}));
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// @fa-software/semi2-dsh/configuration/graphql —— 通用 GraphQL client(本包唯一的网络出口)。
|
|
2
|
+
// 错误码一律取 `errors[].extensions.code`(GraphQL 通路恒 HTTP 200);文档唯一来源是 operations.graphql(与常量逐字一致由 test 钉住)。
|
|
3
|
+
/** 只承载 code,绝不把上游内容插值进 message。 */
|
|
4
|
+
export class ConfigurationError extends Error {
|
|
5
|
+
code;
|
|
6
|
+
constructor(code) {
|
|
7
|
+
super(code);
|
|
8
|
+
this.code = code;
|
|
9
|
+
this.name = 'ConfigurationError';
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
/** 启动装配:五个用户级领域问题一个文档(项目绑定按工作区另行按需取,见下)。 */
|
|
13
|
+
export const RUNTIME_BOOT_DOCUMENT = `query RuntimeBoot {
|
|
14
|
+
runtimeViewer {
|
|
15
|
+
id
|
|
16
|
+
username
|
|
17
|
+
displayName
|
|
18
|
+
}
|
|
19
|
+
runtimeModels {
|
|
20
|
+
id
|
|
21
|
+
upstreamId
|
|
22
|
+
displayName
|
|
23
|
+
metadata { contextWindow maxTokens input reasoningLevels }
|
|
24
|
+
connection { id provider protocol baseUrl }
|
|
25
|
+
}
|
|
26
|
+
runtimeDefaultModelId
|
|
27
|
+
runtimeModelCredentials {
|
|
28
|
+
connectionId
|
|
29
|
+
value
|
|
30
|
+
}
|
|
31
|
+
runtimeSkills {
|
|
32
|
+
id
|
|
33
|
+
projectId
|
|
34
|
+
name
|
|
35
|
+
description
|
|
36
|
+
whenToUse
|
|
37
|
+
content
|
|
38
|
+
version
|
|
39
|
+
files {
|
|
40
|
+
path
|
|
41
|
+
sizeBytes
|
|
42
|
+
mimeType
|
|
43
|
+
isExecutable
|
|
44
|
+
}
|
|
45
|
+
fileCount
|
|
46
|
+
filesBytes
|
|
47
|
+
}
|
|
48
|
+
}`;
|
|
49
|
+
/**
|
|
50
|
+
* 技能附属资源字节:boot 文档只带清单(避免目录膨胀),真正加载某个技能时才按需取这一条。
|
|
51
|
+
* 可空根字段:读取者不可见 / 不存在回空数组(权威「没有」),领域失败回 null(降级,不物化)。
|
|
52
|
+
*/
|
|
53
|
+
export const RUNTIME_SKILL_FILES_DOCUMENT = `query RuntimeSkillFiles($skillId: ID!) {
|
|
54
|
+
runtimeSkillFiles(skillId: $skillId) {
|
|
55
|
+
path
|
|
56
|
+
content
|
|
57
|
+
encoding
|
|
58
|
+
sizeBytes
|
|
59
|
+
mimeType
|
|
60
|
+
isExecutable
|
|
61
|
+
}
|
|
62
|
+
}`;
|
|
63
|
+
/**
|
|
64
|
+
* 项目绑定按 slug 取:只问已登记工作区命中的那几个项目,绝不把整张项目表拉回本地。
|
|
65
|
+
* `slugs` 是读范围之上的过滤(不是绕过),空数组 = 空集合。
|
|
66
|
+
*/
|
|
67
|
+
export const RUNTIME_WORKSPACE_PROJECTS_DOCUMENT = `query RuntimeWorkspaceProjects($slugs: [String!]) {
|
|
68
|
+
runtimeProjects(slugs: $slugs) {
|
|
69
|
+
id slug name enabled planModelId executeModelId
|
|
70
|
+
}
|
|
71
|
+
}`;
|
|
72
|
+
/** 密钥重取(「重新加载」时只刷新密钥)。 */
|
|
73
|
+
export const RUNTIME_MODEL_CREDENTIALS_DOCUMENT = `query RuntimeModelCredentials($connectionIds: [ID!]) {
|
|
74
|
+
runtimeModelCredentials(connectionIds: $connectionIds) {
|
|
75
|
+
connectionId
|
|
76
|
+
value
|
|
77
|
+
}
|
|
78
|
+
}`;
|
|
79
|
+
/** 连接令牌形状:hub 接入密钥(cnbk_)、部署面运行令牌(semi2_)或用户密钥(JWT 三段形态)。 */
|
|
80
|
+
const TOKEN_SHAPE = /^(?:cnbk_[A-Za-z0-9_-]+|semi2_[A-Za-z0-9_-]+|[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+)$/;
|
|
81
|
+
const REQUEST_TIMEOUT_MS = 15_000;
|
|
82
|
+
const MAX_RESPONSE_BYTES = 2 * 1024 * 1024;
|
|
83
|
+
/** semi2-hub 根地址:生产/预览都是 https;development 下额外放行回环 http。 */
|
|
84
|
+
export function consoleEndpoint(value, nodeEnv) {
|
|
85
|
+
let endpoint;
|
|
86
|
+
try {
|
|
87
|
+
endpoint = new URL(value.trim());
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
throw new ConfigurationError('INVALID_CONFIG_URL');
|
|
91
|
+
}
|
|
92
|
+
const loopback = endpoint.hostname === 'localhost' ||
|
|
93
|
+
endpoint.hostname === '[::1]' ||
|
|
94
|
+
/^127(?:\.\d{1,3}){3}$/.test(endpoint.hostname);
|
|
95
|
+
if (endpoint.username ||
|
|
96
|
+
endpoint.password ||
|
|
97
|
+
endpoint.search ||
|
|
98
|
+
endpoint.hash ||
|
|
99
|
+
(endpoint.protocol !== 'https:' &&
|
|
100
|
+
!(endpoint.protocol === 'http:' && nodeEnv !== 'production' && loopback))) {
|
|
101
|
+
throw new ConfigurationError('INVALID_CONFIG_URL');
|
|
102
|
+
}
|
|
103
|
+
endpoint.pathname = `${endpoint.pathname.replace(/\/+$/, '')}/graphql`;
|
|
104
|
+
return endpoint;
|
|
105
|
+
}
|
|
106
|
+
function asRecord(value) {
|
|
107
|
+
return value === null || typeof value !== 'object' || Array.isArray(value)
|
|
108
|
+
? null
|
|
109
|
+
: value;
|
|
110
|
+
}
|
|
111
|
+
function errorCodeOf(payload) {
|
|
112
|
+
const errors = payload.errors;
|
|
113
|
+
if (!Array.isArray(errors))
|
|
114
|
+
return null;
|
|
115
|
+
for (const entry of errors) {
|
|
116
|
+
const code = asRecord(asRecord(entry)?.extensions)?.code;
|
|
117
|
+
if (typeof code === 'string' && code !== '')
|
|
118
|
+
return code;
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
/** 流式累计,超 2MB 判失败;不把上游内容带进错误。 */
|
|
123
|
+
async function readPayload(response) {
|
|
124
|
+
const body = response.body;
|
|
125
|
+
if (body === null)
|
|
126
|
+
throw new ConfigurationError('CONFIG_REQUEST_FAILED');
|
|
127
|
+
const chunks = [];
|
|
128
|
+
let size = 0;
|
|
129
|
+
for await (const chunk of body) {
|
|
130
|
+
if (!(chunk instanceof Uint8Array))
|
|
131
|
+
throw new ConfigurationError('CONFIG_REQUEST_FAILED');
|
|
132
|
+
size += chunk.byteLength;
|
|
133
|
+
if (size > MAX_RESPONSE_BYTES)
|
|
134
|
+
throw new ConfigurationError('CONFIG_REQUEST_FAILED');
|
|
135
|
+
chunks.push(chunk);
|
|
136
|
+
}
|
|
137
|
+
let parsed;
|
|
138
|
+
try {
|
|
139
|
+
parsed = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
throw new ConfigurationError('CONFIG_REQUEST_FAILED');
|
|
143
|
+
}
|
|
144
|
+
const payload = asRecord(parsed);
|
|
145
|
+
if (payload === null)
|
|
146
|
+
throw new ConfigurationError('CONFIG_REQUEST_FAILED');
|
|
147
|
+
return payload;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* 传输层失败抛 `ConfigurationError`;服务端领域失败回落在返回值的 errorCode 上(HTTP 200 +
|
|
151
|
+
* `errors[].extensions.code`),由调用方决定降级。
|
|
152
|
+
*/
|
|
153
|
+
export async function postGraphql(client, request) {
|
|
154
|
+
const token = client.token.trim();
|
|
155
|
+
if (token === '' || token.length > 512 || !TOKEN_SHAPE.test(token))
|
|
156
|
+
throw new ConfigurationError('MISSING_ACCESS_TOKEN');
|
|
157
|
+
const fetchImpl = client.fetchImpl ?? fetch;
|
|
158
|
+
let response;
|
|
159
|
+
try {
|
|
160
|
+
response = await fetchImpl(client.endpoint, {
|
|
161
|
+
method: 'POST',
|
|
162
|
+
headers: {
|
|
163
|
+
'content-type': 'application/json',
|
|
164
|
+
authorization: `Bearer ${token}`,
|
|
165
|
+
'cache-control': 'no-store',
|
|
166
|
+
...(client.creator === undefined
|
|
167
|
+
? {}
|
|
168
|
+
: { 'x-semi2-cnb-creator': client.creator }),
|
|
169
|
+
},
|
|
170
|
+
cache: 'no-store',
|
|
171
|
+
redirect: 'error',
|
|
172
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
173
|
+
body: JSON.stringify({
|
|
174
|
+
operationName: request.operationName,
|
|
175
|
+
query: request.document,
|
|
176
|
+
...(request.variables === undefined
|
|
177
|
+
? {}
|
|
178
|
+
: { variables: request.variables }),
|
|
179
|
+
}),
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
throw new ConfigurationError('CONFIG_REQUEST_FAILED');
|
|
184
|
+
}
|
|
185
|
+
if (!response.ok)
|
|
186
|
+
throw new ConfigurationError(response.status === 401 ? 'UNAUTHENTICATED' : 'CONFIG_REQUEST_FAILED');
|
|
187
|
+
const payload = await readPayload(response);
|
|
188
|
+
const data = payload.data;
|
|
189
|
+
return {
|
|
190
|
+
data: data === null || data === undefined ? null : data,
|
|
191
|
+
errorCode: errorCodeOf(payload),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Semi2ConfigRuntime, } from './runtime.js';
|
|
2
|
+
import { installSkills } from './skills.js';
|
|
3
|
+
/**
|
|
4
|
+
* 同步发布 `ctx.semi2Config`,首次领取在后台——不阻塞插件树 settle,失败只 warn 绝不 reject
|
|
5
|
+
*(读 state.error 诊断);缺任一连接凭据即不做任何注入。
|
|
6
|
+
*/
|
|
7
|
+
export function installConfiguration(ctx, options = {}) {
|
|
8
|
+
const runtime = new Semi2ConfigRuntime(ctx, { ...process.env }, options);
|
|
9
|
+
ctx.provide('semi2Config', runtime);
|
|
10
|
+
// 技能目录由 boot 缓存驱动:注册 provider 只 warn,注册失败不影响配置领取。
|
|
11
|
+
installSkills(ctx, runtime, (message) => {
|
|
12
|
+
warn(ctx, message);
|
|
13
|
+
});
|
|
14
|
+
// fire-and-forget 不得产生 unhandled rejection(start() 自身不抛,仍显式接住)。
|
|
15
|
+
void runtime.start().catch((error) => {
|
|
16
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
17
|
+
warn(ctx, `首次领取失败:${message}`);
|
|
18
|
+
});
|
|
19
|
+
return runtime;
|
|
20
|
+
}
|
|
21
|
+
/** 无 logger 的宿主(单测)忽略。 */
|
|
22
|
+
function warn(ctx, message) {
|
|
23
|
+
try {
|
|
24
|
+
ctx.logger('semi2-config').warn(message);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// 忽略
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// @fa-software/semi2-dsh/configuration/login —— 设置页「登录 semi2-hub」(本地宿主)。
|
|
2
|
+
// `POST {semi2-hub}/auth/token` 一步换 JWT:无 cookie / CSRF / 服务端会话;semi2-hub 要求 Origin 命中其公开地址,故显式带上 origin。
|
|
3
|
+
// 密码只存在本次请求内存里(不写日志、不进设置命名空间);凭据落盘由调用方写 dsh credentials 存储,本模块不落盘。
|
|
4
|
+
import { ConfigurationError } from './graphql.js';
|
|
5
|
+
const USER_KEY_SHAPE = /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/;
|
|
6
|
+
/** semi2-hub 根地址:https(development 下额外放行回环 http),不允许凭据/查询串。 */
|
|
7
|
+
export function consoleOrigin(value, nodeEnv) {
|
|
8
|
+
let endpoint;
|
|
9
|
+
try {
|
|
10
|
+
endpoint = new URL(value.trim());
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
throw new ConfigurationError('INVALID_CONFIG_URL');
|
|
14
|
+
}
|
|
15
|
+
const loopback = endpoint.hostname === 'localhost' ||
|
|
16
|
+
endpoint.hostname === '[::1]' ||
|
|
17
|
+
/^127(?:\.\d{1,3}){3}$/.test(endpoint.hostname);
|
|
18
|
+
if (endpoint.username ||
|
|
19
|
+
endpoint.password ||
|
|
20
|
+
endpoint.search ||
|
|
21
|
+
endpoint.hash ||
|
|
22
|
+
(endpoint.protocol !== 'https:' &&
|
|
23
|
+
!(endpoint.protocol === 'http:' &&
|
|
24
|
+
nodeEnv !== 'production' &&
|
|
25
|
+
loopback)))
|
|
26
|
+
throw new ConfigurationError('INVALID_CONFIG_URL');
|
|
27
|
+
return endpoint.origin;
|
|
28
|
+
}
|
|
29
|
+
async function consoleFailure(response) {
|
|
30
|
+
let code = 'LOGIN_FAILED';
|
|
31
|
+
try {
|
|
32
|
+
const body = (await response.json());
|
|
33
|
+
if (typeof body.code === 'string' && body.code !== '')
|
|
34
|
+
code = body.code;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// 非 JSON 响应:保留通用码。
|
|
38
|
+
}
|
|
39
|
+
return new ConfigurationError(code);
|
|
40
|
+
}
|
|
41
|
+
/** 失败抛 `ConfigurationError`(code 由 UI 透传展示)。 */
|
|
42
|
+
export async function loginToConsole(input, fetchImpl = fetch) {
|
|
43
|
+
const origin = consoleOrigin(input.url, process.env.NODE_ENV);
|
|
44
|
+
const response = await fetchImpl(`${origin}/auth/token`, {
|
|
45
|
+
cache: 'no-store',
|
|
46
|
+
redirect: 'error',
|
|
47
|
+
signal: AbortSignal.timeout(15_000),
|
|
48
|
+
method: 'POST',
|
|
49
|
+
headers: {
|
|
50
|
+
'content-type': 'application/json',
|
|
51
|
+
origin,
|
|
52
|
+
accept: 'application/json',
|
|
53
|
+
},
|
|
54
|
+
body: JSON.stringify({
|
|
55
|
+
username: input.username,
|
|
56
|
+
password: input.password,
|
|
57
|
+
}),
|
|
58
|
+
}).catch(() => {
|
|
59
|
+
throw new ConfigurationError('CONFIG_REQUEST_FAILED');
|
|
60
|
+
});
|
|
61
|
+
if (!response.ok)
|
|
62
|
+
throw await consoleFailure(response);
|
|
63
|
+
const payload = (await response.json().catch(() => null));
|
|
64
|
+
const token = payload?.token;
|
|
65
|
+
if (typeof token !== 'string' || !USER_KEY_SHAPE.test(token))
|
|
66
|
+
throw new ConfigurationError('LOGIN_FAILED');
|
|
67
|
+
const userName = typeof payload?.user?.username === 'string'
|
|
68
|
+
? payload.user.username
|
|
69
|
+
: '';
|
|
70
|
+
return { userName, token, url: origin };
|
|
71
|
+
}
|
|
72
|
+
/** CNB 运行面:平台注入的短期 CNB_TOKEN 换 hub 用户密钥(服务端到服务端,无 Origin/CSRF)。 */
|
|
73
|
+
export async function exchangeCnbToken(input, fetchImpl = fetch) {
|
|
74
|
+
const origin = consoleOrigin(input.url, process.env.NODE_ENV);
|
|
75
|
+
const response = await fetchImpl(`${origin}/auth/cnb-token`, {
|
|
76
|
+
cache: 'no-store',
|
|
77
|
+
redirect: 'error',
|
|
78
|
+
signal: AbortSignal.timeout(15_000),
|
|
79
|
+
method: 'POST',
|
|
80
|
+
headers: {
|
|
81
|
+
'content-type': 'application/json',
|
|
82
|
+
accept: 'application/json',
|
|
83
|
+
},
|
|
84
|
+
body: JSON.stringify({ token: input.token }),
|
|
85
|
+
}).catch(() => {
|
|
86
|
+
throw new ConfigurationError('CONFIG_REQUEST_FAILED');
|
|
87
|
+
});
|
|
88
|
+
if (!response.ok)
|
|
89
|
+
throw await consoleFailure(response);
|
|
90
|
+
const payload = (await response.json().catch(() => null));
|
|
91
|
+
const token = payload?.token;
|
|
92
|
+
if (typeof token !== 'string' || !USER_KEY_SHAPE.test(token))
|
|
93
|
+
throw new ConfigurationError('EXCHANGE_FAILED');
|
|
94
|
+
const userName = typeof payload?.user?.username === 'string' ? payload.user.username : '';
|
|
95
|
+
return { userName, token };
|
|
96
|
+
}
|