@artenx/dshb 0.1.33 → 0.1.35

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 (51) hide show
  1. package/cordis.patch.yml +8 -4
  2. package/lib/auth/reset-cli.d.ts +15 -0
  3. package/lib/auth/reset-cli.js +47 -0
  4. package/lib/auth/reset-cli.js.map +1 -0
  5. package/lib/client.js +34 -3
  6. package/lib/client.js.map +1 -1
  7. package/lib/core/index.js +1 -1
  8. package/lib/core/routes.d.ts +1 -1
  9. package/lib/core/sync/coordinator.d.ts +14 -5
  10. package/lib/core/sync/coordinator.js +83 -18
  11. package/lib/core/sync/coordinator.js.map +1 -1
  12. package/lib/core/sync/host-key.d.ts +1 -1
  13. package/lib/core/sync/internal-routes.d.ts +3 -3
  14. package/lib/core/sync/internal-routes.js +1 -1
  15. package/lib/core/sync/model.d.ts +1 -1
  16. package/lib/core/sync/model.js.map +1 -1
  17. package/lib/core/sync/node-sync.d.ts +1 -1
  18. package/lib/core/sync/offline-archive.d.ts +3 -3
  19. package/lib/core/sync/offline-archive.js +1 -1
  20. package/lib/core/sync/offline-routes.js +1 -1
  21. package/lib/core/sync/peer-manager.d.ts +2 -2
  22. package/lib/core/sync/routes.d.ts +1 -1
  23. package/lib/core/sync/routes.js +19 -0
  24. package/lib/core/sync/routes.js.map +1 -1
  25. package/lib/core/sync/session-journal.d.ts +1 -1
  26. package/lib/core/sync/session-journal.js +16 -0
  27. package/lib/core/sync/session-journal.js.map +1 -1
  28. package/lib/core/sync/session-routes.d.ts +1 -1
  29. package/lib/core/sync/session-routes.js +1 -1
  30. package/lib/core/sync/session-sync.d.ts +1 -1
  31. package/lib/core/sync/session-sync.js +1 -1
  32. package/lib/core/sync/ssh-transport.d.ts +1 -1
  33. package/lib/core/sync/ssh-transport.js +29 -2
  34. package/lib/core/sync/ssh-transport.js.map +1 -1
  35. package/lib/core/sync/stores.d.ts +1 -1
  36. package/lib/core/sync/stores.js +1 -1
  37. package/lib/core/sync/stores.js.map +1 -1
  38. package/lib/core/workspaces.d.ts +1 -1
  39. package/lib/{host-key-C5afhAUd.d.ts → host-key-CSnpIrjq.d.ts} +2 -2
  40. package/lib/{model-BP7Gw1yk.d.ts → model-CKTBf-Pl.d.ts} +2 -2
  41. package/lib/{node-sync-3Z9QLz2n.d.ts → node-sync-C3Ym-HDu.d.ts} +3 -3
  42. package/lib/{session-journal-pgPqcWn9.d.ts → session-journal-VE_1tA2-.d.ts} +8 -2
  43. package/lib/{session-sync-CIqjuWkh.js → session-sync-DLr_BdvN.js} +15 -1
  44. package/lib/{session-sync-CIqjuWkh.js.map → session-sync-DLr_BdvN.js.map} +1 -1
  45. package/lib/{session-sync-BuOJvFxK.d.ts → session-sync-DrKShc5c.d.ts} +11 -5
  46. package/lib/{ssh-transport-C7povM0s.d.ts → ssh-transport-BxPWfu9X.d.ts} +13 -6
  47. package/lib/{stores-CKyEt0yk.d.ts → stores-puyTC075.d.ts} +2 -2
  48. package/package.json +6 -4
  49. package/lib/auth/startup.d.ts +0 -15
  50. package/lib/auth/startup.js +0 -68
  51. package/lib/auth/startup.js.map +0 -1
@@ -1,5 +1,5 @@
1
- import { a as SyncPeer, s as SyncPeerProgress } from "../../model-BP7Gw1yk.js";
2
- import { n as SyncStateStore, t as SyncPeerStore } from "../../stores-CKyEt0yk.js";
1
+ import { a as SyncPeer, s as SyncPeerProgress } from "../../model-CKTBf-Pl.js";
2
+ import { n as SyncStateStore, t as SyncPeerStore } from "../../stores-puyTC075.js";
3
3
  import { CredentialKey } from "@deepseek-ai/dsh-credentials";
4
4
  //#region src/core/sync/peer-manager.d.ts
5
5
  interface SyncPeerSecrets {
@@ -1,5 +1,5 @@
1
1
  import { SyncPeerManager } from "./peer-manager.js";
2
- import { m as SyncSshTransport } from "../../ssh-transport-C7povM0s.js";
2
+ import { m as SyncSshTransport } from "../../ssh-transport-BxPWfu9X.js";
3
3
  import { SyncCoordinator } from "./coordinator.js";
4
4
  import { Context } from "@deepseek-ai/cordis";
5
5
  //#region src/core/sync/routes.d.ts
@@ -320,6 +320,25 @@ function registerSyncPeerRoutes(ctx, manager, transport, coordinator) {
320
320
  }
321
321
  return;
322
322
  }
323
+ if (action === "stop") {
324
+ if (!coordinator) {
325
+ sendJson(res, 503, {
326
+ ok: false,
327
+ error: "sync coordinator unavailable"
328
+ });
329
+ return;
330
+ }
331
+ try {
332
+ await manager.get(peerId);
333
+ sendJson(res, 200, {
334
+ ok: true,
335
+ stopped: coordinator.stop(peerId)
336
+ });
337
+ } catch (error) {
338
+ sendRunFailure(res, error);
339
+ }
340
+ return;
341
+ }
323
342
  sendJson(res, 404, {
324
343
  ok: false,
325
344
  error: "not found"
@@ -1 +1 @@
1
- {"version":3,"file":"routes.js","names":[],"sources":["../../../src/core/sync/routes.ts"],"sourcesContent":["import type { IncomingMessage, ServerResponse } from 'node:http'\nimport type { Context } from '@deepseek-ai/cordis'\nimport type {} from '@deepseek-ai/dsh-host-webserver'\nimport { SyncHostKeyChallengeError, SyncHostKeyChallengeExpiredError } from './host-key.js'\nimport { SyncPeerInputError, SyncPeerManager, SyncPeerNotFoundError } from './peer-manager.js'\nimport { SyncRemoteProbe, SyncSshProbeError, SyncSshTransport } from './ssh-transport.js'\nimport { SyncCoordinator, SyncCoordinatorError } from './coordinator.js'\n\nconst MAX_BODY_BYTES = 128 * 1024\n\nfunction sendJson(res: ServerResponse, status: number, body: unknown): void {\n res.writeHead(status, { 'content-type': 'application/json' })\n res.end(JSON.stringify(body))\n}\n\nfunction readBody(req: IncomingMessage): Promise<Record<string, unknown> | null> {\n return new Promise((resolve) => {\n const contentType = req.headers['content-type']?.split(';', 1)[0]?.trim().toLowerCase()\n if (contentType !== 'application/json') {\n resolve(null)\n return\n }\n\n const chunks: Buffer[] = []\n let size = 0\n let settled = false\n const fail = (): void => {\n if (settled) return\n settled = true\n resolve(null)\n }\n req.on('data', (chunk: Buffer) => {\n if (settled) return\n size += chunk.length\n if (size > MAX_BODY_BYTES) {\n fail()\n req.destroy()\n return\n }\n chunks.push(chunk)\n })\n req.on('end', () => {\n if (settled) return\n try {\n const value: unknown = JSON.parse(Buffer.concat(chunks).toString('utf8'))\n if (value === null || typeof value !== 'object' || Array.isArray(value)) {\n fail()\n return\n }\n settled = true\n resolve(value as Record<string, unknown>)\n } catch {\n fail()\n }\n })\n req.on('error', fail)\n })\n}\n\nfunction pathSegments(req: IncomingMessage): string[] {\n return new URL(req.url ?? '/', 'http://x').pathname.split('/').filter(Boolean)\n}\n\nfunction splitBody(body: Record<string, unknown>): { fields: Record<string, unknown>; secrets: unknown } {\n const { secrets, ...fields } = body\n return { fields, secrets }\n}\n\nfunction sendFailure(res: ServerResponse, error: unknown): void {\n if (error instanceof SyncPeerInputError) {\n sendJson(res, 400, { ok: false, error: error.message })\n return\n }\n if (error instanceof SyncPeerNotFoundError) {\n sendJson(res, 404, { ok: false, error: 'sync peer not found' })\n return\n }\n sendJson(res, 500, { ok: false, error: 'sync peer operation failed' })\n}\n\nfunction sendProbeFailure(res: ServerResponse, error: unknown): void {\n if (error instanceof SyncHostKeyChallengeError) {\n sendJson(res, 409, {\n ok: false,\n code: `host-key-${error.decision}`,\n fingerprint: error.fingerprint,\n ...(error.expected ? { expected: error.expected } : {}),\n })\n return\n }\n if (error instanceof SyncHostKeyChallengeExpiredError) {\n sendJson(res, 409, { ok: false, code: 'host-key-challenge-expired', error: error.message })\n return\n }\n if (error instanceof SyncPeerNotFoundError) {\n sendJson(res, 404, { ok: false, error: 'sync peer not found' })\n return\n }\n if (error instanceof SyncSshProbeError) {\n const status = error.category === 'auth' ? 502 : error.category === 'protocol' || error.category === 'hostkey' ? 409 : 503\n sendJson(res, status, { ok: false, code: `ssh-${error.category}`, error: error.message })\n return\n }\n sendJson(res, 500, { ok: false, error: 'sync peer probe failed' })\n}\n\nfunction sendRunFailure(res: ServerResponse, error: unknown): void {\n if (error instanceof SyncCoordinatorError) {\n const status = error.code === 'peer-not-found' ? 404\n : error.code === 'peer-disabled' || error.code === 'sessions-unavailable' || error.code === 'revision-conflict' ? 409\n : error.code === 'session-bundle-too-large' || error.code === 'session-too-large' ? 413\n : error.code === 'session-workspace-unmapped' || error.code === 'session-format' || error.code === 'session-revision-conflict' || error.code === 'session-same-instance' ? 409\n : error.code.startsWith('ssh-') ? 502 : 500\n sendJson(res, status, { ok: false, code: error.code, error: error.message })\n return\n }\n sendJson(res, 500, { ok: false, error: 'sync run failed' })\n}\n\nexport function registerSyncPeerRoutes(\n ctx: Context,\n manager: SyncPeerManager,\n transport?: SyncSshTransport,\n coordinator?: SyncCoordinator,\n): void {\n ctx.effect(() =>\n ctx.webServer.register({\n kind: 'prefix',\n path: '/api/dshb/sync/peers',\n handler: async (req, res) => {\n const segments = pathSegments(req)\n const peerId = segments[4]\n const action = segments[5]\n if (segments.length > 6) {\n sendJson(res, 404, { ok: false, error: 'not found' })\n return\n }\n\n if (req.method === 'GET' && peerId === undefined) {\n try {\n sendJson(res, 200, { ok: true, peers: await manager.list() })\n } catch {\n sendJson(res, 500, { ok: false, error: 'sync peer list unavailable' })\n }\n return\n }\n\n if (req.method === 'POST' && peerId === 'probe-remote') {\n // 远端配置探测:① peerId 模式——用已保存对端的存储凭据;② 显式模式——\n // 用表单填写的 SSH 连接信息(新建对端场景)。\n const body = await readBody(req)\n if (!body || (typeof body.peerId !== 'string'\n && (typeof body.host !== 'string' || !body.host.trim()\n || typeof body.username !== 'string' || !body.username.trim()\n || !Number.isInteger(body.port) || (body.port as number) < 1 || (body.port as number) > 65535\n || (body.authKind !== 'password' && body.authKind !== 'key')))) {\n sendJson(res, 400, { ok: false, error: 'either peerId or host/port/username/authKind are required' })\n return\n }\n try {\n const probe = new SyncRemoteProbe()\n let result\n if (typeof body.peerId === 'string' && body.peerId !== 'new') {\n const { peer, secrets } = await manager.connectionData(body.peerId)\n result = await probe.probeRemote(\n { host: peer.host, port: peer.port, username: peer.username, authKind: peer.authKind },\n secrets,\n peer.remoteWebPort,\n )\n } else {\n const secrets = (body.secrets ?? {}) as Record<string, unknown>\n if (body.authKind === 'password' && typeof secrets.password !== 'string') {\n sendJson(res, 400, { ok: false, error: 'SSH password is required for probing' })\n return\n }\n if (body.authKind === 'key' && typeof secrets.privateKey !== 'string') {\n sendJson(res, 400, { ok: false, error: 'SSH private key is required for probing' })\n return\n }\n const authKind = body.authKind === 'key' ? 'key' : 'password'\n result = await probe.probeRemote(\n { host: (body.host as string).trim(), port: body.port as number, username: (body.username as string).trim(), authKind },\n secrets as { password?: string; privateKey?: string; passphrase?: string },\n )\n }\n sendJson(res, 200, { ok: true, probe: result })\n } catch (error) {\n if (error instanceof SyncPeerNotFoundError) {\n sendJson(res, 404, { ok: false, error: '同步节点已不存在' })\n return\n }\n sendProbeFailure(res, error)\n }\n return\n }\n if (req.method === 'POST' && peerId === undefined) {\n const body = await readBody(req)\n if (!body) {\n sendJson(res, 400, { ok: false, error: 'request body must be JSON' })\n return\n }\n const { fields, secrets } = splitBody(body)\n try {\n sendJson(res, 201, { ok: true, peer: await manager.create(fields, secrets) })\n } catch (error) {\n sendFailure(res, error)\n }\n return\n }\n\n if (!peerId) {\n sendJson(res, 405, { ok: false, error: 'method not allowed' })\n return\n }\n\n if (action !== undefined) {\n if (segments.length !== 6 || req.method !== 'POST') {\n sendJson(res, 405, { ok: false, error: 'method not allowed' })\n return\n }\n if (!transport) {\n sendJson(res, 503, { ok: false, error: 'SSH sync transport unavailable' })\n return\n }\n if (action === 'probe') {\n try {\n const handshake = await transport.probe(peerId)\n sendJson(res, 200, { ok: true, handshake })\n } catch (error) {\n sendProbeFailure(res, error)\n }\n return\n }\n if (action === 'trust-host-key') {\n try {\n await transport.trustObservedHostKey(peerId)\n const peer = await manager.get(peerId)\n sendJson(res, 200, { ok: true, peer })\n } catch (error) {\n sendProbeFailure(res, error)\n }\n return\n }\n if (action === 'run') {\n if (!coordinator) {\n sendJson(res, 503, { ok: false, error: 'sync coordinator unavailable' })\n return\n }\n try {\n const peer = await manager.get(peerId)\n if (!peer) {\n sendJson(res, 404, { ok: false, error: 'sync peer not found' })\n return\n }\n if (!peer.enabled) {\n sendJson(res, 409, { ok: false, code: 'peer-disabled', error: '此同步节点已停用' })\n return\n }\n void coordinator.run(peerId).catch(() => undefined)\n sendJson(res, 202, { ok: true, accepted: true })\n } catch (error) {\n sendRunFailure(res, error)\n }\n return\n }\n sendJson(res, 404, { ok: false, error: 'not found' })\n return\n }\n\n if (req.method === 'GET') {\n try {\n const peer = await manager.get(peerId)\n if (!peer) {\n sendJson(res, 404, { ok: false, error: 'sync peer not found' })\n return\n }\n sendJson(res, 200, { ok: true, peer })\n } catch {\n sendJson(res, 500, { ok: false, error: 'sync peer lookup failed' })\n }\n return\n }\n\n if (req.method === 'PATCH') {\n const body = await readBody(req)\n if (!body) {\n sendJson(res, 400, { ok: false, error: 'request body must be JSON' })\n return\n }\n const { fields, secrets } = splitBody(body)\n try {\n sendJson(res, 200, { ok: true, peer: await manager.update(peerId, fields, secrets) })\n } catch (error) {\n sendFailure(res, error)\n }\n return\n }\n\n if (req.method === 'DELETE') {\n try {\n if (!(await manager.remove(peerId))) {\n sendJson(res, 404, { ok: false, error: 'sync peer not found' })\n return\n }\n sendJson(res, 200, { ok: true })\n } catch {\n sendJson(res, 500, { ok: false, error: 'sync peer removal failed' })\n }\n return\n }\n\n sendJson(res, 405, { ok: false, error: 'method not allowed' })\n },\n }),\n )\n}\n"],"mappings":";;;;;AAQA,MAAM,iBAAiB;AAEvB,SAAS,SAAS,KAAqB,QAAgB,MAAqB;CAC1E,IAAI,UAAU,QAAQ,EAAE,gBAAgB,mBAAmB,CAAC;CAC5D,IAAI,IAAI,KAAK,UAAU,IAAI,CAAC;AAC9B;AAEA,SAAS,SAAS,KAA+D;CAC/E,OAAO,IAAI,SAAS,YAAY;EAE9B,IADoB,IAAI,QAAQ,eAAe,EAAE,MAAM,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC,YAAY,MAClE,oBAAoB;GACtC,QAAQ,IAAI;GACZ;EACF;EAEA,MAAM,SAAmB,CAAC;EAC1B,IAAI,OAAO;EACX,IAAI,UAAU;EACd,MAAM,aAAmB;GACvB,IAAI,SAAS;GACb,UAAU;GACV,QAAQ,IAAI;EACd;EACA,IAAI,GAAG,SAAS,UAAkB;GAChC,IAAI,SAAS;GACb,QAAQ,MAAM;GACd,IAAI,OAAO,gBAAgB;IACzB,KAAK;IACL,IAAI,QAAQ;IACZ;GACF;GACA,OAAO,KAAK,KAAK;EACnB,CAAC;EACD,IAAI,GAAG,aAAa;GAClB,IAAI,SAAS;GACb,IAAI;IACF,MAAM,QAAiB,KAAK,MAAM,OAAO,OAAO,MAAM,CAAC,CAAC,SAAS,MAAM,CAAC;IACxE,IAAI,UAAU,QAAQ,OAAO,UAAU,YAAY,MAAM,QAAQ,KAAK,GAAG;KACvE,KAAK;KACL;IACF;IACA,UAAU;IACV,QAAQ,KAAgC;GAC1C,QAAQ;IACN,KAAK;GACP;EACF,CAAC;EACD,IAAI,GAAG,SAAS,IAAI;CACtB,CAAC;AACH;AAEA,SAAS,aAAa,KAAgC;CACpD,OAAO,IAAI,IAAI,IAAI,OAAO,KAAK,UAAU,CAAC,CAAC,SAAS,MAAM,GAAG,CAAC,CAAC,OAAO,OAAO;AAC/E;AAEA,SAAS,UAAU,MAAsF;CACvG,MAAM,EAAE,SAAS,GAAG,WAAW;CAC/B,OAAO;EAAE;EAAQ;CAAQ;AAC3B;AAEA,SAAS,YAAY,KAAqB,OAAsB;CAC9D,IAAI,iBAAiB,oBAAoB;EACvC,SAAS,KAAK,KAAK;GAAE,IAAI;GAAO,OAAO,MAAM;EAAQ,CAAC;EACtD;CACF;CACA,IAAI,iBAAiB,uBAAuB;EAC1C,SAAS,KAAK,KAAK;GAAE,IAAI;GAAO,OAAO;EAAsB,CAAC;EAC9D;CACF;CACA,SAAS,KAAK,KAAK;EAAE,IAAI;EAAO,OAAO;CAA6B,CAAC;AACvE;AAEA,SAAS,iBAAiB,KAAqB,OAAsB;CACnE,IAAI,iBAAiB,2BAA2B;EAC9C,SAAS,KAAK,KAAK;GACjB,IAAI;GACJ,MAAM,YAAY,MAAM;GACxB,aAAa,MAAM;GACnB,GAAI,MAAM,WAAW,EAAE,UAAU,MAAM,SAAS,IAAI,CAAC;EACvD,CAAC;EACD;CACF;CACA,IAAI,iBAAiB,kCAAkC;EACrD,SAAS,KAAK,KAAK;GAAE,IAAI;GAAO,MAAM;GAA8B,OAAO,MAAM;EAAQ,CAAC;EAC1F;CACF;CACA,IAAI,iBAAiB,uBAAuB;EAC1C,SAAS,KAAK,KAAK;GAAE,IAAI;GAAO,OAAO;EAAsB,CAAC;EAC9D;CACF;CACA,IAAI,iBAAiB,mBAAmB;EAEtC,SAAS,KADM,MAAM,aAAa,SAAS,MAAM,MAAM,aAAa,cAAc,MAAM,aAAa,YAAY,MAAM,KACjG;GAAE,IAAI;GAAO,MAAM,OAAO,MAAM;GAAY,OAAO,MAAM;EAAQ,CAAC;EACxF;CACF;CACA,SAAS,KAAK,KAAK;EAAE,IAAI;EAAO,OAAO;CAAyB,CAAC;AACnE;AAEA,SAAS,eAAe,KAAqB,OAAsB;CACjE,IAAI,iBAAiB,sBAAsB;EAMzC,SAAS,KALM,MAAM,SAAS,mBAAmB,MAC7C,MAAM,SAAS,mBAAmB,MAAM,SAAS,0BAA0B,MAAM,SAAS,sBAAsB,MAC9G,MAAM,SAAS,8BAA8B,MAAM,SAAS,sBAAsB,MAChF,MAAM,SAAS,gCAAgC,MAAM,SAAS,oBAAoB,MAAM,SAAS,+BAA+B,MAAM,SAAS,0BAA0B,MACvK,MAAM,KAAK,WAAW,MAAM,IAAI,MAAM,KAC1B;GAAE,IAAI;GAAO,MAAM,MAAM;GAAM,OAAO,MAAM;EAAQ,CAAC;EAC3E;CACF;CACA,SAAS,KAAK,KAAK;EAAE,IAAI;EAAO,OAAO;CAAkB,CAAC;AAC5D;AAEA,SAAgB,uBACd,KACA,SACA,WACA,aACM;CACN,IAAI,aACF,IAAI,UAAU,SAAS;EACrB,MAAM;EACN,MAAM;EACN,SAAS,OAAO,KAAK,QAAQ;GAC3B,MAAM,WAAW,aAAa,GAAG;GACjC,MAAM,SAAS,SAAS;GACxB,MAAM,SAAS,SAAS;GACxB,IAAI,SAAS,SAAS,GAAG;IACvB,SAAS,KAAK,KAAK;KAAE,IAAI;KAAO,OAAO;IAAY,CAAC;IACpD;GACF;GAEA,IAAI,IAAI,WAAW,SAAS,WAAW,KAAA,GAAW;IAChD,IAAI;KACF,SAAS,KAAK,KAAK;MAAE,IAAI;MAAM,OAAO,MAAM,QAAQ,KAAK;KAAE,CAAC;IAC9D,QAAQ;KACN,SAAS,KAAK,KAAK;MAAE,IAAI;MAAO,OAAO;KAA6B,CAAC;IACvE;IACA;GACF;GAEA,IAAI,IAAI,WAAW,UAAU,WAAW,gBAAgB;IAGtD,MAAM,OAAO,MAAM,SAAS,GAAG;IAC/B,IAAI,CAAC,QAAS,OAAO,KAAK,WAAW,aAC/B,OAAO,KAAK,SAAS,YAAY,CAAC,KAAK,KAAK,KAAK,KAChD,OAAO,KAAK,aAAa,YAAY,CAAC,KAAK,SAAS,KAAK,KACzD,CAAC,OAAO,UAAU,KAAK,IAAI,KAAM,KAAK,OAAkB,KAAM,KAAK,OAAkB,SACpF,KAAK,aAAa,cAAc,KAAK,aAAa,QAAU;KAClE,SAAS,KAAK,KAAK;MAAE,IAAI;MAAO,OAAO;KAA4D,CAAC;KACpG;IACF;IACA,IAAI;KACF,MAAM,QAAQ,IAAI,gBAAgB;KAClC,IAAI;KACJ,IAAI,OAAO,KAAK,WAAW,YAAY,KAAK,WAAW,OAAO;MAC5D,MAAM,EAAE,MAAM,YAAY,MAAM,QAAQ,eAAe,KAAK,MAAM;MAClE,SAAS,MAAM,MAAM,YACnB;OAAE,MAAM,KAAK;OAAM,MAAM,KAAK;OAAM,UAAU,KAAK;OAAU,UAAU,KAAK;MAAS,GACrF,SACA,KAAK,aACP;KACF,OAAO;MACL,MAAM,UAAW,KAAK,WAAW,CAAC;MAClC,IAAI,KAAK,aAAa,cAAc,OAAO,QAAQ,aAAa,UAAU;OACxE,SAAS,KAAK,KAAK;QAAE,IAAI;QAAO,OAAO;OAAuC,CAAC;OAC/E;MACF;MACA,IAAI,KAAK,aAAa,SAAS,OAAO,QAAQ,eAAe,UAAU;OACrE,SAAS,KAAK,KAAK;QAAE,IAAI;QAAO,OAAO;OAA0C,CAAC;OAClF;MACF;MACA,MAAM,WAAW,KAAK,aAAa,QAAQ,QAAQ;MACnD,SAAS,MAAM,MAAM,YACnB;OAAE,MAAO,KAAK,KAAgB,KAAK;OAAG,MAAM,KAAK;OAAgB,UAAW,KAAK,SAAoB,KAAK;OAAG;MAAS,GACtH,OACF;KACF;KACA,SAAS,KAAK,KAAK;MAAE,IAAI;MAAM,OAAO;KAAO,CAAC;IAChD,SAAS,OAAO;KACd,IAAI,iBAAiB,uBAAuB;MAC1C,SAAS,KAAK,KAAK;OAAE,IAAI;OAAO,OAAO;MAAW,CAAC;MACnD;KACF;KACA,iBAAiB,KAAK,KAAK;IAC7B;IACA;GACF;GACA,IAAI,IAAI,WAAW,UAAU,WAAW,KAAA,GAAW;IACjD,MAAM,OAAO,MAAM,SAAS,GAAG;IAC/B,IAAI,CAAC,MAAM;KACT,SAAS,KAAK,KAAK;MAAE,IAAI;MAAO,OAAO;KAA4B,CAAC;KACpE;IACF;IACA,MAAM,EAAE,QAAQ,YAAY,UAAU,IAAI;IAC1C,IAAI;KACF,SAAS,KAAK,KAAK;MAAE,IAAI;MAAM,MAAM,MAAM,QAAQ,OAAO,QAAQ,OAAO;KAAE,CAAC;IAC9E,SAAS,OAAO;KACd,YAAY,KAAK,KAAK;IACxB;IACA;GACF;GAEA,IAAI,CAAC,QAAQ;IACX,SAAS,KAAK,KAAK;KAAE,IAAI;KAAO,OAAO;IAAqB,CAAC;IAC7D;GACF;GAEA,IAAI,WAAW,KAAA,GAAW;IACxB,IAAI,SAAS,WAAW,KAAK,IAAI,WAAW,QAAQ;KAClD,SAAS,KAAK,KAAK;MAAE,IAAI;MAAO,OAAO;KAAqB,CAAC;KAC7D;IACF;IACA,IAAI,CAAC,WAAW;KACd,SAAS,KAAK,KAAK;MAAE,IAAI;MAAO,OAAO;KAAiC,CAAC;KACzE;IACF;IACA,IAAI,WAAW,SAAS;KACtB,IAAI;MAEF,SAAS,KAAK,KAAK;OAAE,IAAI;OAAM,WAAA,MADP,UAAU,MAAM,MAAM;MACL,CAAC;KAC5C,SAAS,OAAO;MACd,iBAAiB,KAAK,KAAK;KAC7B;KACA;IACF;IACA,IAAI,WAAW,kBAAkB;KAC/B,IAAI;MACF,MAAM,UAAU,qBAAqB,MAAM;MAE3C,SAAS,KAAK,KAAK;OAAE,IAAI;OAAM,MAAA,MADZ,QAAQ,IAAI,MAAM;MACD,CAAC;KACvC,SAAS,OAAO;MACd,iBAAiB,KAAK,KAAK;KAC7B;KACA;IACF;IACA,IAAI,WAAW,OAAO;KACpB,IAAI,CAAC,aAAa;MAChB,SAAS,KAAK,KAAK;OAAE,IAAI;OAAO,OAAO;MAA+B,CAAC;MACvE;KACF;KACA,IAAI;MACF,MAAM,OAAO,MAAM,QAAQ,IAAI,MAAM;MACrC,IAAI,CAAC,MAAM;OACT,SAAS,KAAK,KAAK;QAAE,IAAI;QAAO,OAAO;OAAsB,CAAC;OAC9D;MACF;MACA,IAAI,CAAC,KAAK,SAAS;OACjB,SAAS,KAAK,KAAK;QAAE,IAAI;QAAO,MAAM;QAAiB,OAAO;OAAW,CAAC;OAC1E;MACF;MACA,YAAiB,IAAI,MAAM,CAAC,CAAC,YAAY,KAAA,CAAS;MAClD,SAAS,KAAK,KAAK;OAAE,IAAI;OAAM,UAAU;MAAK,CAAC;KACjD,SAAS,OAAO;MACd,eAAe,KAAK,KAAK;KAC3B;KACA;IACF;IACA,SAAS,KAAK,KAAK;KAAE,IAAI;KAAO,OAAO;IAAY,CAAC;IACpD;GACF;GAEA,IAAI,IAAI,WAAW,OAAO;IACxB,IAAI;KACF,MAAM,OAAO,MAAM,QAAQ,IAAI,MAAM;KACrC,IAAI,CAAC,MAAM;MACT,SAAS,KAAK,KAAK;OAAE,IAAI;OAAO,OAAO;MAAsB,CAAC;MAC9D;KACF;KACA,SAAS,KAAK,KAAK;MAAE,IAAI;MAAM;KAAK,CAAC;IACvC,QAAQ;KACN,SAAS,KAAK,KAAK;MAAE,IAAI;MAAO,OAAO;KAA0B,CAAC;IACpE;IACA;GACF;GAEA,IAAI,IAAI,WAAW,SAAS;IAC1B,MAAM,OAAO,MAAM,SAAS,GAAG;IAC/B,IAAI,CAAC,MAAM;KACT,SAAS,KAAK,KAAK;MAAE,IAAI;MAAO,OAAO;KAA4B,CAAC;KACpE;IACF;IACA,MAAM,EAAE,QAAQ,YAAY,UAAU,IAAI;IAC1C,IAAI;KACF,SAAS,KAAK,KAAK;MAAE,IAAI;MAAM,MAAM,MAAM,QAAQ,OAAO,QAAQ,QAAQ,OAAO;KAAE,CAAC;IACtF,SAAS,OAAO;KACd,YAAY,KAAK,KAAK;IACxB;IACA;GACF;GAEA,IAAI,IAAI,WAAW,UAAU;IAC3B,IAAI;KACF,IAAI,CAAE,MAAM,QAAQ,OAAO,MAAM,GAAI;MACnC,SAAS,KAAK,KAAK;OAAE,IAAI;OAAO,OAAO;MAAsB,CAAC;MAC9D;KACF;KACA,SAAS,KAAK,KAAK,EAAE,IAAI,KAAK,CAAC;IACjC,QAAQ;KACN,SAAS,KAAK,KAAK;MAAE,IAAI;MAAO,OAAO;KAA2B,CAAC;IACrE;IACA;GACF;GAEA,SAAS,KAAK,KAAK;IAAE,IAAI;IAAO,OAAO;GAAqB,CAAC;EAC/D;CACF,CAAC,CACH;AACF"}
1
+ {"version":3,"file":"routes.js","names":[],"sources":["../../../src/core/sync/routes.ts"],"sourcesContent":["import type { IncomingMessage, ServerResponse } from 'node:http'\nimport type { Context } from '@deepseek-ai/cordis'\nimport type {} from '@deepseek-ai/dsh-host-webserver'\nimport { SyncHostKeyChallengeError, SyncHostKeyChallengeExpiredError } from './host-key.js'\nimport { SyncPeerInputError, SyncPeerManager, SyncPeerNotFoundError } from './peer-manager.js'\nimport { SyncRemoteProbe, SyncSshProbeError, SyncSshTransport } from './ssh-transport.js'\nimport { SyncCoordinator, SyncCoordinatorError } from './coordinator.js'\n\nconst MAX_BODY_BYTES = 128 * 1024\n\nfunction sendJson(res: ServerResponse, status: number, body: unknown): void {\n res.writeHead(status, { 'content-type': 'application/json' })\n res.end(JSON.stringify(body))\n}\n\nfunction readBody(req: IncomingMessage): Promise<Record<string, unknown> | null> {\n return new Promise((resolve) => {\n const contentType = req.headers['content-type']?.split(';', 1)[0]?.trim().toLowerCase()\n if (contentType !== 'application/json') {\n resolve(null)\n return\n }\n\n const chunks: Buffer[] = []\n let size = 0\n let settled = false\n const fail = (): void => {\n if (settled) return\n settled = true\n resolve(null)\n }\n req.on('data', (chunk: Buffer) => {\n if (settled) return\n size += chunk.length\n if (size > MAX_BODY_BYTES) {\n fail()\n req.destroy()\n return\n }\n chunks.push(chunk)\n })\n req.on('end', () => {\n if (settled) return\n try {\n const value: unknown = JSON.parse(Buffer.concat(chunks).toString('utf8'))\n if (value === null || typeof value !== 'object' || Array.isArray(value)) {\n fail()\n return\n }\n settled = true\n resolve(value as Record<string, unknown>)\n } catch {\n fail()\n }\n })\n req.on('error', fail)\n })\n}\n\nfunction pathSegments(req: IncomingMessage): string[] {\n return new URL(req.url ?? '/', 'http://x').pathname.split('/').filter(Boolean)\n}\n\nfunction splitBody(body: Record<string, unknown>): { fields: Record<string, unknown>; secrets: unknown } {\n const { secrets, ...fields } = body\n return { fields, secrets }\n}\n\nfunction sendFailure(res: ServerResponse, error: unknown): void {\n if (error instanceof SyncPeerInputError) {\n sendJson(res, 400, { ok: false, error: error.message })\n return\n }\n if (error instanceof SyncPeerNotFoundError) {\n sendJson(res, 404, { ok: false, error: 'sync peer not found' })\n return\n }\n sendJson(res, 500, { ok: false, error: 'sync peer operation failed' })\n}\n\nfunction sendProbeFailure(res: ServerResponse, error: unknown): void {\n if (error instanceof SyncHostKeyChallengeError) {\n sendJson(res, 409, {\n ok: false,\n code: `host-key-${error.decision}`,\n fingerprint: error.fingerprint,\n ...(error.expected ? { expected: error.expected } : {}),\n })\n return\n }\n if (error instanceof SyncHostKeyChallengeExpiredError) {\n sendJson(res, 409, { ok: false, code: 'host-key-challenge-expired', error: error.message })\n return\n }\n if (error instanceof SyncPeerNotFoundError) {\n sendJson(res, 404, { ok: false, error: 'sync peer not found' })\n return\n }\n if (error instanceof SyncSshProbeError) {\n const status = error.category === 'auth' ? 502 : error.category === 'protocol' || error.category === 'hostkey' ? 409 : 503\n sendJson(res, status, { ok: false, code: `ssh-${error.category}`, error: error.message })\n return\n }\n sendJson(res, 500, { ok: false, error: 'sync peer probe failed' })\n}\n\nfunction sendRunFailure(res: ServerResponse, error: unknown): void {\n if (error instanceof SyncCoordinatorError) {\n const status = error.code === 'peer-not-found' ? 404\n : error.code === 'peer-disabled' || error.code === 'sessions-unavailable' || error.code === 'revision-conflict' ? 409\n : error.code === 'session-bundle-too-large' || error.code === 'session-too-large' ? 413\n : error.code === 'session-workspace-unmapped' || error.code === 'session-format' || error.code === 'session-revision-conflict' || error.code === 'session-same-instance' ? 409\n : error.code.startsWith('ssh-') ? 502 : 500\n sendJson(res, status, { ok: false, code: error.code, error: error.message })\n return\n }\n sendJson(res, 500, { ok: false, error: 'sync run failed' })\n}\n\nexport function registerSyncPeerRoutes(\n ctx: Context,\n manager: SyncPeerManager,\n transport?: SyncSshTransport,\n coordinator?: SyncCoordinator,\n): void {\n ctx.effect(() =>\n ctx.webServer.register({\n kind: 'prefix',\n path: '/api/dshb/sync/peers',\n handler: async (req, res) => {\n const segments = pathSegments(req)\n const peerId = segments[4]\n const action = segments[5]\n if (segments.length > 6) {\n sendJson(res, 404, { ok: false, error: 'not found' })\n return\n }\n\n if (req.method === 'GET' && peerId === undefined) {\n try {\n sendJson(res, 200, { ok: true, peers: await manager.list() })\n } catch {\n sendJson(res, 500, { ok: false, error: 'sync peer list unavailable' })\n }\n return\n }\n\n if (req.method === 'POST' && peerId === 'probe-remote') {\n // 远端配置探测:① peerId 模式——用已保存对端的存储凭据;② 显式模式——\n // 用表单填写的 SSH 连接信息(新建对端场景)。\n const body = await readBody(req)\n if (!body || (typeof body.peerId !== 'string'\n && (typeof body.host !== 'string' || !body.host.trim()\n || typeof body.username !== 'string' || !body.username.trim()\n || !Number.isInteger(body.port) || (body.port as number) < 1 || (body.port as number) > 65535\n || (body.authKind !== 'password' && body.authKind !== 'key')))) {\n sendJson(res, 400, { ok: false, error: 'either peerId or host/port/username/authKind are required' })\n return\n }\n try {\n const probe = new SyncRemoteProbe()\n let result\n if (typeof body.peerId === 'string' && body.peerId !== 'new') {\n const { peer, secrets } = await manager.connectionData(body.peerId)\n result = await probe.probeRemote(\n { host: peer.host, port: peer.port, username: peer.username, authKind: peer.authKind },\n secrets,\n peer.remoteWebPort,\n )\n } else {\n const secrets = (body.secrets ?? {}) as Record<string, unknown>\n if (body.authKind === 'password' && typeof secrets.password !== 'string') {\n sendJson(res, 400, { ok: false, error: 'SSH password is required for probing' })\n return\n }\n if (body.authKind === 'key' && typeof secrets.privateKey !== 'string') {\n sendJson(res, 400, { ok: false, error: 'SSH private key is required for probing' })\n return\n }\n const authKind = body.authKind === 'key' ? 'key' : 'password'\n result = await probe.probeRemote(\n { host: (body.host as string).trim(), port: body.port as number, username: (body.username as string).trim(), authKind },\n secrets as { password?: string; privateKey?: string; passphrase?: string },\n )\n }\n sendJson(res, 200, { ok: true, probe: result })\n } catch (error) {\n if (error instanceof SyncPeerNotFoundError) {\n sendJson(res, 404, { ok: false, error: '同步节点已不存在' })\n return\n }\n sendProbeFailure(res, error)\n }\n return\n }\n if (req.method === 'POST' && peerId === undefined) {\n const body = await readBody(req)\n if (!body) {\n sendJson(res, 400, { ok: false, error: 'request body must be JSON' })\n return\n }\n const { fields, secrets } = splitBody(body)\n try {\n sendJson(res, 201, { ok: true, peer: await manager.create(fields, secrets) })\n } catch (error) {\n sendFailure(res, error)\n }\n return\n }\n\n if (!peerId) {\n sendJson(res, 405, { ok: false, error: 'method not allowed' })\n return\n }\n\n if (action !== undefined) {\n if (segments.length !== 6 || req.method !== 'POST') {\n sendJson(res, 405, { ok: false, error: 'method not allowed' })\n return\n }\n if (!transport) {\n sendJson(res, 503, { ok: false, error: 'SSH sync transport unavailable' })\n return\n }\n if (action === 'probe') {\n try {\n const handshake = await transport.probe(peerId)\n sendJson(res, 200, { ok: true, handshake })\n } catch (error) {\n sendProbeFailure(res, error)\n }\n return\n }\n if (action === 'trust-host-key') {\n try {\n await transport.trustObservedHostKey(peerId)\n const peer = await manager.get(peerId)\n sendJson(res, 200, { ok: true, peer })\n } catch (error) {\n sendProbeFailure(res, error)\n }\n return\n }\n if (action === 'run') {\n if (!coordinator) {\n sendJson(res, 503, { ok: false, error: 'sync coordinator unavailable' })\n return\n }\n try {\n const peer = await manager.get(peerId)\n if (!peer) {\n sendJson(res, 404, { ok: false, error: 'sync peer not found' })\n return\n }\n if (!peer.enabled) {\n sendJson(res, 409, { ok: false, code: 'peer-disabled', error: '此同步节点已停用' })\n return\n }\n void coordinator.run(peerId).catch(() => undefined)\n sendJson(res, 202, { ok: true, accepted: true })\n } catch (error) {\n sendRunFailure(res, error)\n }\n return\n }\n if (action === 'stop') {\n // 停止运行中的同步:在最近的安全边界中止,丢弃未完成批次,水位不变。\n if (!coordinator) {\n sendJson(res, 503, { ok: false, error: 'sync coordinator unavailable' })\n return\n }\n try {\n await manager.get(peerId)\n sendJson(res, 200, { ok: true, stopped: coordinator.stop(peerId) })\n } catch (error) {\n sendRunFailure(res, error)\n }\n return\n }\n sendJson(res, 404, { ok: false, error: 'not found' })\n return\n }\n\n if (req.method === 'GET') {\n try {\n const peer = await manager.get(peerId)\n if (!peer) {\n sendJson(res, 404, { ok: false, error: 'sync peer not found' })\n return\n }\n sendJson(res, 200, { ok: true, peer })\n } catch {\n sendJson(res, 500, { ok: false, error: 'sync peer lookup failed' })\n }\n return\n }\n\n if (req.method === 'PATCH') {\n const body = await readBody(req)\n if (!body) {\n sendJson(res, 400, { ok: false, error: 'request body must be JSON' })\n return\n }\n const { fields, secrets } = splitBody(body)\n try {\n sendJson(res, 200, { ok: true, peer: await manager.update(peerId, fields, secrets) })\n } catch (error) {\n sendFailure(res, error)\n }\n return\n }\n\n if (req.method === 'DELETE') {\n try {\n if (!(await manager.remove(peerId))) {\n sendJson(res, 404, { ok: false, error: 'sync peer not found' })\n return\n }\n sendJson(res, 200, { ok: true })\n } catch {\n sendJson(res, 500, { ok: false, error: 'sync peer removal failed' })\n }\n return\n }\n\n sendJson(res, 405, { ok: false, error: 'method not allowed' })\n },\n }),\n )\n}\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- import { t as SyncSessionJournalStore } from "../../session-journal-pgPqcWn9.js";
1
+ import { t as SyncSessionJournalStore } from "../../session-journal-VE_1tA2-.js";
2
2
  export { SyncSessionJournalStore };
@@ -146,6 +146,22 @@ var SyncSessionJournalStore = class {
146
146
  force: true
147
147
  });
148
148
  }
149
+ /**
150
+ * 丢弃一个未完成批次:删除批次记录与暂存目录。用于手动停止同步——
151
+ * 该批未应用的条目不再落地,重启后也不会被 recoverPending 回放。
152
+ * 已应用完成的条目不受影响(它们是完整会话,不是半文件)。
153
+ */
154
+ discard(batchId) {
155
+ if (!/^b-[a-f0-9]{32}$/.test(batchId)) throw new Error("Invalid sync session batch ID");
156
+ const data = this.load();
157
+ if (data.batches[batchId] === void 0) return;
158
+ delete data.batches[batchId];
159
+ this.save();
160
+ rmSync(join(this.home, "dshb", "sync-session-staging", batchId), {
161
+ recursive: true,
162
+ force: true
163
+ });
164
+ }
149
165
  cleanupOrphanedStaging() {
150
166
  const root = join(this.home, "dshb", "sync-session-staging");
151
167
  let entries;
@@ -1 +1 @@
1
- {"version":3,"file":"session-journal.js","names":[],"sources":["../../../src/core/sync/session-journal.ts"],"sourcesContent":["import { createHash, randomBytes } from 'node:crypto'\nimport { chmodSync, closeSync, fsyncSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, rmSync, unlinkSync, writeFileSync } from 'node:fs'\nimport { homedir } from 'node:os'\nimport { join } from 'node:path'\nimport type { Dirent } from 'node:fs'\nimport type { SyncImportMethod } from './model.js'\n\ninterface JournalBatch {\n peerId: string\n sourceName: string\n bundleDigest: string\n startedAt: string\n recordKeys: string[]\n importedVia?: SyncImportMethod\n allowSameInstance?: boolean\n}\n\ninterface JournalData {\n version: 1\n batches: Record<string, JournalBatch>\n}\n\nfunction sessionHome(home?: string): string {\n return home ?? process.env.DSH_HOME ?? join(homedir(), '.dsh')\n}\n\nfunction validBatch(value: unknown): value is JournalBatch {\n if (value === null || typeof value !== 'object' || Array.isArray(value)) return false\n const batch = value as Record<string, unknown>\n return typeof batch.peerId === 'string' && /^p-[a-f0-9]{24}$/.test(batch.peerId) &&\n typeof batch.sourceName === 'string' && batch.sourceName.length <= 128 && !/[\\u0000-\\u001f\\u007f]/.test(batch.sourceName) &&\n typeof batch.bundleDigest === 'string' && /^sha256:[a-f0-9]{64}$/.test(batch.bundleDigest) &&\n typeof batch.startedAt === 'string' && batch.startedAt.length <= 64 &&\n Array.isArray(batch.recordKeys) && batch.recordKeys.every((key) => typeof key === 'string' && key.length <= 8192) &&\n (batch.importedVia === undefined || batch.importedVia === 'peer' || batch.importedVia === 'offline') &&\n (batch.allowSameInstance === undefined || typeof batch.allowSameInstance === 'boolean')\n}\n\nfunction validateJournal(value: unknown): JournalData {\n if (value === null || typeof value !== 'object' || Array.isArray(value)) throw new Error('Invalid sync session journal')\n const journal = value as Record<string, unknown>\n if (journal.version !== 1 || journal.batches === null || typeof journal.batches !== 'object' || Array.isArray(journal.batches)) {\n throw new Error('Invalid sync session journal')\n }\n const batches = Object.create(null) as Record<string, JournalBatch>\n for (const [id, batch] of Object.entries(journal.batches)) {\n if (!/^b-[a-f0-9]{32}$/.test(id) || !validBatch(batch)) throw new Error('Invalid sync session journal batch')\n batches[id] = {\n ...batch,\n importedVia: batch.importedVia ?? 'peer',\n allowSameInstance: batch.allowSameInstance ?? false,\n recordKeys: [...batch.recordKeys],\n }\n }\n return { version: 1, batches }\n}\n\nfunction atomicWrite(path: string, value: unknown): void {\n mkdirSync(join(path, '..'), { recursive: true, mode: 0o700 })\n const temporary = `${path}.${process.pid}.${randomBytes(6).toString('hex')}.tmp`\n let descriptor: number | undefined\n try {\n descriptor = openSync(temporary, 'wx', 0o600)\n writeFileSync(descriptor, `${JSON.stringify(value, null, 2)}\\n`, 'utf8')\n fsyncSync(descriptor)\n closeSync(descriptor)\n descriptor = undefined\n renameSync(temporary, path)\n chmodSync(path, 0o600)\n } catch (error) {\n if (descriptor !== undefined) closeSync(descriptor)\n try {\n unlinkSync(temporary)\n } catch (cleanupError) {\n if (!(cleanupError instanceof Error) || !('code' in cleanupError) || cleanupError.code !== 'ENOENT') {\n throw new AggregateError([error, cleanupError], 'session journal write and cleanup failed')\n }\n }\n throw error\n }\n}\n\nexport class SyncSessionJournalStore {\n private readonly home: string\n private readonly filePath: string\n private data: JournalData | undefined\n\n constructor(home?: string) {\n this.home = sessionHome(home)\n this.filePath = join(this.home, 'dshb', 'sync-session-journal.json')\n }\n\n begin(\n peerId: string,\n sourceName: string,\n bundleDigest: string,\n recordKeys: readonly string[],\n startedAt = new Date().toISOString(),\n importedVia: SyncImportMethod = 'peer',\n allowSameInstance = false,\n ): string {\n const batchId = `b-${createHash('sha256').update(JSON.stringify([peerId, bundleDigest])).digest('hex').slice(0, 32)}`\n const data = this.load()\n const existing = data.batches[batchId]\n const batch: JournalBatch = {\n peerId,\n sourceName,\n bundleDigest,\n startedAt: existing?.startedAt ?? startedAt,\n recordKeys: [...new Set([...(existing?.recordKeys ?? []), ...recordKeys])],\n importedVia,\n allowSameInstance,\n }\n data.batches[batchId] = batch\n this.save()\n return batchId\n }\n\n beginWithBundle(\n peerId: string,\n sourceName: string,\n bundleDigest: string,\n recordKeys: readonly string[],\n bundle: unknown,\n startedAt = new Date().toISOString(),\n importedVia: SyncImportMethod = 'peer',\n allowSameInstance = false,\n ): string {\n const batchId = `b-${createHash('sha256').update(JSON.stringify([peerId, bundleDigest])).digest('hex').slice(0, 32)}`\n this.writeBundle(batchId, bundle)\n const registered = this.begin(\n peerId,\n sourceName,\n bundleDigest,\n recordKeys,\n startedAt,\n importedVia,\n allowSameInstance,\n )\n if (registered !== batchId) throw new Error('Session journal batch identity changed')\n return batchId\n }\n\n pendingPeerIds(): string[] {\n return [...new Set(Object.values(this.load().batches).map((batch) => batch.peerId))]\n }\n\n pendingBatches(): Array<{ batchId: string; peerId: string; sourceName: string; bundleDigest: string; importedVia: SyncImportMethod; allowSameInstance: boolean }> {\n return Object.entries(this.load().batches).map(([batchId, batch]) => ({\n batchId,\n peerId: batch.peerId,\n sourceName: batch.sourceName,\n bundleDigest: batch.bundleDigest,\n importedVia: batch.importedVia ?? 'peer',\n allowSameInstance: batch.allowSameInstance ?? false,\n }))\n }\n\n findBatch(peerId: string, bundleDigest: string): {\n batchId: string\n peerId: string\n sourceName: string\n bundleDigest: string\n importedVia: SyncImportMethod\n allowSameInstance: boolean\n } | undefined {\n const match = Object.entries(this.load().batches).find(([, batch]) =>\n batch.peerId === peerId && batch.bundleDigest === bundleDigest,\n )\n if (!match) return undefined\n const [batchId, batch] = match\n return {\n batchId,\n peerId: batch.peerId,\n sourceName: batch.sourceName,\n bundleDigest: batch.bundleDigest,\n importedVia: batch.importedVia ?? 'peer',\n allowSameInstance: batch.allowSameInstance ?? false,\n }\n }\n\n writeBundle(batchId: string, value: unknown): void {\n if (!/^b-[a-f0-9]{32}$/.test(batchId)) throw new Error('Invalid sync session batch ID')\n atomicWrite(this.bundlePath(batchId), value)\n }\n\n readBundle(batchId: string): unknown | undefined {\n try {\n return JSON.parse(readFileSync(this.bundlePath(batchId), 'utf8')) as unknown\n } catch (error) {\n if (error instanceof Error && 'code' in error && error.code === 'ENOENT') return undefined\n throw error\n }\n }\n\n writePrepared(batchId: string, recordKey: string, value: unknown): void {\n if (!/^b-[a-f0-9]{32}$/.test(batchId)) throw new Error('Invalid sync session batch ID')\n const file = this.preparedPath(batchId, recordKey)\n atomicWrite(file, value)\n }\n\n readPrepared(batchId: string, recordKey: string): unknown | undefined {\n const file = this.preparedPath(batchId, recordKey)\n try {\n return JSON.parse(readFileSync(file, 'utf8')) as unknown\n } catch (error) {\n if (error instanceof Error && 'code' in error && error.code === 'ENOENT') return undefined\n throw error\n }\n }\n\n complete(batchId: string): void {\n const data = this.load()\n delete data.batches[batchId]\n this.save()\n rmSync(join(this.home, 'dshb', 'sync-session-staging', batchId), { recursive: true, force: true })\n }\n\n cleanupOrphanedStaging(): void {\n const root = join(this.home, 'dshb', 'sync-session-staging')\n let entries: Dirent[]\n try {\n entries = readdirSync(root, { withFileTypes: true })\n } catch (error) {\n if (error instanceof Error && 'code' in error && error.code === 'ENOENT') return\n throw error\n }\n const activeBatches = new Set(Object.keys(this.load().batches))\n for (const entry of entries) {\n if (entry.isDirectory() && /^b-[a-f0-9]{32}$/.test(entry.name) && !activeBatches.has(entry.name)) {\n rmSync(join(root, entry.name), { recursive: true, force: true })\n }\n }\n }\n\n private preparedPath(batchId: string, recordKey: string): string {\n const digest = createHash('sha256').update(recordKey).digest('hex')\n return join(this.home, 'dshb', 'sync-session-staging', batchId, `${digest}.json`)\n }\n\n private bundlePath(batchId: string): string {\n return join(this.home, 'dshb', 'sync-session-staging', batchId, 'bundle.json')\n }\n\n private load(): JournalData {\n if (this.data) return this.data\n try {\n this.data = validateJournal(JSON.parse(readFileSync(this.filePath, 'utf8')) as unknown)\n } catch (error) {\n if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {\n this.data = { version: 1, batches: {} }\n } else {\n throw error\n }\n }\n return this.data\n }\n\n private save(): void {\n atomicWrite(this.filePath, this.load())\n }\n}\n"],"mappings":";;;;;AAsBA,SAAS,YAAY,MAAuB;CAC1C,OAAO,QAAQ,QAAQ,IAAI,YAAY,KAAK,QAAQ,GAAG,MAAM;AAC/D;AAEA,SAAS,WAAW,OAAuC;CACzD,IAAI,UAAU,QAAQ,OAAO,UAAU,YAAY,MAAM,QAAQ,KAAK,GAAG,OAAO;CAChF,MAAM,QAAQ;CACd,OAAO,OAAO,MAAM,WAAW,YAAY,mBAAmB,KAAK,MAAM,MAAM,KAC7E,OAAO,MAAM,eAAe,YAAY,MAAM,WAAW,UAAU,OAAO,CAAC,wBAAwB,KAAK,MAAM,UAAU,KACxH,OAAO,MAAM,iBAAiB,YAAY,wBAAwB,KAAK,MAAM,YAAY,KACzF,OAAO,MAAM,cAAc,YAAY,MAAM,UAAU,UAAU,MACjE,MAAM,QAAQ,MAAM,UAAU,KAAK,MAAM,WAAW,OAAO,QAAQ,OAAO,QAAQ,YAAY,IAAI,UAAU,IAAI,MAC/G,MAAM,gBAAgB,KAAA,KAAa,MAAM,gBAAgB,UAAU,MAAM,gBAAgB,eACzF,MAAM,sBAAsB,KAAA,KAAa,OAAO,MAAM,sBAAsB;AACjF;AAEA,SAAS,gBAAgB,OAA6B;CACpD,IAAI,UAAU,QAAQ,OAAO,UAAU,YAAY,MAAM,QAAQ,KAAK,GAAG,MAAM,IAAI,MAAM,8BAA8B;CACvH,MAAM,UAAU;CAChB,IAAI,QAAQ,YAAY,KAAK,QAAQ,YAAY,QAAQ,OAAO,QAAQ,YAAY,YAAY,MAAM,QAAQ,QAAQ,OAAO,GAC3H,MAAM,IAAI,MAAM,8BAA8B;CAEhD,MAAM,UAAU,OAAO,OAAO,IAAI;CAClC,KAAK,MAAM,CAAC,IAAI,UAAU,OAAO,QAAQ,QAAQ,OAAO,GAAG;EACzD,IAAI,CAAC,mBAAmB,KAAK,EAAE,KAAK,CAAC,WAAW,KAAK,GAAG,MAAM,IAAI,MAAM,oCAAoC;EAC5G,QAAQ,MAAM;GACZ,GAAG;GACH,aAAa,MAAM,eAAe;GAClC,mBAAmB,MAAM,qBAAqB;GAC9C,YAAY,CAAC,GAAG,MAAM,UAAU;EAClC;CACF;CACA,OAAO;EAAE,SAAS;EAAG;CAAQ;AAC/B;AAEA,SAAS,YAAY,MAAc,OAAsB;CACvD,UAAU,KAAK,MAAM,IAAI,GAAG;EAAE,WAAW;EAAM,MAAM;CAAM,CAAC;CAC5D,MAAM,YAAY,GAAG,KAAK,GAAG,QAAQ,IAAI,GAAG,YAAY,CAAC,CAAC,CAAC,SAAS,KAAK,EAAE;CAC3E,IAAI;CACJ,IAAI;EACF,aAAa,SAAS,WAAW,MAAM,GAAK;EAC5C,cAAc,YAAY,GAAG,KAAK,UAAU,OAAO,MAAM,CAAC,EAAE,KAAK,MAAM;EACvE,UAAU,UAAU;EACpB,UAAU,UAAU;EACpB,aAAa,KAAA;EACb,WAAW,WAAW,IAAI;EAC1B,UAAU,MAAM,GAAK;CACvB,SAAS,OAAO;EACd,IAAI,eAAe,KAAA,GAAW,UAAU,UAAU;EAClD,IAAI;GACF,WAAW,SAAS;EACtB,SAAS,cAAc;GACrB,IAAI,EAAE,wBAAwB,UAAU,EAAE,UAAU,iBAAiB,aAAa,SAAS,UACzF,MAAM,IAAI,eAAe,CAAC,OAAO,YAAY,GAAG,0CAA0C;EAE9F;EACA,MAAM;CACR;AACF;AAEA,IAAa,0BAAb,MAAqC;CACnC;CACA;CACA;CAEA,YAAY,MAAe;EACzB,KAAK,OAAO,YAAY,IAAI;EAC5B,KAAK,WAAW,KAAK,KAAK,MAAM,QAAQ,2BAA2B;CACrE;CAEA,MACE,QACA,YACA,cACA,YACA,6BAAY,IAAI,KAAK,EAAA,CAAE,YAAY,GACnC,cAAgC,QAChC,oBAAoB,OACZ;EACR,MAAM,UAAU,KAAK,WAAW,QAAQ,CAAC,CAAC,OAAO,KAAK,UAAU,CAAC,QAAQ,YAAY,CAAC,CAAC,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,MAAM,GAAG,EAAE;EAClH,MAAM,OAAO,KAAK,KAAK;EACvB,MAAM,WAAW,KAAK,QAAQ;EAC9B,MAAM,QAAsB;GAC1B;GACA;GACA;GACA,WAAW,UAAU,aAAa;GAClC,YAAY,CAAC,mBAAG,IAAI,IAAI,CAAC,GAAI,UAAU,cAAc,CAAC,GAAI,GAAG,UAAU,CAAC,CAAC;GACzE;GACA;EACF;EACA,KAAK,QAAQ,WAAW;EACxB,KAAK,KAAK;EACV,OAAO;CACT;CAEA,gBACE,QACA,YACA,cACA,YACA,QACA,6BAAY,IAAI,KAAK,EAAA,CAAE,YAAY,GACnC,cAAgC,QAChC,oBAAoB,OACZ;EACR,MAAM,UAAU,KAAK,WAAW,QAAQ,CAAC,CAAC,OAAO,KAAK,UAAU,CAAC,QAAQ,YAAY,CAAC,CAAC,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,MAAM,GAAG,EAAE;EAClH,KAAK,YAAY,SAAS,MAAM;EAUhC,IATmB,KAAK,MACtB,QACA,YACA,cACA,YACA,WACA,aACA,iBAEW,MAAM,SAAS,MAAM,IAAI,MAAM,wCAAwC;EACpF,OAAO;CACT;CAEA,iBAA2B;EACzB,OAAO,CAAC,GAAG,IAAI,IAAI,OAAO,OAAO,KAAK,KAAK,CAAC,CAAC,OAAO,CAAC,CAAC,KAAK,UAAU,MAAM,MAAM,CAAC,CAAC;CACrF;CAEA,iBAAkK;EAChK,OAAO,OAAO,QAAQ,KAAK,KAAK,CAAC,CAAC,OAAO,CAAC,CAAC,KAAK,CAAC,SAAS,YAAY;GACpE;GACA,QAAQ,MAAM;GACd,YAAY,MAAM;GAClB,cAAc,MAAM;GACpB,aAAa,MAAM,eAAe;GAClC,mBAAmB,MAAM,qBAAqB;EAChD,EAAE;CACJ;CAEA,UAAU,QAAgB,cAOZ;EACZ,MAAM,QAAQ,OAAO,QAAQ,KAAK,KAAK,CAAC,CAAC,OAAO,CAAC,CAAC,MAAM,GAAG,WACzD,MAAM,WAAW,UAAU,MAAM,iBAAiB,YACpD;EACA,IAAI,CAAC,OAAO,OAAO,KAAA;EACnB,MAAM,CAAC,SAAS,SAAS;EACzB,OAAO;GACL;GACA,QAAQ,MAAM;GACd,YAAY,MAAM;GAClB,cAAc,MAAM;GACpB,aAAa,MAAM,eAAe;GAClC,mBAAmB,MAAM,qBAAqB;EAChD;CACF;CAEA,YAAY,SAAiB,OAAsB;EACjD,IAAI,CAAC,mBAAmB,KAAK,OAAO,GAAG,MAAM,IAAI,MAAM,+BAA+B;EACtF,YAAY,KAAK,WAAW,OAAO,GAAG,KAAK;CAC7C;CAEA,WAAW,SAAsC;EAC/C,IAAI;GACF,OAAO,KAAK,MAAM,aAAa,KAAK,WAAW,OAAO,GAAG,MAAM,CAAC;EAClE,SAAS,OAAO;GACd,IAAI,iBAAiB,SAAS,UAAU,SAAS,MAAM,SAAS,UAAU,OAAO,KAAA;GACjF,MAAM;EACR;CACF;CAEA,cAAc,SAAiB,WAAmB,OAAsB;EACtE,IAAI,CAAC,mBAAmB,KAAK,OAAO,GAAG,MAAM,IAAI,MAAM,+BAA+B;EAEtF,YADa,KAAK,aAAa,SAAS,SACzB,GAAG,KAAK;CACzB;CAEA,aAAa,SAAiB,WAAwC;EACpE,MAAM,OAAO,KAAK,aAAa,SAAS,SAAS;EACjD,IAAI;GACF,OAAO,KAAK,MAAM,aAAa,MAAM,MAAM,CAAC;EAC9C,SAAS,OAAO;GACd,IAAI,iBAAiB,SAAS,UAAU,SAAS,MAAM,SAAS,UAAU,OAAO,KAAA;GACjF,MAAM;EACR;CACF;CAEA,SAAS,SAAuB;EAC9B,MAAM,OAAO,KAAK,KAAK;EACvB,OAAO,KAAK,QAAQ;EACpB,KAAK,KAAK;EACV,OAAO,KAAK,KAAK,MAAM,QAAQ,wBAAwB,OAAO,GAAG;GAAE,WAAW;GAAM,OAAO;EAAK,CAAC;CACnG;CAEA,yBAA+B;EAC7B,MAAM,OAAO,KAAK,KAAK,MAAM,QAAQ,sBAAsB;EAC3D,IAAI;EACJ,IAAI;GACF,UAAU,YAAY,MAAM,EAAE,eAAe,KAAK,CAAC;EACrD,SAAS,OAAO;GACd,IAAI,iBAAiB,SAAS,UAAU,SAAS,MAAM,SAAS,UAAU;GAC1E,MAAM;EACR;EACA,MAAM,gBAAgB,IAAI,IAAI,OAAO,KAAK,KAAK,KAAK,CAAC,CAAC,OAAO,CAAC;EAC9D,KAAK,MAAM,SAAS,SAClB,IAAI,MAAM,YAAY,KAAK,mBAAmB,KAAK,MAAM,IAAI,KAAK,CAAC,cAAc,IAAI,MAAM,IAAI,GAC7F,OAAO,KAAK,MAAM,MAAM,IAAI,GAAG;GAAE,WAAW;GAAM,OAAO;EAAK,CAAC;CAGrE;CAEA,aAAqB,SAAiB,WAA2B;EAC/D,MAAM,SAAS,WAAW,QAAQ,CAAC,CAAC,OAAO,SAAS,CAAC,CAAC,OAAO,KAAK;EAClE,OAAO,KAAK,KAAK,MAAM,QAAQ,wBAAwB,SAAS,GAAG,OAAO,MAAM;CAClF;CAEA,WAAmB,SAAyB;EAC1C,OAAO,KAAK,KAAK,MAAM,QAAQ,wBAAwB,SAAS,aAAa;CAC/E;CAEA,OAA4B;EAC1B,IAAI,KAAK,MAAM,OAAO,KAAK;EAC3B,IAAI;GACF,KAAK,OAAO,gBAAgB,KAAK,MAAM,aAAa,KAAK,UAAU,MAAM,CAAC,CAAY;EACxF,SAAS,OAAO;GACd,IAAI,iBAAiB,SAAS,UAAU,SAAS,MAAM,SAAS,UAC9D,KAAK,OAAO;IAAE,SAAS;IAAG,SAAS,CAAC;GAAE;QAEtC,MAAM;EAEV;EACA,OAAO,KAAK;CACd;CAEA,OAAqB;EACnB,YAAY,KAAK,UAAU,KAAK,KAAK,CAAC;CACxC;AACF"}
1
+ {"version":3,"file":"session-journal.js","names":[],"sources":["../../../src/core/sync/session-journal.ts"],"sourcesContent":["import { createHash, randomBytes } from 'node:crypto'\nimport { chmodSync, closeSync, fsyncSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, rmSync, unlinkSync, writeFileSync } from 'node:fs'\nimport { homedir } from 'node:os'\nimport { join } from 'node:path'\nimport type { Dirent } from 'node:fs'\nimport type { SyncImportMethod } from './model.js'\n\ninterface JournalBatch {\n peerId: string\n sourceName: string\n bundleDigest: string\n startedAt: string\n recordKeys: string[]\n importedVia?: SyncImportMethod\n allowSameInstance?: boolean\n}\n\ninterface JournalData {\n version: 1\n batches: Record<string, JournalBatch>\n}\n\nfunction sessionHome(home?: string): string {\n return home ?? process.env.DSH_HOME ?? join(homedir(), '.dsh')\n}\n\nfunction validBatch(value: unknown): value is JournalBatch {\n if (value === null || typeof value !== 'object' || Array.isArray(value)) return false\n const batch = value as Record<string, unknown>\n return typeof batch.peerId === 'string' && /^p-[a-f0-9]{24}$/.test(batch.peerId) &&\n typeof batch.sourceName === 'string' && batch.sourceName.length <= 128 && !/[\\u0000-\\u001f\\u007f]/.test(batch.sourceName) &&\n typeof batch.bundleDigest === 'string' && /^sha256:[a-f0-9]{64}$/.test(batch.bundleDigest) &&\n typeof batch.startedAt === 'string' && batch.startedAt.length <= 64 &&\n Array.isArray(batch.recordKeys) && batch.recordKeys.every((key) => typeof key === 'string' && key.length <= 8192) &&\n (batch.importedVia === undefined || batch.importedVia === 'peer' || batch.importedVia === 'offline') &&\n (batch.allowSameInstance === undefined || typeof batch.allowSameInstance === 'boolean')\n}\n\nfunction validateJournal(value: unknown): JournalData {\n if (value === null || typeof value !== 'object' || Array.isArray(value)) throw new Error('Invalid sync session journal')\n const journal = value as Record<string, unknown>\n if (journal.version !== 1 || journal.batches === null || typeof journal.batches !== 'object' || Array.isArray(journal.batches)) {\n throw new Error('Invalid sync session journal')\n }\n const batches = Object.create(null) as Record<string, JournalBatch>\n for (const [id, batch] of Object.entries(journal.batches)) {\n if (!/^b-[a-f0-9]{32}$/.test(id) || !validBatch(batch)) throw new Error('Invalid sync session journal batch')\n batches[id] = {\n ...batch,\n importedVia: batch.importedVia ?? 'peer',\n allowSameInstance: batch.allowSameInstance ?? false,\n recordKeys: [...batch.recordKeys],\n }\n }\n return { version: 1, batches }\n}\n\nfunction atomicWrite(path: string, value: unknown): void {\n mkdirSync(join(path, '..'), { recursive: true, mode: 0o700 })\n const temporary = `${path}.${process.pid}.${randomBytes(6).toString('hex')}.tmp`\n let descriptor: number | undefined\n try {\n descriptor = openSync(temporary, 'wx', 0o600)\n writeFileSync(descriptor, `${JSON.stringify(value, null, 2)}\\n`, 'utf8')\n fsyncSync(descriptor)\n closeSync(descriptor)\n descriptor = undefined\n renameSync(temporary, path)\n chmodSync(path, 0o600)\n } catch (error) {\n if (descriptor !== undefined) closeSync(descriptor)\n try {\n unlinkSync(temporary)\n } catch (cleanupError) {\n if (!(cleanupError instanceof Error) || !('code' in cleanupError) || cleanupError.code !== 'ENOENT') {\n throw new AggregateError([error, cleanupError], 'session journal write and cleanup failed')\n }\n }\n throw error\n }\n}\n\nexport class SyncSessionJournalStore {\n private readonly home: string\n private readonly filePath: string\n private data: JournalData | undefined\n\n constructor(home?: string) {\n this.home = sessionHome(home)\n this.filePath = join(this.home, 'dshb', 'sync-session-journal.json')\n }\n\n begin(\n peerId: string,\n sourceName: string,\n bundleDigest: string,\n recordKeys: readonly string[],\n startedAt = new Date().toISOString(),\n importedVia: SyncImportMethod = 'peer',\n allowSameInstance = false,\n ): string {\n const batchId = `b-${createHash('sha256').update(JSON.stringify([peerId, bundleDigest])).digest('hex').slice(0, 32)}`\n const data = this.load()\n const existing = data.batches[batchId]\n const batch: JournalBatch = {\n peerId,\n sourceName,\n bundleDigest,\n startedAt: existing?.startedAt ?? startedAt,\n recordKeys: [...new Set([...(existing?.recordKeys ?? []), ...recordKeys])],\n importedVia,\n allowSameInstance,\n }\n data.batches[batchId] = batch\n this.save()\n return batchId\n }\n\n beginWithBundle(\n peerId: string,\n sourceName: string,\n bundleDigest: string,\n recordKeys: readonly string[],\n bundle: unknown,\n startedAt = new Date().toISOString(),\n importedVia: SyncImportMethod = 'peer',\n allowSameInstance = false,\n ): string {\n const batchId = `b-${createHash('sha256').update(JSON.stringify([peerId, bundleDigest])).digest('hex').slice(0, 32)}`\n this.writeBundle(batchId, bundle)\n const registered = this.begin(\n peerId,\n sourceName,\n bundleDigest,\n recordKeys,\n startedAt,\n importedVia,\n allowSameInstance,\n )\n if (registered !== batchId) throw new Error('Session journal batch identity changed')\n return batchId\n }\n\n pendingPeerIds(): string[] {\n return [...new Set(Object.values(this.load().batches).map((batch) => batch.peerId))]\n }\n\n pendingBatches(): Array<{ batchId: string; peerId: string; sourceName: string; bundleDigest: string; importedVia: SyncImportMethod; allowSameInstance: boolean }> {\n return Object.entries(this.load().batches).map(([batchId, batch]) => ({\n batchId,\n peerId: batch.peerId,\n sourceName: batch.sourceName,\n bundleDigest: batch.bundleDigest,\n importedVia: batch.importedVia ?? 'peer',\n allowSameInstance: batch.allowSameInstance ?? false,\n }))\n }\n\n findBatch(peerId: string, bundleDigest: string): {\n batchId: string\n peerId: string\n sourceName: string\n bundleDigest: string\n importedVia: SyncImportMethod\n allowSameInstance: boolean\n } | undefined {\n const match = Object.entries(this.load().batches).find(([, batch]) =>\n batch.peerId === peerId && batch.bundleDigest === bundleDigest,\n )\n if (!match) return undefined\n const [batchId, batch] = match\n return {\n batchId,\n peerId: batch.peerId,\n sourceName: batch.sourceName,\n bundleDigest: batch.bundleDigest,\n importedVia: batch.importedVia ?? 'peer',\n allowSameInstance: batch.allowSameInstance ?? false,\n }\n }\n\n writeBundle(batchId: string, value: unknown): void {\n if (!/^b-[a-f0-9]{32}$/.test(batchId)) throw new Error('Invalid sync session batch ID')\n atomicWrite(this.bundlePath(batchId), value)\n }\n\n readBundle(batchId: string): unknown | undefined {\n try {\n return JSON.parse(readFileSync(this.bundlePath(batchId), 'utf8')) as unknown\n } catch (error) {\n if (error instanceof Error && 'code' in error && error.code === 'ENOENT') return undefined\n throw error\n }\n }\n\n writePrepared(batchId: string, recordKey: string, value: unknown): void {\n if (!/^b-[a-f0-9]{32}$/.test(batchId)) throw new Error('Invalid sync session batch ID')\n const file = this.preparedPath(batchId, recordKey)\n atomicWrite(file, value)\n }\n\n readPrepared(batchId: string, recordKey: string): unknown | undefined {\n const file = this.preparedPath(batchId, recordKey)\n try {\n return JSON.parse(readFileSync(file, 'utf8')) as unknown\n } catch (error) {\n if (error instanceof Error && 'code' in error && error.code === 'ENOENT') return undefined\n throw error\n }\n }\n\n complete(batchId: string): void {\n const data = this.load()\n delete data.batches[batchId]\n this.save()\n rmSync(join(this.home, 'dshb', 'sync-session-staging', batchId), { recursive: true, force: true })\n }\n\n /**\n * 丢弃一个未完成批次:删除批次记录与暂存目录。用于手动停止同步——\n * 该批未应用的条目不再落地,重启后也不会被 recoverPending 回放。\n * 已应用完成的条目不受影响(它们是完整会话,不是半文件)。\n */\n discard(batchId: string): void {\n if (!/^b-[a-f0-9]{32}$/.test(batchId)) throw new Error('Invalid sync session batch ID')\n const data = this.load()\n if (data.batches[batchId] === undefined) return\n delete data.batches[batchId]\n this.save()\n rmSync(join(this.home, 'dshb', 'sync-session-staging', batchId), { recursive: true, force: true })\n }\n\n cleanupOrphanedStaging(): void {\n const root = join(this.home, 'dshb', 'sync-session-staging')\n let entries: Dirent[]\n try {\n entries = readdirSync(root, { withFileTypes: true })\n } catch (error) {\n if (error instanceof Error && 'code' in error && error.code === 'ENOENT') return\n throw error\n }\n const activeBatches = new Set(Object.keys(this.load().batches))\n for (const entry of entries) {\n if (entry.isDirectory() && /^b-[a-f0-9]{32}$/.test(entry.name) && !activeBatches.has(entry.name)) {\n rmSync(join(root, entry.name), { recursive: true, force: true })\n }\n }\n }\n\n private preparedPath(batchId: string, recordKey: string): string {\n const digest = createHash('sha256').update(recordKey).digest('hex')\n return join(this.home, 'dshb', 'sync-session-staging', batchId, `${digest}.json`)\n }\n\n private bundlePath(batchId: string): string {\n return join(this.home, 'dshb', 'sync-session-staging', batchId, 'bundle.json')\n }\n\n private load(): JournalData {\n if (this.data) return this.data\n try {\n this.data = validateJournal(JSON.parse(readFileSync(this.filePath, 'utf8')) as unknown)\n } catch (error) {\n if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {\n this.data = { version: 1, batches: {} }\n } else {\n throw error\n }\n }\n return this.data\n }\n\n private save(): void {\n atomicWrite(this.filePath, this.load())\n }\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { h as SyncSessionService } from "../../session-sync-BuOJvFxK.js";
1
+ import { h as SyncSessionService } from "../../session-sync-DrKShc5c.js";
2
2
  import { Context } from "@deepseek-ai/cordis";
3
3
  //#region src/core/sync/session-routes.d.ts
4
4
  declare function registerSyncSessionRoutes(ctx: Context, sessions: SyncSessionService): void;
@@ -1,4 +1,4 @@
1
- import { n as SyncSessionError } from "../../session-sync-CIqjuWkh.js";
1
+ import { n as SyncSessionError } from "../../session-sync-DLr_BdvN.js";
2
2
  //#region src/core/sync/session-routes.ts
3
3
  function sendJson(res, status, body) {
4
4
  res.writeHead(status, {
@@ -1,2 +1,2 @@
1
- import { C as sessionEventListDigest, D as validateSyncSessionManifest, E as validateSyncSessionBundle, S as planSessionTransfers, T as validateObjectPayload, _ as SyncSessionWorkspace, a as SyncSessionBundle, b as makeSyncSessionBundle, c as SyncSessionEventDelta, d as SyncSessionManifest, f as SyncSessionManifestEntry, g as SyncSessionTransferPlan, h as SyncSessionService, i as SyncSessionApplyResult, l as SyncSessionEventPrefix, m as SyncSessionObjectInbox, n as SyncObjectInboxWriter, o as SyncSessionEntry, p as SyncSessionObject, r as SyncSessionApplyOptions, s as SyncSessionError, t as ExportObjectCache, u as SyncSessionInstructionSnapshot, v as WorkspaceRegistryLike, w as syncSessionEntryKey, x as makeSyncSessionManifest, y as estimateJsonBytes } from "../../session-sync-BuOJvFxK.js";
1
+ import { C as sessionEventListDigest, D as validateSyncSessionManifest, E as validateSyncSessionBundle, S as planSessionTransfers, T as validateObjectPayload, _ as SyncSessionWorkspace, a as SyncSessionBundle, b as makeSyncSessionBundle, c as SyncSessionEventDelta, d as SyncSessionManifest, f as SyncSessionManifestEntry, g as SyncSessionTransferPlan, h as SyncSessionService, i as SyncSessionApplyResult, l as SyncSessionEventPrefix, m as SyncSessionObjectInbox, n as SyncObjectInboxWriter, o as SyncSessionEntry, p as SyncSessionObject, r as SyncSessionApplyOptions, s as SyncSessionError, t as ExportObjectCache, u as SyncSessionInstructionSnapshot, v as WorkspaceRegistryLike, w as syncSessionEntryKey, x as makeSyncSessionManifest, y as estimateJsonBytes } from "../../session-sync-DrKShc5c.js";
2
2
  export { ExportObjectCache, SyncObjectInboxWriter, SyncSessionApplyOptions, SyncSessionApplyResult, SyncSessionBundle, SyncSessionEntry, SyncSessionError, SyncSessionEventDelta, SyncSessionEventPrefix, SyncSessionInstructionSnapshot, SyncSessionManifest, SyncSessionManifestEntry, SyncSessionObject, SyncSessionObjectInbox, SyncSessionService, SyncSessionTransferPlan, SyncSessionWorkspace, WorkspaceRegistryLike, estimateJsonBytes, makeSyncSessionBundle, makeSyncSessionManifest, planSessionTransfers, sessionEventListDigest, syncSessionEntryKey, validateObjectPayload, validateSyncSessionBundle, validateSyncSessionManifest };
@@ -1,2 +1,2 @@
1
- import { a as estimateJsonBytes, c as planSessionTransfers, d as validateObjectPayload, f as validateSyncSessionBundle, i as SyncSessionService, l as sessionEventListDigest, n as SyncSessionError, o as makeSyncSessionBundle, p as validateSyncSessionManifest, r as SyncSessionObjectInbox, s as makeSyncSessionManifest, t as ExportObjectCache, u as syncSessionEntryKey } from "../../session-sync-CIqjuWkh.js";
1
+ import { a as estimateJsonBytes, c as planSessionTransfers, d as validateObjectPayload, f as validateSyncSessionBundle, i as SyncSessionService, l as sessionEventListDigest, n as SyncSessionError, o as makeSyncSessionBundle, p as validateSyncSessionManifest, r as SyncSessionObjectInbox, s as makeSyncSessionManifest, t as ExportObjectCache, u as syncSessionEntryKey } from "../../session-sync-DLr_BdvN.js";
2
2
  export { ExportObjectCache, SyncSessionError, SyncSessionObjectInbox, SyncSessionService, estimateJsonBytes, makeSyncSessionBundle, makeSyncSessionManifest, planSessionTransfers, sessionEventListDigest, syncSessionEntryKey, validateObjectPayload, validateSyncSessionBundle, validateSyncSessionManifest };
@@ -1,2 +1,2 @@
1
- import { _ as parseRemoteProbeOutput, a as SyncRemoteProbe, c as SyncRemoteProbeProfile, d as SyncSshErrorCategory, f as SyncSshProbeError, g as buildSyncSshConnectConfig, h as buildRemoteProbeScript, i as SyncRemoteNodeManifest, l as SyncRemoteProbeResult, m as SyncSshTransport, n as REMOTE_PROBE_END_MARKER, o as SyncRemoteProbeDraft, p as SyncSshSession, r as SyncRemoteHandshake, s as SyncRemoteProbeHandshake, t as REMOTE_PROBE_BEGIN_MARKER, u as SyncSessionDeltaPrefixError } from "../../ssh-transport-C7povM0s.js";
1
+ import { _ as parseRemoteProbeOutput, a as SyncRemoteProbe, c as SyncRemoteProbeProfile, d as SyncSshErrorCategory, f as SyncSshProbeError, g as buildSyncSshConnectConfig, h as buildRemoteProbeScript, i as SyncRemoteNodeManifest, l as SyncRemoteProbeResult, m as SyncSshTransport, n as REMOTE_PROBE_END_MARKER, o as SyncRemoteProbeDraft, p as SyncSshSession, r as SyncRemoteHandshake, s as SyncRemoteProbeHandshake, t as REMOTE_PROBE_BEGIN_MARKER, u as SyncSessionDeltaPrefixError } from "../../ssh-transport-BxPWfu9X.js";
2
2
  export { REMOTE_PROBE_BEGIN_MARKER, REMOTE_PROBE_END_MARKER, SyncRemoteHandshake, SyncRemoteNodeManifest, SyncRemoteProbe, SyncRemoteProbeDraft, SyncRemoteProbeHandshake, SyncRemoteProbeProfile, SyncRemoteProbeResult, SyncSessionDeltaPrefixError, SyncSshErrorCategory, SyncSshProbeError, SyncSshSession, SyncSshTransport, buildRemoteProbeScript, buildSyncSshConnectConfig, parseRemoteProbeOutput };
@@ -27,6 +27,7 @@ var SyncSessionDeltaPrefixError = class extends Error {
27
27
  var SyncSshSession = class {
28
28
  client;
29
29
  channel;
30
+ onClosed;
30
31
  decoder = new SyncFrameDecoder();
31
32
  closeSignal = new AbortController();
32
33
  reading = false;
@@ -34,9 +35,10 @@ var SyncSshSession = class {
34
35
  inputEnded = false;
35
36
  nextDataSequence = 0;
36
37
  pendingError;
37
- constructor(client, channel) {
38
+ constructor(client, channel, onClosed) {
38
39
  this.client = client;
39
40
  this.channel = channel;
41
+ this.onClosed = onClosed;
40
42
  channel.on("error", (error) => {
41
43
  this.pendingError = error;
42
44
  });
@@ -104,6 +106,9 @@ var SyncSshSession = class {
104
106
  this.closed = true;
105
107
  this.channel.destroy();
106
108
  this.client.end();
109
+ try {
110
+ this.onClosed?.();
111
+ } catch {}
107
112
  }
108
113
  async write(frame) {
109
114
  if (this.closed || this.inputEnded) throw new SyncProtocolError("SSH sync channel input is closed");
@@ -146,9 +151,30 @@ function buildSyncSshConnectConfig(peer, secrets, challenges, onChallenge) {
146
151
  var SyncSshTransport = class {
147
152
  peerManager;
148
153
  hostKeyChallenges = new SyncHostKeyChallenges();
154
+ /** 每对端当前打开的 SSH 会话:手动停止同步时立即关闭通道,等待中的 readFrames 随即返回。 */
155
+ openSessions = /* @__PURE__ */ new Map();
149
156
  constructor(peerManager) {
150
157
  this.peerManager = peerManager;
151
158
  }
159
+ trackSession(peerId, session) {
160
+ const sessions = this.openSessions.get(peerId) ?? /* @__PURE__ */ new Set();
161
+ sessions.add(session);
162
+ this.openSessions.set(peerId, sessions);
163
+ }
164
+ untrackSession(peerId, session) {
165
+ const sessions = this.openSessions.get(peerId);
166
+ sessions?.delete(session);
167
+ if (sessions !== void 0 && sessions.size === 0) this.openSessions.delete(peerId);
168
+ }
169
+ /** 立即关闭该对端的全部活跃 SSH 会话(幂等;用于手动停止同步)。 */
170
+ stopActiveSessions(peerId) {
171
+ const sessions = this.openSessions.get(peerId);
172
+ if (sessions === void 0) return 0;
173
+ for (const session of [...sessions]) try {
174
+ session.close();
175
+ } catch {}
176
+ return sessions.size;
177
+ }
152
178
  async probe(peerId) {
153
179
  const { peer, session } = await this.openSession(peerId);
154
180
  try {
@@ -207,8 +233,9 @@ var SyncSshTransport = class {
207
233
  if (error) reject(error);
208
234
  else resolve(opened);
209
235
  });
210
- }));
236
+ }), () => this.untrackSession(peer.id, session));
211
237
  sessionOpened = true;
238
+ this.trackSession(peer.id, session);
212
239
  return {
213
240
  peer,
214
241
  session