@artenx/dshb 0.1.18 → 0.1.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/lib/client.js +2157 -567
  2. package/lib/client.js.map +1 -1
  3. package/lib/core/index.js +72 -4
  4. package/lib/core/index.js.map +1 -1
  5. package/lib/core/node-registry.d.ts +2 -108
  6. package/lib/core/node-registry.js +43 -2
  7. package/lib/core/node-registry.js.map +1 -1
  8. package/lib/core/routes.d.ts +4 -3
  9. package/lib/core/routes.js +67 -23
  10. package/lib/core/routes.js.map +1 -1
  11. package/lib/core/sync/coordinator.d.ts +48 -0
  12. package/lib/core/sync/coordinator.js +247 -0
  13. package/lib/core/sync/coordinator.js.map +1 -0
  14. package/lib/core/sync/host-key.d.ts +2 -0
  15. package/lib/core/sync/host-key.js +71 -0
  16. package/lib/core/sync/host-key.js.map +1 -0
  17. package/lib/core/sync/internal-routes.d.ts +9 -0
  18. package/lib/core/sync/internal-routes.js +236 -0
  19. package/lib/core/sync/internal-routes.js.map +1 -0
  20. package/lib/core/sync/model.d.ts +2 -0
  21. package/lib/core/sync/model.js +106 -0
  22. package/lib/core/sync/model.js.map +1 -0
  23. package/lib/core/sync/node-sync.d.ts +2 -0
  24. package/lib/core/sync/node-sync.js +660 -0
  25. package/lib/core/sync/node-sync.js.map +1 -0
  26. package/lib/core/sync/offline-archive.d.ts +55 -0
  27. package/lib/core/sync/offline-archive.js +673 -0
  28. package/lib/core/sync/offline-archive.js.map +1 -0
  29. package/lib/core/sync/offline-journal.d.ts +22 -0
  30. package/lib/core/sync/offline-journal.js +74 -0
  31. package/lib/core/sync/offline-journal.js.map +1 -0
  32. package/lib/core/sync/offline-routes.d.ts +7 -0
  33. package/lib/core/sync/offline-routes.js +192 -0
  34. package/lib/core/sync/offline-routes.js.map +1 -0
  35. package/lib/core/sync/peer-manager.d.ts +56 -0
  36. package/lib/core/sync/peer-manager.js +258 -0
  37. package/lib/core/sync/peer-manager.js.map +1 -0
  38. package/lib/core/sync/protocol.d.ts +2 -0
  39. package/lib/core/sync/protocol.js +124 -0
  40. package/lib/core/sync/protocol.js.map +1 -0
  41. package/lib/core/sync/remote-helper.d.ts +5 -0
  42. package/lib/core/sync/remote-helper.js +160 -0
  43. package/lib/core/sync/remote-helper.js.map +1 -0
  44. package/lib/core/sync/revision.d.ts +2 -0
  45. package/lib/core/sync/revision.js +77 -0
  46. package/lib/core/sync/revision.js.map +1 -0
  47. package/lib/core/sync/routes.d.ts +9 -0
  48. package/lib/core/sync/routes.js +339 -0
  49. package/lib/core/sync/routes.js.map +1 -0
  50. package/lib/core/sync/scheduler.d.ts +14 -0
  51. package/lib/core/sync/scheduler.js +81 -0
  52. package/lib/core/sync/scheduler.js.map +1 -0
  53. package/lib/core/sync/session-journal.d.ts +2 -0
  54. package/lib/core/sync/session-journal.js +191 -0
  55. package/lib/core/sync/session-journal.js.map +1 -0
  56. package/lib/core/sync/session-routes.d.ts +7 -0
  57. package/lib/core/sync/session-routes.js +67 -0
  58. package/lib/core/sync/session-routes.js.map +1 -0
  59. package/lib/core/sync/session-sync.d.ts +2 -0
  60. package/lib/core/sync/session-sync.js +2 -0
  61. package/lib/core/sync/ssh-transport.d.ts +2 -0
  62. package/lib/core/sync/ssh-transport.js +408 -0
  63. package/lib/core/sync/ssh-transport.js.map +1 -0
  64. package/lib/core/sync/stores.d.ts +3 -0
  65. package/lib/core/sync/stores.js +385 -0
  66. package/lib/core/sync/stores.js.map +1 -0
  67. package/lib/core/test.d.ts +1 -1
  68. package/lib/core/workspace-bindings.d.ts +2 -28
  69. package/lib/core/workspace-bindings.js +14 -14
  70. package/lib/core/workspace-bindings.js.map +1 -1
  71. package/lib/core/workspace-instruction-context.d.ts +3 -2260
  72. package/lib/core/workspace-instruction-context.js +104 -2
  73. package/lib/core/workspace-instruction-context.js.map +1 -0
  74. package/lib/core/workspace-instruction-routes.d.ts +1 -1
  75. package/lib/core/workspace-instructions.d.ts +2 -68
  76. package/lib/core/workspace-instructions.js +13 -0
  77. package/lib/core/workspace-instructions.js.map +1 -1
  78. package/lib/core/workspaces.d.ts +4 -3
  79. package/lib/core/workspaces.js +17 -1
  80. package/lib/core/workspaces.js.map +1 -1
  81. package/lib/host-key-C5afhAUd.d.ts +30 -0
  82. package/lib/index-VF8bcBeX.d.ts +2293 -0
  83. package/lib/{workspace-instruction-context-DytAmaPY.js → lib-gdtU_WKT.js} +159 -102
  84. package/lib/lib-gdtU_WKT.js.map +1 -0
  85. package/lib/model-BP7Gw1yk.d.ts +104 -0
  86. package/lib/node-registry-BTnnM5-k.d.ts +112 -0
  87. package/lib/node-sync-3Z9QLz2n.d.ts +89 -0
  88. package/lib/protocol-CV7LGE44.d.ts +35 -0
  89. package/lib/revision-C9sOlDZ6.d.ts +25 -0
  90. package/lib/router/router-fs.d.ts +12 -3
  91. package/lib/router/router-fs.js.map +1 -1
  92. package/lib/session-journal-pgPqcWn9.d.ts +40 -0
  93. package/lib/session-sync-EdfwUON5.js +2177 -0
  94. package/lib/session-sync-EdfwUON5.js.map +1 -0
  95. package/lib/session-sync-g6qKZzjV.d.ts +1252 -0
  96. package/lib/ssh-transport-BcIiLxsX.d.ts +55 -0
  97. package/lib/stores-CKyEt0yk.d.ts +31 -0
  98. package/lib/workspace-bindings-CsB0TIJB.d.ts +28 -0
  99. package/lib/workspace-instructions-DirYYiku.d.ts +69 -0
  100. package/package.json +20 -1
  101. package/lib/workspace-instruction-context-DytAmaPY.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"routes.js","names":[],"sources":["../../src/core/routes.ts"],"sourcesContent":["import type { IncomingMessage, ServerResponse } from 'node:http'\nimport { mkdirSync, readdirSync } from 'node:fs'\nimport type { Context } from '@deepseek-ai/cordis'\nimport type {} from '@deepseek-ai/dsh-host-webserver'\nimport { resolveHostRunner } from '../exec-docker/host-runner.js'\nimport { NodeRegistry, type NodeProfile, type NodeType } from './node-registry.js'\nimport { readSshConfig, resolveSshConfigEntry } from './ssh-config.js'\nimport type { KnownHostsStore } from './known-hosts.js'\nimport { testNode, type SshHandshakeTester, type DockerNodeTester } from './test.js'\nimport type { WorkspaceBindingsStore } from './workspace-bindings.js'\n\ninterface WorldsLike {\n ensure(nodeId: string): Promise<{\n ensureDir(path: string): Promise<void>\n fs: { listDir(path: string): Promise<Array<{ name: string; path: string; isDirectory: boolean; isFile: boolean }>> }\n } | undefined>\n}\n\nfunction resolveWorlds(ctx: Context, nodeType: string): WorldsLike | undefined {\n if (nodeType === 'local-docker' || nodeType === 'remote-docker') {\n return ctx.get('dshbDockerWorldsGeneric') as WorldsLike | undefined\n }\n return ctx.get('dshbWorlds') as WorldsLike | undefined\n}\n\nconst MAX_BODY = 64 * 1024\n\nfunction sendJson(res: ServerResponse, status: number, body: unknown, headers: Record<string, string> = {}): void {\n res.writeHead(status, { 'content-type': 'application/json', ...headers })\n res.end(JSON.stringify(body))\n}\n\nfunction readBody(req: IncomingMessage): Promise<Record<string, unknown> | null> {\n return new Promise((resolve) => {\n if ((req.headers['content-type'] ?? '') !== 'application/json') {\n resolve(null)\n return\n }\n const chunks: Buffer[] = []\n let size = 0\n req.on('data', (chunk: Buffer) => {\n size += chunk.length\n if (size > MAX_BODY) {\n resolve(null)\n req.destroy()\n return\n }\n chunks.push(chunk)\n })\n req.on('end', () => {\n try {\n resolve(JSON.parse(Buffer.concat(chunks).toString('utf8')) as Record<string, unknown>)\n } catch {\n resolve(null)\n }\n })\n req.on('error', () => resolve(null))\n })\n}\n\nfunction pathSegments(req: IncomingMessage): string[] {\n const path = new URL(req.url ?? '/', 'http://x').pathname\n return path.split('/').filter(Boolean)\n}\n\nconst VALID_TYPES: NodeType[] = ['local-host', 'local-docker', 'remote-ssh', 'remote-docker']\n\ninterface DockerProvisionerLike {\n provision(nodeId: string): Promise<unknown>\n status(nodeId: string): { state: string; error?: string; containerId?: string; updatedAt: string } | undefined\n}\n\ninterface WorkspaceRegistryLike {\n list(): Array<{ path: string; title: string }>\n}\n\nfunction maybeProvision(ctx: Context, node: NodeProfile): boolean {\n if (node.type !== 'local-docker' && node.type !== 'remote-docker') return false\n if (!node.docker?.image) return false\n const provisioner = ctx.get('dshbDockerProvisioner') as DockerProvisionerLike | undefined\n if (!provisioner) return false\n void provisioner.provision(node.id).catch(() => {})\n return true\n}\n\nexport async function registerNodeRoutes(\n ctx: Context,\n registry: NodeRegistry,\n bindings: WorkspaceBindingsStore,\n getWorkspaceRegistry?: () => WorkspaceRegistryLike | undefined,\n): Promise<void> {\n const webServer = ctx.webServer\n ctx.effect(() =>\n webServer.register({\n kind: 'prefix',\n path: '/api/dshb/nodes',\n handler: async (req, res) => {\n const segs = pathSegments(req)\n const resourceId = segs[3]\n\n if (req.method === 'GET' && segs[4] === 'browse' && resourceId) {\n const node = registry.get(resourceId)\n if (!node) {\n sendJson(res, 404, { ok: false, error: 'node not found' })\n return\n }\n const path = new URL(req.url ?? '/', 'http://x').searchParams.get('path') ?? (node.type === 'local-host' ? process.env.HOME ?? '/' : '/')\n try {\n if (node.type === 'local-host') {\n const entries = readdirSync(path, { withFileTypes: true }).map((e) => ({\n name: e.name,\n path: `${path.replace(/\\/$/, '')}/${e.name}`,\n isDirectory: e.isDirectory(),\n isFile: e.isFile(),\n }))\n sendJson(res, 200, { ok: true, path, entries })\n return\n }\n const worlds = resolveWorlds(ctx, node.type)\n if (!worlds) {\n sendJson(res, 503, { ok: false, error: '远程执行世界不可用' })\n return\n }\n const world = await worlds.ensure(resourceId)\n if (!world) {\n sendJson(res, 503, { ok: false, error: '节点执行世界未就绪' })\n return\n }\n const entries = await world.fs.listDir(path)\n sendJson(res, 200, { ok: true, path, entries })\n } catch (err) {\n sendJson(res, 502, { ok: false, error: err instanceof Error ? err.message : String(err) })\n }\n return\n }\n\n if (req.method === 'POST' && segs[4] === 'mkdir' && resourceId) {\n const node = registry.get(resourceId)\n if (!node) {\n sendJson(res, 404, { ok: false, error: 'node not found' })\n return\n }\n const body = await readBody(req)\n const path = body && typeof body.path === 'string' ? body.path : undefined\n if (!path) {\n sendJson(res, 400, { ok: false, error: '需要 path' })\n return\n }\n try {\n if (node.type === 'local-host') {\n mkdirSync(path, { recursive: true })\n sendJson(res, 200, { ok: true, path })\n return\n }\n const worlds = resolveWorlds(ctx, node.type)\n if (!worlds) {\n sendJson(res, 503, { ok: false, error: '远程执行世界不可用' })\n return\n }\n const world = await worlds.ensure(resourceId)\n if (!world) {\n sendJson(res, 503, { ok: false, error: '节点执行世界未就绪' })\n return\n }\n await world.ensureDir(path)\n sendJson(res, 200, { ok: true, path })\n } catch (err) {\n sendJson(res, 502, { ok: false, error: err instanceof Error ? err.message : String(err) })\n }\n return\n }\n\n if (req.method === 'GET' && resourceId === undefined) {\n const provisioner = ctx.get('dshbDockerProvisioner') as DockerProvisionerLike | undefined\n const nodes = registry.list()\n const withFlags = await Promise.all(\n nodes.map(async (n) => ({\n ...n,\n hasSecret: await registry.hasSecret(n.id),\n status: registry.status(n.id),\n provision: provisioner?.status(n.id),\n })),\n )\n sendJson(res, 200, { ok: true, nodes: withFlags })\n return\n }\n\n if (req.method === 'POST' && resourceId === undefined) {\n const body = await readBody(req)\n if (!body) {\n sendJson(res, 400, { ok: false, error: '请求体必须是 JSON' })\n return\n }\n const type = String(body.type ?? '') as NodeType\n if (!VALID_TYPES.includes(type)) {\n sendJson(res, 400, { ok: false, error: 'type 必须是 local-host/local-docker/remote-ssh/remote-docker' })\n return\n }\n try {\n const node: NodeProfile = await registry.create({\n name: String(body.name ?? ''),\n type,\n ssh: body.ssh as never,\n docker: body.docker as never,\n secrets: body.secrets as never,\n })\n const provision = maybeProvision(ctx, node)\n sendJson(res, 201, { ok: true, node: { ...node, hasSecret: await registry.hasSecret(node.id) }, provisioning: provision })\n } catch (err) {\n sendJson(res, 409, { ok: false, error: err instanceof Error ? err.message : String(err) })\n }\n return\n }\n\n if (resourceId === undefined) {\n sendJson(res, 405, { ok: false, error: 'method not allowed' })\n return\n }\n\n if (req.method === 'GET' && segs[4] === undefined) {\n const node = registry.get(resourceId)\n if (!node) {\n sendJson(res, 404, { ok: false, error: 'node not found' })\n return\n }\n sendJson(res, 200, { ok: true, node: { ...node, hasSecret: await registry.hasSecret(node.id), status: registry.status(resourceId) } })\n return\n }\n\n if (req.method === 'PATCH' && segs[4] === undefined) {\n const body = await readBody(req)\n if (!body) {\n sendJson(res, 400, { ok: false, error: '请求体必须是 JSON' })\n return\n }\n try {\n const node = await registry.update(resourceId, {\n name: body.name as string | undefined,\n ssh: body.ssh as never,\n docker: body.docker as never,\n secrets: body.secrets as never,\n })\n const provision = maybeProvision(ctx, node)\n sendJson(res, 200, { ok: true, node: { ...node, hasSecret: await registry.hasSecret(node.id) }, provisioning: provision })\n } catch (err) {\n sendJson(res, 404, { ok: false, error: err instanceof Error ? err.message : String(err) })\n }\n return\n }\n\n if (req.method === 'DELETE' && segs[4] === undefined) {\n const node = registry.get(resourceId)\n if (!node) {\n sendJson(res, 404, { ok: false, error: 'node not found' })\n return\n }\n\n const attached = bindings.list().filter((binding) => binding.nodeId === resourceId)\n let workspaceRegistry: WorkspaceRegistryLike | undefined\n try {\n workspaceRegistry = getWorkspaceRegistry?.()\n } catch { /* protect deletion when the workspace registry is unavailable */ }\n let active = attached\n if (workspaceRegistry?.list) {\n try {\n const workspaces = workspaceRegistry.list()\n const byPath = new Map(workspaces.map((workspace) => [workspace.path, workspace.title]))\n active = attached.filter((binding) => byPath.has(binding.mirrorPath))\n if (active.length > 0) {\n const titles = active.map((binding) => byPath.get(binding.mirrorPath) ?? binding.mirrorPath)\n sendJson(res, 409, {\n ok: false,\n code: 'node/in-use',\n error: `节点仍被工作区使用:${titles.join('、')}。请先移除或迁移这些工作区。`,\n workspaces: titles,\n })\n return\n }\n } catch {\n active = attached\n }\n }\n if (active.length > 0) {\n sendJson(res, 409, {\n ok: false,\n code: 'node/in-use',\n error: `节点仍绑定 ${active.length} 个工作区,请先移除或迁移工作区。`,\n })\n return\n }\n\n try {\n if ((node.type === 'local-docker' || node.type === 'remote-docker') && node.docker?.containerId) {\n const runner = await resolveHostRunner(ctx, node)\n if (!runner) throw new Error('Docker 节点执行环境不可用,节点未删除')\n const result = await runner.run(['docker', 'rm', '-f', node.docker.containerId])\n if (result.code !== 0) throw new Error((result.stderr || result.stdout).trim() || '删除容器失败,节点未删除')\n }\n await registry.remove(resourceId)\n const sshWorlds = ctx.get('dshbSshWorlds') as { remove?: (id: string) => void } | undefined\n const dockerWorlds = ctx.get('dshbDockerWorlds') as { remove?: (id: string) => void } | undefined\n sshWorlds?.remove?.(resourceId)\n dockerWorlds?.remove?.(resourceId)\n sendJson(res, 200, { ok: true })\n } catch (err) {\n sendJson(res, 502, { ok: false, error: err instanceof Error ? err.message : String(err) })\n }\n return\n }\n\n if (req.method === 'POST' && resourceId === 'test-unsaved') {\n const body = await readBody(req)\n if (!body || !body.ssh) {\n sendJson(res, 400, { ok: false, error: '需要 ssh 配置' })\n return\n }\n const sshTester = ctx.get('dshbSshPool') as SshHandshakeTester | undefined\n if (!sshTester) {\n sendJson(res, 503, { ok: false, error: 'SSH 执行通道不可用' })\n return\n }\n const ssh = body.ssh as { host: string; port: number; username: string; auth: never; jump?: never }\n const secrets = (body.secrets ?? {}) as { password?: string; privateKey?: string; passphrase?: string }\n const t = await sshTester.test(\n { host: ssh.host, port: Number(ssh.port) || 22, username: ssh.username, auth: ssh.auth, jump: ssh.jump },\n secrets,\n )\n sendJson(res, 200, { ok: true, report: { ok: t.ok, reachable: t.ok, error: t.error, ...(t.category ? { category: t.category } : {}) } })\n return\n }\n\n if (req.method === 'POST' && segs[4] === 'test') {\n const body = await readBody(req)\n const sshTester = ctx.get('dshbSshPool') as SshHandshakeTester | undefined\n const dockerTester = ctx.get('dshbDockerTester') as DockerNodeTester | undefined\n const overrides = body && (body.ssh || body.secrets)\n ? { ssh: body.ssh as never, secrets: body.secrets as never }\n : undefined\n const report = await testNode(registry, resourceId, sshTester, overrides, dockerTester)\n sendJson(res, 200, { ok: true, report })\n return\n }\n\n sendJson(res, 404, { ok: false, error: 'not found' })\n },\n }),\n )\n}\n\nexport async function registerSshConfigRoutes(ctx: Context, knownHosts: KnownHostsStore): Promise<void> {\n const webServer = ctx.webServer\n\n ctx.effect(() =>\n webServer.register({\n kind: 'exact',\n path: '/api/dshb/ssh-config',\n handler: (_req, res) => {\n const entries = readSshConfig().map(resolveSshConfigEntry)\n sendJson(res, 200, { ok: true, entries })\n },\n }),\n )\n\n ctx.effect(() =>\n webServer.register({\n kind: 'prefix',\n path: '/api/dshb/known-hosts',\n handler: async (req, res) => {\n const segs = pathSegments(req)\n const action = segs[3]\n\n if (req.method === 'GET' && action === undefined) {\n sendJson(res, 200, { ok: true, hosts: knownHosts.list() })\n return\n }\n\n if (req.method === 'POST' && action === 'forget') {\n const body = await readBody(req)\n if (!body || typeof body.host !== 'string') {\n sendJson(res, 400, { ok: false, error: '需要 host' })\n return\n }\n const port = Number(body.port ?? 22)\n knownHosts.forget(String(body.host), port)\n sendJson(res, 200, { ok: true })\n return\n }\n\n sendJson(res, 404, { ok: false, error: 'not found' })\n },\n }),\n )\n}\n"],"mappings":";;;;;AAkBA,SAAS,cAAc,KAAc,UAA0C;CAC7E,IAAI,aAAa,kBAAkB,aAAa,iBAC9C,OAAO,IAAI,IAAI,yBAAyB;CAE1C,OAAO,IAAI,IAAI,YAAY;AAC7B;AAEA,MAAM,WAAW;AAEjB,SAAS,SAAS,KAAqB,QAAgB,MAAe,UAAkC,CAAC,GAAS;CAChH,IAAI,UAAU,QAAQ;EAAE,gBAAgB;EAAoB,GAAG;CAAQ,CAAC;CACxE,IAAI,IAAI,KAAK,UAAU,IAAI,CAAC;AAC9B;AAEA,SAAS,SAAS,KAA+D;CAC/E,OAAO,IAAI,SAAS,YAAY;EAC9B,KAAK,IAAI,QAAQ,mBAAmB,QAAQ,oBAAoB;GAC9D,QAAQ,IAAI;GACZ;EACF;EACA,MAAM,SAAmB,CAAC;EAC1B,IAAI,OAAO;EACX,IAAI,GAAG,SAAS,UAAkB;GAChC,QAAQ,MAAM;GACd,IAAI,OAAO,UAAU;IACnB,QAAQ,IAAI;IACZ,IAAI,QAAQ;IACZ;GACF;GACA,OAAO,KAAK,KAAK;EACnB,CAAC;EACD,IAAI,GAAG,aAAa;GAClB,IAAI;IACF,QAAQ,KAAK,MAAM,OAAO,OAAO,MAAM,CAAC,CAAC,SAAS,MAAM,CAAC,CAA4B;GACvF,QAAQ;IACN,QAAQ,IAAI;GACd;EACF,CAAC;EACD,IAAI,GAAG,eAAe,QAAQ,IAAI,CAAC;CACrC,CAAC;AACH;AAEA,SAAS,aAAa,KAAgC;CAEpD,OADa,IAAI,IAAI,IAAI,OAAO,KAAK,UAAU,CAAC,CAAC,SACrC,MAAM,GAAG,CAAC,CAAC,OAAO,OAAO;AACvC;AAEA,MAAM,cAA0B;CAAC;CAAc;CAAgB;CAAc;AAAe;AAW5F,SAAS,eAAe,KAAc,MAA4B;CAChE,IAAI,KAAK,SAAS,kBAAkB,KAAK,SAAS,iBAAiB,OAAO;CAC1E,IAAI,CAAC,KAAK,QAAQ,OAAO,OAAO;CAChC,MAAM,cAAc,IAAI,IAAI,uBAAuB;CACnD,IAAI,CAAC,aAAa,OAAO;CACzB,YAAiB,UAAU,KAAK,EAAE,CAAC,CAAC,YAAY,CAAC,CAAC;CAClD,OAAO;AACT;AAEA,eAAsB,mBACpB,KACA,UACA,UACA,sBACe;CACf,MAAM,YAAY,IAAI;CACtB,IAAI,aACF,UAAU,SAAS;EACjB,MAAM;EACN,MAAM;EACN,SAAS,OAAO,KAAK,QAAQ;GAC3B,MAAM,OAAO,aAAa,GAAG;GAC7B,MAAM,aAAa,KAAK;GAExB,IAAI,IAAI,WAAW,SAAS,KAAK,OAAO,YAAY,YAAY;IAC9D,MAAM,OAAO,SAAS,IAAI,UAAU;IACpC,IAAI,CAAC,MAAM;KACT,SAAS,KAAK,KAAK;MAAE,IAAI;MAAO,OAAO;KAAiB,CAAC;KACzD;IACF;IACA,MAAM,OAAO,IAAI,IAAI,IAAI,OAAO,KAAK,UAAU,CAAC,CAAC,aAAa,IAAI,MAAM,MAAM,KAAK,SAAS,eAAe,QAAQ,IAAI,QAAQ,MAAM;IACrI,IAAI;KACF,IAAI,KAAK,SAAS,cAAc;MAO9B,SAAS,KAAK,KAAK;OAAE,IAAI;OAAM;OAAM,SANrB,YAAY,MAAM,EAAE,eAAe,KAAK,CAAC,CAAC,CAAC,KAAK,OAAO;QACrE,MAAM,EAAE;QACR,MAAM,GAAG,KAAK,QAAQ,OAAO,EAAE,EAAE,GAAG,EAAE;QACtC,aAAa,EAAE,YAAY;QAC3B,QAAQ,EAAE,OAAO;OACnB,EAC2C;MAAE,CAAC;MAC9C;KACF;KACA,MAAM,SAAS,cAAc,KAAK,KAAK,IAAI;KAC3C,IAAI,CAAC,QAAQ;MACX,SAAS,KAAK,KAAK;OAAE,IAAI;OAAO,OAAO;MAAY,CAAC;MACpD;KACF;KACA,MAAM,QAAQ,MAAM,OAAO,OAAO,UAAU;KAC5C,IAAI,CAAC,OAAO;MACV,SAAS,KAAK,KAAK;OAAE,IAAI;OAAO,OAAO;MAAY,CAAC;MACpD;KACF;KAEA,SAAS,KAAK,KAAK;MAAE,IAAI;MAAM;MAAM,SAAA,MADf,MAAM,GAAG,QAAQ,IAAI;KACE,CAAC;IAChD,SAAS,KAAK;KACZ,SAAS,KAAK,KAAK;MAAE,IAAI;MAAO,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;KAAE,CAAC;IAC3F;IACA;GACF;GAEA,IAAI,IAAI,WAAW,UAAU,KAAK,OAAO,WAAW,YAAY;IAC9D,MAAM,OAAO,SAAS,IAAI,UAAU;IACpC,IAAI,CAAC,MAAM;KACT,SAAS,KAAK,KAAK;MAAE,IAAI;MAAO,OAAO;KAAiB,CAAC;KACzD;IACF;IACA,MAAM,OAAO,MAAM,SAAS,GAAG;IAC/B,MAAM,OAAO,QAAQ,OAAO,KAAK,SAAS,WAAW,KAAK,OAAO,KAAA;IACjE,IAAI,CAAC,MAAM;KACT,SAAS,KAAK,KAAK;MAAE,IAAI;MAAO,OAAO;KAAU,CAAC;KAClD;IACF;IACA,IAAI;KACF,IAAI,KAAK,SAAS,cAAc;MAC9B,UAAU,MAAM,EAAE,WAAW,KAAK,CAAC;MACnC,SAAS,KAAK,KAAK;OAAE,IAAI;OAAM;MAAK,CAAC;MACrC;KACF;KACA,MAAM,SAAS,cAAc,KAAK,KAAK,IAAI;KAC3C,IAAI,CAAC,QAAQ;MACX,SAAS,KAAK,KAAK;OAAE,IAAI;OAAO,OAAO;MAAY,CAAC;MACpD;KACF;KACA,MAAM,QAAQ,MAAM,OAAO,OAAO,UAAU;KAC5C,IAAI,CAAC,OAAO;MACV,SAAS,KAAK,KAAK;OAAE,IAAI;OAAO,OAAO;MAAY,CAAC;MACpD;KACF;KACA,MAAM,MAAM,UAAU,IAAI;KAC1B,SAAS,KAAK,KAAK;MAAE,IAAI;MAAM;KAAK,CAAC;IACvC,SAAS,KAAK;KACZ,SAAS,KAAK,KAAK;MAAE,IAAI;MAAO,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;KAAE,CAAC;IAC3F;IACA;GACF;GAEA,IAAI,IAAI,WAAW,SAAS,eAAe,KAAA,GAAW;IACpD,MAAM,cAAc,IAAI,IAAI,uBAAuB;IACnD,MAAM,QAAQ,SAAS,KAAK;IAS5B,SAAS,KAAK,KAAK;KAAE,IAAI;KAAM,OAAO,MARd,QAAQ,IAC9B,MAAM,IAAI,OAAO,OAAO;MACtB,GAAG;MACH,WAAW,MAAM,SAAS,UAAU,EAAE,EAAE;MACxC,QAAQ,SAAS,OAAO,EAAE,EAAE;MAC5B,WAAW,aAAa,OAAO,EAAE,EAAE;KACrC,EAAE,CACJ;IACgD,CAAC;IACjD;GACF;GAEA,IAAI,IAAI,WAAW,UAAU,eAAe,KAAA,GAAW;IACrD,MAAM,OAAO,MAAM,SAAS,GAAG;IAC/B,IAAI,CAAC,MAAM;KACT,SAAS,KAAK,KAAK;MAAE,IAAI;MAAO,OAAO;KAAc,CAAC;KACtD;IACF;IACA,MAAM,OAAO,OAAO,KAAK,QAAQ,EAAE;IACnC,IAAI,CAAC,YAAY,SAAS,IAAI,GAAG;KAC/B,SAAS,KAAK,KAAK;MAAE,IAAI;MAAO,OAAO;KAA4D,CAAC;KACpG;IACF;IACA,IAAI;KACF,MAAM,OAAoB,MAAM,SAAS,OAAO;MAC9C,MAAM,OAAO,KAAK,QAAQ,EAAE;MAC5B;MACA,KAAK,KAAK;MACV,QAAQ,KAAK;MACb,SAAS,KAAK;KAChB,CAAC;KACD,MAAM,YAAY,eAAe,KAAK,IAAI;KAC1C,SAAS,KAAK,KAAK;MAAE,IAAI;MAAM,MAAM;OAAE,GAAG;OAAM,WAAW,MAAM,SAAS,UAAU,KAAK,EAAE;MAAE;MAAG,cAAc;KAAU,CAAC;IAC3H,SAAS,KAAK;KACZ,SAAS,KAAK,KAAK;MAAE,IAAI;MAAO,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;KAAE,CAAC;IAC3F;IACA;GACF;GAEA,IAAI,eAAe,KAAA,GAAW;IAC5B,SAAS,KAAK,KAAK;KAAE,IAAI;KAAO,OAAO;IAAqB,CAAC;IAC7D;GACF;GAEA,IAAI,IAAI,WAAW,SAAS,KAAK,OAAO,KAAA,GAAW;IACjD,MAAM,OAAO,SAAS,IAAI,UAAU;IACpC,IAAI,CAAC,MAAM;KACT,SAAS,KAAK,KAAK;MAAE,IAAI;MAAO,OAAO;KAAiB,CAAC;KACzD;IACF;IACA,SAAS,KAAK,KAAK;KAAE,IAAI;KAAM,MAAM;MAAE,GAAG;MAAM,WAAW,MAAM,SAAS,UAAU,KAAK,EAAE;MAAG,QAAQ,SAAS,OAAO,UAAU;KAAE;IAAE,CAAC;IACrI;GACF;GAEA,IAAI,IAAI,WAAW,WAAW,KAAK,OAAO,KAAA,GAAW;IACnD,MAAM,OAAO,MAAM,SAAS,GAAG;IAC/B,IAAI,CAAC,MAAM;KACT,SAAS,KAAK,KAAK;MAAE,IAAI;MAAO,OAAO;KAAc,CAAC;KACtD;IACF;IACA,IAAI;KACF,MAAM,OAAO,MAAM,SAAS,OAAO,YAAY;MAC7C,MAAM,KAAK;MACX,KAAK,KAAK;MACV,QAAQ,KAAK;MACb,SAAS,KAAK;KAChB,CAAC;KACD,MAAM,YAAY,eAAe,KAAK,IAAI;KAC1C,SAAS,KAAK,KAAK;MAAE,IAAI;MAAM,MAAM;OAAE,GAAG;OAAM,WAAW,MAAM,SAAS,UAAU,KAAK,EAAE;MAAE;MAAG,cAAc;KAAU,CAAC;IAC3H,SAAS,KAAK;KACZ,SAAS,KAAK,KAAK;MAAE,IAAI;MAAO,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;KAAE,CAAC;IAC3F;IACA;GACF;GAEA,IAAI,IAAI,WAAW,YAAY,KAAK,OAAO,KAAA,GAAW;IACpD,MAAM,OAAO,SAAS,IAAI,UAAU;IACpC,IAAI,CAAC,MAAM;KACT,SAAS,KAAK,KAAK;MAAE,IAAI;MAAO,OAAO;KAAiB,CAAC;KACzD;IACF;IAEA,MAAM,WAAW,SAAS,KAAK,CAAC,CAAC,QAAQ,YAAY,QAAQ,WAAW,UAAU;IAClF,IAAI;IACJ,IAAI;KACF,oBAAoB,uBAAuB;IAC7C,QAAQ,CAAoE;IAC5E,IAAI,SAAS;IACb,IAAI,mBAAmB,MACrB,IAAI;KACF,MAAM,aAAa,kBAAkB,KAAK;KAC1C,MAAM,SAAS,IAAI,IAAI,WAAW,KAAK,cAAc,CAAC,UAAU,MAAM,UAAU,KAAK,CAAC,CAAC;KACvF,SAAS,SAAS,QAAQ,YAAY,OAAO,IAAI,QAAQ,UAAU,CAAC;KACpE,IAAI,OAAO,SAAS,GAAG;MACrB,MAAM,SAAS,OAAO,KAAK,YAAY,OAAO,IAAI,QAAQ,UAAU,KAAK,QAAQ,UAAU;MAC3F,SAAS,KAAK,KAAK;OACjB,IAAI;OACJ,MAAM;OACN,OAAO,aAAa,OAAO,KAAK,GAAG,EAAE;OACrC,YAAY;MACd,CAAC;MACD;KACF;IACF,QAAQ;KACN,SAAS;IACX;IAEF,IAAI,OAAO,SAAS,GAAG;KACrB,SAAS,KAAK,KAAK;MACjB,IAAI;MACJ,MAAM;MACN,OAAO,SAAS,OAAO,OAAO;KAChC,CAAC;KACD;IACF;IAEA,IAAI;KACF,KAAK,KAAK,SAAS,kBAAkB,KAAK,SAAS,oBAAoB,KAAK,QAAQ,aAAa;MAC/F,MAAM,SAAS,MAAM,kBAAkB,KAAK,IAAI;MAChD,IAAI,CAAC,QAAQ,MAAM,IAAI,MAAM,wBAAwB;MACrD,MAAM,SAAS,MAAM,OAAO,IAAI;OAAC;OAAU;OAAM;OAAM,KAAK,OAAO;MAAW,CAAC;MAC/E,IAAI,OAAO,SAAS,GAAG,MAAM,IAAI,OAAO,OAAO,UAAU,OAAO,OAAA,CAAQ,KAAK,KAAK,cAAc;KAClG;KACA,MAAM,SAAS,OAAO,UAAU;KAChC,MAAM,YAAY,IAAI,IAAI,eAAe;KACzC,MAAM,eAAe,IAAI,IAAI,kBAAkB;KAC/C,WAAW,SAAS,UAAU;KAC9B,cAAc,SAAS,UAAU;KACjC,SAAS,KAAK,KAAK,EAAE,IAAI,KAAK,CAAC;IACjC,SAAS,KAAK;KACZ,SAAS,KAAK,KAAK;MAAE,IAAI;MAAO,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;KAAE,CAAC;IAC3F;IACA;GACF;GAEA,IAAI,IAAI,WAAW,UAAU,eAAe,gBAAgB;IAC1D,MAAM,OAAO,MAAM,SAAS,GAAG;IAC/B,IAAI,CAAC,QAAQ,CAAC,KAAK,KAAK;KACtB,SAAS,KAAK,KAAK;MAAE,IAAI;MAAO,OAAO;KAAY,CAAC;KACpD;IACF;IACA,MAAM,YAAY,IAAI,IAAI,aAAa;IACvC,IAAI,CAAC,WAAW;KACd,SAAS,KAAK,KAAK;MAAE,IAAI;MAAO,OAAO;KAAc,CAAC;KACtD;IACF;IACA,MAAM,MAAM,KAAK;IACjB,MAAM,UAAW,KAAK,WAAW,CAAC;IAClC,MAAM,IAAI,MAAM,UAAU,KACxB;KAAE,MAAM,IAAI;KAAM,MAAM,OAAO,IAAI,IAAI,KAAK;KAAI,UAAU,IAAI;KAAU,MAAM,IAAI;KAAM,MAAM,IAAI;IAAK,GACvG,OACF;IACA,SAAS,KAAK,KAAK;KAAE,IAAI;KAAM,QAAQ;MAAE,IAAI,EAAE;MAAI,WAAW,EAAE;MAAI,OAAO,EAAE;MAAO,GAAI,EAAE,WAAW,EAAE,UAAU,EAAE,SAAS,IAAI,CAAC;KAAG;IAAE,CAAC;IACvI;GACF;GAEA,IAAI,IAAI,WAAW,UAAU,KAAK,OAAO,QAAQ;IAC/C,MAAM,OAAO,MAAM,SAAS,GAAG;IAC/B,MAAM,YAAY,IAAI,IAAI,aAAa;IACvC,MAAM,eAAe,IAAI,IAAI,kBAAkB;IAC/C,MAAM,YAAY,SAAS,KAAK,OAAO,KAAK,WACxC;KAAE,KAAK,KAAK;KAAc,SAAS,KAAK;IAAiB,IACzD,KAAA;IAEJ,SAAS,KAAK,KAAK;KAAE,IAAI;KAAM,QAAA,MADV,SAAS,UAAU,YAAY,WAAW,WAAW,YAAY;IAChD,CAAC;IACvC;GACF;GAEA,SAAS,KAAK,KAAK;IAAE,IAAI;IAAO,OAAO;GAAY,CAAC;EACtD;CACF,CAAC,CACH;AACF;AAEA,eAAsB,wBAAwB,KAAc,YAA4C;CACtG,MAAM,YAAY,IAAI;CAEtB,IAAI,aACF,UAAU,SAAS;EACjB,MAAM;EACN,MAAM;EACN,UAAU,MAAM,QAAQ;GAEtB,SAAS,KAAK,KAAK;IAAE,IAAI;IAAM,SADf,cAAc,CAAC,CAAC,IAAI,qBACC;GAAE,CAAC;EAC1C;CACF,CAAC,CACH;CAEA,IAAI,aACF,UAAU,SAAS;EACjB,MAAM;EACN,MAAM;EACN,SAAS,OAAO,KAAK,QAAQ;GAE3B,MAAM,SADO,aAAa,GACR,CAAC,CAAC;GAEpB,IAAI,IAAI,WAAW,SAAS,WAAW,KAAA,GAAW;IAChD,SAAS,KAAK,KAAK;KAAE,IAAI;KAAM,OAAO,WAAW,KAAK;IAAE,CAAC;IACzD;GACF;GAEA,IAAI,IAAI,WAAW,UAAU,WAAW,UAAU;IAChD,MAAM,OAAO,MAAM,SAAS,GAAG;IAC/B,IAAI,CAAC,QAAQ,OAAO,KAAK,SAAS,UAAU;KAC1C,SAAS,KAAK,KAAK;MAAE,IAAI;MAAO,OAAO;KAAU,CAAC;KAClD;IACF;IACA,MAAM,OAAO,OAAO,KAAK,QAAQ,EAAE;IACnC,WAAW,OAAO,OAAO,KAAK,IAAI,GAAG,IAAI;IACzC,SAAS,KAAK,KAAK,EAAE,IAAI,KAAK,CAAC;IAC/B;GACF;GAEA,SAAS,KAAK,KAAK;IAAE,IAAI;IAAO,OAAO;GAAY,CAAC;EACtD;CACF,CAAC,CACH;AACF"}
1
+ {"version":3,"file":"routes.js","names":[],"sources":["../../src/core/routes.ts"],"sourcesContent":["import type { IncomingMessage, ServerResponse } from 'node:http'\nimport { mkdirSync, readdirSync } from 'node:fs'\nimport type { Context } from '@deepseek-ai/cordis'\nimport type {} from '@deepseek-ai/dsh-host-webserver'\nimport { resolveHostRunner } from '../exec-docker/host-runner.js'\nimport { NodeRegistry, type NodeProfile, type NodeType } from './node-registry.js'\nimport { readSshConfig, resolveSshConfigEntry } from './ssh-config.js'\nimport type { KnownHostsStore } from './known-hosts.js'\nimport { testNode, type SshHandshakeTester, type DockerNodeTester } from './test.js'\nimport type { WorkspaceBindingsStore } from './workspace-bindings.js'\nimport { SyncNodeInUseError, type SyncNodeService } from './sync/node-sync.js'\n\ninterface WorldsLike {\n ensure(nodeId: string): Promise<{\n ensureDir(path: string): Promise<void>\n fs: { listDir(path: string): Promise<Array<{ name: string; path: string; isDirectory: boolean; isFile: boolean }>> }\n } | undefined>\n}\n\nfunction resolveWorlds(ctx: Context, nodeType: string): WorldsLike | undefined {\n if (nodeType === 'local-docker' || nodeType === 'remote-docker') {\n return ctx.get('dshbDockerWorldsGeneric') as WorldsLike | undefined\n }\n return ctx.get('dshbWorlds') as WorldsLike | undefined\n}\n\nconst MAX_BODY = 64 * 1024\n\nfunction sendJson(res: ServerResponse, status: number, body: unknown, headers: Record<string, string> = {}): void {\n res.writeHead(status, { 'content-type': 'application/json', ...headers })\n res.end(JSON.stringify(body))\n}\n\nfunction readBody(req: IncomingMessage): Promise<Record<string, unknown> | null> {\n return new Promise((resolve) => {\n if ((req.headers['content-type'] ?? '') !== 'application/json') {\n resolve(null)\n return\n }\n const chunks: Buffer[] = []\n let size = 0\n req.on('data', (chunk: Buffer) => {\n size += chunk.length\n if (size > MAX_BODY) {\n resolve(null)\n req.destroy()\n return\n }\n chunks.push(chunk)\n })\n req.on('end', () => {\n try {\n resolve(JSON.parse(Buffer.concat(chunks).toString('utf8')) as Record<string, unknown>)\n } catch {\n resolve(null)\n }\n })\n req.on('error', () => resolve(null))\n })\n}\n\nfunction pathSegments(req: IncomingMessage): string[] {\n const path = new URL(req.url ?? '/', 'http://x').pathname\n return path.split('/').filter(Boolean)\n}\n\nconst VALID_TYPES: NodeType[] = ['local-host', 'local-docker', 'remote-ssh', 'remote-docker']\n\ninterface DockerProvisionerLike {\n provision(nodeId: string): Promise<unknown>\n status(nodeId: string): { state: string; error?: string; containerId?: string; updatedAt: string } | undefined\n}\n\ninterface WorkspaceRegistryLike {\n list(): Array<{ path: string; title: string }>\n}\n\nfunction maybeProvision(ctx: Context, node: NodeProfile): boolean {\n if (node.type !== 'local-docker' && node.type !== 'remote-docker') return false\n if (!node.docker?.image) return false\n const provisioner = ctx.get('dshbDockerProvisioner') as DockerProvisionerLike | undefined\n if (!provisioner) return false\n void provisioner.provision(node.id).catch(() => {})\n return true\n}\n\nexport async function registerNodeRoutes(\n ctx: Context,\n registry: NodeRegistry,\n bindings: WorkspaceBindingsStore,\n getWorkspaceRegistry?: () => WorkspaceRegistryLike | undefined,\n syncNodes?: SyncNodeService,\n): Promise<void> {\n const webServer = ctx.webServer\n ctx.effect(() =>\n webServer.register({\n kind: 'prefix',\n path: '/api/dshb/nodes',\n handler: async (req, res) => {\n const segs = pathSegments(req)\n const resourceId = segs[3]\n\n if (req.method === 'GET' && segs[4] === 'browse' && resourceId) {\n const node = registry.get(resourceId)\n if (!node) {\n sendJson(res, 404, { ok: false, error: 'node not found' })\n return\n }\n if (syncNodes?.isPendingRemoval(resourceId)) {\n sendJson(res, 409, { ok: false, code: 'node/pending-removal', error: '节点等待工作区解绑后清理' })\n return\n }\n const path = new URL(req.url ?? '/', 'http://x').searchParams.get('path') ?? (node.type === 'local-host' ? process.env.HOME ?? '/' : '/')\n try {\n if (node.type === 'local-host') {\n const entries = readdirSync(path, { withFileTypes: true }).map((e) => ({\n name: e.name,\n path: `${path.replace(/\\/$/, '')}/${e.name}`,\n isDirectory: e.isDirectory(),\n isFile: e.isFile(),\n }))\n sendJson(res, 200, { ok: true, path, entries })\n return\n }\n const worlds = resolveWorlds(ctx, node.type)\n if (!worlds) {\n sendJson(res, 503, { ok: false, error: '远程执行世界不可用' })\n return\n }\n const world = await worlds.ensure(resourceId)\n if (!world) {\n sendJson(res, 503, { ok: false, error: '节点执行世界未就绪' })\n return\n }\n const entries = await world.fs.listDir(path)\n sendJson(res, 200, { ok: true, path, entries })\n } catch (err) {\n sendJson(res, 502, { ok: false, error: err instanceof Error ? err.message : String(err) })\n }\n return\n }\n\n if (req.method === 'POST' && segs[4] === 'mkdir' && resourceId) {\n const node = registry.get(resourceId)\n if (!node) {\n sendJson(res, 404, { ok: false, error: 'node not found' })\n return\n }\n if (syncNodes?.isPendingRemoval(resourceId)) {\n sendJson(res, 409, { ok: false, code: 'node/pending-removal', error: '节点等待工作区解绑后清理' })\n return\n }\n const body = await readBody(req)\n const path = body && typeof body.path === 'string' ? body.path : undefined\n if (!path) {\n sendJson(res, 400, { ok: false, error: '需要 path' })\n return\n }\n try {\n if (node.type === 'local-host') {\n mkdirSync(path, { recursive: true })\n sendJson(res, 200, { ok: true, path })\n return\n }\n const worlds = resolveWorlds(ctx, node.type)\n if (!worlds) {\n sendJson(res, 503, { ok: false, error: '远程执行世界不可用' })\n return\n }\n const world = await worlds.ensure(resourceId)\n if (!world) {\n sendJson(res, 503, { ok: false, error: '节点执行世界未就绪' })\n return\n }\n await world.ensureDir(path)\n sendJson(res, 200, { ok: true, path })\n } catch (err) {\n sendJson(res, 502, { ok: false, error: err instanceof Error ? err.message : String(err) })\n }\n return\n }\n\n if (req.method === 'GET' && resourceId === undefined) {\n const provisioner = ctx.get('dshbDockerProvisioner') as DockerProvisionerLike | undefined\n try {\n await syncNodes?.reconcilePendingRemovals()\n } catch {\n sendJson(res, 503, { ok: false, error: '同步删除状态暂不可用' })\n return\n }\n const nodes = registry.list()\n const syncViews = syncNodes?.views(nodes.map((node) => node.id))\n const withFlags = await Promise.all(\n nodes.map(async (n) => {\n const sync = syncViews?.get(n.id)\n return {\n ...n,\n hasSecret: await registry.hasSecret(n.id),\n status: registry.status(n.id),\n provision: provisioner?.status(n.id),\n ...(sync ? { sync } : {}),\n }\n }),\n )\n sendJson(res, 200, { ok: true, nodes: withFlags })\n return\n }\n\n if (req.method === 'POST' && resourceId === undefined) {\n const body = await readBody(req)\n if (!body) {\n sendJson(res, 400, { ok: false, error: '请求体必须是 JSON' })\n return\n }\n const type = String(body.type ?? '') as NodeType\n if (!VALID_TYPES.includes(type)) {\n sendJson(res, 400, { ok: false, error: 'type 必须是 local-host/local-docker/remote-ssh/remote-docker' })\n return\n }\n try {\n const node: NodeProfile = await registry.create({\n name: String(body.name ?? ''),\n type,\n ssh: body.ssh as never,\n docker: body.docker as never,\n secrets: body.secrets as never,\n })\n const provision = maybeProvision(ctx, node)\n sendJson(res, 201, { ok: true, node: { ...node, hasSecret: await registry.hasSecret(node.id) }, provisioning: provision })\n } catch (err) {\n sendJson(res, 409, { ok: false, error: err instanceof Error ? err.message : String(err) })\n }\n return\n }\n\n if (resourceId === undefined) {\n sendJson(res, 405, { ok: false, error: 'method not allowed' })\n return\n }\n\n if (req.method === 'GET' && segs[4] === undefined) {\n const node = registry.get(resourceId)\n if (!node) {\n sendJson(res, 404, { ok: false, error: 'node not found' })\n return\n }\n sendJson(res, 200, { ok: true, node: { ...node, hasSecret: await registry.hasSecret(node.id), status: registry.status(resourceId) } })\n return\n }\n\n if (req.method === 'PATCH' && segs[4] === undefined) {\n const body = await readBody(req)\n if (!body) {\n sendJson(res, 400, { ok: false, error: '请求体必须是 JSON' })\n return\n }\n try {\n const node = await registry.update(resourceId, {\n name: body.name as string | undefined,\n ssh: body.ssh as never,\n docker: body.docker as never,\n secrets: body.secrets as never,\n })\n const provision = maybeProvision(ctx, node)\n sendJson(res, 200, { ok: true, node: { ...node, hasSecret: await registry.hasSecret(node.id) }, provisioning: provision })\n } catch (err) {\n sendJson(res, 404, { ok: false, error: err instanceof Error ? err.message : String(err) })\n }\n return\n }\n\n if (req.method === 'DELETE' && segs[4] === undefined) {\n const node = registry.get(resourceId)\n if (!node) {\n sendJson(res, 404, { ok: false, error: 'node not found' })\n return\n }\n\n const attached = bindings.list().filter((binding) => binding.nodeId === resourceId)\n let workspaceRegistry: WorkspaceRegistryLike | undefined\n try {\n workspaceRegistry = getWorkspaceRegistry?.()\n } catch { /* protect deletion when the workspace registry is unavailable */ }\n let active = attached\n if (workspaceRegistry?.list) {\n try {\n const workspaces = workspaceRegistry.list()\n const byPath = new Map(workspaces.map((workspace) => [workspace.path, workspace.title]))\n active = attached.filter((binding) => byPath.has(binding.mirrorPath))\n if (active.length > 0) {\n const titles = active.map((binding) => byPath.get(binding.mirrorPath) ?? binding.mirrorPath)\n sendJson(res, 409, {\n ok: false,\n code: 'node/in-use',\n error: `节点仍被工作区使用:${titles.join('、')}。请先移除或迁移这些工作区。`,\n workspaces: titles,\n })\n return\n }\n } catch {\n active = attached\n }\n }\n if (active.length > 0) {\n sendJson(res, 409, {\n ok: false,\n code: 'node/in-use',\n error: `节点仍绑定 ${active.length} 个工作区,请先移除或迁移工作区。`,\n })\n return\n }\n\n try {\n const removeNode = async (): Promise<void> => {\n if ((node.type === 'local-docker' || node.type === 'remote-docker') && node.docker?.containerId) {\n const runner = await resolveHostRunner(ctx, node)\n if (!runner) throw new Error('Docker 节点执行环境不可用,节点未删除')\n const result = await runner.run(['docker', 'rm', '-f', node.docker.containerId])\n if (result.code !== 0) throw new Error((result.stderr || result.stdout).trim() || '删除容器失败,节点未删除')\n }\n await registry.remove(resourceId)\n const sshWorlds = ctx.get('dshbSshWorlds') as { remove?: (id: string) => void } | undefined\n const dockerWorlds = ctx.get('dshbDockerWorlds') as { remove?: (id: string) => void } | undefined\n sshWorlds?.remove?.(resourceId)\n dockerWorlds?.remove?.(resourceId)\n }\n if (syncNodes) await syncNodes.removeLocalNode(resourceId, removeNode)\n else await removeNode()\n sendJson(res, 200, { ok: true })\n } catch (err) {\n if (err instanceof SyncNodeInUseError) {\n sendJson(res, 409, {\n ok: false,\n code: 'node/in-use',\n error: '节点仍被工作区使用,请先移除或迁移这些工作区。',\n workspaces: err.workspaces,\n })\n return\n }\n sendJson(res, 502, { ok: false, error: err instanceof Error ? err.message : String(err) })\n }\n return\n }\n\n if (req.method === 'POST' && resourceId === 'test-unsaved') {\n const body = await readBody(req)\n if (!body || !body.ssh) {\n sendJson(res, 400, { ok: false, error: '需要 ssh 配置' })\n return\n }\n const sshTester = ctx.get('dshbSshPool') as SshHandshakeTester | undefined\n if (!sshTester) {\n sendJson(res, 503, { ok: false, error: 'SSH 执行通道不可用' })\n return\n }\n const ssh = body.ssh as { host: string; port: number; username: string; auth: never; jump?: never }\n const secrets = (body.secrets ?? {}) as { password?: string; privateKey?: string; passphrase?: string }\n const t = await sshTester.test(\n { host: ssh.host, port: Number(ssh.port) || 22, username: ssh.username, auth: ssh.auth, jump: ssh.jump },\n secrets,\n )\n sendJson(res, 200, { ok: true, report: { ok: t.ok, reachable: t.ok, error: t.error, ...(t.category ? { category: t.category } : {}) } })\n return\n }\n\n if (req.method === 'POST' && segs[4] === 'test') {\n const body = await readBody(req)\n const sshTester = ctx.get('dshbSshPool') as SshHandshakeTester | undefined\n const dockerTester = ctx.get('dshbDockerTester') as DockerNodeTester | undefined\n const overrides = body && (body.ssh || body.secrets)\n ? { ssh: body.ssh as never, secrets: body.secrets as never }\n : undefined\n const report = await testNode(registry, resourceId, sshTester, overrides, dockerTester)\n sendJson(res, 200, { ok: true, report })\n return\n }\n\n sendJson(res, 404, { ok: false, error: 'not found' })\n },\n }),\n )\n}\n\nexport async function registerSshConfigRoutes(ctx: Context, knownHosts: KnownHostsStore): Promise<void> {\n const webServer = ctx.webServer\n\n ctx.effect(() =>\n webServer.register({\n kind: 'exact',\n path: '/api/dshb/ssh-config',\n handler: (_req, res) => {\n const entries = readSshConfig().map(resolveSshConfigEntry)\n sendJson(res, 200, { ok: true, entries })\n },\n }),\n )\n\n ctx.effect(() =>\n webServer.register({\n kind: 'prefix',\n path: '/api/dshb/known-hosts',\n handler: async (req, res) => {\n const segs = pathSegments(req)\n const action = segs[3]\n\n if (req.method === 'GET' && action === undefined) {\n sendJson(res, 200, { ok: true, hosts: knownHosts.list() })\n return\n }\n\n if (req.method === 'POST' && action === 'forget') {\n const body = await readBody(req)\n if (!body || typeof body.host !== 'string') {\n sendJson(res, 400, { ok: false, error: '需要 host' })\n return\n }\n const port = Number(body.port ?? 22)\n knownHosts.forget(String(body.host), port)\n sendJson(res, 200, { ok: true })\n return\n }\n\n sendJson(res, 404, { ok: false, error: 'not found' })\n },\n }),\n )\n}\n"],"mappings":"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"}
@@ -0,0 +1,48 @@
1
+ import { a as SyncPeer } from "../../model-BP7Gw1yk.js";
2
+ import { n as SyncStateStore } from "../../stores-CKyEt0yk.js";
3
+ import { n as SyncNodeEntry, s as SyncNodeService } from "../../node-sync-3Z9QLz2n.js";
4
+ import { SyncPeerManager } from "./peer-manager.js";
5
+ import { c as SyncSessionService } from "../../session-sync-g6qKZzjV.js";
6
+ import { a as SyncSshTransport } from "../../ssh-transport-BcIiLxsX.js";
7
+ //#region src/core/sync/coordinator.d.ts
8
+ interface NodeTransferPlan {
9
+ toLocal: SyncNodeEntry[];
10
+ toRemote: SyncNodeEntry[];
11
+ skipped: number;
12
+ }
13
+ interface SyncRunSummary {
14
+ peerId: string;
15
+ direction: SyncPeer['direction'];
16
+ startedAt: string;
17
+ finishedAt: string;
18
+ remoteInstanceId: string;
19
+ imported: number;
20
+ exported: number;
21
+ skipped: number;
22
+ pendingRemoval: number;
23
+ importedSessions: number;
24
+ exportedSessions: number;
25
+ skippedSessions: number;
26
+ }
27
+ declare class SyncCoordinatorError extends Error {
28
+ readonly code: string;
29
+ constructor(code: string, message: string);
30
+ }
31
+ declare function planNodeTransfers(direction: SyncPeer['direction'], localEntries: readonly SyncNodeEntry[], remoteEntries: readonly SyncNodeEntry[]): NodeTransferPlan;
32
+ declare class SyncCoordinator {
33
+ private readonly peers;
34
+ private readonly state;
35
+ private readonly nodes;
36
+ private readonly transport;
37
+ private readonly now;
38
+ private readonly sessions?;
39
+ private readonly inFlight;
40
+ constructor(peers: SyncPeerManager, state: SyncStateStore, nodes: SyncNodeService, transport: SyncSshTransport, now?: () => number, sessions?: SyncSessionService | undefined);
41
+ run(peerId: string): Promise<SyncRunSummary>;
42
+ private execute;
43
+ private clear;
44
+ private retryTransport;
45
+ }
46
+ //#endregion
47
+ export { NodeTransferPlan, SyncCoordinator, SyncCoordinatorError, SyncRunSummary, planNodeTransfers };
48
+ //# sourceMappingURL=coordinator.d.ts.map
@@ -0,0 +1,247 @@
1
+ import { compareSyncRevision } from "./revision.js";
2
+ import { syncRecordKey } from "./model.js";
3
+ import { SyncNodeManifestError } from "./node-sync.js";
4
+ import { SyncPeerNotFoundError } from "./peer-manager.js";
5
+ import { a as validateSyncSessionBundle, i as planSessionTransfers, r as makeSyncSessionBundle, t as SyncSessionError } from "../../session-sync-EdfwUON5.js";
6
+ import { SyncHostKeyChallengeError } from "./host-key.js";
7
+ import { SyncSshProbeError } from "./ssh-transport.js";
8
+ import { createHash } from "node:crypto";
9
+ //#region src/core/sync/coordinator.ts
10
+ const EMPTY_APPLY_RESULT = {
11
+ created: 0,
12
+ updated: 0,
13
+ deleted: 0,
14
+ pendingRemoval: 0,
15
+ skipped: 0
16
+ };
17
+ const TRANSPORT_RETRY_DELAY_MS = 250;
18
+ var SyncCoordinatorError = class extends Error {
19
+ code;
20
+ constructor(code, message) {
21
+ super(message);
22
+ this.code = code;
23
+ this.name = "SyncCoordinatorError";
24
+ }
25
+ };
26
+ function addCount(result) {
27
+ return result.created + result.updated + result.deleted;
28
+ }
29
+ function manifestDigest(entries) {
30
+ const versions = entries.map((entry) => ({
31
+ key: syncRecordKey("node", entry.metadata),
32
+ revision: entry.metadata.revision,
33
+ contentDigest: entry.metadata.contentDigest,
34
+ deleted: Boolean(entry.metadata.deleted)
35
+ })).sort((a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : 0);
36
+ return `sha256:${createHash("sha256").update(JSON.stringify(versions)).digest("hex")}`;
37
+ }
38
+ function planNodeTransfers(direction, localEntries, remoteEntries) {
39
+ const toLocal = [];
40
+ const toRemote = [];
41
+ if (direction === "pull") return {
42
+ toLocal: [...remoteEntries],
43
+ toRemote,
44
+ skipped: 0
45
+ };
46
+ const localByKey = new Map(localEntries.map((entry) => [syncRecordKey("node", entry.metadata), entry]));
47
+ const remoteByKey = new Map(remoteEntries.map((entry) => [syncRecordKey("node", entry.metadata), entry]));
48
+ const keys = /* @__PURE__ */ new Set([...localByKey.keys(), ...remoteByKey.keys()]);
49
+ let skipped = 0;
50
+ for (const key of keys) {
51
+ const local = localByKey.get(key);
52
+ const remote = remoteByKey.get(key);
53
+ if (!local) {
54
+ toLocal.push(remote);
55
+ continue;
56
+ }
57
+ if (!remote) {
58
+ toRemote.push(local);
59
+ continue;
60
+ }
61
+ const comparison = compareSyncRevision(local.metadata.revision, remote.metadata.revision);
62
+ if (comparison > 0) toRemote.push(local);
63
+ else if (comparison < 0) toLocal.push(remote);
64
+ else if (local.metadata.contentDigest === remote.metadata.contentDigest && Boolean(local.metadata.deleted) === Boolean(remote.metadata.deleted)) skipped += 1;
65
+ else throw new SyncCoordinatorError("revision-conflict", "Equal node revisions contain different content");
66
+ }
67
+ return {
68
+ toLocal,
69
+ toRemote,
70
+ skipped
71
+ };
72
+ }
73
+ function mapFailure(error) {
74
+ if (error instanceof SyncCoordinatorError) return error;
75
+ if (error instanceof SyncPeerNotFoundError) return new SyncCoordinatorError("peer-not-found", "同步节点已不存在");
76
+ if (error instanceof SyncHostKeyChallengeError) return new SyncCoordinatorError(error.decision === "changed" ? "host-key-changed" : "host-key-untrusted", error.message);
77
+ if (error instanceof SyncSshProbeError) {
78
+ const baseMsg = {
79
+ auth: "SSH 认证失败",
80
+ network: "SSH 节点不可达",
81
+ timeout: "SSH 连接超时",
82
+ hostkey: "SSH 主机密钥校验失败",
83
+ helper: "远端同步 helper 不可用",
84
+ protocol: "同步协议不兼容"
85
+ }[error.category] ?? "SSH 同步失败";
86
+ const detail = error.detail;
87
+ return new SyncCoordinatorError(`ssh-${error.category}`, detail ? `${baseMsg}:${detail}` : baseMsg);
88
+ }
89
+ if (error instanceof SyncNodeManifestError) return new SyncCoordinatorError("manifest-invalid", "节点 manifest 校验失败");
90
+ if (error instanceof SyncSessionError) return new SyncCoordinatorError(error.code.startsWith("session-") ? error.code : `session-${error.code}`, error.message);
91
+ return new SyncCoordinatorError("sync-failed", "同步作业失败");
92
+ }
93
+ function resultCount(result) {
94
+ return {
95
+ applied: addCount(result),
96
+ skipped: result.skipped,
97
+ pendingRemoval: result.pendingRemoval
98
+ };
99
+ }
100
+ function emptySessionResult() {
101
+ return {
102
+ created: 0,
103
+ updated: 0,
104
+ archived: 0,
105
+ deleted: 0,
106
+ skipped: 0,
107
+ unmapped: 0
108
+ };
109
+ }
110
+ function sessionResultCount(result) {
111
+ return result.created + result.updated + result.deleted;
112
+ }
113
+ var SyncCoordinator = class {
114
+ peers;
115
+ state;
116
+ nodes;
117
+ transport;
118
+ now;
119
+ sessions;
120
+ inFlight = /* @__PURE__ */ new Map();
121
+ constructor(peers, state, nodes, transport, now = Date.now, sessions) {
122
+ this.peers = peers;
123
+ this.state = state;
124
+ this.nodes = nodes;
125
+ this.transport = transport;
126
+ this.now = now;
127
+ this.sessions = sessions;
128
+ }
129
+ run(peerId) {
130
+ const active = this.inFlight.get(peerId);
131
+ if (active) return active;
132
+ const run = this.execute(peerId);
133
+ this.inFlight.set(peerId, run);
134
+ run.then(() => this.clear(peerId, run), () => this.clear(peerId, run));
135
+ return run;
136
+ }
137
+ async execute(peerId) {
138
+ const peer = await this.peers.get(peerId);
139
+ if (!peer) throw new SyncCoordinatorError("peer-not-found", "同步节点已不存在");
140
+ if (!peer.enabled) throw new SyncCoordinatorError("peer-disabled", "此同步节点已停用");
141
+ const startedAt = new Date(this.now()).toISOString();
142
+ this.peers.updateProgress(peer.id, {
143
+ lastRunAt: startedAt,
144
+ lastRunStatus: "running",
145
+ lastErrorCode: void 0,
146
+ lastErrorMessage: void 0
147
+ });
148
+ try {
149
+ if (peer.syncSessions && !this.sessions) throw new SyncCoordinatorError("sessions-unavailable", "会话同步服务不可用");
150
+ const remoteManifest = await this.retryTransport(() => this.transport.fetchNodeManifest(peer.id));
151
+ if (remoteManifest.protocolVersion !== 1 || remoteManifest.instanceId === this.state.instanceId()) throw new SyncCoordinatorError("protocol-invalid", "同步 peer 协议版本或实例身份无效");
152
+ const remoteEntries = this.nodes.validateManifest(remoteManifest.records);
153
+ const localManifest = await this.nodes.exportManifest();
154
+ const localEntries = this.nodes.validateManifest(localManifest.records);
155
+ const plan = planNodeTransfers(peer.direction, localEntries, remoteEntries);
156
+ const localResult = plan.toLocal.length > 0 ? await this.nodes.applyManifest(plan.toLocal, peer.name, "peer") : EMPTY_APPLY_RESULT;
157
+ const remoteResult = plan.toRemote.length > 0 ? await this.retryTransport(() => this.transport.applyNodeManifest(peer.id, plan.toRemote, localManifest.instanceId)) : EMPTY_APPLY_RESULT;
158
+ const localCounts = resultCount(localResult);
159
+ const remoteCounts = resultCount(remoteResult);
160
+ let importedSessions = 0;
161
+ let exportedSessions = 0;
162
+ let skippedSessions = 0;
163
+ let digest = manifestDigest(remoteEntries);
164
+ if (peer.syncSessions) {
165
+ const sessionService = this.sessions;
166
+ const remoteBundle = validateSyncSessionBundle(await this.retryTransport(() => this.transport.fetchSessionBundle(peer.id)));
167
+ if (remoteBundle.instanceId !== remoteManifest.instanceId) throw new SyncCoordinatorError("protocol-invalid", "节点与会话 manifest 的实例身份不一致");
168
+ const localBundle = await sessionService.exportBundle();
169
+ const plan = planSessionTransfers(peer.direction, localBundle.entries, remoteBundle.entries);
170
+ const localBundleToApply = makeSyncSessionBundle(remoteBundle.instanceId, plan.toLocal, remoteBundle.skipped);
171
+ const remoteBundleToApply = makeSyncSessionBundle(localBundle.instanceId, plan.toRemote, localBundle.skipped);
172
+ const sessionLocalResult = plan.toLocal.length > 0 || remoteBundle.skipped.length > 0 ? await sessionService.applyBundle(localBundleToApply, peer.name, peer.id) : emptySessionResult();
173
+ const sessionRemoteResult = plan.toRemote.length > 0 ? await this.retryTransport(() => this.transport.applySessionBundle(peer.id, remoteBundleToApply, localBundle.instanceId)) : emptySessionResult();
174
+ importedSessions = sessionResultCount(sessionLocalResult);
175
+ exportedSessions = sessionResultCount(sessionRemoteResult);
176
+ skippedSessions = plan.skipped + sessionLocalResult.skipped + sessionRemoteResult.skipped;
177
+ digest = `sha256:${createHash("sha256").update(`${digest}\0${remoteBundle.digest}\0${localBundle.digest}`).digest("hex")}`;
178
+ }
179
+ const finishedAt = new Date(this.now()).toISOString();
180
+ const summary = {
181
+ peerId,
182
+ direction: peer.direction,
183
+ startedAt,
184
+ finishedAt,
185
+ remoteInstanceId: remoteManifest.instanceId,
186
+ imported: localCounts.applied,
187
+ exported: remoteCounts.applied,
188
+ skipped: plan.skipped + localCounts.skipped + remoteCounts.skipped + skippedSessions,
189
+ pendingRemoval: localCounts.pendingRemoval + remoteCounts.pendingRemoval,
190
+ importedSessions,
191
+ exportedSessions,
192
+ skippedSessions
193
+ };
194
+ this.peers.updateProgress(peer.id, {
195
+ lastRunAt: finishedAt,
196
+ lastRunStatus: "succeeded",
197
+ lastSuccessAt: finishedAt,
198
+ lastManifestDigest: digest,
199
+ watermark: digest,
200
+ lastErrorCode: void 0,
201
+ lastErrorMessage: void 0,
202
+ lastResult: {
203
+ imported: summary.imported,
204
+ exported: summary.exported,
205
+ skipped: summary.skipped,
206
+ pendingRemoval: summary.pendingRemoval,
207
+ importedSessions,
208
+ exportedSessions,
209
+ skippedSessions
210
+ }
211
+ });
212
+ return summary;
213
+ } catch (error) {
214
+ const failure = mapFailure(error);
215
+ if (!(error instanceof SyncCoordinatorError) && !(error instanceof SyncSshProbeError) && !(error instanceof SyncSessionError) && !(error instanceof SyncNodeManifestError) && !(error instanceof SyncHostKeyChallengeError) && !(error instanceof SyncPeerNotFoundError)) failure.message = `${failure.message}:${error instanceof Error ? error.message : String(error)}`;
216
+ try {
217
+ this.peers.updateProgress(peer.id, {
218
+ lastRunAt: new Date(this.now()).toISOString(),
219
+ lastRunStatus: "failed",
220
+ lastErrorCode: failure.code,
221
+ lastErrorMessage: failure.message
222
+ });
223
+ } catch {}
224
+ throw failure;
225
+ }
226
+ }
227
+ clear(peerId, run) {
228
+ if (this.inFlight.get(peerId) === run) this.inFlight.delete(peerId);
229
+ }
230
+ async retryTransport(operation) {
231
+ try {
232
+ return await operation();
233
+ } catch (error) {
234
+ if (!(error instanceof SyncSshProbeError) || ![
235
+ "network",
236
+ "timeout",
237
+ "helper"
238
+ ].includes(error.category)) throw error;
239
+ await new Promise((resolve) => setTimeout(resolve, TRANSPORT_RETRY_DELAY_MS));
240
+ return operation();
241
+ }
242
+ }
243
+ };
244
+ //#endregion
245
+ export { SyncCoordinator, SyncCoordinatorError, planNodeTransfers };
246
+
247
+ //# sourceMappingURL=coordinator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"coordinator.js","names":[],"sources":["../../../src/core/sync/coordinator.ts"],"sourcesContent":["import { createHash } from 'node:crypto'\nimport { SyncNodeManifestError, type SyncNodeApplyResult, type SyncNodeEntry, type SyncNodeService } from './node-sync.js'\nimport { SyncHostKeyChallengeError } from './host-key.js'\nimport { compareSyncRevision } from './revision.js'\nimport { syncRecordKey, type SyncPeer } from './model.js'\nimport { SyncPeerManager, SyncPeerNotFoundError } from './peer-manager.js'\nimport type { SyncStateStore } from './stores.js'\nimport { SyncSshProbeError, SyncSshTransport } from './ssh-transport.js'\nimport { makeSyncSessionBundle, planSessionTransfers, SyncSessionError, SyncSessionService, validateSyncSessionBundle } from './session-sync.js'\nimport type { SyncSessionApplyResult } from './session-sync.js'\n\nconst EMPTY_APPLY_RESULT: SyncNodeApplyResult = { created: 0, updated: 0, deleted: 0, pendingRemoval: 0, skipped: 0 }\nconst TRANSPORT_RETRY_DELAY_MS = 250\n\nexport interface NodeTransferPlan {\n toLocal: SyncNodeEntry[]\n toRemote: SyncNodeEntry[]\n skipped: number\n}\n\nexport interface SyncRunSummary {\n peerId: string\n direction: SyncPeer['direction']\n startedAt: string\n finishedAt: string\n remoteInstanceId: string\n imported: number\n exported: number\n skipped: number\n pendingRemoval: number\n importedSessions: number\n exportedSessions: number\n skippedSessions: number\n}\n\nexport class SyncCoordinatorError extends Error {\n constructor(readonly code: string, message: string) {\n super(message)\n this.name = 'SyncCoordinatorError'\n }\n}\n\nfunction addCount(result: SyncNodeApplyResult): number {\n return result.created + result.updated + result.deleted\n}\n\nfunction manifestDigest(entries: readonly SyncNodeEntry[]): string {\n const versions = entries\n .map((entry) => ({\n key: syncRecordKey('node', entry.metadata),\n revision: entry.metadata.revision,\n contentDigest: entry.metadata.contentDigest,\n deleted: Boolean(entry.metadata.deleted),\n }))\n .sort((a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : 0)\n return `sha256:${createHash('sha256').update(JSON.stringify(versions)).digest('hex')}`\n}\n\nexport function planNodeTransfers(\n direction: SyncPeer['direction'],\n localEntries: readonly SyncNodeEntry[],\n remoteEntries: readonly SyncNodeEntry[],\n): NodeTransferPlan {\n const toLocal: SyncNodeEntry[] = []\n const toRemote: SyncNodeEntry[] = []\n if (direction === 'pull') return { toLocal: [...remoteEntries], toRemote, skipped: 0 }\n\n const localByKey = new Map(localEntries.map((entry) => [syncRecordKey('node', entry.metadata), entry]))\n const remoteByKey = new Map(remoteEntries.map((entry) => [syncRecordKey('node', entry.metadata), entry]))\n const keys = new Set([...localByKey.keys(), ...remoteByKey.keys()])\n let skipped = 0\n\n for (const key of keys) {\n const local = localByKey.get(key)\n const remote = remoteByKey.get(key)\n if (!local) {\n toLocal.push(remote!)\n continue\n }\n if (!remote) {\n toRemote.push(local)\n continue\n }\n\n const comparison = compareSyncRevision(local.metadata.revision, remote.metadata.revision)\n if (comparison > 0) {\n toRemote.push(local)\n } else if (comparison < 0) {\n toLocal.push(remote)\n } else if (\n local.metadata.contentDigest === remote.metadata.contentDigest &&\n Boolean(local.metadata.deleted) === Boolean(remote.metadata.deleted)\n ) {\n skipped += 1\n } else {\n throw new SyncCoordinatorError('revision-conflict', 'Equal node revisions contain different content')\n }\n }\n return { toLocal, toRemote, skipped }\n}\n\nfunction mapFailure(error: unknown): SyncCoordinatorError {\n if (error instanceof SyncCoordinatorError) return error\n if (error instanceof SyncPeerNotFoundError) return new SyncCoordinatorError('peer-not-found', '同步节点已不存在')\n if (error instanceof SyncHostKeyChallengeError) {\n const code = error.decision === 'changed' ? 'host-key-changed' : 'host-key-untrusted'\n return new SyncCoordinatorError(code, error.message)\n }\n if (error instanceof SyncSshProbeError) {\n const messages: Record<string, string> = {\n auth: 'SSH 认证失败',\n network: 'SSH 节点不可达',\n timeout: 'SSH 连接超时',\n hostkey: 'SSH 主机密钥校验失败',\n helper: '远端同步 helper 不可用',\n protocol: '同步协议不兼容',\n }\n const baseMsg = messages[error.category] ?? 'SSH 同步失败'\n const detail = (error as Error & { detail?: string }).detail\n return new SyncCoordinatorError(`ssh-${error.category}`, detail ? `${baseMsg}:${detail}` : baseMsg)\n }\n if (error instanceof SyncNodeManifestError) return new SyncCoordinatorError('manifest-invalid', '节点 manifest 校验失败')\n if (error instanceof SyncSessionError) {\n return new SyncCoordinatorError(error.code.startsWith('session-') ? error.code : `session-${error.code}`, error.message)\n }\n return new SyncCoordinatorError('sync-failed', '同步作业失败')\n}\n\nfunction resultCount(result: SyncNodeApplyResult): { applied: number; skipped: number; pendingRemoval: number } {\n return {\n applied: addCount(result),\n skipped: result.skipped,\n pendingRemoval: result.pendingRemoval,\n }\n}\n\nfunction emptySessionResult(): SyncSessionApplyResult {\n return { created: 0, updated: 0, archived: 0, deleted: 0, skipped: 0, unmapped: 0 }\n}\n\nfunction sessionResultCount(result: SyncSessionApplyResult): number {\n return result.created + result.updated + result.deleted\n}\n\nexport class SyncCoordinator {\n private readonly inFlight = new Map<string, Promise<SyncRunSummary>>()\n\n constructor(\n private readonly peers: SyncPeerManager,\n private readonly state: SyncStateStore,\n private readonly nodes: SyncNodeService,\n private readonly transport: SyncSshTransport,\n private readonly now: () => number = Date.now,\n private readonly sessions?: SyncSessionService,\n ) {}\n\n run(peerId: string): Promise<SyncRunSummary> {\n const active = this.inFlight.get(peerId)\n if (active) return active\n const run = this.execute(peerId)\n this.inFlight.set(peerId, run)\n void run.then(() => this.clear(peerId, run), () => this.clear(peerId, run))\n return run\n }\n\n private async execute(peerId: string): Promise<SyncRunSummary> {\n const peer = await this.peers.get(peerId)\n if (!peer) throw new SyncCoordinatorError('peer-not-found', '同步节点已不存在')\n if (!peer.enabled) throw new SyncCoordinatorError('peer-disabled', '此同步节点已停用')\n\n const startedAt = new Date(this.now()).toISOString()\n this.peers.updateProgress(peer.id, {\n lastRunAt: startedAt,\n lastRunStatus: 'running',\n lastErrorCode: undefined,\n lastErrorMessage: undefined,\n })\n\n try {\n if (peer.syncSessions && !this.sessions) throw new SyncCoordinatorError('sessions-unavailable', '会话同步服务不可用')\n const remoteManifest = await this.retryTransport(() => this.transport.fetchNodeManifest(peer.id))\n if (remoteManifest.protocolVersion !== 1 || remoteManifest.instanceId === this.state.instanceId()) {\n throw new SyncCoordinatorError('protocol-invalid', '同步 peer 协议版本或实例身份无效')\n }\n const remoteEntries = this.nodes.validateManifest(remoteManifest.records)\n const localManifest = await this.nodes.exportManifest()\n const localEntries = this.nodes.validateManifest(localManifest.records)\n const plan = planNodeTransfers(peer.direction, localEntries, remoteEntries)\n\n const localResult = plan.toLocal.length > 0\n ? await this.nodes.applyManifest(plan.toLocal, peer.name, 'peer')\n : EMPTY_APPLY_RESULT\n const remoteResult = plan.toRemote.length > 0\n ? await this.retryTransport(() => this.transport.applyNodeManifest(peer.id, plan.toRemote, localManifest.instanceId))\n : EMPTY_APPLY_RESULT\n const localCounts = resultCount(localResult)\n const remoteCounts = resultCount(remoteResult)\n let importedSessions = 0\n let exportedSessions = 0\n let skippedSessions = 0\n let digest = manifestDigest(remoteEntries)\n if (peer.syncSessions) {\n const sessionService = this.sessions!\n const remoteBundle = validateSyncSessionBundle(await this.retryTransport(() => this.transport.fetchSessionBundle(peer.id)))\n if (remoteBundle.instanceId !== remoteManifest.instanceId) {\n throw new SyncCoordinatorError('protocol-invalid', '节点与会话 manifest 的实例身份不一致')\n }\n const localBundle = await sessionService.exportBundle()\n const plan = planSessionTransfers(peer.direction, localBundle.entries, remoteBundle.entries)\n const localBundleToApply = makeSyncSessionBundle(remoteBundle.instanceId, plan.toLocal, remoteBundle.skipped)\n const remoteBundleToApply = makeSyncSessionBundle(localBundle.instanceId, plan.toRemote, localBundle.skipped)\n const sessionLocalResult = plan.toLocal.length > 0 || remoteBundle.skipped.length > 0\n ? await sessionService.applyBundle(localBundleToApply, peer.name, peer.id)\n : emptySessionResult()\n const sessionRemoteResult = plan.toRemote.length > 0\n ? await this.retryTransport(() => this.transport.applySessionBundle(peer.id, remoteBundleToApply, localBundle.instanceId))\n : emptySessionResult()\n importedSessions = sessionResultCount(sessionLocalResult)\n exportedSessions = sessionResultCount(sessionRemoteResult)\n skippedSessions = plan.skipped + sessionLocalResult.skipped + sessionRemoteResult.skipped\n digest = `sha256:${createHash('sha256').update(`${digest}\\0${remoteBundle.digest}\\0${localBundle.digest}`).digest('hex')}`\n }\n const finishedAt = new Date(this.now()).toISOString()\n const summary: SyncRunSummary = {\n peerId,\n direction: peer.direction,\n startedAt,\n finishedAt,\n remoteInstanceId: remoteManifest.instanceId,\n imported: localCounts.applied,\n exported: remoteCounts.applied,\n skipped: plan.skipped + localCounts.skipped + remoteCounts.skipped + skippedSessions,\n pendingRemoval: localCounts.pendingRemoval + remoteCounts.pendingRemoval,\n importedSessions,\n exportedSessions,\n skippedSessions,\n }\n\n this.peers.updateProgress(peer.id, {\n lastRunAt: finishedAt,\n lastRunStatus: 'succeeded',\n lastSuccessAt: finishedAt,\n lastManifestDigest: digest,\n watermark: digest,\n lastErrorCode: undefined,\n lastErrorMessage: undefined,\n lastResult: {\n imported: summary.imported,\n exported: summary.exported,\n skipped: summary.skipped,\n pendingRemoval: summary.pendingRemoval,\n importedSessions,\n exportedSessions,\n skippedSessions,\n },\n })\n return summary\n } catch (error) {\n const failure = mapFailure(error)\n if (!(error instanceof SyncCoordinatorError) && !(error instanceof SyncSshProbeError) && !(error instanceof SyncSessionError) && !(error instanceof SyncNodeManifestError) && !(error instanceof SyncHostKeyChallengeError) && !(error instanceof SyncPeerNotFoundError)) {\n failure.message = `${failure.message}:${error instanceof Error ? error.message : String(error)}`\n }\n try {\n this.peers.updateProgress(peer.id, {\n lastRunAt: new Date(this.now()).toISOString(),\n lastRunStatus: 'failed',\n lastErrorCode: failure.code,\n lastErrorMessage: failure.message,\n })\n } catch {}\n throw failure\n }\n }\n\n private clear(peerId: string, run: Promise<SyncRunSummary>): void {\n if (this.inFlight.get(peerId) === run) this.inFlight.delete(peerId)\n }\n\n private async retryTransport<T>(operation: () => Promise<T>): Promise<T> {\n try {\n return await operation()\n } catch (error) {\n if (!(error instanceof SyncSshProbeError) || !['network', 'timeout', 'helper'].includes(error.category)) throw error\n await new Promise((resolve) => setTimeout(resolve, TRANSPORT_RETRY_DELAY_MS))\n return operation()\n }\n }\n}\n"],"mappings":";;;;;;;;;AAWA,MAAM,qBAA0C;CAAE,SAAS;CAAG,SAAS;CAAG,SAAS;CAAG,gBAAgB;CAAG,SAAS;AAAE;AACpH,MAAM,2BAA2B;AAuBjC,IAAa,uBAAb,cAA0C,MAAM;CACzB;CAArB,YAAY,MAAuB,SAAiB;EAClD,MAAM,OAAO;EADM,KAAA,OAAA;EAEnB,KAAK,OAAO;CACd;AACF;AAEA,SAAS,SAAS,QAAqC;CACrD,OAAO,OAAO,UAAU,OAAO,UAAU,OAAO;AAClD;AAEA,SAAS,eAAe,SAA2C;CACjE,MAAM,WAAW,QACd,KAAK,WAAW;EACf,KAAK,cAAc,QAAQ,MAAM,QAAQ;EACzC,UAAU,MAAM,SAAS;EACzB,eAAe,MAAM,SAAS;EAC9B,SAAS,QAAQ,MAAM,SAAS,OAAO;CACzC,EAAE,CAAC,CACF,MAAM,GAAG,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,EAAE,MAAM,EAAE,MAAM,IAAI,CAAC;CAC5D,OAAO,UAAU,WAAW,QAAQ,CAAC,CAAC,OAAO,KAAK,UAAU,QAAQ,CAAC,CAAC,CAAC,OAAO,KAAK;AACrF;AAEA,SAAgB,kBACd,WACA,cACA,eACkB;CAClB,MAAM,UAA2B,CAAC;CAClC,MAAM,WAA4B,CAAC;CACnC,IAAI,cAAc,QAAQ,OAAO;EAAE,SAAS,CAAC,GAAG,aAAa;EAAG;EAAU,SAAS;CAAE;CAErF,MAAM,aAAa,IAAI,IAAI,aAAa,KAAK,UAAU,CAAC,cAAc,QAAQ,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC;CACtG,MAAM,cAAc,IAAI,IAAI,cAAc,KAAK,UAAU,CAAC,cAAc,QAAQ,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC;CACxG,MAAM,uBAAO,IAAI,IAAI,CAAC,GAAG,WAAW,KAAK,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC;CAClE,IAAI,UAAU;CAEd,KAAK,MAAM,OAAO,MAAM;EACtB,MAAM,QAAQ,WAAW,IAAI,GAAG;EAChC,MAAM,SAAS,YAAY,IAAI,GAAG;EAClC,IAAI,CAAC,OAAO;GACV,QAAQ,KAAK,MAAO;GACpB;EACF;EACA,IAAI,CAAC,QAAQ;GACX,SAAS,KAAK,KAAK;GACnB;EACF;EAEA,MAAM,aAAa,oBAAoB,MAAM,SAAS,UAAU,OAAO,SAAS,QAAQ;EACxF,IAAI,aAAa,GACf,SAAS,KAAK,KAAK;OACd,IAAI,aAAa,GACtB,QAAQ,KAAK,MAAM;OACd,IACL,MAAM,SAAS,kBAAkB,OAAO,SAAS,iBACjD,QAAQ,MAAM,SAAS,OAAO,MAAM,QAAQ,OAAO,SAAS,OAAO,GAEnE,WAAW;OAEX,MAAM,IAAI,qBAAqB,qBAAqB,gDAAgD;CAExG;CACA,OAAO;EAAE;EAAS;EAAU;CAAQ;AACtC;AAEA,SAAS,WAAW,OAAsC;CACxD,IAAI,iBAAiB,sBAAsB,OAAO;CAClD,IAAI,iBAAiB,uBAAuB,OAAO,IAAI,qBAAqB,kBAAkB,UAAU;CACxG,IAAI,iBAAiB,2BAEnB,OAAO,IAAI,qBADE,MAAM,aAAa,YAAY,qBAAqB,sBAC3B,MAAM,OAAO;CAErD,IAAI,iBAAiB,mBAAmB;EAStC,MAAM,UAAU;GAPd,MAAM;GACN,SAAS;GACT,SAAS;GACT,SAAS;GACT,QAAQ;GACR,UAAU;EAEW,EAAE,MAAM,aAAa;EAC5C,MAAM,SAAU,MAAsC;EACtD,OAAO,IAAI,qBAAqB,OAAO,MAAM,YAAY,SAAS,GAAG,QAAQ,GAAG,WAAW,OAAO;CACpG;CACA,IAAI,iBAAiB,uBAAuB,OAAO,IAAI,qBAAqB,oBAAoB,kBAAkB;CAClH,IAAI,iBAAiB,kBACnB,OAAO,IAAI,qBAAqB,MAAM,KAAK,WAAW,UAAU,IAAI,MAAM,OAAO,WAAW,MAAM,QAAQ,MAAM,OAAO;CAEzH,OAAO,IAAI,qBAAqB,eAAe,QAAQ;AACzD;AAEA,SAAS,YAAY,QAA2F;CAC9G,OAAO;EACL,SAAS,SAAS,MAAM;EACxB,SAAS,OAAO;EAChB,gBAAgB,OAAO;CACzB;AACF;AAEA,SAAS,qBAA6C;CACpD,OAAO;EAAE,SAAS;EAAG,SAAS;EAAG,UAAU;EAAG,SAAS;EAAG,SAAS;EAAG,UAAU;CAAE;AACpF;AAEA,SAAS,mBAAmB,QAAwC;CAClE,OAAO,OAAO,UAAU,OAAO,UAAU,OAAO;AAClD;AAEA,IAAa,kBAAb,MAA6B;CAIR;CACA;CACA;CACA;CACA;CACA;CARnB,2BAA4B,IAAI,IAAqC;CAErE,YACE,OACA,OACA,OACA,WACA,MAAqC,KAAK,KAC1C,UACA;EANiB,KAAA,QAAA;EACA,KAAA,QAAA;EACA,KAAA,QAAA;EACA,KAAA,YAAA;EACA,KAAA,MAAA;EACA,KAAA,WAAA;CAChB;CAEH,IAAI,QAAyC;EAC3C,MAAM,SAAS,KAAK,SAAS,IAAI,MAAM;EACvC,IAAI,QAAQ,OAAO;EACnB,MAAM,MAAM,KAAK,QAAQ,MAAM;EAC/B,KAAK,SAAS,IAAI,QAAQ,GAAG;EAC7B,IAAS,WAAW,KAAK,MAAM,QAAQ,GAAG,SAAS,KAAK,MAAM,QAAQ,GAAG,CAAC;EAC1E,OAAO;CACT;CAEA,MAAc,QAAQ,QAAyC;EAC7D,MAAM,OAAO,MAAM,KAAK,MAAM,IAAI,MAAM;EACxC,IAAI,CAAC,MAAM,MAAM,IAAI,qBAAqB,kBAAkB,UAAU;EACtE,IAAI,CAAC,KAAK,SAAS,MAAM,IAAI,qBAAqB,iBAAiB,UAAU;EAE7E,MAAM,YAAY,IAAI,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,YAAY;EACnD,KAAK,MAAM,eAAe,KAAK,IAAI;GACjC,WAAW;GACX,eAAe;GACf,eAAe,KAAA;GACf,kBAAkB,KAAA;EACpB,CAAC;EAED,IAAI;GACF,IAAI,KAAK,gBAAgB,CAAC,KAAK,UAAU,MAAM,IAAI,qBAAqB,wBAAwB,WAAW;GAC3G,MAAM,iBAAiB,MAAM,KAAK,qBAAqB,KAAK,UAAU,kBAAkB,KAAK,EAAE,CAAC;GAChG,IAAI,eAAe,oBAAoB,KAAK,eAAe,eAAe,KAAK,MAAM,WAAW,GAC9F,MAAM,IAAI,qBAAqB,oBAAoB,qBAAqB;GAE1E,MAAM,gBAAgB,KAAK,MAAM,iBAAiB,eAAe,OAAO;GACxE,MAAM,gBAAgB,MAAM,KAAK,MAAM,eAAe;GACtD,MAAM,eAAe,KAAK,MAAM,iBAAiB,cAAc,OAAO;GACtE,MAAM,OAAO,kBAAkB,KAAK,WAAW,cAAc,aAAa;GAE1E,MAAM,cAAc,KAAK,QAAQ,SAAS,IACtC,MAAM,KAAK,MAAM,cAAc,KAAK,SAAS,KAAK,MAAM,MAAM,IAC9D;GACJ,MAAM,eAAe,KAAK,SAAS,SAAS,IACxC,MAAM,KAAK,qBAAqB,KAAK,UAAU,kBAAkB,KAAK,IAAI,KAAK,UAAU,cAAc,UAAU,CAAC,IAClH;GACJ,MAAM,cAAc,YAAY,WAAW;GAC3C,MAAM,eAAe,YAAY,YAAY;GAC7C,IAAI,mBAAmB;GACvB,IAAI,mBAAmB;GACvB,IAAI,kBAAkB;GACtB,IAAI,SAAS,eAAe,aAAa;GACzC,IAAI,KAAK,cAAc;IACrB,MAAM,iBAAiB,KAAK;IAC5B,MAAM,eAAe,0BAA0B,MAAM,KAAK,qBAAqB,KAAK,UAAU,mBAAmB,KAAK,EAAE,CAAC,CAAC;IAC1H,IAAI,aAAa,eAAe,eAAe,YAC7C,MAAM,IAAI,qBAAqB,oBAAoB,yBAAyB;IAE9E,MAAM,cAAc,MAAM,eAAe,aAAa;IACtD,MAAM,OAAO,qBAAqB,KAAK,WAAW,YAAY,SAAS,aAAa,OAAO;IAC3F,MAAM,qBAAqB,sBAAsB,aAAa,YAAY,KAAK,SAAS,aAAa,OAAO;IAC5G,MAAM,sBAAsB,sBAAsB,YAAY,YAAY,KAAK,UAAU,YAAY,OAAO;IAC5G,MAAM,qBAAqB,KAAK,QAAQ,SAAS,KAAK,aAAa,QAAQ,SAAS,IAChF,MAAM,eAAe,YAAY,oBAAoB,KAAK,MAAM,KAAK,EAAE,IACvE,mBAAmB;IACvB,MAAM,sBAAsB,KAAK,SAAS,SAAS,IAC/C,MAAM,KAAK,qBAAqB,KAAK,UAAU,mBAAmB,KAAK,IAAI,qBAAqB,YAAY,UAAU,CAAC,IACvH,mBAAmB;IACvB,mBAAmB,mBAAmB,kBAAkB;IACxD,mBAAmB,mBAAmB,mBAAmB;IACzD,kBAAkB,KAAK,UAAU,mBAAmB,UAAU,oBAAoB;IAClF,SAAS,UAAU,WAAW,QAAQ,CAAC,CAAC,OAAO,GAAG,OAAO,IAAI,aAAa,OAAO,IAAI,YAAY,QAAQ,CAAC,CAAC,OAAO,KAAK;GACzH;GACA,MAAM,aAAa,IAAI,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,YAAY;GACpD,MAAM,UAA0B;IAC9B;IACA,WAAW,KAAK;IAChB;IACA;IACA,kBAAkB,eAAe;IACjC,UAAU,YAAY;IACtB,UAAU,aAAa;IACvB,SAAS,KAAK,UAAU,YAAY,UAAU,aAAa,UAAU;IACrE,gBAAgB,YAAY,iBAAiB,aAAa;IAC1D;IACA;IACA;GACF;GAEA,KAAK,MAAM,eAAe,KAAK,IAAI;IACjC,WAAW;IACX,eAAe;IACf,eAAe;IACf,oBAAoB;IACpB,WAAW;IACX,eAAe,KAAA;IACf,kBAAkB,KAAA;IAClB,YAAY;KACV,UAAU,QAAQ;KAClB,UAAU,QAAQ;KAClB,SAAS,QAAQ;KACjB,gBAAgB,QAAQ;KACxB;KACA;KACA;IACF;GACF,CAAC;GACD,OAAO;EACT,SAAS,OAAO;GACd,MAAM,UAAU,WAAW,KAAK;GAChC,IAAI,EAAE,iBAAiB,yBAAyB,EAAE,iBAAiB,sBAAsB,EAAE,iBAAiB,qBAAqB,EAAE,iBAAiB,0BAA0B,EAAE,iBAAiB,8BAA8B,EAAE,iBAAiB,wBAChP,QAAQ,UAAU,GAAG,QAAQ,QAAQ,GAAG,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;GAE/F,IAAI;IACF,KAAK,MAAM,eAAe,KAAK,IAAI;KACjC,WAAW,IAAI,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,YAAY;KAC5C,eAAe;KACf,eAAe,QAAQ;KACvB,kBAAkB,QAAQ;IAC5B,CAAC;GACH,QAAQ,CAAC;GACT,MAAM;EACR;CACF;CAEA,MAAc,QAAgB,KAAoC;EAChE,IAAI,KAAK,SAAS,IAAI,MAAM,MAAM,KAAK,KAAK,SAAS,OAAO,MAAM;CACpE;CAEA,MAAc,eAAkB,WAAyC;EACvE,IAAI;GACF,OAAO,MAAM,UAAU;EACzB,SAAS,OAAO;GACd,IAAI,EAAE,iBAAiB,sBAAsB,CAAC;IAAC;IAAW;IAAW;GAAQ,CAAC,CAAC,SAAS,MAAM,QAAQ,GAAG,MAAM;GAC/G,MAAM,IAAI,SAAS,YAAY,WAAW,SAAS,wBAAwB,CAAC;GAC5E,OAAO,UAAU;EACnB;CACF;AACF"}
@@ -0,0 +1,2 @@
1
+ import { a as inspectSyncHostKey, i as SyncHostKeyDecision, n as SyncHostKeyChallengeExpiredError, o as isOpenSshFingerprint, r as SyncHostKeyChallenges, s as opensshFingerprintFromSha256Hex, t as SyncHostKeyChallengeError } from "../../host-key-C5afhAUd.js";
2
+ export { SyncHostKeyChallengeError, SyncHostKeyChallengeExpiredError, SyncHostKeyChallenges, SyncHostKeyDecision, inspectSyncHostKey, isOpenSshFingerprint, opensshFingerprintFromSha256Hex };
@@ -0,0 +1,71 @@
1
+ //#region src/core/sync/host-key.ts
2
+ const CHALLENGE_TTL_MS = 3e5;
3
+ var SyncHostKeyChallengeError = class extends Error {
4
+ decision;
5
+ fingerprint;
6
+ expected;
7
+ constructor(decision, fingerprint, expected) {
8
+ super(decision === "untrusted" ? "SSH host key requires explicit trust" : "SSH host key changed");
9
+ this.decision = decision;
10
+ this.fingerprint = fingerprint;
11
+ this.expected = expected;
12
+ this.name = "SyncHostKeyChallengeError";
13
+ }
14
+ };
15
+ var SyncHostKeyChallengeExpiredError = class extends Error {
16
+ constructor() {
17
+ super("SSH host key challenge expired");
18
+ this.name = "SyncHostKeyChallengeExpiredError";
19
+ }
20
+ };
21
+ function opensshFingerprintFromSha256Hex(value) {
22
+ if (!/^[a-f0-9]{64}$/i.test(value)) throw new Error("invalid SSH host-key hash");
23
+ return `SHA256:${Buffer.from(value, "hex").toString("base64").replace(/=+$/, "")}`;
24
+ }
25
+ function isOpenSshFingerprint(value) {
26
+ return /^SHA256:[A-Za-z0-9+/]{43}$/.test(value);
27
+ }
28
+ function inspectSyncHostKey(peer, sha256Hex, challenges) {
29
+ const fingerprint = opensshFingerprintFromSha256Hex(sha256Hex);
30
+ try {
31
+ if (challenges.verify(peer, fingerprint)) return { trusted: true };
32
+ } catch (error) {
33
+ if (error instanceof SyncHostKeyChallengeError) return {
34
+ trusted: false,
35
+ challenge: error
36
+ };
37
+ throw error;
38
+ }
39
+ throw new Error("host-key verifier returned an invalid decision");
40
+ }
41
+ var SyncHostKeyChallenges = class {
42
+ now;
43
+ pending = /* @__PURE__ */ new Map();
44
+ constructor(now = Date.now) {
45
+ this.now = now;
46
+ }
47
+ verify(peer, fingerprint) {
48
+ if (!isOpenSshFingerprint(fingerprint)) throw new Error("invalid SSH host-key fingerprint");
49
+ if (peer.hostKeyFingerprint === fingerprint) {
50
+ this.pending.delete(peer.id);
51
+ return true;
52
+ }
53
+ this.pending.set(peer.id, {
54
+ host: peer.host,
55
+ port: peer.port,
56
+ fingerprint,
57
+ expiresAt: this.now() + CHALLENGE_TTL_MS
58
+ });
59
+ throw new SyncHostKeyChallengeError(peer.hostKeyFingerprint ? "changed" : "untrusted", fingerprint, peer.hostKeyFingerprint);
60
+ }
61
+ acceptObserved(peer) {
62
+ const challenge = this.pending.get(peer.id);
63
+ this.pending.delete(peer.id);
64
+ if (!challenge || challenge.expiresAt <= this.now() || challenge.host !== peer.host || challenge.port !== peer.port) throw new SyncHostKeyChallengeExpiredError();
65
+ return challenge.fingerprint;
66
+ }
67
+ };
68
+ //#endregion
69
+ export { SyncHostKeyChallengeError, SyncHostKeyChallengeExpiredError, SyncHostKeyChallenges, inspectSyncHostKey, isOpenSshFingerprint, opensshFingerprintFromSha256Hex };
70
+
71
+ //# sourceMappingURL=host-key.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"host-key.js","names":[],"sources":["../../../src/core/sync/host-key.ts"],"sourcesContent":["import type { SyncPeer } from './model.js'\n\nconst CHALLENGE_TTL_MS = 5 * 60 * 1000\n\nexport type SyncHostKeyDecision = 'trusted' | 'untrusted' | 'changed'\n\ninterface PendingHostKey {\n host: string\n port: number\n fingerprint: string\n expiresAt: number\n}\n\nexport class SyncHostKeyChallengeError extends Error {\n constructor(\n readonly decision: Exclude<SyncHostKeyDecision, 'trusted'>,\n readonly fingerprint: string,\n readonly expected?: string,\n ) {\n super(decision === 'untrusted' ? 'SSH host key requires explicit trust' : 'SSH host key changed')\n this.name = 'SyncHostKeyChallengeError'\n }\n}\n\nexport class SyncHostKeyChallengeExpiredError extends Error {\n constructor() {\n super('SSH host key challenge expired')\n this.name = 'SyncHostKeyChallengeExpiredError'\n }\n}\n\nexport function opensshFingerprintFromSha256Hex(value: string): string {\n if (!/^[a-f0-9]{64}$/i.test(value)) throw new Error('invalid SSH host-key hash')\n const base64 = Buffer.from(value, 'hex').toString('base64').replace(/=+$/, '')\n return `SHA256:${base64}`\n}\n\nexport function isOpenSshFingerprint(value: string): boolean {\n return /^SHA256:[A-Za-z0-9+/]{43}$/.test(value)\n}\n\nexport function inspectSyncHostKey(\n peer: SyncPeer,\n sha256Hex: string,\n challenges: SyncHostKeyChallenges,\n): { trusted: true } | { trusted: false; challenge: SyncHostKeyChallengeError } {\n const fingerprint = opensshFingerprintFromSha256Hex(sha256Hex)\n try {\n if (challenges.verify(peer, fingerprint)) return { trusted: true }\n } catch (error) {\n if (error instanceof SyncHostKeyChallengeError) return { trusted: false, challenge: error }\n throw error\n }\n throw new Error('host-key verifier returned an invalid decision')\n}\n\nexport class SyncHostKeyChallenges {\n private readonly pending = new Map<string, PendingHostKey>()\n\n constructor(private readonly now: () => number = Date.now) {}\n\n verify(peer: SyncPeer, fingerprint: string): boolean {\n if (!isOpenSshFingerprint(fingerprint)) throw new Error('invalid SSH host-key fingerprint')\n if (peer.hostKeyFingerprint === fingerprint) {\n this.pending.delete(peer.id)\n return true\n }\n this.pending.set(peer.id, {\n host: peer.host,\n port: peer.port,\n fingerprint,\n expiresAt: this.now() + CHALLENGE_TTL_MS,\n })\n throw new SyncHostKeyChallengeError(\n peer.hostKeyFingerprint ? 'changed' : 'untrusted',\n fingerprint,\n peer.hostKeyFingerprint,\n )\n }\n\n acceptObserved(peer: SyncPeer): string {\n const challenge = this.pending.get(peer.id)\n this.pending.delete(peer.id)\n if (!challenge || challenge.expiresAt <= this.now() || challenge.host !== peer.host || challenge.port !== peer.port) {\n throw new SyncHostKeyChallengeExpiredError()\n }\n return challenge.fingerprint\n }\n}\n"],"mappings":";AAEA,MAAM,mBAAmB;AAWzB,IAAa,4BAAb,cAA+C,MAAM;CAExC;CACA;CACA;CAHX,YACE,UACA,aACA,UACA;EACA,MAAM,aAAa,cAAc,yCAAyC,sBAAsB;EAJvF,KAAA,WAAA;EACA,KAAA,cAAA;EACA,KAAA,WAAA;EAGT,KAAK,OAAO;CACd;AACF;AAEA,IAAa,mCAAb,cAAsD,MAAM;CAC1D,cAAc;EACZ,MAAM,gCAAgC;EACtC,KAAK,OAAO;CACd;AACF;AAEA,SAAgB,gCAAgC,OAAuB;CACrE,IAAI,CAAC,kBAAkB,KAAK,KAAK,GAAG,MAAM,IAAI,MAAM,2BAA2B;CAE/E,OAAO,UADQ,OAAO,KAAK,OAAO,KAAK,CAAC,CAAC,SAAS,QAAQ,CAAC,CAAC,QAAQ,OAAO,EACrD;AACxB;AAEA,SAAgB,qBAAqB,OAAwB;CAC3D,OAAO,6BAA6B,KAAK,KAAK;AAChD;AAEA,SAAgB,mBACd,MACA,WACA,YAC8E;CAC9E,MAAM,cAAc,gCAAgC,SAAS;CAC7D,IAAI;EACF,IAAI,WAAW,OAAO,MAAM,WAAW,GAAG,OAAO,EAAE,SAAS,KAAK;CACnE,SAAS,OAAO;EACd,IAAI,iBAAiB,2BAA2B,OAAO;GAAE,SAAS;GAAO,WAAW;EAAM;EAC1F,MAAM;CACR;CACA,MAAM,IAAI,MAAM,gDAAgD;AAClE;AAEA,IAAa,wBAAb,MAAmC;CAGJ;CAF7B,0BAA2B,IAAI,IAA4B;CAE3D,YAAY,MAAqC,KAAK,KAAK;EAA9B,KAAA,MAAA;CAA+B;CAE5D,OAAO,MAAgB,aAA8B;EACnD,IAAI,CAAC,qBAAqB,WAAW,GAAG,MAAM,IAAI,MAAM,kCAAkC;EAC1F,IAAI,KAAK,uBAAuB,aAAa;GAC3C,KAAK,QAAQ,OAAO,KAAK,EAAE;GAC3B,OAAO;EACT;EACA,KAAK,QAAQ,IAAI,KAAK,IAAI;GACxB,MAAM,KAAK;GACX,MAAM,KAAK;GACX;GACA,WAAW,KAAK,IAAI,IAAI;EAC1B,CAAC;EACD,MAAM,IAAI,0BACR,KAAK,qBAAqB,YAAY,aACtC,aACA,KAAK,kBACP;CACF;CAEA,eAAe,MAAwB;EACrC,MAAM,YAAY,KAAK,QAAQ,IAAI,KAAK,EAAE;EAC1C,KAAK,QAAQ,OAAO,KAAK,EAAE;EAC3B,IAAI,CAAC,aAAa,UAAU,aAAa,KAAK,IAAI,KAAK,UAAU,SAAS,KAAK,QAAQ,UAAU,SAAS,KAAK,MAC7G,MAAM,IAAI,iCAAiC;EAE7C,OAAO,UAAU;CACnB;AACF"}
@@ -0,0 +1,9 @@
1
+ import { n as SyncStateStore } from "../../stores-CKyEt0yk.js";
2
+ import { s as SyncNodeService } from "../../node-sync-3Z9QLz2n.js";
3
+ import { c as SyncSessionService } from "../../session-sync-g6qKZzjV.js";
4
+ import { Context } from "@deepseek-ai/cordis";
5
+ //#region src/core/sync/internal-routes.d.ts
6
+ declare function registerSyncInternalRoutes(ctx: Context, state: SyncStateStore, nodeSync?: SyncNodeService, sessionSync?: SyncSessionService): void;
7
+ //#endregion
8
+ export { registerSyncInternalRoutes };
9
+ //# sourceMappingURL=internal-routes.d.ts.map