@artenx/dshb 0.1.22 → 0.1.24

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 (31) hide show
  1. package/lib/core/index.js +2 -2
  2. package/lib/core/index.js.map +1 -1
  3. package/lib/core/sync/coordinator.d.ts +2 -2
  4. package/lib/core/sync/coordinator.js +135 -15
  5. package/lib/core/sync/coordinator.js.map +1 -1
  6. package/lib/core/sync/internal-routes.d.ts +1 -1
  7. package/lib/core/sync/internal-routes.js +58 -9
  8. package/lib/core/sync/internal-routes.js.map +1 -1
  9. package/lib/core/sync/offline-archive.d.ts +1 -1
  10. package/lib/core/sync/offline-archive.js +1 -1
  11. package/lib/core/sync/offline-routes.js +1 -1
  12. package/lib/core/sync/protocol.d.ts +2 -2
  13. package/lib/core/sync/protocol.js +4 -1
  14. package/lib/core/sync/protocol.js.map +1 -1
  15. package/lib/core/sync/remote-helper.js +10 -4
  16. package/lib/core/sync/remote-helper.js.map +1 -1
  17. package/lib/core/sync/routes.d.ts +1 -1
  18. package/lib/core/sync/session-routes.d.ts +1 -1
  19. package/lib/core/sync/session-routes.js +1 -1
  20. package/lib/core/sync/session-sync.d.ts +2 -2
  21. package/lib/core/sync/session-sync.js +2 -2
  22. package/lib/core/sync/ssh-transport.d.ts +2 -2
  23. package/lib/core/sync/ssh-transport.js +127 -107
  24. package/lib/core/sync/ssh-transport.js.map +1 -1
  25. package/lib/{protocol-CV7LGE44.d.ts → protocol-KaWZArY8.d.ts} +5 -2
  26. package/lib/{session-sync-EdfwUON5.js → session-sync-C0uoizyB.js} +834 -98
  27. package/lib/session-sync-C0uoizyB.js.map +1 -0
  28. package/lib/{session-sync-g6qKZzjV.d.ts → session-sync-C2j2rk9K.d.ts} +56 -4
  29. package/lib/{ssh-transport-BcIiLxsX.d.ts → ssh-transport-gb2PVatu.d.ts} +15 -6
  30. package/package.json +1 -1
  31. package/lib/session-sync-EdfwUON5.js.map +0 -1
@@ -1,4 +1,4 @@
1
- import { t as SyncSessionError } from "../../session-sync-EdfwUON5.js";
1
+ import { t as SyncSessionError } from "../../session-sync-C0uoizyB.js";
2
2
  import { SyncOfflineArchiveError } from "./offline-archive.js";
3
3
  import { createReadStream } from "node:fs";
4
4
  import { randomBytes } from "node:crypto";
@@ -1,2 +1,2 @@
1
- import { a as SyncFrame, c as encodeSyncControlFrame, i as SYNC_PROTOCOL_VERSION, l as encodeSyncDataFrame, n as MAX_SYNC_DATA_CHUNK_BYTES, o as SyncFrameDecoder, r as MAX_SYNC_TRANSFER_BYTES, s as SyncProtocolError, t as MAX_SYNC_CONTROL_FRAME_BYTES } from "../../protocol-CV7LGE44.js";
2
- export { MAX_SYNC_CONTROL_FRAME_BYTES, MAX_SYNC_DATA_CHUNK_BYTES, MAX_SYNC_TRANSFER_BYTES, SYNC_PROTOCOL_VERSION, SyncFrame, SyncFrameDecoder, SyncProtocolError, encodeSyncControlFrame, encodeSyncDataFrame };
1
+ import { a as SYNC_SESSION_BUNDLE_OBJECTS_CAPABILITY, c as SyncFrame, d as encodeSyncControlFrame, f as encodeSyncDataFrame, i as SYNC_PROTOCOL_VERSION, l as SyncFrameDecoder, n as MAX_SYNC_DATA_CHUNK_BYTES, o as SYNC_SESSION_EVENT_TAIL_CAPABILITY, r as MAX_SYNC_TRANSFER_BYTES, s as SYNC_SESSION_MANIFEST_CAPABILITY, t as MAX_SYNC_CONTROL_FRAME_BYTES, u as SyncProtocolError } from "../../protocol-KaWZArY8.js";
2
+ export { MAX_SYNC_CONTROL_FRAME_BYTES, MAX_SYNC_DATA_CHUNK_BYTES, MAX_SYNC_TRANSFER_BYTES, SYNC_PROTOCOL_VERSION, SYNC_SESSION_BUNDLE_OBJECTS_CAPABILITY, SYNC_SESSION_EVENT_TAIL_CAPABILITY, SYNC_SESSION_MANIFEST_CAPABILITY, SyncFrame, SyncFrameDecoder, SyncProtocolError, encodeSyncControlFrame, encodeSyncDataFrame };
@@ -1,6 +1,9 @@
1
1
  import { createHash, timingSafeEqual } from "node:crypto";
2
2
  //#region src/core/sync/protocol.ts
3
3
  const SYNC_PROTOCOL_VERSION = 1;
4
+ const SYNC_SESSION_MANIFEST_CAPABILITY = "session-manifest-v1";
5
+ const SYNC_SESSION_EVENT_TAIL_CAPABILITY = "session-event-tail-delta-v1";
6
+ const SYNC_SESSION_BUNDLE_OBJECTS_CAPABILITY = "session-bundle-objects-v1";
4
7
  const MAX_SYNC_CONTROL_FRAME_BYTES = 65536;
5
8
  const MAX_SYNC_DATA_CHUNK_BYTES = 1048576;
6
9
  const MAX_SYNC_TRANSFER_BYTES = 107374182400;
@@ -119,6 +122,6 @@ var SyncFrameDecoder = class {
119
122
  }
120
123
  };
121
124
  //#endregion
122
- export { MAX_SYNC_CONTROL_FRAME_BYTES, MAX_SYNC_DATA_CHUNK_BYTES, MAX_SYNC_TRANSFER_BYTES, SYNC_PROTOCOL_VERSION, SyncFrameDecoder, SyncProtocolError, encodeSyncControlFrame, encodeSyncDataFrame };
125
+ export { MAX_SYNC_CONTROL_FRAME_BYTES, MAX_SYNC_DATA_CHUNK_BYTES, MAX_SYNC_TRANSFER_BYTES, SYNC_PROTOCOL_VERSION, SYNC_SESSION_BUNDLE_OBJECTS_CAPABILITY, SYNC_SESSION_EVENT_TAIL_CAPABILITY, SYNC_SESSION_MANIFEST_CAPABILITY, SyncFrameDecoder, SyncProtocolError, encodeSyncControlFrame, encodeSyncDataFrame };
123
126
 
124
127
  //# sourceMappingURL=protocol.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"protocol.js","names":[],"sources":["../../../src/core/sync/protocol.ts"],"sourcesContent":["import { createHash, timingSafeEqual } from 'node:crypto'\n\nexport const SYNC_PROTOCOL_VERSION = 1\nexport const MAX_SYNC_CONTROL_FRAME_BYTES = 64 * 1024\nexport const MAX_SYNC_DATA_CHUNK_BYTES = 1024 * 1024\nexport const MAX_SYNC_TRANSFER_BYTES = 100 * 1024 * 1024 * 1024\n\nconst FRAME_HEADER_BYTES = 5\nconst CONTROL_FRAME = 0\nconst DATA_FRAME = 1\nconst DATA_METADATA_BYTES = 36\n\nexport type SyncFrame =\n | { kind: 'control'; value: Record<string, unknown> }\n | { kind: 'data'; sequence: number; digest: string; data: Buffer }\n\nexport class SyncProtocolError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'SyncProtocolError'\n }\n}\n\nfunction makeHeader(type: number, payloadLength: number): Buffer {\n const header = Buffer.allocUnsafe(FRAME_HEADER_BYTES)\n header[0] = type\n header.writeUInt32BE(payloadLength, 1)\n return header\n}\n\nexport function encodeSyncControlFrame(value: Record<string, unknown>): Buffer {\n const payload = Buffer.from(`${JSON.stringify(value)}\\n`, 'utf8')\n if (payload.length > MAX_SYNC_CONTROL_FRAME_BYTES) throw new SyncProtocolError('control frame exceeds size limit')\n return Buffer.concat([makeHeader(CONTROL_FRAME, payload.length), payload])\n}\n\nexport function encodeSyncDataFrame(sequence: number, data: Buffer): Buffer {\n if (!Number.isSafeInteger(sequence) || sequence < 0 || sequence > 0xffffffff) {\n throw new SyncProtocolError('data sequence is invalid')\n }\n if (data.length > MAX_SYNC_DATA_CHUNK_BYTES) throw new SyncProtocolError('data chunk exceeds size limit')\n const metadata = Buffer.allocUnsafe(DATA_METADATA_BYTES)\n metadata.writeUInt32BE(sequence, 0)\n createHash('sha256').update(data).digest().copy(metadata, 4)\n return Buffer.concat([makeHeader(DATA_FRAME, metadata.length + data.length), metadata, data])\n}\n\nexport class SyncFrameDecoder {\n private readonly header = Buffer.allocUnsafe(FRAME_HEADER_BYTES)\n private headerOffset = 0\n private frameType = -1\n private payload: Buffer | undefined\n private payloadOffset = 0\n private nextDataSequence = 0\n private ended = false\n\n push(chunk: Buffer): SyncFrame[] {\n if (this.ended) throw new SyncProtocolError('frame decoder is already finished')\n const frames: SyncFrame[] = []\n let inputOffset = 0\n\n while (inputOffset < chunk.length) {\n if (!this.payload) {\n const copied = Math.min(FRAME_HEADER_BYTES - this.headerOffset, chunk.length - inputOffset)\n chunk.copy(this.header, this.headerOffset, inputOffset, inputOffset + copied)\n this.headerOffset += copied\n inputOffset += copied\n if (this.headerOffset < FRAME_HEADER_BYTES) continue\n\n this.frameType = this.header[0]\n const payloadLength = this.header.readUInt32BE(1)\n if (this.frameType === CONTROL_FRAME) {\n if (payloadLength < 2 || payloadLength > MAX_SYNC_CONTROL_FRAME_BYTES) {\n throw new SyncProtocolError('control frame length is invalid')\n }\n } else if (this.frameType === DATA_FRAME) {\n if (payloadLength < DATA_METADATA_BYTES || payloadLength > DATA_METADATA_BYTES + MAX_SYNC_DATA_CHUNK_BYTES) {\n throw new SyncProtocolError('data frame length is invalid')\n }\n } else {\n throw new SyncProtocolError('unknown frame type')\n }\n this.payload = Buffer.allocUnsafe(payloadLength)\n this.payloadOffset = 0\n }\n\n const payload = this.payload\n const copied = Math.min(payload.length - this.payloadOffset, chunk.length - inputOffset)\n chunk.copy(payload, this.payloadOffset, inputOffset, inputOffset + copied)\n this.payloadOffset += copied\n inputOffset += copied\n\n if (this.payloadOffset === payload.length) {\n frames.push(this.decodeFrame(this.frameType, payload))\n this.resetFrame()\n }\n }\n return frames\n }\n\n finish(): void {\n if (this.headerOffset !== 0 || this.payload !== undefined) throw new SyncProtocolError('truncated frame stream')\n this.ended = true\n }\n\n private decodeFrame(type: number, payload: Buffer): SyncFrame {\n if (type === CONTROL_FRAME) {\n if (payload[payload.length - 1] !== 0x0a) throw new SyncProtocolError('control frame must end with a newline')\n let value: unknown\n try {\n value = JSON.parse(payload.subarray(0, -1).toString('utf8'))\n } catch {\n throw new SyncProtocolError('control frame contains invalid JSON')\n }\n if (value === null || typeof value !== 'object' || Array.isArray(value)) {\n throw new SyncProtocolError('control frame must contain an object')\n }\n return { kind: 'control', value: value as Record<string, unknown> }\n }\n\n const sequence = payload.readUInt32BE(0)\n if (sequence !== this.nextDataSequence) throw new SyncProtocolError('data sequence is out of order')\n const expectedDigest = payload.subarray(4, DATA_METADATA_BYTES)\n const data = payload.subarray(DATA_METADATA_BYTES)\n const actualDigest = createHash('sha256').update(data).digest()\n if (!timingSafeEqual(expectedDigest, actualDigest)) throw new SyncProtocolError('data chunk digest mismatch')\n this.nextDataSequence += 1\n return { kind: 'data', sequence, digest: actualDigest.toString('hex'), data: Buffer.from(data) }\n }\n\n private resetFrame(): void {\n this.headerOffset = 0\n this.frameType = -1\n this.payload = undefined\n this.payloadOffset = 0\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"protocol.js","names":[],"sources":["../../../src/core/sync/protocol.ts"],"sourcesContent":["import { createHash, timingSafeEqual } from 'node:crypto'\n\nexport const SYNC_PROTOCOL_VERSION = 1\nexport const SYNC_SESSION_MANIFEST_CAPABILITY = 'session-manifest-v1'\nexport const SYNC_SESSION_EVENT_TAIL_CAPABILITY = 'session-event-tail-delta-v1'\nexport const SYNC_SESSION_BUNDLE_OBJECTS_CAPABILITY = 'session-bundle-objects-v1'\nexport const MAX_SYNC_CONTROL_FRAME_BYTES = 64 * 1024\nexport const MAX_SYNC_DATA_CHUNK_BYTES = 1024 * 1024\nexport const MAX_SYNC_TRANSFER_BYTES = 100 * 1024 * 1024 * 1024\n\nconst FRAME_HEADER_BYTES = 5\nconst CONTROL_FRAME = 0\nconst DATA_FRAME = 1\nconst DATA_METADATA_BYTES = 36\n\nexport type SyncFrame =\n | { kind: 'control'; value: Record<string, unknown> }\n | { kind: 'data'; sequence: number; digest: string; data: Buffer }\n\nexport class SyncProtocolError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'SyncProtocolError'\n }\n}\n\nfunction makeHeader(type: number, payloadLength: number): Buffer {\n const header = Buffer.allocUnsafe(FRAME_HEADER_BYTES)\n header[0] = type\n header.writeUInt32BE(payloadLength, 1)\n return header\n}\n\nexport function encodeSyncControlFrame(value: Record<string, unknown>): Buffer {\n const payload = Buffer.from(`${JSON.stringify(value)}\\n`, 'utf8')\n if (payload.length > MAX_SYNC_CONTROL_FRAME_BYTES) throw new SyncProtocolError('control frame exceeds size limit')\n return Buffer.concat([makeHeader(CONTROL_FRAME, payload.length), payload])\n}\n\nexport function encodeSyncDataFrame(sequence: number, data: Buffer): Buffer {\n if (!Number.isSafeInteger(sequence) || sequence < 0 || sequence > 0xffffffff) {\n throw new SyncProtocolError('data sequence is invalid')\n }\n if (data.length > MAX_SYNC_DATA_CHUNK_BYTES) throw new SyncProtocolError('data chunk exceeds size limit')\n const metadata = Buffer.allocUnsafe(DATA_METADATA_BYTES)\n metadata.writeUInt32BE(sequence, 0)\n createHash('sha256').update(data).digest().copy(metadata, 4)\n return Buffer.concat([makeHeader(DATA_FRAME, metadata.length + data.length), metadata, data])\n}\n\nexport class SyncFrameDecoder {\n private readonly header = Buffer.allocUnsafe(FRAME_HEADER_BYTES)\n private headerOffset = 0\n private frameType = -1\n private payload: Buffer | undefined\n private payloadOffset = 0\n private nextDataSequence = 0\n private ended = false\n\n push(chunk: Buffer): SyncFrame[] {\n if (this.ended) throw new SyncProtocolError('frame decoder is already finished')\n const frames: SyncFrame[] = []\n let inputOffset = 0\n\n while (inputOffset < chunk.length) {\n if (!this.payload) {\n const copied = Math.min(FRAME_HEADER_BYTES - this.headerOffset, chunk.length - inputOffset)\n chunk.copy(this.header, this.headerOffset, inputOffset, inputOffset + copied)\n this.headerOffset += copied\n inputOffset += copied\n if (this.headerOffset < FRAME_HEADER_BYTES) continue\n\n this.frameType = this.header[0]\n const payloadLength = this.header.readUInt32BE(1)\n if (this.frameType === CONTROL_FRAME) {\n if (payloadLength < 2 || payloadLength > MAX_SYNC_CONTROL_FRAME_BYTES) {\n throw new SyncProtocolError('control frame length is invalid')\n }\n } else if (this.frameType === DATA_FRAME) {\n if (payloadLength < DATA_METADATA_BYTES || payloadLength > DATA_METADATA_BYTES + MAX_SYNC_DATA_CHUNK_BYTES) {\n throw new SyncProtocolError('data frame length is invalid')\n }\n } else {\n throw new SyncProtocolError('unknown frame type')\n }\n this.payload = Buffer.allocUnsafe(payloadLength)\n this.payloadOffset = 0\n }\n\n const payload = this.payload\n const copied = Math.min(payload.length - this.payloadOffset, chunk.length - inputOffset)\n chunk.copy(payload, this.payloadOffset, inputOffset, inputOffset + copied)\n this.payloadOffset += copied\n inputOffset += copied\n\n if (this.payloadOffset === payload.length) {\n frames.push(this.decodeFrame(this.frameType, payload))\n this.resetFrame()\n }\n }\n return frames\n }\n\n finish(): void {\n if (this.headerOffset !== 0 || this.payload !== undefined) throw new SyncProtocolError('truncated frame stream')\n this.ended = true\n }\n\n private decodeFrame(type: number, payload: Buffer): SyncFrame {\n if (type === CONTROL_FRAME) {\n if (payload[payload.length - 1] !== 0x0a) throw new SyncProtocolError('control frame must end with a newline')\n let value: unknown\n try {\n value = JSON.parse(payload.subarray(0, -1).toString('utf8'))\n } catch {\n throw new SyncProtocolError('control frame contains invalid JSON')\n }\n if (value === null || typeof value !== 'object' || Array.isArray(value)) {\n throw new SyncProtocolError('control frame must contain an object')\n }\n return { kind: 'control', value: value as Record<string, unknown> }\n }\n\n const sequence = payload.readUInt32BE(0)\n if (sequence !== this.nextDataSequence) throw new SyncProtocolError('data sequence is out of order')\n const expectedDigest = payload.subarray(4, DATA_METADATA_BYTES)\n const data = payload.subarray(DATA_METADATA_BYTES)\n const actualDigest = createHash('sha256').update(data).digest()\n if (!timingSafeEqual(expectedDigest, actualDigest)) throw new SyncProtocolError('data chunk digest mismatch')\n this.nextDataSequence += 1\n return { kind: 'data', sequence, digest: actualDigest.toString('hex'), data: Buffer.from(data) }\n }\n\n private resetFrame(): void {\n this.headerOffset = 0\n this.frameType = -1\n this.payload = undefined\n this.payloadOffset = 0\n }\n}\n"],"mappings":"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"}
@@ -10,6 +10,8 @@ const PATHS = {
10
10
  'node-manifest': { path: '/api/dshb/sync/internal/nodes/manifest', method: 'GET' },
11
11
  'node-apply': { path: '/api/dshb/sync/internal/nodes/apply', method: 'POST' },
12
12
  'session-bundle': { path: '/api/dshb/sync/internal/sessions/bundle', method: 'GET' },
13
+ 'session-manifest': { path: '/api/dshb/sync/internal/sessions/manifest', method: 'GET' },
14
+ 'session-entries': { path: '/api/dshb/sync/internal/sessions/entries', method: 'POST' },
13
15
  'session-apply': { path: '/api/dshb/sync/internal/sessions/apply', method: 'POST' },
14
16
  }
15
17
  const input = []
@@ -84,7 +86,7 @@ function parseRequest(buffer) {
84
86
  }
85
87
  if (!Number.isInteger(request.port) || request.port < 1 || request.port > 65535) throw new Error('invalid loopback port')
86
88
  const operation = PATHS[request.operation]
87
- if (request.operation === 'node-apply' || request.operation === 'session-apply') {
89
+ if (request.operation === 'node-apply' || request.operation === 'session-entries' || request.operation === 'session-apply') {
88
90
  const last = frames[frames.length - 1]
89
91
  if (!last || last.kind !== 'control' || last.value.kind !== 'request-end') throw new Error('apply request terminator required')
90
92
  if (frames.slice(1, -1).some((frame) => frame.kind !== 'data')) throw new Error('invalid apply request body')
@@ -113,7 +115,8 @@ async function run() {
113
115
  })
114
116
  }
115
117
 
116
- if (request.operation === 'node-manifest' || request.operation === 'session-bundle') {
118
+ if (request.operation === 'node-manifest' || request.operation === 'session-bundle' ||
119
+ request.operation === 'session-manifest' || request.operation === 'session-entries') {
117
120
  await writeStdout(encodeControl({ kind: 'response', status: response.status, bodyMode: 'data' }))
118
121
  if (!response.body) throw new Error('manifest response body missing')
119
122
  var sequence = 0
@@ -133,8 +136,11 @@ async function run() {
133
136
 
134
137
  var text = await response.text()
135
138
  if (Buffer.byteLength(text, 'utf8') > MAX_CONTROL) throw new Error('response control frame too large')
136
- await writeStdout(encodeControl({ kind: 'response', status: response.status, body: JSON.parse(text) }))
137
- if (!response.ok) process.exitCode = 1
139
+ var responseBody = JSON.parse(text)
140
+ await writeStdout(encodeControl({ kind: 'response', status: response.status, body: responseBody }))
141
+ var recoverableDeltaMismatch = request.operation === 'session-apply' && response.status === 409 &&
142
+ responseBody && responseBody.code === 'session-delta-prefix'
143
+ if (!response.ok && !recoverableDeltaMismatch) process.exitCode = 1
138
144
  }
139
145
 
140
146
  process.stdin.on('data', (chunk) => {
@@ -1 +1 @@
1
- {"version":3,"file":"remote-helper.js","names":[],"sources":["../../../src/core/sync/remote-helper.ts"],"sourcesContent":["import { Buffer } from 'node:buffer'\n\nconst REMOTE_HELPER_SOURCE = String.raw`'use strict'\nconst { createHash, timingSafeEqual } = require('node:crypto')\nconst MAX_CONTROL = 65536\nconst MAX_CHUNK = 1048576\nconst MAX_TRANSFER = 107374182400\nconst PATHS = {\n handshake: { path: '/api/dshb/sync/internal/handshake', method: 'GET' },\n 'node-manifest': { path: '/api/dshb/sync/internal/nodes/manifest', method: 'GET' },\n 'node-apply': { path: '/api/dshb/sync/internal/nodes/apply', method: 'POST' },\n 'session-bundle': { path: '/api/dshb/sync/internal/sessions/bundle', method: 'GET' },\n 'session-apply': { path: '/api/dshb/sync/internal/sessions/apply', method: 'POST' },\n}\nconst input = []\nlet inputBytes = 0\n\nfunction encodeControl(value) {\n const body = Buffer.from(JSON.stringify(value) + '\\n', 'utf8')\n if (body.length > MAX_CONTROL) throw new Error('control frame too large')\n const header = Buffer.alloc(5)\n header[0] = 0\n header.writeUInt32BE(body.length, 1)\n return Buffer.concat([header, body])\n}\n\nfunction encodeData(sequence, data) {\n const digest = createHash('sha256').update(data).digest()\n const metadata = Buffer.alloc(36)\n metadata.writeUInt32BE(sequence, 0)\n digest.copy(metadata, 4)\n const payload = Buffer.concat([metadata, data])\n const header = Buffer.alloc(5)\n header[0] = 1\n header.writeUInt32BE(payload.length, 1)\n return Buffer.concat([header, payload])\n}\n\nfunction decodeFrames(buffer) {\n const frames = []\n let offset = 0\n let sequence = 0\n let totalData = 0\n while (offset < buffer.length) {\n if (buffer.length - offset < 5) throw new Error('truncated frame header')\n const kind = buffer[offset]\n const length = buffer.readUInt32BE(offset + 1)\n offset += 5\n const maxLength = kind === 0 ? MAX_CONTROL : 36 + MAX_CHUNK\n if ((kind !== 0 && kind !== 1) || length > maxLength || length > buffer.length - offset) {\n throw new Error('invalid frame length')\n }\n const payload = buffer.subarray(offset, offset + length)\n offset += length\n if (kind === 0) {\n if (length < 2 || payload[length - 1] !== 10) throw new Error('invalid control frame')\n frames.push({ kind: 'control', value: JSON.parse(payload.subarray(0, -1).toString('utf8')) })\n } else {\n if (length < 36) throw new Error('invalid data frame')\n const currentSequence = payload.readUInt32BE(0)\n const expected = payload.subarray(4, 36)\n const data = payload.subarray(36)\n const actual = createHash('sha256').update(data).digest()\n if (currentSequence !== sequence || !timingSafeEqual(expected, actual)) throw new Error('invalid data chunk')\n sequence += 1\n totalData += data.length\n if (totalData > MAX_TRANSFER) throw new Error('transfer exceeds size limit')\n frames.push({ kind: 'data', data })\n }\n }\n return frames\n}\n\nfunction parseRequest(buffer) {\n const frames = decodeFrames(buffer)\n const first = frames[0]\n if (!first || first.kind !== 'control') throw new Error('request control frame required')\n const request = first.value\n if (!request || request.kind !== 'request' || request.protocolVersion !== 1 || !Object.prototype.hasOwnProperty.call(PATHS, request.operation)) {\n throw new Error('unsupported helper request')\n }\n if (Object.keys(request).some((key) => !['kind', 'protocolVersion', 'operation', 'port'].includes(key))) {\n throw new Error('unsupported helper request field')\n }\n if (!Number.isInteger(request.port) || request.port < 1 || request.port > 65535) throw new Error('invalid loopback port')\n const operation = PATHS[request.operation]\n if (request.operation === 'node-apply' || request.operation === 'session-apply') {\n const last = frames[frames.length - 1]\n if (!last || last.kind !== 'control' || last.value.kind !== 'request-end') throw new Error('apply request terminator required')\n if (frames.slice(1, -1).some((frame) => frame.kind !== 'data')) throw new Error('invalid apply request body')\n const chunks = frames.slice(1, -1).map((frame) => frame.data)\n return { request, operation, body: Buffer.concat(chunks) }\n }\n if (frames.length !== 1) throw new Error('unexpected helper request frames')\n return { request, operation }\n}\n\nasync function run() {\n const buffer = Buffer.concat(input, inputBytes)\n const { request, operation, body } = parseRequest(buffer)\n const url = 'http://127.0.0.1:' + request.port + operation.path\n const response = await fetch(url, {\n method: operation.method,\n redirect: 'error',\n headers: { accept: 'application/json', ...(body ? { 'content-type': 'application/json' } : {}) },\n ...(body ? { body } : {}),\n })\n\n function writeStdout(data) {\n return new Promise(function(resolve, reject) {\n if (process.stdout.write(data)) resolve()\n else process.stdout.once('drain', resolve)\n })\n }\n\n if (request.operation === 'node-manifest' || request.operation === 'session-bundle') {\n await writeStdout(encodeControl({ kind: 'response', status: response.status, bodyMode: 'data' }))\n if (!response.body) throw new Error('manifest response body missing')\n var sequence = 0\n var total = 0\n for await (var chunk of response.body) {\n var data = Buffer.from(chunk)\n total += data.length\n if (total > MAX_TRANSFER) throw new Error('manifest exceeds size limit')\n for (var offset = 0; offset < data.length; offset += MAX_CHUNK) {\n await writeStdout(encodeData(sequence++, data.subarray(offset, Math.min(offset + MAX_CHUNK, data.length))))\n }\n }\n await writeStdout(encodeControl({ kind: 'response-end' }))\n if (!response.ok) process.exitCode = 1\n return\n }\n\n var text = await response.text()\n if (Buffer.byteLength(text, 'utf8') > MAX_CONTROL) throw new Error('response control frame too large')\n await writeStdout(encodeControl({ kind: 'response', status: response.status, body: JSON.parse(text) }))\n if (!response.ok) process.exitCode = 1\n}\n\nprocess.stdin.on('data', (chunk) => {\n inputBytes += chunk.length\n if (inputBytes > MAX_TRANSFER + 2 * MAX_CONTROL) {\n process.exitCode = 1\n process.stdin.destroy()\n return\n }\n input.push(Buffer.from(chunk))\n})\nprocess.stdin.on('end', () => {\n run().catch(() => {\n try { process.stdout.write(encodeControl({ kind: 'error', code: 'helper-failed' })) } catch {}\n process.exitCode = 1\n })\n})\n`\n\nconst REMOTE_HELPER_BASE64 = Buffer.from(REMOTE_HELPER_SOURCE, 'utf8').toString('base64')\n\nexport const REMOTE_SYNC_HELPER_COMMAND =\n `node -e 'eval(Buffer.from(\"${REMOTE_HELPER_BASE64}\",\"base64\").toString(\"utf8\"))'`\n"],"mappings":";;AAEA,MAAM,uBAAuB,OAAO,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4JvC,MAAa,6BACX,8BAH2B,OAAO,KAAK,sBAAsB,MAAM,CAAC,CAAC,SAAS,QAGhD,EAAqB"}
1
+ {"version":3,"file":"remote-helper.js","names":[],"sources":["../../../src/core/sync/remote-helper.ts"],"sourcesContent":["import { Buffer } from 'node:buffer'\n\nconst REMOTE_HELPER_SOURCE = String.raw`'use strict'\nconst { createHash, timingSafeEqual } = require('node:crypto')\nconst MAX_CONTROL = 65536\nconst MAX_CHUNK = 1048576\nconst MAX_TRANSFER = 107374182400\nconst PATHS = {\n handshake: { path: '/api/dshb/sync/internal/handshake', method: 'GET' },\n 'node-manifest': { path: '/api/dshb/sync/internal/nodes/manifest', method: 'GET' },\n 'node-apply': { path: '/api/dshb/sync/internal/nodes/apply', method: 'POST' },\n 'session-bundle': { path: '/api/dshb/sync/internal/sessions/bundle', method: 'GET' },\n 'session-manifest': { path: '/api/dshb/sync/internal/sessions/manifest', method: 'GET' },\n 'session-entries': { path: '/api/dshb/sync/internal/sessions/entries', method: 'POST' },\n 'session-apply': { path: '/api/dshb/sync/internal/sessions/apply', method: 'POST' },\n}\nconst input = []\nlet inputBytes = 0\n\nfunction encodeControl(value) {\n const body = Buffer.from(JSON.stringify(value) + '\\n', 'utf8')\n if (body.length > MAX_CONTROL) throw new Error('control frame too large')\n const header = Buffer.alloc(5)\n header[0] = 0\n header.writeUInt32BE(body.length, 1)\n return Buffer.concat([header, body])\n}\n\nfunction encodeData(sequence, data) {\n const digest = createHash('sha256').update(data).digest()\n const metadata = Buffer.alloc(36)\n metadata.writeUInt32BE(sequence, 0)\n digest.copy(metadata, 4)\n const payload = Buffer.concat([metadata, data])\n const header = Buffer.alloc(5)\n header[0] = 1\n header.writeUInt32BE(payload.length, 1)\n return Buffer.concat([header, payload])\n}\n\nfunction decodeFrames(buffer) {\n const frames = []\n let offset = 0\n let sequence = 0\n let totalData = 0\n while (offset < buffer.length) {\n if (buffer.length - offset < 5) throw new Error('truncated frame header')\n const kind = buffer[offset]\n const length = buffer.readUInt32BE(offset + 1)\n offset += 5\n const maxLength = kind === 0 ? MAX_CONTROL : 36 + MAX_CHUNK\n if ((kind !== 0 && kind !== 1) || length > maxLength || length > buffer.length - offset) {\n throw new Error('invalid frame length')\n }\n const payload = buffer.subarray(offset, offset + length)\n offset += length\n if (kind === 0) {\n if (length < 2 || payload[length - 1] !== 10) throw new Error('invalid control frame')\n frames.push({ kind: 'control', value: JSON.parse(payload.subarray(0, -1).toString('utf8')) })\n } else {\n if (length < 36) throw new Error('invalid data frame')\n const currentSequence = payload.readUInt32BE(0)\n const expected = payload.subarray(4, 36)\n const data = payload.subarray(36)\n const actual = createHash('sha256').update(data).digest()\n if (currentSequence !== sequence || !timingSafeEqual(expected, actual)) throw new Error('invalid data chunk')\n sequence += 1\n totalData += data.length\n if (totalData > MAX_TRANSFER) throw new Error('transfer exceeds size limit')\n frames.push({ kind: 'data', data })\n }\n }\n return frames\n}\n\nfunction parseRequest(buffer) {\n const frames = decodeFrames(buffer)\n const first = frames[0]\n if (!first || first.kind !== 'control') throw new Error('request control frame required')\n const request = first.value\n if (!request || request.kind !== 'request' || request.protocolVersion !== 1 || !Object.prototype.hasOwnProperty.call(PATHS, request.operation)) {\n throw new Error('unsupported helper request')\n }\n if (Object.keys(request).some((key) => !['kind', 'protocolVersion', 'operation', 'port'].includes(key))) {\n throw new Error('unsupported helper request field')\n }\n if (!Number.isInteger(request.port) || request.port < 1 || request.port > 65535) throw new Error('invalid loopback port')\n const operation = PATHS[request.operation]\n if (request.operation === 'node-apply' || request.operation === 'session-entries' || request.operation === 'session-apply') {\n const last = frames[frames.length - 1]\n if (!last || last.kind !== 'control' || last.value.kind !== 'request-end') throw new Error('apply request terminator required')\n if (frames.slice(1, -1).some((frame) => frame.kind !== 'data')) throw new Error('invalid apply request body')\n const chunks = frames.slice(1, -1).map((frame) => frame.data)\n return { request, operation, body: Buffer.concat(chunks) }\n }\n if (frames.length !== 1) throw new Error('unexpected helper request frames')\n return { request, operation }\n}\n\nasync function run() {\n const buffer = Buffer.concat(input, inputBytes)\n const { request, operation, body } = parseRequest(buffer)\n const url = 'http://127.0.0.1:' + request.port + operation.path\n const response = await fetch(url, {\n method: operation.method,\n redirect: 'error',\n headers: { accept: 'application/json', ...(body ? { 'content-type': 'application/json' } : {}) },\n ...(body ? { body } : {}),\n })\n\n function writeStdout(data) {\n return new Promise(function(resolve, reject) {\n if (process.stdout.write(data)) resolve()\n else process.stdout.once('drain', resolve)\n })\n }\n\n if (request.operation === 'node-manifest' || request.operation === 'session-bundle' ||\n request.operation === 'session-manifest' || request.operation === 'session-entries') {\n await writeStdout(encodeControl({ kind: 'response', status: response.status, bodyMode: 'data' }))\n if (!response.body) throw new Error('manifest response body missing')\n var sequence = 0\n var total = 0\n for await (var chunk of response.body) {\n var data = Buffer.from(chunk)\n total += data.length\n if (total > MAX_TRANSFER) throw new Error('manifest exceeds size limit')\n for (var offset = 0; offset < data.length; offset += MAX_CHUNK) {\n await writeStdout(encodeData(sequence++, data.subarray(offset, Math.min(offset + MAX_CHUNK, data.length))))\n }\n }\n await writeStdout(encodeControl({ kind: 'response-end' }))\n if (!response.ok) process.exitCode = 1\n return\n }\n\n var text = await response.text()\n if (Buffer.byteLength(text, 'utf8') > MAX_CONTROL) throw new Error('response control frame too large')\n var responseBody = JSON.parse(text)\n await writeStdout(encodeControl({ kind: 'response', status: response.status, body: responseBody }))\n var recoverableDeltaMismatch = request.operation === 'session-apply' && response.status === 409 &&\n responseBody && responseBody.code === 'session-delta-prefix'\n if (!response.ok && !recoverableDeltaMismatch) process.exitCode = 1\n}\n\nprocess.stdin.on('data', (chunk) => {\n inputBytes += chunk.length\n if (inputBytes > MAX_TRANSFER + 2 * MAX_CONTROL) {\n process.exitCode = 1\n process.stdin.destroy()\n return\n }\n input.push(Buffer.from(chunk))\n})\nprocess.stdin.on('end', () => {\n run().catch(() => {\n try { process.stdout.write(encodeControl({ kind: 'error', code: 'helper-failed' })) } catch {}\n process.exitCode = 1\n })\n})\n`\n\nconst REMOTE_HELPER_BASE64 = Buffer.from(REMOTE_HELPER_SOURCE, 'utf8').toString('base64')\n\nexport const REMOTE_SYNC_HELPER_COMMAND =\n `node -e 'eval(Buffer.from(\"${REMOTE_HELPER_BASE64}\",\"base64\").toString(\"utf8\"))'`\n"],"mappings":";;AAEA,MAAM,uBAAuB,OAAO,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkKvC,MAAa,6BACX,8BAH2B,OAAO,KAAK,sBAAsB,MAAM,CAAC,CAAC,SAAS,QAGhD,EAAqB"}
@@ -1,5 +1,5 @@
1
1
  import { SyncPeerManager } from "./peer-manager.js";
2
- import { a as SyncSshTransport } from "../../ssh-transport-BcIiLxsX.js";
2
+ import { s as SyncSshTransport } from "../../ssh-transport-gb2PVatu.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
@@ -1,4 +1,4 @@
1
- import { c as SyncSessionService } from "../../session-sync-g6qKZzjV.js";
1
+ import { f as SyncSessionService } from "../../session-sync-C2j2rk9K.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 { t as SyncSessionError } from "../../session-sync-EdfwUON5.js";
1
+ import { t as SyncSessionError } from "../../session-sync-C0uoizyB.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 { a as SyncSessionError, c as SyncSessionService, d as WorkspaceRegistryLike, f as makeSyncSessionBundle, i as SyncSessionEntry, l as SyncSessionTransferPlan, m as validateSyncSessionBundle, n as SyncSessionApplyResult, o as SyncSessionInstructionSnapshot, p as planSessionTransfers, r as SyncSessionBundle, s as SyncSessionObject, t as SyncSessionApplyOptions, u as SyncSessionWorkspace } from "../../session-sync-g6qKZzjV.js";
2
- export { SyncSessionApplyOptions, SyncSessionApplyResult, SyncSessionBundle, SyncSessionEntry, SyncSessionError, SyncSessionInstructionSnapshot, SyncSessionObject, SyncSessionService, SyncSessionTransferPlan, SyncSessionWorkspace, WorkspaceRegistryLike, makeSyncSessionBundle, planSessionTransfers, validateSyncSessionBundle };
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,2 +1,2 @@
1
- import { a as validateSyncSessionBundle, i as planSessionTransfers, n as SyncSessionService, r as makeSyncSessionBundle, t as SyncSessionError } from "../../session-sync-EdfwUON5.js";
2
- export { SyncSessionError, SyncSessionService, makeSyncSessionBundle, planSessionTransfers, validateSyncSessionBundle };
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,2 +1,2 @@
1
- import { a as SyncSshTransport, i as SyncSshSession, n as SyncSshErrorCategory, o as buildSyncSshConnectConfig, r as SyncSshProbeError, t as SyncRemoteHandshake } from "../../ssh-transport-BcIiLxsX.js";
2
- export { SyncRemoteHandshake, SyncSshErrorCategory, SyncSshProbeError, SyncSshSession, SyncSshTransport, buildSyncSshConnectConfig };
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-gb2PVatu.js";
2
+ export { SyncRemoteHandshake, SyncRemoteNodeManifest, SyncSessionDeltaPrefixError, SyncSshErrorCategory, SyncSshProbeError, SyncSshSession, SyncSshTransport, buildSyncSshConnectConfig };
@@ -8,6 +8,7 @@ import { Client } from "ssh2";
8
8
  import { once } from "node:events";
9
9
  //#region src/core/sync/ssh-transport.ts
10
10
  const SSH_READY_TIMEOUT_MS = 3e4;
11
+ const SSH_READ_IDLE_TIMEOUT_MS = 6e4;
11
12
  const DEFAULT_REMOTE_WEB_PORT = 3080;
12
13
  var SyncSshProbeError = class extends Error {
13
14
  category;
@@ -17,6 +18,12 @@ var SyncSshProbeError = class extends Error {
17
18
  this.name = "SyncSshProbeError";
18
19
  }
19
20
  };
21
+ var SyncSessionDeltaPrefixError = class extends Error {
22
+ constructor() {
23
+ super("remote session event prefix changed during transfer");
24
+ this.name = "SyncSessionDeltaPrefixError";
25
+ }
26
+ };
20
27
  var SyncSshSession = class {
21
28
  client;
22
29
  channel;
@@ -49,19 +56,33 @@ var SyncSshSession = class {
49
56
  this.inputEnded = true;
50
57
  this.channel.end();
51
58
  }
52
- async *readFrames() {
59
+ async *readFrames(idleTimeoutMs = SSH_READ_IDLE_TIMEOUT_MS) {
53
60
  if (this.reading) throw new SyncProtocolError("SSH sync channel can only be read once");
54
61
  this.reading = true;
55
62
  const channel = this.channel;
56
63
  const decoder = this.decoder;
64
+ let idleDeadline;
65
+ const armIdleDeadline = () => {
66
+ clearTimeout(idleDeadline);
67
+ idleDeadline = setTimeout(() => {
68
+ this.pendingError = /* @__PURE__ */ new Error(`SSH channel idle for more than ${Math.round(idleTimeoutMs / 1e3)}s`);
69
+ channel.destroy();
70
+ }, idleTimeoutMs);
71
+ };
57
72
  try {
58
- for await (const chunk of channel.stdout) for (const frame of decoder.push(Buffer.from(chunk))) yield frame;
73
+ armIdleDeadline();
74
+ for await (const chunk of channel.stdout) {
75
+ armIdleDeadline();
76
+ for (const frame of decoder.push(Buffer.from(chunk))) yield frame;
77
+ }
59
78
  if (this.pendingError) throw new SyncProtocolError(`SSH channel error: ${this.pendingError.message}`);
60
79
  decoder.finish();
61
80
  } catch (error) {
62
81
  if (error instanceof SyncProtocolError) throw error;
63
82
  const detail = this.pendingError ? this.pendingError.message : error instanceof Error ? error.message : "unknown error";
64
83
  throw new SyncProtocolError(`SSH channel read failed: ${detail}`);
84
+ } finally {
85
+ clearTimeout(idleDeadline);
65
86
  }
66
87
  }
67
88
  close() {
@@ -82,7 +103,7 @@ function buildSyncSshConnectConfig(peer, secrets, challenges, onChallenge) {
82
103
  username: peer.username,
83
104
  hostHash: "sha256",
84
105
  readyTimeout: SSH_READY_TIMEOUT_MS,
85
- keepaliveInterval: 0,
106
+ keepaliveInterval: 15e3,
86
107
  hostVerifier: (sha256Hex) => {
87
108
  try {
88
109
  const decision = inspectSyncHostKey(peer, sha256Hex, challenges);
@@ -132,9 +153,16 @@ var SyncSshTransport = class {
132
153
  const finish = (error) => {
133
154
  if (settled) return;
134
155
  settled = true;
156
+ clearTimeout(readyDeadline);
135
157
  if (error) reject(error);
136
158
  else resolve();
137
159
  };
160
+ const readyDeadline = setTimeout(() => {
161
+ const timeout = /* @__PURE__ */ new Error("Timed out while waiting for SSH handshake");
162
+ timeout.level = "client-timeout";
163
+ finish(timeout);
164
+ client.end();
165
+ }, SSH_READY_TIMEOUT_MS * 2);
138
166
  client.once("ready", () => finish());
139
167
  client.on("error", (error) => finish(challenge ?? error));
140
168
  const config = buildSyncSshConnectConfig(peer, secrets, this.hostKeyChallenges, (error) => {
@@ -147,7 +175,12 @@ var SyncSshTransport = class {
147
175
  }
148
176
  });
149
177
  const session = new SyncSshSession(client, await new Promise((resolve, reject) => {
178
+ const execDeadline = setTimeout(() => {
179
+ reject(/* @__PURE__ */ new Error("Timed out while waiting for the sync helper channel"));
180
+ client.end();
181
+ }, SSH_READY_TIMEOUT_MS);
150
182
  client.exec(REMOTE_SYNC_HELPER_COMMAND, (error, opened) => {
183
+ clearTimeout(execDeadline);
151
184
  if (error) reject(error);
152
185
  else resolve(opened);
153
186
  });
@@ -172,50 +205,52 @@ var SyncSshTransport = class {
172
205
  }
173
206
  }
174
207
  async fetchNodeManifest(peerId) {
208
+ const value = await this.fetchDataOperation(peerId, "node-manifest");
209
+ if (!isRecord(value) || value.ok !== true || !isRecord(value.manifest)) throw new SyncSshProbeError("helper");
210
+ const manifest = value.manifest;
211
+ if (manifest.protocolVersion !== 1 || typeof manifest.instanceId !== "string" || !Array.isArray(manifest.records)) throw new SyncSshProbeError("protocol");
212
+ const capabilities = value.capabilities === void 0 ? void 0 : value.capabilities;
213
+ if (capabilities !== void 0 && (!Array.isArray(capabilities) || !capabilities.every((item) => typeof item === "string"))) throw new SyncSshProbeError("protocol");
214
+ return {
215
+ ...manifest,
216
+ ...capabilities ? { capabilities } : {}
217
+ };
218
+ }
219
+ async applyNodeManifest(peerId, records, sourceName) {
220
+ const requestBody = Buffer.from(JSON.stringify({
221
+ records,
222
+ sourceName
223
+ }), "utf8");
224
+ if (requestBody.length > 107374182400) throw new SyncSshProbeError("protocol");
175
225
  const { peer, session } = await this.openSession(peerId);
176
226
  try {
177
227
  await session.sendControl({
178
228
  kind: "request",
179
229
  protocolVersion: 1,
180
- operation: "node-manifest",
230
+ operation: "node-apply",
181
231
  port: peer.remoteWebPort ?? DEFAULT_REMOTE_WEB_PORT
182
232
  });
233
+ for (let offset = 0, sequence = 0; offset < requestBody.length; offset += MAX_SYNC_DATA_CHUNK_BYTES, sequence += 1) await session.sendData(sequence, requestBody.subarray(offset, Math.min(offset + MAX_SYNC_DATA_CHUNK_BYTES, requestBody.length)));
234
+ await session.sendControl({ kind: "request-end" });
183
235
  session.endInput();
184
- let responseStatus;
185
- let bodyMode;
186
- let responseEnded = false;
187
- let totalBytes = 0;
188
- const chunks = [];
236
+ let response;
189
237
  for await (const frame of session.readFrames()) {
190
- if (frame.kind === "control") {
191
- if (frame.value.kind === "error") throw new SyncSshProbeError("helper");
192
- if (frame.value.kind === "response" && responseStatus === void 0) {
193
- responseStatus = typeof frame.value.status === "number" ? frame.value.status : void 0;
194
- bodyMode = typeof frame.value.bodyMode === "string" ? frame.value.bodyMode : void 0;
195
- continue;
196
- }
197
- if (frame.value.kind === "response-end" && responseStatus !== void 0 && !responseEnded) {
198
- responseEnded = true;
199
- continue;
200
- }
201
- throw new SyncProtocolError("unexpected node manifest control frame");
202
- }
203
- if (responseStatus === void 0 || bodyMode !== "data" || responseEnded) throw new SyncProtocolError("unexpected node manifest data frame");
204
- totalBytes += frame.data.length;
205
- if (totalBytes > 107374182400) throw new SyncProtocolError("node manifest exceeds transfer limit");
206
- chunks.push(frame.data);
207
- }
208
- if (responseStatus !== 200 || !responseEnded) throw new SyncSshProbeError("helper");
209
- let value;
210
- try {
211
- value = JSON.parse(Buffer.concat(chunks, totalBytes).toString("utf8"));
212
- } catch {
213
- throw new SyncProtocolError("node manifest response is invalid JSON");
238
+ if (frame.kind !== "control" || response) throw new SyncProtocolError("unexpected node apply response frame");
239
+ response = frame.value;
214
240
  }
215
- if (value === null || typeof value !== "object" || Array.isArray(value) || value.ok !== true) throw new SyncSshProbeError("helper");
216
- const manifest = value.manifest;
217
- if (manifest === null || typeof manifest !== "object" || Array.isArray(manifest) || manifest.protocolVersion !== 1 || typeof manifest.instanceId !== "string" || !Array.isArray(manifest.records)) throw new SyncSshProbeError("protocol");
218
- return manifest;
241
+ if (!response || response.kind === "error") throw new SyncSshProbeError("helper");
242
+ if (response.kind !== "response" || response.status !== 200 || response.body === null || typeof response.body !== "object") throw new SyncSshProbeError("helper");
243
+ const body = response.body;
244
+ if (body.ok !== true || body.result === null || typeof body.result !== "object") throw new SyncSshProbeError("helper");
245
+ const result = body.result;
246
+ for (const key of [
247
+ "created",
248
+ "updated",
249
+ "deleted",
250
+ "pendingRemoval",
251
+ "skipped"
252
+ ]) if (!Number.isSafeInteger(result[key]) || result[key] < 0) throw new SyncSshProbeError("protocol");
253
+ return result;
219
254
  } catch (error) {
220
255
  if (error instanceof SyncSshProbeError) throw error;
221
256
  if (error instanceof SyncProtocolError) throw new SyncSshProbeError("protocol");
@@ -224,10 +259,36 @@ var SyncSshTransport = class {
224
259
  session.close();
225
260
  }
226
261
  }
227
- async applyNodeManifest(peerId, records, sourceName) {
262
+ async fetchSessionBundle(peerId) {
263
+ const value = await this.fetchDataOperation(peerId, "session-bundle");
264
+ if (!isRecord(value) || value.ok !== true || !isRecord(value.bundle)) throw new SyncSshProbeError("helper");
265
+ const bundle = value.bundle;
266
+ if (bundle.protocolVersion !== 1 || typeof bundle.instanceId !== "string" || !Array.isArray(bundle.entries) || !Array.isArray(bundle.skipped) || typeof bundle.digest !== "string") throw new SyncSshProbeError("protocol");
267
+ return bundle;
268
+ }
269
+ async fetchSessionManifest(peerId) {
270
+ const value = await this.fetchDataOperation(peerId, "session-manifest");
271
+ if (!isRecord(value) || value.ok !== true || !isRecord(value.manifest)) throw new SyncSshProbeError("helper");
272
+ const manifest = value.manifest;
273
+ if (manifest.protocolVersion !== 1 || typeof manifest.instanceId !== "string" || !Array.isArray(manifest.entries) || !Array.isArray(manifest.skipped) || typeof manifest.digest !== "string") throw new SyncSshProbeError("protocol");
274
+ return manifest;
275
+ }
276
+ async fetchSessionEntries(peerId, keys, manifestDigest, prefixes = []) {
277
+ const value = await this.fetchDataOperation(peerId, "session-entries", {
278
+ keys,
279
+ manifestDigest,
280
+ prefixes
281
+ });
282
+ if (!isRecord(value) || value.ok !== true || !isRecord(value.bundle)) throw new SyncSshProbeError("helper");
283
+ const bundle = value.bundle;
284
+ if (bundle.protocolVersion !== 1 || typeof bundle.instanceId !== "string" || !Array.isArray(bundle.entries) || !Array.isArray(bundle.skipped) || typeof bundle.digest !== "string") throw new SyncSshProbeError("protocol");
285
+ return bundle;
286
+ }
287
+ async applySessionBundle(peerId, bundle, sourceName) {
228
288
  const requestBody = Buffer.from(JSON.stringify({
229
- records,
230
- sourceName
289
+ bundle,
290
+ sourceName,
291
+ peerId
231
292
  }), "utf8");
232
293
  if (requestBody.length > 107374182400) throw new SyncSshProbeError("protocol");
233
294
  const { peer, session } = await this.openSession(peerId);
@@ -235,7 +296,7 @@ var SyncSshTransport = class {
235
296
  await session.sendControl({
236
297
  kind: "request",
237
298
  protocolVersion: 1,
238
- operation: "node-apply",
299
+ operation: "session-apply",
239
300
  port: peer.remoteWebPort ?? DEFAULT_REMOTE_WEB_PORT
240
301
  });
241
302
  for (let offset = 0, sequence = 0; offset < requestBody.length; offset += MAX_SYNC_DATA_CHUNK_BYTES, sequence += 1) await session.sendData(sequence, requestBody.subarray(offset, Math.min(offset + MAX_SYNC_DATA_CHUNK_BYTES, requestBody.length)));
@@ -243,10 +304,11 @@ var SyncSshTransport = class {
243
304
  session.endInput();
244
305
  let response;
245
306
  for await (const frame of session.readFrames()) {
246
- if (frame.kind !== "control" || response) throw new SyncProtocolError("unexpected node apply response frame");
307
+ if (frame.kind !== "control" || response) throw new SyncProtocolError("unexpected session apply response frame");
247
308
  response = frame.value;
248
309
  }
249
310
  if (!response || response.kind === "error") throw new SyncSshProbeError("helper");
311
+ if (response.kind === "response" && response.status === 409 && isRecord(response.body) && response.body.code === "session-delta-prefix") throw new SyncSessionDeltaPrefixError();
250
312
  if (response.kind !== "response" || response.status !== 200 || response.body === null || typeof response.body !== "object") throw new SyncSshProbeError("helper");
251
313
  const body = response.body;
252
314
  if (body.ok !== true || body.result === null || typeof body.result !== "object") throw new SyncSshProbeError("helper");
@@ -254,12 +316,14 @@ var SyncSshTransport = class {
254
316
  for (const key of [
255
317
  "created",
256
318
  "updated",
319
+ "archived",
257
320
  "deleted",
258
- "pendingRemoval",
259
- "skipped"
321
+ "skipped",
322
+ "unmapped"
260
323
  ]) if (!Number.isSafeInteger(result[key]) || result[key] < 0) throw new SyncSshProbeError("protocol");
261
324
  return result;
262
325
  } catch (error) {
326
+ if (error instanceof SyncSessionDeltaPrefixError) throw error;
263
327
  if (error instanceof SyncSshProbeError) throw error;
264
328
  if (error instanceof SyncProtocolError) throw new SyncSshProbeError("protocol");
265
329
  throw new SyncSshProbeError("helper");
@@ -267,15 +331,26 @@ var SyncSshTransport = class {
267
331
  session.close();
268
332
  }
269
333
  }
270
- async fetchSessionBundle(peerId) {
334
+ async trustObservedHostKey(peerId) {
335
+ const { peer } = await this.peerManager.connectionData(peerId);
336
+ const fingerprint = this.hostKeyChallenges.acceptObserved(peer);
337
+ await this.peerManager.pinHostKey(peerId, fingerprint);
338
+ }
339
+ async fetchDataOperation(peerId, operation, requestBody) {
340
+ const body = requestBody === void 0 ? void 0 : Buffer.from(JSON.stringify(requestBody), "utf8");
341
+ if (body && body.length > 107374182400) throw new SyncSshProbeError("protocol");
271
342
  const { peer, session } = await this.openSession(peerId);
272
343
  try {
273
344
  await session.sendControl({
274
345
  kind: "request",
275
346
  protocolVersion: 1,
276
- operation: "session-bundle",
347
+ operation,
277
348
  port: peer.remoteWebPort ?? DEFAULT_REMOTE_WEB_PORT
278
349
  });
350
+ if (body) {
351
+ for (let offset = 0, sequence = 0; offset < body.length; offset += MAX_SYNC_DATA_CHUNK_BYTES, sequence += 1) await session.sendData(sequence, body.subarray(offset, Math.min(offset + MAX_SYNC_DATA_CHUNK_BYTES, body.length)));
352
+ await session.sendControl({ kind: "request-end" });
353
+ }
279
354
  session.endInput();
280
355
  let responseStatus;
281
356
  let bodyMode;
@@ -294,24 +369,19 @@ var SyncSshTransport = class {
294
369
  responseEnded = true;
295
370
  continue;
296
371
  }
297
- throw new SyncProtocolError("unexpected session bundle control frame");
372
+ throw new SyncProtocolError(`unexpected ${operation} control frame`);
298
373
  }
299
- if (responseStatus === void 0 || bodyMode !== "data" || responseEnded) throw new SyncProtocolError("unexpected session bundle data frame");
374
+ if (responseStatus === void 0 || bodyMode !== "data" || responseEnded) throw new SyncProtocolError(`unexpected ${operation} data frame`);
300
375
  totalBytes += frame.data.length;
301
- if (totalBytes > 107374182400) throw new SyncProtocolError("session bundle exceeds transfer limit");
376
+ if (totalBytes > 107374182400) throw new SyncProtocolError(`${operation} exceeds transfer limit`);
302
377
  chunks.push(frame.data);
303
378
  }
304
379
  if (responseStatus !== 200 || !responseEnded) throw new SyncSshProbeError("helper");
305
- let value;
306
380
  try {
307
- value = JSON.parse(Buffer.concat(chunks, totalBytes).toString("utf8"));
381
+ return JSON.parse(Buffer.concat(chunks, totalBytes).toString("utf8"));
308
382
  } catch {
309
- throw new SyncProtocolError("session bundle response is invalid JSON");
383
+ throw new SyncProtocolError(`${operation} response is invalid JSON`);
310
384
  }
311
- if (!isRecord(value) || value.ok !== true || !isRecord(value.bundle)) throw new SyncSshProbeError("helper");
312
- const bundle = value.bundle;
313
- if (bundle.protocolVersion !== 1 || typeof bundle.instanceId !== "string" || !Array.isArray(bundle.entries) || !Array.isArray(bundle.skipped) || typeof bundle.digest !== "string") throw new SyncSshProbeError("protocol");
314
- return bundle;
315
385
  } catch (error) {
316
386
  if (error instanceof SyncSshProbeError) throw error;
317
387
  if (error instanceof SyncProtocolError) {
@@ -324,56 +394,6 @@ var SyncSshTransport = class {
324
394
  session.close();
325
395
  }
326
396
  }
327
- async applySessionBundle(peerId, bundle, sourceName) {
328
- const requestBody = Buffer.from(JSON.stringify({
329
- bundle,
330
- sourceName,
331
- peerId
332
- }), "utf8");
333
- if (requestBody.length > 107374182400) throw new SyncSshProbeError("protocol");
334
- const { peer, session } = await this.openSession(peerId);
335
- try {
336
- await session.sendControl({
337
- kind: "request",
338
- protocolVersion: 1,
339
- operation: "session-apply",
340
- port: peer.remoteWebPort ?? DEFAULT_REMOTE_WEB_PORT
341
- });
342
- for (let offset = 0, sequence = 0; offset < requestBody.length; offset += MAX_SYNC_DATA_CHUNK_BYTES, sequence += 1) await session.sendData(sequence, requestBody.subarray(offset, Math.min(offset + MAX_SYNC_DATA_CHUNK_BYTES, requestBody.length)));
343
- await session.sendControl({ kind: "request-end" });
344
- session.endInput();
345
- let response;
346
- for await (const frame of session.readFrames()) {
347
- if (frame.kind !== "control" || response) throw new SyncProtocolError("unexpected session apply response frame");
348
- response = frame.value;
349
- }
350
- if (!response || response.kind === "error") throw new SyncSshProbeError("helper");
351
- if (response.kind !== "response" || response.status !== 200 || response.body === null || typeof response.body !== "object") throw new SyncSshProbeError("helper");
352
- const body = response.body;
353
- if (body.ok !== true || body.result === null || typeof body.result !== "object") throw new SyncSshProbeError("helper");
354
- const result = body.result;
355
- for (const key of [
356
- "created",
357
- "updated",
358
- "archived",
359
- "deleted",
360
- "skipped",
361
- "unmapped"
362
- ]) if (!Number.isSafeInteger(result[key]) || result[key] < 0) throw new SyncSshProbeError("protocol");
363
- return result;
364
- } catch (error) {
365
- if (error instanceof SyncSshProbeError) throw error;
366
- if (error instanceof SyncProtocolError) throw new SyncSshProbeError("protocol");
367
- throw new SyncSshProbeError("helper");
368
- } finally {
369
- session.close();
370
- }
371
- }
372
- async trustObservedHostKey(peerId) {
373
- const { peer } = await this.peerManager.connectionData(peerId);
374
- const fingerprint = this.hostKeyChallenges.acceptObserved(peer);
375
- await this.peerManager.pinHostKey(peerId, fingerprint);
376
- }
377
397
  async requestHandshake(session, port) {
378
398
  await session.sendControl({
379
399
  kind: "request",
@@ -403,6 +423,6 @@ function validateHandshakeResponse(value) {
403
423
  };
404
424
  }
405
425
  //#endregion
406
- export { SyncSshProbeError, SyncSshSession, SyncSshTransport, buildSyncSshConnectConfig };
426
+ export { SyncSessionDeltaPrefixError, SyncSshProbeError, SyncSshSession, SyncSshTransport, buildSyncSshConnectConfig };
407
427
 
408
428
  //# sourceMappingURL=ssh-transport.js.map