@delali/sirannon-db 0.3.5 → 0.3.6-next.78
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/dist/backup/index.d.ts +5 -5
- package/dist/backup/index.mjs +5 -3
- package/dist/backup-scheduler/index.d.ts +2 -2
- package/dist/backup-scheduler/index.mjs +2 -2
- package/dist/chunk-22TYGH7J.mjs +55 -0
- package/dist/{chunk-SB3MIFLG.mjs → chunk-2ZQOZFX7.mjs} +29 -6
- package/dist/{chunk-UIDQGPJY.mjs → chunk-4O7WYZHN.mjs} +38 -3
- package/dist/{chunk-ZFFUVYYE.mjs → chunk-5DKWHT6K.mjs} +109 -1
- package/dist/chunk-5O4J6LQQ.mjs +35 -0
- package/dist/{chunk-CZL4D3BM.mjs → chunk-6CABU54O.mjs} +2 -2
- package/dist/{chunk-JAQ2I4OT.mjs → chunk-A5ULJVK7.mjs} +11 -11
- package/dist/{chunk-RLBKZZ24.mjs → chunk-AC7TKWNH.mjs} +1 -0
- package/dist/chunk-BKO4SCEC.mjs +99 -0
- package/dist/{chunk-KOTBP4AW.mjs → chunk-C7E4H6UN.mjs} +24 -4
- package/dist/{chunk-G4AG3ESO.mjs → chunk-FBA7P4ZA.mjs} +114 -149
- package/dist/{chunk-DKML5425.mjs → chunk-GBVB6WSC.mjs} +2 -2
- package/dist/chunk-I4JY5QPA.mjs +51 -0
- package/dist/{chunk-L6EX6K27.mjs → chunk-MO4APBYG.mjs} +54 -13
- package/dist/{chunk-RPWBYASV.mjs → chunk-MTOQ6QIS.mjs} +1 -1
- package/dist/chunk-MX77PGEN.mjs +60 -0
- package/dist/{chunk-BTUD6XQO.mjs → chunk-N3MHZV3B.mjs} +11 -29
- package/dist/{chunk-UHUDPQFI.mjs → chunk-OJXV4L3N.mjs} +1 -3
- package/dist/chunk-QF7WF7RJ.mjs +34 -0
- package/dist/chunk-RLBV4YTB.mjs +22 -0
- package/dist/{chunk-ISDT4RBD.mjs → chunk-ULUZUBON.mjs} +1 -1
- package/dist/{chunk-AP4367HK.mjs → chunk-VGRAAQX3.mjs} +4 -4
- package/dist/chunk-W74HFQFD.mjs +153 -0
- package/dist/chunk-Y4SP7CRF.mjs +9 -0
- package/dist/{chunk-JFVEGN7J.mjs → chunk-ZAUHAJ4G.mjs} +2 -1
- package/dist/{chunk-DDIHBUEN.mjs → chunk-ZWW53O5P.mjs} +19 -7
- package/dist/client/index.d.ts +8 -8
- package/dist/client/topology.d.ts +4 -4
- package/dist/{client-base-Doaqwrx3.d.ts → client-base-DJZbSY55.d.ts} +2 -2
- package/dist/{client-streams-MKPE4MXW.mjs → client-streams-F7IG3PTU.mjs} +13 -16
- package/dist/core/index.d.ts +48 -12
- package/dist/core/index.mjs +1812 -107
- package/dist/core/writer-worker.mjs +28 -4
- package/dist/{database-fz-8CJBP.d.ts → database-BZ96ZzAx.d.ts} +411 -184
- package/dist/{database-backup-BwTNiBm2.d.ts → database-backup-BhM8yJ1A.d.ts} +32 -2
- package/dist/driver/better-sqlite3.d.ts +11 -1
- package/dist/driver/better-sqlite3.mjs +212 -23
- package/dist/driver/bun.d.ts +1 -1
- package/dist/driver/bun.mjs +15 -22
- package/dist/driver/expo.d.ts +1 -1
- package/dist/driver/expo.mjs +185 -16
- package/dist/driver/node.d.ts +1 -1
- package/dist/driver/node.mjs +15 -62
- package/dist/driver/wa-sqlite.d.ts +1 -1
- package/dist/driver/wa-sqlite.mjs +14 -153
- package/dist/{change-tracker-Dpo99EI-.d.ts → errors-DLMAGQFb.d.ts} +200 -2
- package/dist/file-migrations/index.d.ts +1 -1
- package/dist/peer-refusal-RR37UOLV.mjs +1 -0
- package/dist/{protocol-DZSYSv0S.d.ts → protocol-DE10taas.d.ts} +1 -1
- package/dist/replication/coordinator/etcd.mjs +1 -1
- package/dist/replication/index.d.ts +7 -7
- package/dist/replication/index.mjs +66 -9
- package/dist/{restore-options-CcMukOLO.d.ts → restore-options-B6HV_5XT.d.ts} +6 -1
- package/dist/server/index.d.ts +10 -10
- package/dist/server/index.mjs +506 -350
- package/dist/{server-options-B9CsAZp5.d.ts → server-options-Bhdlvehe.d.ts} +14 -1
- package/dist/{server-streams-SBYCHWWM.mjs → server-streams-Q2AZAW3N.mjs} +6 -30
- package/dist/{sirannon-D0CjDULa.d.ts → sirannon-Ba1o2HUQ.d.ts} +5 -5
- package/dist/transport/grpc.d.ts +15 -4
- package/dist/transport/grpc.mjs +34 -37
- package/dist/transport/memory.d.ts +4 -3
- package/dist/transport/memory.mjs +8 -26
- package/dist/{types-DTmi7pXp.d.ts → types-BWyv_N18.d.ts} +189 -1
- package/dist/{types-BlZ9GG1a.d.ts → types-CDnlkVRK.d.ts} +8 -2
- package/package.json +17 -9
- package/dist/chunk-DBN57KVR.mjs +0 -171
- package/dist/chunk-W67A4H2E.mjs +0 -23
- package/dist/chunk-YW7IKQ7T.mjs +0 -62
package/dist/server/index.mjs
CHANGED
|
@@ -1,22 +1,24 @@
|
|
|
1
|
-
import { restoreBackup, MAX_RESTORE_BATCH_SIZE } from '../chunk-
|
|
2
|
-
import { tablesInFkOrder, dumpSchema, dumpTablePages } from '../chunk-
|
|
3
|
-
import { resolveDeviceRetentionPolicy, ChangeTracker, SubscriptionManager, ensureCdcEpoch, migrationChecksum,
|
|
4
|
-
import '../chunk-
|
|
5
|
-
import { SEQ_STRING_RE, highestMigrationVersion, selectCountTableRows, selectTableExists, selectAppliedMigrations, PkResolver, ensureDeviceCursorsTable, upsertDeviceCursor, updateDeviceCursor, selectMaxAssignedChangeSeq, MAX_CHANGE_SEQ, HLC, selectMaxChangeSeq, canonicaliseForChecksum, selectDeviceCursorExists, IDENTIFIER_RE } from '../chunk-
|
|
1
|
+
import { restoreBackup, MAX_RESTORE_BATCH_SIZE } from '../chunk-MO4APBYG.mjs';
|
|
2
|
+
import { tablesInFkOrder, dumpSchema, dumpTablePages } from '../chunk-GBVB6WSC.mjs';
|
|
3
|
+
import { resolveDeviceRetentionPolicy, ChangeTracker, SubscriptionManager, ensureCdcEpoch, migrationChecksum, needsResync, filteredChange, TransactionGrouper, isBulkLoadDurability, PrimedSubscription, applyDeviceCursorBoundary } from '../chunk-2ZQOZFX7.mjs';
|
|
4
|
+
import { wipeEncryptionKey, resolveEncryptionKey } from '../chunk-W74HFQFD.mjs';
|
|
5
|
+
import { SEQ_STRING_RE, highestMigrationVersion, selectCountTableRows, selectTableExists, selectAppliedMigrations, PkResolver, ensureDeviceCursorsTable, upsertDeviceCursor, updateDeviceCursor, selectMaxAssignedChangeSeq, MAX_CHANGE_SEQ, HLC, selectMaxChangeSeq, canonicaliseForChecksum, selectDeviceCursorExists, IDENTIFIER_RE } from '../chunk-MTOQ6QIS.mjs';
|
|
6
|
+
import { object, nullable, pipe, string, minLength, maxLength, safeParse } from '../chunk-5DKWHT6K.mjs';
|
|
6
7
|
import { BatchValidationError } from '../chunk-BLMRNSUG.mjs';
|
|
7
|
-
import { MIGRATIONS_TABLE, decodeTaggedValues, encodeTaggedValues, encodeWireRowsInPlace, DEVICE_CURSORS_TABLE, CHANGES_TABLE, INTERNAL_TABLE_PREFIX } from '../chunk-
|
|
8
|
+
import { MIGRATIONS_TABLE, decodeTaggedValues, encodeTaggedValues, encodeWireRowsInPlace, DEVICE_CURSORS_TABLE, CHANGES_TABLE, INTERNAL_TABLE_PREFIX } from '../chunk-ZAUHAJ4G.mjs';
|
|
8
9
|
import { createOperationSource, operationRegistryDigest, findRead, isRefusal, resolveArguments, findWrite } from '../chunk-KB4OLJAD.mjs';
|
|
9
10
|
import '../chunk-SEL4A4WT.mjs';
|
|
10
11
|
import '../chunk-GVCNMPOS.mjs';
|
|
11
12
|
import { optionalDependencyError } from '../chunk-WOMADPM4.mjs';
|
|
12
|
-
import { discardStagedChain } from '../chunk-
|
|
13
|
+
import { discardStagedChain } from '../chunk-OJXV4L3N.mjs';
|
|
13
14
|
import '../chunk-QGVRRKB2.mjs';
|
|
14
15
|
import '../chunk-AHCMO4YL.mjs';
|
|
15
16
|
import '../chunk-BUWJH3D3.mjs';
|
|
16
|
-
import '../chunk-
|
|
17
|
-
import '../chunk-CZL4D3BM.mjs';
|
|
17
|
+
import '../chunk-6CABU54O.mjs';
|
|
18
18
|
import '../chunk-BGJ5JVYL.mjs';
|
|
19
19
|
import '../chunk-HO2XTXVX.mjs';
|
|
20
|
+
import '../chunk-YCMB6GR4.mjs';
|
|
21
|
+
import '../chunk-5O4J6LQQ.mjs';
|
|
20
22
|
import { SQLITE_USER_VERSION_MAX } from '../chunk-BZWDPF4G.mjs';
|
|
21
23
|
import { SirannonError, WriteOverloadError } from '../chunk-KINDFBQP.mjs';
|
|
22
24
|
import { createHash } from 'crypto';
|
|
@@ -152,10 +154,23 @@ var SERVER_FAULT_MESSAGES = /* @__PURE__ */ new Map([
|
|
|
152
154
|
],
|
|
153
155
|
["SERVER_DEPENDENCY_MISSING", OPTIONAL_PACKAGE_MISSING],
|
|
154
156
|
["TRANSPORT_DEPENDENCY_MISSING", OPTIONAL_PACKAGE_MISSING],
|
|
155
|
-
["COORDINATOR_DEPENDENCY_MISSING", OPTIONAL_PACKAGE_MISSING]
|
|
157
|
+
["COORDINATOR_DEPENDENCY_MISSING", OPTIONAL_PACKAGE_MISSING],
|
|
158
|
+
["ENCRYPTION_UNSUPPORTED", "The server's SQLite driver cannot encrypt a database"],
|
|
159
|
+
["INVALID_ENCRYPTION_KEY", "The encryption key for this database is not a 32-byte key"],
|
|
160
|
+
["ENCRYPTION_KEY_UNAVAILABLE", "The server could not obtain the encryption key for this database"],
|
|
161
|
+
["ENCRYPTION_KEY_MISSING", "This database is encrypted, and the server opened it without a key"],
|
|
162
|
+
["ENCRYPTION_KEY_REJECTED", "The key that the server holds does not open this database"],
|
|
163
|
+
["DATABASE_NOT_ENCRYPTED", "This database is not encrypted, and the server opened it with a key"],
|
|
164
|
+
["ENCRYPTED_PAGE_CORRUPT", "A page of this encrypted database failed its integrity check"],
|
|
165
|
+
["ENCRYPTION_REQUIRED", "An encrypted node sends no data to a node that holds a plaintext database"],
|
|
166
|
+
["ENCRYPTION_CHANGE_IN_PROGRESS", "A key change is running on this database"],
|
|
167
|
+
["ENCRYPTION_CHANGE_ABORTED", "The key change stopped and left the database under its current key"]
|
|
156
168
|
]);
|
|
157
169
|
function clientErrorMessage(err) {
|
|
158
|
-
return
|
|
170
|
+
return clientMessageFor(err);
|
|
171
|
+
}
|
|
172
|
+
function clientMessageFor(error) {
|
|
173
|
+
return SERVER_FAULT_MESSAGES.get(error.code) ?? error.message;
|
|
159
174
|
}
|
|
160
175
|
function sharesErrorDetails(err) {
|
|
161
176
|
return !SERVER_FAULT_MESSAGES.has(err.code);
|
|
@@ -477,8 +492,12 @@ function httpStatusForError(err) {
|
|
|
477
492
|
case "HOOK_DENIED":
|
|
478
493
|
return 403;
|
|
479
494
|
case "BACKUP_CHAIN_BROKEN":
|
|
495
|
+
case "REPLICATION_ACTIVE":
|
|
496
|
+
case "ENCRYPTION_CHANGE_IN_PROGRESS":
|
|
497
|
+
case "ENCRYPTION_REQUIRED":
|
|
480
498
|
return 409;
|
|
481
499
|
case "BACKUP_UNSUPPORTED":
|
|
500
|
+
case "ENCRYPTION_UNSUPPORTED":
|
|
482
501
|
return 501;
|
|
483
502
|
case "BACKUP_DESTINATION_ERROR":
|
|
484
503
|
return 502;
|
|
@@ -492,6 +511,7 @@ function httpStatusForError(err) {
|
|
|
492
511
|
case "NODE_DRAINING":
|
|
493
512
|
case "UNSAFE_RECOVERY_REQUIRED":
|
|
494
513
|
case "WRITE_OVERLOADED":
|
|
514
|
+
case "ENCRYPTION_KEY_UNAVAILABLE":
|
|
495
515
|
return 503;
|
|
496
516
|
default:
|
|
497
517
|
return 500;
|
|
@@ -1158,232 +1178,6 @@ function readinessStatusForReplication(replication, current) {
|
|
|
1158
1178
|
return current;
|
|
1159
1179
|
}
|
|
1160
1180
|
|
|
1161
|
-
// src/server/backup-protocol.ts
|
|
1162
|
-
var BACKUP_RESTORE_NOT_ACCEPTED_MESSAGE = "This server does not restore databases over the wire; turn on acceptBackupRestore to open the route";
|
|
1163
|
-
function wholeNumberError(field, value, atLeast, atMost) {
|
|
1164
|
-
if (value === void 0) return null;
|
|
1165
|
-
if (typeof value !== "number" || !Number.isInteger(value) || value < atLeast) {
|
|
1166
|
-
return `Field "${field}" must be a whole number of at least ${atLeast}`;
|
|
1167
|
-
}
|
|
1168
|
-
if (atMost !== void 0 && value > atMost) {
|
|
1169
|
-
return `Field "${field}" must be a whole number of at most ${atMost}`;
|
|
1170
|
-
}
|
|
1171
|
-
return null;
|
|
1172
|
-
}
|
|
1173
|
-
function verifyNameValidationError(value) {
|
|
1174
|
-
if (typeof value !== "string" || value.length === 0) {
|
|
1175
|
-
return 'Field "name" is required and must be the name a chain record states';
|
|
1176
|
-
}
|
|
1177
|
-
return null;
|
|
1178
|
-
}
|
|
1179
|
-
function restorableFromValidationError(value) {
|
|
1180
|
-
return wholeNumberError("restorableFrom", value, 0);
|
|
1181
|
-
}
|
|
1182
|
-
function restoreRequestValidationError(body) {
|
|
1183
|
-
return wholeNumberError("moment", body.moment, 0) ?? wholeNumberError("batchSize", body.batchSize, 1, MAX_RESTORE_BATCH_SIZE);
|
|
1184
|
-
}
|
|
1185
|
-
|
|
1186
|
-
// src/server/http-backups.ts
|
|
1187
|
-
function parseOptionalBody(res, rawBody) {
|
|
1188
|
-
if (rawBody.length === 0) return {};
|
|
1189
|
-
return parseBody(res, rawBody);
|
|
1190
|
-
}
|
|
1191
|
-
async function resolveBackupDatabase(res, abort, sirannon, dbId) {
|
|
1192
|
-
let database;
|
|
1193
|
-
try {
|
|
1194
|
-
database = await sirannon.resolve(dbId);
|
|
1195
|
-
} catch (err) {
|
|
1196
|
-
sendCaughtError(res, abort, err);
|
|
1197
|
-
return null;
|
|
1198
|
-
}
|
|
1199
|
-
if (abort.aborted) return null;
|
|
1200
|
-
if (!database) {
|
|
1201
|
-
sendError(res, 404, "DATABASE_NOT_FOUND", `Database '${dbId}' not found`);
|
|
1202
|
-
return null;
|
|
1203
|
-
}
|
|
1204
|
-
return database;
|
|
1205
|
-
}
|
|
1206
|
-
function refusedOnTheFirstTick(turn) {
|
|
1207
|
-
const settled = turn.then(
|
|
1208
|
-
() => void 0,
|
|
1209
|
-
(err) => err ?? new SirannonError("The backup turn failed", "BACKUP_ERROR")
|
|
1210
|
-
);
|
|
1211
|
-
const tick = new Promise((resolve) => {
|
|
1212
|
-
setImmediate(() => resolve(void 0));
|
|
1213
|
-
});
|
|
1214
|
-
return Promise.race([settled, tick]);
|
|
1215
|
-
}
|
|
1216
|
-
function readStatus(res, abort, database) {
|
|
1217
|
-
try {
|
|
1218
|
-
return database.backupStatus();
|
|
1219
|
-
} catch (err) {
|
|
1220
|
-
sendCaughtError(res, abort, err);
|
|
1221
|
-
return null;
|
|
1222
|
-
}
|
|
1223
|
-
}
|
|
1224
|
-
function handleBackupTrigger(sirannon) {
|
|
1225
|
-
return async (res, dbId, rawBody, abort) => {
|
|
1226
|
-
if (!parseOptionalBody(res, rawBody)) return;
|
|
1227
|
-
const database = await resolveBackupDatabase(res, abort, sirannon, dbId);
|
|
1228
|
-
if (!database) return;
|
|
1229
|
-
if (!readStatus(res, abort, database)) return;
|
|
1230
|
-
const refusal = await refusedOnTheFirstTick(database.captureBackupChanges());
|
|
1231
|
-
if (refusal !== void 0) {
|
|
1232
|
-
sendCaughtError(res, abort, refusal);
|
|
1233
|
-
return;
|
|
1234
|
-
}
|
|
1235
|
-
if (abort.aborted) return;
|
|
1236
|
-
sendJson(res, { started: true }, "202 Accepted");
|
|
1237
|
-
};
|
|
1238
|
-
}
|
|
1239
|
-
function handleBackupStatus(sirannon) {
|
|
1240
|
-
return async (res, dbId, _ctx, abort) => {
|
|
1241
|
-
const database = await resolveBackupDatabase(res, abort, sirannon, dbId);
|
|
1242
|
-
if (!database) return;
|
|
1243
|
-
const status = readStatus(res, abort, database);
|
|
1244
|
-
if (!status || abort.aborted) return;
|
|
1245
|
-
sendJson(res, status);
|
|
1246
|
-
};
|
|
1247
|
-
}
|
|
1248
|
-
function handleBackupChain(sirannon) {
|
|
1249
|
-
return async (res, dbId, _ctx, abort) => {
|
|
1250
|
-
const database = await resolveBackupDatabase(res, abort, sirannon, dbId);
|
|
1251
|
-
if (!database) return;
|
|
1252
|
-
try {
|
|
1253
|
-
const chains = await database.backupChain();
|
|
1254
|
-
if (abort.aborted) return;
|
|
1255
|
-
const body = { chains };
|
|
1256
|
-
sendJson(res, body);
|
|
1257
|
-
} catch (err) {
|
|
1258
|
-
sendCaughtError(res, abort, err);
|
|
1259
|
-
}
|
|
1260
|
-
};
|
|
1261
|
-
}
|
|
1262
|
-
function handleBackupVerify(sirannon) {
|
|
1263
|
-
return async (res, dbId, rawBody, abort) => {
|
|
1264
|
-
const body = parseOptionalBody(res, rawBody);
|
|
1265
|
-
if (!body) return;
|
|
1266
|
-
const name = body.name;
|
|
1267
|
-
const nameError = verifyNameValidationError(name);
|
|
1268
|
-
if (nameError !== null || typeof name !== "string") {
|
|
1269
|
-
sendError(res, 400, "INVALID_REQUEST", nameError ?? 'Field "name" must be a string');
|
|
1270
|
-
return;
|
|
1271
|
-
}
|
|
1272
|
-
const database = await resolveBackupDatabase(res, abort, sirannon, dbId);
|
|
1273
|
-
if (!database) return;
|
|
1274
|
-
try {
|
|
1275
|
-
const result = await database.verifyBackup(name);
|
|
1276
|
-
if (abort.aborted) return;
|
|
1277
|
-
sendJson(res, result);
|
|
1278
|
-
} catch (err) {
|
|
1279
|
-
sendCaughtError(res, abort, err);
|
|
1280
|
-
}
|
|
1281
|
-
};
|
|
1282
|
-
}
|
|
1283
|
-
function handleBackupSafeToDelete(sirannon) {
|
|
1284
|
-
return async (res, dbId, rawBody, abort) => {
|
|
1285
|
-
const body = parseOptionalBody(res, rawBody);
|
|
1286
|
-
if (!body) return;
|
|
1287
|
-
const restorableFromError = restorableFromValidationError(body.restorableFrom);
|
|
1288
|
-
if (restorableFromError !== null) {
|
|
1289
|
-
sendError(res, 400, "INVALID_REQUEST", restorableFromError);
|
|
1290
|
-
return;
|
|
1291
|
-
}
|
|
1292
|
-
const database = await resolveBackupDatabase(res, abort, sirannon, dbId);
|
|
1293
|
-
if (!database) return;
|
|
1294
|
-
try {
|
|
1295
|
-
const records = await database.backupPiecesSafeToDelete(
|
|
1296
|
-
body.restorableFrom === void 0 ? void 0 : { restorableFrom: body.restorableFrom }
|
|
1297
|
-
);
|
|
1298
|
-
if (abort.aborted) return;
|
|
1299
|
-
const answer = { records };
|
|
1300
|
-
sendJson(res, answer);
|
|
1301
|
-
} catch (err) {
|
|
1302
|
-
sendCaughtError(res, abort, err);
|
|
1303
|
-
}
|
|
1304
|
-
};
|
|
1305
|
-
}
|
|
1306
|
-
|
|
1307
|
-
// src/server/http-backup-restore.ts
|
|
1308
|
-
function restoreAlreadyRunning(databaseId) {
|
|
1309
|
-
return `A restore of database '${databaseId}' is already under way, and it keeps that file to itself until it finishes`;
|
|
1310
|
-
}
|
|
1311
|
-
async function rebuildDatabase(request) {
|
|
1312
|
-
const { sirannon, runs, databaseId, location } = request;
|
|
1313
|
-
try {
|
|
1314
|
-
const outcome = await sirannon.withDatabaseOffline(databaseId, async (destPath) => {
|
|
1315
|
-
await discardStagedChain(location.stagingDir);
|
|
1316
|
-
return restoreBackup({
|
|
1317
|
-
destination: location.destination,
|
|
1318
|
-
chainName: location.chainName,
|
|
1319
|
-
driver: sirannon.driver,
|
|
1320
|
-
destPath,
|
|
1321
|
-
replaceExisting: true,
|
|
1322
|
-
moment: request.moment,
|
|
1323
|
-
...location.destinationTimeoutMs === void 0 ? {} : { destinationTimeoutMs: location.destinationTimeoutMs },
|
|
1324
|
-
...request.batchSize === void 0 ? {} : { batchSize: request.batchSize },
|
|
1325
|
-
onProgress: (progress) => runs.progressed(databaseId, progress)
|
|
1326
|
-
});
|
|
1327
|
-
});
|
|
1328
|
-
if (!outcome.ok) {
|
|
1329
|
-
runs.failed(databaseId, outcome.failure);
|
|
1330
|
-
return;
|
|
1331
|
-
}
|
|
1332
|
-
runs.finished(databaseId, outcome.value, outcome.reopenFailure);
|
|
1333
|
-
} catch (err) {
|
|
1334
|
-
runs.failed(databaseId, err);
|
|
1335
|
-
}
|
|
1336
|
-
}
|
|
1337
|
-
function handleBackupRestore(sirannon, accepted, runs) {
|
|
1338
|
-
return async (res, dbId, rawBody, abort) => {
|
|
1339
|
-
if (!accepted) {
|
|
1340
|
-
sendError(res, 403, "BACKUP_RESTORE_NOT_ACCEPTED", BACKUP_RESTORE_NOT_ACCEPTED_MESSAGE);
|
|
1341
|
-
return;
|
|
1342
|
-
}
|
|
1343
|
-
const body = parseOptionalBody(res, rawBody);
|
|
1344
|
-
if (!body) return;
|
|
1345
|
-
const validationError = restoreRequestValidationError(body);
|
|
1346
|
-
if (validationError !== null) {
|
|
1347
|
-
sendError(res, 400, "INVALID_REQUEST", validationError);
|
|
1348
|
-
return;
|
|
1349
|
-
}
|
|
1350
|
-
if (runs.read(dbId).state === "running") {
|
|
1351
|
-
sendError(res, 409, "BACKUP_RESTORE_IN_PROGRESS", restoreAlreadyRunning(dbId));
|
|
1352
|
-
return;
|
|
1353
|
-
}
|
|
1354
|
-
const database = await resolveBackupDatabase(res, abort, sirannon, dbId);
|
|
1355
|
-
if (!database) return;
|
|
1356
|
-
let location;
|
|
1357
|
-
try {
|
|
1358
|
-
location = database.backupLocation();
|
|
1359
|
-
} catch (err) {
|
|
1360
|
-
sendCaughtError(res, abort, err);
|
|
1361
|
-
return;
|
|
1362
|
-
}
|
|
1363
|
-
const moment = body.moment === void 0 ? Date.now() : body.moment;
|
|
1364
|
-
if (!runs.claim(dbId, moment)) {
|
|
1365
|
-
sendError(res, 409, "BACKUP_RESTORE_IN_PROGRESS", restoreAlreadyRunning(dbId));
|
|
1366
|
-
return;
|
|
1367
|
-
}
|
|
1368
|
-
void rebuildDatabase({
|
|
1369
|
-
sirannon,
|
|
1370
|
-
runs,
|
|
1371
|
-
databaseId: dbId,
|
|
1372
|
-
location,
|
|
1373
|
-
moment,
|
|
1374
|
-
...body.batchSize === void 0 ? {} : { batchSize: body.batchSize }
|
|
1375
|
-
});
|
|
1376
|
-
if (abort.aborted) return;
|
|
1377
|
-
sendJson(res, { started: true }, "202 Accepted");
|
|
1378
|
-
};
|
|
1379
|
-
}
|
|
1380
|
-
function handleBackupRestoreStatus(runs) {
|
|
1381
|
-
return async (res, dbId, _ctx, abort) => {
|
|
1382
|
-
if (abort.aborted) return;
|
|
1383
|
-
sendJson(res, runs.read(dbId));
|
|
1384
|
-
};
|
|
1385
|
-
}
|
|
1386
|
-
|
|
1387
1181
|
// src/server/sync-protocol.ts
|
|
1388
1182
|
var MAX_SYNC_BATCH_CHANGES = 1e3;
|
|
1389
1183
|
var NODE_ID_RE = /^[0-9a-f]{32}$/;
|
|
@@ -1547,111 +1341,351 @@ function decodeSyncBatch(body) {
|
|
|
1547
1341
|
function toChangesResponse(result) {
|
|
1548
1342
|
return { applied: result.applied, skipped: result.skipped, conflicts: result.conflicts };
|
|
1549
1343
|
}
|
|
1550
|
-
|
|
1551
|
-
// src/server/http-changes.ts
|
|
1552
|
-
var MAX_DEVICE_CLOCK_AHEAD_MS = 3e5;
|
|
1553
|
-
function changeClockRefusal(batch, nowMs) {
|
|
1554
|
-
for (const [index, change] of batch.changes.entries()) {
|
|
1555
|
-
let wallMs;
|
|
1344
|
+
|
|
1345
|
+
// src/server/http-changes.ts
|
|
1346
|
+
var MAX_DEVICE_CLOCK_AHEAD_MS = 3e5;
|
|
1347
|
+
function changeClockRefusal(batch, nowMs) {
|
|
1348
|
+
for (const [index, change] of batch.changes.entries()) {
|
|
1349
|
+
let wallMs;
|
|
1350
|
+
try {
|
|
1351
|
+
wallMs = HLC.decode(change.hlc).wallMs;
|
|
1352
|
+
} catch {
|
|
1353
|
+
wallMs = Number.NaN;
|
|
1354
|
+
}
|
|
1355
|
+
if (!Number.isFinite(wallMs)) {
|
|
1356
|
+
return { code: "INVALID_REQUEST", message: `changes[${index}].hlc is not a valid timestamp` };
|
|
1357
|
+
}
|
|
1358
|
+
if (wallMs > nowMs + MAX_DEVICE_CLOCK_AHEAD_MS) {
|
|
1359
|
+
return {
|
|
1360
|
+
code: "DEVICE_CLOCK_AHEAD",
|
|
1361
|
+
message: `changes[${index}] is stamped more than ${MAX_DEVICE_CLOCK_AHEAD_MS} ms ahead of the server clock; correct the device clock`
|
|
1362
|
+
};
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
return null;
|
|
1366
|
+
}
|
|
1367
|
+
function clockRangeOf(changes) {
|
|
1368
|
+
let min = changes[0].hlc;
|
|
1369
|
+
let max = changes[0].hlc;
|
|
1370
|
+
for (const change of changes) {
|
|
1371
|
+
if (change.hlc < min) min = change.hlc;
|
|
1372
|
+
if (change.hlc > max) max = change.hlc;
|
|
1373
|
+
}
|
|
1374
|
+
return { min, max };
|
|
1375
|
+
}
|
|
1376
|
+
function decodeVerifiableBatch(body) {
|
|
1377
|
+
let decoded;
|
|
1378
|
+
try {
|
|
1379
|
+
decoded = decodeSyncBatch(body);
|
|
1380
|
+
} catch {
|
|
1381
|
+
return null;
|
|
1382
|
+
}
|
|
1383
|
+
return { ...decoded, hlcRange: clockRangeOf(decoded.changes) };
|
|
1384
|
+
}
|
|
1385
|
+
async function refuseUngrantedPush(sirannon, databaseId, batch, identity) {
|
|
1386
|
+
if (!sirannon.hookRegistry.has("beforePush")) return;
|
|
1387
|
+
const tables = new Set(batch.changes.map((change) => change.table));
|
|
1388
|
+
for (const table of tables) {
|
|
1389
|
+
const ctx = { databaseId, table, deviceId: batch.sourceNodeId, identity };
|
|
1390
|
+
await sirannon.hookRegistry.invoke("beforePush", ctx);
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
async function refuseSchemaMismatch(res, abort, target, deviceVersion) {
|
|
1394
|
+
const appliedMigrations = target.appliedMigrations;
|
|
1395
|
+
if (typeof appliedMigrations !== "function") return false;
|
|
1396
|
+
const serverVersion = highestMigrationVersion(await appliedMigrations.call(target));
|
|
1397
|
+
const refusal = schemaVersionGateRefusal(deviceVersion, serverVersion);
|
|
1398
|
+
if (refusal === null) return false;
|
|
1399
|
+
if (!abort.aborted) {
|
|
1400
|
+
sendError(res, 409, refusal.code, refusal.message, { serverVersion });
|
|
1401
|
+
}
|
|
1402
|
+
return true;
|
|
1403
|
+
}
|
|
1404
|
+
function handleChanges(sirannon, resolveTarget) {
|
|
1405
|
+
return async (res, dbId, rawBody, abort, identity) => {
|
|
1406
|
+
const body = parseBody(res, rawBody);
|
|
1407
|
+
if (!body) return;
|
|
1408
|
+
const batchError = syncBatchValidationError(body.batch);
|
|
1409
|
+
if (batchError !== null) {
|
|
1410
|
+
sendError(res, 400, "INVALID_REQUEST", batchError);
|
|
1411
|
+
return;
|
|
1412
|
+
}
|
|
1413
|
+
const schemaVersionError = schemaVersionValidationError(body.schemaVersion);
|
|
1414
|
+
if (schemaVersionError !== null) {
|
|
1415
|
+
sendError(res, 400, "INVALID_REQUEST", schemaVersionError);
|
|
1416
|
+
return;
|
|
1417
|
+
}
|
|
1418
|
+
const batch = decodeVerifiableBatch(body.batch);
|
|
1419
|
+
if (batch === null) {
|
|
1420
|
+
sendError(res, 400, "INVALID_REQUEST", '"batch.changes" carries a malformed tagged value');
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1423
|
+
const target = await resolveExecutionTarget(res, abort, sirannon, dbId, resolveTarget);
|
|
1424
|
+
if (!target) return;
|
|
1425
|
+
try {
|
|
1426
|
+
await refuseUngrantedPush(sirannon, dbId, batch, identity);
|
|
1427
|
+
const clockRefusal = changeClockRefusal(batch, Date.now());
|
|
1428
|
+
if (clockRefusal !== null) {
|
|
1429
|
+
sendError(res, 400, clockRefusal.code, clockRefusal.message);
|
|
1430
|
+
return;
|
|
1431
|
+
}
|
|
1432
|
+
if (await refuseSchemaMismatch(res, abort, target, body.schemaVersion ?? 0)) return;
|
|
1433
|
+
const applyChanges = target.applyChanges;
|
|
1434
|
+
if (typeof applyChanges !== "function") {
|
|
1435
|
+
sendError(
|
|
1436
|
+
res,
|
|
1437
|
+
501,
|
|
1438
|
+
"SYNC_UNSUPPORTED",
|
|
1439
|
+
"The execution target for this database does not support applying sync changes"
|
|
1440
|
+
);
|
|
1441
|
+
return;
|
|
1442
|
+
}
|
|
1443
|
+
const result = await applyChanges.call(target, batch);
|
|
1444
|
+
if (abort.aborted) return;
|
|
1445
|
+
sendJson(res, toChangesResponse(result));
|
|
1446
|
+
} catch (err) {
|
|
1447
|
+
sendCaughtError(res, abort, err);
|
|
1448
|
+
}
|
|
1449
|
+
};
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
// src/server/backup-protocol.ts
|
|
1453
|
+
var BACKUP_RESTORE_NOT_ACCEPTED_MESSAGE = "This server does not restore databases over the wire; turn on acceptBackupRestore to open the route";
|
|
1454
|
+
function wholeNumberError(field, value, atLeast, atMost) {
|
|
1455
|
+
if (value === void 0) return null;
|
|
1456
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < atLeast) {
|
|
1457
|
+
return `Field "${field}" must be a whole number of at least ${atLeast}`;
|
|
1458
|
+
}
|
|
1459
|
+
if (atMost !== void 0 && value > atMost) {
|
|
1460
|
+
return `Field "${field}" must be a whole number of at most ${atMost}`;
|
|
1461
|
+
}
|
|
1462
|
+
return null;
|
|
1463
|
+
}
|
|
1464
|
+
function verifyNameValidationError(value) {
|
|
1465
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
1466
|
+
return 'Field "name" is required and must be the name a chain record states';
|
|
1467
|
+
}
|
|
1468
|
+
return null;
|
|
1469
|
+
}
|
|
1470
|
+
function restorableFromValidationError(value) {
|
|
1471
|
+
return wholeNumberError("restorableFrom", value, 0);
|
|
1472
|
+
}
|
|
1473
|
+
function restoreRequestValidationError(body) {
|
|
1474
|
+
return wholeNumberError("moment", body.moment, 0) ?? wholeNumberError("batchSize", body.batchSize, 1, MAX_RESTORE_BATCH_SIZE);
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
// src/server/http-backups.ts
|
|
1478
|
+
function parseOptionalBody(res, rawBody) {
|
|
1479
|
+
if (rawBody.length === 0) return {};
|
|
1480
|
+
return parseBody(res, rawBody);
|
|
1481
|
+
}
|
|
1482
|
+
async function resolveBackupDatabase(res, abort, sirannon, dbId) {
|
|
1483
|
+
let database;
|
|
1484
|
+
try {
|
|
1485
|
+
database = await sirannon.resolve(dbId);
|
|
1486
|
+
} catch (err) {
|
|
1487
|
+
sendCaughtError(res, abort, err);
|
|
1488
|
+
return null;
|
|
1489
|
+
}
|
|
1490
|
+
if (abort.aborted) return null;
|
|
1491
|
+
if (!database) {
|
|
1492
|
+
sendError(res, 404, "DATABASE_NOT_FOUND", `Database '${dbId}' not found`);
|
|
1493
|
+
return null;
|
|
1494
|
+
}
|
|
1495
|
+
return database;
|
|
1496
|
+
}
|
|
1497
|
+
function refusedOnTheFirstTick(turn) {
|
|
1498
|
+
const settled = turn.then(
|
|
1499
|
+
() => void 0,
|
|
1500
|
+
(err) => err ?? new SirannonError("The backup turn failed", "BACKUP_ERROR")
|
|
1501
|
+
);
|
|
1502
|
+
const tick = new Promise((resolve) => {
|
|
1503
|
+
setImmediate(() => resolve(void 0));
|
|
1504
|
+
});
|
|
1505
|
+
return Promise.race([settled, tick]);
|
|
1506
|
+
}
|
|
1507
|
+
function readStatus(res, abort, database) {
|
|
1508
|
+
try {
|
|
1509
|
+
return database.backupStatus();
|
|
1510
|
+
} catch (err) {
|
|
1511
|
+
sendCaughtError(res, abort, err);
|
|
1512
|
+
return null;
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
function handleBackupTrigger(sirannon) {
|
|
1516
|
+
return async (res, dbId, rawBody, abort) => {
|
|
1517
|
+
if (!parseOptionalBody(res, rawBody)) return;
|
|
1518
|
+
const database = await resolveBackupDatabase(res, abort, sirannon, dbId);
|
|
1519
|
+
if (!database) return;
|
|
1520
|
+
if (!readStatus(res, abort, database)) return;
|
|
1521
|
+
const refusal = await refusedOnTheFirstTick(database.captureBackupChanges());
|
|
1522
|
+
if (refusal !== void 0) {
|
|
1523
|
+
sendCaughtError(res, abort, refusal);
|
|
1524
|
+
return;
|
|
1525
|
+
}
|
|
1526
|
+
if (abort.aborted) return;
|
|
1527
|
+
sendJson(res, { started: true }, "202 Accepted");
|
|
1528
|
+
};
|
|
1529
|
+
}
|
|
1530
|
+
function handleBackupStatus(sirannon) {
|
|
1531
|
+
return async (res, dbId, _ctx, abort) => {
|
|
1532
|
+
const database = await resolveBackupDatabase(res, abort, sirannon, dbId);
|
|
1533
|
+
if (!database) return;
|
|
1534
|
+
const status = readStatus(res, abort, database);
|
|
1535
|
+
if (!status || abort.aborted) return;
|
|
1536
|
+
sendJson(res, status);
|
|
1537
|
+
};
|
|
1538
|
+
}
|
|
1539
|
+
function handleBackupChain(sirannon) {
|
|
1540
|
+
return async (res, dbId, _ctx, abort) => {
|
|
1541
|
+
const database = await resolveBackupDatabase(res, abort, sirannon, dbId);
|
|
1542
|
+
if (!database) return;
|
|
1543
|
+
try {
|
|
1544
|
+
const chains = await database.backupChain();
|
|
1545
|
+
if (abort.aborted) return;
|
|
1546
|
+
const body = { chains };
|
|
1547
|
+
sendJson(res, body);
|
|
1548
|
+
} catch (err) {
|
|
1549
|
+
sendCaughtError(res, abort, err);
|
|
1550
|
+
}
|
|
1551
|
+
};
|
|
1552
|
+
}
|
|
1553
|
+
function handleBackupVerify(sirannon) {
|
|
1554
|
+
return async (res, dbId, rawBody, abort) => {
|
|
1555
|
+
const body = parseOptionalBody(res, rawBody);
|
|
1556
|
+
if (!body) return;
|
|
1557
|
+
const name = body.name;
|
|
1558
|
+
const nameError = verifyNameValidationError(name);
|
|
1559
|
+
if (nameError !== null || typeof name !== "string") {
|
|
1560
|
+
sendError(res, 400, "INVALID_REQUEST", nameError ?? 'Field "name" must be a string');
|
|
1561
|
+
return;
|
|
1562
|
+
}
|
|
1563
|
+
const database = await resolveBackupDatabase(res, abort, sirannon, dbId);
|
|
1564
|
+
if (!database) return;
|
|
1556
1565
|
try {
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1566
|
+
const result = await database.verifyBackup(name);
|
|
1567
|
+
if (abort.aborted) return;
|
|
1568
|
+
sendJson(res, result);
|
|
1569
|
+
} catch (err) {
|
|
1570
|
+
sendCaughtError(res, abort, err);
|
|
1560
1571
|
}
|
|
1561
|
-
|
|
1562
|
-
|
|
1572
|
+
};
|
|
1573
|
+
}
|
|
1574
|
+
function handleBackupSafeToDelete(sirannon) {
|
|
1575
|
+
return async (res, dbId, rawBody, abort) => {
|
|
1576
|
+
const body = parseOptionalBody(res, rawBody);
|
|
1577
|
+
if (!body) return;
|
|
1578
|
+
const restorableFromError = restorableFromValidationError(body.restorableFrom);
|
|
1579
|
+
if (restorableFromError !== null) {
|
|
1580
|
+
sendError(res, 400, "INVALID_REQUEST", restorableFromError);
|
|
1581
|
+
return;
|
|
1563
1582
|
}
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1583
|
+
const database = await resolveBackupDatabase(res, abort, sirannon, dbId);
|
|
1584
|
+
if (!database) return;
|
|
1585
|
+
try {
|
|
1586
|
+
const records = await database.backupPiecesSafeToDelete(
|
|
1587
|
+
body.restorableFrom === void 0 ? void 0 : { restorableFrom: body.restorableFrom }
|
|
1588
|
+
);
|
|
1589
|
+
if (abort.aborted) return;
|
|
1590
|
+
const answer = { records };
|
|
1591
|
+
sendJson(res, answer);
|
|
1592
|
+
} catch (err) {
|
|
1593
|
+
sendCaughtError(res, abort, err);
|
|
1569
1594
|
}
|
|
1570
|
-
}
|
|
1571
|
-
return null;
|
|
1595
|
+
};
|
|
1572
1596
|
}
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1597
|
+
|
|
1598
|
+
// src/server/http-encryption.ts
|
|
1599
|
+
var MAX_KEY_REF_LENGTH = 256;
|
|
1600
|
+
var ENCRYPTION_CHANGE_REQUEST = object({
|
|
1601
|
+
keyRef: nullable(pipe(string(), minLength(1), maxLength(MAX_KEY_REF_LENGTH)))
|
|
1602
|
+
});
|
|
1603
|
+
var ENCRYPTION_CHANGE_NOT_ACCEPTED_MESSAGE = "This server does not change database keys over the wire; turn on acceptEncryptionChange to open the route";
|
|
1604
|
+
function resolveEncryptionChangeSettings(options) {
|
|
1605
|
+
const accepted = options?.acceptEncryptionChange === true;
|
|
1606
|
+
if (accepted && (options?.authenticate === void 0 || typeof options.encryptionKeys !== "function")) {
|
|
1607
|
+
throw new SirannonError(
|
|
1608
|
+
"ServerOptions.acceptEncryptionChange opens a route that changes the key of a running database, so it needs an authenticate hook to name the caller and an encryptionKeys function to resolve each keyRef",
|
|
1609
|
+
"INVALID_ENCRYPTION_CHANGE"
|
|
1610
|
+
);
|
|
1579
1611
|
}
|
|
1580
|
-
return {
|
|
1612
|
+
return { accepted, keys: options?.encryptionKeys };
|
|
1581
1613
|
}
|
|
1582
|
-
function
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
} catch {
|
|
1587
|
-
return null;
|
|
1588
|
-
}
|
|
1589
|
-
return { ...decoded, hlcRange: clockRangeOf(decoded.changes) };
|
|
1614
|
+
async function checkedKeySource(keys, databaseId, keyRef) {
|
|
1615
|
+
const source = () => keys(databaseId, keyRef);
|
|
1616
|
+
wipeEncryptionKey(await resolveEncryptionKey(source));
|
|
1617
|
+
return source;
|
|
1590
1618
|
}
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
const tables = new Set(batch.changes.map((change) => change.table));
|
|
1594
|
-
for (const table of tables) {
|
|
1595
|
-
const ctx = { databaseId, table, deviceId: batch.sourceNodeId, identity };
|
|
1596
|
-
await sirannon.hookRegistry.invoke("beforePush", ctx);
|
|
1597
|
-
}
|
|
1619
|
+
function errorForClient(error) {
|
|
1620
|
+
return { code: error.code, message: clientMessageFor(error) };
|
|
1598
1621
|
}
|
|
1599
|
-
|
|
1600
|
-
const
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1622
|
+
function statusForClient(status) {
|
|
1623
|
+
const { error, reopenError } = status.change;
|
|
1624
|
+
return {
|
|
1625
|
+
...status,
|
|
1626
|
+
change: {
|
|
1627
|
+
...status.change,
|
|
1628
|
+
...error === void 0 ? {} : { error: errorForClient(error) },
|
|
1629
|
+
...reopenError === void 0 ? {} : { reopenError: errorForClient(reopenError) }
|
|
1630
|
+
}
|
|
1631
|
+
};
|
|
1609
1632
|
}
|
|
1610
|
-
function
|
|
1611
|
-
return async (res, dbId, rawBody, abort
|
|
1612
|
-
const
|
|
1613
|
-
if (!
|
|
1614
|
-
|
|
1615
|
-
if (batchError !== null) {
|
|
1616
|
-
sendError(res, 400, "INVALID_REQUEST", batchError);
|
|
1633
|
+
function handleEncryptionChange(sirannon, settings) {
|
|
1634
|
+
return async (res, dbId, rawBody, abort) => {
|
|
1635
|
+
const keys = settings.keys;
|
|
1636
|
+
if (!settings.accepted || keys === void 0) {
|
|
1637
|
+
sendError(res, 403, "ENCRYPTION_CHANGE_NOT_ACCEPTED", ENCRYPTION_CHANGE_NOT_ACCEPTED_MESSAGE);
|
|
1617
1638
|
return;
|
|
1618
1639
|
}
|
|
1619
|
-
const
|
|
1620
|
-
if (
|
|
1621
|
-
|
|
1640
|
+
const body = parseOptionalBody(res, rawBody);
|
|
1641
|
+
if (body === null) return;
|
|
1642
|
+
const request = safeParse(ENCRYPTION_CHANGE_REQUEST, body);
|
|
1643
|
+
if (!request.success) {
|
|
1644
|
+
sendError(
|
|
1645
|
+
res,
|
|
1646
|
+
400,
|
|
1647
|
+
"INVALID_REQUEST",
|
|
1648
|
+
`keyRef takes a string of 1 to ${MAX_KEY_REF_LENGTH} characters that names the target key, or null to remove encryption`
|
|
1649
|
+
);
|
|
1622
1650
|
return;
|
|
1623
1651
|
}
|
|
1624
|
-
const
|
|
1625
|
-
if (
|
|
1626
|
-
sendError(res,
|
|
1652
|
+
const database = sirannon.get(dbId);
|
|
1653
|
+
if (!database) {
|
|
1654
|
+
sendError(res, 404, "DATABASE_NOT_FOUND", `Database '${dbId}' not found`);
|
|
1627
1655
|
return;
|
|
1628
1656
|
}
|
|
1629
|
-
|
|
1630
|
-
if (!target) return;
|
|
1657
|
+
let key = null;
|
|
1631
1658
|
try {
|
|
1632
|
-
await
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1659
|
+
if (request.output.keyRef !== null) key = await checkedKeySource(keys, dbId, request.output.keyRef);
|
|
1660
|
+
} catch (err) {
|
|
1661
|
+
sendCaughtError(res, abort, err);
|
|
1662
|
+
return;
|
|
1663
|
+
}
|
|
1664
|
+
const refusal = await refusedOnTheFirstTick(database.changeEncryption({ key }));
|
|
1665
|
+
if (refusal !== void 0) {
|
|
1666
|
+
sendCaughtError(res, abort, refusal);
|
|
1667
|
+
return;
|
|
1668
|
+
}
|
|
1669
|
+
if (abort.aborted) return;
|
|
1670
|
+
sendJson(res, { started: true }, "202 Accepted");
|
|
1671
|
+
};
|
|
1672
|
+
}
|
|
1673
|
+
function handleEncryptionStatus(sirannon) {
|
|
1674
|
+
return async (res, dbId, _ctx, abort) => {
|
|
1675
|
+
const database = sirannon.get(dbId);
|
|
1676
|
+
if (!database) {
|
|
1677
|
+
sendError(res, 404, "DATABASE_NOT_FOUND", `Database '${dbId}' not found`);
|
|
1678
|
+
return;
|
|
1679
|
+
}
|
|
1680
|
+
let status;
|
|
1681
|
+
try {
|
|
1682
|
+
status = await database.encryptionStatus();
|
|
1652
1683
|
} catch (err) {
|
|
1653
1684
|
sendCaughtError(res, abort, err);
|
|
1685
|
+
return;
|
|
1654
1686
|
}
|
|
1687
|
+
if (abort.aborted) return;
|
|
1688
|
+
sendJson(res, statusForClient(status));
|
|
1655
1689
|
};
|
|
1656
1690
|
}
|
|
1657
1691
|
|
|
@@ -1880,7 +1914,7 @@ function handleSnapshotManifest(sirannon) {
|
|
|
1880
1914
|
await database.runCdcMaintenance(async (writer) => {
|
|
1881
1915
|
epoch = await ensureCdcEpoch(writer);
|
|
1882
1916
|
});
|
|
1883
|
-
const conn = await
|
|
1917
|
+
const conn = await database.openFileConnection({ walMode: true });
|
|
1884
1918
|
try {
|
|
1885
1919
|
const tableNames = await tablesInFkOrder(conn);
|
|
1886
1920
|
await refuseUngrantedTables(sirannon, dbId, tableNames, identity);
|
|
@@ -1935,7 +1969,7 @@ function handleSnapshotPage(sirannon) {
|
|
|
1935
1969
|
if (!database) return;
|
|
1936
1970
|
try {
|
|
1937
1971
|
await refuseUngrantedTables(sirannon, dbId, [body.table], identity);
|
|
1938
|
-
const conn = await
|
|
1972
|
+
const conn = await database.openFileConnection({ walMode: true });
|
|
1939
1973
|
try {
|
|
1940
1974
|
if (!await selectTableExists(conn, body.table)) {
|
|
1941
1975
|
sendError(res, 404, "TABLE_NOT_FOUND", `Table '${body.table}' not found`);
|
|
@@ -2116,6 +2150,104 @@ function wrapOperationRoute(deps, handler) {
|
|
|
2116
2150
|
};
|
|
2117
2151
|
}
|
|
2118
2152
|
|
|
2153
|
+
// src/server/http-backup-restore.ts
|
|
2154
|
+
function restoreAlreadyRunning(databaseId) {
|
|
2155
|
+
return `A restore of database '${databaseId}' is already under way, and it keeps that file to itself until it finishes`;
|
|
2156
|
+
}
|
|
2157
|
+
async function rebuildDatabase(request) {
|
|
2158
|
+
const { sirannon, runs, databaseId, location } = request;
|
|
2159
|
+
try {
|
|
2160
|
+
const outcome = await sirannon.withDatabaseOffline(databaseId, async (destPath, encryption) => {
|
|
2161
|
+
await discardStagedChain(location.stagingDir);
|
|
2162
|
+
return restoreBackup({
|
|
2163
|
+
destination: location.destination,
|
|
2164
|
+
chainName: location.chainName,
|
|
2165
|
+
driver: sirannon.driver,
|
|
2166
|
+
destPath,
|
|
2167
|
+
...encryption === void 0 ? {} : { encryption },
|
|
2168
|
+
replaceExisting: true,
|
|
2169
|
+
moment: request.moment,
|
|
2170
|
+
...location.destinationTimeoutMs === void 0 ? {} : { destinationTimeoutMs: location.destinationTimeoutMs },
|
|
2171
|
+
...request.batchSize === void 0 ? {} : { batchSize: request.batchSize },
|
|
2172
|
+
onProgress: (progress) => runs.progressed(databaseId, progress)
|
|
2173
|
+
});
|
|
2174
|
+
});
|
|
2175
|
+
if (!outcome.ok) {
|
|
2176
|
+
runs.failed(databaseId, outcome.failure);
|
|
2177
|
+
return;
|
|
2178
|
+
}
|
|
2179
|
+
runs.finished(databaseId, outcome.value, outcome.reopenFailure);
|
|
2180
|
+
} catch (err) {
|
|
2181
|
+
runs.failed(databaseId, err);
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
function handleBackupRestore(sirannon, accepted, runs) {
|
|
2185
|
+
return async (res, dbId, rawBody, abort) => {
|
|
2186
|
+
if (!accepted) {
|
|
2187
|
+
sendError(res, 403, "BACKUP_RESTORE_NOT_ACCEPTED", BACKUP_RESTORE_NOT_ACCEPTED_MESSAGE);
|
|
2188
|
+
return;
|
|
2189
|
+
}
|
|
2190
|
+
const body = parseOptionalBody(res, rawBody);
|
|
2191
|
+
if (!body) return;
|
|
2192
|
+
const validationError = restoreRequestValidationError(body);
|
|
2193
|
+
if (validationError !== null) {
|
|
2194
|
+
sendError(res, 400, "INVALID_REQUEST", validationError);
|
|
2195
|
+
return;
|
|
2196
|
+
}
|
|
2197
|
+
if (runs.read(dbId).state === "running") {
|
|
2198
|
+
sendError(res, 409, "BACKUP_RESTORE_IN_PROGRESS", restoreAlreadyRunning(dbId));
|
|
2199
|
+
return;
|
|
2200
|
+
}
|
|
2201
|
+
const database = await resolveBackupDatabase(res, abort, sirannon, dbId);
|
|
2202
|
+
if (!database) return;
|
|
2203
|
+
let location;
|
|
2204
|
+
try {
|
|
2205
|
+
database.assertNoReplicationEngine("restore");
|
|
2206
|
+
location = database.backupLocation();
|
|
2207
|
+
} catch (err) {
|
|
2208
|
+
sendCaughtError(res, abort, err);
|
|
2209
|
+
return;
|
|
2210
|
+
}
|
|
2211
|
+
const moment = body.moment === void 0 ? Date.now() : body.moment;
|
|
2212
|
+
if (!runs.claim(dbId, moment)) {
|
|
2213
|
+
sendError(res, 409, "BACKUP_RESTORE_IN_PROGRESS", restoreAlreadyRunning(dbId));
|
|
2214
|
+
return;
|
|
2215
|
+
}
|
|
2216
|
+
void rebuildDatabase({
|
|
2217
|
+
sirannon,
|
|
2218
|
+
runs,
|
|
2219
|
+
databaseId: dbId,
|
|
2220
|
+
location,
|
|
2221
|
+
moment,
|
|
2222
|
+
...body.batchSize === void 0 ? {} : { batchSize: body.batchSize }
|
|
2223
|
+
});
|
|
2224
|
+
if (abort.aborted) return;
|
|
2225
|
+
sendJson(res, { started: true }, "202 Accepted");
|
|
2226
|
+
};
|
|
2227
|
+
}
|
|
2228
|
+
function handleBackupRestoreStatus(runs) {
|
|
2229
|
+
return async (res, dbId, _ctx, abort) => {
|
|
2230
|
+
if (abort.aborted) return;
|
|
2231
|
+
sendJson(res, runs.read(dbId));
|
|
2232
|
+
};
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
// src/server/operator-routes.ts
|
|
2236
|
+
function registerOperatorRoutes(routes, deps) {
|
|
2237
|
+
const { sirannon, routeDeps } = deps;
|
|
2238
|
+
const post = (path, handler) => routes.post(path, wrapDbRoute(routeDeps, handler));
|
|
2239
|
+
const get = (path, handler) => routes.get(path, wrapDbGetRoute(routeDeps, handler));
|
|
2240
|
+
post("/db/:id/backup", handleBackupTrigger(sirannon));
|
|
2241
|
+
get("/db/:id/backup", handleBackupStatus(sirannon));
|
|
2242
|
+
get("/db/:id/backup/chain", handleBackupChain(sirannon));
|
|
2243
|
+
post("/db/:id/backup/verify", handleBackupVerify(sirannon));
|
|
2244
|
+
post("/db/:id/backup/safe-to-delete", handleBackupSafeToDelete(sirannon));
|
|
2245
|
+
post("/db/:id/backup/restore", handleBackupRestore(sirannon, deps.acceptBackupRestore, deps.restoreRuns));
|
|
2246
|
+
get("/db/:id/backup/restore", handleBackupRestoreStatus(deps.restoreRuns));
|
|
2247
|
+
post("/db/:id/encryption", handleEncryptionChange(sirannon, deps.encryptionChange));
|
|
2248
|
+
get("/db/:id/encryption", handleEncryptionStatus(sirannon));
|
|
2249
|
+
}
|
|
2250
|
+
|
|
2119
2251
|
// src/server/uws-loader.ts
|
|
2120
2252
|
var UWEBSOCKETS_DEPENDENCY = {
|
|
2121
2253
|
packageName: "uWebSockets.js",
|
|
@@ -2229,6 +2361,7 @@ var CdcContextRegistry = class {
|
|
|
2229
2361
|
const ctx = await promise;
|
|
2230
2362
|
if (this.closed) {
|
|
2231
2363
|
ctx.stopPolling();
|
|
2364
|
+
ctx.releaseHold();
|
|
2232
2365
|
await ctx.cdcConn.close().catch(() => {
|
|
2233
2366
|
});
|
|
2234
2367
|
throw new SirannonError("WebSocket handler is shut down", "HANDLER_CLOSED");
|
|
@@ -2246,6 +2379,7 @@ var CdcContextRegistry = class {
|
|
|
2246
2379
|
const ctx = this.contexts.get(databaseId);
|
|
2247
2380
|
if (!ctx || ctx.manager.size > 0) return;
|
|
2248
2381
|
ctx.stopPolling();
|
|
2382
|
+
ctx.releaseHold();
|
|
2249
2383
|
ctx.cdcConn.close().catch(() => {
|
|
2250
2384
|
});
|
|
2251
2385
|
this.contexts.delete(databaseId);
|
|
@@ -2255,6 +2389,7 @@ var CdcContextRegistry = class {
|
|
|
2255
2389
|
this.closed = true;
|
|
2256
2390
|
for (const ctx of this.contexts.values()) {
|
|
2257
2391
|
ctx.stopPolling();
|
|
2392
|
+
ctx.releaseHold();
|
|
2258
2393
|
try {
|
|
2259
2394
|
await ctx.cdcConn.close();
|
|
2260
2395
|
} catch {
|
|
@@ -2263,7 +2398,7 @@ var CdcContextRegistry = class {
|
|
|
2263
2398
|
this.contexts.clear();
|
|
2264
2399
|
}
|
|
2265
2400
|
async createContext(database) {
|
|
2266
|
-
const cdcConn = await
|
|
2401
|
+
const cdcConn = await database.openFileConnection({ walMode: true });
|
|
2267
2402
|
const retention = database.changeRetention.cdcRetention ?? this.retentionMs;
|
|
2268
2403
|
const deviceRetention = resolveDeviceRetentionPolicy(database.changeRetention, this.serverDeviceRetention);
|
|
2269
2404
|
const tracker = new ChangeTracker(retention === void 0 ? void 0 : { retention });
|
|
@@ -2284,19 +2419,21 @@ var CdcContextRegistry = class {
|
|
|
2284
2419
|
});
|
|
2285
2420
|
throw new SirannonError(`Database '${database.id}' is closed`, "DATABASE_CLOSED");
|
|
2286
2421
|
}
|
|
2287
|
-
|
|
2422
|
+
const ctx = { cdcConn, tracker, manager, stopPolling: () => {
|
|
2423
|
+
}, releaseHold: () => {
|
|
2424
|
+
}, epoch };
|
|
2425
|
+
let interval = null;
|
|
2426
|
+
let inFlight = null;
|
|
2288
2427
|
let consecutiveErrors = 0;
|
|
2289
2428
|
let tickCount = 0;
|
|
2290
2429
|
const MAX_CONSECUTIVE_ERRORS = 10;
|
|
2291
2430
|
const stopPolling = () => {
|
|
2292
|
-
clearInterval(interval);
|
|
2431
|
+
if (interval !== null) clearInterval(interval);
|
|
2432
|
+
interval = null;
|
|
2293
2433
|
};
|
|
2294
|
-
const
|
|
2295
|
-
if (manager.size === 0) return;
|
|
2296
|
-
if (polling) return;
|
|
2297
|
-
polling = true;
|
|
2434
|
+
const pollOnce = async () => {
|
|
2298
2435
|
try {
|
|
2299
|
-
const events = await tracker.poll(cdcConn);
|
|
2436
|
+
const events = await tracker.poll(ctx.cdcConn);
|
|
2300
2437
|
if (events.length > 0) {
|
|
2301
2438
|
manager.dispatch(events);
|
|
2302
2439
|
}
|
|
@@ -2316,13 +2453,36 @@ var CdcContextRegistry = class {
|
|
|
2316
2453
|
if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
|
|
2317
2454
|
stopPolling();
|
|
2318
2455
|
}
|
|
2319
|
-
} finally {
|
|
2320
|
-
polling = false;
|
|
2321
2456
|
}
|
|
2322
2457
|
};
|
|
2323
|
-
const
|
|
2324
|
-
|
|
2325
|
-
|
|
2458
|
+
const tick = () => {
|
|
2459
|
+
if (manager.size === 0 || inFlight !== null) return;
|
|
2460
|
+
inFlight = pollOnce().finally(() => {
|
|
2461
|
+
inFlight = null;
|
|
2462
|
+
});
|
|
2463
|
+
};
|
|
2464
|
+
const startPolling = () => {
|
|
2465
|
+
stopPolling();
|
|
2466
|
+
consecutiveErrors = 0;
|
|
2467
|
+
const started = setInterval(tick, DEFAULT_POLL_INTERVAL_MS);
|
|
2468
|
+
started.unref?.();
|
|
2469
|
+
interval = started;
|
|
2470
|
+
};
|
|
2471
|
+
ctx.stopPolling = stopPolling;
|
|
2472
|
+
startPolling();
|
|
2473
|
+
ctx.releaseHold = database.holdFileConnection({
|
|
2474
|
+
release: async () => {
|
|
2475
|
+
stopPolling();
|
|
2476
|
+
await inFlight;
|
|
2477
|
+
await ctx.cdcConn.close().catch(() => {
|
|
2478
|
+
});
|
|
2479
|
+
},
|
|
2480
|
+
reopen: async () => {
|
|
2481
|
+
ctx.cdcConn = await database.openFileConnection({ walMode: true });
|
|
2482
|
+
startPolling();
|
|
2483
|
+
}
|
|
2484
|
+
});
|
|
2485
|
+
return ctx;
|
|
2326
2486
|
}
|
|
2327
2487
|
};
|
|
2328
2488
|
|
|
@@ -4093,6 +4253,7 @@ var SirannonServer = class {
|
|
|
4093
4253
|
authenticateHook;
|
|
4094
4254
|
acceptSql;
|
|
4095
4255
|
acceptBackupRestore;
|
|
4256
|
+
encryptionChange;
|
|
4096
4257
|
acceptDeviceSync;
|
|
4097
4258
|
restoreRuns = new BackupRestoreRuns();
|
|
4098
4259
|
drain = new ConnectionDrain();
|
|
@@ -4115,6 +4276,7 @@ var SirannonServer = class {
|
|
|
4115
4276
|
this.acceptSql = options?.acceptSql === true;
|
|
4116
4277
|
this.acceptBackupRestore = options?.acceptBackupRestore === true;
|
|
4117
4278
|
assertBackupRestoreAuthenticated(this.acceptBackupRestore, this.authenticateHook !== void 0);
|
|
4279
|
+
this.encryptionChange = resolveEncryptionChangeSettings(options);
|
|
4118
4280
|
this.acceptDeviceSync = options?.acceptDeviceSync === true;
|
|
4119
4281
|
assertDeviceSyncAuthenticated(this.acceptDeviceSync, this.authenticateHook !== void 0);
|
|
4120
4282
|
this.operations = { registry: options?.operations, shared: options?.sharedOperations };
|
|
@@ -4268,7 +4430,13 @@ var SirannonServer = class {
|
|
|
4268
4430
|
"/db/:id/execute/:name",
|
|
4269
4431
|
this.wrapOperationRoute(handleOperationExecute(this.sirannon, this.operations, this.resolveExecutionTarget))
|
|
4270
4432
|
);
|
|
4271
|
-
|
|
4433
|
+
registerOperatorRoutes(routes, {
|
|
4434
|
+
sirannon: this.sirannon,
|
|
4435
|
+
routeDeps: this.routeDeps(),
|
|
4436
|
+
acceptBackupRestore: this.acceptBackupRestore,
|
|
4437
|
+
restoreRuns: this.restoreRuns,
|
|
4438
|
+
encryptionChange: this.encryptionChange
|
|
4439
|
+
});
|
|
4272
4440
|
if (this.acceptDeviceSync) {
|
|
4273
4441
|
routes.post("/db/:id/changes", this.wrapDbRoute(handleChanges(this.sirannon, this.resolveExecutionTarget)));
|
|
4274
4442
|
routes.post("/db/:id/migrations", this.wrapDbRoute(handleMigrationList(this.sirannon)));
|
|
@@ -4291,18 +4459,6 @@ var SirannonServer = class {
|
|
|
4291
4459
|
sendError(res, 404, "NOT_FOUND", "Route not found");
|
|
4292
4460
|
});
|
|
4293
4461
|
}
|
|
4294
|
-
registerBackupRoutes(routes) {
|
|
4295
|
-
routes.post("/db/:id/backup", this.wrapDbRoute(handleBackupTrigger(this.sirannon)));
|
|
4296
|
-
routes.get("/db/:id/backup", this.wrapDbGetRoute(handleBackupStatus(this.sirannon)));
|
|
4297
|
-
routes.get("/db/:id/backup/chain", this.wrapDbGetRoute(handleBackupChain(this.sirannon)));
|
|
4298
|
-
routes.post("/db/:id/backup/verify", this.wrapDbRoute(handleBackupVerify(this.sirannon)));
|
|
4299
|
-
routes.post("/db/:id/backup/safe-to-delete", this.wrapDbRoute(handleBackupSafeToDelete(this.sirannon)));
|
|
4300
|
-
routes.post(
|
|
4301
|
-
"/db/:id/backup/restore",
|
|
4302
|
-
this.wrapDbRoute(handleBackupRestore(this.sirannon, this.acceptBackupRestore, this.restoreRuns))
|
|
4303
|
-
);
|
|
4304
|
-
routes.get("/db/:id/backup/restore", this.wrapDbGetRoute(handleBackupRestoreStatus(this.restoreRuns)));
|
|
4305
|
-
}
|
|
4306
4462
|
withCors(handler) {
|
|
4307
4463
|
if (!this.cors) return handler;
|
|
4308
4464
|
const cors = this.cors;
|