@claudexor/journal 3.3.16 → 3.4.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.
@@ -0,0 +1,78 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { closeSync, constants, openSync, renameSync } from "node:fs";
3
+ import { dirname } from "node:path";
4
+ import { gzipSync } from "node:zlib";
5
+ import { fsyncDirectory } from "@claudexor/util";
6
+ import { encodeJournalPayload } from "./append-batch.js";
7
+ import { COMPACTED_SNAPSHOT, HASH_BYTES, MAX_PAYLOAD_BYTES, ZERO_HASH, encodeFrame, } from "./frame-codec.js";
8
+ import { appendAndSync } from "./journal-files.js";
9
+ export function compactJournalFile(input) {
10
+ if (input.entries.length === 0)
11
+ return null;
12
+ const logical = input.entries.map((record) => ({
13
+ time: record.time,
14
+ type: record.type,
15
+ payload: cloneJson(record.payload),
16
+ }));
17
+ const compressed = gzipSync(Buffer.from(JSON.stringify(logical)));
18
+ const payload = {
19
+ version: 1,
20
+ count: logical.length,
21
+ encoding: "gzip-base64",
22
+ data: compressed.toString("base64"),
23
+ };
24
+ const payloadBytes = encodeJournalPayload(payload);
25
+ if (payloadBytes.length > MAX_PAYLOAD_BYTES)
26
+ return null;
27
+ const epoch = randomUUID();
28
+ const header = {
29
+ partition: input.partition,
30
+ epoch,
31
+ seq: 1,
32
+ previousFrameHash: ZERO_HASH,
33
+ time: input.now().toISOString(),
34
+ type: COMPACTED_SNAPSHOT,
35
+ logicalSpan: logical.length,
36
+ };
37
+ const frame = encodeFrame(header, payloadBytes);
38
+ if (frame.length >= input.knownFileBytes)
39
+ return null;
40
+ const temp = `${input.path}.${randomUUID()}.compact`;
41
+ const tempFd = openSync(temp, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, 0o600);
42
+ try {
43
+ appendAndSync(tempFd, frame);
44
+ }
45
+ finally {
46
+ closeSync(tempFd);
47
+ }
48
+ renameSync(temp, input.path);
49
+ fsyncDirectory(dirname(input.path));
50
+ const frameHash = frame.subarray(frame.length - HASH_BYTES).toString("hex");
51
+ const records = logical.map((record, index) => ({
52
+ partition: input.partition,
53
+ epoch,
54
+ seq: index + 1,
55
+ previousFrameHash: index === 0 ? ZERO_HASH : frameHash,
56
+ frameHash,
57
+ time: record.time,
58
+ type: record.type,
59
+ payload: cloneJson(record.payload),
60
+ byteOffset: 0,
61
+ }));
62
+ return {
63
+ receipt: {
64
+ beforeBytes: input.knownFileBytes,
65
+ afterBytes: frame.length,
66
+ records: logical.length,
67
+ },
68
+ records,
69
+ epoch,
70
+ nextSeq: logical.length + 1,
71
+ previousFrameHash: frameHash,
72
+ knownFileBytes: frame.length,
73
+ };
74
+ }
75
+ function cloneJson(value) {
76
+ return JSON.parse(JSON.stringify(value));
77
+ }
78
+ //# sourceMappingURL=journal-compaction.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"journal-compaction.js","sourceRoot":"","sources":["../src/journal-compaction.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AACrC,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,EACL,kBAAkB,EAClB,UAAU,EACV,iBAAiB,EACjB,SAAS,EACT,WAAW,GAKZ,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAWnD,MAAM,UAAU,kBAAkB,CAAC,KAMlC;IACC,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAC5C,MAAM,OAAO,GAAsB,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;QAChE,IAAI,EAAE,MAAM,CAAC,IAAI;QACjB,IAAI,EAAE,MAAM,CAAC,IAAI;QACjB,OAAO,EAAE,SAAS,CAAC,MAAM,CAAC,OAAO,CAAC;KACnC,CAAC,CAAC,CAAC;IACJ,MAAM,UAAU,GAAG,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;IAClE,MAAM,OAAO,GAA6B;QACxC,OAAO,EAAE,CAAC;QACV,KAAK,EAAE,OAAO,CAAC,MAAM;QACrB,QAAQ,EAAE,aAAa;QACvB,IAAI,EAAE,UAAU,CAAC,QAAQ,CAAC,QAAQ,CAAC;KACpC,CAAC;IACF,MAAM,YAAY,GAAG,oBAAoB,CAAC,OAAO,CAAC,CAAC;IACnD,IAAI,YAAY,CAAC,MAAM,GAAG,iBAAiB;QAAE,OAAO,IAAI,CAAC;IACzD,MAAM,KAAK,GAAG,UAAU,EAAE,CAAC;IAC3B,MAAM,MAAM,GAAgB;QAC1B,SAAS,EAAE,KAAK,CAAC,SAAS;QAC1B,KAAK;QACL,GAAG,EAAE,CAAC;QACN,iBAAiB,EAAE,SAAS;QAC5B,IAAI,EAAE,KAAK,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE;QAC/B,IAAI,EAAE,kBAAkB;QACxB,WAAW,EAAE,OAAO,CAAC,MAAM;KAC5B,CAAC;IACF,MAAM,KAAK,GAAG,WAAW,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IAChD,IAAI,KAAK,CAAC,MAAM,IAAI,KAAK,CAAC,cAAc;QAAE,OAAO,IAAI,CAAC;IACtD,MAAM,IAAI,GAAG,GAAG,KAAK,CAAC,IAAI,IAAI,UAAU,EAAE,UAAU,CAAC;IACrD,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC,QAAQ,GAAG,SAAS,CAAC,OAAO,GAAG,SAAS,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IAChG,IAAI,CAAC;QACH,aAAa,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IAC/B,CAAC;YAAS,CAAC;QACT,SAAS,CAAC,MAAM,CAAC,CAAC;IACpB,CAAC;IACD,UAAU,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;IAC7B,cAAc,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;IACpC,MAAM,SAAS,GAAG,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,GAAG,UAAU,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IAC5E,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC;QAC9C,SAAS,EAAE,KAAK,CAAC,SAAS;QAC1B,KAAK;QACL,GAAG,EAAE,KAAK,GAAG,CAAC;QACd,iBAAiB,EAAE,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS;QACtD,SAAS;QACT,IAAI,EAAE,MAAM,CAAC,IAAI;QACjB,IAAI,EAAE,MAAM,CAAC,IAAI;QACjB,OAAO,EAAE,SAAS,CAAC,MAAM,CAAC,OAAO,CAAC;QAClC,UAAU,EAAE,CAAC;KACd,CAAC,CAAC,CAAC;IACJ,OAAO;QACL,OAAO,EAAE;YACP,WAAW,EAAE,KAAK,CAAC,cAAc;YACjC,UAAU,EAAE,KAAK,CAAC,MAAM;YACxB,OAAO,EAAE,OAAO,CAAC,MAAM;SACxB;QACD,OAAO;QACP,KAAK;QACL,OAAO,EAAE,OAAO,CAAC,MAAM,GAAG,CAAC;QAC3B,iBAAiB,EAAE,SAAS;QAC5B,cAAc,EAAE,KAAK,CAAC,MAAM;KAC7B,CAAC;AACJ,CAAC;AAED,SAAS,SAAS,CAAI,KAAQ;IAC5B,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAM,CAAC;AAChD,CAAC"}
@@ -0,0 +1,9 @@
1
+ export declare class JournalCursorError extends Error {
2
+ readonly code = "journal_cursor_invalid";
3
+ readonly status = 409;
4
+ readonly retryable = true;
5
+ readonly requiredActions: string[];
6
+ }
7
+ export declare function encodeCursor(partition: string, epoch: string, seq: number): string;
8
+ export declare function cursorError(detail: string): JournalCursorError;
9
+ //# sourceMappingURL=journal-cursor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"journal-cursor.d.ts","sourceRoot":"","sources":["../src/journal-cursor.ts"],"names":[],"mappings":"AAAA,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,QAAQ,CAAC,IAAI,4BAA4B;IACzC,QAAQ,CAAC,MAAM,OAAO;IACtB,QAAQ,CAAC,SAAS,QAAQ;IAC1B,QAAQ,CAAC,eAAe,WAAkB;CAC3C;AAED,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAIlF;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,kBAAkB,CAE9D"}
@@ -0,0 +1,13 @@
1
+ export class JournalCursorError extends Error {
2
+ code = "journal_cursor_invalid";
3
+ status = 409;
4
+ retryable = true;
5
+ requiredActions = ["resnapshot"];
6
+ }
7
+ export function encodeCursor(partition, epoch, seq) {
8
+ return Buffer.from(JSON.stringify({ v: 1, p: partition, e: epoch, s: seq })).toString("base64url");
9
+ }
10
+ export function cursorError(detail) {
11
+ return new JournalCursorError(`journal cursor is ${detail}; resnapshot is required`);
12
+ }
13
+ //# sourceMappingURL=journal-cursor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"journal-cursor.js","sourceRoot":"","sources":["../src/journal-cursor.ts"],"names":[],"mappings":"AAAA,MAAM,OAAO,kBAAmB,SAAQ,KAAK;IAClC,IAAI,GAAG,wBAAwB,CAAC;IAChC,MAAM,GAAG,GAAG,CAAC;IACb,SAAS,GAAG,IAAI,CAAC;IACjB,eAAe,GAAG,CAAC,YAAY,CAAC,CAAC;CAC3C;AAED,MAAM,UAAU,YAAY,CAAC,SAAiB,EAAE,KAAa,EAAE,GAAW;IACxE,OAAO,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,QAAQ,CACnF,WAAW,CACZ,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,MAAc;IACxC,OAAO,IAAI,kBAAkB,CAAC,qBAAqB,MAAM,0BAA0B,CAAC,CAAC;AACvF,CAAC"}
@@ -0,0 +1,12 @@
1
+ export interface AppendIntent {
2
+ v: 1;
3
+ offset: number;
4
+ length: number;
5
+ }
6
+ export declare function appendAndSync(fd: number, bytes: Buffer): void;
7
+ export declare function ensurePrivateFile(path: string): void;
8
+ export declare function readDescriptor(fd: number): Buffer;
9
+ export declare function readIntent(path: string): AppendIntent | null;
10
+ export declare function writeIntent(path: string, value: AppendIntent): void;
11
+ export declare function removeFile(path: string): void;
12
+ //# sourceMappingURL=journal-files.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"journal-files.d.ts","sourceRoot":"","sources":["../src/journal-files.ts"],"names":[],"mappings":"AAiBA,MAAM,WAAW,YAAY;IAC3B,CAAC,EAAE,CAAC,CAAC;IACL,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAI7D;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAcpD;AAED,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAMjD;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CAqB5D;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,GAAG,IAAI,CAanE;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAI7C"}
@@ -0,0 +1,84 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { closeSync, constants, existsSync, fstatSync, fsyncSync, lstatSync, openSync, readFileSync, renameSync, rmSync, writeSync, } from "node:fs";
3
+ import { dirname } from "node:path";
4
+ import { fsyncDirectory } from "@claudexor/util";
5
+ export function appendAndSync(fd, bytes) {
6
+ let offset = 0;
7
+ while (offset < bytes.length)
8
+ offset += writeSync(fd, bytes, offset, bytes.length - offset);
9
+ fsyncSync(fd);
10
+ }
11
+ export function ensurePrivateFile(path) {
12
+ if (!existsSync(path)) {
13
+ const fd = openSync(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, 0o600);
14
+ try {
15
+ fsyncSync(fd);
16
+ }
17
+ finally {
18
+ closeSync(fd);
19
+ }
20
+ fsyncDirectory(dirname(path));
21
+ }
22
+ const stat = lstatSync(path);
23
+ if (stat.isSymbolicLink() || !stat.isFile() || stat.nlink !== 1) {
24
+ throw new Error("journal path is not a private regular file");
25
+ }
26
+ }
27
+ export function readDescriptor(fd) {
28
+ const size = Number(fstatSync(fd, { bigint: true }).size);
29
+ if (!Number.isSafeInteger(size) || size < 0)
30
+ throw new Error("journal file size is invalid");
31
+ const bytes = readFileSync(fd);
32
+ if (bytes.length !== size)
33
+ throw new Error("journal changed while being read");
34
+ return bytes;
35
+ }
36
+ export function readIntent(path) {
37
+ if (!existsSync(path))
38
+ return null;
39
+ const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
40
+ try {
41
+ const stat = fstatSync(fd);
42
+ if (!stat.isFile() || stat.nlink !== 1 || stat.size > 1024)
43
+ throw new Error("unsafe file");
44
+ const value = JSON.parse(readFileSync(fd, "utf8"));
45
+ if (!isRecord(value) ||
46
+ value.v !== 1 ||
47
+ !Number.isSafeInteger(value.offset) ||
48
+ Number(value.offset) < 0 ||
49
+ !Number.isSafeInteger(value.length) ||
50
+ Number(value.length) <= 0) {
51
+ throw new Error("invalid shape");
52
+ }
53
+ return value;
54
+ }
55
+ finally {
56
+ closeSync(fd);
57
+ }
58
+ }
59
+ export function writeIntent(path, value) {
60
+ const temp = `${path}.${randomUUID()}.tmp`;
61
+ const bytes = Buffer.from(`${JSON.stringify(value)}\n`);
62
+ const fd = openSync(temp, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, 0o600);
63
+ try {
64
+ let offset = 0;
65
+ while (offset < bytes.length)
66
+ offset += writeSync(fd, bytes, offset, bytes.length - offset);
67
+ fsyncSync(fd);
68
+ }
69
+ finally {
70
+ closeSync(fd);
71
+ }
72
+ renameSync(temp, path);
73
+ fsyncDirectory(dirname(path));
74
+ }
75
+ export function removeFile(path) {
76
+ if (!existsSync(path))
77
+ return;
78
+ rmSync(path);
79
+ fsyncDirectory(dirname(path));
80
+ }
81
+ function isRecord(value) {
82
+ return typeof value === "object" && value !== null && !Array.isArray(value);
83
+ }
84
+ //# sourceMappingURL=journal-files.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"journal-files.js","sourceRoot":"","sources":["../src/journal-files.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export declare function journalPartitionDirectory(rootDir: string, partition: string): string;
2
+ //# sourceMappingURL=journal-partition.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"journal-partition.d.ts","sourceRoot":"","sources":["../src/journal-partition.ts"],"names":[],"mappings":"AAGA,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAKpF"}
@@ -0,0 +1,10 @@
1
+ import { createHash } from "node:crypto";
2
+ import { join } from "node:path";
3
+ export function journalPartitionDirectory(rootDir, partition) {
4
+ if (!partition.trim())
5
+ throw new Error("journal partition must not be empty");
6
+ const slug = partition.replace(/[^A-Za-z0-9._-]+/g, "-").slice(0, 48) || "partition";
7
+ const digest = createHash("sha256").update(Buffer.from(partition)).digest("hex");
8
+ return join(rootDir, `${slug}-${digest.slice(0, 12)}`);
9
+ }
10
+ //# sourceMappingURL=journal-partition.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"journal-partition.js","sourceRoot":"","sources":["../src/journal-partition.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,MAAM,UAAU,yBAAyB,CAAC,OAAe,EAAE,SAAiB;IAC1E,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE;QAAE,MAAM,IAAI,KAAK,CAAC,qCAAqC,CAAC,CAAC;IAC9E,MAAM,IAAI,GAAG,SAAS,CAAC,OAAO,CAAC,mBAAmB,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,WAAW,CAAC;IACrF,MAAM,MAAM,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACjF,OAAO,IAAI,CAAC,OAAO,EAAE,GAAG,IAAI,IAAI,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC;AACzD,CAAC"}
@@ -0,0 +1,41 @@
1
+ import { type JournalRecord } from "./frame-codec.js";
2
+ export interface JournalPreparationReceipt {
3
+ fingerprint: string;
4
+ preparationIdentity: string;
5
+ virtual: boolean;
6
+ deferredRepair: null | {
7
+ kind: "discard_unacknowledged_append";
8
+ discardedBytes: number;
9
+ };
10
+ }
11
+ export type PreparedJournalRecovery = {
12
+ status: "ready";
13
+ discardedTailBytes: number;
14
+ } | {
15
+ status: "recovery_required";
16
+ location: {
17
+ kind: "byte";
18
+ byteOffset: number;
19
+ };
20
+ reason: string;
21
+ discardedTailBytes: number;
22
+ };
23
+ export interface PreparedJournalInspection {
24
+ receipt: JournalPreparationReceipt;
25
+ recovery: PreparedJournalRecovery;
26
+ records: JournalRecord[];
27
+ epoch: string;
28
+ nextSeq: number;
29
+ previousFrameHash: string;
30
+ knownFileBytes: number;
31
+ }
32
+ export declare function inspectPreparedJournal(input: {
33
+ rootDir: string;
34
+ partitionDir: string;
35
+ journalPath: string;
36
+ intentPath: string;
37
+ partition: string;
38
+ initialEpoch: string;
39
+ }): PreparedJournalInspection;
40
+ export declare function fingerprintPreparedJournal(rootDir: string, partitionDir: string): Pick<JournalPreparationReceipt, "fingerprint" | "preparationIdentity">;
41
+ //# sourceMappingURL=read-only-preparation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"read-only-preparation.d.ts","sourceRoot":"","sources":["../src/read-only-preparation.ts"],"names":[],"mappings":"AAcA,OAAO,EAA2B,KAAK,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAE/E,MAAM,WAAW,yBAAyB;IACxC,WAAW,EAAE,MAAM,CAAC;IACpB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,OAAO,EAAE,OAAO,CAAC;IACjB,cAAc,EAAE,IAAI,GAAG;QACrB,IAAI,EAAE,+BAA+B,CAAC;QACtC,cAAc,EAAE,MAAM,CAAC;KACxB,CAAC;CACH;AAED,MAAM,MAAM,uBAAuB,GAC/B;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,kBAAkB,EAAE,MAAM,CAAA;CAAE,GAC/C;IACE,MAAM,EAAE,mBAAmB,CAAC;IAC5B,QAAQ,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC;IAC/C,MAAM,EAAE,MAAM,CAAC;IACf,kBAAkB,EAAE,MAAM,CAAC;CAC5B,CAAC;AAEN,MAAM,WAAW,yBAAyB;IACxC,OAAO,EAAE,yBAAyB,CAAC;IACnC,QAAQ,EAAE,uBAAuB,CAAC;IAClC,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,cAAc,EAAE,MAAM,CAAC;CACxB;AAkBD,wBAAgB,sBAAsB,CAAC,KAAK,EAAE;IAC5C,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;CACtB,GAAG,yBAAyB,CAgF5B;AAED,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,MAAM,GACnB,IAAI,CAAC,yBAAyB,EAAE,aAAa,GAAG,qBAAqB,CAAC,CAMxE"}
@@ -0,0 +1,366 @@
1
+ import { createHash } from "node:crypto";
2
+ import { closeSync, constants, fstatSync, lstatSync, openSync, readFileSync, readdirSync, readlinkSync, realpathSync, } from "node:fs";
3
+ import { dirname, resolve } from "node:path";
4
+ import { ZERO_HASH, replayFrames } from "./frame-codec.js";
5
+ export function inspectPreparedJournal(input) {
6
+ const tree = snapshotTree(input.rootDir, input.partitionDir);
7
+ const journalBytes = tree.files.get(input.journalPath);
8
+ const intentBytes = tree.files.get(input.intentPath);
9
+ const unexpected = [...tree.entries].filter((path) => path !== input.journalPath && path !== input.intentPath);
10
+ let problem = tree.problems[0] ?? null;
11
+ let byteOffset = 0;
12
+ let deferredRepair = null;
13
+ if (!problem &&
14
+ journalBytes === undefined &&
15
+ (intentBytes !== undefined || tree.partitionExists)) {
16
+ if (intentBytes !== undefined || unexpected.length > 0) {
17
+ problem = "journal file is missing while partition state exists";
18
+ }
19
+ }
20
+ let logicalBytes = journalBytes ?? Buffer.alloc(0);
21
+ if (!problem && intentBytes !== undefined) {
22
+ try {
23
+ const intent = parseIntent(intentBytes);
24
+ const prefix = replayFrames(logicalBytes.subarray(0, intent.offset), input.partition);
25
+ if (intent.offset > logicalBytes.length ||
26
+ logicalBytes.length > intent.offset + intent.length ||
27
+ prefix.error ||
28
+ prefix.incompleteOffset !== null) {
29
+ problem = "append intent does not match the journal prefix";
30
+ byteOffset = intent.offset;
31
+ }
32
+ else {
33
+ deferredRepair = {
34
+ kind: "discard_unacknowledged_append",
35
+ discardedBytes: logicalBytes.length - intent.offset,
36
+ };
37
+ logicalBytes = logicalBytes.subarray(0, intent.offset);
38
+ }
39
+ }
40
+ catch (error) {
41
+ problem = `append intent is malformed: ${safeMessage(error)}`;
42
+ }
43
+ }
44
+ const decoded = replayFrames(logicalBytes, input.partition);
45
+ if (!problem && decoded.incompleteOffset !== null) {
46
+ problem = "unexplained suffix without append intent";
47
+ byteOffset = decoded.incompleteOffset;
48
+ }
49
+ if (!problem && decoded.error) {
50
+ problem = decoded.error.reason;
51
+ byteOffset = decoded.error.offset;
52
+ }
53
+ const records = problem ? [] : decoded.records;
54
+ const last = records.at(-1);
55
+ const virtual = !tree.rootExists || !tree.partitionExists || journalBytes === undefined;
56
+ return {
57
+ receipt: {
58
+ fingerprint: tree.fingerprint,
59
+ preparationIdentity: tree.preparationIdentity,
60
+ virtual,
61
+ deferredRepair,
62
+ },
63
+ recovery: problem
64
+ ? {
65
+ status: "recovery_required",
66
+ location: { kind: "byte", byteOffset },
67
+ reason: problem,
68
+ discardedTailBytes: 0,
69
+ }
70
+ : { status: "ready", discardedTailBytes: 0 },
71
+ records,
72
+ epoch: last?.epoch ?? input.initialEpoch,
73
+ nextSeq: (last?.seq ?? 0) + 1,
74
+ previousFrameHash: last?.frameHash ?? ZERO_HASH,
75
+ knownFileBytes: logicalBytes.length,
76
+ };
77
+ }
78
+ export function fingerprintPreparedJournal(rootDir, partitionDir) {
79
+ const observed = snapshotTree(rootDir, partitionDir);
80
+ return {
81
+ fingerprint: observed.fingerprint,
82
+ preparationIdentity: observed.preparationIdentity,
83
+ };
84
+ }
85
+ function snapshotTree(rootDir, partitionDir) {
86
+ const contentHash = createHash("sha256");
87
+ const identityHash = createHash("sha256");
88
+ const files = new Map();
89
+ const entries = new Set();
90
+ const problems = [];
91
+ if (!inspectTrustedParent(dirname(resolve(rootDir)), identityHash, problems)) {
92
+ contentHash.update("root\0unreachable\0");
93
+ return finishSnapshot({
94
+ contentHash,
95
+ identityHash,
96
+ rootExists: false,
97
+ partitionExists: false,
98
+ entries,
99
+ files,
100
+ problems,
101
+ });
102
+ }
103
+ const root = inspectEntry(rootDir, "root", contentHash, identityHash, files, problems, false);
104
+ if (root.kind !== "directory") {
105
+ contentHash.update("partition\0unreachable\0");
106
+ identityHash.update("partition\0unreachable\0");
107
+ return finishSnapshot({
108
+ contentHash,
109
+ identityHash,
110
+ rootExists: root.kind !== "missing",
111
+ partitionExists: false,
112
+ entries,
113
+ files,
114
+ problems,
115
+ });
116
+ }
117
+ const partition = inspectEntry(partitionDir, "partition", contentHash, identityHash, files, problems, false);
118
+ if (partition.kind === "directory") {
119
+ walkPartition(partitionDir, partitionDir, partition.stat, contentHash, identityHash, files, entries, problems);
120
+ }
121
+ assertDirectoryUnchanged(rootDir, root.stat, problems, "root");
122
+ return finishSnapshot({
123
+ contentHash,
124
+ identityHash,
125
+ rootExists: true,
126
+ partitionExists: partition.kind !== "missing",
127
+ entries,
128
+ files,
129
+ problems,
130
+ });
131
+ }
132
+ function walkPartition(path, partitionDir, before, contentHash, identityHash, files, entries, problems) {
133
+ let names;
134
+ try {
135
+ names = readdirSync(path).sort();
136
+ }
137
+ catch (error) {
138
+ problems.push(`journal partition cannot be listed: ${safeMessage(error)}`);
139
+ contentHash.update(`list-error\0${safeMessage(error)}\0`);
140
+ identityHash.update(`list-error\0${safeMessage(error)}\0`);
141
+ return;
142
+ }
143
+ for (const name of names) {
144
+ const child = `${path}/${name}`;
145
+ const relative = child.slice(partitionDir.length + 1);
146
+ entries.add(child);
147
+ const inspected = inspectEntry(child, relative, contentHash, identityHash, files, problems, true);
148
+ if (inspected.kind === "directory") {
149
+ walkPartition(child, partitionDir, inspected.stat, contentHash, identityHash, files, entries, problems);
150
+ }
151
+ }
152
+ try {
153
+ if (readdirSync(path).sort().join("\0") !== names.join("\0")) {
154
+ problems.push(`journal directory entries changed while being read: ${path}`);
155
+ }
156
+ }
157
+ catch (error) {
158
+ problems.push(`journal partition cannot be relisted: ${safeMessage(error)}`);
159
+ }
160
+ assertDirectoryUnchanged(path, before, problems, path);
161
+ }
162
+ function inspectEntry(path, label, contentHash, identityHash, files, problems, allowFile) {
163
+ let stat;
164
+ try {
165
+ stat = lstatSync(path, { bigint: true });
166
+ }
167
+ catch (error) {
168
+ if (errorCode(error) === "ENOENT") {
169
+ contentHash.update(`${label}\0missing\0`);
170
+ identityHash.update(`${label}\0missing\0`);
171
+ assertStillMissing(path, problems, label);
172
+ return { kind: "missing" };
173
+ }
174
+ problems.push(`${label} cannot be inspected: ${safeMessage(error)}`);
175
+ contentHash.update(`${label}\0error\0${safeMessage(error)}\0`);
176
+ identityHash.update(`${label}\0error\0${safeMessage(error)}\0`);
177
+ return { kind: "other" };
178
+ }
179
+ contentHash.update(`${label}\0${semanticMetadata(stat)}\0`);
180
+ identityHash.update(`${label}\0${identityMetadata(stat)}\0`);
181
+ if (stat.isSymbolicLink()) {
182
+ try {
183
+ const target = readlinkSync(path);
184
+ contentHash.update(`symlink\0${target}\0`);
185
+ identityHash.update(`symlink\0${target}\0`);
186
+ }
187
+ catch (error) {
188
+ problems.push(`${label} symbolic-link target cannot be read: ${safeMessage(error)}`);
189
+ }
190
+ problems.push(`${label} is a symbolic link`);
191
+ return { kind: "other" };
192
+ }
193
+ if (typeof process.getuid === "function" && stat.uid !== BigInt(process.getuid())) {
194
+ problems.push(`${label} is not owned by the current user`);
195
+ }
196
+ if (stat.isDirectory()) {
197
+ let safe = true;
198
+ try {
199
+ if (realpathSync.native(path) !== resolve(path)) {
200
+ problems.push(`${label} is not canonical`);
201
+ safe = false;
202
+ }
203
+ }
204
+ catch (error) {
205
+ problems.push(`${label} cannot be resolved canonically: ${safeMessage(error)}`);
206
+ safe = false;
207
+ }
208
+ if (process.platform !== "win32" && Number(stat.mode & 511n) !== 0o700) {
209
+ problems.push(`${label} is not private`);
210
+ safe = false;
211
+ }
212
+ if (typeof process.getuid === "function" && stat.uid !== BigInt(process.getuid()))
213
+ safe = false;
214
+ return safe ? { kind: "directory", stat } : { kind: "other" };
215
+ }
216
+ if (!allowFile || !stat.isFile() || stat.nlink !== 1n) {
217
+ problems.push(`${label} is not a private regular file`);
218
+ return { kind: "other" };
219
+ }
220
+ if (process.platform !== "win32" && Number(stat.mode & 511n) !== 0o600) {
221
+ problems.push(`${label} is not private`);
222
+ }
223
+ try {
224
+ const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
225
+ try {
226
+ const opened = fstatSync(fd, { bigint: true });
227
+ if (opened.dev !== stat.dev ||
228
+ opened.ino !== stat.ino ||
229
+ !opened.isFile() ||
230
+ observationMetadata(opened) !== observationMetadata(stat)) {
231
+ throw new Error("pathname identity changed while being read");
232
+ }
233
+ const bytes = readFileSync(fd);
234
+ const after = fstatSync(fd, { bigint: true });
235
+ if (BigInt(bytes.length) !== opened.size ||
236
+ observationMetadata(opened) !== observationMetadata(after)) {
237
+ throw new Error("file changed while being read");
238
+ }
239
+ const namedAfter = lstatSync(path, { bigint: true });
240
+ if (observationMetadata(stat) !== observationMetadata(namedAfter)) {
241
+ throw new Error("pathname identity changed after being read");
242
+ }
243
+ files.set(path, bytes);
244
+ contentHash.update(bytes);
245
+ }
246
+ finally {
247
+ closeSync(fd);
248
+ }
249
+ }
250
+ catch (error) {
251
+ problems.push(`${label} cannot be read safely: ${safeMessage(error)}`);
252
+ contentHash.update(`read-error\0${safeMessage(error)}\0`);
253
+ identityHash.update(`read-error\0${safeMessage(error)}\0`);
254
+ }
255
+ return { kind: "file", stat };
256
+ }
257
+ function finishSnapshot(input) {
258
+ const fingerprint = input.contentHash.digest("hex");
259
+ input.identityHash.update(`content\0${fingerprint}\0`);
260
+ return {
261
+ fingerprint,
262
+ preparationIdentity: input.identityHash.digest("hex"),
263
+ rootExists: input.rootExists,
264
+ partitionExists: input.partitionExists,
265
+ entries: input.entries,
266
+ files: input.files,
267
+ problems: input.problems,
268
+ };
269
+ }
270
+ function inspectTrustedParent(path, identityHash, problems) {
271
+ try {
272
+ const before = lstatSync(path, { bigint: true });
273
+ identityHash.update(`trusted-parent\0${identityMetadata(before)}\0`);
274
+ if (before.isSymbolicLink() ||
275
+ !before.isDirectory() ||
276
+ realpathSync.native(path) !== path ||
277
+ (typeof process.getuid === "function" && before.uid !== BigInt(process.getuid())) ||
278
+ (process.platform !== "win32" && Number(before.mode & 511n) !== 0o700)) {
279
+ problems.push("journal root parent is not canonical and private");
280
+ return false;
281
+ }
282
+ const after = lstatSync(path, { bigint: true });
283
+ if (observationMetadata(before) !== observationMetadata(after)) {
284
+ problems.push("journal root parent changed while being inspected");
285
+ return false;
286
+ }
287
+ return true;
288
+ }
289
+ catch (error) {
290
+ problems.push(`journal root parent cannot be inspected: ${safeMessage(error)}`);
291
+ identityHash.update(`trusted-parent-error\0${safeMessage(error)}\0`);
292
+ return false;
293
+ }
294
+ }
295
+ function assertDirectoryUnchanged(path, before, problems, label) {
296
+ try {
297
+ const after = lstatSync(path, { bigint: true });
298
+ if (observationMetadata(before) !== observationMetadata(after)) {
299
+ problems.push(`${label} changed while being inspected`);
300
+ }
301
+ }
302
+ catch (error) {
303
+ problems.push(`${label} became unavailable while being inspected: ${safeMessage(error)}`);
304
+ }
305
+ }
306
+ function assertStillMissing(path, problems, label) {
307
+ try {
308
+ lstatSync(path);
309
+ problems.push(`${label} appeared while being inspected`);
310
+ }
311
+ catch (error) {
312
+ if (errorCode(error) !== "ENOENT") {
313
+ problems.push(`${label} missing state is ambiguous: ${safeMessage(error)}`);
314
+ }
315
+ }
316
+ }
317
+ function semanticMetadata(stat) {
318
+ const base = [entryType(stat), stat.mode & 511n, stat.uid, stat.gid];
319
+ if (!stat.isDirectory())
320
+ base.push(stat.nlink, stat.size);
321
+ return base.join(":");
322
+ }
323
+ function identityMetadata(stat) {
324
+ const base = [stat.dev, stat.ino, entryType(stat), stat.mode & 511n, stat.uid, stat.gid];
325
+ if (!stat.isDirectory())
326
+ base.push(stat.nlink);
327
+ return base.join(":");
328
+ }
329
+ function observationMetadata(stat) {
330
+ return [identityMetadata(stat), stat.size, stat.mtimeNs, stat.ctimeNs].join(":");
331
+ }
332
+ function entryType(stat) {
333
+ if (stat.isDirectory())
334
+ return "directory";
335
+ if (stat.isFile())
336
+ return "file";
337
+ if (stat.isSymbolicLink())
338
+ return "symlink";
339
+ return "other";
340
+ }
341
+ function parseIntent(bytes) {
342
+ if (bytes.length > 1024)
343
+ throw new Error("unsafe file");
344
+ const value = JSON.parse(bytes.toString("utf8"));
345
+ if (!isRecord(value) ||
346
+ value.v !== 1 ||
347
+ !Number.isSafeInteger(value.offset) ||
348
+ Number(value.offset) < 0 ||
349
+ !Number.isSafeInteger(value.length) ||
350
+ Number(value.length) <= 0) {
351
+ throw new Error("invalid shape");
352
+ }
353
+ return value;
354
+ }
355
+ function errorCode(error) {
356
+ return error && typeof error === "object" && "code" in error
357
+ ? String(error.code)
358
+ : undefined;
359
+ }
360
+ function safeMessage(error) {
361
+ return error instanceof Error ? error.message : String(error);
362
+ }
363
+ function isRecord(value) {
364
+ return typeof value === "object" && value !== null && !Array.isArray(value);
365
+ }
366
+ //# sourceMappingURL=read-only-preparation.js.map