@artenx/dshb 0.1.30 → 0.1.31

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.
@@ -1,2 +1,2 @@
1
- import { S as validateSyncSessionManifest, _ as makeSyncSessionManifest, a as SyncSessionError, b as syncSessionEntryKey, c as SyncSessionInstructionSnapshot, d as SyncSessionObject, f as SyncSessionService, g as makeSyncSessionBundle, h as WorkspaceRegistryLike, i as SyncSessionEntry, l as SyncSessionManifest, m as SyncSessionWorkspace, n as SyncSessionApplyResult, o as SyncSessionEventDelta, p as SyncSessionTransferPlan, r as SyncSessionBundle, s as SyncSessionEventPrefix, t as SyncSessionApplyOptions, u as SyncSessionManifestEntry, v as planSessionTransfers, x as validateSyncSessionBundle, y as sessionEventListDigest } from "../../session-sync-C2j2rk9K.js";
2
- export { SyncSessionApplyOptions, SyncSessionApplyResult, SyncSessionBundle, SyncSessionEntry, SyncSessionError, SyncSessionEventDelta, SyncSessionEventPrefix, SyncSessionInstructionSnapshot, SyncSessionManifest, SyncSessionManifestEntry, SyncSessionObject, SyncSessionService, SyncSessionTransferPlan, SyncSessionWorkspace, WorkspaceRegistryLike, makeSyncSessionBundle, makeSyncSessionManifest, planSessionTransfers, sessionEventListDigest, syncSessionEntryKey, validateSyncSessionBundle, validateSyncSessionManifest };
1
+ import { C as validateSyncSessionBundle, S as syncSessionEntryKey, _ as estimateJsonBytes, a as SyncSessionEntry, b as planSessionTransfers, c as SyncSessionEventPrefix, d as SyncSessionManifestEntry, f as SyncSessionObject, g as WorkspaceRegistryLike, h as SyncSessionWorkspace, i as SyncSessionBundle, l as SyncSessionInstructionSnapshot, m as SyncSessionTransferPlan, n as SyncSessionApplyOptions, o as SyncSessionError, p as SyncSessionService, r as SyncSessionApplyResult, s as SyncSessionEventDelta, t as ExportObjectCache, u as SyncSessionManifest, v as makeSyncSessionBundle, w as validateSyncSessionManifest, x as sessionEventListDigest, y as makeSyncSessionManifest } from "../../session-sync-Zfo1Q-qT.js";
2
+ export { ExportObjectCache, SyncSessionApplyOptions, SyncSessionApplyResult, SyncSessionBundle, SyncSessionEntry, SyncSessionError, SyncSessionEventDelta, SyncSessionEventPrefix, SyncSessionInstructionSnapshot, SyncSessionManifest, SyncSessionManifestEntry, SyncSessionObject, SyncSessionService, SyncSessionTransferPlan, SyncSessionWorkspace, WorkspaceRegistryLike, estimateJsonBytes, makeSyncSessionBundle, makeSyncSessionManifest, planSessionTransfers, sessionEventListDigest, syncSessionEntryKey, validateSyncSessionBundle, validateSyncSessionManifest };
@@ -1,2 +1,2 @@
1
- import { a as planSessionTransfers, c as validateSyncSessionBundle, i as makeSyncSessionManifest, l as validateSyncSessionManifest, n as SyncSessionService, o as sessionEventListDigest, r as makeSyncSessionBundle, s as syncSessionEntryKey, t as SyncSessionError } from "../../session-sync-C0uoizyB.js";
2
- export { SyncSessionError, SyncSessionService, makeSyncSessionBundle, makeSyncSessionManifest, planSessionTransfers, sessionEventListDigest, syncSessionEntryKey, validateSyncSessionBundle, validateSyncSessionManifest };
1
+ import { a as makeSyncSessionBundle, c as sessionEventListDigest, d as validateSyncSessionManifest, i as estimateJsonBytes, l as syncSessionEntryKey, n as SyncSessionError, o as makeSyncSessionManifest, r as SyncSessionService, s as planSessionTransfers, t as ExportObjectCache, u as validateSyncSessionBundle } from "../../session-sync-BQt3HXkp.js";
2
+ export { ExportObjectCache, SyncSessionError, SyncSessionService, estimateJsonBytes, makeSyncSessionBundle, makeSyncSessionManifest, planSessionTransfers, sessionEventListDigest, syncSessionEntryKey, validateSyncSessionBundle, validateSyncSessionManifest };
@@ -1,2 +1,2 @@
1
- import { a as SyncSshProbeError, c as buildSyncSshConnectConfig, i as SyncSshErrorCategory, n as SyncRemoteNodeManifest, o as SyncSshSession, r as SyncSessionDeltaPrefixError, s as SyncSshTransport, t as SyncRemoteHandshake } from "../../ssh-transport-uK9Ep0yp.js";
1
+ import { a as SyncSshProbeError, c as buildSyncSshConnectConfig, i as SyncSshErrorCategory, n as SyncRemoteNodeManifest, o as SyncSshSession, r as SyncSessionDeltaPrefixError, s as SyncSshTransport, t as SyncRemoteHandshake } from "../../ssh-transport-Cfb_nwkY.js";
2
2
  export { SyncRemoteHandshake, SyncRemoteNodeManifest, SyncSessionDeltaPrefixError, SyncSshErrorCategory, SyncSshProbeError, SyncSshSession, SyncSshTransport, buildSyncSshConnectConfig };
@@ -244,7 +244,7 @@ var SyncSshTransport = class {
244
244
  records,
245
245
  sourceName
246
246
  }), "utf8");
247
- if (requestBody.length > 107374182400) throw new SyncSshProbeError("protocol");
247
+ if (requestBody.length > 134217728) throw new SyncSshProbeError("protocol");
248
248
  const { peer, session } = await this.openSession(peerId);
249
249
  try {
250
250
  await session.sendControl({
@@ -313,7 +313,7 @@ var SyncSshTransport = class {
313
313
  sourceName,
314
314
  peerId
315
315
  }), "utf8");
316
- if (requestBody.length > 107374182400) throw new SyncSshProbeError("protocol");
316
+ if (requestBody.length > 134217728) throw new SyncSshProbeError("protocol");
317
317
  const { peer, session } = await this.openSession(peerId);
318
318
  try {
319
319
  await session.sendControl({
@@ -361,7 +361,7 @@ var SyncSshTransport = class {
361
361
  }
362
362
  async fetchDataOperation(peerId, operation, requestBody) {
363
363
  const body = requestBody === void 0 ? void 0 : Buffer.from(JSON.stringify(requestBody), "utf8");
364
- if (body && body.length > 107374182400) throw new SyncSshProbeError("protocol");
364
+ if (body && body.length > 134217728) throw new SyncSshProbeError("protocol");
365
365
  const { peer, session } = await this.openSession(peerId);
366
366
  try {
367
367
  await session.sendControl({
@@ -396,7 +396,7 @@ var SyncSshTransport = class {
396
396
  }
397
397
  if (responseStatus === void 0 || bodyMode !== "data" || responseEnded) throw new SyncProtocolError(`unexpected ${operation} data frame`);
398
398
  totalBytes += frame.data.length;
399
- if (totalBytes > 107374182400) throw new SyncProtocolError(`${operation} exceeds transfer limit`);
399
+ if (totalBytes > 134217728) throw new SyncProtocolError(`${operation} exceeds transfer limit`);
400
400
  chunks.push(frame.data);
401
401
  }
402
402
  if (responseStatus !== 200 || !responseEnded) throw new SyncSshProbeError("helper");
@@ -1 +1 @@
1
- {"version":3,"file":"ssh-transport.js","names":[],"sources":["../../../src/core/sync/ssh-transport.ts"],"sourcesContent":["import { Client, type ConnectConfig } from 'ssh2'\nimport type { ClientChannel } from 'ssh2'\nimport { once } from 'node:events'\nimport { classifySshError } from '../../exec-ssh/connection.js'\nimport { SyncPeerManager, SyncPeerNotFoundError, type SyncPeerSecrets } from './peer-manager.js'\nimport { isRecord, type SyncPeer } from './model.js'\nimport { SyncHostKeyChallengeError, SyncHostKeyChallenges, inspectSyncHostKey } from './host-key.js'\nimport type { SyncNodeApplyResult, SyncNodeManifest } from './node-sync.js'\nimport {\n MAX_SYNC_DATA_CHUNK_BYTES,\n MAX_SYNC_TRANSFER_BYTES,\n SYNC_PROTOCOL_VERSION,\n SyncFrameDecoder,\n SyncProtocolError,\n encodeSyncControlFrame,\n encodeSyncDataFrame,\n type SyncFrame,\n} from './protocol.js'\nimport { REMOTE_SYNC_HELPER_COMMAND } from './remote-helper.js'\nimport type { SyncSessionApplyResult, SyncSessionBundle, SyncSessionEventPrefix, SyncSessionManifest } from './session-sync.js'\n\nconst SSH_READY_TIMEOUT_MS = 30_000\nconst SSH_READ_IDLE_TIMEOUT_MS = 60_000\nconst DEFAULT_REMOTE_WEB_PORT = 3080\n\nexport type SyncSshErrorCategory = 'auth' | 'network' | 'hostkey' | 'timeout' | 'helper' | 'protocol'\n\nexport class SyncSshProbeError extends Error {\n constructor(readonly category: SyncSshErrorCategory) {\n super(`SSH sync probe failed: ${category}`)\n this.name = 'SyncSshProbeError'\n }\n}\n\nexport class SyncSessionDeltaPrefixError extends Error {\n constructor() {\n super('remote session event prefix changed during transfer')\n this.name = 'SyncSessionDeltaPrefixError'\n }\n}\n\nexport interface SyncRemoteHandshake {\n protocolVersion: number\n instanceId: string\n capabilities: string[]\n}\n\nexport interface SyncRemoteNodeManifest extends SyncNodeManifest {\n capabilities?: string[]\n}\n\nexport class SyncSshSession {\n private readonly decoder = new SyncFrameDecoder()\n private readonly closeSignal = new AbortController()\n private reading = false\n private closed = false\n private inputEnded = false\n private nextDataSequence = 0\n private pendingError: Error | undefined\n\n constructor(\n private readonly client: Client,\n private readonly channel: ClientChannel,\n ) {\n channel.on('error', (error: Error) => { this.pendingError = error })\n channel.on('exit', (code: number | null) => {\n if (code !== null && code !== 0) this.pendingError = new Error(`remote helper exited with code ${code}`)\n })\n channel.on('close', () => { this.closeSignal.abort() })\n }\n\n async sendControl(value: Record<string, unknown>): Promise<void> {\n await this.write(encodeSyncControlFrame(value))\n }\n\n async sendData(sequence: number, data: Buffer): Promise<void> {\n if (sequence !== this.nextDataSequence) throw new SyncProtocolError('outgoing data sequence is out of order')\n await this.write(encodeSyncDataFrame(sequence, data))\n this.nextDataSequence += 1\n }\n\n endInput(): void {\n if (this.closed || this.inputEnded) return\n this.inputEnded = true\n this.channel.end()\n }\n\n async *readFrames(idleTimeoutMs = SSH_READ_IDLE_TIMEOUT_MS): AsyncGenerator<SyncFrame> {\n if (this.reading) throw new SyncProtocolError('SSH sync channel can only be read once')\n this.reading = true\n const channel = this.channel\n const decoder = this.decoder\n let idleDeadline: NodeJS.Timeout | undefined\n const armIdleDeadline = (): void => {\n clearTimeout(idleDeadline)\n idleDeadline = setTimeout(() => {\n this.pendingError = new Error(`SSH channel idle for more than ${Math.round(idleTimeoutMs / 1000)}s`)\n // Full teardown: destroying the channel alone may leave the stdout\n // stream open, which would keep a plain for-await suspended forever.\n this.close()\n }, idleTimeoutMs)\n }\n // channel.destroy() does not reliably end ssh2's stdout stream; racing the\n // iterator against the close signal guarantees this generator settles.\n // A channel close resolves with a sentinel instead of rejecting: a normal\n // helper exit closes the channel around the same moment stdout reaches EOF,\n // and only a pendingError (idle timeout, helper crash) makes it an error.\n const CLOSE: unique symbol = Symbol('channel-close')\n const closed: Promise<IteratorResult<Buffer> | typeof CLOSE> = new Promise((resolve) => {\n this.closeSignal.signal.addEventListener('abort', () => resolve(CLOSE), { once: true })\n })\n const iterator = (channel.stdout as AsyncIterable<Buffer>)[Symbol.asyncIterator]()\n try {\n armIdleDeadline()\n for (;;) {\n const next = await Promise.race([iterator.next(), closed])\n if (next === CLOSE || next.done) {\n if (this.pendingError) throw new SyncProtocolError(`SSH channel error: ${this.pendingError.message}`)\n break\n }\n armIdleDeadline()\n for (const frame of decoder.push(Buffer.from(next.value))) yield frame\n }\n decoder.finish()\n } catch (error) {\n if (error instanceof SyncProtocolError) throw error\n const detail = this.pendingError ? this.pendingError.message : error instanceof Error ? error.message : 'unknown error'\n throw new SyncProtocolError(`SSH channel read failed: ${detail}`)\n } finally {\n clearTimeout(idleDeadline)\n void iterator.return?.().catch(() => {})\n }\n }\n\n close(): void {\n if (this.closed) return\n this.closed = true\n this.channel.destroy()\n this.client.end()\n }\n\n private async write(frame: Buffer): Promise<void> {\n if (this.closed || this.inputEnded) throw new SyncProtocolError('SSH sync channel input is closed')\n if (this.channel.write(frame)) return\n // A frozen connection never drains; bound the wait so writes cannot hang.\n const drainDeadline = setTimeout(() => this.closeSignal.abort(), SSH_READ_IDLE_TIMEOUT_MS)\n try {\n await once(this.channel, 'drain', { signal: this.closeSignal.signal })\n } catch (error) {\n if (this.closeSignal.signal.aborted) throw new SyncProtocolError('SSH channel closed before write could drain')\n throw error\n } finally {\n clearTimeout(drainDeadline)\n }\n }\n}\n\nexport function buildSyncSshConnectConfig(\n peer: SyncPeer,\n secrets: SyncPeerSecrets,\n challenges: SyncHostKeyChallenges,\n onChallenge: (error: SyncHostKeyChallengeError) => void,\n): ConnectConfig {\n return {\n host: peer.host,\n port: peer.port,\n username: peer.username,\n hostHash: 'sha256',\n readyTimeout: SSH_READY_TIMEOUT_MS,\n keepaliveInterval: 15_000,\n hostVerifier: (sha256Hex: string): boolean => {\n try {\n const decision = inspectSyncHostKey(peer, sha256Hex, challenges)\n if (!decision.trusted) onChallenge(decision.challenge)\n return decision.trusted\n } catch (error) {\n if (error instanceof SyncHostKeyChallengeError) onChallenge(error)\n return false\n }\n },\n ...(peer.authKind === 'password'\n ? { password: secrets.password }\n : { privateKey: secrets.privateKey, ...(secrets.passphrase ? { passphrase: secrets.passphrase } : {}) }),\n }\n}\n\nexport class SyncSshTransport {\n private readonly hostKeyChallenges = new SyncHostKeyChallenges()\n\n constructor(private readonly peerManager: SyncPeerManager) {}\n\n async probe(peerId: string): Promise<SyncRemoteHandshake> {\n const { peer, session } = await this.openSession(peerId)\n try {\n const handshake = await this.requestHandshake(session, peer.remoteWebPort ?? DEFAULT_REMOTE_WEB_PORT)\n if (handshake.protocolVersion !== SYNC_PROTOCOL_VERSION) throw new SyncSshProbeError('protocol')\n return handshake\n } catch (error) {\n if (\n error instanceof SyncHostKeyChallengeError ||\n error instanceof SyncPeerNotFoundError ||\n error instanceof SyncSshProbeError\n ) {\n throw error\n }\n if (error instanceof SyncProtocolError) throw new SyncSshProbeError('protocol')\n throw new SyncSshProbeError('helper')\n } finally {\n session.close()\n }\n }\n\n async openSession(peerId: string): Promise<{ peer: SyncPeer; session: SyncSshSession }> {\n const { peer, secrets } = await this.peerManager.connectionData(peerId)\n if (peer.authKind === 'password' && !secrets.password) throw new SyncSshProbeError('auth')\n if (peer.authKind === 'key' && !secrets.privateKey) throw new SyncSshProbeError('auth')\n\n const client = new Client()\n let challenge: SyncHostKeyChallengeError | undefined\n let sessionOpened = false\n try {\n await new Promise<void>((resolve, reject) => {\n let settled = false\n const finish = (error?: unknown): void => {\n if (settled) return\n settled = true\n clearTimeout(readyDeadline)\n if (error) reject(error)\n else resolve()\n }\n\n // ssh2's readyTimeout can be defeated by stalls behind transparent\n // proxies; this deadline guarantees the connect promise settles.\n const readyDeadline = setTimeout(() => {\n const timeout = new Error('Timed out while waiting for SSH handshake')\n ;(timeout as Error & { level?: string }).level = 'client-timeout'\n finish(timeout)\n client.end()\n }, SSH_READY_TIMEOUT_MS * 2)\n client.once('ready', () => finish())\n client.on('error', (error: Error) => finish(challenge ?? error))\n const config = buildSyncSshConnectConfig(peer, secrets, this.hostKeyChallenges, (error) => {\n challenge = error\n })\n\n try {\n client.connect(config)\n } catch (error) {\n finish(error)\n }\n })\n\n const channel = await new Promise<ClientChannel>((resolve, reject) => {\n const execDeadline = setTimeout(() => {\n reject(new Error('Timed out while waiting for the sync helper channel'))\n client.end()\n }, SSH_READY_TIMEOUT_MS)\n client.exec(REMOTE_SYNC_HELPER_COMMAND, (error, opened) => {\n clearTimeout(execDeadline)\n if (error) reject(error)\n else resolve(opened)\n })\n })\n const session = new SyncSshSession(client, channel)\n sessionOpened = true\n return { peer, session }\n } catch (error) {\n if (error instanceof SyncHostKeyChallengeError || error instanceof SyncPeerNotFoundError || error instanceof SyncSshProbeError) {\n throw error\n }\n if (challenge) throw challenge\n const category = classifySshError(error)\n if (category === 'auth' || category === 'network' || category === 'timeout') {\n throw new SyncSshProbeError(category)\n }\n if (category === 'hostkey') throw new SyncSshProbeError('hostkey')\n if (error instanceof SyncProtocolError) throw new SyncSshProbeError('protocol')\n throw new SyncSshProbeError('helper')\n } finally {\n if (!sessionOpened) {\n try {\n client.end()\n } catch {}\n }\n }\n }\n\n async fetchNodeManifest(peerId: string): Promise<SyncRemoteNodeManifest> {\n const value = await this.fetchDataOperation(peerId, 'node-manifest')\n if (!isRecord(value) || value.ok !== true || !isRecord(value.manifest)) throw new SyncSshProbeError('helper')\n const manifest = value.manifest\n if (manifest.protocolVersion !== SYNC_PROTOCOL_VERSION || typeof manifest.instanceId !== 'string' || !Array.isArray(manifest.records)) {\n throw new SyncSshProbeError('protocol')\n }\n const capabilities = value.capabilities === undefined ? undefined : value.capabilities\n if (capabilities !== undefined && (!Array.isArray(capabilities) || !capabilities.every((item) => typeof item === 'string'))) {\n throw new SyncSshProbeError('protocol')\n }\n return { ...manifest, ...(capabilities ? { capabilities } : {}) } as unknown as SyncRemoteNodeManifest\n }\n\n async applyNodeManifest(\n peerId: string,\n records: SyncNodeManifest['records'],\n sourceName: string,\n ): Promise<SyncNodeApplyResult> {\n const requestBody = Buffer.from(JSON.stringify({ records, sourceName }), 'utf8')\n if (requestBody.length > MAX_SYNC_TRANSFER_BYTES) throw new SyncSshProbeError('protocol')\n const { peer, session } = await this.openSession(peerId)\n try {\n await session.sendControl({\n kind: 'request',\n protocolVersion: SYNC_PROTOCOL_VERSION,\n operation: 'node-apply',\n port: peer.remoteWebPort ?? DEFAULT_REMOTE_WEB_PORT,\n })\n for (let offset = 0, sequence = 0; offset < requestBody.length; offset += MAX_SYNC_DATA_CHUNK_BYTES, sequence += 1) {\n await session.sendData(sequence, requestBody.subarray(offset, Math.min(offset + MAX_SYNC_DATA_CHUNK_BYTES, requestBody.length)))\n }\n await session.sendControl({ kind: 'request-end' })\n session.endInput()\n\n let response: Record<string, unknown> | undefined\n for await (const frame of session.readFrames()) {\n if (frame.kind !== 'control' || response) throw new SyncProtocolError('unexpected node apply response frame')\n response = frame.value\n }\n if (!response || response.kind === 'error') throw new SyncSshProbeError('helper')\n if (response.kind !== 'response' || response.status !== 200 || response.body === null || typeof response.body !== 'object') {\n throw new SyncSshProbeError('helper')\n }\n const body = response.body as Record<string, unknown>\n if (body.ok !== true || body.result === null || typeof body.result !== 'object') throw new SyncSshProbeError('helper')\n const result = body.result as Record<string, unknown>\n for (const key of ['created', 'updated', 'deleted', 'pendingRemoval', 'skipped'] as const) {\n if (!Number.isSafeInteger(result[key]) || (result[key] as number) < 0) throw new SyncSshProbeError('protocol')\n }\n return result as unknown as SyncNodeApplyResult\n } catch (error) {\n if (error instanceof SyncSshProbeError) throw error\n if (error instanceof SyncProtocolError) throw new SyncSshProbeError('protocol')\n throw new SyncSshProbeError('helper')\n } finally {\n session.close()\n }\n }\n\n async fetchSessionBundle(peerId: string): Promise<SyncSessionBundle> {\n const value = await this.fetchDataOperation(peerId, 'session-bundle')\n if (!isRecord(value) || value.ok !== true || !isRecord(value.bundle)) throw new SyncSshProbeError('helper')\n const bundle = value.bundle\n if (bundle.protocolVersion !== 1 || typeof bundle.instanceId !== 'string' ||\n !Array.isArray(bundle.entries) || !Array.isArray(bundle.skipped) || typeof bundle.digest !== 'string') {\n throw new SyncSshProbeError('protocol')\n }\n return bundle as unknown as SyncSessionBundle\n }\n\n async fetchSessionManifest(peerId: string): Promise<SyncSessionManifest> {\n const value = await this.fetchDataOperation(peerId, 'session-manifest')\n if (!isRecord(value) || value.ok !== true || !isRecord(value.manifest)) throw new SyncSshProbeError('helper')\n const manifest = value.manifest\n if (manifest.protocolVersion !== 1 || typeof manifest.instanceId !== 'string' ||\n !Array.isArray(manifest.entries) || !Array.isArray(manifest.skipped) || typeof manifest.digest !== 'string') {\n throw new SyncSshProbeError('protocol')\n }\n return manifest as unknown as SyncSessionManifest\n }\n\n async fetchSessionEntries(\n peerId: string,\n keys: readonly string[],\n manifestDigest: string,\n prefixes: readonly SyncSessionEventPrefix[] = [],\n ): Promise<SyncSessionBundle> {\n const value = await this.fetchDataOperation(peerId, 'session-entries', { keys, manifestDigest, prefixes })\n if (!isRecord(value) || value.ok !== true || !isRecord(value.bundle)) throw new SyncSshProbeError('helper')\n const bundle = value.bundle\n if (bundle.protocolVersion !== 1 || typeof bundle.instanceId !== 'string' ||\n !Array.isArray(bundle.entries) || !Array.isArray(bundle.skipped) || typeof bundle.digest !== 'string') {\n throw new SyncSshProbeError('protocol')\n }\n return bundle as unknown as SyncSessionBundle\n }\n\n async applySessionBundle(peerId: string, bundle: SyncSessionBundle, sourceName: string): Promise<SyncSessionApplyResult> {\n const requestBody = Buffer.from(JSON.stringify({ bundle, sourceName, peerId }), 'utf8')\n if (requestBody.length > MAX_SYNC_TRANSFER_BYTES) throw new SyncSshProbeError('protocol')\n const { peer, session } = await this.openSession(peerId)\n try {\n await session.sendControl({\n kind: 'request',\n protocolVersion: SYNC_PROTOCOL_VERSION,\n operation: 'session-apply',\n port: peer.remoteWebPort ?? DEFAULT_REMOTE_WEB_PORT,\n })\n for (let offset = 0, sequence = 0; offset < requestBody.length; offset += MAX_SYNC_DATA_CHUNK_BYTES, sequence += 1) {\n await session.sendData(sequence, requestBody.subarray(offset, Math.min(offset + MAX_SYNC_DATA_CHUNK_BYTES, requestBody.length)))\n }\n await session.sendControl({ kind: 'request-end' })\n session.endInput()\n\n let response: Record<string, unknown> | undefined\n for await (const frame of session.readFrames()) {\n if (frame.kind !== 'control' || response) throw new SyncProtocolError('unexpected session apply response frame')\n response = frame.value\n }\n if (!response || response.kind === 'error') throw new SyncSshProbeError('helper')\n if (response.kind === 'response' && response.status === 409 && isRecord(response.body) &&\n response.body.code === 'session-delta-prefix') throw new SyncSessionDeltaPrefixError()\n if (response.kind !== 'response' || response.status !== 200 || response.body === null || typeof response.body !== 'object') {\n throw new SyncSshProbeError('helper')\n }\n const body = response.body as Record<string, unknown>\n if (body.ok !== true || body.result === null || typeof body.result !== 'object') throw new SyncSshProbeError('helper')\n const result = body.result as Record<string, unknown>\n for (const key of ['created', 'updated', 'archived', 'deleted', 'skipped', 'unmapped'] as const) {\n if (!Number.isSafeInteger(result[key]) || (result[key] as number) < 0) throw new SyncSshProbeError('protocol')\n }\n return result as unknown as SyncSessionApplyResult\n } catch (error) {\n if (error instanceof SyncSessionDeltaPrefixError) throw error\n if (error instanceof SyncSshProbeError) throw error\n if (error instanceof SyncProtocolError) throw new SyncSshProbeError('protocol')\n throw new SyncSshProbeError('helper')\n } finally {\n session.close()\n }\n }\n\n async trustObservedHostKey(peerId: string): Promise<void> {\n const { peer } = await this.peerManager.connectionData(peerId)\n const fingerprint = this.hostKeyChallenges.acceptObserved(peer)\n await this.peerManager.pinHostKey(peerId, fingerprint)\n }\n\n private async fetchDataOperation(\n peerId: string,\n operation: 'node-manifest' | 'session-bundle' | 'session-manifest' | 'session-entries',\n requestBody?: Record<string, unknown>,\n ): Promise<unknown> {\n const body = requestBody === undefined ? undefined : Buffer.from(JSON.stringify(requestBody), 'utf8')\n if (body && body.length > MAX_SYNC_TRANSFER_BYTES) throw new SyncSshProbeError('protocol')\n const { peer, session } = await this.openSession(peerId)\n try {\n await session.sendControl({\n kind: 'request',\n protocolVersion: SYNC_PROTOCOL_VERSION,\n operation,\n port: peer.remoteWebPort ?? DEFAULT_REMOTE_WEB_PORT,\n })\n if (body) {\n for (let offset = 0, sequence = 0; offset < body.length; offset += MAX_SYNC_DATA_CHUNK_BYTES, sequence += 1) {\n await session.sendData(sequence, body.subarray(offset, Math.min(offset + MAX_SYNC_DATA_CHUNK_BYTES, body.length)))\n }\n await session.sendControl({ kind: 'request-end' })\n }\n session.endInput()\n\n let responseStatus: number | undefined\n let bodyMode: string | undefined\n let responseEnded = false\n let totalBytes = 0\n const chunks: Buffer[] = []\n for await (const frame of session.readFrames()) {\n if (frame.kind === 'control') {\n if (frame.value.kind === 'error') throw new SyncSshProbeError('helper')\n if (frame.value.kind === 'response' && responseStatus === undefined) {\n responseStatus = typeof frame.value.status === 'number' ? frame.value.status : undefined\n bodyMode = typeof frame.value.bodyMode === 'string' ? frame.value.bodyMode : undefined\n continue\n }\n if (frame.value.kind === 'response-end' && responseStatus !== undefined && !responseEnded) {\n responseEnded = true\n continue\n }\n throw new SyncProtocolError(`unexpected ${operation} control frame`)\n }\n if (responseStatus === undefined || bodyMode !== 'data' || responseEnded) {\n throw new SyncProtocolError(`unexpected ${operation} data frame`)\n }\n totalBytes += frame.data.length\n if (totalBytes > MAX_SYNC_TRANSFER_BYTES) throw new SyncProtocolError(`${operation} exceeds transfer limit`)\n chunks.push(frame.data)\n }\n if (responseStatus !== 200 || !responseEnded) throw new SyncSshProbeError('helper')\n try {\n return JSON.parse(Buffer.concat(chunks, totalBytes).toString('utf8')) as unknown\n } catch {\n throw new SyncProtocolError(`${operation} response is invalid JSON`)\n }\n } catch (error) {\n if (error instanceof SyncSshProbeError) throw error\n if (error instanceof SyncProtocolError) {\n const probe = new SyncSshProbeError('protocol')\n ;(probe as Error & { detail?: string }).detail = error.message\n throw probe\n }\n throw new SyncSshProbeError('helper')\n } finally {\n session.close()\n }\n }\n\n private async requestHandshake(session: SyncSshSession, port: number): Promise<SyncRemoteHandshake> {\n await session.sendControl({\n kind: 'request',\n protocolVersion: SYNC_PROTOCOL_VERSION,\n operation: 'handshake',\n port,\n })\n session.endInput()\n\n let response: Record<string, unknown> | undefined\n for await (const frame of session.readFrames()) {\n if (frame.kind !== 'control' || response) throw new SyncProtocolError('unexpected helper frame')\n response = frame.value\n }\n if (!response) throw new SyncProtocolError('helper returned no handshake response')\n return validateHandshakeResponse(response)\n }\n}\n\nfunction validateHandshakeResponse(value: Record<string, unknown>): SyncRemoteHandshake {\n if (value.kind === 'error') throw new SyncSshProbeError('helper')\n if (value.kind !== 'response' || value.status !== 200 || value.body === null || typeof value.body !== 'object') {\n throw new SyncSshProbeError('helper')\n }\n const body = value.body as Record<string, unknown>\n if (\n typeof body.protocolVersion !== 'number' ||\n typeof body.instanceId !== 'string' ||\n !Array.isArray(body.capabilities) ||\n !body.capabilities.every((capability) => typeof capability === 'string')\n ) {\n throw new SyncSshProbeError('protocol')\n }\n return {\n protocolVersion: body.protocolVersion,\n instanceId: body.instanceId,\n capabilities: body.capabilities,\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ssh-transport.js","names":[],"sources":["../../../src/core/sync/ssh-transport.ts"],"sourcesContent":["import { Client, type ConnectConfig } from 'ssh2'\nimport type { ClientChannel } from 'ssh2'\nimport { once } from 'node:events'\nimport { classifySshError } from '../../exec-ssh/connection.js'\nimport { SyncPeerManager, SyncPeerNotFoundError, type SyncPeerSecrets } from './peer-manager.js'\nimport { isRecord, type SyncPeer } from './model.js'\nimport { SyncHostKeyChallengeError, SyncHostKeyChallenges, inspectSyncHostKey } from './host-key.js'\nimport type { SyncNodeApplyResult, SyncNodeManifest } from './node-sync.js'\nimport {\n MAX_SYNC_DATA_CHUNK_BYTES,\n MAX_SYNC_TRANSFER_BYTES,\n SYNC_PROTOCOL_VERSION,\n SyncFrameDecoder,\n SyncProtocolError,\n encodeSyncControlFrame,\n encodeSyncDataFrame,\n type SyncFrame,\n} from './protocol.js'\nimport { REMOTE_SYNC_HELPER_COMMAND } from './remote-helper.js'\nimport type { SyncSessionApplyResult, SyncSessionBundle, SyncSessionEventPrefix, SyncSessionManifest } from './session-sync.js'\n\nconst SSH_READY_TIMEOUT_MS = 30_000\nconst SSH_READ_IDLE_TIMEOUT_MS = 60_000\nconst DEFAULT_REMOTE_WEB_PORT = 3080\n\nexport type SyncSshErrorCategory = 'auth' | 'network' | 'hostkey' | 'timeout' | 'helper' | 'protocol'\n\nexport class SyncSshProbeError extends Error {\n constructor(readonly category: SyncSshErrorCategory) {\n super(`SSH sync probe failed: ${category}`)\n this.name = 'SyncSshProbeError'\n }\n}\n\nexport class SyncSessionDeltaPrefixError extends Error {\n constructor() {\n super('remote session event prefix changed during transfer')\n this.name = 'SyncSessionDeltaPrefixError'\n }\n}\n\nexport interface SyncRemoteHandshake {\n protocolVersion: number\n instanceId: string\n capabilities: string[]\n}\n\nexport interface SyncRemoteNodeManifest extends SyncNodeManifest {\n capabilities?: string[]\n}\n\nexport class SyncSshSession {\n private readonly decoder = new SyncFrameDecoder()\n private readonly closeSignal = new AbortController()\n private reading = false\n private closed = false\n private inputEnded = false\n private nextDataSequence = 0\n private pendingError: Error | undefined\n\n constructor(\n private readonly client: Client,\n private readonly channel: ClientChannel,\n ) {\n channel.on('error', (error: Error) => { this.pendingError = error })\n channel.on('exit', (code: number | null) => {\n if (code !== null && code !== 0) this.pendingError = new Error(`remote helper exited with code ${code}`)\n })\n channel.on('close', () => { this.closeSignal.abort() })\n }\n\n async sendControl(value: Record<string, unknown>): Promise<void> {\n await this.write(encodeSyncControlFrame(value))\n }\n\n async sendData(sequence: number, data: Buffer): Promise<void> {\n if (sequence !== this.nextDataSequence) throw new SyncProtocolError('outgoing data sequence is out of order')\n await this.write(encodeSyncDataFrame(sequence, data))\n this.nextDataSequence += 1\n }\n\n endInput(): void {\n if (this.closed || this.inputEnded) return\n this.inputEnded = true\n this.channel.end()\n }\n\n async *readFrames(idleTimeoutMs = SSH_READ_IDLE_TIMEOUT_MS): AsyncGenerator<SyncFrame> {\n if (this.reading) throw new SyncProtocolError('SSH sync channel can only be read once')\n this.reading = true\n const channel = this.channel\n const decoder = this.decoder\n let idleDeadline: NodeJS.Timeout | undefined\n const armIdleDeadline = (): void => {\n clearTimeout(idleDeadline)\n idleDeadline = setTimeout(() => {\n this.pendingError = new Error(`SSH channel idle for more than ${Math.round(idleTimeoutMs / 1000)}s`)\n // Full teardown: destroying the channel alone may leave the stdout\n // stream open, which would keep a plain for-await suspended forever.\n this.close()\n }, idleTimeoutMs)\n }\n // channel.destroy() does not reliably end ssh2's stdout stream; racing the\n // iterator against the close signal guarantees this generator settles.\n // A channel close resolves with a sentinel instead of rejecting: a normal\n // helper exit closes the channel around the same moment stdout reaches EOF,\n // and only a pendingError (idle timeout, helper crash) makes it an error.\n const CLOSE: unique symbol = Symbol('channel-close')\n const closed: Promise<IteratorResult<Buffer> | typeof CLOSE> = new Promise((resolve) => {\n this.closeSignal.signal.addEventListener('abort', () => resolve(CLOSE), { once: true })\n })\n const iterator = (channel.stdout as AsyncIterable<Buffer>)[Symbol.asyncIterator]()\n try {\n armIdleDeadline()\n for (;;) {\n const next = await Promise.race([iterator.next(), closed])\n if (next === CLOSE || next.done) {\n if (this.pendingError) throw new SyncProtocolError(`SSH channel error: ${this.pendingError.message}`)\n break\n }\n armIdleDeadline()\n for (const frame of decoder.push(Buffer.from(next.value))) yield frame\n }\n decoder.finish()\n } catch (error) {\n if (error instanceof SyncProtocolError) throw error\n const detail = this.pendingError ? this.pendingError.message : error instanceof Error ? error.message : 'unknown error'\n throw new SyncProtocolError(`SSH channel read failed: ${detail}`)\n } finally {\n clearTimeout(idleDeadline)\n void iterator.return?.().catch(() => {})\n }\n }\n\n close(): void {\n if (this.closed) return\n this.closed = true\n this.channel.destroy()\n this.client.end()\n }\n\n private async write(frame: Buffer): Promise<void> {\n if (this.closed || this.inputEnded) throw new SyncProtocolError('SSH sync channel input is closed')\n if (this.channel.write(frame)) return\n // A frozen connection never drains; bound the wait so writes cannot hang.\n const drainDeadline = setTimeout(() => this.closeSignal.abort(), SSH_READ_IDLE_TIMEOUT_MS)\n try {\n await once(this.channel, 'drain', { signal: this.closeSignal.signal })\n } catch (error) {\n if (this.closeSignal.signal.aborted) throw new SyncProtocolError('SSH channel closed before write could drain')\n throw error\n } finally {\n clearTimeout(drainDeadline)\n }\n }\n}\n\nexport function buildSyncSshConnectConfig(\n peer: SyncPeer,\n secrets: SyncPeerSecrets,\n challenges: SyncHostKeyChallenges,\n onChallenge: (error: SyncHostKeyChallengeError) => void,\n): ConnectConfig {\n return {\n host: peer.host,\n port: peer.port,\n username: peer.username,\n hostHash: 'sha256',\n readyTimeout: SSH_READY_TIMEOUT_MS,\n keepaliveInterval: 15_000,\n hostVerifier: (sha256Hex: string): boolean => {\n try {\n const decision = inspectSyncHostKey(peer, sha256Hex, challenges)\n if (!decision.trusted) onChallenge(decision.challenge)\n return decision.trusted\n } catch (error) {\n if (error instanceof SyncHostKeyChallengeError) onChallenge(error)\n return false\n }\n },\n ...(peer.authKind === 'password'\n ? { password: secrets.password }\n : { privateKey: secrets.privateKey, ...(secrets.passphrase ? { passphrase: secrets.passphrase } : {}) }),\n }\n}\n\nexport class SyncSshTransport {\n private readonly hostKeyChallenges = new SyncHostKeyChallenges()\n\n constructor(private readonly peerManager: SyncPeerManager) {}\n\n async probe(peerId: string): Promise<SyncRemoteHandshake> {\n const { peer, session } = await this.openSession(peerId)\n try {\n const handshake = await this.requestHandshake(session, peer.remoteWebPort ?? DEFAULT_REMOTE_WEB_PORT)\n if (handshake.protocolVersion !== SYNC_PROTOCOL_VERSION) throw new SyncSshProbeError('protocol')\n return handshake\n } catch (error) {\n if (\n error instanceof SyncHostKeyChallengeError ||\n error instanceof SyncPeerNotFoundError ||\n error instanceof SyncSshProbeError\n ) {\n throw error\n }\n if (error instanceof SyncProtocolError) throw new SyncSshProbeError('protocol')\n throw new SyncSshProbeError('helper')\n } finally {\n session.close()\n }\n }\n\n async openSession(peerId: string): Promise<{ peer: SyncPeer; session: SyncSshSession }> {\n const { peer, secrets } = await this.peerManager.connectionData(peerId)\n if (peer.authKind === 'password' && !secrets.password) throw new SyncSshProbeError('auth')\n if (peer.authKind === 'key' && !secrets.privateKey) throw new SyncSshProbeError('auth')\n\n const client = new Client()\n let challenge: SyncHostKeyChallengeError | undefined\n let sessionOpened = false\n try {\n await new Promise<void>((resolve, reject) => {\n let settled = false\n const finish = (error?: unknown): void => {\n if (settled) return\n settled = true\n clearTimeout(readyDeadline)\n if (error) reject(error)\n else resolve()\n }\n\n // ssh2's readyTimeout can be defeated by stalls behind transparent\n // proxies; this deadline guarantees the connect promise settles.\n const readyDeadline = setTimeout(() => {\n const timeout = new Error('Timed out while waiting for SSH handshake')\n ;(timeout as Error & { level?: string }).level = 'client-timeout'\n finish(timeout)\n client.end()\n }, SSH_READY_TIMEOUT_MS * 2)\n client.once('ready', () => finish())\n client.on('error', (error: Error) => finish(challenge ?? error))\n const config = buildSyncSshConnectConfig(peer, secrets, this.hostKeyChallenges, (error) => {\n challenge = error\n })\n\n try {\n client.connect(config)\n } catch (error) {\n finish(error)\n }\n })\n\n const channel = await new Promise<ClientChannel>((resolve, reject) => {\n const execDeadline = setTimeout(() => {\n reject(new Error('Timed out while waiting for the sync helper channel'))\n client.end()\n }, SSH_READY_TIMEOUT_MS)\n client.exec(REMOTE_SYNC_HELPER_COMMAND, (error, opened) => {\n clearTimeout(execDeadline)\n if (error) reject(error)\n else resolve(opened)\n })\n })\n const session = new SyncSshSession(client, channel)\n sessionOpened = true\n return { peer, session }\n } catch (error) {\n if (error instanceof SyncHostKeyChallengeError || error instanceof SyncPeerNotFoundError || error instanceof SyncSshProbeError) {\n throw error\n }\n if (challenge) throw challenge\n const category = classifySshError(error)\n if (category === 'auth' || category === 'network' || category === 'timeout') {\n throw new SyncSshProbeError(category)\n }\n if (category === 'hostkey') throw new SyncSshProbeError('hostkey')\n if (error instanceof SyncProtocolError) throw new SyncSshProbeError('protocol')\n throw new SyncSshProbeError('helper')\n } finally {\n if (!sessionOpened) {\n try {\n client.end()\n } catch {}\n }\n }\n }\n\n async fetchNodeManifest(peerId: string): Promise<SyncRemoteNodeManifest> {\n const value = await this.fetchDataOperation(peerId, 'node-manifest')\n if (!isRecord(value) || value.ok !== true || !isRecord(value.manifest)) throw new SyncSshProbeError('helper')\n const manifest = value.manifest\n if (manifest.protocolVersion !== SYNC_PROTOCOL_VERSION || typeof manifest.instanceId !== 'string' || !Array.isArray(manifest.records)) {\n throw new SyncSshProbeError('protocol')\n }\n const capabilities = value.capabilities === undefined ? undefined : value.capabilities\n if (capabilities !== undefined && (!Array.isArray(capabilities) || !capabilities.every((item) => typeof item === 'string'))) {\n throw new SyncSshProbeError('protocol')\n }\n return { ...manifest, ...(capabilities ? { capabilities } : {}) } as unknown as SyncRemoteNodeManifest\n }\n\n async applyNodeManifest(\n peerId: string,\n records: SyncNodeManifest['records'],\n sourceName: string,\n ): Promise<SyncNodeApplyResult> {\n const requestBody = Buffer.from(JSON.stringify({ records, sourceName }), 'utf8')\n if (requestBody.length > MAX_SYNC_TRANSFER_BYTES) throw new SyncSshProbeError('protocol')\n const { peer, session } = await this.openSession(peerId)\n try {\n await session.sendControl({\n kind: 'request',\n protocolVersion: SYNC_PROTOCOL_VERSION,\n operation: 'node-apply',\n port: peer.remoteWebPort ?? DEFAULT_REMOTE_WEB_PORT,\n })\n for (let offset = 0, sequence = 0; offset < requestBody.length; offset += MAX_SYNC_DATA_CHUNK_BYTES, sequence += 1) {\n await session.sendData(sequence, requestBody.subarray(offset, Math.min(offset + MAX_SYNC_DATA_CHUNK_BYTES, requestBody.length)))\n }\n await session.sendControl({ kind: 'request-end' })\n session.endInput()\n\n let response: Record<string, unknown> | undefined\n for await (const frame of session.readFrames()) {\n if (frame.kind !== 'control' || response) throw new SyncProtocolError('unexpected node apply response frame')\n response = frame.value\n }\n if (!response || response.kind === 'error') throw new SyncSshProbeError('helper')\n if (response.kind !== 'response' || response.status !== 200 || response.body === null || typeof response.body !== 'object') {\n throw new SyncSshProbeError('helper')\n }\n const body = response.body as Record<string, unknown>\n if (body.ok !== true || body.result === null || typeof body.result !== 'object') throw new SyncSshProbeError('helper')\n const result = body.result as Record<string, unknown>\n for (const key of ['created', 'updated', 'deleted', 'pendingRemoval', 'skipped'] as const) {\n if (!Number.isSafeInteger(result[key]) || (result[key] as number) < 0) throw new SyncSshProbeError('protocol')\n }\n return result as unknown as SyncNodeApplyResult\n } catch (error) {\n if (error instanceof SyncSshProbeError) throw error\n if (error instanceof SyncProtocolError) throw new SyncSshProbeError('protocol')\n throw new SyncSshProbeError('helper')\n } finally {\n session.close()\n }\n }\n\n async fetchSessionBundle(peerId: string): Promise<SyncSessionBundle> {\n const value = await this.fetchDataOperation(peerId, 'session-bundle')\n if (!isRecord(value) || value.ok !== true || !isRecord(value.bundle)) throw new SyncSshProbeError('helper')\n const bundle = value.bundle\n if (bundle.protocolVersion !== 1 || typeof bundle.instanceId !== 'string' ||\n !Array.isArray(bundle.entries) || !Array.isArray(bundle.skipped) || typeof bundle.digest !== 'string') {\n throw new SyncSshProbeError('protocol')\n }\n return bundle as unknown as SyncSessionBundle\n }\n\n async fetchSessionManifest(peerId: string): Promise<SyncSessionManifest> {\n const value = await this.fetchDataOperation(peerId, 'session-manifest')\n if (!isRecord(value) || value.ok !== true || !isRecord(value.manifest)) throw new SyncSshProbeError('helper')\n const manifest = value.manifest\n if (manifest.protocolVersion !== 1 || typeof manifest.instanceId !== 'string' ||\n !Array.isArray(manifest.entries) || !Array.isArray(manifest.skipped) || typeof manifest.digest !== 'string') {\n throw new SyncSshProbeError('protocol')\n }\n return manifest as unknown as SyncSessionManifest\n }\n\n async fetchSessionEntries(\n peerId: string,\n keys: readonly string[],\n manifestDigest: string,\n prefixes: readonly SyncSessionEventPrefix[] = [],\n ): Promise<SyncSessionBundle> {\n const value = await this.fetchDataOperation(peerId, 'session-entries', { keys, manifestDigest, prefixes })\n if (!isRecord(value) || value.ok !== true || !isRecord(value.bundle)) throw new SyncSshProbeError('helper')\n const bundle = value.bundle\n if (bundle.protocolVersion !== 1 || typeof bundle.instanceId !== 'string' ||\n !Array.isArray(bundle.entries) || !Array.isArray(bundle.skipped) || typeof bundle.digest !== 'string') {\n throw new SyncSshProbeError('protocol')\n }\n return bundle as unknown as SyncSessionBundle\n }\n\n async applySessionBundle(peerId: string, bundle: SyncSessionBundle, sourceName: string): Promise<SyncSessionApplyResult> {\n const requestBody = Buffer.from(JSON.stringify({ bundle, sourceName, peerId }), 'utf8')\n if (requestBody.length > MAX_SYNC_TRANSFER_BYTES) throw new SyncSshProbeError('protocol')\n const { peer, session } = await this.openSession(peerId)\n try {\n await session.sendControl({\n kind: 'request',\n protocolVersion: SYNC_PROTOCOL_VERSION,\n operation: 'session-apply',\n port: peer.remoteWebPort ?? DEFAULT_REMOTE_WEB_PORT,\n })\n for (let offset = 0, sequence = 0; offset < requestBody.length; offset += MAX_SYNC_DATA_CHUNK_BYTES, sequence += 1) {\n await session.sendData(sequence, requestBody.subarray(offset, Math.min(offset + MAX_SYNC_DATA_CHUNK_BYTES, requestBody.length)))\n }\n await session.sendControl({ kind: 'request-end' })\n session.endInput()\n\n let response: Record<string, unknown> | undefined\n for await (const frame of session.readFrames()) {\n if (frame.kind !== 'control' || response) throw new SyncProtocolError('unexpected session apply response frame')\n response = frame.value\n }\n if (!response || response.kind === 'error') throw new SyncSshProbeError('helper')\n if (response.kind === 'response' && response.status === 409 && isRecord(response.body) &&\n response.body.code === 'session-delta-prefix') throw new SyncSessionDeltaPrefixError()\n if (response.kind !== 'response' || response.status !== 200 || response.body === null || typeof response.body !== 'object') {\n throw new SyncSshProbeError('helper')\n }\n const body = response.body as Record<string, unknown>\n if (body.ok !== true || body.result === null || typeof body.result !== 'object') throw new SyncSshProbeError('helper')\n const result = body.result as Record<string, unknown>\n for (const key of ['created', 'updated', 'archived', 'deleted', 'skipped', 'unmapped'] as const) {\n if (!Number.isSafeInteger(result[key]) || (result[key] as number) < 0) throw new SyncSshProbeError('protocol')\n }\n return result as unknown as SyncSessionApplyResult\n } catch (error) {\n if (error instanceof SyncSessionDeltaPrefixError) throw error\n if (error instanceof SyncSshProbeError) throw error\n if (error instanceof SyncProtocolError) throw new SyncSshProbeError('protocol')\n throw new SyncSshProbeError('helper')\n } finally {\n session.close()\n }\n }\n\n async trustObservedHostKey(peerId: string): Promise<void> {\n const { peer } = await this.peerManager.connectionData(peerId)\n const fingerprint = this.hostKeyChallenges.acceptObserved(peer)\n await this.peerManager.pinHostKey(peerId, fingerprint)\n }\n\n private async fetchDataOperation(\n peerId: string,\n operation: 'node-manifest' | 'session-bundle' | 'session-manifest' | 'session-entries',\n requestBody?: Record<string, unknown>,\n ): Promise<unknown> {\n const body = requestBody === undefined ? undefined : Buffer.from(JSON.stringify(requestBody), 'utf8')\n if (body && body.length > MAX_SYNC_TRANSFER_BYTES) throw new SyncSshProbeError('protocol')\n const { peer, session } = await this.openSession(peerId)\n try {\n await session.sendControl({\n kind: 'request',\n protocolVersion: SYNC_PROTOCOL_VERSION,\n operation,\n port: peer.remoteWebPort ?? DEFAULT_REMOTE_WEB_PORT,\n })\n if (body) {\n for (let offset = 0, sequence = 0; offset < body.length; offset += MAX_SYNC_DATA_CHUNK_BYTES, sequence += 1) {\n await session.sendData(sequence, body.subarray(offset, Math.min(offset + MAX_SYNC_DATA_CHUNK_BYTES, body.length)))\n }\n await session.sendControl({ kind: 'request-end' })\n }\n session.endInput()\n\n let responseStatus: number | undefined\n let bodyMode: string | undefined\n let responseEnded = false\n let totalBytes = 0\n const chunks: Buffer[] = []\n for await (const frame of session.readFrames()) {\n if (frame.kind === 'control') {\n if (frame.value.kind === 'error') throw new SyncSshProbeError('helper')\n if (frame.value.kind === 'response' && responseStatus === undefined) {\n responseStatus = typeof frame.value.status === 'number' ? frame.value.status : undefined\n bodyMode = typeof frame.value.bodyMode === 'string' ? frame.value.bodyMode : undefined\n continue\n }\n if (frame.value.kind === 'response-end' && responseStatus !== undefined && !responseEnded) {\n responseEnded = true\n continue\n }\n throw new SyncProtocolError(`unexpected ${operation} control frame`)\n }\n if (responseStatus === undefined || bodyMode !== 'data' || responseEnded) {\n throw new SyncProtocolError(`unexpected ${operation} data frame`)\n }\n totalBytes += frame.data.length\n if (totalBytes > MAX_SYNC_TRANSFER_BYTES) throw new SyncProtocolError(`${operation} exceeds transfer limit`)\n chunks.push(frame.data)\n }\n if (responseStatus !== 200 || !responseEnded) throw new SyncSshProbeError('helper')\n try {\n return JSON.parse(Buffer.concat(chunks, totalBytes).toString('utf8')) as unknown\n } catch {\n throw new SyncProtocolError(`${operation} response is invalid JSON`)\n }\n } catch (error) {\n if (error instanceof SyncSshProbeError) throw error\n if (error instanceof SyncProtocolError) {\n const probe = new SyncSshProbeError('protocol')\n ;(probe as Error & { detail?: string }).detail = error.message\n throw probe\n }\n throw new SyncSshProbeError('helper')\n } finally {\n session.close()\n }\n }\n\n private async requestHandshake(session: SyncSshSession, port: number): Promise<SyncRemoteHandshake> {\n await session.sendControl({\n kind: 'request',\n protocolVersion: SYNC_PROTOCOL_VERSION,\n operation: 'handshake',\n port,\n })\n session.endInput()\n\n let response: Record<string, unknown> | undefined\n for await (const frame of session.readFrames()) {\n if (frame.kind !== 'control' || response) throw new SyncProtocolError('unexpected helper frame')\n response = frame.value\n }\n if (!response) throw new SyncProtocolError('helper returned no handshake response')\n return validateHandshakeResponse(response)\n }\n}\n\nfunction validateHandshakeResponse(value: Record<string, unknown>): SyncRemoteHandshake {\n if (value.kind === 'error') throw new SyncSshProbeError('helper')\n if (value.kind !== 'response' || value.status !== 200 || value.body === null || typeof value.body !== 'object') {\n throw new SyncSshProbeError('helper')\n }\n const body = value.body as Record<string, unknown>\n if (\n typeof body.protocolVersion !== 'number' ||\n typeof body.instanceId !== 'string' ||\n !Array.isArray(body.capabilities) ||\n !body.capabilities.every((capability) => typeof capability === 'string')\n ) {\n throw new SyncSshProbeError('protocol')\n }\n return {\n protocolVersion: body.protocolVersion,\n instanceId: body.instanceId,\n capabilities: body.capabilities,\n }\n}\n"],"mappings":"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"}
@@ -496,6 +496,34 @@ const SPILL_ARTIFACT_META_KEY = "@deepseek-ai/dsh-spill";
496
496
  const LEGACY_LOCAL_SPILL_HINT = "Use read with offset/limit, or grep this path to search within it.";
497
497
  const LEGACY_LOCAL_SPILL_MARKER = "Full formatted result stored at: ";
498
498
  const LEGACY_LOCAL_SPILL_SUFFIX = `. ${LEGACY_LOCAL_SPILL_HINT})`;
499
+ /**
500
+ * 导出期附件对象缓存:跨会话去重同一附件(同 id 只读一次存储、只算一次摘要)。
501
+ * 附件对象携带 base64 数据,若整轮导出无上限持有(此前是普通 Map),多图会话的
502
+ * 首次同步会把堆吃满(桌面端 OOM 崩溃的直接推手之一)。这里按原始字节数做
503
+ * LRU 上限,超限淘汰最早条目;被淘汰的附件在后续会话中按 cache miss 重新读取。
504
+ */
505
+ const MAX_EXPORT_OBJECT_CACHE_BYTES = 67108864;
506
+ var ExportObjectCache = class {
507
+ entries = /* @__PURE__ */ new Map();
508
+ bytes = 0;
509
+ get(key) {
510
+ return this.entries.get(key);
511
+ }
512
+ set(key, entry) {
513
+ const existing = this.entries.get(key);
514
+ if (existing) this.bytes -= existing.object.bytes;
515
+ this.entries.delete(key);
516
+ this.entries.set(key, entry);
517
+ this.bytes += entry.object.bytes;
518
+ while (this.bytes > MAX_EXPORT_OBJECT_CACHE_BYTES && this.entries.size > 1) {
519
+ const oldest = this.entries.keys().next().value;
520
+ if (oldest === void 0) break;
521
+ const removed = this.entries.get(oldest);
522
+ this.entries.delete(oldest);
523
+ if (removed) this.bytes -= removed.object.bytes;
524
+ }
525
+ }
526
+ };
499
527
  var SyncSessionError = class extends Error {
500
528
  code;
501
529
  constructor(code, message, cause) {
@@ -865,30 +893,43 @@ function sessionEventDigest(header, inheritedEventCount, events, workspace, inst
865
893
  return output;
866
894
  };
867
895
  const { id: _id, cwd: _cwd, parentSession: _parent, ...stableHeader } = header;
868
- return sha256(canonicalJson({
869
- header: stableHeader,
870
- inheritedEventCount,
871
- events: events.map(normalize),
872
- workspace: {
873
- originInstanceId: workspace.originInstanceId,
874
- originWorkspaceId: workspace.originWorkspaceId,
875
- nodeOrigin: workspace.nodeOrigin,
876
- remotePath: workspace.remotePath
877
- },
878
- parentOrigin,
879
- instructionSnapshot: instructionSnapshot ? {
880
- text: instructionSnapshot.text,
881
- capturedAt: instructionSnapshot.capturedAt,
882
- workspaceOrigin: instructionSnapshot.workspaceOrigin,
883
- workspaceRemotePath: instructionSnapshot.workspaceRemotePath,
884
- workspaceRevision: instructionSnapshot.workspaceRevision,
885
- digest: instructionSnapshot.digest,
886
- inheritedFromOrigin: instructionSnapshot.inheritedFromOrigin
887
- } : void 0
888
- }));
896
+ const hash = createHash("sha256");
897
+ hash.update("{\"events\":[");
898
+ for (let index = 0; index < events.length; index += 1) {
899
+ if (index > 0) hash.update(",");
900
+ hash.update(canonicalJson(normalize(events[index])));
901
+ }
902
+ hash.update("]");
903
+ hash.update(`,"header":${canonicalJson(stableHeader)}`);
904
+ hash.update(`,"inheritedEventCount":${canonicalJson(inheritedEventCount)}`);
905
+ hash.update(`,"instructionSnapshot":${instructionSnapshot ? canonicalJson({
906
+ text: instructionSnapshot.text,
907
+ capturedAt: instructionSnapshot.capturedAt,
908
+ workspaceOrigin: instructionSnapshot.workspaceOrigin,
909
+ workspaceRemotePath: instructionSnapshot.workspaceRemotePath,
910
+ workspaceRevision: instructionSnapshot.workspaceRevision,
911
+ digest: instructionSnapshot.digest,
912
+ inheritedFromOrigin: instructionSnapshot.inheritedFromOrigin
913
+ }) : "undefined"}`);
914
+ hash.update(`,"parentOrigin":${parentOrigin ? canonicalJson(parentOrigin) : "undefined"}`);
915
+ hash.update(`,"workspace":${canonicalJson({
916
+ originInstanceId: workspace.originInstanceId,
917
+ originWorkspaceId: workspace.originWorkspaceId,
918
+ nodeOrigin: workspace.nodeOrigin,
919
+ remotePath: workspace.remotePath
920
+ })}`);
921
+ hash.update("}");
922
+ return `sha256:${hash.digest("hex")}`;
889
923
  }
890
924
  function sessionEventListDigest(events) {
891
- return sha256(canonicalJson(events));
925
+ const hash = createHash("sha256");
926
+ hash.update("[");
927
+ for (let index = 0; index < events.length; index += 1) {
928
+ if (index > 0) hash.update(",");
929
+ hash.update(canonicalJson(events[index]));
930
+ }
931
+ hash.update("]");
932
+ return `sha256:${hash.digest("hex")}`;
892
933
  }
893
934
  function sessionStateDigest(workspace, workspacePosition, archived, pinned) {
894
935
  return sha256(canonicalJson({
@@ -1292,7 +1333,7 @@ function validateSyncSessionBundle(value) {
1292
1333
  digest: value.digest
1293
1334
  };
1294
1335
  if (bundleDigest(bundle) !== bundle.digest) throw new SyncSessionError("manifest-digest", "session bundle digest does not match");
1295
- if (Buffer.byteLength(JSON.stringify(bundle), "utf8") > 107374182400) throw new SyncSessionError("bundle-too-large", "session bundle exceeds the transfer limit");
1336
+ if (Buffer.byteLength(JSON.stringify(bundle), "utf8") > 134217728) throw new SyncSessionError("bundle-too-large", "session bundle exceeds the transfer limit");
1296
1337
  return bundle;
1297
1338
  }
1298
1339
  function makeSyncSessionBundle(instanceId, entries, skipped = [], sharedObjects) {
@@ -1314,7 +1355,7 @@ function makeSyncSessionBundle(instanceId, entries, skipped = [], sharedObjects)
1314
1355
  digest: ""
1315
1356
  };
1316
1357
  bundle.digest = bundleDigest(bundle);
1317
- if (Buffer.byteLength(JSON.stringify(bundle), "utf8") > 107374182400) throw new SyncSessionError("bundle-too-large", "session bundle exceeds the transfer limit");
1358
+ if (Buffer.byteLength(JSON.stringify(bundle), "utf8") > 134217728) throw new SyncSessionError("bundle-too-large", "session bundle exceeds the transfer limit");
1318
1359
  return bundle;
1319
1360
  }
1320
1361
  const bundle = {
@@ -1325,7 +1366,7 @@ function makeSyncSessionBundle(instanceId, entries, skipped = [], sharedObjects)
1325
1366
  digest: ""
1326
1367
  };
1327
1368
  bundle.digest = bundleDigest(bundle);
1328
- if (Buffer.byteLength(JSON.stringify(bundle), "utf8") > 107374182400) throw new SyncSessionError("bundle-too-large", "session bundle exceeds the transfer limit");
1369
+ if (Buffer.byteLength(JSON.stringify(bundle), "utf8") > 134217728) throw new SyncSessionError("bundle-too-large", "session bundle exceeds the transfer limit");
1329
1370
  return bundle;
1330
1371
  }
1331
1372
  function syncSessionEntryKey(entry) {
@@ -1380,7 +1421,7 @@ function makeSyncSessionManifest(instanceId, entries, skipped = []) {
1380
1421
  digest: ""
1381
1422
  };
1382
1423
  manifest.digest = sessionManifestDigest(manifest);
1383
- if (Buffer.byteLength(JSON.stringify(manifest), "utf8") > 107374182400) throw new SyncSessionError("bundle-too-large", "session manifest exceeds the transfer limit");
1424
+ if (Buffer.byteLength(JSON.stringify(manifest), "utf8") > 134217728) throw new SyncSessionError("bundle-too-large", "session manifest exceeds the transfer limit");
1384
1425
  return manifest;
1385
1426
  }
1386
1427
  function validateSyncSessionManifest(value) {
@@ -1399,7 +1440,7 @@ function validateSyncSessionManifest(value) {
1399
1440
  digest: value.digest
1400
1441
  };
1401
1442
  if (sessionManifestDigest(manifest) !== manifest.digest) throw new SyncSessionError("manifest-digest", "session manifest digest does not match");
1402
- if (Buffer.byteLength(JSON.stringify(manifest), "utf8") > 107374182400) throw new SyncSessionError("bundle-too-large", "session manifest exceeds the transfer limit");
1443
+ if (Buffer.byteLength(JSON.stringify(manifest), "utf8") > 134217728) throw new SyncSessionError("bundle-too-large", "session manifest exceeds the transfer limit");
1403
1444
  return manifest;
1404
1445
  }
1405
1446
  function emptyApplyResult() {
@@ -1711,7 +1752,7 @@ var SyncSessionService = class {
1711
1752
  const eligibleLocalIds = /* @__PURE__ */ new Set();
1712
1753
  const workspaceById = /* @__PURE__ */ new Map();
1713
1754
  const archivedBranchSessionIds = new Set(Object.values(stateIndex.state.archives).map((archive) => archive.localSessionId));
1714
- const objectCache = /* @__PURE__ */ new Map();
1755
+ const objectCache = new ExportObjectCache();
1715
1756
  const descriptorCache = /* @__PURE__ */ new Map();
1716
1757
  const getWorkspaceDescriptor = (workspace, binding, nodeOrigin, preferredOrigin) => {
1717
1758
  const key = canonicalJson({
@@ -1764,7 +1805,17 @@ var SyncSessionService = class {
1764
1805
  continue;
1765
1806
  }
1766
1807
  }
1767
- const exported = await this.exportSessionEntry(persistence, workspace, descriptor, position, SessionId(sessionId), snapshot, archivedIds.has(sessionId), pinnedIds.has(sessionId), void 0, stateIndex, objectCache);
1808
+ let exported;
1809
+ try {
1810
+ exported = await this.exportSessionEntry(persistence, workspace, descriptor, position, SessionId(sessionId), snapshot, archivedIds.has(sessionId), pinnedIds.has(sessionId), void 0, stateIndex, objectCache);
1811
+ } catch (error) {
1812
+ if (!(error instanceof SyncSessionError && error.code === "session-too-large")) throw error;
1813
+ skipped.push({
1814
+ sessionId,
1815
+ reason: "session-payload-too-large"
1816
+ });
1817
+ continue;
1818
+ }
1768
1819
  if (exported) {
1769
1820
  const { entry } = exported;
1770
1821
  if (manifestOnly && options.payloadCache) {
@@ -1828,7 +1879,17 @@ var SyncSessionService = class {
1828
1879
  const descriptor = getWorkspaceDescriptor(workspace, binding, nodeOrigin, stateIndex.archivesByLocalSessionId.get(archive.localSessionId)?.workspaceOrigin);
1829
1880
  const snapshot = await persistence.stat(SessionId(archive.localSessionId));
1830
1881
  if (!snapshot) continue;
1831
- const exported = await this.exportSessionEntry(persistence, workspace, descriptor, archive.workspacePosition, SessionId(archive.localSessionId), snapshot, true, false, archive, stateIndex, objectCache);
1882
+ let exported;
1883
+ try {
1884
+ exported = await this.exportSessionEntry(persistence, workspace, descriptor, archive.workspacePosition, SessionId(archive.localSessionId), snapshot, true, false, archive, stateIndex, objectCache);
1885
+ } catch (error) {
1886
+ if (!(error instanceof SyncSessionError && error.code === "session-too-large")) throw error;
1887
+ skipped.push({
1888
+ sessionId: archive.localSessionId,
1889
+ reason: "session-payload-too-large"
1890
+ });
1891
+ continue;
1892
+ }
1832
1893
  if (exported) {
1833
1894
  const { entry } = exported;
1834
1895
  if (manifestOnly && options.payloadCache) {
@@ -2829,7 +2890,13 @@ var SyncSessionService = class {
2829
2890
  payloads.set(key, cached);
2830
2891
  continue;
2831
2892
  }
2832
- const data = await collectBytes(attachmentStore.readFileStream(ref));
2893
+ let data;
2894
+ try {
2895
+ data = await collectBytes(attachmentStore.readFileStream(ref));
2896
+ } catch (error) {
2897
+ if (error instanceof SyncSessionError && error.code === "bundle-too-large") throw new SyncSessionError("session-too-large", "会话文件附件超过传输大小限制");
2898
+ throw error;
2899
+ }
2833
2900
  const object = {
2834
2901
  id: key,
2835
2902
  kind: "file",
@@ -2908,6 +2975,6 @@ var SyncSessionService = class {
2908
2975
  }
2909
2976
  };
2910
2977
  //#endregion
2911
- export { planSessionTransfers as a, validateSyncSessionBundle as c, makeSyncSessionManifest as i, validateSyncSessionManifest as l, SyncSessionService as n, sessionEventListDigest as o, makeSyncSessionBundle as r, syncSessionEntryKey as s, SyncSessionError as t };
2978
+ export { makeSyncSessionBundle as a, sessionEventListDigest as c, validateSyncSessionManifest as d, estimateJsonBytes as i, syncSessionEntryKey as l, SyncSessionError as n, makeSyncSessionManifest as o, SyncSessionService as r, planSessionTransfers as s, ExportObjectCache as t, validateSyncSessionBundle as u };
2912
2979
 
2913
- //# sourceMappingURL=session-sync-C0uoizyB.js.map
2980
+ //# sourceMappingURL=session-sync-BQt3HXkp.js.map