@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.
- package/lib/core/index.js +2 -2
- package/lib/core/index.js.map +1 -1
- package/lib/core/sync/coordinator.d.ts +2 -2
- package/lib/core/sync/coordinator.js +135 -15
- package/lib/core/sync/coordinator.js.map +1 -1
- package/lib/core/sync/internal-routes.d.ts +1 -1
- package/lib/core/sync/internal-routes.js +58 -9
- package/lib/core/sync/internal-routes.js.map +1 -1
- package/lib/core/sync/offline-archive.d.ts +1 -1
- package/lib/core/sync/offline-archive.js +1 -1
- package/lib/core/sync/offline-routes.js +1 -1
- package/lib/core/sync/protocol.d.ts +2 -2
- package/lib/core/sync/protocol.js +4 -1
- package/lib/core/sync/protocol.js.map +1 -1
- package/lib/core/sync/remote-helper.js +10 -4
- package/lib/core/sync/remote-helper.js.map +1 -1
- package/lib/core/sync/routes.d.ts +1 -1
- package/lib/core/sync/session-routes.d.ts +1 -1
- package/lib/core/sync/session-routes.js +1 -1
- package/lib/core/sync/session-sync.d.ts +2 -2
- package/lib/core/sync/session-sync.js +2 -2
- package/lib/core/sync/ssh-transport.d.ts +2 -2
- package/lib/core/sync/ssh-transport.js +127 -107
- package/lib/core/sync/ssh-transport.js.map +1 -1
- package/lib/{protocol-CV7LGE44.d.ts → protocol-KaWZArY8.d.ts} +5 -2
- package/lib/{session-sync-EdfwUON5.js → session-sync-C0uoizyB.js} +834 -98
- package/lib/session-sync-C0uoizyB.js.map +1 -0
- package/lib/{session-sync-g6qKZzjV.d.ts → session-sync-C2j2rk9K.d.ts} +56 -4
- package/lib/{ssh-transport-BcIiLxsX.d.ts → ssh-transport-gb2PVatu.d.ts} +15 -6
- package/package.json +1 -1
- package/lib/session-sync-EdfwUON5.js.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as SyncSessionError } from "../../session-sync-
|
|
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
|
|
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":";;AAEA,MAAa,wBAAwB;AACrC,MAAa,+BAA+B;AAC5C,MAAa,4BAA4B;AACzC,MAAa,0BAA0B;AAEvC,MAAM,qBAAqB;AAC3B,MAAM,gBAAgB;AACtB,MAAM,aAAa;AACnB,MAAM,sBAAsB;AAM5B,IAAa,oBAAb,cAAuC,MAAM;CAC3C,YAAY,SAAiB;EAC3B,MAAM,OAAO;EACb,KAAK,OAAO;CACd;AACF;AAEA,SAAS,WAAW,MAAc,eAA+B;CAC/D,MAAM,SAAS,OAAO,YAAY,kBAAkB;CACpD,OAAO,KAAK;CACZ,OAAO,cAAc,eAAe,CAAC;CACrC,OAAO;AACT;AAEA,SAAgB,uBAAuB,OAAwC;CAC7E,MAAM,UAAU,OAAO,KAAK,GAAG,KAAK,UAAU,KAAK,EAAE,KAAK,MAAM;CAChE,IAAI,QAAQ,SAAA,OAAuC,MAAM,IAAI,kBAAkB,kCAAkC;CACjH,OAAO,OAAO,OAAO,CAAC,WAAW,eAAe,QAAQ,MAAM,GAAG,OAAO,CAAC;AAC3E;AAEA,SAAgB,oBAAoB,UAAkB,MAAsB;CAC1E,IAAI,CAAC,OAAO,cAAc,QAAQ,KAAK,WAAW,KAAK,WAAW,YAChE,MAAM,IAAI,kBAAkB,0BAA0B;CAExD,IAAI,KAAK,SAAA,SAAoC,MAAM,IAAI,kBAAkB,+BAA+B;CACxG,MAAM,WAAW,OAAO,YAAY,mBAAmB;CACvD,SAAS,cAAc,UAAU,CAAC;CAClC,WAAW,QAAQ,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,KAAK,UAAU,CAAC;CAC3D,OAAO,OAAO,OAAO;EAAC,WAAW,YAAY,SAAS,SAAS,KAAK,MAAM;EAAG;EAAU;CAAI,CAAC;AAC9F;AAEA,IAAa,mBAAb,MAA8B;CAC5B,SAA0B,OAAO,YAAY,kBAAkB;CAC/D,eAAuB;CACvB,YAAoB;CACpB;CACA,gBAAwB;CACxB,mBAA2B;CAC3B,QAAgB;CAEhB,KAAK,OAA4B;EAC/B,IAAI,KAAK,OAAO,MAAM,IAAI,kBAAkB,mCAAmC;EAC/E,MAAM,SAAsB,CAAC;EAC7B,IAAI,cAAc;EAElB,OAAO,cAAc,MAAM,QAAQ;GACjC,IAAI,CAAC,KAAK,SAAS;IACjB,MAAM,SAAS,KAAK,IAAI,qBAAqB,KAAK,cAAc,MAAM,SAAS,WAAW;IAC1F,MAAM,KAAK,KAAK,QAAQ,KAAK,cAAc,aAAa,cAAc,MAAM;IAC5E,KAAK,gBAAgB;IACrB,eAAe;IACf,IAAI,KAAK,eAAe,oBAAoB;IAE5C,KAAK,YAAY,KAAK,OAAO;IAC7B,MAAM,gBAAgB,KAAK,OAAO,aAAa,CAAC;IAChD,IAAI,KAAK,cAAc,eACjB;SAAA,gBAAgB,KAAK,gBAAA,OACvB,MAAM,IAAI,kBAAkB,iCAAiC;IAAA,OAE1D,IAAI,KAAK,cAAc,YACxB;SAAA,gBAAgB,uBAAuB,gBAAgB,SACzD,MAAM,IAAI,kBAAkB,8BAA8B;IAAA,OAG5D,MAAM,IAAI,kBAAkB,oBAAoB;IAElD,KAAK,UAAU,OAAO,YAAY,aAAa;IAC/C,KAAK,gBAAgB;GACvB;GAEA,MAAM,UAAU,KAAK;GACrB,MAAM,SAAS,KAAK,IAAI,QAAQ,SAAS,KAAK,eAAe,MAAM,SAAS,WAAW;GACvF,MAAM,KAAK,SAAS,KAAK,eAAe,aAAa,cAAc,MAAM;GACzE,KAAK,iBAAiB;GACtB,eAAe;GAEf,IAAI,KAAK,kBAAkB,QAAQ,QAAQ;IACzC,OAAO,KAAK,KAAK,YAAY,KAAK,WAAW,OAAO,CAAC;IACrD,KAAK,WAAW;GAClB;EACF;EACA,OAAO;CACT;CAEA,SAAe;EACb,IAAI,KAAK,iBAAiB,KAAK,KAAK,YAAY,KAAA,GAAW,MAAM,IAAI,kBAAkB,wBAAwB;EAC/G,KAAK,QAAQ;CACf;CAEA,YAAoB,MAAc,SAA4B;EAC5D,IAAI,SAAS,eAAe;GAC1B,IAAI,QAAQ,QAAQ,SAAS,OAAO,IAAM,MAAM,IAAI,kBAAkB,uCAAuC;GAC7G,IAAI;GACJ,IAAI;IACF,QAAQ,KAAK,MAAM,QAAQ,SAAS,GAAG,EAAE,CAAC,CAAC,SAAS,MAAM,CAAC;GAC7D,QAAQ;IACN,MAAM,IAAI,kBAAkB,qCAAqC;GACnE;GACA,IAAI,UAAU,QAAQ,OAAO,UAAU,YAAY,MAAM,QAAQ,KAAK,GACpE,MAAM,IAAI,kBAAkB,sCAAsC;GAEpE,OAAO;IAAE,MAAM;IAAkB;GAAiC;EACpE;EAEA,MAAM,WAAW,QAAQ,aAAa,CAAC;EACvC,IAAI,aAAa,KAAK,kBAAkB,MAAM,IAAI,kBAAkB,+BAA+B;EACnG,MAAM,iBAAiB,QAAQ,SAAS,GAAG,mBAAmB;EAC9D,MAAM,OAAO,QAAQ,SAAS,mBAAmB;EACjD,MAAM,eAAe,WAAW,QAAQ,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,OAAO;EAC9D,IAAI,CAAC,gBAAgB,gBAAgB,YAAY,GAAG,MAAM,IAAI,kBAAkB,4BAA4B;EAC5G,KAAK,oBAAoB;EACzB,OAAO;GAAE,MAAM;GAAQ;GAAU,QAAQ,aAAa,SAAS,KAAK;GAAG,MAAM,OAAO,KAAK,IAAI;EAAE;CACjG;CAEA,aAA2B;EACzB,KAAK,eAAe;EACpB,KAAK,YAAY;EACjB,KAAK,UAAU,KAAA;EACf,KAAK,gBAAgB;CACvB;AACF"}
|
|
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
|
-
|
|
137
|
-
|
|
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:
|
|
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 {
|
|
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 {
|
|
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,2 +1,2 @@
|
|
|
1
|
-
import { a as SyncSessionError, c as
|
|
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
|
|
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
|
|
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
|
-
|
|
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:
|
|
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-
|
|
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
|
|
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
|
|
191
|
-
|
|
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 (
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
|
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
|
-
|
|
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: "
|
|
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
|
|
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
|
-
"
|
|
259
|
-
"
|
|
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
|
|
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
|
|
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(
|
|
372
|
+
throw new SyncProtocolError(`unexpected ${operation} control frame`);
|
|
298
373
|
}
|
|
299
|
-
if (responseStatus === void 0 || bodyMode !== "data" || responseEnded) throw new SyncProtocolError(
|
|
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(
|
|
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
|
-
|
|
381
|
+
return JSON.parse(Buffer.concat(chunks, totalBytes).toString("utf8"));
|
|
308
382
|
} catch {
|
|
309
|
-
throw new SyncProtocolError(
|
|
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
|