@ours.network/cli 2.0.3 → 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-BUGOBQGZ.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-UIWEBE5K.js → chunk-UHYUIHM2.js} +1 -1
- package/dist/{chunk-ZCCCTR27.js → chunk-YPDZQ4XD.js} +1023 -196
- package/dist/commands.d.ts +1 -1
- package/dist/commands.js +1 -1
- package/dist/config-command.js +1 -1
- package/dist/{daemon-3C5SDTHJ.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-TVV2EZZ6.js → server-3PM3QOPD.js} +337 -158
- 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
|
|
@@ -802,6 +1513,7 @@ function setWrapper(w) {
|
|
|
802
1513
|
wrapper = w;
|
|
803
1514
|
}
|
|
804
1515
|
var identities = /* @__PURE__ */ new Map();
|
|
1516
|
+
var quarantinedIdentities = /* @__PURE__ */ new Map();
|
|
805
1517
|
var registrar = null;
|
|
806
1518
|
var registrarAdBlob = null;
|
|
807
1519
|
function setRegistrar(id) {
|
|
@@ -810,15 +1522,15 @@ function setRegistrar(id) {
|
|
|
810
1522
|
function setRegistrarAdBlob(blob) {
|
|
811
1523
|
registrarAdBlob = blob;
|
|
812
1524
|
}
|
|
813
|
-
var identityDir = (name) =>
|
|
814
|
-
var keyPath = (dir) =>
|
|
815
|
-
var dataPath = (dir) =>
|
|
816
|
-
var notifyLogPath = (dir) =>
|
|
817
|
-
var unreadPath = (dir) =>
|
|
818
|
-
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");
|
|
819
1531
|
var hashLeaseToken = (token) => createHash2("sha256").update(token).digest("hex");
|
|
820
1532
|
function writeTempMetaFile(dir, meta) {
|
|
821
|
-
|
|
1533
|
+
fs5.writeFileSync(
|
|
822
1534
|
tempMetaPath(dir),
|
|
823
1535
|
JSON.stringify({ v: 1, owner: { token_sha256: meta.owner.tokenHash, pid: meta.owner.pid }, created_at: meta.createdAt }),
|
|
824
1536
|
{ mode: 384 }
|
|
@@ -826,7 +1538,7 @@ function writeTempMetaFile(dir, meta) {
|
|
|
826
1538
|
}
|
|
827
1539
|
function readTempMetaFile(dir) {
|
|
828
1540
|
try {
|
|
829
|
-
const raw = JSON.parse(
|
|
1541
|
+
const raw = JSON.parse(fs5.readFileSync(tempMetaPath(dir), "utf8"));
|
|
830
1542
|
if (raw.v !== 1 || typeof raw.owner?.token_sha256 !== "string" || typeof raw.owner?.pid !== "number") return null;
|
|
831
1543
|
return {
|
|
832
1544
|
owner: { tokenHash: raw.owner.token_sha256, pid: raw.owner.pid },
|
|
@@ -838,33 +1550,33 @@ function readTempMetaFile(dir) {
|
|
|
838
1550
|
}
|
|
839
1551
|
function tightenIdentityPerms() {
|
|
840
1552
|
for (const name of listPersistedNames()) {
|
|
841
|
-
const dir =
|
|
1553
|
+
const dir = join5(STATE_DIR, name);
|
|
842
1554
|
try {
|
|
843
|
-
|
|
1555
|
+
fs5.chmodSync(dir, 448);
|
|
844
1556
|
} catch (err) {
|
|
845
1557
|
log(`[${name}] chmod 0700 failed:`, String(err));
|
|
846
1558
|
}
|
|
847
1559
|
for (const f of [dataPath(dir), keyPath(dir), historyPath({ dir }), `${historyPath({ dir })}-wal`, `${historyPath({ dir })}-shm`, notifyLogPath(dir), unreadPath(dir)]) {
|
|
848
|
-
if (!
|
|
1560
|
+
if (!fs5.existsSync(f)) continue;
|
|
849
1561
|
try {
|
|
850
|
-
|
|
1562
|
+
fs5.chmodSync(f, 384);
|
|
851
1563
|
} catch (err) {
|
|
852
1564
|
log(`[${name}] chmod 0600 ${f} failed:`, String(err));
|
|
853
1565
|
}
|
|
854
1566
|
}
|
|
855
|
-
const blobs =
|
|
856
|
-
if (
|
|
1567
|
+
const blobs = join5(dir, "blobs");
|
|
1568
|
+
if (fs5.existsSync(blobs)) {
|
|
857
1569
|
const tightenTree = (path) => {
|
|
858
|
-
for (const entry of
|
|
859
|
-
const child =
|
|
1570
|
+
for (const entry of fs5.readdirSync(path, { withFileTypes: true })) {
|
|
1571
|
+
const child = join5(path, entry.name);
|
|
860
1572
|
if (entry.isDirectory()) {
|
|
861
|
-
|
|
1573
|
+
fs5.chmodSync(child, 448);
|
|
862
1574
|
tightenTree(child);
|
|
863
|
-
} else if (entry.isFile())
|
|
1575
|
+
} else if (entry.isFile()) fs5.chmodSync(child, 384);
|
|
864
1576
|
}
|
|
865
1577
|
};
|
|
866
1578
|
try {
|
|
867
|
-
|
|
1579
|
+
fs5.chmodSync(blobs, 448);
|
|
868
1580
|
tightenTree(blobs);
|
|
869
1581
|
} catch (err) {
|
|
870
1582
|
log(`[${name}] chmod history blobs failed:`, String(err));
|
|
@@ -883,41 +1595,41 @@ function findIdentityFile(id, wireId) {
|
|
|
883
1595
|
}
|
|
884
1596
|
}
|
|
885
1597
|
function listPersistedNames() {
|
|
886
|
-
if (!
|
|
887
|
-
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);
|
|
888
1600
|
}
|
|
889
1601
|
|
|
890
1602
|
// ../../src/identity/lease.ts
|
|
891
|
-
import { join as
|
|
892
|
-
import * as
|
|
1603
|
+
import { join as join6 } from "node:path";
|
|
1604
|
+
import * as fs6 from "node:fs";
|
|
893
1605
|
var leases = /* @__PURE__ */ new Map();
|
|
894
1606
|
var tombstones = /* @__PURE__ */ new Set();
|
|
895
1607
|
var sessionHeaders = /* @__PURE__ */ new Map();
|
|
896
1608
|
var outboundRemovalInFlight = /* @__PURE__ */ new Set();
|
|
897
|
-
function
|
|
898
|
-
if (!Number.isInteger(pid) || pid <=
|
|
1609
|
+
function pidDefinitelyDead(pid) {
|
|
1610
|
+
if (!Number.isInteger(pid) || pid <= 1) return false;
|
|
899
1611
|
try {
|
|
900
1612
|
process.kill(pid, 0);
|
|
901
|
-
return
|
|
1613
|
+
return false;
|
|
902
1614
|
} catch (err) {
|
|
903
|
-
return err.code === "
|
|
1615
|
+
return err.code === "ESRCH";
|
|
904
1616
|
}
|
|
905
1617
|
}
|
|
906
1618
|
function leaseByToken(token) {
|
|
907
1619
|
for (const l of leases.values()) if (l.token === token) return l;
|
|
908
1620
|
return void 0;
|
|
909
1621
|
}
|
|
910
|
-
var bindingsSnapshotPath = () =>
|
|
1622
|
+
var bindingsSnapshotPath = () => join6(STATE_DIR, "bindings.json");
|
|
911
1623
|
function persistBindings() {
|
|
912
1624
|
try {
|
|
913
|
-
|
|
1625
|
+
fs6.mkdirSync(STATE_DIR, { recursive: true });
|
|
914
1626
|
const tmp = `${bindingsSnapshotPath()}.tmp`;
|
|
915
|
-
|
|
1627
|
+
fs6.writeFileSync(tmp, JSON.stringify({
|
|
916
1628
|
pid: process.pid,
|
|
917
1629
|
bound: [...leases.keys()],
|
|
918
1630
|
holders: [...leases.values()].map((l) => ({ identity: l.identity, pid: l.pid }))
|
|
919
1631
|
}));
|
|
920
|
-
|
|
1632
|
+
fs6.renameSync(tmp, bindingsSnapshotPath());
|
|
921
1633
|
} catch (err) {
|
|
922
1634
|
log("failed to persist bindings snapshot:", String(err));
|
|
923
1635
|
}
|
|
@@ -963,19 +1675,19 @@ function bindSession(sid, name) {
|
|
|
963
1675
|
}
|
|
964
1676
|
|
|
965
1677
|
// ../../src/identity/hierarchy.ts
|
|
966
|
-
import { join as
|
|
967
|
-
import * as
|
|
1678
|
+
import { join as join9 } from "node:path";
|
|
1679
|
+
import * as fs11 from "node:fs";
|
|
968
1680
|
|
|
969
1681
|
// ../../src/mufl/tx.ts
|
|
970
1682
|
import { AdaptObjectLifetime as AdaptObjectLifetime2 } from "@adapt-toolkit/sdk/common";
|
|
971
1683
|
import { object_to_adapt_value } from "@adapt-toolkit/sdk/wrapper";
|
|
972
1684
|
|
|
973
1685
|
// ../../src/state.ts
|
|
974
|
-
import * as
|
|
1686
|
+
import * as fs10 from "node:fs";
|
|
975
1687
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
976
1688
|
|
|
977
1689
|
// ../../src/identity/provision.ts
|
|
978
|
-
import * as
|
|
1690
|
+
import * as fs9 from "node:fs";
|
|
979
1691
|
import { randomBytes } from "node:crypto";
|
|
980
1692
|
import { PacketWrapperConfigurator } from "@adapt-toolkit/sdk/wrappers";
|
|
981
1693
|
|
|
@@ -983,8 +1695,8 @@ import { PacketWrapperConfigurator } from "@adapt-toolkit/sdk/wrappers";
|
|
|
983
1695
|
import { AdaptObjectLifetime } from "@adapt-toolkit/sdk/common";
|
|
984
1696
|
|
|
985
1697
|
// ../../src/notify.ts
|
|
986
|
-
import { join as
|
|
987
|
-
import * as
|
|
1698
|
+
import { join as join7 } from "node:path";
|
|
1699
|
+
import * as fs7 from "node:fs";
|
|
988
1700
|
|
|
989
1701
|
// ../../src/events.ts
|
|
990
1702
|
var nonEmptyString = (value) => typeof value === "string" && value.length > 0;
|
|
@@ -1108,7 +1820,7 @@ function fireNotifyWaiters(name) {
|
|
|
1108
1820
|
}
|
|
1109
1821
|
}
|
|
1110
1822
|
function waitForNotify(name, ms) {
|
|
1111
|
-
return new Promise((
|
|
1823
|
+
return new Promise((resolve4) => {
|
|
1112
1824
|
let set = notifyWaiters.get(name);
|
|
1113
1825
|
if (!set) {
|
|
1114
1826
|
set = /* @__PURE__ */ new Set();
|
|
@@ -1121,7 +1833,7 @@ function waitForNotify(name, ms) {
|
|
|
1121
1833
|
set.delete(fn);
|
|
1122
1834
|
if (set.size === 0) notifyWaiters.delete(name);
|
|
1123
1835
|
clearTimeout(timer);
|
|
1124
|
-
|
|
1836
|
+
resolve4();
|
|
1125
1837
|
};
|
|
1126
1838
|
const fn = finish;
|
|
1127
1839
|
const timer = setTimeout(finish, ms);
|
|
@@ -1146,10 +1858,10 @@ function bodyFreeNotifyValue(value) {
|
|
|
1146
1858
|
function appendNotifyLog(id, event) {
|
|
1147
1859
|
const contentFree = bodyFreeNotifyValue(event);
|
|
1148
1860
|
try {
|
|
1149
|
-
|
|
1861
|
+
fs7.mkdirSync(id.dir, { recursive: true, mode: 448 });
|
|
1150
1862
|
const path = notifyLogPath(id.dir);
|
|
1151
|
-
|
|
1152
|
-
|
|
1863
|
+
fs7.appendFileSync(path, JSON.stringify(contentFree) + "\n", { mode: 384 });
|
|
1864
|
+
fs7.chmodSync(path, 384);
|
|
1153
1865
|
} catch (err) {
|
|
1154
1866
|
log(`[${id.name}] failed to append notifications.log:`, String(err));
|
|
1155
1867
|
}
|
|
@@ -1164,7 +1876,7 @@ var NOTIFY_LONGPOLL_MS = Number(process.env.OURS_NOTIFY_LONGPOLL_MS) > 0 ? Numbe
|
|
|
1164
1876
|
var NOTIFY_RECHECK_MS = 250;
|
|
1165
1877
|
function notifyLogSize(logPath) {
|
|
1166
1878
|
try {
|
|
1167
|
-
return
|
|
1879
|
+
return fs7.statSync(logPath).size;
|
|
1168
1880
|
} catch {
|
|
1169
1881
|
return 0;
|
|
1170
1882
|
}
|
|
@@ -1174,11 +1886,11 @@ function readNotifyRange(logPath, from, to) {
|
|
|
1174
1886
|
const buf = Buffer.alloc(to - from);
|
|
1175
1887
|
let read = 0;
|
|
1176
1888
|
try {
|
|
1177
|
-
const fd =
|
|
1889
|
+
const fd = fs7.openSync(logPath, "r");
|
|
1178
1890
|
try {
|
|
1179
|
-
read =
|
|
1891
|
+
read = fs7.readSync(fd, buf, 0, buf.length, from);
|
|
1180
1892
|
} finally {
|
|
1181
|
-
|
|
1893
|
+
fs7.closeSync(fd);
|
|
1182
1894
|
}
|
|
1183
1895
|
} catch {
|
|
1184
1896
|
return { events: [], cursor: from };
|
|
@@ -1197,7 +1909,7 @@ function readNotifyRange(logPath, from, to) {
|
|
|
1197
1909
|
return { events, cursor: from + lastNl + 1 };
|
|
1198
1910
|
}
|
|
1199
1911
|
async function serveNotifications(req, res, name, sinceParam, kindsParam = null) {
|
|
1200
|
-
const logPath = notifyLogPath(
|
|
1912
|
+
const logPath = notifyLogPath(join7(STATE_DIR, name));
|
|
1201
1913
|
const send = (cursor, events) => {
|
|
1202
1914
|
if (res.writableEnded) return;
|
|
1203
1915
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
@@ -1268,11 +1980,11 @@ function refreshUnread(id) {
|
|
|
1268
1980
|
files: unreadFiles.length,
|
|
1269
1981
|
unread_files: unreadFiles.slice(-10)
|
|
1270
1982
|
};
|
|
1271
|
-
|
|
1983
|
+
fs7.mkdirSync(id.dir, { recursive: true, mode: 448 });
|
|
1272
1984
|
const tmp = `${unreadPath(id.dir)}.tmp`;
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1985
|
+
fs7.writeFileSync(tmp, JSON.stringify(snapshot), { mode: 384 });
|
|
1986
|
+
fs7.chmodSync(tmp, 384);
|
|
1987
|
+
fs7.renameSync(tmp, unreadPath(id.dir));
|
|
1276
1988
|
} catch (err) {
|
|
1277
1989
|
log(`[${id.name}] failed to refresh unread snapshot:`, String(err));
|
|
1278
1990
|
}
|
|
@@ -1281,7 +1993,7 @@ function unreadSummary() {
|
|
|
1281
1993
|
const out = [];
|
|
1282
1994
|
let entries = [];
|
|
1283
1995
|
try {
|
|
1284
|
-
entries =
|
|
1996
|
+
entries = fs7.readdirSync(STATE_DIR, { withFileTypes: true });
|
|
1285
1997
|
} catch {
|
|
1286
1998
|
return { identities: out };
|
|
1287
1999
|
}
|
|
@@ -1289,7 +2001,7 @@ function unreadSummary() {
|
|
|
1289
2001
|
if (!entry.isDirectory() || validateName(entry.name) !== null) continue;
|
|
1290
2002
|
let value;
|
|
1291
2003
|
try {
|
|
1292
|
-
value = JSON.parse(
|
|
2004
|
+
value = JSON.parse(fs7.readFileSync(unreadPath(join7(STATE_DIR, entry.name)), "utf8"));
|
|
1293
2005
|
} catch {
|
|
1294
2006
|
continue;
|
|
1295
2007
|
}
|
|
@@ -1834,24 +2546,24 @@ function wireHandlers(id, hooks) {
|
|
|
1834
2546
|
}
|
|
1835
2547
|
|
|
1836
2548
|
// ../../src/book.ts
|
|
1837
|
-
import { join as
|
|
1838
|
-
import * as
|
|
1839
|
-
var bookDir = () =>
|
|
1840
|
-
var registrarKeyPath = () =>
|
|
1841
|
-
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");
|
|
1842
2554
|
function readBook() {
|
|
1843
2555
|
try {
|
|
1844
|
-
const parsed = JSON.parse(
|
|
2556
|
+
const parsed = JSON.parse(fs8.readFileSync(bookPath(), "utf8"));
|
|
1845
2557
|
return parsed && typeof parsed.entries === "object" ? parsed.entries : {};
|
|
1846
2558
|
} catch {
|
|
1847
2559
|
return {};
|
|
1848
2560
|
}
|
|
1849
2561
|
}
|
|
1850
2562
|
function writeBook(entries) {
|
|
1851
|
-
|
|
2563
|
+
fs8.mkdirSync(bookDir(), { recursive: true });
|
|
1852
2564
|
const tmp = `${bookPath()}.tmp`;
|
|
1853
|
-
|
|
1854
|
-
|
|
2565
|
+
fs8.writeFileSync(tmp, JSON.stringify({ v: 1, entries }, null, 2), { mode: 384 });
|
|
2566
|
+
fs8.renameSync(tmp, bookPath());
|
|
1855
2567
|
}
|
|
1856
2568
|
function exportAdBlob(id) {
|
|
1857
2569
|
return withScope(
|
|
@@ -1902,6 +2614,85 @@ async function pinRegistrar(id) {
|
|
|
1902
2614
|
});
|
|
1903
2615
|
}
|
|
1904
2616
|
|
|
2617
|
+
// ../../src/identity/migration.ts
|
|
2618
|
+
function wasPublished(id) {
|
|
2619
|
+
return Object.values(readBook()).some((entry) => entry.container_id === id.cid);
|
|
2620
|
+
}
|
|
2621
|
+
async function finishRestoredActivation(id) {
|
|
2622
|
+
wrapper.expose_packet(id.cid);
|
|
2623
|
+
identities.set(id.name, id);
|
|
2624
|
+
quarantinedIdentities.delete(id.name);
|
|
2625
|
+
log(`[${id.name}] EXPOSED (routing + broker registration) \u2014 hierarchy reconciled after restore`);
|
|
2626
|
+
try {
|
|
2627
|
+
await contactRestoreSweep(id);
|
|
2628
|
+
} catch (err) {
|
|
2629
|
+
log(`[${id.name}] post-activation contact restore sweep failed:`, String(err));
|
|
2630
|
+
}
|
|
2631
|
+
try {
|
|
2632
|
+
refreshUnread(id);
|
|
2633
|
+
} catch (err) {
|
|
2634
|
+
log(`[${id.name}] post-activation unread refresh failed:`, String(err));
|
|
2635
|
+
}
|
|
2636
|
+
return id;
|
|
2637
|
+
}
|
|
2638
|
+
function quarantine(id, status) {
|
|
2639
|
+
const exposeLocal = wasPublished(id);
|
|
2640
|
+
if (exposeLocal) unpublishFromBook(id);
|
|
2641
|
+
identities.delete(id.name);
|
|
2642
|
+
quarantinedIdentities.set(id.name, { identity: id, status, exposeLocal });
|
|
2643
|
+
log(`[${id.name}] QUARANTINED (${status}) \u2014 management-only, unexposed and unbindable`);
|
|
2644
|
+
return id;
|
|
2645
|
+
}
|
|
2646
|
+
async function reconcileRestoredIdentity(id) {
|
|
2647
|
+
if (rootName === id.name) return finishRestoredActivation(id);
|
|
2648
|
+
const hostRoot = rootName ? identities.get(rootName) : void 0;
|
|
2649
|
+
let info;
|
|
2650
|
+
try {
|
|
2651
|
+
info = describeIdentity(id);
|
|
2652
|
+
} catch (err) {
|
|
2653
|
+
log(`[${id.name}] hierarchy classification failed:`, String(err));
|
|
2654
|
+
return quarantine(id, "migration-failed");
|
|
2655
|
+
}
|
|
2656
|
+
if (hostRoot) {
|
|
2657
|
+
if (info.hasCert && info.roleId !== "" && info.rootCid !== hostRoot.cid) {
|
|
2658
|
+
log(`[${id.name}] preserved imported delegation from root ${info.rootCid.slice(0, 12)}\u2026`);
|
|
2659
|
+
return finishRestoredActivation(id);
|
|
2660
|
+
}
|
|
2661
|
+
try {
|
|
2662
|
+
await delegateRole(hostRoot, id);
|
|
2663
|
+
return finishRestoredActivation(id);
|
|
2664
|
+
} catch (err) {
|
|
2665
|
+
log(`[${id.name}] root delegation during restore failed:`, String(err));
|
|
2666
|
+
return quarantine(id, "migration-failed");
|
|
2667
|
+
}
|
|
2668
|
+
}
|
|
2669
|
+
if (info.hasCert && info.roleId !== "") return finishRestoredActivation(id);
|
|
2670
|
+
return quarantine(id, "awaiting-root");
|
|
2671
|
+
}
|
|
2672
|
+
async function adoptQuarantinedIdentities(root) {
|
|
2673
|
+
const adopted = [];
|
|
2674
|
+
const failed = [];
|
|
2675
|
+
for (const [name, held] of [...quarantinedIdentities]) {
|
|
2676
|
+
try {
|
|
2677
|
+
await delegateRole(root, held.identity);
|
|
2678
|
+
if (held.exposeLocal) await publishToBook(held.identity);
|
|
2679
|
+
await finishRestoredActivation(held.identity);
|
|
2680
|
+
quarantinedIdentities.delete(name);
|
|
2681
|
+
adopted.push(name);
|
|
2682
|
+
} catch (err) {
|
|
2683
|
+
try {
|
|
2684
|
+
unpublishFromBook(held.identity);
|
|
2685
|
+
} catch {
|
|
2686
|
+
}
|
|
2687
|
+
held.status = "migration-failed";
|
|
2688
|
+
quarantinedIdentities.set(name, held);
|
|
2689
|
+
log(`[${name}] quarantine adoption failed under root "${root.name}":`, String(err));
|
|
2690
|
+
failed.push(name);
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
return { adopted, failed };
|
|
2694
|
+
}
|
|
2695
|
+
|
|
1905
2696
|
// ../../src/identity/provision.ts
|
|
1906
2697
|
function createPacket(name, seed, dir, track = true, signingSecret, deferExposure = false) {
|
|
1907
2698
|
const config = new PacketWrapperConfigurator();
|
|
@@ -1958,7 +2749,7 @@ async function provisionIdentity(name, opts = { exposeLocal: true, localAutoAcce
|
|
|
1958
2749
|
let provisioned;
|
|
1959
2750
|
try {
|
|
1960
2751
|
const dir = identityDir(name);
|
|
1961
|
-
|
|
2752
|
+
fs9.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
1962
2753
|
let tempMeta;
|
|
1963
2754
|
if (opts.temp) {
|
|
1964
2755
|
tempMeta = { owner: { tokenHash: opts.temp.tokenHash, pid: opts.temp.pid }, createdAt: Date.now() };
|
|
@@ -1969,7 +2760,7 @@ async function provisionIdentity(name, opts = { exposeLocal: true, localAutoAcce
|
|
|
1969
2760
|
provisioned = id;
|
|
1970
2761
|
openHistory(id);
|
|
1971
2762
|
if (tempMeta) id.temp = tempMeta;
|
|
1972
|
-
|
|
2763
|
+
fs9.writeFileSync(keyPath(dir), exportSigningSecret(id), { mode: 384 });
|
|
1973
2764
|
await withScopeAsync(async (lt) => {
|
|
1974
2765
|
await mutatingTx(id, "::a2a_messaging::set_my_name", { name }, lt);
|
|
1975
2766
|
});
|
|
@@ -1992,7 +2783,7 @@ async function provisionIdentity(name, opts = { exposeLocal: true, localAutoAcce
|
|
|
1992
2783
|
}
|
|
1993
2784
|
async function restoreIdentity(name) {
|
|
1994
2785
|
const dir = identityDir(name);
|
|
1995
|
-
const secret =
|
|
2786
|
+
const secret = fs9.readFileSync(keyPath(dir), "utf8").trim();
|
|
1996
2787
|
const id = await createPacket(name, "", dir, false, secret, true);
|
|
1997
2788
|
log(`[${name}] created QUARANTINED (no routing/broker registration, not client-bindable) \u2014 importing state before exposure`);
|
|
1998
2789
|
const holdMs = Number(process.env.OURS_TEST_RESTORE_HOLD_MS || "") || 0;
|
|
@@ -2017,7 +2808,7 @@ async function restoreIdentity(name) {
|
|
|
2017
2808
|
if (process.env.OURS_TEST_FORCE_IMPORT_TIMEOUT === "1") {
|
|
2018
2809
|
throw new Error("timed out waiting for the transaction result (forced by OURS_TEST_FORCE_IMPORT_TIMEOUT)");
|
|
2019
2810
|
}
|
|
2020
|
-
const buf =
|
|
2811
|
+
const buf = fs9.readFileSync(dataPath(dir));
|
|
2021
2812
|
await withScopeAsync(async (lt) => {
|
|
2022
2813
|
const adaptData = id.pw.packet.ParseValue(new Uint8Array(buf)).Attach(lt);
|
|
2023
2814
|
const importTimeoutMs = Number(process.env.OURS_IMPORT_TIMEOUT_MS || "") || void 0;
|
|
@@ -2086,33 +2877,30 @@ async function restoreIdentity(name) {
|
|
|
2086
2877
|
} catch (err) {
|
|
2087
2878
|
tearDownUnexposed("history_open", "FAILED (SQLite history unavailable)", err);
|
|
2088
2879
|
}
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
await contactRestoreSweep(id);
|
|
2093
|
-
refreshUnread(id);
|
|
2094
|
-
return id;
|
|
2880
|
+
const restoredTemp = readTempMetaFile(id.dir);
|
|
2881
|
+
if (restoredTemp) id.temp = restoredTemp;
|
|
2882
|
+
return reconcileRestoredIdentity(id);
|
|
2095
2883
|
}
|
|
2096
2884
|
|
|
2097
2885
|
// ../../src/state.ts
|
|
2098
2886
|
async function ensureRegistrar() {
|
|
2099
|
-
|
|
2887
|
+
fs10.mkdirSync(bookDir(), { recursive: true });
|
|
2100
2888
|
let secret;
|
|
2101
2889
|
try {
|
|
2102
|
-
secret =
|
|
2890
|
+
secret = fs10.readFileSync(registrarKeyPath(), "utf8").trim();
|
|
2103
2891
|
} catch {
|
|
2104
2892
|
}
|
|
2105
2893
|
const seed = randomBytes2(24).toString("hex");
|
|
2106
2894
|
setRegistrar(await createPacket(BOOK_DIR_NAME, seed, bookDir(), false, secret));
|
|
2107
2895
|
if (!secret) {
|
|
2108
|
-
|
|
2896
|
+
fs10.writeFileSync(registrarKeyPath(), exportSigningSecret(registrar), { mode: 384 });
|
|
2109
2897
|
}
|
|
2110
2898
|
setRegistrarAdBlob(exportAdBlob(registrar));
|
|
2111
2899
|
log(`contact-book registrar ready (${registrar.cid})`);
|
|
2112
2900
|
}
|
|
2113
2901
|
function hasSavedState(dir) {
|
|
2114
2902
|
try {
|
|
2115
|
-
return
|
|
2903
|
+
return fs10.existsSync(dataPath(dir)) && fs10.statSync(dataPath(dir)).size > 0;
|
|
2116
2904
|
} catch {
|
|
2117
2905
|
return false;
|
|
2118
2906
|
}
|
|
@@ -2121,38 +2909,38 @@ function saveState(id) {
|
|
|
2121
2909
|
const bytes = withScope(
|
|
2122
2910
|
(lt) => Buffer.from(readonlyTx(id, "::actor::export_state", lt).Serialize())
|
|
2123
2911
|
);
|
|
2124
|
-
|
|
2912
|
+
fs10.mkdirSync(id.dir, { recursive: true, mode: 448 });
|
|
2125
2913
|
const final = dataPath(id.dir);
|
|
2126
2914
|
const tmp = `${final}.tmp`;
|
|
2127
2915
|
let fd;
|
|
2128
2916
|
try {
|
|
2129
|
-
fd =
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
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);
|
|
2134
2922
|
fd = void 0;
|
|
2135
|
-
|
|
2136
|
-
|
|
2923
|
+
fs10.renameSync(tmp, final);
|
|
2924
|
+
fs10.chmodSync(final, 384);
|
|
2137
2925
|
try {
|
|
2138
|
-
|
|
2926
|
+
fs10.chmodSync(keyPath(id.dir), 384);
|
|
2139
2927
|
} catch {
|
|
2140
2928
|
}
|
|
2141
|
-
const dirFd =
|
|
2929
|
+
const dirFd = fs10.openSync(id.dir, "r");
|
|
2142
2930
|
try {
|
|
2143
|
-
|
|
2931
|
+
fs10.fsyncSync(dirFd);
|
|
2144
2932
|
} finally {
|
|
2145
|
-
|
|
2933
|
+
fs10.closeSync(dirFd);
|
|
2146
2934
|
}
|
|
2147
2935
|
} catch (err) {
|
|
2148
2936
|
if (fd !== void 0) {
|
|
2149
2937
|
try {
|
|
2150
|
-
|
|
2938
|
+
fs10.closeSync(fd);
|
|
2151
2939
|
} catch {
|
|
2152
2940
|
}
|
|
2153
2941
|
}
|
|
2154
2942
|
try {
|
|
2155
|
-
|
|
2943
|
+
fs10.rmSync(tmp, { force: true });
|
|
2156
2944
|
} catch {
|
|
2157
2945
|
}
|
|
2158
2946
|
throw err;
|
|
@@ -2264,24 +3052,24 @@ function mutatingTx(id, name, targ, lt, timeoutMs) {
|
|
|
2264
3052
|
function setRootName(name) {
|
|
2265
3053
|
rootName = name;
|
|
2266
3054
|
}
|
|
2267
|
-
var rootMarkerPath = () =>
|
|
3055
|
+
var rootMarkerPath = () => join9(STATE_DIR, "root.json");
|
|
2268
3056
|
var rootName = null;
|
|
2269
3057
|
function readRootMarker() {
|
|
2270
3058
|
try {
|
|
2271
|
-
const parsed = JSON.parse(
|
|
3059
|
+
const parsed = JSON.parse(fs11.readFileSync(rootMarkerPath(), "utf8"));
|
|
2272
3060
|
return typeof parsed.name === "string" ? parsed.name : null;
|
|
2273
3061
|
} catch {
|
|
2274
3062
|
return null;
|
|
2275
3063
|
}
|
|
2276
3064
|
}
|
|
2277
3065
|
function writeRootMarker(name) {
|
|
2278
|
-
|
|
3066
|
+
fs11.mkdirSync(STATE_DIR, { recursive: true });
|
|
2279
3067
|
const tmp = `${rootMarkerPath()}.tmp`;
|
|
2280
|
-
|
|
2281
|
-
|
|
3068
|
+
fs11.writeFileSync(tmp, JSON.stringify({ v: 1, name }));
|
|
3069
|
+
fs11.renameSync(tmp, rootMarkerPath());
|
|
2282
3070
|
}
|
|
2283
3071
|
function clearRootMarker() {
|
|
2284
|
-
|
|
3072
|
+
fs11.rmSync(rootMarkerPath(), { force: true });
|
|
2285
3073
|
}
|
|
2286
3074
|
function describeIdentity(id) {
|
|
2287
3075
|
return withScope((lt) => {
|
|
@@ -2345,9 +3133,12 @@ async function establishRoot(id) {
|
|
|
2345
3133
|
return { adopted, failed };
|
|
2346
3134
|
}
|
|
2347
3135
|
|
|
3136
|
+
// ../../src/identity/reaper.ts
|
|
3137
|
+
import { join as join10 } from "node:path";
|
|
3138
|
+
import * as fs13 from "node:fs";
|
|
3139
|
+
|
|
2348
3140
|
// ../../src/identity/lifecycle.ts
|
|
2349
|
-
import
|
|
2350
|
-
import * as fs11 from "node:fs";
|
|
3141
|
+
import * as fs12 from "node:fs";
|
|
2351
3142
|
|
|
2352
3143
|
// ../../src/render/adapt-to-json.ts
|
|
2353
3144
|
import { brotliCompressSync, brotliDecompressSync } from "node:zlib";
|
|
@@ -2672,7 +3463,7 @@ function deleteIdentityCompletely(id) {
|
|
|
2672
3463
|
clearRootMarker();
|
|
2673
3464
|
}
|
|
2674
3465
|
try {
|
|
2675
|
-
|
|
3466
|
+
fs12.rmSync(id.dir, { recursive: true, force: true });
|
|
2676
3467
|
reservedNames.delete(id.name);
|
|
2677
3468
|
} catch (err) {
|
|
2678
3469
|
return `deleting ${id.dir} failed: ${String(err)}`;
|
|
@@ -2724,31 +3515,46 @@ function closeTemporaryIdentity(id, cause) {
|
|
|
2724
3515
|
})();
|
|
2725
3516
|
return t.closing;
|
|
2726
3517
|
}
|
|
2727
|
-
|
|
2728
|
-
|
|
3518
|
+
|
|
3519
|
+
// ../../src/identity/reaper.ts
|
|
3520
|
+
function sessionReaperIntervalMs(raw = process.env.OURS_SESSION_REAPER_INTERVAL_MS) {
|
|
3521
|
+
if (raw === void 0 || raw.trim() === "") return 5e3;
|
|
3522
|
+
const parsed = Number(raw);
|
|
3523
|
+
if (!Number.isFinite(parsed)) return 5e3;
|
|
3524
|
+
return Math.max(100, Math.trunc(parsed));
|
|
3525
|
+
}
|
|
3526
|
+
async function reapStaleTemporaryIdentities() {
|
|
3527
|
+
const all = [
|
|
3528
|
+
...identities.values(),
|
|
3529
|
+
...[...quarantinedIdentities.values()].map((held) => held.identity)
|
|
3530
|
+
];
|
|
3531
|
+
const closes = [];
|
|
3532
|
+
for (const id of all) {
|
|
2729
3533
|
const t = id.temp;
|
|
2730
|
-
if (!t || t.closing) continue;
|
|
2731
|
-
if (pidAlive(t.owner.pid)) continue;
|
|
3534
|
+
if (!t || t.closing || !pidDefinitelyDead(t.owner.pid)) continue;
|
|
2732
3535
|
const lease = leases.get(id.name);
|
|
2733
|
-
if (lease &&
|
|
2734
|
-
|
|
2735
|
-
(
|
|
3536
|
+
if (lease && !pidDefinitelyDead(lease.pid)) continue;
|
|
3537
|
+
closes.push(
|
|
3538
|
+
closeTemporaryIdentity(id, `stale lease \u2014 owner pid ${t.owner.pid} is dead`).then(() => {
|
|
3539
|
+
quarantinedIdentities.delete(id.name);
|
|
3540
|
+
}).catch((err) => log(`[${id.name}] stale-temp reclaim failed:`, String(err)))
|
|
2736
3541
|
);
|
|
2737
3542
|
}
|
|
3543
|
+
await Promise.all(closes);
|
|
2738
3544
|
}
|
|
2739
|
-
function
|
|
2740
|
-
if (!
|
|
2741
|
-
for (const d of
|
|
2742
|
-
if (!d.isDirectory() || identities.has(d.name)) continue;
|
|
2743
|
-
const dir =
|
|
3545
|
+
async function reapOrphanTemporaryDirectories() {
|
|
3546
|
+
if (!fs13.existsSync(STATE_DIR)) return;
|
|
3547
|
+
for (const d of fs13.readdirSync(STATE_DIR, { withFileTypes: true })) {
|
|
3548
|
+
if (!d.isDirectory() || identities.has(d.name) || quarantinedIdentities.has(d.name)) continue;
|
|
3549
|
+
const dir = join10(STATE_DIR, d.name);
|
|
2744
3550
|
const meta = readTempMetaFile(dir);
|
|
2745
|
-
if (!meta ||
|
|
3551
|
+
if (!meta || !pidDefinitelyDead(meta.owner.pid)) continue;
|
|
2746
3552
|
try {
|
|
2747
|
-
|
|
3553
|
+
fs13.rmSync(dir, { recursive: true, force: true });
|
|
2748
3554
|
reservedNames.delete(d.name);
|
|
2749
|
-
log(`[${d.name}] removed orphaned temporary-identity dir (owner pid ${meta.owner.pid} dead
|
|
3555
|
+
log(`[${d.name}] removed orphaned temporary-identity dir (owner pid ${meta.owner.pid} confirmed dead)`);
|
|
2750
3556
|
} catch (err) {
|
|
2751
|
-
|
|
3557
|
+
throw new Error(`[${d.name}] failed to remove orphaned temporary-identity dir: ${String(err)}`);
|
|
2752
3558
|
}
|
|
2753
3559
|
}
|
|
2754
3560
|
}
|
|
@@ -2859,7 +3665,9 @@ async function bootWrapper() {
|
|
|
2859
3665
|
log("failed to start the contact-book registrar (local contact book disabled):", String(err));
|
|
2860
3666
|
}
|
|
2861
3667
|
tightenIdentityPerms();
|
|
2862
|
-
|
|
3668
|
+
setRootName(readRootMarker());
|
|
3669
|
+
const persistedNames = listPersistedNames();
|
|
3670
|
+
const names = rootName && persistedNames.includes(rootName) ? [rootName, ...persistedNames.filter((name) => name !== rootName)] : persistedNames;
|
|
2863
3671
|
const fakeRestoreCount = Math.max(0, Number(process.env.OURS_TEST_FAKE_RESTORE_COUNT || "") || 0);
|
|
2864
3672
|
const fakeRestoreMs = Math.max(0, Number(process.env.OURS_TEST_FAKE_RESTORE_MS || "") || 0);
|
|
2865
3673
|
const restoreTotal = names.length === 0 && fakeRestoreCount > 0 ? fakeRestoreCount : names.length;
|
|
@@ -2945,8 +3753,8 @@ async function bootWrapper() {
|
|
|
2945
3753
|
if (refreshedRoles.length > 0) log(`refreshed ${refreshedRoles.length} role delegation cert(s) against the live AD on boot`);
|
|
2946
3754
|
}
|
|
2947
3755
|
persistBindings();
|
|
2948
|
-
|
|
2949
|
-
|
|
3756
|
+
await reapOrphanTemporaryDirectories();
|
|
3757
|
+
await reapStaleTemporaryIdentities();
|
|
2950
3758
|
}
|
|
2951
3759
|
|
|
2952
3760
|
export {
|
|
@@ -2956,12 +3764,21 @@ export {
|
|
|
2956
3764
|
BROKER_URL,
|
|
2957
3765
|
PORT,
|
|
2958
3766
|
GC_INTERVAL_MS,
|
|
3767
|
+
HISTORY_MAX_BYTES,
|
|
2959
3768
|
API_VISIBILITY,
|
|
2960
3769
|
startupProgress,
|
|
2961
3770
|
log,
|
|
2962
3771
|
requireAuth,
|
|
2963
3772
|
validateName,
|
|
3773
|
+
initializeHistoryRetention,
|
|
3774
|
+
noteHistoryStorageMutation,
|
|
3775
|
+
getHistoryStorageStatus,
|
|
3776
|
+
stopHistoryRetention,
|
|
3777
|
+
pinHistoryItems,
|
|
3778
|
+
unpinHistoryItems,
|
|
2964
3779
|
consumeOutboundHistoryFailure,
|
|
3780
|
+
openHistory,
|
|
3781
|
+
closeHistory,
|
|
2965
3782
|
listIncomingMessages,
|
|
2966
3783
|
takeUnreadMessages,
|
|
2967
3784
|
listIncomingFiles,
|
|
@@ -2972,8 +3789,12 @@ export {
|
|
|
2972
3789
|
listFileHistory,
|
|
2973
3790
|
getFileHistoryItem,
|
|
2974
3791
|
FILE_SELECTION_CAP,
|
|
3792
|
+
wrapper,
|
|
2975
3793
|
identities,
|
|
3794
|
+
quarantinedIdentities,
|
|
2976
3795
|
registrar,
|
|
3796
|
+
identityDir,
|
|
3797
|
+
keyPath,
|
|
2977
3798
|
hashLeaseToken,
|
|
2978
3799
|
writeTempMetaFile,
|
|
2979
3800
|
isSelectableWireId,
|
|
@@ -2982,7 +3803,7 @@ export {
|
|
|
2982
3803
|
tombstones,
|
|
2983
3804
|
sessionHeaders,
|
|
2984
3805
|
outboundRemovalInFlight,
|
|
2985
|
-
|
|
3806
|
+
pidDefinitelyDead,
|
|
2986
3807
|
leaseByToken,
|
|
2987
3808
|
persistBindings,
|
|
2988
3809
|
resolveBound,
|
|
@@ -3000,11 +3821,16 @@ export {
|
|
|
3000
3821
|
e2eRecoverySweep,
|
|
3001
3822
|
readBook,
|
|
3002
3823
|
exportAdBlob,
|
|
3824
|
+
exportSigningSecret,
|
|
3003
3825
|
publishToBook,
|
|
3004
3826
|
unpublishFromBook,
|
|
3827
|
+
pinRegistrar,
|
|
3828
|
+
adoptQuarantinedIdentities,
|
|
3829
|
+
createPacket,
|
|
3005
3830
|
reservedNames,
|
|
3006
3831
|
provisionIdentity,
|
|
3007
3832
|
saveState,
|
|
3833
|
+
saveStateFailClosed,
|
|
3008
3834
|
withScope,
|
|
3009
3835
|
withScopeAsync,
|
|
3010
3836
|
readonlyTx,
|
|
@@ -3025,7 +3851,8 @@ export {
|
|
|
3025
3851
|
decodeWireBin,
|
|
3026
3852
|
deleteIdentityCompletely,
|
|
3027
3853
|
closeTemporaryIdentity,
|
|
3028
|
-
|
|
3854
|
+
sessionReaperIntervalMs,
|
|
3855
|
+
reapStaleTemporaryIdentities,
|
|
3029
3856
|
envelopeDispatch,
|
|
3030
3857
|
PROTOCOL_VERSION,
|
|
3031
3858
|
clusterSweep,
|