@mokuyoaxis/dsh-iris 0.2.7 → 0.3.0-beta.1
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/CHANGELOG.md +143 -1
- package/README.en.md +109 -311
- package/README.md +106 -309
- package/bin/dsh-iris.js +452 -92
- package/docs/AGENT_CORE_QUICKSTART.md +64 -0
- package/docs/AGENT_QUICKSTART.md +2 -0
- package/docs/ARCHITECTURE.md +13 -3
- package/docs/ARTIFACT_MANIFEST.md +4 -0
- package/docs/AUDIO_FLOWS.md +87 -0
- package/docs/BETA_SCOPE.md +42 -0
- package/docs/CONTACT_SHEET.md +62 -0
- package/docs/CORE_CONNECTIONS.md +101 -0
- package/docs/CORE_QUICKSTART.md +110 -0
- package/docs/CORE_RUNTIME_CONTRACT.md +7 -2
- package/docs/CORE_SERVICE.md +101 -0
- package/docs/CORE_SKILLS.md +35 -3
- package/docs/DSH_COMPATIBILITY.md +21 -1
- package/docs/DSH_CORE_MIGRATION.md +6 -0
- package/docs/DSH_RC2_ADAPTATION.md +1 -1
- package/docs/DSH_RUNTIME.md +6 -2
- package/docs/HEADLESS_CLI.md +30 -5
- package/docs/HOST_ADAPTER_CONTRACT.md +5 -3
- package/docs/IMAGE_FLOW.md +4 -0
- package/docs/IMAGE_GENERATION.md +127 -0
- package/docs/LOCAL_API.md +4 -2
- package/docs/LOCAL_API_CONTRACT.md +42 -15
- package/docs/LOCAL_CREATION.md +7 -1
- package/docs/LOCAL_OPERATIONS.md +46 -0
- package/docs/LOCAL_SELECTION.md +57 -0
- package/docs/LOCAL_WEB.md +207 -0
- package/docs/MCP.md +212 -0
- package/docs/MEDIA_SCHEDULING.md +72 -0
- package/docs/MUSIC.md +2 -0
- package/docs/NETWORK_RESOURCES.md +106 -0
- package/docs/NETWORK_ROUTING.md +103 -0
- package/docs/NETWORK_WORKBENCH.md +65 -0
- package/docs/PANEL_EXTENSIONS.md +64 -0
- package/docs/PLUGINS.md +193 -0
- package/docs/PLUGIN_DEVELOPMENT.md +39 -0
- package/docs/PLUGIN_STARTUP.md +113 -0
- package/docs/PROVIDER_ADAPTER_CONTRACT.md +8 -2
- package/docs/PROVIDER_CREDENTIALS.md +67 -0
- package/docs/PROVIDER_EXTENSIONS.md +82 -0
- package/docs/PROVIDER_HEALTH.md +5 -1
- package/docs/PROVIDER_NETWORK.md +45 -0
- package/docs/PROVIDER_PLUGIN_CONTRACT.md +24 -0
- package/docs/PROVIDER_SETTINGS.md +91 -0
- package/docs/PROVIDER_SUBMISSION_CONTRACT.md +8 -0
- package/docs/ROADMAP.md +18 -8
- package/docs/SECURITY.md +1 -1
- package/docs/SYNC_DELIVERY_RECOVERY.md +5 -1
- package/docs/VIDEO_FLOWS.md +89 -0
- package/docs/VISION_CLI.md +1 -1
- package/docs/VISION_MODEL.md +2 -0
- package/docs/WEB_PROTOTYPE.md +21 -6
- package/docs/WEB_RECOVERY.md +46 -0
- package/docs/WORKBENCH_ARTIFACTS.md +6 -0
- package/docs/WORKBENCH_LOGIN.md +63 -0
- package/docs/assets/screenshots/core-connections-preview.webp +0 -0
- package/docs/assets/screenshots/core-model-pools-preview.webp +0 -0
- package/docs/assets/screenshots/core-overview-preview.webp +0 -0
- package/docs/assets/screenshots/core-spectrum-preview.webp +0 -0
- package/docs/assets/screenshots/core-studio-dark-preview.webp +0 -0
- package/docs/assets/screenshots/core-studio-mobile-preview.webp +0 -0
- package/docs/assets/screenshots/core-studio-preview.webp +0 -0
- package/docs/releases/0.3.0-alpha.1.md +51 -0
- package/docs/releases/0.3.0-beta.1.md +34 -0
- package/docs/screenshots.md +41 -1
- package/lib/actions.js +56 -4
- package/lib/adapters.js +75 -49
- package/lib/api.js +23 -13
- package/lib/application-runtime.js +73 -17
- package/lib/application-writer.js +193 -0
- package/lib/audio-flow-input.js +37 -0
- package/lib/client.d.ts +3 -3
- package/lib/client.js +402 -72
- package/lib/comfyui-adapter.js +95 -0
- package/lib/comfyui-client.js +60 -0
- package/lib/comfyui-inspection.js +73 -0
- package/lib/comfyui-workflow.js +94 -0
- package/lib/command-service.js +18 -5
- package/lib/config.js +49 -10
- package/lib/configured-image-runtime.js +69 -0
- package/lib/connection-skill.js +98 -0
- package/lib/connection-template.js +23 -0
- package/lib/core-artifact-store.js +84 -28
- package/lib/core-bridge-selection.js +31 -0
- package/lib/core-bridge-tools.js +85 -0
- package/lib/core-capability-catalog.js +192 -0
- package/lib/core-connection-diagnostics.js +49 -0
- package/lib/core-connection-store.js +277 -0
- package/lib/core-connections.js +225 -0
- package/lib/core-delivery-cache.js +8 -3
- package/lib/core-maintenance.js +9 -3
- package/lib/core-runtime.js +2 -1
- package/lib/core-service-client.js +174 -0
- package/lib/core-tasks.js +113 -21
- package/lib/core-writer-lease.js +16 -0
- package/lib/creation-request-store.js +41 -64
- package/lib/doctor.js +2 -2
- package/lib/dsh-core-adapter.js +61 -9
- package/lib/dsh-host-adapter.js +4 -1
- package/lib/dsh-provider-config.js +1 -1
- package/lib/dsh-service-bridge.js +67 -0
- package/lib/execution-ownership.js +43 -0
- package/lib/flow-store.js +156 -14
- package/lib/generation-input.js +10 -1
- package/lib/headless-vision.js +7 -7
- package/lib/image-edit-input.js +1 -1
- package/lib/image-flow.js +97 -31
- package/lib/index.js +47 -22
- package/lib/local-api-projection.js +15 -6
- package/lib/local-api-token-directory.js +31 -0
- package/lib/local-api.js +861 -71
- package/lib/local-creation.js +96 -24
- package/lib/local-operations.js +36 -0
- package/lib/local-request-root.js +86 -0
- package/lib/local-web-assets.js +39 -0
- package/lib/mcp-bridge.js +58 -0
- package/lib/mcp-check.js +26 -0
- package/lib/mcp-config.js +42 -0
- package/lib/mcp-delivery.js +46 -0
- package/lib/mcp-http.js +174 -0
- package/lib/mcp-server.js +69 -0
- package/lib/media-scheduler.js +194 -0
- package/lib/model-directory-service.js +37 -10
- package/lib/model-directory.js +3 -3
- package/lib/models.js +2 -2
- package/lib/music-transport.js +6 -3
- package/lib/network-config-service.js +123 -0
- package/lib/network-config.js +127 -0
- package/lib/network-doctor.js +94 -0
- package/lib/network-management.js +51 -0
- package/lib/network-policy.js +19 -0
- package/lib/network-route-evidence.js +38 -0
- package/lib/network-routing.js +283 -0
- package/lib/operation-runner.js +281 -0
- package/lib/operation-store.js +216 -0
- package/lib/panel-extension.js +36 -0
- package/lib/plugin-contract.js +194 -0
- package/lib/plugin-execution.js +51 -0
- package/lib/plugin-launch.js +86 -0
- package/lib/plugin-operation-store.js +101 -0
- package/lib/plugin-operations.js +28 -0
- package/lib/plugin-registry.js +109 -0
- package/lib/plugin-selection.js +15 -0
- package/lib/plugin-source-image.js +5 -0
- package/lib/plugin-web-assets.js +54 -0
- package/lib/plugins.d.ts +307 -0
- package/lib/plugins.js +59 -0
- package/lib/provider-adapter.js +7 -3
- package/lib/provider-adapters.js +92 -22
- package/lib/provider-catalog.js +64 -36
- package/lib/provider-config-format.js +51 -0
- package/lib/provider-config-service.js +143 -31
- package/lib/provider-context.d.ts +90 -0
- package/lib/provider-context.js +42 -0
- package/lib/provider-credentials.js +97 -0
- package/lib/provider-diagnostics.js +56 -0
- package/lib/provider-extensions.js +29 -0
- package/lib/provider-health.js +18 -6
- package/lib/provider-monitor.js +81 -0
- package/lib/provider-network.js +52 -0
- package/lib/provider-protocol.js +73 -22
- package/lib/provider-registry.js +128 -0
- package/lib/provider-response.js +38 -0
- package/lib/provider-settings.js +136 -0
- package/lib/provider-task-runner.js +132 -43
- package/lib/provider-transport.js +16 -0
- package/lib/render.js +11 -1
- package/lib/scheduling-policy.js +66 -0
- package/lib/skill-installation.js +23 -11
- package/lib/tasks.js +3 -2
- package/lib/video-flow-input.js +16 -0
- package/lib/video-input.js +12 -5
- package/lib/vision-model-routing.js +5 -0
- package/lib/web/app.js +703 -0
- package/lib/web/appearance.js +111 -0
- package/lib/web/comfyui.js +146 -0
- package/lib/web/connections.js +516 -0
- package/lib/web/creation.js +359 -0
- package/lib/web/flow-view.js +34 -0
- package/lib/web/gallery.js +133 -0
- package/lib/web/index.html +344 -0
- package/lib/web/media-client.js +218 -0
- package/lib/web/network.js +232 -0
- package/lib/web/plugin-panel.js +223 -0
- package/lib/web/plugin-status.js +140 -0
- package/lib/web/plugin-surface.css +17 -0
- package/lib/web/providers.js +513 -0
- package/lib/web/recovery.js +105 -0
- package/lib/web/startup.js +18 -0
- package/lib/web/styles.css +571 -0
- package/lib/web/transport.js +101 -0
- package/lib/web/workbench-login.js +165 -0
- package/lib/workbench-artifacts.js +3 -2
- package/lib/workbench-auth-cli.js +47 -0
- package/lib/workbench-auth.js +200 -0
- package/package.json +24 -6
- package/plugins/contact-sheet/index.d.mts +4 -0
- package/plugins/contact-sheet/index.mjs +33 -0
- package/plugins/contact-sheet/web/board.css +14 -0
- package/plugins/contact-sheet/web/board.mjs +93 -0
- package/skills/iris-core/SKILL.md +8 -4
- package/skills/iris-core/references/cli.md +2 -2
- package/skills/iris-core/references/comfyui.md +69 -0
- package/skills/iris-core/references/tasks.md +2 -0
- package/templates/plugin-starter/LICENSE +21 -0
- package/templates/plugin-starter/README.md +74 -0
- package/templates/plugin-starter/assets/peach.png +0 -0
- package/templates/plugin-starter/assets/violet.png +0 -0
- package/templates/plugin-starter/index.mjs +27 -0
- package/templates/plugin-starter/package.json +18 -0
- package/templates/plugin-starter/scripts/core-cli.mjs +20 -0
- package/templates/plugin-starter/scripts/environment.mjs +65 -0
- package/templates/plugin-starter/scripts/mcp-probe.mjs +22 -0
- package/templates/plugin-starter/scripts/setup.mjs +42 -0
- package/templates/plugin-starter/scripts/smoke.mjs +76 -0
- package/templates/plugin-starter/web/board.css +27 -0
- package/templates/plugin-starter/web/board.mjs +160 -0
- package/templates/plugin-starter/web/png.mjs +23 -0
- package/templates/provider-starter/LICENSE +21 -0
- package/templates/provider-starter/README.md +50 -0
- package/templates/provider-starter/index.d.mts +5 -0
- package/templates/provider-starter/index.mjs +165 -0
- package/templates/provider-starter/package.json +13 -0
- package/templates/provider-starter/scripts/core-cli.mjs +22 -0
- package/templates/provider-starter/scripts/environment.mjs +122 -0
- package/templates/provider-starter/scripts/fixture.mjs +54 -0
- package/templates/provider-starter/scripts/mcp-probe.mjs +21 -0
- package/templates/provider-starter/scripts/protocol-smoke.mjs +48 -0
- package/templates/provider-starter/scripts/setup.mjs +44 -0
- package/templates/provider-starter/scripts/smoke.mjs +145 -0
- package/user_guide.md +5 -1
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// Owner CLI resources and explicit bindings. Editing never sends network traffic.
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import crypto from 'node:crypto';
|
|
5
|
+
import { atomicCreatePrivate, atomicWritePrivate, ensurePrivateDir, privateSibling } from './private-storage.js';
|
|
6
|
+
import { NETWORK_CONFIG_VERSION, NETWORK_ROUTING_CONFIG_VERSION, NETWORK_CONFIG_MAX_BYTES, networkConfigFail as fail, networkFields, normalizeNetworkNode, normalizeNetworkPool, validateNetworkConfig, projectNetworkConfig } from './network-config.js';
|
|
7
|
+
|
|
8
|
+
const digest = bytes => crypto.createHash('sha256').update(bytes).digest('hex');
|
|
9
|
+
function absolute(file) {
|
|
10
|
+
if (typeof file !== 'string' || !path.isAbsolute(file)) fail('IRIS_NETWORK_CONFIG_INVALID', 'network-config 必须是显式绝对路径');
|
|
11
|
+
}
|
|
12
|
+
function io(action) {
|
|
13
|
+
try { return action(); }
|
|
14
|
+
catch (error) {
|
|
15
|
+
if (typeof error.code !== 'string' || typeof error.syscall !== 'string') throw error;
|
|
16
|
+
const detail = /^[A-Z][A-Z0-9_]{0,39}$/.test(error.code) ? `(${error.code})` : '';
|
|
17
|
+
fail('IRIS_NETWORK_CONFIG_IO', `网络配置文件操作失败${detail},请检查目录权限、可用空间与文件状态;原文件和已有备份不会自动清理`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function readNetworkConfig(file) {
|
|
21
|
+
absolute(file);
|
|
22
|
+
const info = io(() => fs.lstatSync(file));
|
|
23
|
+
if (!info.isFile() || info.isSymbolicLink()) fail('IRIS_NETWORK_CONFIG_INVALID', 'network-config 必须是普通文件,不能使用符号链接');
|
|
24
|
+
if (process.platform !== 'win32' && (info.mode & 0o077) !== 0) fail('IRIS_NETWORK_CONFIG_PERMISSIONS', 'network-config 权限过宽,请限制为仅当前用户可读写');
|
|
25
|
+
if (info.size > NETWORK_CONFIG_MAX_BYTES) fail('IRIS_NETWORK_CONFIG_INVALID', 'network-config 不能超过1 MiB');
|
|
26
|
+
const bytes = io(() => fs.readFileSync(file));
|
|
27
|
+
if (bytes.length > NETWORK_CONFIG_MAX_BYTES) fail('IRIS_NETWORK_CONFIG_INVALID', 'network-config 不能超过1 MiB');
|
|
28
|
+
let parsed;
|
|
29
|
+
try { parsed = JSON.parse(bytes.toString('utf8')); }
|
|
30
|
+
catch (_) { fail('IRIS_NETWORK_CONFIG_INVALID', 'network-config 不是有效 JSON;请修正原文件,不会自动回退'); }
|
|
31
|
+
return { config: validateNetworkConfig(parsed), bytes, revision: digest(bytes) };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const READS = ['show', 'check', 'nodes.list', 'pools.list', 'bindings.list'];
|
|
35
|
+
const WRITES = ['nodes.add', 'nodes.set', 'nodes.remove', 'pools.add', 'pools.set', 'pools.remove', 'bindings.set', 'bindings.remove'];
|
|
36
|
+
export function executeNetworkConfigCommand(file, command, input = {}, { signal, env = process.env } = {}) {
|
|
37
|
+
absolute(file); signal?.throwIfAborted();
|
|
38
|
+
if (['init', 'routing.init', 'routing.copy'].includes(command)) {
|
|
39
|
+
const copy = command === 'routing.copy', routing = command !== 'init';
|
|
40
|
+
networkFields(input, copy ? ['source'] : [], copy ? ['source'] : []);
|
|
41
|
+
const source = copy ? readNetworkConfig(input.source).config : undefined;
|
|
42
|
+
const config = { schemaVersion: routing ? NETWORK_ROUTING_CONFIG_VERSION : NETWORK_CONFIG_VERSION,
|
|
43
|
+
configId: crypto.randomUUID(), generation: 1, nodes: source?.nodes || [], pools: source?.pools || [], ...(routing ? { bindings: [] } : {}) };
|
|
44
|
+
io(() => {
|
|
45
|
+
ensurePrivateDir(path.dirname(file));
|
|
46
|
+
try { atomicCreatePrivate(file, JSON.stringify(config, null, 2) + '\n'); }
|
|
47
|
+
catch (error) { if (error.code === 'EEXIST') fail('IRIS_NETWORK_CONFIG_EXISTS', '网络配置已存在,初始化不会覆盖;请先读取并核对'); throw error; }
|
|
48
|
+
});
|
|
49
|
+
const saved = readNetworkConfig(file);
|
|
50
|
+
return { command, created: true, ...projectNetworkConfig(saved.config, saved.revision, env) };
|
|
51
|
+
}
|
|
52
|
+
if (!READS.includes(command) && !WRITES.includes(command)) fail('IRIS_NETWORK_CONFIG_INVALID', '不支持的网络配置命令');
|
|
53
|
+
const before = readNetworkConfig(file);
|
|
54
|
+
if (command.startsWith('bindings.') && before.config.schemaVersion !== NETWORK_ROUTING_CONFIG_VERSION) {
|
|
55
|
+
fail('IRIS_NETWORK_CONFIG_VERSION', '绑定只用于显式新建的 v2 网络配置;可用 routing.copy 另存新文件,原文件不会自动迁移');
|
|
56
|
+
}
|
|
57
|
+
if (READS.includes(command)) {
|
|
58
|
+
networkFields(input, []);
|
|
59
|
+
return { command, ...(command === 'check' ? { valid: true, networkTested: false } : {}), ...projectNetworkConfig(before.config, before.revision, env) };
|
|
60
|
+
}
|
|
61
|
+
const [group, verb] = command.split('.'), field = group === 'nodes' ? 'node' : 'pool';
|
|
62
|
+
const fields = group === 'bindings' ? ['expectedRevision', 'catalogId', 'providerId', ...(verb === 'set' ? ['poolId'] : ['confirm'])]
|
|
63
|
+
: verb === 'remove' ? ['expectedRevision', 'id', 'confirm'] : ['expectedRevision', field];
|
|
64
|
+
networkFields(input, fields, fields);
|
|
65
|
+
if (input.expectedRevision !== before.revision) fail('IRIS_NETWORK_CONFIG_CHANGED', '网络配置已变化,请重新读取后核对编辑');
|
|
66
|
+
const next = structuredClone(before.config), records = next[group];
|
|
67
|
+
if (group === 'bindings') {
|
|
68
|
+
const index = records.findIndex(binding => binding.catalogId === input.catalogId && binding.providerId === input.providerId);
|
|
69
|
+
if (verb === 'remove') {
|
|
70
|
+
if (input.confirm !== true) fail('IRIS_NETWORK_CONFIG_INVALID', '删除绑定需要明确 confirm:true');
|
|
71
|
+
if (index < 0) fail('IRIS_NETWORK_CONFIG_NOT_FOUND', '所选绑定不存在');
|
|
72
|
+
records.splice(index, 1);
|
|
73
|
+
} else {
|
|
74
|
+
const binding = { catalogId: input.catalogId, providerId: input.providerId, poolId: input.poolId };
|
|
75
|
+
if (index < 0) records.push(binding); else records[index] = binding;
|
|
76
|
+
}
|
|
77
|
+
} else if (verb === 'remove') {
|
|
78
|
+
if (input.confirm !== true) fail('IRIS_NETWORK_CONFIG_INVALID', '删除资源需要明确 confirm:true');
|
|
79
|
+
const index = records.findIndex(record => record.id === input.id);
|
|
80
|
+
if (index < 0) fail('IRIS_NETWORK_CONFIG_NOT_FOUND', '所选网络资源不存在');
|
|
81
|
+
if (group === 'nodes' && next.pools.some(pool => pool.candidates.some(candidate => candidate.kind === 'node' && candidate.nodeId === input.id))) {
|
|
82
|
+
fail('IRIS_NETWORK_CONFIG_IN_USE', '节点仍被池引用;请先显式编辑或移除引用,停用的池也保留引用');
|
|
83
|
+
}
|
|
84
|
+
if (group === 'pools' && next.bindings?.some(binding => binding.poolId === input.id)) {
|
|
85
|
+
fail('IRIS_NETWORK_CONFIG_IN_USE', '池仍被供应商绑定引用;请先显式解除绑定,停用的池也保留引用');
|
|
86
|
+
}
|
|
87
|
+
records.splice(index, 1);
|
|
88
|
+
} else {
|
|
89
|
+
const supplied = input[field];
|
|
90
|
+
networkFields(supplied, group === 'nodes' ? ['id', 'label', 'enabled', 'protocol', 'host', 'port', 'auth'] : ['id', 'label', 'enabled', 'mode', 'candidates'], ['id']);
|
|
91
|
+
const index = records.findIndex(record => record.id === supplied.id);
|
|
92
|
+
if (verb === 'add' && index !== -1) fail('IRIS_NETWORK_CONFIG_EXISTS', '同类网络资源 ID 已存在,不能覆盖');
|
|
93
|
+
if (verb === 'set' && index === -1) fail('IRIS_NETWORK_CONFIG_NOT_FOUND', '所选网络资源不存在');
|
|
94
|
+
const normalize = group === 'nodes' ? normalizeNetworkNode : normalizeNetworkPool;
|
|
95
|
+
const record = normalize(verb === 'set' ? { ...records[index], ...supplied } : supplied);
|
|
96
|
+
if (verb === 'set') records[index] = record; else records.push(record);
|
|
97
|
+
}
|
|
98
|
+
const validated = validateNetworkConfig(next);
|
|
99
|
+
const changed = JSON.stringify(validated) !== JSON.stringify(before.config);
|
|
100
|
+
let fd;
|
|
101
|
+
try { fd = io(() => {
|
|
102
|
+
try { return fs.openSync(file + '.lock', 'wx', 0o600); }
|
|
103
|
+
catch (error) { if (error.code === 'EEXIST') fail('IRIS_NETWORK_CONFIG_BUSY', '网络配置正在修改或留有未核查的锁;不会自动删除或抢占,请核查写入进程'); throw error; }
|
|
104
|
+
});
|
|
105
|
+
// Revalidate the schema before CAS; a newer writer must never be downgraded.
|
|
106
|
+
const current = readNetworkConfig(file);
|
|
107
|
+
if (current.revision !== before.revision) fail('IRIS_NETWORK_CONFIG_CHANGED', '网络配置在编辑期间变化,请重新读取后核对');
|
|
108
|
+
signal?.throwIfAborted();
|
|
109
|
+
if (!changed) return { command, changed: false, ...projectNetworkConfig(current.config, current.revision, env) };
|
|
110
|
+
if (validated.generation === Number.MAX_SAFE_INTEGER) fail('IRIS_NETWORK_CONFIG_INVALID', '网络配置修订已超出支持范围');
|
|
111
|
+
validated.generation++;
|
|
112
|
+
const encoded = Buffer.from(JSON.stringify(validated, null, 2) + '\n');
|
|
113
|
+
if (encoded.length > NETWORK_CONFIG_MAX_BYTES) fail('IRIS_NETWORK_CONFIG_INVALID', 'network-config 不能超过1 MiB');
|
|
114
|
+
const backupPath = privateSibling(file, 'backup');
|
|
115
|
+
io(() => {
|
|
116
|
+
fs.writeFileSync(backupPath, before.bytes, { flag: 'wx', mode: 0o600 });
|
|
117
|
+
atomicWritePrivate(file, encoded);
|
|
118
|
+
});
|
|
119
|
+
return { command, changed: true, backupPath, ...projectNetworkConfig(validated, digest(encoded), env) };
|
|
120
|
+
} finally {
|
|
121
|
+
if (fd !== undefined) io(() => { try { fs.closeSync(fd); } finally { fs.unlinkSync(file + '.lock'); } });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// Private, explicit network resources. This schema does not activate routing.
|
|
2
|
+
import { isIP } from 'node:net';
|
|
3
|
+
import { redactProviderMessage } from './provider-contract.js';
|
|
4
|
+
|
|
5
|
+
export const NETWORK_CONFIG_VERSION = 1;
|
|
6
|
+
export const NETWORK_ROUTING_CONFIG_VERSION = 2;
|
|
7
|
+
export const NETWORK_CONFIG_MAX_BYTES = 1024 * 1024;
|
|
8
|
+
const ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
9
|
+
const ENV = /^[A-Za-z_][A-Za-z0-9_]{0,127}$/;
|
|
10
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
11
|
+
|
|
12
|
+
export function networkConfigFail(code, message) { throw Object.assign(new Error(message), { code }); }
|
|
13
|
+
const invalid = message => networkConfigFail('IRIS_NETWORK_CONFIG_INVALID', message);
|
|
14
|
+
export function networkFields(value, allowed, required = []) {
|
|
15
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)
|
|
16
|
+
|| Object.keys(value).some(key => !allowed.includes(key)) || required.some(key => !Object.hasOwn(value, key))) {
|
|
17
|
+
invalid('网络配置字段不符合约定,请检查对应对象的字段名称与必填项');
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function identity(value) {
|
|
22
|
+
if (typeof value !== 'string' || !ID.test(value)) invalid('资源 ID 须为1–64字符的字母、数字、点、短横线或下划线,且以字母或数字开头');
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
function label(value) {
|
|
26
|
+
if (typeof value !== 'string' || !value.trim() || value.length > 120 || /[\x00-\x1f\x7f]/.test(value)) invalid('资源名称须为非空且最多120字符的单行文本');
|
|
27
|
+
return value.trim();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function normalizeNetworkNode(value) {
|
|
31
|
+
networkFields(value, ['id', 'label', 'enabled', 'protocol', 'host', 'port', 'auth'], ['id', 'protocol', 'host', 'port']);
|
|
32
|
+
const id = identity(value.id);
|
|
33
|
+
if (!['http', 'https'].includes(value.protocol)) invalid('代理协议当前只接受 http 或 https;协议声明不代表已完成连通测试');
|
|
34
|
+
if (typeof value.host !== 'string' || !value.host || value.host.length > 253 || /[\s/\\@?#%]/.test(value.host)
|
|
35
|
+
|| !Number.isInteger(value.port) || value.port < 1 || value.port > 65535) invalid('代理 host 和 port 无效;请分开填写主机与1–65535端口,不填写认证 URL');
|
|
36
|
+
let host = value.host;
|
|
37
|
+
if (host.startsWith('[') && host.endsWith(']')) host = host.slice(1, -1);
|
|
38
|
+
if (host.includes(':') && isIP(host) !== 6) invalid('代理 IPv6 地址无效');
|
|
39
|
+
try {
|
|
40
|
+
const url = new URL(`${value.protocol}://${isIP(host) === 6 ? '[' + host + ']' : host}:${value.port}`);
|
|
41
|
+
if (!url.hostname || url.username || url.password || url.pathname !== '/' || url.search || url.hash) invalid('代理 host 无效');
|
|
42
|
+
host = url.hostname.replace(/^\[|\]$/g, '');
|
|
43
|
+
} catch (error) {
|
|
44
|
+
if (error.code === 'IRIS_NETWORK_CONFIG_INVALID') throw error;
|
|
45
|
+
invalid('代理 host 无效');
|
|
46
|
+
}
|
|
47
|
+
if (value.enabled !== undefined && typeof value.enabled !== 'boolean') invalid('enabled 必须是布尔值');
|
|
48
|
+
let auth;
|
|
49
|
+
if (value.auth !== undefined && value.auth !== null) {
|
|
50
|
+
networkFields(value.auth, ['usernameEnv', 'passwordEnv'], ['usernameEnv', 'passwordEnv']);
|
|
51
|
+
if (![value.auth.usernameEnv, value.auth.passwordEnv].every(name => typeof name === 'string' && ENV.test(name))) invalid('代理认证只接受成对的用户名/密码环境变量名');
|
|
52
|
+
auth = { usernameEnv: value.auth.usernameEnv, passwordEnv: value.auth.passwordEnv };
|
|
53
|
+
}
|
|
54
|
+
return { id, label: label(value.label === undefined ? id : value.label), enabled: value.enabled ?? true, protocol: value.protocol, host, port: value.port, ...(auth ? { auth } : {}) };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function normalizeNetworkPool(value) {
|
|
58
|
+
networkFields(value, ['id', 'label', 'enabled', 'mode', 'candidates'], ['id', 'mode', 'candidates']);
|
|
59
|
+
const id = identity(value.id);
|
|
60
|
+
if (!['fixed', 'ordered'].includes(value.mode) || !Array.isArray(value.candidates)
|
|
61
|
+
|| value.candidates.length > (value.mode === 'fixed' ? 1 : 16)) invalid('池需要 fixed(最多1候选)或 ordered(最多16候选),空候选仅为草稿');
|
|
62
|
+
if (value.enabled !== undefined && typeof value.enabled !== 'boolean') invalid('enabled 必须是布尔值');
|
|
63
|
+
const seen = new Set();
|
|
64
|
+
const candidates = value.candidates.map(candidate => {
|
|
65
|
+
networkFields(candidate, ['kind', 'nodeId'], ['kind']);
|
|
66
|
+
let next, key;
|
|
67
|
+
if (candidate.kind === 'node') { next = { kind: 'node', nodeId: identity(candidate.nodeId) }; key = 'node:' + next.nodeId; }
|
|
68
|
+
else if (candidate.kind === 'direct' && !Object.hasOwn(candidate, 'nodeId')) { next = { kind: 'direct' }; key = 'direct'; }
|
|
69
|
+
else invalid('池候选必须是明确的 node 引用或 direct');
|
|
70
|
+
if (seen.has(key)) invalid('池中不能重复引用同一节点或直连候选');
|
|
71
|
+
seen.add(key); return next;
|
|
72
|
+
});
|
|
73
|
+
return { id, label: label(value.label === undefined ? id : value.label), enabled: value.enabled ?? true, mode: value.mode, candidates };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function validateNetworkConfig(value) {
|
|
77
|
+
if (![NETWORK_CONFIG_VERSION, NETWORK_ROUTING_CONFIG_VERSION].includes(value?.schemaVersion)) networkConfigFail('IRIS_NETWORK_CONFIG_VERSION', '不支持此网络配置版本;请使用兼容版本,原文件不会自动迁移或覆盖');
|
|
78
|
+
const routing = value.schemaVersion === NETWORK_ROUTING_CONFIG_VERSION;
|
|
79
|
+
const fields = ['schemaVersion', 'configId', 'generation', 'nodes', 'pools', ...(routing ? ['bindings'] : [])];
|
|
80
|
+
networkFields(value, fields, fields);
|
|
81
|
+
if (typeof value.configId !== 'string' || !UUID.test(value.configId) || !Number.isSafeInteger(value.generation) || value.generation < 1
|
|
82
|
+
|| !Array.isArray(value.nodes) || !Array.isArray(value.pools) || value.nodes.length > 256 || value.pools.length > 256) invalid('网络配置身份、修订或资源列表无效(节点/池各最多256项)');
|
|
83
|
+
const nodes = value.nodes.map(normalizeNetworkNode), pools = value.pools.map(normalizeNetworkPool);
|
|
84
|
+
const ids = new Set(nodes.map(node => node.id));
|
|
85
|
+
if (ids.size !== nodes.length || new Set(pools.map(pool => pool.id)).size !== pools.length) invalid('同类资源 ID 不能重复');
|
|
86
|
+
for (const pool of pools) for (const candidate of pool.candidates) {
|
|
87
|
+
if (candidate.kind === 'node' && !ids.has(candidate.nodeId)) invalid('池引用了不存在的节点;请先添加节点或显式编辑候选');
|
|
88
|
+
}
|
|
89
|
+
let bindings;
|
|
90
|
+
if (routing) {
|
|
91
|
+
if (!Array.isArray(value.bindings) || value.bindings.length > 1024) invalid('网络绑定必须是最多1024项的列表');
|
|
92
|
+
const seen = new Set(), poolIds = new Set(pools.map(pool => pool.id));
|
|
93
|
+
bindings = value.bindings.map(binding => {
|
|
94
|
+
networkFields(binding, ['catalogId', 'providerId', 'poolId'], ['catalogId', 'providerId', 'poolId']);
|
|
95
|
+
if (typeof binding.catalogId !== 'string' || !UUID.test(binding.catalogId)
|
|
96
|
+
|| typeof binding.providerId !== 'string' || !binding.providerId.trim() || binding.providerId.length > 256
|
|
97
|
+
|| /[\x00-\x1f\x7f]/.test(binding.providerId)) invalid('绑定需要有效的目录身份与供应商 ID');
|
|
98
|
+
const poolId = identity(binding.poolId), key = JSON.stringify([binding.catalogId, binding.providerId]);
|
|
99
|
+
if (seen.has(key) || !poolIds.has(poolId)) invalid('绑定重复或引用了不存在的池');
|
|
100
|
+
seen.add(key);
|
|
101
|
+
return { catalogId: binding.catalogId, providerId: binding.providerId, poolId };
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
return { schemaVersion: value.schemaVersion, configId: value.configId, generation: value.generation, nodes, pools, ...(routing ? { bindings } : {}) };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function networkNodeCredentialStatus(node, env = process.env) {
|
|
108
|
+
if (!node.auth) return 'none';
|
|
109
|
+
const values = [env[node.auth.usernameEnv], env[node.auth.passwordEnv]];
|
|
110
|
+
if (values.some(value => typeof value !== 'string' || !value.length)) return 'missing';
|
|
111
|
+
return values.some(value => value.length > 4096 || /[\x00-\x1f\x7f]/.test(value)) || values[0].includes(':') ? 'invalid' : 'ready';
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function projectNetworkConfig(config, revision, env = process.env) {
|
|
115
|
+
const secrets = [...new Set(config.nodes.flatMap(node => node.auth ? [env[node.auth.usernameEnv], env[node.auth.passwordEnv]] : [])
|
|
116
|
+
.filter(value => typeof value === 'string' && value.length))].sort((a, b) => b.length - a.length);
|
|
117
|
+
const display = value => redactProviderMessage(secrets.reduce((text, secret) => text.split(secret).join('[redacted]'), value));
|
|
118
|
+
const nodes = config.nodes.map(node => ({ ...node, label: display(node.label), host: display(node.host), credentialStatus: networkNodeCredentialStatus(node, env),
|
|
119
|
+
referencedBy: config.pools.filter(pool => pool.candidates.some(candidate => candidate.kind === 'node' && candidate.nodeId === node.id)).map(pool => pool.id) }));
|
|
120
|
+
const pools = config.pools.map(pool => ({ ...pool, label: display(pool.label),
|
|
121
|
+
...(config.bindings ? { referencedBy: config.bindings.filter(binding => binding.poolId === pool.id).map(({ catalogId, providerId }) => ({ catalogId, providerId })) } : {}),
|
|
122
|
+
status: !pool.enabled ? 'disabled' : !pool.candidates.length ? 'empty'
|
|
123
|
+
: pool.candidates.some(candidate => candidate.kind === 'direct' || nodes.some(node => node.id === candidate.nodeId && node.enabled && ['none', 'ready'].includes(node.credentialStatus))) ? 'configured' : 'unavailable' }));
|
|
124
|
+
return { schemaVersion: config.schemaVersion, configId: config.configId, generation: config.generation, revision,
|
|
125
|
+
...(config.bindings ? { routingConfigured: config.bindings.length > 0, runtimeStatus: 'not_observed' } : { routingActive: false }), nodes, pools,
|
|
126
|
+
...(config.bindings ? { bindings: config.bindings.map(binding => ({ ...binding })) } : {}) };
|
|
127
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// Explicit GET diagnostics in the caller's Core process, never a media probe.
|
|
2
|
+
import net from 'node:net';
|
|
3
|
+
import { performance } from 'node:perf_hooks';
|
|
4
|
+
import { loadProviderCatalog } from './provider-catalog.js';
|
|
5
|
+
import { providerMediaBaseUrl } from './provider-protocol.js';
|
|
6
|
+
import { networkManagementSettings } from './network-management.js';
|
|
7
|
+
import { networkFields } from './network-config.js';
|
|
8
|
+
import { providerNetworkRequirement, sameNetworkRequirement, networkPolicyError } from './network-policy.js';
|
|
9
|
+
import { inheritedProviderFetch } from './provider-network.js';
|
|
10
|
+
import { readProviderText } from './provider-response.js';
|
|
11
|
+
import { settingsRevision } from './provider-config-service.js';
|
|
12
|
+
|
|
13
|
+
export const NETWORK_PROBE_TIMEOUT_MS = 8000, NETWORK_PROBE_MAX_BYTES = 16 * 1024;
|
|
14
|
+
const proxyNames = ['HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY', 'NO_PROXY', 'http_proxy', 'https_proxy', 'all_proxy', 'no_proxy'];
|
|
15
|
+
export function networkEnvironmentFacts({ env = process.env, execArgv = process.execArgv } = {}) {
|
|
16
|
+
const nodeFlagRequested = execArgv.includes('--use-env-proxy') || /(?:^|\s)--use-env-proxy(?:\s|$)/.test(env.NODE_OPTIONS || '') || env.NODE_USE_ENV_PROXY === '1';
|
|
17
|
+
return { variables: Object.fromEntries(proxyNames.map(name => [name, Boolean(env[name])])), nodeFlagRequested,
|
|
18
|
+
inheritedPolicy: 'unknown', systemTunnel: 'unknown',
|
|
19
|
+
note: '变量存在或Node开关被请求不等于所有请求已代理;宿主fetch/dispatcher和系统TUN的实际路径未由静态检查证明。' };
|
|
20
|
+
}
|
|
21
|
+
export function networkDoctor(options) {
|
|
22
|
+
const report = { contractVersion: 0, observedAt: new Date().toISOString(), source: 'core-process',
|
|
23
|
+
process: { node: process.version, platform: process.platform, arch: process.arch },
|
|
24
|
+
environment: networkEnvironmentFacts(), networkTested: false };
|
|
25
|
+
try { return { ...report, settings: networkManagementSettings(options), configuration: { state: 'readable' } }; }
|
|
26
|
+
catch (error) {
|
|
27
|
+
if (!/^IRIS_(?:NETWORK|PROVIDER)_/.test(error.code || '')) throw error;
|
|
28
|
+
return { ...report, settings: null, configuration: { state: 'invalid', issue: error.code,
|
|
29
|
+
message: '指定的网络或供应商配置无法读取;请核对文件版本、格式、权限与错误码。未自动修复或联网。' } };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function probeNetworkRoute(options, input, { signal } = {}) {
|
|
34
|
+
const fields = ['expectedRevision', 'expectedProviderRevision', 'providerId', 'target', ...(input?.target === 'provider' ? [] : ['url'])];
|
|
35
|
+
networkFields(input, fields, fields);
|
|
36
|
+
if (!['provider', 'custom', 'ip'].includes(input.target)) throw networkPolicyError('IRIS_NETWORK_PROBE_INVALID', '请选择供应商地址、自定义GET或IP观察');
|
|
37
|
+
if (input.target !== 'provider' && (typeof input.url !== 'string' || !input.url || input.url.length > 2048)) throw networkPolicyError('IRIS_NETWORK_PROBE_INVALID', '测试URL须为最多2048字符的文本');
|
|
38
|
+
const settings = networkManagementSettings(options);
|
|
39
|
+
if (input.expectedRevision !== settings.network?.revision || input.expectedProviderRevision !== settings.catalog.revision) {
|
|
40
|
+
throw networkPolicyError('IRIS_NETWORK_CONFIG_CHANGED', '网络或供应商目录已变化,请重新读取后测试');
|
|
41
|
+
}
|
|
42
|
+
if (['mismatch', 'missing_network'].includes(settings.catalog.state)) throw networkPolicyError('IRIS_NETWORK_CONFIG_MISMATCH', '供应商与网络配置不匹配,未进行测试');
|
|
43
|
+
if (!settings.providers.some(provider => provider.id === input.providerId)) throw networkPolicyError('IRIS_NETWORK_PROVIDER_NOT_FOUND', '测试需要当前目录中的明确供应商');
|
|
44
|
+
const catalog = loadProviderCatalog(options.providerConfigPath), provider = catalog.providers.find(item => item.id === input.providerId);
|
|
45
|
+
if (settingsRevision(catalog) !== input.expectedProviderRevision) throw networkPolicyError('IRIS_NETWORK_PROVIDER_CHANGED', '供应商目录已变化,未进行测试');
|
|
46
|
+
if (!provider) throw networkPolicyError('IRIS_NETWORK_PROVIDER_NOT_FOUND', '测试需要当前目录中的明确供应商');
|
|
47
|
+
let target;
|
|
48
|
+
try { target = new URL(input.target === 'provider' ? providerMediaBaseUrl(provider) : input.url); }
|
|
49
|
+
catch (_) { throw networkPolicyError('IRIS_NETWORK_PROBE_INVALID', '测试地址需要有效的HTTP(S) URL'); }
|
|
50
|
+
if (!['http:', 'https:'].includes(target.protocol) || target.username || target.password || target.hash) {
|
|
51
|
+
throw networkPolicyError('IRIS_NETWORK_PROBE_INVALID', '测试地址只接受不含认证信息或片段的HTTP(S) URL');
|
|
52
|
+
}
|
|
53
|
+
const combined = AbortSignal.any([AbortSignal.timeout(NETWORK_PROBE_TIMEOUT_MS), ...(signal ? [signal] : [])]);
|
|
54
|
+
const started = performance.now(); let route = null, dispatchStarted = false, status = null, responseReceived = false;
|
|
55
|
+
const report = () => ({ contractVersion: 0, source: 'core-process', observedAt: new Date().toISOString(),
|
|
56
|
+
providerId: provider.id, target: { kind: input.target, origin: target.origin }, elapsedMs: Math.round(performance.now() - started),
|
|
57
|
+
timeoutMs: NETWORK_PROBE_TIMEOUT_MS, maxBytes: NETWORK_PROBE_MAX_BYTES, requestLimit: 1, requestLimitKind: 'application_get',
|
|
58
|
+
dispatchStarted, responseReceived, httpStatus: status, route,
|
|
59
|
+
note: '只表示本次目标与路线;不证明其他服务、所有任务或系统TUN行为。不携带供应商Key,不生成媒体。' });
|
|
60
|
+
try {
|
|
61
|
+
const requirement = providerNetworkRequirement(provider);
|
|
62
|
+
const network = requirement ? options.resolveProviderNetwork?.({ providerId: provider.id, networkRequirement: requirement }) : undefined;
|
|
63
|
+
if (requirement && (!network || network.providerId !== provider.id || !sameNetworkRequirement(requirement, network.networkRequirement))) throw networkPolicyError('IRIS_NETWORK_CONTEXT_REQUIRED', '测试需要匹配的Core网络上下文,不会静默直连');
|
|
64
|
+
const action = async fetch => {
|
|
65
|
+
combined.throwIfAborted();
|
|
66
|
+
if (!network) { dispatchStarted = true; route = { route: 'inherit', providerId: provider.id, legacy: true }; }
|
|
67
|
+
const response = await fetch(target.href, { method: 'GET', redirect: 'error', credentials: 'omit', signal: combined,
|
|
68
|
+
headers: { Accept: input.target === 'ip' ? 'application/json' : '*/*' } });
|
|
69
|
+
status = response.status; responseReceived = true;
|
|
70
|
+
const text = await readProviderText(response, { maxBytes: NETWORK_PROBE_MAX_BYTES, stage: 'discover', acceptance: 'not_accepted' });
|
|
71
|
+
let observedIp;
|
|
72
|
+
if (input.target === 'ip' && response.ok) {
|
|
73
|
+
let value; try { value = JSON.parse(text); } catch (_) { /* Report a limited observation, never the response body. */ }
|
|
74
|
+
if (value && typeof value.ip === 'string' && net.isIP(value.ip)) observedIp = value.ip;
|
|
75
|
+
else return { ...report(), ok: false, reachable: true, issue: 'IRIS_NETWORK_IP_NOT_OBSERVED' };
|
|
76
|
+
}
|
|
77
|
+
return { ...report(), ok: response.ok, reachable: true, ...(observedIp ? { observedIp } : {}),
|
|
78
|
+
...(!response.ok ? { issue: 'IRIS_NETWORK_TARGET_HTTP_ERROR' } : {}) };
|
|
79
|
+
};
|
|
80
|
+
return network ? await network.run(action, { operation: 'discover', recordNetworkRoute: fact => {
|
|
81
|
+
dispatchStarted = true;
|
|
82
|
+
route = { route: fact.route, providerId: fact.providerId, ...(fact.poolId ? { poolId: fact.poolId } : {}),
|
|
83
|
+
...(fact.nodeId ? { nodeId: fact.nodeId } : {}), configGeneration: fact.configGeneration, configRevision: fact.configRevision };
|
|
84
|
+
} }) : await action(inheritedProviderFetch);
|
|
85
|
+
} catch (error) {
|
|
86
|
+
signal?.throwIfAborted();
|
|
87
|
+
const code = error.providerCode || error.code;
|
|
88
|
+
if (!(error instanceof TypeError && !error.code) && !['AbortError', 'TimeoutError'].includes(error.name)
|
|
89
|
+
&& !/^IRIS_NETWORK_/.test(code || '') && code !== 'IRIS_PROVIDER_RESPONSE_TOO_LARGE') throw error;
|
|
90
|
+
return { ...report(), ok: false, reachable: responseReceived,
|
|
91
|
+
issue: combined.aborted ? 'IRIS_NETWORK_PROBE_TIMEOUT' : code === 'IRIS_PROVIDER_RESPONSE_TOO_LARGE' ? 'IRIS_NETWORK_PROBE_TOO_LARGE'
|
|
92
|
+
: /^IRIS_NETWORK_/.test(code || '') ? code : 'IRIS_NETWORK_PROBE_FAILED' };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Owner-only composition over the existing resource store. No implicit enrollment.
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import { readNetworkConfig, executeNetworkConfigCommand } from './network-config-service.js';
|
|
4
|
+
import { networkFields, projectNetworkConfig } from './network-config.js';
|
|
5
|
+
import { loadProviderCatalog } from './provider-catalog.js';
|
|
6
|
+
import { projectProviderSettings } from './provider-settings.js';
|
|
7
|
+
import { settingsRevision } from './provider-config-service.js';
|
|
8
|
+
import { providerNetworkRequirement, networkPolicyError } from './network-policy.js';
|
|
9
|
+
|
|
10
|
+
export const NETWORK_MANAGEMENT_ACTIONS = Object.freeze(['init', 'nodes/add', 'nodes/set', 'nodes/remove',
|
|
11
|
+
'pools/add', 'pools/set', 'pools/remove', 'bindings/set', 'bindings/remove']);
|
|
12
|
+
|
|
13
|
+
export function networkManagementSettings({ networkConfigPath, providerConfigPath, registry }) {
|
|
14
|
+
let saved;
|
|
15
|
+
try { fs.lstatSync(networkConfigPath); saved = readNetworkConfig(networkConfigPath); }
|
|
16
|
+
catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
17
|
+
const catalog = providerConfigPath ? loadProviderCatalog(providerConfigPath) : undefined;
|
|
18
|
+
const requirement = providerNetworkRequirement(catalog);
|
|
19
|
+
const revision = catalog ? settingsRevision(catalog) : null;
|
|
20
|
+
const state = !catalog ? 'absent' : !requirement ? 'legacy' : !saved ? 'missing_network'
|
|
21
|
+
: saved.config.schemaVersion !== 2 || saved.config.configId !== requirement.configId ? 'mismatch' : 'ready';
|
|
22
|
+
const network = saved ? projectNetworkConfig(saved.config, saved.revision) : null;
|
|
23
|
+
const settings = catalog ? projectProviderSettings(catalog, revision, registry) : undefined;
|
|
24
|
+
const providers = (settings?.providers || []).map(provider => {
|
|
25
|
+
const binding = state === 'ready' && saved.config.bindings.find(item => item.catalogId === requirement.catalogId && item.providerId === provider.id);
|
|
26
|
+
const pool = binding && network.pools.find(item => item.id === binding.poolId);
|
|
27
|
+
return { id: provider.id, name: provider.name, enabled: provider.enabled,
|
|
28
|
+
route: { state: ['mismatch', 'missing_network'].includes(state) ? 'blocked' : !binding ? 'inherit' : pool.status === 'configured' ? 'pool' : 'blocked',
|
|
29
|
+
...(pool ? { poolId: pool.id, poolLabel: pool.label, poolStatus: pool.status, mode: pool.mode } : {}),
|
|
30
|
+
...(state === 'legacy' ? { legacy: true } : {}) } };
|
|
31
|
+
});
|
|
32
|
+
return { contractVersion: 0, storage: saved ? 'ready' : 'missing', network,
|
|
33
|
+
catalog: { state, revision, ...(requirement ? { configId: requirement.configId, catalogId: requirement.catalogId } : {}) }, providers };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function manageNetworkResources(options, action, input, { signal } = {}) {
|
|
37
|
+
if (!NETWORK_MANAGEMENT_ACTIONS.includes(action)) throw new TypeError('未知网络管理操作');
|
|
38
|
+
let command = action === 'init' ? 'routing.init' : action.replace('/', '.');
|
|
39
|
+
if (action === 'bindings/set') {
|
|
40
|
+
networkFields(input, ['expectedRevision', 'expectedProviderRevision', 'providerId', 'poolId'],
|
|
41
|
+
['expectedRevision', 'expectedProviderRevision', 'providerId', 'poolId']);
|
|
42
|
+
const settings = networkManagementSettings(options);
|
|
43
|
+
if (settings.catalog.state !== 'ready') throw networkPolicyError('IRIS_NETWORK_CATALOG_REQUIRED', '绑定需要匹配的v2网络文件和供应商目录;请先按指引另存启用,不自动迁移');
|
|
44
|
+
if (input.expectedProviderRevision !== settings.catalog.revision) throw networkPolicyError('IRIS_NETWORK_PROVIDER_CHANGED', '供应商目录已变化,请重新读取后核对绑定');
|
|
45
|
+
if (!settings.providers.some(provider => provider.id === input.providerId)) throw networkPolicyError('IRIS_NETWORK_PROVIDER_NOT_FOUND', '所选供应商不在当前目录');
|
|
46
|
+
input = { expectedRevision: input.expectedRevision, catalogId: settings.catalog.catalogId, providerId: input.providerId, poolId: input.poolId };
|
|
47
|
+
}
|
|
48
|
+
const changed = executeNetworkConfigCommand(options.networkConfigPath, command, input, { signal });
|
|
49
|
+
return { contractVersion: 0, changed: changed.created === true || changed.changed === true,
|
|
50
|
+
settings: networkManagementSettings(options) };
|
|
51
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Nonsecret identity shared by persisted records and owned Provider assembly.
|
|
2
|
+
export const CATALOG_NETWORK = Symbol('iris.catalog-network');
|
|
3
|
+
const UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/;
|
|
4
|
+
export function checkedNetworkRequirement(value) {
|
|
5
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)
|
|
6
|
+
|| Object.keys(value).length !== 3 || Object.keys(value).some(key => !['schemaVersion', 'configId', 'catalogId'].includes(key))
|
|
7
|
+
|| value.schemaVersion !== 1 || !UUID.test(value.configId) || !UUID.test(value.catalogId)) {
|
|
8
|
+
throw Object.assign(new Error('网络要求身份或版本无效'), { code: 'IRIS_NETWORK_REQUIREMENT_INVALID' });
|
|
9
|
+
}
|
|
10
|
+
return { schemaVersion: 1, configId: value.configId, catalogId: value.catalogId };
|
|
11
|
+
}
|
|
12
|
+
export function sameNetworkRequirement(a, b) {
|
|
13
|
+
return a === undefined || b === undefined ? a === b : a.schemaVersion === b.schemaVersion && a.configId === b.configId && a.catalogId === b.catalogId;
|
|
14
|
+
}
|
|
15
|
+
export function providerNetworkRequirement(provider) { return provider?.[CATALOG_NETWORK]; }
|
|
16
|
+
export function createNetworkAttemptBudget() { return { remaining: 16, probes: new Map() }; }
|
|
17
|
+
export function networkPolicyError(code, message, stage = 'validate') {
|
|
18
|
+
return Object.assign(new Error(message), { code, stage, category: 'invalid_request', acceptance: 'not_accepted' });
|
|
19
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Nonsecret dispatch intentions. These records do not prove receipt or public IP.
|
|
2
|
+
import { checkedNetworkRequirement } from './network-policy.js';
|
|
3
|
+
|
|
4
|
+
export const MAX_NETWORK_ROUTE_FACTS = 128;
|
|
5
|
+
const ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
6
|
+
const ATTEMPT = /^attempt_[a-f0-9]{24}$/;
|
|
7
|
+
const SHA = /^[a-f0-9]{64}$/;
|
|
8
|
+
const OPERATIONS = ['discover', 'prepareInput', 'submit', 'poll', 'download', 'cancel', 'vision'];
|
|
9
|
+
const FIELDS = ['schemaVersion', 'configId', 'catalogId', 'configGeneration', 'configRevision', 'providerId', 'operation', 'route', 'poolId', 'nodeId', 'attemptId'];
|
|
10
|
+
const invalid = () => { throw Object.assign(new Error('网络路线事实包含无效身份、阶段或未允许字段'), { code: 'IRIS_NETWORK_ROUTE_INVALID' }); };
|
|
11
|
+
|
|
12
|
+
export function checkedNetworkRouteFact(value) {
|
|
13
|
+
if (!value || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).some(key => !FIELDS.includes(key))
|
|
14
|
+
|| value.schemaVersion !== 1 || !Number.isSafeInteger(value.configGeneration) || value.configGeneration < 1 || !SHA.test(value.configRevision)
|
|
15
|
+
|| typeof value.providerId !== 'string' || !value.providerId || value.providerId.length > 256 || /[\x00-\x1f\x7f]/.test(value.providerId)
|
|
16
|
+
|| !OPERATIONS.includes(value.operation) || !['inherit', 'direct', 'proxy'].includes(value.route)
|
|
17
|
+
|| value.poolId !== undefined && !ID.test(value.poolId) || value.nodeId !== undefined && !ID.test(value.nodeId)
|
|
18
|
+
|| value.attemptId !== undefined && !ATTEMPT.test(value.attemptId)
|
|
19
|
+
|| value.route === 'proxy' && (!value.poolId || !value.nodeId)
|
|
20
|
+
|| value.route !== 'proxy' && value.nodeId !== undefined || value.route === 'inherit' && value.poolId !== undefined) invalid();
|
|
21
|
+
try { checkedNetworkRequirement({ schemaVersion: 1, configId: value.configId, catalogId: value.catalogId }); }
|
|
22
|
+
catch (_) { invalid(); }
|
|
23
|
+
return { schemaVersion: 1, configId: value.configId, catalogId: value.catalogId,
|
|
24
|
+
configGeneration: value.configGeneration, configRevision: value.configRevision, providerId: value.providerId,
|
|
25
|
+
operation: value.operation, route: value.route,
|
|
26
|
+
...(value.poolId !== undefined ? { poolId: value.poolId } : {}), ...(value.nodeId !== undefined ? { nodeId: value.nodeId } : {}),
|
|
27
|
+
...(value.attemptId !== undefined ? { attemptId: value.attemptId } : {}) };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const networkRouteFactKey = value => JSON.stringify(checkedNetworkRouteFact(value));
|
|
31
|
+
export function checkedNetworkRouteRecord(value) {
|
|
32
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) invalid();
|
|
33
|
+
const { firstAt, lastAt, requestCount, phase, ...fact } = value;
|
|
34
|
+
const checked = checkedNetworkRouteFact(fact);
|
|
35
|
+
const iso = time => typeof time === 'string' && Number.isFinite(Date.parse(time)) && new Date(time).toISOString() === time;
|
|
36
|
+
if (phase !== 'dispatch_started' || !iso(firstAt) || !iso(lastAt) || lastAt < firstAt || !Number.isSafeInteger(requestCount) || requestCount < 1) invalid();
|
|
37
|
+
return { ...checked, phase, firstAt, lastAt, requestCount };
|
|
38
|
+
}
|