@artenx/dshb 0.1.34 → 0.1.36

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 (46) hide show
  1. package/lib/client.js +34 -3
  2. package/lib/client.js.map +1 -1
  3. package/lib/core/index.js +1 -1
  4. package/lib/core/routes.d.ts +1 -1
  5. package/lib/core/sync/coordinator.d.ts +16 -5
  6. package/lib/core/sync/coordinator.js +108 -37
  7. package/lib/core/sync/coordinator.js.map +1 -1
  8. package/lib/core/sync/host-key.d.ts +1 -1
  9. package/lib/core/sync/internal-routes.d.ts +3 -3
  10. package/lib/core/sync/internal-routes.js +1 -1
  11. package/lib/core/sync/model.d.ts +1 -1
  12. package/lib/core/sync/model.js.map +1 -1
  13. package/lib/core/sync/node-sync.d.ts +1 -1
  14. package/lib/core/sync/offline-archive.d.ts +3 -3
  15. package/lib/core/sync/offline-archive.js +1 -1
  16. package/lib/core/sync/offline-routes.js +1 -1
  17. package/lib/core/sync/peer-manager.d.ts +2 -2
  18. package/lib/core/sync/peer-manager.js +18 -5
  19. package/lib/core/sync/peer-manager.js.map +1 -1
  20. package/lib/core/sync/routes.d.ts +1 -1
  21. package/lib/core/sync/routes.js +19 -0
  22. package/lib/core/sync/routes.js.map +1 -1
  23. package/lib/core/sync/session-journal.d.ts +1 -1
  24. package/lib/core/sync/session-journal.js +16 -0
  25. package/lib/core/sync/session-journal.js.map +1 -1
  26. package/lib/core/sync/session-routes.d.ts +1 -1
  27. package/lib/core/sync/session-routes.js +1 -1
  28. package/lib/core/sync/session-sync.d.ts +1 -1
  29. package/lib/core/sync/session-sync.js +1 -1
  30. package/lib/core/sync/ssh-transport.d.ts +1 -1
  31. package/lib/core/sync/ssh-transport.js +29 -2
  32. package/lib/core/sync/ssh-transport.js.map +1 -1
  33. package/lib/core/sync/stores.d.ts +1 -1
  34. package/lib/core/sync/stores.js +1 -1
  35. package/lib/core/sync/stores.js.map +1 -1
  36. package/lib/core/workspaces.d.ts +1 -1
  37. package/lib/{host-key-C5afhAUd.d.ts → host-key-CSnpIrjq.d.ts} +2 -2
  38. package/lib/{model-BP7Gw1yk.d.ts → model-CKTBf-Pl.d.ts} +2 -2
  39. package/lib/{node-sync-3Z9QLz2n.d.ts → node-sync-C3Ym-HDu.d.ts} +3 -3
  40. package/lib/{session-journal-pgPqcWn9.d.ts → session-journal-VE_1tA2-.d.ts} +8 -2
  41. package/lib/{session-sync-CIqjuWkh.js → session-sync-DLr_BdvN.js} +15 -1
  42. package/lib/{session-sync-CIqjuWkh.js.map → session-sync-DLr_BdvN.js.map} +1 -1
  43. package/lib/{session-sync-BuOJvFxK.d.ts → session-sync-DrKShc5c.d.ts} +11 -5
  44. package/lib/{ssh-transport-C7povM0s.d.ts → ssh-transport-BxPWfu9X.d.ts} +13 -6
  45. package/lib/{stores-CKyEt0yk.d.ts → stores-puyTC075.d.ts} +2 -2
  46. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"model.js","names":[],"sources":["../../../src/core/sync/model.ts"],"sourcesContent":["import { posix } from 'node:path'\nimport type { SyncRevision } from './revision.js'\n\nexport type SyncDirection = 'pull' | 'bidirectional'\nexport type SyncSchedule = 'manual' | 'every-5m'\nexport type SyncEntityKind = 'node' | 'session'\nexport type SyncImportMethod = 'peer' | 'offline'\n\nexport interface SyncPeer {\n id: string\n name: string\n host: string\n port: number\n username: string\n authKind: 'password' | 'key'\n remoteDshHome?: string\n remoteProfile?: string\n remoteWebPort?: number\n direction: SyncDirection\n schedule: SyncSchedule\n syncSessions: boolean\n enabled: boolean\n hostKeyFingerprint?: string\n}\n\nexport type SyncPeerDraft = Omit<SyncPeer, 'id' | 'hostKeyFingerprint'>\n\nexport interface SyncOrigin {\n originInstanceId: string\n originEntityId: string\n}\n\nexport interface SyncProvenance extends SyncOrigin {\n localEntityId: string\n importedVia?: SyncImportMethod\n sourceName?: string\n}\n\nexport interface SyncRecordMetadata extends SyncProvenance {\n kind: SyncEntityKind\n revision: SyncRevision\n contentDigest: string\n stateDigest?: string\n workspaceOrigin?: SyncOrigin\n deleted?: boolean\n pendingRemoval?: boolean\n credentialsRequired?: boolean\n}\n\nexport interface SyncPeerProgress {\n lastRunAt?: string\n lastRunStatus?: 'running' | 'succeeded' | 'failed'\n lastSuccessAt?: string\n lastManifestDigest?: string\n watermark?: string\n lastErrorCode?: string\n lastErrorMessage?: string\n lastResult?: {\n imported: number\n exported: number\n skipped: number\n pendingRemoval: number\n importedSessions: number\n exportedSessions: number\n skippedSessions: number\n }\n}\n\nexport interface SyncWorkspaceMapping {\n originInstanceId: string\n originWorkspaceId: string\n localWorkspaceId: string\n localPath: string\n localNodeId: string\n remotePath: string\n title: string\n revision: SyncRevision\n contentDigest: string\n}\n\nexport interface SyncSessionArchive {\n branchId: string\n originInstanceId: string\n originSessionId: string\n localSessionId: string\n revision: SyncRevision\n contentDigest: string\n stateDigest: string\n workspacePosition: number\n workspaceId?: string\n workspaceOrigin?: SyncOrigin\n archivedAt: string\n}\n\nexport interface SyncState {\n version: 1\n clock: { physicalTimeMs: number; logicalCounter: number }\n peers: Record<string, SyncPeerProgress>\n records: Record<string, SyncRecordMetadata>\n workspaces: Record<string, SyncWorkspaceMapping>\n archives: Record<string, SyncSessionArchive>\n}\n\nconst PEER_FIELDS = new Set([\n 'id',\n 'name',\n 'host',\n 'port',\n 'username',\n 'authKind',\n 'remoteDshHome',\n 'remoteProfile',\n 'remoteWebPort',\n 'direction',\n 'schedule',\n 'syncSessions',\n 'enabled',\n 'hostKeyFingerprint',\n])\n\nconst PEER_DRAFT_FIELDS = new Set([...PEER_FIELDS].filter((field) => field !== 'id' && field !== 'hostKeyFingerprint'))\n\nfunction record(value: unknown, label: string): Record<string, unknown> {\n if (value === null || typeof value !== 'object' || Array.isArray(value)) {\n throw new Error(`${label} must be an object`)\n }\n return value as Record<string, unknown>\n}\n\nfunction text(value: unknown, label: string, maxLength: number): string {\n if (typeof value !== 'string') throw new Error(`${label} must be a string`)\n const result = value.trim()\n if (!result || result.length > maxLength || /[\\u0000-\\u001f\\u007f]/.test(result)) {\n throw new Error(`${label} is invalid`)\n }\n return result\n}\n\nfunction optionalText(value: unknown, label: string, maxLength: number): string | undefined {\n return value === undefined ? undefined : text(value, label, maxLength)\n}\n\nfunction port(value: unknown, label: string): number {\n if (!Number.isSafeInteger(value) || (value as number) < 1 || (value as number) > 65535) {\n throw new Error(`${label} must be an integer between 1 and 65535`)\n }\n return value as number\n}\n\nexport function parseSyncPeer(value: unknown): SyncPeer {\n const input = record(value, 'sync peer')\n for (const key of Object.keys(input)) {\n if (!PEER_FIELDS.has(key)) throw new Error(`sync peer contains unsupported field: ${key}`)\n }\n\n const id = text(input.id, 'id', 128)\n if (!/^[a-z][a-z0-9-]*$/.test(id)) throw new Error('id is invalid')\n\n const name = text(input.name, 'name', 128)\n const host = text(input.host, 'host', 255)\n if (/\\s/.test(host)) throw new Error('host is invalid')\n\n const username = text(input.username, 'username', 128)\n if (/\\s/.test(username)) throw new Error('username is invalid')\n\n if (input.authKind !== 'password' && input.authKind !== 'key') {\n throw new Error('authKind must be password or key')\n }\n if (input.direction !== 'pull' && input.direction !== 'bidirectional') {\n throw new Error('direction must be pull or bidirectional')\n }\n if (input.schedule !== 'manual' && input.schedule !== 'every-5m') {\n throw new Error('schedule must be manual or every-5m')\n }\n if (typeof input.syncSessions !== 'boolean' || typeof input.enabled !== 'boolean') {\n throw new Error('syncSessions and enabled must be booleans')\n }\n\n const remoteDshHome = optionalText(input.remoteDshHome, 'remoteDshHome', 4096)\n if (remoteDshHome !== undefined) {\n if (!posix.isAbsolute(remoteDshHome) || remoteDshHome.split('/').includes('..') || remoteDshHome.includes('\\\\')) {\n throw new Error('remoteDshHome must be a normalized absolute path')\n }\n }\n\n const remoteProfile = optionalText(input.remoteProfile, 'remoteProfile', 256)\n const hostKeyFingerprint = optionalText(input.hostKeyFingerprint, 'hostKeyFingerprint', 512)\n\n return {\n id,\n name,\n host,\n port: port(input.port, 'port'),\n username,\n authKind: input.authKind,\n ...(remoteDshHome === undefined ? {} : { remoteDshHome: posix.normalize(remoteDshHome) }),\n ...(remoteProfile === undefined ? {} : { remoteProfile }),\n ...(input.remoteWebPort === undefined ? {} : { remoteWebPort: port(input.remoteWebPort, 'remoteWebPort') }),\n direction: input.direction,\n schedule: input.schedule,\n syncSessions: input.syncSessions,\n enabled: input.enabled,\n ...(hostKeyFingerprint === undefined ? {} : { hostKeyFingerprint }),\n }\n}\n\nexport function parseSyncPeerDraft(value: unknown): SyncPeerDraft {\n const input = record(value, 'sync peer draft')\n for (const key of Object.keys(input)) {\n if (!PEER_DRAFT_FIELDS.has(key)) throw new Error(`sync peer draft contains unsupported field: ${key}`)\n }\n const { id, ...draft } = parseSyncPeer({ ...input, id: `p-${'0'.repeat(24)}` })\n void id\n return draft\n}\n\nexport function syncRecordKey(kind: SyncEntityKind, origin: SyncOrigin): string {\n return JSON.stringify([kind, origin.originInstanceId, origin.originEntityId])\n}\n\nexport function syncWorkspaceKey(originInstanceId: string, originWorkspaceId: string, remotePath: string): string {\n return JSON.stringify([originInstanceId, originWorkspaceId, remotePath])\n}\n\nexport function isRecord(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === 'object' && !Array.isArray(value)\n}\n\nexport function isNonNegativeSafeInteger(value: unknown): value is number {\n return Number.isSafeInteger(value) && (value as number) >= 0\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"model.js","names":[],"sources":["../../../src/core/sync/model.ts"],"sourcesContent":["import { posix } from 'node:path'\nimport type { SyncRevision } from './revision.js'\n\nexport type SyncDirection = 'pull' | 'bidirectional'\nexport type SyncSchedule = 'manual' | 'every-5m'\nexport type SyncEntityKind = 'node' | 'session'\nexport type SyncImportMethod = 'peer' | 'offline'\n\nexport interface SyncPeer {\n id: string\n name: string\n host: string\n port: number\n username: string\n authKind: 'password' | 'key'\n remoteDshHome?: string\n remoteProfile?: string\n remoteWebPort?: number\n direction: SyncDirection\n schedule: SyncSchedule\n syncSessions: boolean\n enabled: boolean\n hostKeyFingerprint?: string\n}\n\nexport type SyncPeerDraft = Omit<SyncPeer, 'id' | 'hostKeyFingerprint'>\n\nexport interface SyncOrigin {\n originInstanceId: string\n originEntityId: string\n}\n\nexport interface SyncProvenance extends SyncOrigin {\n localEntityId: string\n importedVia?: SyncImportMethod\n sourceName?: string\n}\n\nexport interface SyncRecordMetadata extends SyncProvenance {\n kind: SyncEntityKind\n revision: SyncRevision\n contentDigest: string\n stateDigest?: string\n workspaceOrigin?: SyncOrigin\n deleted?: boolean\n pendingRemoval?: boolean\n credentialsRequired?: boolean\n}\n\nexport interface SyncPeerProgress {\n lastRunAt?: string\n lastRunStatus?: 'running' | 'succeeded' | 'failed' | 'stopped'\n lastSuccessAt?: string\n lastManifestDigest?: string\n watermark?: string\n lastErrorCode?: string\n lastErrorMessage?: string\n lastResult?: {\n imported: number\n exported: number\n skipped: number\n pendingRemoval: number\n importedSessions: number\n exportedSessions: number\n skippedSessions: number\n }\n}\n\nexport interface SyncWorkspaceMapping {\n originInstanceId: string\n originWorkspaceId: string\n localWorkspaceId: string\n localPath: string\n localNodeId: string\n remotePath: string\n title: string\n revision: SyncRevision\n contentDigest: string\n}\n\nexport interface SyncSessionArchive {\n branchId: string\n originInstanceId: string\n originSessionId: string\n localSessionId: string\n revision: SyncRevision\n contentDigest: string\n stateDigest: string\n workspacePosition: number\n workspaceId?: string\n workspaceOrigin?: SyncOrigin\n archivedAt: string\n}\n\nexport interface SyncState {\n version: 1\n clock: { physicalTimeMs: number; logicalCounter: number }\n peers: Record<string, SyncPeerProgress>\n records: Record<string, SyncRecordMetadata>\n workspaces: Record<string, SyncWorkspaceMapping>\n archives: Record<string, SyncSessionArchive>\n}\n\nconst PEER_FIELDS = new Set([\n 'id',\n 'name',\n 'host',\n 'port',\n 'username',\n 'authKind',\n 'remoteDshHome',\n 'remoteProfile',\n 'remoteWebPort',\n 'direction',\n 'schedule',\n 'syncSessions',\n 'enabled',\n 'hostKeyFingerprint',\n])\n\nconst PEER_DRAFT_FIELDS = new Set([...PEER_FIELDS].filter((field) => field !== 'id' && field !== 'hostKeyFingerprint'))\n\nfunction record(value: unknown, label: string): Record<string, unknown> {\n if (value === null || typeof value !== 'object' || Array.isArray(value)) {\n throw new Error(`${label} must be an object`)\n }\n return value as Record<string, unknown>\n}\n\nfunction text(value: unknown, label: string, maxLength: number): string {\n if (typeof value !== 'string') throw new Error(`${label} must be a string`)\n const result = value.trim()\n if (!result || result.length > maxLength || /[\\u0000-\\u001f\\u007f]/.test(result)) {\n throw new Error(`${label} is invalid`)\n }\n return result\n}\n\nfunction optionalText(value: unknown, label: string, maxLength: number): string | undefined {\n return value === undefined ? undefined : text(value, label, maxLength)\n}\n\nfunction port(value: unknown, label: string): number {\n if (!Number.isSafeInteger(value) || (value as number) < 1 || (value as number) > 65535) {\n throw new Error(`${label} must be an integer between 1 and 65535`)\n }\n return value as number\n}\n\nexport function parseSyncPeer(value: unknown): SyncPeer {\n const input = record(value, 'sync peer')\n for (const key of Object.keys(input)) {\n if (!PEER_FIELDS.has(key)) throw new Error(`sync peer contains unsupported field: ${key}`)\n }\n\n const id = text(input.id, 'id', 128)\n if (!/^[a-z][a-z0-9-]*$/.test(id)) throw new Error('id is invalid')\n\n const name = text(input.name, 'name', 128)\n const host = text(input.host, 'host', 255)\n if (/\\s/.test(host)) throw new Error('host is invalid')\n\n const username = text(input.username, 'username', 128)\n if (/\\s/.test(username)) throw new Error('username is invalid')\n\n if (input.authKind !== 'password' && input.authKind !== 'key') {\n throw new Error('authKind must be password or key')\n }\n if (input.direction !== 'pull' && input.direction !== 'bidirectional') {\n throw new Error('direction must be pull or bidirectional')\n }\n if (input.schedule !== 'manual' && input.schedule !== 'every-5m') {\n throw new Error('schedule must be manual or every-5m')\n }\n if (typeof input.syncSessions !== 'boolean' || typeof input.enabled !== 'boolean') {\n throw new Error('syncSessions and enabled must be booleans')\n }\n\n const remoteDshHome = optionalText(input.remoteDshHome, 'remoteDshHome', 4096)\n if (remoteDshHome !== undefined) {\n if (!posix.isAbsolute(remoteDshHome) || remoteDshHome.split('/').includes('..') || remoteDshHome.includes('\\\\')) {\n throw new Error('remoteDshHome must be a normalized absolute path')\n }\n }\n\n const remoteProfile = optionalText(input.remoteProfile, 'remoteProfile', 256)\n const hostKeyFingerprint = optionalText(input.hostKeyFingerprint, 'hostKeyFingerprint', 512)\n\n return {\n id,\n name,\n host,\n port: port(input.port, 'port'),\n username,\n authKind: input.authKind,\n ...(remoteDshHome === undefined ? {} : { remoteDshHome: posix.normalize(remoteDshHome) }),\n ...(remoteProfile === undefined ? {} : { remoteProfile }),\n ...(input.remoteWebPort === undefined ? {} : { remoteWebPort: port(input.remoteWebPort, 'remoteWebPort') }),\n direction: input.direction,\n schedule: input.schedule,\n syncSessions: input.syncSessions,\n enabled: input.enabled,\n ...(hostKeyFingerprint === undefined ? {} : { hostKeyFingerprint }),\n }\n}\n\nexport function parseSyncPeerDraft(value: unknown): SyncPeerDraft {\n const input = record(value, 'sync peer draft')\n for (const key of Object.keys(input)) {\n if (!PEER_DRAFT_FIELDS.has(key)) throw new Error(`sync peer draft contains unsupported field: ${key}`)\n }\n const { id, ...draft } = parseSyncPeer({ ...input, id: `p-${'0'.repeat(24)}` })\n void id\n return draft\n}\n\nexport function syncRecordKey(kind: SyncEntityKind, origin: SyncOrigin): string {\n return JSON.stringify([kind, origin.originInstanceId, origin.originEntityId])\n}\n\nexport function syncWorkspaceKey(originInstanceId: string, originWorkspaceId: string, remotePath: string): string {\n return JSON.stringify([originInstanceId, originWorkspaceId, remotePath])\n}\n\nexport function isRecord(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === 'object' && !Array.isArray(value)\n}\n\nexport function isNonNegativeSafeInteger(value: unknown): value is number {\n return Number.isSafeInteger(value) && (value as number) >= 0\n}\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- import { a as SyncNodeManifestError, c as SyncNodeView, i as SyncNodeManifest, n as SyncNodeEntry, o as SyncNodeProfile, r as SyncNodeInUseError, s as SyncNodeService, t as SyncNodeApplyResult } from "../../node-sync-3Z9QLz2n.js";
1
+ import { a as SyncNodeManifestError, c as SyncNodeView, i as SyncNodeManifest, n as SyncNodeEntry, o as SyncNodeProfile, r as SyncNodeInUseError, s as SyncNodeService, t as SyncNodeApplyResult } from "../../node-sync-C3Ym-HDu.js";
2
2
  export { SyncNodeApplyResult, SyncNodeEntry, SyncNodeInUseError, SyncNodeManifest, SyncNodeManifestError, SyncNodeProfile, SyncNodeService, SyncNodeView };
@@ -1,6 +1,6 @@
1
- import { n as SyncStateStore } from "../../stores-CKyEt0yk.js";
2
- import { s as SyncNodeService } from "../../node-sync-3Z9QLz2n.js";
3
- import { h as SyncSessionService } from "../../session-sync-BuOJvFxK.js";
1
+ import { n as SyncStateStore } from "../../stores-puyTC075.js";
2
+ import { s as SyncNodeService } from "../../node-sync-C3Ym-HDu.js";
3
+ import { h as SyncSessionService } from "../../session-sync-DrKShc5c.js";
4
4
  //#region src/core/sync/offline-archive.d.ts
5
5
  declare const MAX_OFFLINE_ARCHIVE_BYTES: number;
6
6
  declare class SyncOfflineArchiveError extends Error {
@@ -1,4 +1,4 @@
1
- import { f as validateSyncSessionBundle, n as SyncSessionError } from "../../session-sync-CIqjuWkh.js";
1
+ import { f as validateSyncSessionBundle, n as SyncSessionError } from "../../session-sync-DLr_BdvN.js";
2
2
  import { SyncOfflineArchiveJournalStore } from "./offline-journal.js";
3
3
  import { homedir } from "node:os";
4
4
  import { appendFileSync, chmodSync, closeSync, createReadStream, createWriteStream, mkdirSync, mkdtempSync, openSync, readSync, readdirSync, rmSync, statSync, statfsSync, writeFileSync } from "node:fs";
@@ -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
  import { SyncOfflineArchiveError } from "./offline-archive.js";
3
3
  import { createReadStream } from "node:fs";
4
4
  import { randomBytes } from "node:crypto";
@@ -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 {
@@ -114,12 +114,25 @@ var SyncPeerManager = class {
114
114
  }
115
115
  updateProgress(id, patch) {
116
116
  if (!this.peers.get(id)) throw new SyncPeerNotFoundError(id);
117
- this.state.update((state) => {
118
- state.peers[id] = {
119
- ...state.peers[id],
120
- ...patch
117
+ try {
118
+ this.state.update((state) => {
119
+ state.peers[id] = {
120
+ ...state.peers[id],
121
+ ...patch
122
+ };
123
+ });
124
+ } catch (error) {
125
+ const fallback = {
126
+ ...patch,
127
+ lastErrorMessage: patch.lastErrorMessage === void 0 ? void 0 : ("进度详情省略:" + patch.lastErrorMessage).replace(/[\u0000-\u001f\u007f]/g, " ").slice(0, 200)
121
128
  };
122
- });
129
+ this.state.update((state) => {
130
+ state.peers[id] = {
131
+ ...state.peers[id],
132
+ ...fallback
133
+ };
134
+ });
135
+ }
123
136
  }
124
137
  async get(id) {
125
138
  const peer = this.peers.get(id);
@@ -1 +1 @@
1
- {"version":3,"file":"peer-manager.js","names":[],"sources":["../../../src/core/sync/peer-manager.ts"],"sourcesContent":["import { credentialKey, type CredentialKey, type CredentialRecord } from '@deepseek-ai/dsh-credentials'\nimport { parseSyncPeer, parseSyncPeerDraft, type SyncPeer, type SyncPeerDraft, type SyncPeerProgress } from './model.js'\nimport { SyncPeerStore, SyncStateStore } from './stores.js'\n\nexport interface SyncPeerSecrets {\n password?: string\n privateKey?: string\n passphrase?: string\n}\n\nexport interface SyncPeerView extends SyncPeer {\n hasSecret: { hasPassword: boolean; hasKey: boolean; hasPassphrase: boolean }\n progress?: SyncPeerProgress\n}\n\nexport type SyncPeerChangeListener = (peerId: string, peer: SyncPeer | undefined) => void\n\ninterface CredentialsLike {\n readRecord(key: CredentialKey): Promise<unknown>\n modifyRecord(key: CredentialKey, mutate: (current: unknown) => Promise<unknown>): Promise<unknown>\n deleteRecord(key: CredentialKey): Promise<void>\n}\n\nconst SECRET_FIELDS = new Set(['password', 'privateKey', 'passphrase'])\nconst PATCH_FIELDS = new Set([\n 'name',\n 'host',\n 'port',\n 'username',\n 'authKind',\n 'remoteDshHome',\n 'remoteProfile',\n 'remoteWebPort',\n 'direction',\n 'schedule',\n 'syncSessions',\n 'enabled',\n])\nconst MAX_SECRET_BYTES = 64 * 1024\n\nexport class SyncPeerInputError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'SyncPeerInputError'\n }\n}\n\nexport class SyncPeerNotFoundError extends Error {\n constructor(id: string) {\n super(`sync peer ${id} not found`)\n this.name = 'SyncPeerNotFoundError'\n }\n}\n\nfunction asRecord(value: unknown, label: string): Record<string, unknown> {\n if (value === null || typeof value !== 'object' || Array.isArray(value)) {\n throw new SyncPeerInputError(`${label} must be an object`)\n }\n return value as Record<string, unknown>\n}\n\nfunction parseSecrets(value: unknown): SyncPeerSecrets | undefined {\n if (value === undefined) return undefined\n const input = asRecord(value, 'secrets')\n for (const key of Object.keys(input)) {\n if (!SECRET_FIELDS.has(key)) throw new SyncPeerInputError(`unsupported secret field: ${key}`)\n }\n\n const output: SyncPeerSecrets = {}\n for (const field of ['password', 'privateKey', 'passphrase'] as const) {\n const secret = input[field]\n if (secret === undefined || secret === '') continue\n if (typeof secret !== 'string' || Buffer.byteLength(secret, 'utf8') > MAX_SECRET_BYTES || secret.includes('\\0')) {\n throw new SyncPeerInputError(`${field} is invalid`)\n }\n output[field] = secret\n }\n return Object.keys(output).length === 0 ? undefined : output\n}\n\nfunction payloadFromRecord(value: unknown): SyncPeerSecrets {\n if (value === null || typeof value !== 'object' || Array.isArray(value)) return {}\n const payload = (value as { payload?: unknown }).payload\n if (payload === null || typeof payload !== 'object' || Array.isArray(payload)) return {}\n const source = payload as Record<string, unknown>\n return {\n ...(typeof source.password === 'string' ? { password: source.password } : {}),\n ...(typeof source.privateKey === 'string' ? { privateKey: source.privateKey } : {}),\n ...(typeof source.passphrase === 'string' ? { passphrase: source.passphrase } : {}),\n }\n}\n\nfunction toView(peer: SyncPeer, secrets: SyncPeerSecrets): SyncPeerView {\n return {\n ...peer,\n hasSecret: {\n hasPassword: Boolean(secrets.password),\n hasKey: Boolean(secrets.privateKey),\n hasPassphrase: Boolean(secrets.passphrase),\n },\n }\n}\n\nfunction validateAuthSecrets(\n authKind: SyncPeer['authKind'],\n supplied: SyncPeerSecrets | undefined,\n effective: SyncPeerSecrets,\n): void {\n if (authKind === 'password') {\n if (supplied?.privateKey !== undefined || supplied?.passphrase !== undefined) {\n throw new SyncPeerInputError('key credentials do not match password authentication')\n }\n if (!effective.password) throw new SyncPeerInputError('password authentication requires a saved password')\n return\n }\n if (supplied?.password !== undefined) throw new SyncPeerInputError('password does not match key authentication')\n if (!effective.privateKey) throw new SyncPeerInputError('key authentication requires a saved private key')\n}\n\nfunction secretsForAuth(authKind: SyncPeer['authKind'], secrets: SyncPeerSecrets): SyncPeerSecrets {\n return authKind === 'password'\n ? (secrets.password ? { password: secrets.password } : {})\n : {\n ...(secrets.privateKey ? { privateKey: secrets.privateKey } : {}),\n ...(secrets.passphrase ? { passphrase: secrets.passphrase } : {}),\n }\n}\n\nfunction asInputError(error: unknown): SyncPeerInputError {\n return error instanceof SyncPeerInputError\n ? error\n : new SyncPeerInputError(error instanceof Error ? error.message : 'invalid sync peer')\n}\n\nexport class SyncPeerManager {\n private readonly listeners = new Set<SyncPeerChangeListener>()\n\n constructor(\n private readonly peers: SyncPeerStore,\n private readonly state: SyncStateStore,\n private readonly credentials: CredentialsLike,\n ) {}\n\n async list(): Promise<SyncPeerView[]> {\n return Promise.all(this.peers.list().map((peer) => this.view(peer)))\n }\n\n subscribe(listener: SyncPeerChangeListener): () => void {\n this.listeners.add(listener)\n return () => { this.listeners.delete(listener) }\n }\n\n updateProgress(id: string, patch: SyncPeerProgress): void {\n if (!this.peers.get(id)) throw new SyncPeerNotFoundError(id)\n this.state.update((state) => {\n state.peers[id] = { ...state.peers[id], ...patch }\n })\n }\n\n async get(id: string): Promise<SyncPeerView | undefined> {\n const peer = this.peers.get(id)\n return peer ? this.view(peer) : undefined\n }\n\n async connectionData(id: string): Promise<{ peer: SyncPeer; secrets: SyncPeerSecrets }> {\n const peer = this.peers.get(id)\n if (!peer) throw new SyncPeerNotFoundError(id)\n const record = await this.credentials.readRecord(this.credentialKey(id))\n return { peer, secrets: payloadFromRecord(record) }\n }\n\n async pinHostKey(id: string, fingerprint: string): Promise<SyncPeerView> {\n if (!/^SHA256:[A-Za-z0-9+/]{43}$/.test(fingerprint)) {\n throw new SyncPeerInputError('invalid SSH host-key fingerprint')\n }\n const peer = this.peers.update(id, { hostKeyFingerprint: fingerprint })\n return this.view(peer)\n }\n\n async create(draftValue: unknown, secretsValue?: unknown): Promise<SyncPeerView> {\n let draft: SyncPeerDraft\n let secrets: SyncPeerSecrets | undefined\n try {\n draft = parseSyncPeerDraft(draftValue)\n secrets = parseSecrets(secretsValue)\n validateAuthSecrets(draft.authKind, secrets, secrets ?? {})\n } catch (error) {\n throw asInputError(error)\n }\n\n const peer = this.peers.create(draft)\n let attemptedSecrets = false\n try {\n if (secrets) {\n attemptedSecrets = true\n await this.writeSecrets(peer.id, secretsForAuth(peer.authKind, secrets))\n }\n const view = await this.view(peer)\n this.notify(peer.id, peer)\n return view\n } catch (error) {\n this.peers.remove(peer.id)\n if (attemptedSecrets) {\n try {\n await this.credentials.deleteRecord(this.credentialKey(peer.id))\n } catch {}\n }\n throw error\n }\n }\n\n async update(id: string, patchValue: unknown, secretsValue?: unknown): Promise<SyncPeerView> {\n const current = this.peers.get(id)\n if (!current) throw new SyncPeerNotFoundError(id)\n\n let patch: Partial<Omit<SyncPeer, 'id' | 'hostKeyFingerprint'>>\n let secrets: SyncPeerSecrets | undefined\n try {\n const input = asRecord(patchValue, 'sync peer patch')\n for (const key of Object.keys(input)) {\n if (!PATCH_FIELDS.has(key)) throw new SyncPeerInputError(`unsupported peer field: ${key}`)\n }\n const validated = parseSyncPeer({ ...current, ...input, id })\n const { id: _id, hostKeyFingerprint: _fingerprint, ...validatedPatch } = validated\n void _id\n void _fingerprint\n patch = validatedPatch\n secrets = parseSecrets(secretsValue)\n } catch (error) {\n throw asInputError(error)\n }\n\n const credentialKeyValue = this.credentialKey(id)\n const previousRecord = await this.credentials.readRecord(credentialKeyValue)\n const effectiveSecrets = { ...payloadFromRecord(previousRecord), ...secrets }\n const authKind = patch.authKind ?? current.authKind\n validateAuthSecrets(authKind, secrets, effectiveSecrets)\n if (secrets || authKind !== current.authKind) {\n try {\n await this.writeSecrets(id, secretsForAuth(authKind, effectiveSecrets))\n } catch (error) {\n await this.restoreCredentialRecord(credentialKeyValue, previousRecord)\n throw error\n }\n }\n\n let updated: SyncPeer\n try {\n updated = this.peers.update(id, patch)\n } catch (error) {\n await this.restoreCredentialRecord(credentialKeyValue, previousRecord)\n throw error\n }\n this.notify(id, updated)\n return this.view(updated)\n }\n\n async remove(id: string): Promise<boolean> {\n if (!this.peers.get(id)) return false\n this.peers.remove(id)\n this.notify(id, undefined)\n await this.credentials.deleteRecord(this.credentialKey(id))\n this.state.update((state) => {\n delete state.peers[id]\n })\n return true\n }\n\n private async view(peer: SyncPeer): Promise<SyncPeerView> {\n const record = await this.credentials.readRecord(this.credentialKey(peer.id))\n const progress = this.state.read().peers[peer.id]\n return { ...toView(peer, payloadFromRecord(record)), ...(progress ? { progress } : {}) }\n }\n\n private notify(peerId: string, peer: SyncPeer | undefined): void {\n for (const listener of this.listeners) {\n try {\n listener(peerId, peer)\n } catch {}\n }\n }\n\n private async writeSecrets(id: string, secrets: SyncPeerSecrets): Promise<void> {\n const key = this.credentialKey(id)\n await this.credentials.modifyRecord(key, async (_current) => ({\n kind: 'grant',\n payload: secrets,\n }) as CredentialRecord)\n }\n\n private async restoreCredentialRecord(key: CredentialKey, previous: unknown): Promise<void> {\n try {\n if (previous === undefined) await this.credentials.deleteRecord(key)\n else await this.credentials.modifyRecord(key, async () => previous)\n } catch {}\n }\n\n private credentialKey(id: string): CredentialKey {\n try {\n return credentialKey('dshb-core', id)\n } catch {\n throw new Error('credentials service unavailable')\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"peer-manager.js","names":[],"sources":["../../../src/core/sync/peer-manager.ts"],"sourcesContent":["import { credentialKey, type CredentialKey, type CredentialRecord } from '@deepseek-ai/dsh-credentials'\nimport { parseSyncPeer, parseSyncPeerDraft, type SyncPeer, type SyncPeerDraft, type SyncPeerProgress } from './model.js'\nimport { SyncPeerStore, SyncStateStore } from './stores.js'\n\nexport interface SyncPeerSecrets {\n password?: string\n privateKey?: string\n passphrase?: string\n}\n\nexport interface SyncPeerView extends SyncPeer {\n hasSecret: { hasPassword: boolean; hasKey: boolean; hasPassphrase: boolean }\n progress?: SyncPeerProgress\n}\n\nexport type SyncPeerChangeListener = (peerId: string, peer: SyncPeer | undefined) => void\n\ninterface CredentialsLike {\n readRecord(key: CredentialKey): Promise<unknown>\n modifyRecord(key: CredentialKey, mutate: (current: unknown) => Promise<unknown>): Promise<unknown>\n deleteRecord(key: CredentialKey): Promise<void>\n}\n\nconst SECRET_FIELDS = new Set(['password', 'privateKey', 'passphrase'])\nconst PATCH_FIELDS = new Set([\n 'name',\n 'host',\n 'port',\n 'username',\n 'authKind',\n 'remoteDshHome',\n 'remoteProfile',\n 'remoteWebPort',\n 'direction',\n 'schedule',\n 'syncSessions',\n 'enabled',\n])\nconst MAX_SECRET_BYTES = 64 * 1024\n\nexport class SyncPeerInputError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'SyncPeerInputError'\n }\n}\n\nexport class SyncPeerNotFoundError extends Error {\n constructor(id: string) {\n super(`sync peer ${id} not found`)\n this.name = 'SyncPeerNotFoundError'\n }\n}\n\nfunction asRecord(value: unknown, label: string): Record<string, unknown> {\n if (value === null || typeof value !== 'object' || Array.isArray(value)) {\n throw new SyncPeerInputError(`${label} must be an object`)\n }\n return value as Record<string, unknown>\n}\n\nfunction parseSecrets(value: unknown): SyncPeerSecrets | undefined {\n if (value === undefined) return undefined\n const input = asRecord(value, 'secrets')\n for (const key of Object.keys(input)) {\n if (!SECRET_FIELDS.has(key)) throw new SyncPeerInputError(`unsupported secret field: ${key}`)\n }\n\n const output: SyncPeerSecrets = {}\n for (const field of ['password', 'privateKey', 'passphrase'] as const) {\n const secret = input[field]\n if (secret === undefined || secret === '') continue\n if (typeof secret !== 'string' || Buffer.byteLength(secret, 'utf8') > MAX_SECRET_BYTES || secret.includes('\\0')) {\n throw new SyncPeerInputError(`${field} is invalid`)\n }\n output[field] = secret\n }\n return Object.keys(output).length === 0 ? undefined : output\n}\n\nfunction payloadFromRecord(value: unknown): SyncPeerSecrets {\n if (value === null || typeof value !== 'object' || Array.isArray(value)) return {}\n const payload = (value as { payload?: unknown }).payload\n if (payload === null || typeof payload !== 'object' || Array.isArray(payload)) return {}\n const source = payload as Record<string, unknown>\n return {\n ...(typeof source.password === 'string' ? { password: source.password } : {}),\n ...(typeof source.privateKey === 'string' ? { privateKey: source.privateKey } : {}),\n ...(typeof source.passphrase === 'string' ? { passphrase: source.passphrase } : {}),\n }\n}\n\nfunction toView(peer: SyncPeer, secrets: SyncPeerSecrets): SyncPeerView {\n return {\n ...peer,\n hasSecret: {\n hasPassword: Boolean(secrets.password),\n hasKey: Boolean(secrets.privateKey),\n hasPassphrase: Boolean(secrets.passphrase),\n },\n }\n}\n\nfunction validateAuthSecrets(\n authKind: SyncPeer['authKind'],\n supplied: SyncPeerSecrets | undefined,\n effective: SyncPeerSecrets,\n): void {\n if (authKind === 'password') {\n if (supplied?.privateKey !== undefined || supplied?.passphrase !== undefined) {\n throw new SyncPeerInputError('key credentials do not match password authentication')\n }\n if (!effective.password) throw new SyncPeerInputError('password authentication requires a saved password')\n return\n }\n if (supplied?.password !== undefined) throw new SyncPeerInputError('password does not match key authentication')\n if (!effective.privateKey) throw new SyncPeerInputError('key authentication requires a saved private key')\n}\n\nfunction secretsForAuth(authKind: SyncPeer['authKind'], secrets: SyncPeerSecrets): SyncPeerSecrets {\n return authKind === 'password'\n ? (secrets.password ? { password: secrets.password } : {})\n : {\n ...(secrets.privateKey ? { privateKey: secrets.privateKey } : {}),\n ...(secrets.passphrase ? { passphrase: secrets.passphrase } : {}),\n }\n}\n\nfunction asInputError(error: unknown): SyncPeerInputError {\n return error instanceof SyncPeerInputError\n ? error\n : new SyncPeerInputError(error instanceof Error ? error.message : 'invalid sync peer')\n}\n\nexport class SyncPeerManager {\n private readonly listeners = new Set<SyncPeerChangeListener>()\n\n constructor(\n private readonly peers: SyncPeerStore,\n private readonly state: SyncStateStore,\n private readonly credentials: CredentialsLike,\n ) {}\n\n async list(): Promise<SyncPeerView[]> {\n return Promise.all(this.peers.list().map((peer) => this.view(peer)))\n }\n\n subscribe(listener: SyncPeerChangeListener): () => void {\n this.listeners.add(listener)\n return () => { this.listeners.delete(listener) }\n }\n\n updateProgress(id: string, patch: SyncPeerProgress): void {\n if (!this.peers.get(id)) throw new SyncPeerNotFoundError(id)\n try {\n this.state.update((state) => {\n state.peers[id] = { ...state.peers[id], ...patch }\n })\n } catch (error) {\n // 进度写失败(校验拒绝、长消息等)绝不能让运行收尾丢失状态:\n // 降级为最小合法补丁(截断错误消息),保证状态机总能离开 running。\n const fallback: SyncPeerProgress = {\n ...patch,\n lastErrorMessage: patch.lastErrorMessage === undefined\n ? undefined\n : ('进度详情省略:' + patch.lastErrorMessage).replace(/[\\u0000-\\u001f\\u007f]/g, ' ').slice(0, 200),\n }\n this.state.update((state) => {\n state.peers[id] = { ...state.peers[id], ...fallback }\n })\n void error\n }\n }\n\n async get(id: string): Promise<SyncPeerView | undefined> {\n const peer = this.peers.get(id)\n return peer ? this.view(peer) : undefined\n }\n\n async connectionData(id: string): Promise<{ peer: SyncPeer; secrets: SyncPeerSecrets }> {\n const peer = this.peers.get(id)\n if (!peer) throw new SyncPeerNotFoundError(id)\n const record = await this.credentials.readRecord(this.credentialKey(id))\n return { peer, secrets: payloadFromRecord(record) }\n }\n\n async pinHostKey(id: string, fingerprint: string): Promise<SyncPeerView> {\n if (!/^SHA256:[A-Za-z0-9+/]{43}$/.test(fingerprint)) {\n throw new SyncPeerInputError('invalid SSH host-key fingerprint')\n }\n const peer = this.peers.update(id, { hostKeyFingerprint: fingerprint })\n return this.view(peer)\n }\n\n async create(draftValue: unknown, secretsValue?: unknown): Promise<SyncPeerView> {\n let draft: SyncPeerDraft\n let secrets: SyncPeerSecrets | undefined\n try {\n draft = parseSyncPeerDraft(draftValue)\n secrets = parseSecrets(secretsValue)\n validateAuthSecrets(draft.authKind, secrets, secrets ?? {})\n } catch (error) {\n throw asInputError(error)\n }\n\n const peer = this.peers.create(draft)\n let attemptedSecrets = false\n try {\n if (secrets) {\n attemptedSecrets = true\n await this.writeSecrets(peer.id, secretsForAuth(peer.authKind, secrets))\n }\n const view = await this.view(peer)\n this.notify(peer.id, peer)\n return view\n } catch (error) {\n this.peers.remove(peer.id)\n if (attemptedSecrets) {\n try {\n await this.credentials.deleteRecord(this.credentialKey(peer.id))\n } catch {}\n }\n throw error\n }\n }\n\n async update(id: string, patchValue: unknown, secretsValue?: unknown): Promise<SyncPeerView> {\n const current = this.peers.get(id)\n if (!current) throw new SyncPeerNotFoundError(id)\n\n let patch: Partial<Omit<SyncPeer, 'id' | 'hostKeyFingerprint'>>\n let secrets: SyncPeerSecrets | undefined\n try {\n const input = asRecord(patchValue, 'sync peer patch')\n for (const key of Object.keys(input)) {\n if (!PATCH_FIELDS.has(key)) throw new SyncPeerInputError(`unsupported peer field: ${key}`)\n }\n const validated = parseSyncPeer({ ...current, ...input, id })\n const { id: _id, hostKeyFingerprint: _fingerprint, ...validatedPatch } = validated\n void _id\n void _fingerprint\n patch = validatedPatch\n secrets = parseSecrets(secretsValue)\n } catch (error) {\n throw asInputError(error)\n }\n\n const credentialKeyValue = this.credentialKey(id)\n const previousRecord = await this.credentials.readRecord(credentialKeyValue)\n const effectiveSecrets = { ...payloadFromRecord(previousRecord), ...secrets }\n const authKind = patch.authKind ?? current.authKind\n validateAuthSecrets(authKind, secrets, effectiveSecrets)\n if (secrets || authKind !== current.authKind) {\n try {\n await this.writeSecrets(id, secretsForAuth(authKind, effectiveSecrets))\n } catch (error) {\n await this.restoreCredentialRecord(credentialKeyValue, previousRecord)\n throw error\n }\n }\n\n let updated: SyncPeer\n try {\n updated = this.peers.update(id, patch)\n } catch (error) {\n await this.restoreCredentialRecord(credentialKeyValue, previousRecord)\n throw error\n }\n this.notify(id, updated)\n return this.view(updated)\n }\n\n async remove(id: string): Promise<boolean> {\n if (!this.peers.get(id)) return false\n this.peers.remove(id)\n this.notify(id, undefined)\n await this.credentials.deleteRecord(this.credentialKey(id))\n this.state.update((state) => {\n delete state.peers[id]\n })\n return true\n }\n\n private async view(peer: SyncPeer): Promise<SyncPeerView> {\n const record = await this.credentials.readRecord(this.credentialKey(peer.id))\n const progress = this.state.read().peers[peer.id]\n return { ...toView(peer, payloadFromRecord(record)), ...(progress ? { progress } : {}) }\n }\n\n private notify(peerId: string, peer: SyncPeer | undefined): void {\n for (const listener of this.listeners) {\n try {\n listener(peerId, peer)\n } catch {}\n }\n }\n\n private async writeSecrets(id: string, secrets: SyncPeerSecrets): Promise<void> {\n const key = this.credentialKey(id)\n await this.credentials.modifyRecord(key, async (_current) => ({\n kind: 'grant',\n payload: secrets,\n }) as CredentialRecord)\n }\n\n private async restoreCredentialRecord(key: CredentialKey, previous: unknown): Promise<void> {\n try {\n if (previous === undefined) await this.credentials.deleteRecord(key)\n else await this.credentials.modifyRecord(key, async () => previous)\n } catch {}\n }\n\n private credentialKey(id: string): CredentialKey {\n try {\n return credentialKey('dshb-core', id)\n } catch {\n throw new Error('credentials service unavailable')\n }\n }\n}\n"],"mappings":"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"}
@@ -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":"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"}
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":"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"}
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