@ours.network/cli 2.0.4 → 2.1.0
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/README.md +6 -2
- package/dist/{boot-HNJ7ZR5L.js → boot-QLA3LXVX.js} +2 -2
- package/dist/{chunk-DRQEV4SN.js → chunk-3C5WAQAT.js} +8 -1
- package/dist/{chunk-FXKFSNKS.js → chunk-DTWZF3ZG.js} +28 -1
- package/dist/{chunk-QKDH6CJR.js → chunk-GQHDQR6I.js} +1 -1
- package/dist/{chunk-NNHFXFLS.js → chunk-H7DX6EHZ.js} +2 -1
- package/dist/{chunk-DZH2YEXT.js → chunk-UHYUIHM2.js} +1 -1
- package/dist/{chunk-TSYICL3I.js → chunk-YPDZQ4XD.js} +888 -170
- package/dist/commands.d.ts +1 -1
- package/dist/commands.js +1 -1
- package/dist/config-command.js +1 -1
- package/dist/{daemon-LTJHJDWZ.js → daemon-5UZIR37V.js} +3 -3
- package/dist/help.js +2 -2
- package/dist/lifecycle.js +1 -1
- package/dist/main.js +15 -6
- package/dist/mufl_code/351F16D444E6DAA252D4A8D829D17204360D9015FEF670ACE68FB4747BB3B14E.muflo +0 -0
- package/dist/{server-35QOYLY4.js → server-3PM3QOPD.js} +36 -3
- package/package.json +1 -1
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
explicitApiToken,
|
|
5
5
|
loadConfig,
|
|
6
6
|
resolveApiToken
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-DTWZF3ZG.js";
|
|
8
8
|
|
|
9
9
|
// ../../src/boot.ts
|
|
10
10
|
import { adapt_wrapper } from "@adapt-toolkit/sdk/executables";
|
|
@@ -89,13 +89,14 @@ function createStartupProgressReporter(stateDir, opts = {}) {
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
// ../../src/runtime/env.ts
|
|
92
|
-
var VERSION = true ? "3.0
|
|
92
|
+
var VERSION = true ? "3.1.0" : "0.0.0-dev";
|
|
93
93
|
var CONFIG = loadConfig();
|
|
94
94
|
var STATE_DIR = CONFIG.stateDir;
|
|
95
95
|
var BROKER_URL = CONFIG.brokerUrl;
|
|
96
96
|
var TRANSPORT = process.env.OURS_TRANSPORT ?? "http";
|
|
97
97
|
var PORT = CONFIG.port;
|
|
98
98
|
var GC_INTERVAL_MS = CONFIG.gcIntervalMs;
|
|
99
|
+
var HISTORY_MAX_BYTES = CONFIG.historyMaxBytes;
|
|
99
100
|
var API_VISIBILITY = CONFIG.apiVisibility;
|
|
100
101
|
var startupHeartbeatMs = Number(process.env.OURS_TEST_STARTUP_HEARTBEAT_MS || "") || void 0;
|
|
101
102
|
var startupProgress = TRANSPORT === "http" ? createStartupProgressReporter(STATE_DIR, { heartbeatMs: startupHeartbeatMs }) : null;
|
|
@@ -166,8 +167,8 @@ function locateUnit() {
|
|
|
166
167
|
const override = process.env.OURS_UNIT_DIR;
|
|
167
168
|
const noCap = process.env.OURS_ADVERTISE_MIGRATE === "0";
|
|
168
169
|
const withNoCap = (dir) => noCap ? [`${dir}-nocap`] : [dir];
|
|
169
|
-
const
|
|
170
|
-
for (const dir of
|
|
170
|
+
const candidates2 = override ? withNoCap(resolve(override)) : [join2(here, "mufl_code"), join2(here, "..", "mufl_code")].flatMap(withNoCap);
|
|
171
|
+
for (const dir of candidates2) {
|
|
171
172
|
if (!fs2.existsSync(dir)) continue;
|
|
172
173
|
const muflo = fs2.readdirSync(dir).find((f) => f.endsWith(".muflo"));
|
|
173
174
|
if (muflo) {
|
|
@@ -177,7 +178,7 @@ function locateUnit() {
|
|
|
177
178
|
}
|
|
178
179
|
}
|
|
179
180
|
throw new Error(
|
|
180
|
-
`no compiled .muflo packet found (looked in: ${
|
|
181
|
+
`no compiled .muflo packet found (looked in: ${candidates2.join(", ")})` + (noCap ? ' \u2014 OURS_ADVERTISE_MIGRATE=0 requires a "<dir>-nocap" packet variant (compiled with $advertise = [core.e2e] only); build it or unset the env.' : "")
|
|
181
182
|
);
|
|
182
183
|
}
|
|
183
184
|
var UNIT;
|
|
@@ -186,25 +187,702 @@ function setUnit(u) {
|
|
|
186
187
|
}
|
|
187
188
|
|
|
188
189
|
// ../../src/identity/model.ts
|
|
189
|
-
import { join as
|
|
190
|
+
import { join as join5 } from "node:path";
|
|
190
191
|
import { createHash as createHash2 } from "node:crypto";
|
|
191
|
-
import * as
|
|
192
|
+
import * as fs5 from "node:fs";
|
|
192
193
|
|
|
193
194
|
// ../../src/history.ts
|
|
194
195
|
import { createHash, randomUUID } from "node:crypto";
|
|
196
|
+
import * as fs4 from "node:fs";
|
|
197
|
+
import { dirname as dirname3, join as join4, resolve as resolve3, sep as sep2 } from "node:path";
|
|
198
|
+
import Database2 from "better-sqlite3";
|
|
199
|
+
|
|
200
|
+
// ../../src/history-retention.ts
|
|
195
201
|
import * as fs3 from "node:fs";
|
|
196
|
-
import { dirname as dirname2, join as join3, resolve as resolve2, sep } from "node:path";
|
|
202
|
+
import { basename, dirname as dirname2, join as join3, resolve as resolve2, sep } from "node:path";
|
|
197
203
|
import Database from "better-sqlite3";
|
|
204
|
+
var DB_NAME = "history.sqlite3";
|
|
205
|
+
var BLOB_SUBDIR = join3("blobs", "sha256");
|
|
206
|
+
var DELETE_BATCH = 256;
|
|
207
|
+
var VACUUM_PAGES_PER_PASS = 1024;
|
|
208
|
+
var MAX_SYNCHRONOUS_PASSES = 8;
|
|
209
|
+
var LEGACY_VACUUM_LIMIT_BYTES = 128 * 1024 * 1024;
|
|
210
|
+
var SQLITE_MUTATION_UPPER_BOUND = 1024 * 1024;
|
|
211
|
+
var FILE_READ_GRACE_MS = 5 * 60 * 1e3;
|
|
212
|
+
var RETRY_INITIAL_DELAY_MS = 50;
|
|
213
|
+
var RETRY_MAX_DELAY_MS = 5e3;
|
|
214
|
+
var settings = null;
|
|
215
|
+
var approximatePhysicalBytes = 0;
|
|
216
|
+
var running = false;
|
|
217
|
+
var scheduled = null;
|
|
218
|
+
var transientMaintenanceFailure = false;
|
|
219
|
+
var maintenanceRetryDelayMs = RETRY_INITIAL_DELAY_MS;
|
|
220
|
+
var compactionBlocked = /* @__PURE__ */ new Set();
|
|
221
|
+
var inFlightItems = /* @__PURE__ */ new Map();
|
|
222
|
+
var evictedMessages = 0;
|
|
223
|
+
var evictedFiles = 0;
|
|
224
|
+
var orphanedBlobsRemoved = 0;
|
|
225
|
+
var lastRunAtMs = null;
|
|
226
|
+
var cachedStatus = {
|
|
227
|
+
state: "disabled",
|
|
228
|
+
enabled: false,
|
|
229
|
+
max_bytes: 0,
|
|
230
|
+
physical_bytes: 0,
|
|
231
|
+
overflow_bytes: 0,
|
|
232
|
+
protected_logical_bytes: 0,
|
|
233
|
+
eligible_logical_bytes: 0,
|
|
234
|
+
protected_items: 0,
|
|
235
|
+
eligible_items: 0,
|
|
236
|
+
newest_item_retained: false,
|
|
237
|
+
maintenance_pending: false,
|
|
238
|
+
compaction_blocked_identities: [],
|
|
239
|
+
evicted_messages: 0,
|
|
240
|
+
evicted_files: 0,
|
|
241
|
+
orphaned_blobs_removed: 0,
|
|
242
|
+
last_run_at_ms: null
|
|
243
|
+
};
|
|
244
|
+
function isSafeCap(value) {
|
|
245
|
+
return Number.isSafeInteger(value) && value >= 0;
|
|
246
|
+
}
|
|
247
|
+
function withinStateRoot(path) {
|
|
248
|
+
if (!settings) return false;
|
|
249
|
+
const root = settings.stateDir;
|
|
250
|
+
const candidate = resolve2(path);
|
|
251
|
+
return candidate === root || candidate.startsWith(`${root}${sep}`);
|
|
252
|
+
}
|
|
253
|
+
function physicalSize(path, seen) {
|
|
254
|
+
let st;
|
|
255
|
+
try {
|
|
256
|
+
st = fs3.lstatSync(path);
|
|
257
|
+
} catch {
|
|
258
|
+
return 0;
|
|
259
|
+
}
|
|
260
|
+
if (!st.isFile()) return 0;
|
|
261
|
+
const inode = `${String(st.dev)}:${String(st.ino)}`;
|
|
262
|
+
if (st.ino && seen.has(inode)) return 0;
|
|
263
|
+
if (st.ino) seen.add(inode);
|
|
264
|
+
return typeof st.blocks === "number" ? st.blocks * 512 : st.size;
|
|
265
|
+
}
|
|
266
|
+
function walkRegularFiles(root, visit) {
|
|
267
|
+
let entries;
|
|
268
|
+
try {
|
|
269
|
+
entries = fs3.readdirSync(root, { withFileTypes: true });
|
|
270
|
+
} catch {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
for (const entry of entries) {
|
|
274
|
+
const path = join3(root, entry.name);
|
|
275
|
+
if (entry.isDirectory()) walkRegularFiles(path, visit);
|
|
276
|
+
else if (entry.isFile()) visit(path);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
function databasePaths(stateDir) {
|
|
280
|
+
let entries;
|
|
281
|
+
try {
|
|
282
|
+
entries = fs3.readdirSync(stateDir, { withFileTypes: true });
|
|
283
|
+
} catch {
|
|
284
|
+
return [];
|
|
285
|
+
}
|
|
286
|
+
return entries.filter((entry) => entry.isDirectory()).map((entry) => join3(stateDir, entry.name, DB_NAME)).filter((path) => fs3.existsSync(path)).sort();
|
|
287
|
+
}
|
|
288
|
+
function measureHistoryStorage(stateDir = settings?.stateDir) {
|
|
289
|
+
if (!stateDir) return 0;
|
|
290
|
+
const seen = /* @__PURE__ */ new Set();
|
|
291
|
+
let total = 0;
|
|
292
|
+
for (const dbPath of databasePaths(stateDir)) {
|
|
293
|
+
for (const suffix of ["", "-wal", "-shm"]) total += physicalSize(`${dbPath}${suffix}`, seen);
|
|
294
|
+
walkRegularFiles(join3(dirname2(dbPath), BLOB_SUBDIR), (path) => {
|
|
295
|
+
total += physicalSize(path, seen);
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
return total;
|
|
299
|
+
}
|
|
300
|
+
function openMaintenanceDatabase(path) {
|
|
301
|
+
const db = new Database(path);
|
|
302
|
+
db.pragma("busy_timeout = 50");
|
|
303
|
+
db.pragma("foreign_keys = ON");
|
|
304
|
+
return db;
|
|
305
|
+
}
|
|
306
|
+
function isRetryableSqliteError(error) {
|
|
307
|
+
const code = error && typeof error === "object" && "code" in error ? String(error.code ?? "") : "";
|
|
308
|
+
if (code === "SQLITE_BUSY" || code.startsWith("SQLITE_BUSY_") || code === "SQLITE_LOCKED" || code.startsWith("SQLITE_LOCKED_")) return true;
|
|
309
|
+
return /(?:database|table) is locked|database is busy/i.test(String(error));
|
|
310
|
+
}
|
|
311
|
+
function recordMaintenanceError(error, identityName) {
|
|
312
|
+
if (isRetryableSqliteError(error)) transientMaintenanceFailure = true;
|
|
313
|
+
else compactionBlocked.add(identityName);
|
|
314
|
+
}
|
|
315
|
+
function checkpointDatabase(db) {
|
|
316
|
+
const rows = db.pragma("wal_checkpoint(TRUNCATE)");
|
|
317
|
+
if (Number(rows[0]?.busy ?? 0) === 0) return true;
|
|
318
|
+
transientMaintenanceFailure = true;
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
function prepareHistoryDatabase(path) {
|
|
322
|
+
if (!fs3.existsSync(path)) return;
|
|
323
|
+
const identityName = basename(dirname2(path));
|
|
324
|
+
let db = null;
|
|
325
|
+
try {
|
|
326
|
+
db = openMaintenanceDatabase(path);
|
|
327
|
+
const mode = Number(db.pragma("auto_vacuum", { simple: true }));
|
|
328
|
+
if (mode === 2) {
|
|
329
|
+
compactionBlocked.delete(identityName);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
if (mode === 1) {
|
|
333
|
+
db.pragma("auto_vacuum = INCREMENTAL");
|
|
334
|
+
compactionBlocked.delete(identityName);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
const bytes = physicalSize(path, /* @__PURE__ */ new Set());
|
|
338
|
+
if (bytes > LEGACY_VACUUM_LIMIT_BYTES) {
|
|
339
|
+
compactionBlocked.add(identityName);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
if (!checkpointDatabase(db)) return;
|
|
343
|
+
db.pragma("journal_mode = DELETE");
|
|
344
|
+
db.pragma("auto_vacuum = INCREMENTAL");
|
|
345
|
+
db.exec("VACUUM");
|
|
346
|
+
db.pragma("journal_mode = WAL");
|
|
347
|
+
compactionBlocked.delete(identityName);
|
|
348
|
+
} catch (error) {
|
|
349
|
+
recordMaintenanceError(error, identityName);
|
|
350
|
+
} finally {
|
|
351
|
+
try {
|
|
352
|
+
db?.close();
|
|
353
|
+
} catch {
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
function newestItem(dbPath) {
|
|
358
|
+
let db = null;
|
|
359
|
+
try {
|
|
360
|
+
db = openMaintenanceDatabase(dbPath);
|
|
361
|
+
const row = db.prepare(`
|
|
362
|
+
SELECT kind, seq, occurred_at_ms, inserted_at_ms, logical_bytes, eligible FROM (
|
|
363
|
+
SELECT 'message' AS kind, seq, occurred_at_ms, inserted_at_ms,
|
|
364
|
+
length(CAST(body AS BLOB)) + 512 AS logical_bytes,
|
|
365
|
+
CASE WHEN inbox_state = 'read' AND direction = 'in' THEN 1
|
|
366
|
+
WHEN direction = 'out' AND delivery_state IN ('delivered','read') THEN 1 ELSE 0 END AS eligible
|
|
367
|
+
FROM messages
|
|
368
|
+
UNION ALL
|
|
369
|
+
SELECT 'file' AS kind, seq, occurred_at_ms, inserted_at_ms, byte_length + 512 AS logical_bytes,
|
|
370
|
+
CASE WHEN direction = 'in' AND inbox_state = 'read' AND human_read_at_ms <= ? THEN 1
|
|
371
|
+
WHEN direction = 'out' AND delivery_state IN ('delivered','read') THEN 1 ELSE 0 END AS eligible
|
|
372
|
+
FROM files
|
|
373
|
+
) ORDER BY inserted_at_ms DESC, occurred_at_ms DESC, kind DESC, seq DESC LIMIT 1
|
|
374
|
+
`).get(Date.now() - FILE_READ_GRACE_MS);
|
|
375
|
+
return row ? {
|
|
376
|
+
identityDir: dirname2(dbPath),
|
|
377
|
+
kind: row.kind,
|
|
378
|
+
seq: row.seq,
|
|
379
|
+
occurredAtMs: row.occurred_at_ms,
|
|
380
|
+
insertedAtMs: row.inserted_at_ms,
|
|
381
|
+
logicalBytes: Number(row.logical_bytes),
|
|
382
|
+
eligible: row.eligible === 1
|
|
383
|
+
} : null;
|
|
384
|
+
} catch (error) {
|
|
385
|
+
recordMaintenanceError(error, basename(dirname2(dbPath)));
|
|
386
|
+
return null;
|
|
387
|
+
} finally {
|
|
388
|
+
try {
|
|
389
|
+
db?.close();
|
|
390
|
+
} catch {
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
function newer(a, b) {
|
|
395
|
+
if (a.insertedAtMs !== b.insertedAtMs) return a.insertedAtMs > b.insertedAtMs;
|
|
396
|
+
if (a.occurredAtMs !== b.occurredAtMs) return a.occurredAtMs > b.occurredAtMs;
|
|
397
|
+
if (a.identityDir !== b.identityDir) return a.identityDir > b.identityDir;
|
|
398
|
+
if (a.kind !== b.kind) return a.kind > b.kind;
|
|
399
|
+
return a.seq > b.seq;
|
|
400
|
+
}
|
|
401
|
+
function globalNewest(paths) {
|
|
402
|
+
let newest = null;
|
|
403
|
+
for (const path of paths) {
|
|
404
|
+
const item = newestItem(path);
|
|
405
|
+
if (item && (!newest || newer(item, newest))) newest = item;
|
|
406
|
+
}
|
|
407
|
+
return newest;
|
|
408
|
+
}
|
|
409
|
+
function isSameItem(a, b) {
|
|
410
|
+
return !!b && a.identityDir === b.identityDir && a.kind === b.kind && a.seq === b.seq;
|
|
411
|
+
}
|
|
412
|
+
function inFlightKey(identityDir2, kind, wireId) {
|
|
413
|
+
return `${resolve2(identityDir2)}\0${kind}\0${wireId}`;
|
|
414
|
+
}
|
|
415
|
+
function candidates(paths, newest) {
|
|
416
|
+
const result = [];
|
|
417
|
+
for (const dbPath of paths) {
|
|
418
|
+
let db = null;
|
|
419
|
+
try {
|
|
420
|
+
db = openMaintenanceDatabase(dbPath);
|
|
421
|
+
const rows = db.prepare(`
|
|
422
|
+
SELECT kind, seq, wire_id, occurred_at_ms, inserted_at_ms, logical_bytes, blob_relpath FROM (
|
|
423
|
+
SELECT 'message' AS kind, seq, wire_id, occurred_at_ms, inserted_at_ms,
|
|
424
|
+
length(CAST(body AS BLOB)) + 512 AS logical_bytes, NULL AS blob_relpath
|
|
425
|
+
FROM messages WHERE (direction = 'in' AND inbox_state = 'read')
|
|
426
|
+
OR (direction = 'out' AND delivery_state IN ('delivered','read'))
|
|
427
|
+
UNION ALL
|
|
428
|
+
SELECT 'file' AS kind, seq, wire_id, occurred_at_ms, inserted_at_ms,
|
|
429
|
+
byte_length + 512 AS logical_bytes, blob_relpath
|
|
430
|
+
FROM files WHERE (direction = 'in' AND inbox_state = 'read' AND human_read_at_ms <= ?)
|
|
431
|
+
OR (direction = 'out' AND delivery_state IN ('delivered','read'))
|
|
432
|
+
) ORDER BY occurred_at_ms ASC, inserted_at_ms ASC, kind ASC, seq ASC LIMIT ?
|
|
433
|
+
`).all(Date.now() - FILE_READ_GRACE_MS, DELETE_BATCH + 1);
|
|
434
|
+
for (const row of rows) {
|
|
435
|
+
const item = {
|
|
436
|
+
dbPath,
|
|
437
|
+
identityDir: dirname2(dbPath),
|
|
438
|
+
kind: row.kind,
|
|
439
|
+
seq: row.seq,
|
|
440
|
+
wireId: row.wire_id,
|
|
441
|
+
occurredAtMs: row.occurred_at_ms,
|
|
442
|
+
insertedAtMs: row.inserted_at_ms,
|
|
443
|
+
logicalBytes: Number(row.logical_bytes),
|
|
444
|
+
eligible: true,
|
|
445
|
+
blobRelpath: row.blob_relpath
|
|
446
|
+
};
|
|
447
|
+
if (!isSameItem(item, newest) && !inFlightItems.has(inFlightKey(item.identityDir, item.kind, item.wireId))) {
|
|
448
|
+
result.push(item);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
} catch (error) {
|
|
452
|
+
recordMaintenanceError(error, basename(dirname2(dbPath)));
|
|
453
|
+
} finally {
|
|
454
|
+
try {
|
|
455
|
+
db?.close();
|
|
456
|
+
} catch {
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
result.sort((a, b) => a.occurredAtMs - b.occurredAtMs || a.insertedAtMs - b.insertedAtMs || a.identityDir.localeCompare(b.identityDir) || a.kind.localeCompare(b.kind) || a.seq - b.seq);
|
|
461
|
+
return result.slice(0, DELETE_BATCH);
|
|
462
|
+
}
|
|
463
|
+
function safeBlobPath(identityDir2, relpath) {
|
|
464
|
+
const root = resolve2(join3(identityDir2, BLOB_SUBDIR));
|
|
465
|
+
const path = resolve2(identityDir2, relpath);
|
|
466
|
+
return path.startsWith(`${root}${sep}`) ? path : null;
|
|
467
|
+
}
|
|
468
|
+
function pruneEmptyBlobDirs(identityDir2, from) {
|
|
469
|
+
const stop = resolve2(join3(identityDir2, "blobs"));
|
|
470
|
+
let current = resolve2(dirname2(from));
|
|
471
|
+
while (current.startsWith(`${stop}${sep}`)) {
|
|
472
|
+
try {
|
|
473
|
+
fs3.rmdirSync(current);
|
|
474
|
+
} catch {
|
|
475
|
+
break;
|
|
476
|
+
}
|
|
477
|
+
current = dirname2(current);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
function unlinkBlobIfOrphaned(db, identityDir2, relpath) {
|
|
481
|
+
if (db.prepare("SELECT 1 FROM files WHERE blob_relpath = ? LIMIT 1").get(relpath)) return false;
|
|
482
|
+
const path = safeBlobPath(identityDir2, relpath);
|
|
483
|
+
if (!path) return false;
|
|
484
|
+
try {
|
|
485
|
+
const st = fs3.lstatSync(path);
|
|
486
|
+
if (!st.isFile()) return false;
|
|
487
|
+
fs3.unlinkSync(path);
|
|
488
|
+
pruneEmptyBlobDirs(identityDir2, path);
|
|
489
|
+
return true;
|
|
490
|
+
} catch {
|
|
491
|
+
return false;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
function deleteCandidates(items, targetBytes) {
|
|
495
|
+
const selected = [];
|
|
496
|
+
let logical = 0;
|
|
497
|
+
for (const item of items) {
|
|
498
|
+
selected.push(item);
|
|
499
|
+
logical += Math.max(1, item.logicalBytes);
|
|
500
|
+
if (logical >= targetBytes) break;
|
|
501
|
+
}
|
|
502
|
+
const byDatabase = /* @__PURE__ */ new Map();
|
|
503
|
+
for (const item of selected) {
|
|
504
|
+
const rows = byDatabase.get(item.dbPath) ?? [];
|
|
505
|
+
rows.push(item);
|
|
506
|
+
byDatabase.set(item.dbPath, rows);
|
|
507
|
+
}
|
|
508
|
+
let deleted = 0;
|
|
509
|
+
for (const [dbPath, rows] of byDatabase) {
|
|
510
|
+
let db = null;
|
|
511
|
+
try {
|
|
512
|
+
db = openMaintenanceDatabase(dbPath);
|
|
513
|
+
const removedBlobs = [];
|
|
514
|
+
db.transaction(() => {
|
|
515
|
+
const delMessage = db.prepare(`
|
|
516
|
+
DELETE FROM messages WHERE seq = ? AND inbox_state = 'read'
|
|
517
|
+
AND (direction = 'in' OR delivery_state IN ('delivered','read'))
|
|
518
|
+
`);
|
|
519
|
+
const delFile = db.prepare(`
|
|
520
|
+
DELETE FROM files WHERE seq = ? AND (
|
|
521
|
+
(direction = 'in' AND inbox_state = 'read' AND human_read_at_ms <= ?)
|
|
522
|
+
OR (direction = 'out' AND delivery_state IN ('delivered','read'))
|
|
523
|
+
)
|
|
524
|
+
`);
|
|
525
|
+
for (const row of rows) {
|
|
526
|
+
const changes = row.kind === "message" ? delMessage.run(row.seq).changes : delFile.run(row.seq, Date.now() - FILE_READ_GRACE_MS).changes;
|
|
527
|
+
if (changes !== 1) continue;
|
|
528
|
+
deleted += 1;
|
|
529
|
+
if (row.kind === "message") evictedMessages += 1;
|
|
530
|
+
else {
|
|
531
|
+
evictedFiles += 1;
|
|
532
|
+
if (row.blobRelpath) removedBlobs.push(row.blobRelpath);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
})();
|
|
536
|
+
for (const relpath of new Set(removedBlobs)) {
|
|
537
|
+
if (unlinkBlobIfOrphaned(db, dirname2(dbPath), relpath)) orphanedBlobsRemoved += 1;
|
|
538
|
+
}
|
|
539
|
+
checkpointDatabase(db);
|
|
540
|
+
if (Number(db.pragma("auto_vacuum", { simple: true })) === 2) {
|
|
541
|
+
db.pragma(`incremental_vacuum(${VACUUM_PAGES_PER_PASS})`);
|
|
542
|
+
checkpointDatabase(db);
|
|
543
|
+
compactionBlocked.delete(basename(dirname2(dbPath)));
|
|
544
|
+
} else {
|
|
545
|
+
compactionBlocked.add(basename(dirname2(dbPath)));
|
|
546
|
+
}
|
|
547
|
+
} catch (error) {
|
|
548
|
+
recordMaintenanceError(error, basename(dirname2(dbPath)));
|
|
549
|
+
} finally {
|
|
550
|
+
try {
|
|
551
|
+
db?.close();
|
|
552
|
+
} catch {
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
return deleted;
|
|
557
|
+
}
|
|
558
|
+
function recoverOrphanedBlobs(dbPath) {
|
|
559
|
+
const identityDir2 = dirname2(dbPath);
|
|
560
|
+
let db = null;
|
|
561
|
+
let removed = 0;
|
|
562
|
+
try {
|
|
563
|
+
db = openMaintenanceDatabase(dbPath);
|
|
564
|
+
const referenced = new Set(
|
|
565
|
+
db.prepare("SELECT DISTINCT blob_relpath FROM files").pluck().all().map((p) => resolve2(identityDir2, p))
|
|
566
|
+
);
|
|
567
|
+
const root = join3(identityDir2, BLOB_SUBDIR);
|
|
568
|
+
const paths = [];
|
|
569
|
+
walkRegularFiles(root, (path) => paths.push(path));
|
|
570
|
+
for (const path of paths) {
|
|
571
|
+
if (referenced.has(resolve2(path))) continue;
|
|
572
|
+
try {
|
|
573
|
+
fs3.unlinkSync(path);
|
|
574
|
+
removed += 1;
|
|
575
|
+
pruneEmptyBlobDirs(identityDir2, path);
|
|
576
|
+
} catch {
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
} catch (error) {
|
|
580
|
+
recordMaintenanceError(error, basename(identityDir2));
|
|
581
|
+
} finally {
|
|
582
|
+
try {
|
|
583
|
+
db?.close();
|
|
584
|
+
} catch {
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
return removed;
|
|
588
|
+
}
|
|
589
|
+
function logicalStats(paths, newest) {
|
|
590
|
+
const totals = {
|
|
591
|
+
protectedBytes: 0,
|
|
592
|
+
eligibleBytes: 0,
|
|
593
|
+
protectedItems: 0,
|
|
594
|
+
eligibleItems: 0,
|
|
595
|
+
newestRetained: false
|
|
596
|
+
};
|
|
597
|
+
for (const dbPath of paths) {
|
|
598
|
+
let db = null;
|
|
599
|
+
try {
|
|
600
|
+
db = openMaintenanceDatabase(dbPath);
|
|
601
|
+
const row = db.prepare(`
|
|
602
|
+
SELECT
|
|
603
|
+
COALESCE(SUM(CASE WHEN eligible = 1 THEN logical_bytes ELSE 0 END), 0) AS eligible_bytes,
|
|
604
|
+
COALESCE(SUM(CASE WHEN eligible = 0 THEN logical_bytes ELSE 0 END), 0) AS protected_bytes,
|
|
605
|
+
COALESCE(SUM(eligible), 0) AS eligible_items,
|
|
606
|
+
COALESCE(SUM(CASE WHEN eligible = 0 THEN 1 ELSE 0 END), 0) AS protected_items
|
|
607
|
+
FROM (
|
|
608
|
+
SELECT length(CAST(body AS BLOB)) + 512 AS logical_bytes,
|
|
609
|
+
CASE WHEN direction = 'in' AND inbox_state = 'read' THEN 1
|
|
610
|
+
WHEN direction = 'out' AND delivery_state IN ('delivered','read') THEN 1 ELSE 0 END AS eligible
|
|
611
|
+
FROM messages
|
|
612
|
+
UNION ALL
|
|
613
|
+
SELECT byte_length + 512 AS logical_bytes,
|
|
614
|
+
CASE WHEN direction = 'in' AND inbox_state = 'read' AND human_read_at_ms <= ? THEN 1
|
|
615
|
+
WHEN direction = 'out' AND delivery_state IN ('delivered','read') THEN 1 ELSE 0 END AS eligible
|
|
616
|
+
FROM files
|
|
617
|
+
UNION ALL
|
|
618
|
+
SELECT length(CAST(body AS BLOB)) + 512, 0 FROM monitoring_items
|
|
619
|
+
UNION ALL
|
|
620
|
+
SELECT length(CAST(payload AS BLOB)) + 512, 0 FROM control_requests
|
|
621
|
+
)
|
|
622
|
+
`).get(Date.now() - FILE_READ_GRACE_MS);
|
|
623
|
+
totals.eligibleBytes += Number(row.eligible_bytes);
|
|
624
|
+
totals.protectedBytes += Number(row.protected_bytes);
|
|
625
|
+
totals.eligibleItems += Number(row.eligible_items);
|
|
626
|
+
totals.protectedItems += Number(row.protected_items);
|
|
627
|
+
} catch (error) {
|
|
628
|
+
recordMaintenanceError(error, basename(dirname2(dbPath)));
|
|
629
|
+
} finally {
|
|
630
|
+
try {
|
|
631
|
+
db?.close();
|
|
632
|
+
} catch {
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
if (newest?.eligible) {
|
|
637
|
+
totals.newestRetained = true;
|
|
638
|
+
totals.eligibleItems = Math.max(0, totals.eligibleItems - 1);
|
|
639
|
+
totals.protectedItems += 1;
|
|
640
|
+
totals.eligibleBytes = Math.max(0, totals.eligibleBytes - newest.logicalBytes);
|
|
641
|
+
totals.protectedBytes += newest.logicalBytes;
|
|
642
|
+
}
|
|
643
|
+
for (const item of inFlightItems.values()) {
|
|
644
|
+
let db = null;
|
|
645
|
+
try {
|
|
646
|
+
db = openMaintenanceDatabase(join3(item.identityDir, DB_NAME));
|
|
647
|
+
const table = item.kind === "file" ? "files" : "messages";
|
|
648
|
+
const sizeExpr = item.kind === "file" ? "byte_length + 512" : "length(CAST(body AS BLOB)) + 512";
|
|
649
|
+
const eligibility = item.kind === "file" ? `((direction = 'in' AND inbox_state = 'read' AND human_read_at_ms <= ?)
|
|
650
|
+
OR (direction = 'out' AND delivery_state IN ('delivered','read')))` : `((direction = 'in' AND inbox_state = 'read')
|
|
651
|
+
OR (direction = 'out' AND delivery_state IN ('delivered','read')))`;
|
|
652
|
+
const row = db.prepare(`
|
|
653
|
+
SELECT seq, ${sizeExpr} AS logical_bytes FROM ${table}
|
|
654
|
+
WHERE wire_id = ? AND ${eligibility}
|
|
655
|
+
`).get(...item.kind === "file" ? [item.wireId, Date.now() - FILE_READ_GRACE_MS] : [item.wireId]);
|
|
656
|
+
if (!row) continue;
|
|
657
|
+
if (isSameItem({ identityDir: item.identityDir, kind: item.kind, seq: row.seq }, newest)) continue;
|
|
658
|
+
const bytes = Number(row.logical_bytes);
|
|
659
|
+
totals.eligibleItems = Math.max(0, totals.eligibleItems - 1);
|
|
660
|
+
totals.protectedItems += 1;
|
|
661
|
+
totals.eligibleBytes = Math.max(0, totals.eligibleBytes - bytes);
|
|
662
|
+
totals.protectedBytes += bytes;
|
|
663
|
+
} catch (error) {
|
|
664
|
+
recordMaintenanceError(error, basename(item.identityDir));
|
|
665
|
+
} finally {
|
|
666
|
+
try {
|
|
667
|
+
db?.close();
|
|
668
|
+
} catch {
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
return totals;
|
|
673
|
+
}
|
|
674
|
+
function hasReclaimablePages(paths) {
|
|
675
|
+
let reclaimable = false;
|
|
676
|
+
for (const dbPath of paths) {
|
|
677
|
+
const identityName = basename(dirname2(dbPath));
|
|
678
|
+
let db = null;
|
|
679
|
+
try {
|
|
680
|
+
db = openMaintenanceDatabase(dbPath);
|
|
681
|
+
if (Number(db.pragma("auto_vacuum", { simple: true })) === 2) {
|
|
682
|
+
if (Number(db.pragma("freelist_count", { simple: true })) > 0) reclaimable = true;
|
|
683
|
+
} else {
|
|
684
|
+
compactionBlocked.add(identityName);
|
|
685
|
+
}
|
|
686
|
+
} catch (error) {
|
|
687
|
+
recordMaintenanceError(error, identityName);
|
|
688
|
+
} finally {
|
|
689
|
+
try {
|
|
690
|
+
db?.close();
|
|
691
|
+
} catch {
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
return reclaimable;
|
|
696
|
+
}
|
|
697
|
+
function compactDatabases(paths) {
|
|
698
|
+
for (const dbPath of paths) {
|
|
699
|
+
const identityName = basename(dirname2(dbPath));
|
|
700
|
+
let db = null;
|
|
701
|
+
try {
|
|
702
|
+
db = openMaintenanceDatabase(dbPath);
|
|
703
|
+
const firstCheckpoint = checkpointDatabase(db);
|
|
704
|
+
if (Number(db.pragma("auto_vacuum", { simple: true })) === 2) {
|
|
705
|
+
db.pragma(`incremental_vacuum(${VACUUM_PAGES_PER_PASS})`);
|
|
706
|
+
const secondCheckpoint = checkpointDatabase(db);
|
|
707
|
+
if (firstCheckpoint && secondCheckpoint) compactionBlocked.delete(identityName);
|
|
708
|
+
} else {
|
|
709
|
+
compactionBlocked.add(identityName);
|
|
710
|
+
}
|
|
711
|
+
} catch (error) {
|
|
712
|
+
recordMaintenanceError(error, identityName);
|
|
713
|
+
} finally {
|
|
714
|
+
try {
|
|
715
|
+
db?.close();
|
|
716
|
+
} catch {
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
function classify(physical, logical, reclaimable) {
|
|
722
|
+
const maxBytes = settings?.maxBytes ?? 0;
|
|
723
|
+
const enabled = maxBytes > 0;
|
|
724
|
+
const overflow = enabled ? Math.max(0, physical - maxBytes) : 0;
|
|
725
|
+
const blocked = [...compactionBlocked].sort();
|
|
726
|
+
let state;
|
|
727
|
+
if (!enabled) state = "disabled";
|
|
728
|
+
else if (overflow === 0) state = "within_cap";
|
|
729
|
+
else if (transientMaintenanceFailure || logical.eligibleItems > 0 || reclaimable) state = "maintenance_pending";
|
|
730
|
+
else if (blocked.length > 0) state = "compaction_blocked";
|
|
731
|
+
else state = "protected_overflow";
|
|
732
|
+
return {
|
|
733
|
+
state,
|
|
734
|
+
enabled,
|
|
735
|
+
max_bytes: maxBytes,
|
|
736
|
+
physical_bytes: physical,
|
|
737
|
+
overflow_bytes: overflow,
|
|
738
|
+
protected_logical_bytes: logical.protectedBytes,
|
|
739
|
+
eligible_logical_bytes: logical.eligibleBytes,
|
|
740
|
+
protected_items: logical.protectedItems,
|
|
741
|
+
eligible_items: logical.eligibleItems,
|
|
742
|
+
newest_item_retained: logical.newestRetained,
|
|
743
|
+
maintenance_pending: state === "maintenance_pending",
|
|
744
|
+
compaction_blocked_identities: blocked,
|
|
745
|
+
evicted_messages: evictedMessages,
|
|
746
|
+
evicted_files: evictedFiles,
|
|
747
|
+
orphaned_blobs_removed: orphanedBlobsRemoved,
|
|
748
|
+
last_run_at_ms: lastRunAtMs
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
function pruneMissingBlockedIdentities(paths) {
|
|
752
|
+
const present = new Set(paths.map((path) => basename(dirname2(path))));
|
|
753
|
+
for (const name of compactionBlocked) if (!present.has(name)) compactionBlocked.delete(name);
|
|
754
|
+
}
|
|
755
|
+
function refreshStatus() {
|
|
756
|
+
const paths = settings ? databasePaths(settings.stateDir) : [];
|
|
757
|
+
pruneMissingBlockedIdentities(paths);
|
|
758
|
+
const physical = settings ? measureHistoryStorage(settings.stateDir) : 0;
|
|
759
|
+
approximatePhysicalBytes = physical;
|
|
760
|
+
cachedStatus = classify(physical, logicalStats(paths, globalNewest(paths)), hasReclaimablePages(paths));
|
|
761
|
+
return cachedStatus;
|
|
762
|
+
}
|
|
763
|
+
function scheduleMaintenance() {
|
|
764
|
+
if (scheduled || !settings || settings.maxBytes === 0) return;
|
|
765
|
+
const delayMs = transientMaintenanceFailure ? maintenanceRetryDelayMs : 0;
|
|
766
|
+
maintenanceRetryDelayMs = transientMaintenanceFailure ? Math.min(RETRY_MAX_DELAY_MS, maintenanceRetryDelayMs * 2) : RETRY_INITIAL_DELAY_MS;
|
|
767
|
+
scheduled = setTimeout(() => {
|
|
768
|
+
scheduled = null;
|
|
769
|
+
enforceHistoryStorageLimit(1);
|
|
770
|
+
}, delayMs);
|
|
771
|
+
scheduled.unref?.();
|
|
772
|
+
}
|
|
773
|
+
function initializeHistoryRetention(stateDir, maxBytes) {
|
|
774
|
+
if (!isSafeCap(maxBytes)) throw new Error("historyMaxBytes must be a non-negative safe integer");
|
|
775
|
+
if (scheduled) {
|
|
776
|
+
clearTimeout(scheduled);
|
|
777
|
+
scheduled = null;
|
|
778
|
+
}
|
|
779
|
+
settings = { stateDir: resolve2(stateDir), maxBytes };
|
|
780
|
+
approximatePhysicalBytes = 0;
|
|
781
|
+
running = false;
|
|
782
|
+
transientMaintenanceFailure = false;
|
|
783
|
+
maintenanceRetryDelayMs = RETRY_INITIAL_DELAY_MS;
|
|
784
|
+
compactionBlocked.clear();
|
|
785
|
+
inFlightItems.clear();
|
|
786
|
+
evictedMessages = 0;
|
|
787
|
+
evictedFiles = 0;
|
|
788
|
+
orphanedBlobsRemoved = 0;
|
|
789
|
+
lastRunAtMs = null;
|
|
790
|
+
for (const path of databasePaths(settings.stateDir)) {
|
|
791
|
+
prepareHistoryDatabase(path);
|
|
792
|
+
orphanedBlobsRemoved += recoverOrphanedBlobs(path);
|
|
793
|
+
}
|
|
794
|
+
return enforceHistoryStorageLimit(2);
|
|
795
|
+
}
|
|
796
|
+
function noteHistoryStorageMutation(identityDir2, payloadBytes = 0) {
|
|
797
|
+
if (!settings || settings.maxBytes === 0 || !withinStateRoot(identityDir2)) return;
|
|
798
|
+
approximatePhysicalBytes += Math.max(0, payloadBytes) + SQLITE_MUTATION_UPPER_BOUND;
|
|
799
|
+
if (approximatePhysicalBytes >= settings.maxBytes) {
|
|
800
|
+
const status = enforceHistoryStorageLimit(MAX_SYNCHRONOUS_PASSES);
|
|
801
|
+
if (status.maintenance_pending) scheduleMaintenance();
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
function enforceHistoryStorageLimit(maxPasses = MAX_SYNCHRONOUS_PASSES) {
|
|
805
|
+
if (!settings) return cachedStatus;
|
|
806
|
+
if (running) {
|
|
807
|
+
scheduleMaintenance();
|
|
808
|
+
return cachedStatus;
|
|
809
|
+
}
|
|
810
|
+
running = true;
|
|
811
|
+
transientMaintenanceFailure = false;
|
|
812
|
+
try {
|
|
813
|
+
for (let pass = 0; pass < Math.max(1, maxPasses); pass += 1) {
|
|
814
|
+
let physical = measureHistoryStorage(settings.stateDir);
|
|
815
|
+
if (settings.maxBytes === 0 || physical <= settings.maxBytes) break;
|
|
816
|
+
const paths = databasePaths(settings.stateDir);
|
|
817
|
+
compactDatabases(paths);
|
|
818
|
+
const compactedPhysical = measureHistoryStorage(settings.stateDir);
|
|
819
|
+
if (compactedPhysical <= settings.maxBytes) break;
|
|
820
|
+
const compacted = compactedPhysical < physical;
|
|
821
|
+
physical = compactedPhysical;
|
|
822
|
+
const newest = globalNewest(paths);
|
|
823
|
+
const available = candidates(paths, newest);
|
|
824
|
+
if (available.length === 0) {
|
|
825
|
+
if (!compacted) break;
|
|
826
|
+
continue;
|
|
827
|
+
}
|
|
828
|
+
const removed = deleteCandidates(available, physical - settings.maxBytes);
|
|
829
|
+
if (removed === 0) break;
|
|
830
|
+
}
|
|
831
|
+
lastRunAtMs = Date.now();
|
|
832
|
+
const status = refreshStatus();
|
|
833
|
+
if (status.maintenance_pending) scheduleMaintenance();
|
|
834
|
+
return status;
|
|
835
|
+
} finally {
|
|
836
|
+
running = false;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
function getHistoryStorageStatus() {
|
|
840
|
+
if (!settings) return { ...cachedStatus, compaction_blocked_identities: [...cachedStatus.compaction_blocked_identities] };
|
|
841
|
+
const status = refreshStatus();
|
|
842
|
+
return { ...status, compaction_blocked_identities: [...status.compaction_blocked_identities] };
|
|
843
|
+
}
|
|
844
|
+
function stopHistoryRetention() {
|
|
845
|
+
if (scheduled) {
|
|
846
|
+
clearTimeout(scheduled);
|
|
847
|
+
scheduled = null;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
function pinHistoryItems(identityDir2, kind, wireIds) {
|
|
851
|
+
for (const wireId of wireIds) {
|
|
852
|
+
inFlightItems.set(inFlightKey(identityDir2, kind, wireId), { identityDir: identityDir2, kind, wireId });
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
function unpinHistoryItems(identityDir2, kind, wireIds) {
|
|
856
|
+
for (const wireId of wireIds) inFlightItems.delete(inFlightKey(identityDir2, kind, wireId));
|
|
857
|
+
}
|
|
858
|
+
function cleanupHistoryBlob(identityDir2, relpath) {
|
|
859
|
+
const dbPath = join3(identityDir2, DB_NAME);
|
|
860
|
+
if (!fs3.existsSync(dbPath)) return false;
|
|
861
|
+
let db = null;
|
|
862
|
+
try {
|
|
863
|
+
db = openMaintenanceDatabase(dbPath);
|
|
864
|
+
const removed = unlinkBlobIfOrphaned(db, identityDir2, relpath);
|
|
865
|
+
if (removed) orphanedBlobsRemoved += 1;
|
|
866
|
+
return removed;
|
|
867
|
+
} finally {
|
|
868
|
+
try {
|
|
869
|
+
db?.close();
|
|
870
|
+
} catch {
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// ../../src/history.ts
|
|
198
876
|
var MAX_BATCH = 200;
|
|
199
877
|
var DEFAULT_BATCH = 50;
|
|
200
878
|
var MAX_TEXT_BYTES = 2 * 1024 * 1024;
|
|
201
879
|
var MAX_FILE_BYTES = 8 * 1024 * 1024;
|
|
202
|
-
var
|
|
880
|
+
var DB_NAME2 = "history.sqlite3";
|
|
203
881
|
var SCHEMA_VERSION = 1;
|
|
204
882
|
var stores = /* @__PURE__ */ new Map();
|
|
205
883
|
var health = /* @__PURE__ */ new Map();
|
|
206
884
|
var outboundFailures = /* @__PURE__ */ new Map();
|
|
207
|
-
var historyPath = (id) =>
|
|
885
|
+
var historyPath = (id) => join4(id.dir, DB_NAME2);
|
|
208
886
|
function initialHealth() {
|
|
209
887
|
return {
|
|
210
888
|
write_failures: 0,
|
|
@@ -328,12 +1006,15 @@ function schema(db) {
|
|
|
328
1006
|
function openHistory(id) {
|
|
329
1007
|
const existing = stores.get(id.dir);
|
|
330
1008
|
if (existing) return existing;
|
|
331
|
-
|
|
332
|
-
|
|
1009
|
+
fs4.mkdirSync(id.dir, { recursive: true, mode: 448 });
|
|
1010
|
+
fs4.chmodSync(id.dir, 448);
|
|
333
1011
|
const path = historyPath(id);
|
|
334
|
-
const
|
|
1012
|
+
const isNew = !fs4.existsSync(path) || fs4.statSync(path).size === 0;
|
|
1013
|
+
if (!isNew) prepareHistoryDatabase(path);
|
|
1014
|
+
const db = new Database2(path);
|
|
335
1015
|
try {
|
|
336
|
-
|
|
1016
|
+
fs4.chmodSync(path, 384);
|
|
1017
|
+
if (isNew) db.pragma("auto_vacuum = INCREMENTAL");
|
|
337
1018
|
db.pragma("journal_mode = WAL");
|
|
338
1019
|
db.pragma("synchronous = NORMAL");
|
|
339
1020
|
db.pragma("foreign_keys = ON");
|
|
@@ -341,7 +1022,7 @@ function openHistory(id) {
|
|
|
341
1022
|
schema(db);
|
|
342
1023
|
for (const suffix of ["", "-wal", "-shm"]) {
|
|
343
1024
|
const candidate = `${path}${suffix}`;
|
|
344
|
-
if (
|
|
1025
|
+
if (fs4.existsSync(candidate)) fs4.chmodSync(candidate, 384);
|
|
345
1026
|
}
|
|
346
1027
|
} catch (error) {
|
|
347
1028
|
db.close();
|
|
@@ -430,36 +1111,36 @@ function fileFromRow(id, row) {
|
|
|
430
1111
|
delivery_state: row.delivery_state,
|
|
431
1112
|
human_read_at_ms: row.human_read_at_ms,
|
|
432
1113
|
reply_to: reply(row),
|
|
433
|
-
blob_path:
|
|
1114
|
+
blob_path: join4(id.dir, row.blob_relpath),
|
|
434
1115
|
kind: mime.startsWith("audio/") ? "voice_message" : "file"
|
|
435
1116
|
};
|
|
436
1117
|
}
|
|
437
1118
|
function writeBlob(id, bytes) {
|
|
438
1119
|
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
|
439
|
-
const relpath =
|
|
440
|
-
const final =
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
if (!
|
|
444
|
-
const tmp =
|
|
1120
|
+
const relpath = join4("blobs", "sha256", sha256.slice(0, 2), sha256);
|
|
1121
|
+
const final = join4(id.dir, relpath);
|
|
1122
|
+
fs4.mkdirSync(dirname3(final), { recursive: true, mode: 448 });
|
|
1123
|
+
fs4.chmodSync(dirname3(final), 448);
|
|
1124
|
+
if (!fs4.existsSync(final)) {
|
|
1125
|
+
const tmp = join4(dirname3(final), `.${sha256}.${process.pid}.${randomUUID()}.tmp`);
|
|
445
1126
|
try {
|
|
446
|
-
|
|
447
|
-
|
|
1127
|
+
fs4.writeFileSync(tmp, bytes, { flag: "wx", mode: 384 });
|
|
1128
|
+
fs4.chmodSync(tmp, 384);
|
|
448
1129
|
try {
|
|
449
|
-
|
|
1130
|
+
fs4.renameSync(tmp, final);
|
|
450
1131
|
} catch (error) {
|
|
451
|
-
if (!
|
|
452
|
-
|
|
1132
|
+
if (!fs4.existsSync(final)) throw error;
|
|
1133
|
+
fs4.rmSync(tmp, { force: true });
|
|
453
1134
|
}
|
|
454
1135
|
} catch (error) {
|
|
455
1136
|
try {
|
|
456
|
-
|
|
1137
|
+
fs4.rmSync(tmp, { force: true });
|
|
457
1138
|
} catch {
|
|
458
1139
|
}
|
|
459
1140
|
throw error;
|
|
460
1141
|
}
|
|
461
1142
|
}
|
|
462
|
-
|
|
1143
|
+
fs4.chmodSync(final, 384);
|
|
463
1144
|
return { sha256, relpath };
|
|
464
1145
|
}
|
|
465
1146
|
function ingestApplicationEvent(id, event) {
|
|
@@ -469,7 +1150,7 @@ function ingestApplicationEvent(id, event) {
|
|
|
469
1150
|
const inboxState = event.direction === "in" ? event.inboxState ?? "unread" : "read";
|
|
470
1151
|
const deliveryState = event.direction === "out" ? "sent" : null;
|
|
471
1152
|
if (event.kind === "message") {
|
|
472
|
-
|
|
1153
|
+
const inserted2 = db.transaction(() => {
|
|
473
1154
|
const claimed = db.prepare(`
|
|
474
1155
|
INSERT INTO wire_items(wire_id, item_kind) VALUES (?, 'message')
|
|
475
1156
|
ON CONFLICT(wire_id) DO NOTHING
|
|
@@ -498,44 +1179,55 @@ function ingestApplicationEvent(id, event) {
|
|
|
498
1179
|
);
|
|
499
1180
|
return true;
|
|
500
1181
|
})();
|
|
1182
|
+
if (inserted2) noteHistoryStorageMutation(id.dir, Buffer.byteLength(event.body, "utf8"));
|
|
1183
|
+
return inserted2;
|
|
501
1184
|
}
|
|
502
1185
|
const duplicate = db.prepare("SELECT 1 FROM wire_items WHERE wire_id = ?").get(event.wireId);
|
|
503
1186
|
if (duplicate) return false;
|
|
504
1187
|
const blob = writeBlob(id, event.bytes);
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
1188
|
+
let inserted;
|
|
1189
|
+
try {
|
|
1190
|
+
inserted = db.transaction(() => {
|
|
1191
|
+
const claimed = db.prepare(`
|
|
1192
|
+
INSERT INTO wire_items(wire_id, item_kind) VALUES (?, 'file')
|
|
1193
|
+
ON CONFLICT(wire_id) DO NOTHING
|
|
1194
|
+
`).run(event.wireId);
|
|
1195
|
+
if (claimed.changes !== 1) return false;
|
|
1196
|
+
db.prepare(`
|
|
1197
|
+
INSERT INTO files (
|
|
1198
|
+
wire_id, peer_cid, peer_name_snapshot, direction, filename, mime,
|
|
1199
|
+
byte_length, sha256, blob_relpath, occurred_at_ms, reply_to_wire_id,
|
|
1200
|
+
reply_to_sentence, encryption, inbox_state, delivery_state,
|
|
1201
|
+
human_read_at_ms, inserted_at_ms
|
|
1202
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1203
|
+
`).run(
|
|
1204
|
+
event.wireId,
|
|
1205
|
+
event.peerCid,
|
|
1206
|
+
event.peerName,
|
|
1207
|
+
event.direction,
|
|
1208
|
+
event.filename,
|
|
1209
|
+
event.mime || "application/octet-stream",
|
|
1210
|
+
event.bytes.byteLength,
|
|
1211
|
+
blob.sha256,
|
|
1212
|
+
blob.relpath,
|
|
1213
|
+
event.occurredAtMs,
|
|
1214
|
+
event.replyToWireId ?? null,
|
|
1215
|
+
event.replyToSentence ?? null,
|
|
1216
|
+
event.encryption,
|
|
1217
|
+
inboxState,
|
|
1218
|
+
deliveryState,
|
|
1219
|
+
event.direction === "out" ? event.occurredAtMs : null,
|
|
1220
|
+
Date.now()
|
|
1221
|
+
);
|
|
1222
|
+
return true;
|
|
1223
|
+
})();
|
|
1224
|
+
} catch (error) {
|
|
1225
|
+
cleanupHistoryBlob(id.dir, blob.relpath);
|
|
1226
|
+
throw error;
|
|
1227
|
+
}
|
|
1228
|
+
if (!inserted) cleanupHistoryBlob(id.dir, blob.relpath);
|
|
1229
|
+
else noteHistoryStorageMutation(id.dir, event.bytes.byteLength);
|
|
1230
|
+
return inserted;
|
|
539
1231
|
}
|
|
540
1232
|
function ingestMonitoringEvent(id, event) {
|
|
541
1233
|
if (!event.itemId || !event.sourceCid || !event.sourceName || !event.peerCid || !event.peerName) {
|
|
@@ -543,7 +1235,7 @@ function ingestMonitoringEvent(id, event) {
|
|
|
543
1235
|
}
|
|
544
1236
|
if (!Number.isSafeInteger(event.occurredAtMs) || event.occurredAtMs < 0) throw new Error("invalid monitoring timestamp");
|
|
545
1237
|
if (Buffer.byteLength(event.body, "utf8") > MAX_TEXT_BYTES) throw new Error("monitoring body exceeds host limit");
|
|
546
|
-
|
|
1238
|
+
const inserted = openHistory(id).prepare(`
|
|
547
1239
|
INSERT INTO monitoring_items (
|
|
548
1240
|
item_id, source_cid, source_name_snapshot, direction, peer_cid,
|
|
549
1241
|
peer_name_snapshot, body, occurred_at_ms, queue_state, inserted_at_ms
|
|
@@ -560,12 +1252,14 @@ function ingestMonitoringEvent(id, event) {
|
|
|
560
1252
|
event.occurredAtMs,
|
|
561
1253
|
Date.now()
|
|
562
1254
|
).changes === 1;
|
|
1255
|
+
if (inserted) noteHistoryStorageMutation(id.dir, Buffer.byteLength(event.body, "utf8"));
|
|
1256
|
+
return inserted;
|
|
563
1257
|
}
|
|
564
1258
|
function ingestControlEvent(id, event) {
|
|
565
1259
|
if (!event.itemId || !event.senderCid || !event.senderName) throw new Error("invalid control event metadata");
|
|
566
1260
|
if (!Number.isSafeInteger(event.occurredAtMs) || event.occurredAtMs < 0) throw new Error("invalid control timestamp");
|
|
567
1261
|
if (Buffer.byteLength(event.payload, "utf8") > MAX_TEXT_BYTES) throw new Error("control payload exceeds host limit");
|
|
568
|
-
|
|
1262
|
+
const inserted = openHistory(id).prepare(`
|
|
569
1263
|
INSERT INTO control_requests (
|
|
570
1264
|
item_id, sender_cid, sender_name_snapshot, payload,
|
|
571
1265
|
occurred_at_ms, queue_state, inserted_at_ms
|
|
@@ -579,10 +1273,12 @@ function ingestControlEvent(id, event) {
|
|
|
579
1273
|
event.occurredAtMs,
|
|
580
1274
|
Date.now()
|
|
581
1275
|
).changes === 1;
|
|
1276
|
+
if (inserted) noteHistoryStorageMutation(id.dir, Buffer.byteLength(event.payload, "utf8"));
|
|
1277
|
+
return inserted;
|
|
582
1278
|
}
|
|
583
1279
|
function promotePendingIntroduction(id, peerCid) {
|
|
584
1280
|
const db = openHistory(id);
|
|
585
|
-
|
|
1281
|
+
const changed = db.transaction(() => {
|
|
586
1282
|
const messages = db.prepare(`
|
|
587
1283
|
UPDATE messages SET inbox_state = 'unread'
|
|
588
1284
|
WHERE direction = 'in' AND peer_cid = ? AND inbox_state = 'pending_introduction'
|
|
@@ -593,10 +1289,16 @@ function promotePendingIntroduction(id, peerCid) {
|
|
|
593
1289
|
`).run(peerCid).changes;
|
|
594
1290
|
return messages + files;
|
|
595
1291
|
})();
|
|
1292
|
+
if (changed > 0) noteHistoryStorageMutation(id.dir);
|
|
1293
|
+
return changed;
|
|
596
1294
|
}
|
|
597
1295
|
function rejectPendingIntroduction(id, peerCid) {
|
|
598
1296
|
const db = openHistory(id);
|
|
599
|
-
|
|
1297
|
+
const blobRelpaths = db.prepare(`
|
|
1298
|
+
SELECT DISTINCT blob_relpath FROM files
|
|
1299
|
+
WHERE direction = 'in' AND peer_cid = ? AND inbox_state = 'pending_introduction'
|
|
1300
|
+
`).pluck().all(peerCid);
|
|
1301
|
+
const changed = db.transaction(() => {
|
|
600
1302
|
const messages = db.prepare(`
|
|
601
1303
|
DELETE FROM messages
|
|
602
1304
|
WHERE direction = 'in' AND peer_cid = ? AND inbox_state = 'pending_introduction'
|
|
@@ -607,6 +1309,9 @@ function rejectPendingIntroduction(id, peerCid) {
|
|
|
607
1309
|
`).run(peerCid).changes;
|
|
608
1310
|
return messages + files;
|
|
609
1311
|
})();
|
|
1312
|
+
for (const relpath of blobRelpaths) cleanupHistoryBlob(id.dir, relpath);
|
|
1313
|
+
if (changed > 0) noteHistoryStorageMutation(id.dir);
|
|
1314
|
+
return changed;
|
|
610
1315
|
}
|
|
611
1316
|
function batchLimit(value) {
|
|
612
1317
|
const limit = value ?? DEFAULT_BATCH;
|
|
@@ -640,7 +1345,7 @@ function takeUnreadMessages(id, input = {}) {
|
|
|
640
1345
|
const db = openHistory(id);
|
|
641
1346
|
const limit = batchLimit(input.limit);
|
|
642
1347
|
const requested = input.wire_ids;
|
|
643
|
-
|
|
1348
|
+
const result = db.transaction(() => {
|
|
644
1349
|
let rows;
|
|
645
1350
|
if (requested !== void 0) {
|
|
646
1351
|
if (requested.length < 1 || requested.length > MAX_BATCH) {
|
|
@@ -676,6 +1381,8 @@ function takeUnreadMessages(id, input = {}) {
|
|
|
676
1381
|
`).pluck().get());
|
|
677
1382
|
return { messages: rows.map(messageFromRow), remaining };
|
|
678
1383
|
})();
|
|
1384
|
+
if (result.messages.length > 0) noteHistoryStorageMutation(id.dir);
|
|
1385
|
+
return result;
|
|
679
1386
|
}
|
|
680
1387
|
function listIncomingFiles(id) {
|
|
681
1388
|
const rows = openHistory(id).prepare(`
|
|
@@ -687,7 +1394,7 @@ function takeUnreadFiles(id, input = {}) {
|
|
|
687
1394
|
const db = openHistory(id);
|
|
688
1395
|
const limit = batchLimit(input.limit);
|
|
689
1396
|
const requested = input.wire_ids;
|
|
690
|
-
|
|
1397
|
+
const result = db.transaction(() => {
|
|
691
1398
|
let rows;
|
|
692
1399
|
if (requested !== void 0) {
|
|
693
1400
|
if (requested.length < 1 || requested.length > MAX_BATCH) throw new Error(`wire_ids must contain 1-${MAX_BATCH} items`);
|
|
@@ -704,18 +1411,21 @@ function takeUnreadFiles(id, input = {}) {
|
|
|
704
1411
|
`).all(limit);
|
|
705
1412
|
}
|
|
706
1413
|
const mark = db.prepare(`
|
|
707
|
-
UPDATE files SET inbox_state = 'read'
|
|
1414
|
+
UPDATE files SET inbox_state = 'read', human_read_at_ms = COALESCE(human_read_at_ms, ?)
|
|
708
1415
|
WHERE seq = ? AND direction = 'in' AND inbox_state = 'unread'
|
|
709
1416
|
`);
|
|
710
1417
|
for (const row of rows) {
|
|
711
|
-
|
|
1418
|
+
const readAt = Date.now();
|
|
1419
|
+
if (mark.run(readAt, row.seq).changes !== 1) throw new Error(`file row ${row.seq} changed during read batch`);
|
|
712
1420
|
row.inbox_state = "read";
|
|
1421
|
+
row.human_read_at_ms = readAt;
|
|
713
1422
|
}
|
|
714
1423
|
const remaining = Number(db.prepare(`
|
|
715
1424
|
SELECT COUNT(*) FROM files WHERE direction = 'in' AND inbox_state = 'unread'
|
|
716
1425
|
`).pluck().get());
|
|
717
1426
|
return { files: rows.map((row) => fileFromRow(id, row)), remaining };
|
|
718
1427
|
})();
|
|
1428
|
+
return result;
|
|
719
1429
|
}
|
|
720
1430
|
function historyWhere(query) {
|
|
721
1431
|
const clauses = [];
|
|
@@ -770,10 +1480,10 @@ function getFileHistoryItem(id, wireId) {
|
|
|
770
1480
|
}
|
|
771
1481
|
function resolveStoredFile(id, wireId) {
|
|
772
1482
|
const item = getFileHistoryItem(id, wireId);
|
|
773
|
-
if (!item || !
|
|
774
|
-
const identityRoot =
|
|
775
|
-
const resolvedPath =
|
|
776
|
-
return resolvedPath.startsWith(`${identityRoot}${
|
|
1483
|
+
if (!item || !fs4.existsSync(item.blob_path)) return null;
|
|
1484
|
+
const identityRoot = resolve3(id.dir);
|
|
1485
|
+
const resolvedPath = resolve3(item.blob_path);
|
|
1486
|
+
return resolvedPath.startsWith(`${identityRoot}${sep2}`) ? resolvedPath : null;
|
|
777
1487
|
}
|
|
778
1488
|
function updateDeliveryState(id, peerCid, kind, wireIds) {
|
|
779
1489
|
const db = openHistory(id);
|
|
@@ -791,6 +1501,7 @@ function updateDeliveryState(id, peerCid, kind, wireIds) {
|
|
|
791
1501
|
}
|
|
792
1502
|
});
|
|
793
1503
|
tx();
|
|
1504
|
+
if (wireIds.length > 0) noteHistoryStorageMutation(id.dir);
|
|
794
1505
|
}
|
|
795
1506
|
|
|
796
1507
|
// ../../src/constants.ts
|
|
@@ -811,15 +1522,15 @@ function setRegistrar(id) {
|
|
|
811
1522
|
function setRegistrarAdBlob(blob) {
|
|
812
1523
|
registrarAdBlob = blob;
|
|
813
1524
|
}
|
|
814
|
-
var identityDir = (name) =>
|
|
815
|
-
var keyPath = (dir) =>
|
|
816
|
-
var dataPath = (dir) =>
|
|
817
|
-
var notifyLogPath = (dir) =>
|
|
818
|
-
var unreadPath = (dir) =>
|
|
819
|
-
var tempMetaPath = (dir) =>
|
|
1525
|
+
var identityDir = (name) => join5(STATE_DIR, name);
|
|
1526
|
+
var keyPath = (dir) => join5(dir, "identity.key");
|
|
1527
|
+
var dataPath = (dir) => join5(dir, "state_data.bin");
|
|
1528
|
+
var notifyLogPath = (dir) => join5(dir, "notifications.log");
|
|
1529
|
+
var unreadPath = (dir) => join5(dir, "unread.json");
|
|
1530
|
+
var tempMetaPath = (dir) => join5(dir, "temp.json");
|
|
820
1531
|
var hashLeaseToken = (token) => createHash2("sha256").update(token).digest("hex");
|
|
821
1532
|
function writeTempMetaFile(dir, meta) {
|
|
822
|
-
|
|
1533
|
+
fs5.writeFileSync(
|
|
823
1534
|
tempMetaPath(dir),
|
|
824
1535
|
JSON.stringify({ v: 1, owner: { token_sha256: meta.owner.tokenHash, pid: meta.owner.pid }, created_at: meta.createdAt }),
|
|
825
1536
|
{ mode: 384 }
|
|
@@ -827,7 +1538,7 @@ function writeTempMetaFile(dir, meta) {
|
|
|
827
1538
|
}
|
|
828
1539
|
function readTempMetaFile(dir) {
|
|
829
1540
|
try {
|
|
830
|
-
const raw = JSON.parse(
|
|
1541
|
+
const raw = JSON.parse(fs5.readFileSync(tempMetaPath(dir), "utf8"));
|
|
831
1542
|
if (raw.v !== 1 || typeof raw.owner?.token_sha256 !== "string" || typeof raw.owner?.pid !== "number") return null;
|
|
832
1543
|
return {
|
|
833
1544
|
owner: { tokenHash: raw.owner.token_sha256, pid: raw.owner.pid },
|
|
@@ -839,33 +1550,33 @@ function readTempMetaFile(dir) {
|
|
|
839
1550
|
}
|
|
840
1551
|
function tightenIdentityPerms() {
|
|
841
1552
|
for (const name of listPersistedNames()) {
|
|
842
|
-
const dir =
|
|
1553
|
+
const dir = join5(STATE_DIR, name);
|
|
843
1554
|
try {
|
|
844
|
-
|
|
1555
|
+
fs5.chmodSync(dir, 448);
|
|
845
1556
|
} catch (err) {
|
|
846
1557
|
log(`[${name}] chmod 0700 failed:`, String(err));
|
|
847
1558
|
}
|
|
848
1559
|
for (const f of [dataPath(dir), keyPath(dir), historyPath({ dir }), `${historyPath({ dir })}-wal`, `${historyPath({ dir })}-shm`, notifyLogPath(dir), unreadPath(dir)]) {
|
|
849
|
-
if (!
|
|
1560
|
+
if (!fs5.existsSync(f)) continue;
|
|
850
1561
|
try {
|
|
851
|
-
|
|
1562
|
+
fs5.chmodSync(f, 384);
|
|
852
1563
|
} catch (err) {
|
|
853
1564
|
log(`[${name}] chmod 0600 ${f} failed:`, String(err));
|
|
854
1565
|
}
|
|
855
1566
|
}
|
|
856
|
-
const blobs =
|
|
857
|
-
if (
|
|
1567
|
+
const blobs = join5(dir, "blobs");
|
|
1568
|
+
if (fs5.existsSync(blobs)) {
|
|
858
1569
|
const tightenTree = (path) => {
|
|
859
|
-
for (const entry of
|
|
860
|
-
const child =
|
|
1570
|
+
for (const entry of fs5.readdirSync(path, { withFileTypes: true })) {
|
|
1571
|
+
const child = join5(path, entry.name);
|
|
861
1572
|
if (entry.isDirectory()) {
|
|
862
|
-
|
|
1573
|
+
fs5.chmodSync(child, 448);
|
|
863
1574
|
tightenTree(child);
|
|
864
|
-
} else if (entry.isFile())
|
|
1575
|
+
} else if (entry.isFile()) fs5.chmodSync(child, 384);
|
|
865
1576
|
}
|
|
866
1577
|
};
|
|
867
1578
|
try {
|
|
868
|
-
|
|
1579
|
+
fs5.chmodSync(blobs, 448);
|
|
869
1580
|
tightenTree(blobs);
|
|
870
1581
|
} catch (err) {
|
|
871
1582
|
log(`[${name}] chmod history blobs failed:`, String(err));
|
|
@@ -884,13 +1595,13 @@ function findIdentityFile(id, wireId) {
|
|
|
884
1595
|
}
|
|
885
1596
|
}
|
|
886
1597
|
function listPersistedNames() {
|
|
887
|
-
if (!
|
|
888
|
-
return
|
|
1598
|
+
if (!fs5.existsSync(STATE_DIR)) return [];
|
|
1599
|
+
return fs5.readdirSync(STATE_DIR, { withFileTypes: true }).filter((d) => d.isDirectory() && fs5.existsSync(keyPath(join5(STATE_DIR, d.name)))).map((d) => d.name);
|
|
889
1600
|
}
|
|
890
1601
|
|
|
891
1602
|
// ../../src/identity/lease.ts
|
|
892
|
-
import { join as
|
|
893
|
-
import * as
|
|
1603
|
+
import { join as join6 } from "node:path";
|
|
1604
|
+
import * as fs6 from "node:fs";
|
|
894
1605
|
var leases = /* @__PURE__ */ new Map();
|
|
895
1606
|
var tombstones = /* @__PURE__ */ new Set();
|
|
896
1607
|
var sessionHeaders = /* @__PURE__ */ new Map();
|
|
@@ -908,17 +1619,17 @@ function leaseByToken(token) {
|
|
|
908
1619
|
for (const l of leases.values()) if (l.token === token) return l;
|
|
909
1620
|
return void 0;
|
|
910
1621
|
}
|
|
911
|
-
var bindingsSnapshotPath = () =>
|
|
1622
|
+
var bindingsSnapshotPath = () => join6(STATE_DIR, "bindings.json");
|
|
912
1623
|
function persistBindings() {
|
|
913
1624
|
try {
|
|
914
|
-
|
|
1625
|
+
fs6.mkdirSync(STATE_DIR, { recursive: true });
|
|
915
1626
|
const tmp = `${bindingsSnapshotPath()}.tmp`;
|
|
916
|
-
|
|
1627
|
+
fs6.writeFileSync(tmp, JSON.stringify({
|
|
917
1628
|
pid: process.pid,
|
|
918
1629
|
bound: [...leases.keys()],
|
|
919
1630
|
holders: [...leases.values()].map((l) => ({ identity: l.identity, pid: l.pid }))
|
|
920
1631
|
}));
|
|
921
|
-
|
|
1632
|
+
fs6.renameSync(tmp, bindingsSnapshotPath());
|
|
922
1633
|
} catch (err) {
|
|
923
1634
|
log("failed to persist bindings snapshot:", String(err));
|
|
924
1635
|
}
|
|
@@ -964,19 +1675,19 @@ function bindSession(sid, name) {
|
|
|
964
1675
|
}
|
|
965
1676
|
|
|
966
1677
|
// ../../src/identity/hierarchy.ts
|
|
967
|
-
import { join as
|
|
968
|
-
import * as
|
|
1678
|
+
import { join as join9 } from "node:path";
|
|
1679
|
+
import * as fs11 from "node:fs";
|
|
969
1680
|
|
|
970
1681
|
// ../../src/mufl/tx.ts
|
|
971
1682
|
import { AdaptObjectLifetime as AdaptObjectLifetime2 } from "@adapt-toolkit/sdk/common";
|
|
972
1683
|
import { object_to_adapt_value } from "@adapt-toolkit/sdk/wrapper";
|
|
973
1684
|
|
|
974
1685
|
// ../../src/state.ts
|
|
975
|
-
import * as
|
|
1686
|
+
import * as fs10 from "node:fs";
|
|
976
1687
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
977
1688
|
|
|
978
1689
|
// ../../src/identity/provision.ts
|
|
979
|
-
import * as
|
|
1690
|
+
import * as fs9 from "node:fs";
|
|
980
1691
|
import { randomBytes } from "node:crypto";
|
|
981
1692
|
import { PacketWrapperConfigurator } from "@adapt-toolkit/sdk/wrappers";
|
|
982
1693
|
|
|
@@ -984,8 +1695,8 @@ import { PacketWrapperConfigurator } from "@adapt-toolkit/sdk/wrappers";
|
|
|
984
1695
|
import { AdaptObjectLifetime } from "@adapt-toolkit/sdk/common";
|
|
985
1696
|
|
|
986
1697
|
// ../../src/notify.ts
|
|
987
|
-
import { join as
|
|
988
|
-
import * as
|
|
1698
|
+
import { join as join7 } from "node:path";
|
|
1699
|
+
import * as fs7 from "node:fs";
|
|
989
1700
|
|
|
990
1701
|
// ../../src/events.ts
|
|
991
1702
|
var nonEmptyString = (value) => typeof value === "string" && value.length > 0;
|
|
@@ -1109,7 +1820,7 @@ function fireNotifyWaiters(name) {
|
|
|
1109
1820
|
}
|
|
1110
1821
|
}
|
|
1111
1822
|
function waitForNotify(name, ms) {
|
|
1112
|
-
return new Promise((
|
|
1823
|
+
return new Promise((resolve4) => {
|
|
1113
1824
|
let set = notifyWaiters.get(name);
|
|
1114
1825
|
if (!set) {
|
|
1115
1826
|
set = /* @__PURE__ */ new Set();
|
|
@@ -1122,7 +1833,7 @@ function waitForNotify(name, ms) {
|
|
|
1122
1833
|
set.delete(fn);
|
|
1123
1834
|
if (set.size === 0) notifyWaiters.delete(name);
|
|
1124
1835
|
clearTimeout(timer);
|
|
1125
|
-
|
|
1836
|
+
resolve4();
|
|
1126
1837
|
};
|
|
1127
1838
|
const fn = finish;
|
|
1128
1839
|
const timer = setTimeout(finish, ms);
|
|
@@ -1147,10 +1858,10 @@ function bodyFreeNotifyValue(value) {
|
|
|
1147
1858
|
function appendNotifyLog(id, event) {
|
|
1148
1859
|
const contentFree = bodyFreeNotifyValue(event);
|
|
1149
1860
|
try {
|
|
1150
|
-
|
|
1861
|
+
fs7.mkdirSync(id.dir, { recursive: true, mode: 448 });
|
|
1151
1862
|
const path = notifyLogPath(id.dir);
|
|
1152
|
-
|
|
1153
|
-
|
|
1863
|
+
fs7.appendFileSync(path, JSON.stringify(contentFree) + "\n", { mode: 384 });
|
|
1864
|
+
fs7.chmodSync(path, 384);
|
|
1154
1865
|
} catch (err) {
|
|
1155
1866
|
log(`[${id.name}] failed to append notifications.log:`, String(err));
|
|
1156
1867
|
}
|
|
@@ -1165,7 +1876,7 @@ var NOTIFY_LONGPOLL_MS = Number(process.env.OURS_NOTIFY_LONGPOLL_MS) > 0 ? Numbe
|
|
|
1165
1876
|
var NOTIFY_RECHECK_MS = 250;
|
|
1166
1877
|
function notifyLogSize(logPath) {
|
|
1167
1878
|
try {
|
|
1168
|
-
return
|
|
1879
|
+
return fs7.statSync(logPath).size;
|
|
1169
1880
|
} catch {
|
|
1170
1881
|
return 0;
|
|
1171
1882
|
}
|
|
@@ -1175,11 +1886,11 @@ function readNotifyRange(logPath, from, to) {
|
|
|
1175
1886
|
const buf = Buffer.alloc(to - from);
|
|
1176
1887
|
let read = 0;
|
|
1177
1888
|
try {
|
|
1178
|
-
const fd =
|
|
1889
|
+
const fd = fs7.openSync(logPath, "r");
|
|
1179
1890
|
try {
|
|
1180
|
-
read =
|
|
1891
|
+
read = fs7.readSync(fd, buf, 0, buf.length, from);
|
|
1181
1892
|
} finally {
|
|
1182
|
-
|
|
1893
|
+
fs7.closeSync(fd);
|
|
1183
1894
|
}
|
|
1184
1895
|
} catch {
|
|
1185
1896
|
return { events: [], cursor: from };
|
|
@@ -1198,7 +1909,7 @@ function readNotifyRange(logPath, from, to) {
|
|
|
1198
1909
|
return { events, cursor: from + lastNl + 1 };
|
|
1199
1910
|
}
|
|
1200
1911
|
async function serveNotifications(req, res, name, sinceParam, kindsParam = null) {
|
|
1201
|
-
const logPath = notifyLogPath(
|
|
1912
|
+
const logPath = notifyLogPath(join7(STATE_DIR, name));
|
|
1202
1913
|
const send = (cursor, events) => {
|
|
1203
1914
|
if (res.writableEnded) return;
|
|
1204
1915
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
@@ -1269,11 +1980,11 @@ function refreshUnread(id) {
|
|
|
1269
1980
|
files: unreadFiles.length,
|
|
1270
1981
|
unread_files: unreadFiles.slice(-10)
|
|
1271
1982
|
};
|
|
1272
|
-
|
|
1983
|
+
fs7.mkdirSync(id.dir, { recursive: true, mode: 448 });
|
|
1273
1984
|
const tmp = `${unreadPath(id.dir)}.tmp`;
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1985
|
+
fs7.writeFileSync(tmp, JSON.stringify(snapshot), { mode: 384 });
|
|
1986
|
+
fs7.chmodSync(tmp, 384);
|
|
1987
|
+
fs7.renameSync(tmp, unreadPath(id.dir));
|
|
1277
1988
|
} catch (err) {
|
|
1278
1989
|
log(`[${id.name}] failed to refresh unread snapshot:`, String(err));
|
|
1279
1990
|
}
|
|
@@ -1282,7 +1993,7 @@ function unreadSummary() {
|
|
|
1282
1993
|
const out = [];
|
|
1283
1994
|
let entries = [];
|
|
1284
1995
|
try {
|
|
1285
|
-
entries =
|
|
1996
|
+
entries = fs7.readdirSync(STATE_DIR, { withFileTypes: true });
|
|
1286
1997
|
} catch {
|
|
1287
1998
|
return { identities: out };
|
|
1288
1999
|
}
|
|
@@ -1290,7 +2001,7 @@ function unreadSummary() {
|
|
|
1290
2001
|
if (!entry.isDirectory() || validateName(entry.name) !== null) continue;
|
|
1291
2002
|
let value;
|
|
1292
2003
|
try {
|
|
1293
|
-
value = JSON.parse(
|
|
2004
|
+
value = JSON.parse(fs7.readFileSync(unreadPath(join7(STATE_DIR, entry.name)), "utf8"));
|
|
1294
2005
|
} catch {
|
|
1295
2006
|
continue;
|
|
1296
2007
|
}
|
|
@@ -1835,24 +2546,24 @@ function wireHandlers(id, hooks) {
|
|
|
1835
2546
|
}
|
|
1836
2547
|
|
|
1837
2548
|
// ../../src/book.ts
|
|
1838
|
-
import { join as
|
|
1839
|
-
import * as
|
|
1840
|
-
var bookDir = () =>
|
|
1841
|
-
var registrarKeyPath = () =>
|
|
1842
|
-
var bookPath = () =>
|
|
2549
|
+
import { join as join8 } from "node:path";
|
|
2550
|
+
import * as fs8 from "node:fs";
|
|
2551
|
+
var bookDir = () => join8(STATE_DIR, BOOK_DIR_NAME);
|
|
2552
|
+
var registrarKeyPath = () => join8(bookDir(), "registrar.key");
|
|
2553
|
+
var bookPath = () => join8(bookDir(), "book.json");
|
|
1843
2554
|
function readBook() {
|
|
1844
2555
|
try {
|
|
1845
|
-
const parsed = JSON.parse(
|
|
2556
|
+
const parsed = JSON.parse(fs8.readFileSync(bookPath(), "utf8"));
|
|
1846
2557
|
return parsed && typeof parsed.entries === "object" ? parsed.entries : {};
|
|
1847
2558
|
} catch {
|
|
1848
2559
|
return {};
|
|
1849
2560
|
}
|
|
1850
2561
|
}
|
|
1851
2562
|
function writeBook(entries) {
|
|
1852
|
-
|
|
2563
|
+
fs8.mkdirSync(bookDir(), { recursive: true });
|
|
1853
2564
|
const tmp = `${bookPath()}.tmp`;
|
|
1854
|
-
|
|
1855
|
-
|
|
2565
|
+
fs8.writeFileSync(tmp, JSON.stringify({ v: 1, entries }, null, 2), { mode: 384 });
|
|
2566
|
+
fs8.renameSync(tmp, bookPath());
|
|
1856
2567
|
}
|
|
1857
2568
|
function exportAdBlob(id) {
|
|
1858
2569
|
return withScope(
|
|
@@ -2038,7 +2749,7 @@ async function provisionIdentity(name, opts = { exposeLocal: true, localAutoAcce
|
|
|
2038
2749
|
let provisioned;
|
|
2039
2750
|
try {
|
|
2040
2751
|
const dir = identityDir(name);
|
|
2041
|
-
|
|
2752
|
+
fs9.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
2042
2753
|
let tempMeta;
|
|
2043
2754
|
if (opts.temp) {
|
|
2044
2755
|
tempMeta = { owner: { tokenHash: opts.temp.tokenHash, pid: opts.temp.pid }, createdAt: Date.now() };
|
|
@@ -2049,7 +2760,7 @@ async function provisionIdentity(name, opts = { exposeLocal: true, localAutoAcce
|
|
|
2049
2760
|
provisioned = id;
|
|
2050
2761
|
openHistory(id);
|
|
2051
2762
|
if (tempMeta) id.temp = tempMeta;
|
|
2052
|
-
|
|
2763
|
+
fs9.writeFileSync(keyPath(dir), exportSigningSecret(id), { mode: 384 });
|
|
2053
2764
|
await withScopeAsync(async (lt) => {
|
|
2054
2765
|
await mutatingTx(id, "::a2a_messaging::set_my_name", { name }, lt);
|
|
2055
2766
|
});
|
|
@@ -2072,7 +2783,7 @@ async function provisionIdentity(name, opts = { exposeLocal: true, localAutoAcce
|
|
|
2072
2783
|
}
|
|
2073
2784
|
async function restoreIdentity(name) {
|
|
2074
2785
|
const dir = identityDir(name);
|
|
2075
|
-
const secret =
|
|
2786
|
+
const secret = fs9.readFileSync(keyPath(dir), "utf8").trim();
|
|
2076
2787
|
const id = await createPacket(name, "", dir, false, secret, true);
|
|
2077
2788
|
log(`[${name}] created QUARANTINED (no routing/broker registration, not client-bindable) \u2014 importing state before exposure`);
|
|
2078
2789
|
const holdMs = Number(process.env.OURS_TEST_RESTORE_HOLD_MS || "") || 0;
|
|
@@ -2097,7 +2808,7 @@ async function restoreIdentity(name) {
|
|
|
2097
2808
|
if (process.env.OURS_TEST_FORCE_IMPORT_TIMEOUT === "1") {
|
|
2098
2809
|
throw new Error("timed out waiting for the transaction result (forced by OURS_TEST_FORCE_IMPORT_TIMEOUT)");
|
|
2099
2810
|
}
|
|
2100
|
-
const buf =
|
|
2811
|
+
const buf = fs9.readFileSync(dataPath(dir));
|
|
2101
2812
|
await withScopeAsync(async (lt) => {
|
|
2102
2813
|
const adaptData = id.pw.packet.ParseValue(new Uint8Array(buf)).Attach(lt);
|
|
2103
2814
|
const importTimeoutMs = Number(process.env.OURS_IMPORT_TIMEOUT_MS || "") || void 0;
|
|
@@ -2173,23 +2884,23 @@ async function restoreIdentity(name) {
|
|
|
2173
2884
|
|
|
2174
2885
|
// ../../src/state.ts
|
|
2175
2886
|
async function ensureRegistrar() {
|
|
2176
|
-
|
|
2887
|
+
fs10.mkdirSync(bookDir(), { recursive: true });
|
|
2177
2888
|
let secret;
|
|
2178
2889
|
try {
|
|
2179
|
-
secret =
|
|
2890
|
+
secret = fs10.readFileSync(registrarKeyPath(), "utf8").trim();
|
|
2180
2891
|
} catch {
|
|
2181
2892
|
}
|
|
2182
2893
|
const seed = randomBytes2(24).toString("hex");
|
|
2183
2894
|
setRegistrar(await createPacket(BOOK_DIR_NAME, seed, bookDir(), false, secret));
|
|
2184
2895
|
if (!secret) {
|
|
2185
|
-
|
|
2896
|
+
fs10.writeFileSync(registrarKeyPath(), exportSigningSecret(registrar), { mode: 384 });
|
|
2186
2897
|
}
|
|
2187
2898
|
setRegistrarAdBlob(exportAdBlob(registrar));
|
|
2188
2899
|
log(`contact-book registrar ready (${registrar.cid})`);
|
|
2189
2900
|
}
|
|
2190
2901
|
function hasSavedState(dir) {
|
|
2191
2902
|
try {
|
|
2192
|
-
return
|
|
2903
|
+
return fs10.existsSync(dataPath(dir)) && fs10.statSync(dataPath(dir)).size > 0;
|
|
2193
2904
|
} catch {
|
|
2194
2905
|
return false;
|
|
2195
2906
|
}
|
|
@@ -2198,38 +2909,38 @@ function saveState(id) {
|
|
|
2198
2909
|
const bytes = withScope(
|
|
2199
2910
|
(lt) => Buffer.from(readonlyTx(id, "::actor::export_state", lt).Serialize())
|
|
2200
2911
|
);
|
|
2201
|
-
|
|
2912
|
+
fs10.mkdirSync(id.dir, { recursive: true, mode: 448 });
|
|
2202
2913
|
const final = dataPath(id.dir);
|
|
2203
2914
|
const tmp = `${final}.tmp`;
|
|
2204
2915
|
let fd;
|
|
2205
2916
|
try {
|
|
2206
|
-
fd =
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2917
|
+
fd = fs10.openSync(tmp, "w", 384);
|
|
2918
|
+
fs10.fchmodSync(fd, 384);
|
|
2919
|
+
fs10.writeFileSync(fd, bytes);
|
|
2920
|
+
fs10.fsyncSync(fd);
|
|
2921
|
+
fs10.closeSync(fd);
|
|
2211
2922
|
fd = void 0;
|
|
2212
|
-
|
|
2213
|
-
|
|
2923
|
+
fs10.renameSync(tmp, final);
|
|
2924
|
+
fs10.chmodSync(final, 384);
|
|
2214
2925
|
try {
|
|
2215
|
-
|
|
2926
|
+
fs10.chmodSync(keyPath(id.dir), 384);
|
|
2216
2927
|
} catch {
|
|
2217
2928
|
}
|
|
2218
|
-
const dirFd =
|
|
2929
|
+
const dirFd = fs10.openSync(id.dir, "r");
|
|
2219
2930
|
try {
|
|
2220
|
-
|
|
2931
|
+
fs10.fsyncSync(dirFd);
|
|
2221
2932
|
} finally {
|
|
2222
|
-
|
|
2933
|
+
fs10.closeSync(dirFd);
|
|
2223
2934
|
}
|
|
2224
2935
|
} catch (err) {
|
|
2225
2936
|
if (fd !== void 0) {
|
|
2226
2937
|
try {
|
|
2227
|
-
|
|
2938
|
+
fs10.closeSync(fd);
|
|
2228
2939
|
} catch {
|
|
2229
2940
|
}
|
|
2230
2941
|
}
|
|
2231
2942
|
try {
|
|
2232
|
-
|
|
2943
|
+
fs10.rmSync(tmp, { force: true });
|
|
2233
2944
|
} catch {
|
|
2234
2945
|
}
|
|
2235
2946
|
throw err;
|
|
@@ -2341,24 +3052,24 @@ function mutatingTx(id, name, targ, lt, timeoutMs) {
|
|
|
2341
3052
|
function setRootName(name) {
|
|
2342
3053
|
rootName = name;
|
|
2343
3054
|
}
|
|
2344
|
-
var rootMarkerPath = () =>
|
|
3055
|
+
var rootMarkerPath = () => join9(STATE_DIR, "root.json");
|
|
2345
3056
|
var rootName = null;
|
|
2346
3057
|
function readRootMarker() {
|
|
2347
3058
|
try {
|
|
2348
|
-
const parsed = JSON.parse(
|
|
3059
|
+
const parsed = JSON.parse(fs11.readFileSync(rootMarkerPath(), "utf8"));
|
|
2349
3060
|
return typeof parsed.name === "string" ? parsed.name : null;
|
|
2350
3061
|
} catch {
|
|
2351
3062
|
return null;
|
|
2352
3063
|
}
|
|
2353
3064
|
}
|
|
2354
3065
|
function writeRootMarker(name) {
|
|
2355
|
-
|
|
3066
|
+
fs11.mkdirSync(STATE_DIR, { recursive: true });
|
|
2356
3067
|
const tmp = `${rootMarkerPath()}.tmp`;
|
|
2357
|
-
|
|
2358
|
-
|
|
3068
|
+
fs11.writeFileSync(tmp, JSON.stringify({ v: 1, name }));
|
|
3069
|
+
fs11.renameSync(tmp, rootMarkerPath());
|
|
2359
3070
|
}
|
|
2360
3071
|
function clearRootMarker() {
|
|
2361
|
-
|
|
3072
|
+
fs11.rmSync(rootMarkerPath(), { force: true });
|
|
2362
3073
|
}
|
|
2363
3074
|
function describeIdentity(id) {
|
|
2364
3075
|
return withScope((lt) => {
|
|
@@ -2423,11 +3134,11 @@ async function establishRoot(id) {
|
|
|
2423
3134
|
}
|
|
2424
3135
|
|
|
2425
3136
|
// ../../src/identity/reaper.ts
|
|
2426
|
-
import { join as
|
|
2427
|
-
import * as
|
|
3137
|
+
import { join as join10 } from "node:path";
|
|
3138
|
+
import * as fs13 from "node:fs";
|
|
2428
3139
|
|
|
2429
3140
|
// ../../src/identity/lifecycle.ts
|
|
2430
|
-
import * as
|
|
3141
|
+
import * as fs12 from "node:fs";
|
|
2431
3142
|
|
|
2432
3143
|
// ../../src/render/adapt-to-json.ts
|
|
2433
3144
|
import { brotliCompressSync, brotliDecompressSync } from "node:zlib";
|
|
@@ -2752,7 +3463,7 @@ function deleteIdentityCompletely(id) {
|
|
|
2752
3463
|
clearRootMarker();
|
|
2753
3464
|
}
|
|
2754
3465
|
try {
|
|
2755
|
-
|
|
3466
|
+
fs12.rmSync(id.dir, { recursive: true, force: true });
|
|
2756
3467
|
reservedNames.delete(id.name);
|
|
2757
3468
|
} catch (err) {
|
|
2758
3469
|
return `deleting ${id.dir} failed: ${String(err)}`;
|
|
@@ -2832,14 +3543,14 @@ async function reapStaleTemporaryIdentities() {
|
|
|
2832
3543
|
await Promise.all(closes);
|
|
2833
3544
|
}
|
|
2834
3545
|
async function reapOrphanTemporaryDirectories() {
|
|
2835
|
-
if (!
|
|
2836
|
-
for (const d of
|
|
3546
|
+
if (!fs13.existsSync(STATE_DIR)) return;
|
|
3547
|
+
for (const d of fs13.readdirSync(STATE_DIR, { withFileTypes: true })) {
|
|
2837
3548
|
if (!d.isDirectory() || identities.has(d.name) || quarantinedIdentities.has(d.name)) continue;
|
|
2838
|
-
const dir =
|
|
3549
|
+
const dir = join10(STATE_DIR, d.name);
|
|
2839
3550
|
const meta = readTempMetaFile(dir);
|
|
2840
3551
|
if (!meta || !pidDefinitelyDead(meta.owner.pid)) continue;
|
|
2841
3552
|
try {
|
|
2842
|
-
|
|
3553
|
+
fs13.rmSync(dir, { recursive: true, force: true });
|
|
2843
3554
|
reservedNames.delete(d.name);
|
|
2844
3555
|
log(`[${d.name}] removed orphaned temporary-identity dir (owner pid ${meta.owner.pid} confirmed dead)`);
|
|
2845
3556
|
} catch (err) {
|
|
@@ -3053,11 +3764,18 @@ export {
|
|
|
3053
3764
|
BROKER_URL,
|
|
3054
3765
|
PORT,
|
|
3055
3766
|
GC_INTERVAL_MS,
|
|
3767
|
+
HISTORY_MAX_BYTES,
|
|
3056
3768
|
API_VISIBILITY,
|
|
3057
3769
|
startupProgress,
|
|
3058
3770
|
log,
|
|
3059
3771
|
requireAuth,
|
|
3060
3772
|
validateName,
|
|
3773
|
+
initializeHistoryRetention,
|
|
3774
|
+
noteHistoryStorageMutation,
|
|
3775
|
+
getHistoryStorageStatus,
|
|
3776
|
+
stopHistoryRetention,
|
|
3777
|
+
pinHistoryItems,
|
|
3778
|
+
unpinHistoryItems,
|
|
3061
3779
|
consumeOutboundHistoryFailure,
|
|
3062
3780
|
openHistory,
|
|
3063
3781
|
closeHistory,
|