@artenx/dshb 0.1.18 → 0.1.19
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/client.js +2157 -567
- package/lib/client.js.map +1 -1
- package/lib/core/index.js +72 -4
- package/lib/core/index.js.map +1 -1
- package/lib/core/node-registry.d.ts +2 -108
- package/lib/core/node-registry.js +43 -2
- package/lib/core/node-registry.js.map +1 -1
- package/lib/core/routes.d.ts +4 -3
- package/lib/core/routes.js +67 -23
- package/lib/core/routes.js.map +1 -1
- package/lib/core/sync/coordinator.d.ts +48 -0
- package/lib/core/sync/coordinator.js +247 -0
- package/lib/core/sync/coordinator.js.map +1 -0
- package/lib/core/sync/host-key.d.ts +2 -0
- package/lib/core/sync/host-key.js +71 -0
- package/lib/core/sync/host-key.js.map +1 -0
- package/lib/core/sync/internal-routes.d.ts +9 -0
- package/lib/core/sync/internal-routes.js +236 -0
- package/lib/core/sync/internal-routes.js.map +1 -0
- package/lib/core/sync/model.d.ts +2 -0
- package/lib/core/sync/model.js +106 -0
- package/lib/core/sync/model.js.map +1 -0
- package/lib/core/sync/node-sync.d.ts +2 -0
- package/lib/core/sync/node-sync.js +660 -0
- package/lib/core/sync/node-sync.js.map +1 -0
- package/lib/core/sync/offline-archive.d.ts +55 -0
- package/lib/core/sync/offline-archive.js +673 -0
- package/lib/core/sync/offline-archive.js.map +1 -0
- package/lib/core/sync/offline-journal.d.ts +22 -0
- package/lib/core/sync/offline-journal.js +74 -0
- package/lib/core/sync/offline-journal.js.map +1 -0
- package/lib/core/sync/offline-routes.d.ts +7 -0
- package/lib/core/sync/offline-routes.js +192 -0
- package/lib/core/sync/offline-routes.js.map +1 -0
- package/lib/core/sync/peer-manager.d.ts +56 -0
- package/lib/core/sync/peer-manager.js +258 -0
- package/lib/core/sync/peer-manager.js.map +1 -0
- package/lib/core/sync/protocol.d.ts +2 -0
- package/lib/core/sync/protocol.js +124 -0
- package/lib/core/sync/protocol.js.map +1 -0
- package/lib/core/sync/remote-helper.d.ts +5 -0
- package/lib/core/sync/remote-helper.js +160 -0
- package/lib/core/sync/remote-helper.js.map +1 -0
- package/lib/core/sync/revision.d.ts +2 -0
- package/lib/core/sync/revision.js +77 -0
- package/lib/core/sync/revision.js.map +1 -0
- package/lib/core/sync/routes.d.ts +9 -0
- package/lib/core/sync/routes.js +339 -0
- package/lib/core/sync/routes.js.map +1 -0
- package/lib/core/sync/scheduler.d.ts +14 -0
- package/lib/core/sync/scheduler.js +81 -0
- package/lib/core/sync/scheduler.js.map +1 -0
- package/lib/core/sync/session-journal.d.ts +2 -0
- package/lib/core/sync/session-journal.js +191 -0
- package/lib/core/sync/session-journal.js.map +1 -0
- package/lib/core/sync/session-routes.d.ts +7 -0
- package/lib/core/sync/session-routes.js +67 -0
- package/lib/core/sync/session-routes.js.map +1 -0
- package/lib/core/sync/session-sync.d.ts +2 -0
- package/lib/core/sync/session-sync.js +2 -0
- package/lib/core/sync/ssh-transport.d.ts +2 -0
- package/lib/core/sync/ssh-transport.js +408 -0
- package/lib/core/sync/ssh-transport.js.map +1 -0
- package/lib/core/sync/stores.d.ts +3 -0
- package/lib/core/sync/stores.js +385 -0
- package/lib/core/sync/stores.js.map +1 -0
- package/lib/core/test.d.ts +1 -1
- package/lib/core/workspace-bindings.d.ts +2 -28
- package/lib/core/workspace-bindings.js +14 -14
- package/lib/core/workspace-bindings.js.map +1 -1
- package/lib/core/workspace-instruction-context.d.ts +3 -2260
- package/lib/core/workspace-instruction-context.js +104 -2
- package/lib/core/workspace-instruction-context.js.map +1 -0
- package/lib/core/workspace-instruction-routes.d.ts +1 -1
- package/lib/core/workspace-instructions.d.ts +2 -68
- package/lib/core/workspace-instructions.js +13 -0
- package/lib/core/workspace-instructions.js.map +1 -1
- package/lib/core/workspaces.d.ts +4 -3
- package/lib/core/workspaces.js +17 -1
- package/lib/core/workspaces.js.map +1 -1
- package/lib/host-key-C5afhAUd.d.ts +30 -0
- package/lib/index-VF8bcBeX.d.ts +2293 -0
- package/lib/{workspace-instruction-context-DytAmaPY.js → lib-gdtU_WKT.js} +159 -102
- package/lib/lib-gdtU_WKT.js.map +1 -0
- package/lib/model-BP7Gw1yk.d.ts +104 -0
- package/lib/node-registry-BTnnM5-k.d.ts +112 -0
- package/lib/node-sync-3Z9QLz2n.d.ts +89 -0
- package/lib/protocol-CV7LGE44.d.ts +35 -0
- package/lib/revision-C9sOlDZ6.d.ts +25 -0
- package/lib/router/router-fs.d.ts +12 -3
- package/lib/router/router-fs.js.map +1 -1
- package/lib/session-journal-pgPqcWn9.d.ts +40 -0
- package/lib/session-sync-EdfwUON5.js +2177 -0
- package/lib/session-sync-EdfwUON5.js.map +1 -0
- package/lib/session-sync-g6qKZzjV.d.ts +1252 -0
- package/lib/ssh-transport-BcIiLxsX.d.ts +55 -0
- package/lib/stores-CKyEt0yk.d.ts +31 -0
- package/lib/workspace-bindings-CsB0TIJB.d.ts +28 -0
- package/lib/workspace-instructions-DirYYiku.d.ts +69 -0
- package/package.json +20 -1
- package/lib/workspace-instruction-context-DytAmaPY.js.map +0 -1
|
@@ -0,0 +1,673 @@
|
|
|
1
|
+
import { a as validateSyncSessionBundle, t as SyncSessionError } from "../../session-sync-EdfwUON5.js";
|
|
2
|
+
import { SyncOfflineArchiveJournalStore } from "./offline-journal.js";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { appendFileSync, chmodSync, closeSync, createReadStream, createWriteStream, mkdirSync, mkdtempSync, openSync, readSync, readdirSync, rmSync, statSync, statfsSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { createCipheriv, createDecipheriv, createHash, randomBytes, scrypt } from "node:crypto";
|
|
7
|
+
import { inflateRawSync } from "node:zlib";
|
|
8
|
+
import { Readable, Transform } from "node:stream";
|
|
9
|
+
import { pipeline } from "node:stream/promises";
|
|
10
|
+
//#region src/core/sync/offline-archive.ts
|
|
11
|
+
const ARCHIVE_MAGIC = Buffer.from("DSHBSYNC");
|
|
12
|
+
const ARCHIVE_VERSION = 1;
|
|
13
|
+
const HEADER_SIZE = 50;
|
|
14
|
+
const TAG_SIZE = 16;
|
|
15
|
+
const SALT_SIZE = 16;
|
|
16
|
+
const NONCE_SIZE = 12;
|
|
17
|
+
const SCRYPT_N = 32768;
|
|
18
|
+
const SCRYPT_R = 8;
|
|
19
|
+
const SCRYPT_P = 1;
|
|
20
|
+
const KEY_BYTES = 32;
|
|
21
|
+
const MIN_PASSWORD_BYTES = 8;
|
|
22
|
+
const MAX_PASSWORD_BYTES = 1024;
|
|
23
|
+
const MAX_OFFLINE_ARCHIVE_BYTES = 134217728;
|
|
24
|
+
const MAX_ARCHIVE_EXPANDED_BYTES = 134217728;
|
|
25
|
+
const MAX_ZIP_BYTES = 134217662;
|
|
26
|
+
const ZIP_EOCD_SIGNATURE = 101010256;
|
|
27
|
+
const ZIP_LOCAL_SIGNATURE = 67324752;
|
|
28
|
+
const ZIP_CENTRAL_SIGNATURE = 33639248;
|
|
29
|
+
const CRC_TABLE = Uint32Array.from({ length: 256 }, (_, index) => {
|
|
30
|
+
let crc = index;
|
|
31
|
+
for (let bit = 0; bit < 8; bit += 1) crc = crc & 1 ? 3988292384 ^ crc >>> 1 : crc >>> 1;
|
|
32
|
+
return crc >>> 0;
|
|
33
|
+
});
|
|
34
|
+
var SyncOfflineArchiveError = class extends Error {
|
|
35
|
+
code;
|
|
36
|
+
constructor(code, message, cause) {
|
|
37
|
+
super(message, cause === void 0 ? void 0 : { cause });
|
|
38
|
+
this.code = code;
|
|
39
|
+
this.name = "SyncOfflineArchiveError";
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
function sha256(data) {
|
|
43
|
+
return `sha256:${createHash("sha256").update(data).digest("hex")}`;
|
|
44
|
+
}
|
|
45
|
+
function canonicalJson(value) {
|
|
46
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null";
|
|
47
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
|
48
|
+
const object = value;
|
|
49
|
+
return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(object[key])}`).join(",")}}`;
|
|
50
|
+
}
|
|
51
|
+
function crc32(data) {
|
|
52
|
+
let crc = 4294967295;
|
|
53
|
+
for (const byte of data) crc = CRC_TABLE[(crc ^ byte) & 255] ^ crc >>> 8;
|
|
54
|
+
return (crc ^ 4294967295) >>> 0;
|
|
55
|
+
}
|
|
56
|
+
function validatePassword(password) {
|
|
57
|
+
if (typeof password !== "string") throw new SyncOfflineArchiveError("password-required", "Package password is required");
|
|
58
|
+
const bytes = Buffer.from(password, "utf8");
|
|
59
|
+
if (bytes.byteLength < MIN_PASSWORD_BYTES || bytes.byteLength > MAX_PASSWORD_BYTES || password.includes("\0")) throw new SyncOfflineArchiveError("password-invalid", `Package password must be ${MIN_PASSWORD_BYTES}-${MAX_PASSWORD_BYTES} UTF-8 bytes`);
|
|
60
|
+
return bytes;
|
|
61
|
+
}
|
|
62
|
+
async function deriveKey(password, salt) {
|
|
63
|
+
return new Promise((resolve, reject) => {
|
|
64
|
+
scrypt(password, salt, KEY_BYTES, {
|
|
65
|
+
N: SCRYPT_N,
|
|
66
|
+
r: SCRYPT_R,
|
|
67
|
+
p: SCRYPT_P,
|
|
68
|
+
maxmem: 67108864
|
|
69
|
+
}, (error, key) => {
|
|
70
|
+
if (error) reject(error);
|
|
71
|
+
else resolve(key);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
function archiveHeader(salt, nonce) {
|
|
76
|
+
const header = Buffer.alloc(HEADER_SIZE);
|
|
77
|
+
ARCHIVE_MAGIC.copy(header, 0);
|
|
78
|
+
header.writeUInt16BE(ARCHIVE_VERSION, 8);
|
|
79
|
+
header.writeUInt32BE(SCRYPT_N, 10);
|
|
80
|
+
header.writeUInt32BE(SCRYPT_R, 14);
|
|
81
|
+
header.writeUInt32BE(SCRYPT_P, 18);
|
|
82
|
+
salt.copy(header, 22);
|
|
83
|
+
nonce.copy(header, 38);
|
|
84
|
+
return header;
|
|
85
|
+
}
|
|
86
|
+
function parseArchiveHeader(header) {
|
|
87
|
+
if (header.length !== HEADER_SIZE || !header.subarray(0, 8).equals(ARCHIVE_MAGIC) || header.readUInt16BE(8) !== ARCHIVE_VERSION || header.readUInt32BE(10) !== SCRYPT_N || header.readUInt32BE(14) !== SCRYPT_R || header.readUInt32BE(18) !== SCRYPT_P) throw new SyncOfflineArchiveError("format-unsupported", "Offline archive format or KDF parameters are unsupported");
|
|
88
|
+
return {
|
|
89
|
+
salt: header.subarray(22, 38),
|
|
90
|
+
nonce: header.subarray(38, HEADER_SIZE)
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function safeZipName(name) {
|
|
94
|
+
return Boolean(name) && !name.startsWith("/") && !name.includes("\\") && !name.includes("\0") && name.split("/").every((part) => part !== "" && part !== "." && part !== "..") && !name.includes(":");
|
|
95
|
+
}
|
|
96
|
+
function writeZipParts(files) {
|
|
97
|
+
const localRecords = [];
|
|
98
|
+
const centralRecords = [];
|
|
99
|
+
let localOffset = 0;
|
|
100
|
+
let expandedBytes = 0;
|
|
101
|
+
for (const file of files) {
|
|
102
|
+
if (!safeZipName(file.name)) throw new SyncOfflineArchiveError("path-invalid", "Offline archive entry path is invalid");
|
|
103
|
+
expandedBytes += file.data.byteLength;
|
|
104
|
+
if (expandedBytes > MAX_ARCHIVE_EXPANDED_BYTES || file.data.byteLength > 4294967295) throw new SyncOfflineArchiveError("archive-too-large", "Offline archive contents exceed the size limit");
|
|
105
|
+
const name = Buffer.from(file.name, "utf8");
|
|
106
|
+
const checksum = crc32(file.data);
|
|
107
|
+
const localHeader = Buffer.alloc(30);
|
|
108
|
+
localHeader.writeUInt32LE(ZIP_LOCAL_SIGNATURE, 0);
|
|
109
|
+
localHeader.writeUInt16LE(20, 4);
|
|
110
|
+
localHeader.writeUInt16LE(2048, 6);
|
|
111
|
+
localHeader.writeUInt16LE(0, 8);
|
|
112
|
+
localHeader.writeUInt32LE(checksum, 14);
|
|
113
|
+
localHeader.writeUInt32LE(file.data.byteLength, 18);
|
|
114
|
+
localHeader.writeUInt32LE(file.data.byteLength, 22);
|
|
115
|
+
localHeader.writeUInt16LE(name.byteLength, 26);
|
|
116
|
+
localHeader.writeUInt16LE(0, 28);
|
|
117
|
+
localRecords.push(localHeader, name, file.data);
|
|
118
|
+
const centralHeader = Buffer.alloc(46);
|
|
119
|
+
centralHeader.writeUInt32LE(ZIP_CENTRAL_SIGNATURE, 0);
|
|
120
|
+
centralHeader.writeUInt16LE(788, 4);
|
|
121
|
+
centralHeader.writeUInt16LE(20, 6);
|
|
122
|
+
centralHeader.writeUInt16LE(2048, 8);
|
|
123
|
+
centralHeader.writeUInt16LE(0, 10);
|
|
124
|
+
centralHeader.writeUInt32LE(checksum, 16);
|
|
125
|
+
centralHeader.writeUInt32LE(file.data.byteLength, 20);
|
|
126
|
+
centralHeader.writeUInt32LE(file.data.byteLength, 24);
|
|
127
|
+
centralHeader.writeUInt16LE(name.byteLength, 28);
|
|
128
|
+
centralHeader.writeUInt16LE(0, 30);
|
|
129
|
+
centralHeader.writeUInt16LE(0, 32);
|
|
130
|
+
centralHeader.writeUInt16LE(0, 34);
|
|
131
|
+
centralHeader.writeUInt16LE(0, 36);
|
|
132
|
+
centralHeader.writeUInt32LE(33152 << 16 >>> 0, 38);
|
|
133
|
+
centralHeader.writeUInt32LE(localOffset, 42);
|
|
134
|
+
centralRecords.push(centralHeader, name);
|
|
135
|
+
localOffset += localHeader.byteLength + name.byteLength + file.data.byteLength;
|
|
136
|
+
}
|
|
137
|
+
if (files.length > 65535 || localOffset > 4294967295) throw new SyncOfflineArchiveError("archive-too-large", "Offline archive exceeds classic ZIP limits");
|
|
138
|
+
const centralBytes = centralRecords.reduce((total, record) => total + record.byteLength, 0);
|
|
139
|
+
const end = Buffer.alloc(22);
|
|
140
|
+
end.writeUInt32LE(ZIP_EOCD_SIGNATURE, 0);
|
|
141
|
+
end.writeUInt16LE(files.length, 8);
|
|
142
|
+
end.writeUInt16LE(files.length, 10);
|
|
143
|
+
end.writeUInt32LE(centralBytes, 12);
|
|
144
|
+
end.writeUInt32LE(localOffset, 16);
|
|
145
|
+
end.writeUInt16LE(0, 20);
|
|
146
|
+
const bytes = localOffset + centralBytes + end.byteLength;
|
|
147
|
+
if (bytes > MAX_ZIP_BYTES) throw new SyncOfflineArchiveError("archive-too-large", "Offline archive exceeds the size limit");
|
|
148
|
+
return {
|
|
149
|
+
chunks: [
|
|
150
|
+
...localRecords,
|
|
151
|
+
...centralRecords,
|
|
152
|
+
end
|
|
153
|
+
],
|
|
154
|
+
bytes
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
function parseZip(archive) {
|
|
158
|
+
const minimumEocd = Math.max(0, archive.length - 65557);
|
|
159
|
+
let eocd = -1;
|
|
160
|
+
for (let offset = archive.length - 22; offset >= minimumEocd; offset -= 1) if (archive.readUInt32LE(offset) === ZIP_EOCD_SIGNATURE) {
|
|
161
|
+
eocd = offset;
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
if (eocd < 0 || eocd + 22 > archive.length) throw new SyncOfflineArchiveError("zip-invalid", "Offline archive ZIP directory is invalid");
|
|
165
|
+
const commentLength = archive.readUInt16LE(eocd + 20);
|
|
166
|
+
if (eocd + 22 + commentLength !== archive.length || archive.readUInt16LE(eocd + 4) !== 0 || archive.readUInt16LE(eocd + 6) !== 0 || archive.readUInt16LE(eocd + 8) !== archive.readUInt16LE(eocd + 10)) throw new SyncOfflineArchiveError("zip-invalid", "Offline archive ZIP directory is inconsistent");
|
|
167
|
+
const count = archive.readUInt16LE(eocd + 10);
|
|
168
|
+
const centralSize = archive.readUInt32LE(eocd + 12);
|
|
169
|
+
const centralOffset = archive.readUInt32LE(eocd + 16);
|
|
170
|
+
if (count < 2 || count > 3 || centralOffset + centralSize !== eocd) throw new SyncOfflineArchiveError("zip-invalid", "Offline archive contains an unsupported entry count or layout");
|
|
171
|
+
const entries = [];
|
|
172
|
+
let offset = centralOffset;
|
|
173
|
+
let totalExpanded = 0;
|
|
174
|
+
const names = /* @__PURE__ */ new Set();
|
|
175
|
+
for (let index = 0; index < count; index += 1) {
|
|
176
|
+
if (offset + 46 > eocd || archive.readUInt32LE(offset) !== ZIP_CENTRAL_SIGNATURE) throw new SyncOfflineArchiveError("zip-invalid", "Offline archive central entry is invalid");
|
|
177
|
+
const madeBy = archive.readUInt16LE(offset + 4);
|
|
178
|
+
const flags = archive.readUInt16LE(offset + 8);
|
|
179
|
+
const method = archive.readUInt16LE(offset + 10);
|
|
180
|
+
const checksum = archive.readUInt32LE(offset + 16);
|
|
181
|
+
const compressedBytes = archive.readUInt32LE(offset + 20);
|
|
182
|
+
const expandedBytes = archive.readUInt32LE(offset + 24);
|
|
183
|
+
const nameLength = archive.readUInt16LE(offset + 28);
|
|
184
|
+
const extraLength = archive.readUInt16LE(offset + 30);
|
|
185
|
+
const entryCommentLength = archive.readUInt16LE(offset + 32);
|
|
186
|
+
const diskNumber = archive.readUInt16LE(offset + 34);
|
|
187
|
+
const externalAttributes = archive.readUInt32LE(offset + 38);
|
|
188
|
+
const localOffset = archive.readUInt32LE(offset + 42);
|
|
189
|
+
const end = offset + 46 + nameLength + extraLength + entryCommentLength;
|
|
190
|
+
if (end > eocd || nameLength === 0 || diskNumber !== 0 || (flags & -2049) !== 0 || method !== 0 && method !== 8) throw new SyncOfflineArchiveError("zip-invalid", "Offline archive entry flags or compression are unsupported");
|
|
191
|
+
let name;
|
|
192
|
+
try {
|
|
193
|
+
name = new TextDecoder("utf-8", { fatal: true }).decode(archive.subarray(offset + 46, offset + 46 + nameLength));
|
|
194
|
+
} catch (error) {
|
|
195
|
+
throw new SyncOfflineArchiveError("zip-invalid", "Offline archive entry name is not valid UTF-8", error);
|
|
196
|
+
}
|
|
197
|
+
const fileType = (madeBy >> 8 === 3 ? externalAttributes >>> 16 : 0) & 61440;
|
|
198
|
+
if (!safeZipName(name) || names.has(name) || fileType !== 0 && fileType !== 32768 || expandedBytes > MAX_ARCHIVE_EXPANDED_BYTES) throw new SyncOfflineArchiveError("path-invalid", "Offline archive contains a duplicate, unsafe, linked or oversized path");
|
|
199
|
+
if (method === 0 && compressedBytes !== expandedBytes) throw new SyncOfflineArchiveError("zip-invalid", "Stored ZIP entry sizes differ");
|
|
200
|
+
totalExpanded += expandedBytes;
|
|
201
|
+
if (totalExpanded > MAX_ARCHIVE_EXPANDED_BYTES) throw new SyncOfflineArchiveError("archive-too-large", "Expanded offline archive exceeds the size limit");
|
|
202
|
+
names.add(name);
|
|
203
|
+
entries.push({
|
|
204
|
+
name,
|
|
205
|
+
method,
|
|
206
|
+
crc: checksum,
|
|
207
|
+
compressedBytes,
|
|
208
|
+
expandedBytes,
|
|
209
|
+
localOffset
|
|
210
|
+
});
|
|
211
|
+
offset = end;
|
|
212
|
+
}
|
|
213
|
+
if (offset !== eocd || !names.has("manifest.json") || !names.has("nodes.json") || names.has("sessions.json") !== (count === 3)) throw new SyncOfflineArchiveError("zip-invalid", "Offline archive has missing or unexpected files");
|
|
214
|
+
const ranges = [];
|
|
215
|
+
const files = /* @__PURE__ */ new Map();
|
|
216
|
+
for (const entry of entries) {
|
|
217
|
+
const local = entry.localOffset;
|
|
218
|
+
if (local + 30 > centralOffset || archive.readUInt32LE(local) !== ZIP_LOCAL_SIGNATURE) throw new SyncOfflineArchiveError("zip-invalid", "Offline archive local entry is invalid");
|
|
219
|
+
const flags = archive.readUInt16LE(local + 6);
|
|
220
|
+
const method = archive.readUInt16LE(local + 8);
|
|
221
|
+
const crc = archive.readUInt32LE(local + 14);
|
|
222
|
+
const compressedBytes = archive.readUInt32LE(local + 18);
|
|
223
|
+
const expandedBytes = archive.readUInt32LE(local + 22);
|
|
224
|
+
const nameLength = archive.readUInt16LE(local + 26);
|
|
225
|
+
const extraLength = archive.readUInt16LE(local + 28);
|
|
226
|
+
const nameStart = local + 30;
|
|
227
|
+
const dataStart = nameStart + nameLength + extraLength;
|
|
228
|
+
const dataEnd = dataStart + entry.compressedBytes;
|
|
229
|
+
const localName = archive.subarray(nameStart, nameStart + nameLength).toString("utf8");
|
|
230
|
+
if (flags !== 2048 || method !== entry.method || crc !== entry.crc || compressedBytes !== entry.compressedBytes || expandedBytes !== entry.expandedBytes || localName !== entry.name || dataEnd > centralOffset) throw new SyncOfflineArchiveError("zip-invalid", "Offline archive local and central entries do not match");
|
|
231
|
+
ranges.push([local, dataEnd]);
|
|
232
|
+
const compressed = archive.subarray(dataStart, dataEnd);
|
|
233
|
+
let data;
|
|
234
|
+
try {
|
|
235
|
+
data = entry.method === 0 ? Buffer.from(compressed) : inflateRawSync(compressed, { maxOutputLength: entry.expandedBytes + 1 });
|
|
236
|
+
} catch (error) {
|
|
237
|
+
throw new SyncOfflineArchiveError("zip-invalid", "Offline archive entry could not be expanded safely", error);
|
|
238
|
+
}
|
|
239
|
+
if (data.byteLength !== entry.expandedBytes || crc32(data) !== entry.crc) throw new SyncOfflineArchiveError("archive-integrity", "Offline archive entry checksum does not match");
|
|
240
|
+
files.set(entry.name, data);
|
|
241
|
+
}
|
|
242
|
+
ranges.sort((a, b) => a[0] - b[0]);
|
|
243
|
+
for (let index = 1; index < ranges.length; index += 1) if (ranges[index - 1][1] > ranges[index][0]) throw new SyncOfflineArchiveError("zip-invalid", "Offline archive entries overlap");
|
|
244
|
+
return {
|
|
245
|
+
files,
|
|
246
|
+
expandedBytes: totalExpanded
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
function validateOfflineZipArchive(archive) {
|
|
250
|
+
parseZip(archive);
|
|
251
|
+
}
|
|
252
|
+
function parseJson(data, label) {
|
|
253
|
+
try {
|
|
254
|
+
return JSON.parse(data.toString("utf8"));
|
|
255
|
+
} catch (error) {
|
|
256
|
+
throw new SyncOfflineArchiveError("archive-invalid", `${label} is invalid JSON`, error);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
function recordSummary(metadata, branchId) {
|
|
260
|
+
return {
|
|
261
|
+
kind: metadata.kind,
|
|
262
|
+
originInstanceId: metadata.originInstanceId,
|
|
263
|
+
originEntityId: metadata.originEntityId,
|
|
264
|
+
revision: metadata.revision,
|
|
265
|
+
contentDigest: metadata.contentDigest,
|
|
266
|
+
deleted: Boolean(metadata.deleted),
|
|
267
|
+
...branchId ? { branchId } : {}
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
function summaryRecords(nodes, sessions) {
|
|
271
|
+
return [...nodes.records.map((entry) => recordSummary(entry.metadata)), ...sessions?.entries.map((entry) => recordSummary(entry.metadata, entry.branchId)) ?? []].sort((a, b) => canonicalJson(a).localeCompare(canonicalJson(b)));
|
|
272
|
+
}
|
|
273
|
+
function validateManifest(value, files) {
|
|
274
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) throw new SyncOfflineArchiveError("archive-invalid", "Offline archive manifest is invalid");
|
|
275
|
+
const manifest = value;
|
|
276
|
+
if (Object.keys(manifest).some((key) => ![
|
|
277
|
+
"format",
|
|
278
|
+
"formatVersion",
|
|
279
|
+
"instanceId",
|
|
280
|
+
"createdAt",
|
|
281
|
+
"includeSessions",
|
|
282
|
+
"sessionFormatVersion",
|
|
283
|
+
"files",
|
|
284
|
+
"records"
|
|
285
|
+
].includes(key)) || manifest.files === null || typeof manifest.files !== "object" || Array.isArray(manifest.files) || !manifest.files.nodes || typeof manifest.files.nodes !== "object" || Array.isArray(manifest.files.nodes)) throw new SyncOfflineArchiveError("archive-invalid", "Offline archive manifest contains unsupported fields");
|
|
286
|
+
if (manifest.format !== "dshb-sync" || manifest.formatVersion !== ARCHIVE_VERSION || typeof manifest.instanceId !== "string" || !/^i-[a-f0-9]{32}$/.test(manifest.instanceId) || typeof manifest.createdAt !== "string" || !Number.isFinite(Date.parse(manifest.createdAt)) || typeof manifest.includeSessions !== "boolean" || !Array.isArray(manifest.records) || manifest.includeSessions !== files.has("sessions.json") || manifest.includeSessions && manifest.sessionFormatVersion !== 4 || !manifest.includeSessions && manifest.sessionFormatVersion !== void 0) throw new SyncOfflineArchiveError("format-unsupported", "Offline archive manifest version or fields are unsupported");
|
|
287
|
+
const nodesData = files.get("nodes.json");
|
|
288
|
+
const manifestData = files.get("manifest.json");
|
|
289
|
+
if (!nodesData || !manifestData || manifest.files.nodes.bytes !== nodesData.byteLength || manifest.files.nodes.digest !== sha256(nodesData)) throw new SyncOfflineArchiveError("archive-integrity", "Offline archive node manifest digest does not match");
|
|
290
|
+
const nodes = parseJson(nodesData, "Node manifest");
|
|
291
|
+
if (nodes.protocolVersion !== 1 || nodes.instanceId !== manifest.instanceId || !Array.isArray(nodes.records)) throw new SyncOfflineArchiveError("archive-invalid", "Offline archive node manifest identity is invalid");
|
|
292
|
+
let sessions;
|
|
293
|
+
const sessionsData = files.get("sessions.json");
|
|
294
|
+
if (sessionsData) {
|
|
295
|
+
if (!manifest.files.sessions || manifest.files.sessions.bytes !== sessionsData.byteLength || manifest.files.sessions.digest !== sha256(sessionsData)) throw new SyncOfflineArchiveError("archive-integrity", "Offline archive session bundle digest does not match");
|
|
296
|
+
sessions = validateSyncSessionBundle(parseJson(sessionsData, "Session bundle"));
|
|
297
|
+
if (sessions.instanceId !== manifest.instanceId) throw new SyncOfflineArchiveError("archive-invalid", "Session bundle source identity differs from archive");
|
|
298
|
+
} else if (manifest.files.sessions !== void 0) throw new SyncOfflineArchiveError("archive-invalid", "Offline archive declares a missing session file");
|
|
299
|
+
if (canonicalJson(manifest.records) !== canonicalJson(summaryRecords(nodes, sessions))) throw new SyncOfflineArchiveError("archive-integrity", "Offline archive record summary does not match payloads");
|
|
300
|
+
return {
|
|
301
|
+
manifest,
|
|
302
|
+
nodes,
|
|
303
|
+
...sessions ? { sessions } : {}
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
function decodeOfflinePayload(zip, nodesService) {
|
|
307
|
+
const parsed = parseZip(zip);
|
|
308
|
+
const manifestData = parsed.files.get("manifest.json");
|
|
309
|
+
if (!manifestData) throw new SyncOfflineArchiveError("archive-invalid", "Offline archive manifest is missing");
|
|
310
|
+
const payloads = validateManifest(parseJson(manifestData, "Archive manifest"), parsed.files);
|
|
311
|
+
try {
|
|
312
|
+
nodesService.validateManifest(payloads.nodes.records);
|
|
313
|
+
if (payloads.sessions) validateSyncSessionBundle(payloads.sessions);
|
|
314
|
+
} catch (error) {
|
|
315
|
+
throw new SyncOfflineArchiveError("archive-invalid", "Offline archive contains an invalid sync manifest", error);
|
|
316
|
+
}
|
|
317
|
+
return {
|
|
318
|
+
files: parsed.files,
|
|
319
|
+
payloads
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
function readFileRange(path, position, length) {
|
|
323
|
+
const file = openSync(path, "r");
|
|
324
|
+
try {
|
|
325
|
+
const buffer = Buffer.alloc(length);
|
|
326
|
+
if (readSync(file, buffer, 0, length, position) !== length) throw new SyncOfflineArchiveError("archive-invalid", "Offline archive header is truncated");
|
|
327
|
+
return buffer;
|
|
328
|
+
} finally {
|
|
329
|
+
closeSync(file);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
function homeDirectory(home) {
|
|
333
|
+
return home ?? process.env.DSH_HOME ?? join(homedir(), ".dsh");
|
|
334
|
+
}
|
|
335
|
+
function availableBytes(path) {
|
|
336
|
+
const available = statfsSync(path, { bigint: true });
|
|
337
|
+
return available.bavail * available.bsize;
|
|
338
|
+
}
|
|
339
|
+
function checkAvailableSpace(path, requiredBytes, getAvailableBytes) {
|
|
340
|
+
try {
|
|
341
|
+
if (getAvailableBytes(path) < BigInt(requiredBytes)) throw new SyncOfflineArchiveError("disk-space", "Insufficient disk space for offline archive import");
|
|
342
|
+
} catch (error) {
|
|
343
|
+
if (error instanceof SyncOfflineArchiveError) throw error;
|
|
344
|
+
throw new SyncOfflineArchiveError("disk-space", "Unable to verify available disk space", error);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
function requiredBytesForImport(payloads, state, nodesPending, sessionsPending) {
|
|
348
|
+
const attachmentBytes = sessionsPending ? payloads.sessions?.entries.reduce((total, entry) => total + (entry.objects?.reduce((sum, object) => sum + object.bytes, 0) ?? 0), 0) ?? 0 : 0;
|
|
349
|
+
const sessionBytes = sessionsPending && payloads.sessions ? Buffer.byteLength(`${JSON.stringify(payloads.sessions, null, 2)}\n`, "utf8") : 0;
|
|
350
|
+
const nodeBytes = nodesPending ? payloads.manifest.files.nodes.bytes : 0;
|
|
351
|
+
const stateBytes = nodesPending || sessionsPending ? state ? Buffer.byteLength(`${JSON.stringify(state.read(), null, 2)}\n`, "utf8") : 0 : 0;
|
|
352
|
+
return attachmentBytes * 2 + sessionBytes * 3 + nodeBytes * 2 + stateBytes * 2 + 4194304;
|
|
353
|
+
}
|
|
354
|
+
var SyncOfflineArchiveService = class {
|
|
355
|
+
nodes;
|
|
356
|
+
sessions;
|
|
357
|
+
getAvailableBytes;
|
|
358
|
+
state;
|
|
359
|
+
operationTail = Promise.resolve();
|
|
360
|
+
home;
|
|
361
|
+
journal;
|
|
362
|
+
constructor(nodes, sessions, home, getAvailableBytes = availableBytes, state) {
|
|
363
|
+
this.nodes = nodes;
|
|
364
|
+
this.sessions = sessions;
|
|
365
|
+
this.getAvailableBytes = getAvailableBytes;
|
|
366
|
+
this.state = state;
|
|
367
|
+
this.home = homeDirectory(home);
|
|
368
|
+
this.journal = new SyncOfflineArchiveJournalStore(this.home);
|
|
369
|
+
}
|
|
370
|
+
exportArchive(password, includeSessions = false) {
|
|
371
|
+
return this.serializeMutation(() => this.exportArchiveNow(password, includeSessions));
|
|
372
|
+
}
|
|
373
|
+
importArchive(password, input) {
|
|
374
|
+
return this.serializeMutation(async () => {
|
|
375
|
+
await this.recoverPendingNow();
|
|
376
|
+
await this.sessions.recoverPending();
|
|
377
|
+
return this.importArchiveNow(password, input);
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
recoverPending() {
|
|
381
|
+
return this.serializeMutation(async () => {
|
|
382
|
+
await this.recoverPendingNow();
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
cleanupStagingDirectories() {
|
|
386
|
+
const root = join(this.home, "dshb");
|
|
387
|
+
let entries;
|
|
388
|
+
try {
|
|
389
|
+
entries = readdirSync(root, { withFileTypes: true });
|
|
390
|
+
} catch (error) {
|
|
391
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") return;
|
|
392
|
+
throw error;
|
|
393
|
+
}
|
|
394
|
+
const pendingDirectory = this.journal.read()?.stagingDirectory;
|
|
395
|
+
for (const entry of entries) if (entry.isDirectory() && entry.name !== pendingDirectory && /^(?:sync-offline-staging|sync-offline-export)-/.test(entry.name)) rmSync(join(root, entry.name), {
|
|
396
|
+
recursive: true,
|
|
397
|
+
force: true
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
async exportArchiveNow(passwordValue, includeSessions) {
|
|
401
|
+
const password = validatePassword(passwordValue);
|
|
402
|
+
if (typeof includeSessions !== "boolean") throw new SyncOfflineArchiveError("request-invalid", "Session inclusion option is invalid");
|
|
403
|
+
const nodeManifest = await this.nodes.exportManifest();
|
|
404
|
+
this.nodes.validateManifest(nodeManifest.records);
|
|
405
|
+
const sessionBundle = includeSessions ? await this.sessions.exportBundle() : void 0;
|
|
406
|
+
const nodesData = Buffer.from(JSON.stringify(nodeManifest), "utf8");
|
|
407
|
+
const sessionsData = sessionBundle ? Buffer.from(JSON.stringify(sessionBundle), "utf8") : void 0;
|
|
408
|
+
const records = summaryRecords(nodeManifest, sessionBundle);
|
|
409
|
+
const manifest = {
|
|
410
|
+
format: "dshb-sync",
|
|
411
|
+
formatVersion: ARCHIVE_VERSION,
|
|
412
|
+
instanceId: nodeManifest.instanceId,
|
|
413
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
414
|
+
includeSessions,
|
|
415
|
+
...sessionBundle ? { sessionFormatVersion: 4 } : {},
|
|
416
|
+
files: {
|
|
417
|
+
nodes: {
|
|
418
|
+
bytes: nodesData.byteLength,
|
|
419
|
+
digest: sha256(nodesData)
|
|
420
|
+
},
|
|
421
|
+
...sessionsData ? { sessions: {
|
|
422
|
+
bytes: sessionsData.byteLength,
|
|
423
|
+
digest: sha256(sessionsData)
|
|
424
|
+
} } : {}
|
|
425
|
+
},
|
|
426
|
+
records
|
|
427
|
+
};
|
|
428
|
+
const zip = writeZipParts([
|
|
429
|
+
{
|
|
430
|
+
name: "manifest.json",
|
|
431
|
+
data: Buffer.from(JSON.stringify(manifest), "utf8")
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
name: "nodes.json",
|
|
435
|
+
data: nodesData
|
|
436
|
+
},
|
|
437
|
+
...sessionsData ? [{
|
|
438
|
+
name: "sessions.json",
|
|
439
|
+
data: sessionsData
|
|
440
|
+
}] : []
|
|
441
|
+
]);
|
|
442
|
+
if (zip.bytes > MAX_ARCHIVE_EXPANDED_BYTES) throw new SyncOfflineArchiveError("archive-too-large", "Offline archive contents exceed the size limit");
|
|
443
|
+
mkdirSync(join(this.home, "dshb"), {
|
|
444
|
+
recursive: true,
|
|
445
|
+
mode: 448
|
|
446
|
+
});
|
|
447
|
+
const directory = mkdtempSync(join(this.home, "dshb", "sync-offline-export-"));
|
|
448
|
+
chmodSync(directory, 448);
|
|
449
|
+
const path = join(directory, "export.dshb-sync");
|
|
450
|
+
const salt = randomBytes(SALT_SIZE);
|
|
451
|
+
const nonce = randomBytes(NONCE_SIZE);
|
|
452
|
+
const header = archiveHeader(salt, nonce);
|
|
453
|
+
let key;
|
|
454
|
+
try {
|
|
455
|
+
key = await deriveKey(password, salt);
|
|
456
|
+
const cipher = createCipheriv("aes-256-gcm", key, nonce);
|
|
457
|
+
cipher.setAAD(header);
|
|
458
|
+
writeFileSync(path, header, {
|
|
459
|
+
flag: "wx",
|
|
460
|
+
mode: 384
|
|
461
|
+
});
|
|
462
|
+
await pipeline(Readable.from(zip.chunks), cipher, createWriteStream(path, {
|
|
463
|
+
flags: "a",
|
|
464
|
+
mode: 384
|
|
465
|
+
}));
|
|
466
|
+
appendFileSync(path, cipher.getAuthTag());
|
|
467
|
+
chmodSync(path, 384);
|
|
468
|
+
const bytes = statSync(path).size;
|
|
469
|
+
if (bytes > 134217728) throw new SyncOfflineArchiveError("archive-too-large", "Offline archive exceeds the size limit");
|
|
470
|
+
return {
|
|
471
|
+
path,
|
|
472
|
+
bytes,
|
|
473
|
+
cleanup: () => rmSync(directory, {
|
|
474
|
+
recursive: true,
|
|
475
|
+
force: true
|
|
476
|
+
})
|
|
477
|
+
};
|
|
478
|
+
} catch (error) {
|
|
479
|
+
rmSync(directory, {
|
|
480
|
+
recursive: true,
|
|
481
|
+
force: true
|
|
482
|
+
});
|
|
483
|
+
throw error instanceof SyncOfflineArchiveError ? error : new SyncOfflineArchiveError("archive-export-failed", "Offline archive export failed", error);
|
|
484
|
+
} finally {
|
|
485
|
+
password.fill(0);
|
|
486
|
+
key?.fill(0);
|
|
487
|
+
for (const chunk of zip.chunks) chunk.fill(0);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
async importArchiveNow(passwordValue, input) {
|
|
491
|
+
const password = validatePassword(passwordValue);
|
|
492
|
+
mkdirSync(join(this.home, "dshb"), {
|
|
493
|
+
recursive: true,
|
|
494
|
+
mode: 448
|
|
495
|
+
});
|
|
496
|
+
const batchId = randomBytes(16).toString("hex");
|
|
497
|
+
const stagingDirectory = `sync-offline-staging-${batchId}`;
|
|
498
|
+
const directory = join(this.home, "dshb", stagingDirectory);
|
|
499
|
+
mkdirSync(directory, { mode: 448 });
|
|
500
|
+
chmodSync(directory, 448);
|
|
501
|
+
const encryptedPath = join(directory, "input.enc");
|
|
502
|
+
const zipPath = join(directory, "payload.zip");
|
|
503
|
+
let key;
|
|
504
|
+
let plaintextZip;
|
|
505
|
+
let payloadFiles;
|
|
506
|
+
try {
|
|
507
|
+
let encryptedBytes = 0;
|
|
508
|
+
const sizeLimit = new Transform({ transform(chunk, _encoding, callback) {
|
|
509
|
+
encryptedBytes += chunk.byteLength;
|
|
510
|
+
if (encryptedBytes > 134217728) {
|
|
511
|
+
callback(new SyncOfflineArchiveError("archive-too-large", "Offline archive exceeds the upload size limit"));
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
callback(null, chunk);
|
|
515
|
+
} });
|
|
516
|
+
await pipeline(Readable.from(input), sizeLimit, createWriteStream(encryptedPath, {
|
|
517
|
+
flags: "wx",
|
|
518
|
+
mode: 384
|
|
519
|
+
}));
|
|
520
|
+
if (encryptedBytes < 88) throw new SyncOfflineArchiveError("archive-invalid", "Offline archive is truncated");
|
|
521
|
+
const header = readFileRange(encryptedPath, 0, HEADER_SIZE);
|
|
522
|
+
const { salt, nonce } = parseArchiveHeader(header);
|
|
523
|
+
const tag = readFileRange(encryptedPath, encryptedBytes - TAG_SIZE, TAG_SIZE);
|
|
524
|
+
key = await deriveKey(password, salt);
|
|
525
|
+
const decipher = createDecipheriv("aes-256-gcm", key, nonce);
|
|
526
|
+
decipher.setAAD(header);
|
|
527
|
+
decipher.setAuthTag(tag);
|
|
528
|
+
try {
|
|
529
|
+
await pipeline(createReadStream(encryptedPath, {
|
|
530
|
+
start: HEADER_SIZE,
|
|
531
|
+
end: encryptedBytes - TAG_SIZE - 1
|
|
532
|
+
}), decipher, createWriteStream(zipPath, {
|
|
533
|
+
flags: "wx",
|
|
534
|
+
mode: 384
|
|
535
|
+
}));
|
|
536
|
+
} catch (error) {
|
|
537
|
+
throw new SyncOfflineArchiveError("password-or-integrity", "Package password is incorrect or archive authentication failed", error);
|
|
538
|
+
}
|
|
539
|
+
const zipStat = statSync(zipPath);
|
|
540
|
+
if (zipStat.size > MAX_ARCHIVE_EXPANDED_BYTES) throw new SyncOfflineArchiveError("archive-too-large", "Offline archive contents exceed the size limit");
|
|
541
|
+
plaintextZip = readFileRange(zipPath, 0, zipStat.size);
|
|
542
|
+
const decoded = decodeOfflinePayload(plaintextZip, this.nodes);
|
|
543
|
+
payloadFiles = decoded.files;
|
|
544
|
+
const payloads = decoded.payloads;
|
|
545
|
+
const requiredBytes = requiredBytesForImport(payloads, this.state, true, Boolean(payloads.sessions));
|
|
546
|
+
if (!Number.isSafeInteger(requiredBytes)) throw new SyncOfflineArchiveError("archive-too-large", "Offline archive storage estimate exceeds the supported range");
|
|
547
|
+
checkAvailableSpace(this.home, requiredBytes, this.getAvailableBytes);
|
|
548
|
+
await this.preflightPayloads(payloads, true, Boolean(payloads.sessions));
|
|
549
|
+
const journal = {
|
|
550
|
+
version: 1,
|
|
551
|
+
batchId,
|
|
552
|
+
stagingDirectory,
|
|
553
|
+
zipDigest: sha256(plaintextZip),
|
|
554
|
+
includeSessions: Boolean(payloads.sessions),
|
|
555
|
+
nodesApplied: false,
|
|
556
|
+
sessionsApplied: !payloads.sessions,
|
|
557
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
558
|
+
};
|
|
559
|
+
this.journal.begin(journal);
|
|
560
|
+
return await this.applyJournaledPayloads(payloads, journal, false);
|
|
561
|
+
} catch (error) {
|
|
562
|
+
if (error instanceof SyncOfflineArchiveError || error instanceof SyncSessionError) throw error;
|
|
563
|
+
throw new SyncOfflineArchiveError("archive-import-failed", "Offline archive import failed", error);
|
|
564
|
+
} finally {
|
|
565
|
+
password.fill(0);
|
|
566
|
+
key?.fill(0);
|
|
567
|
+
plaintextZip?.fill(0);
|
|
568
|
+
if (payloadFiles) for (const data of payloadFiles.values()) data.fill(0);
|
|
569
|
+
let preserveStaging = false;
|
|
570
|
+
try {
|
|
571
|
+
preserveStaging = this.journal.read()?.stagingDirectory === stagingDirectory;
|
|
572
|
+
} catch {
|
|
573
|
+
preserveStaging = true;
|
|
574
|
+
}
|
|
575
|
+
if (!preserveStaging) rmSync(directory, {
|
|
576
|
+
recursive: true,
|
|
577
|
+
force: true
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
async recoverPendingNow() {
|
|
582
|
+
const journal = this.journal.read();
|
|
583
|
+
if (!journal) return;
|
|
584
|
+
const directory = join(this.home, "dshb", journal.stagingDirectory);
|
|
585
|
+
const zipPath = join(directory, "payload.zip");
|
|
586
|
+
let zip;
|
|
587
|
+
let files;
|
|
588
|
+
try {
|
|
589
|
+
const zipBytes = statSync(zipPath).size;
|
|
590
|
+
if (zipBytes > MAX_ARCHIVE_EXPANDED_BYTES) throw new SyncOfflineArchiveError("archive-too-large", "Offline archive staging exceeds the size limit");
|
|
591
|
+
zip = readFileRange(zipPath, 0, zipBytes);
|
|
592
|
+
if (sha256(zip) !== journal.zipDigest) throw new SyncOfflineArchiveError("journal-integrity", "Staged offline archive digest does not match its journal");
|
|
593
|
+
const decoded = decodeOfflinePayload(zip, this.nodes);
|
|
594
|
+
files = decoded.files;
|
|
595
|
+
if (decoded.payloads.manifest.includeSessions !== journal.includeSessions) throw new SyncOfflineArchiveError("journal-invalid", "Offline archive journal options do not match the staged payload");
|
|
596
|
+
await this.applyJournaledPayloads(decoded.payloads, journal, true);
|
|
597
|
+
} finally {
|
|
598
|
+
zip?.fill(0);
|
|
599
|
+
if (files) for (const data of files.values()) data.fill(0);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
async preflightPayloads(payloads, nodesPending, sessionsPending) {
|
|
603
|
+
if (nodesPending) try {
|
|
604
|
+
await this.nodes.preflightManifest(payloads.nodes.records);
|
|
605
|
+
} catch (error) {
|
|
606
|
+
throw new SyncOfflineArchiveError("archive-conflict", "Target node state conflicts with the offline archive", error);
|
|
607
|
+
}
|
|
608
|
+
if (sessionsPending && payloads.sessions) {
|
|
609
|
+
await this.sessions.recoverPending();
|
|
610
|
+
await this.sessions.exportBundle();
|
|
611
|
+
this.sessions.preflightBundle(payloads.sessions, { allowSameInstance: true });
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
async applyJournaledPayloads(payloads, initialJournal, runPreflight) {
|
|
615
|
+
let journal = initialJournal;
|
|
616
|
+
const nodesPending = !journal.nodesApplied;
|
|
617
|
+
const sessionsPending = journal.includeSessions && !journal.sessionsApplied;
|
|
618
|
+
if (!nodesPending && !sessionsPending) {
|
|
619
|
+
this.journal.complete();
|
|
620
|
+
rmSync(join(this.home, "dshb", journal.stagingDirectory), {
|
|
621
|
+
recursive: true,
|
|
622
|
+
force: true
|
|
623
|
+
});
|
|
624
|
+
return { nodes: {
|
|
625
|
+
created: 0,
|
|
626
|
+
updated: 0,
|
|
627
|
+
deleted: 0,
|
|
628
|
+
pendingRemoval: 0,
|
|
629
|
+
skipped: 0
|
|
630
|
+
} };
|
|
631
|
+
}
|
|
632
|
+
if (runPreflight) {
|
|
633
|
+
const requiredBytes = requiredBytesForImport(payloads, this.state, nodesPending, sessionsPending);
|
|
634
|
+
if (!Number.isSafeInteger(requiredBytes)) throw new SyncOfflineArchiveError("archive-too-large", "Offline archive storage estimate exceeds the supported range");
|
|
635
|
+
checkAvailableSpace(this.home, requiredBytes, this.getAvailableBytes);
|
|
636
|
+
await this.preflightPayloads(payloads, nodesPending, sessionsPending);
|
|
637
|
+
}
|
|
638
|
+
const nodeResult = nodesPending ? await this.nodes.applyManifest(payloads.nodes.records, "offline archive", "offline") : {
|
|
639
|
+
created: 0,
|
|
640
|
+
updated: 0,
|
|
641
|
+
deleted: 0,
|
|
642
|
+
pendingRemoval: 0,
|
|
643
|
+
skipped: 0
|
|
644
|
+
};
|
|
645
|
+
if (nodesPending) journal = this.journal.update({ nodesApplied: true });
|
|
646
|
+
let sessionResult;
|
|
647
|
+
if (sessionsPending && payloads.sessions) {
|
|
648
|
+
sessionResult = await this.sessions.applyBundle(payloads.sessions, "offline archive", "p-000000000000000000000000", {
|
|
649
|
+
importedVia: "offline",
|
|
650
|
+
allowSameInstance: true
|
|
651
|
+
});
|
|
652
|
+
journal = this.journal.update({ sessionsApplied: true });
|
|
653
|
+
}
|
|
654
|
+
this.journal.complete();
|
|
655
|
+
rmSync(join(this.home, "dshb", journal.stagingDirectory), {
|
|
656
|
+
recursive: true,
|
|
657
|
+
force: true
|
|
658
|
+
});
|
|
659
|
+
return {
|
|
660
|
+
nodes: nodeResult,
|
|
661
|
+
...sessionResult ? { sessions: sessionResult } : {}
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
serializeMutation(operation) {
|
|
665
|
+
const result = this.operationTail.then(operation);
|
|
666
|
+
this.operationTail = result.then(() => void 0, () => void 0);
|
|
667
|
+
return result;
|
|
668
|
+
}
|
|
669
|
+
};
|
|
670
|
+
//#endregion
|
|
671
|
+
export { MAX_OFFLINE_ARCHIVE_BYTES, SyncOfflineArchiveError, SyncOfflineArchiveService, validateOfflineZipArchive };
|
|
672
|
+
|
|
673
|
+
//# sourceMappingURL=offline-archive.js.map
|