trajectories 0.0.0 → 0.1.2
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/CHANGELOG.md +36 -0
- package/LICENSE +201 -0
- package/README.md +288 -2
- package/dist/cli.js +30210 -0
- package/dist/convert.d.ts +31 -0
- package/dist/convert.js +431 -0
- package/dist/deepagents_checkpoint.py +362 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +8402 -0
- package/dist/log-baseline.d.ts +26 -0
- package/dist/log-baseline.js +104 -0
- package/dist/log-content.d.ts +16 -0
- package/dist/log-content.js +80 -0
- package/dist/log-io.d.ts +10 -0
- package/dist/log-io.js +121 -0
- package/dist/log-read.d.ts +24 -0
- package/dist/log-read.js +117 -0
- package/dist/log-reconcile.d.ts +5 -0
- package/dist/log-reconcile.js +38 -0
- package/dist/log-records.d.ts +35 -0
- package/dist/log-records.js +85 -0
- package/dist/log-recovery.d.ts +18 -0
- package/dist/log-recovery.js +123 -0
- package/dist/log-stage.d.ts +14 -0
- package/dist/log-stage.js +138 -0
- package/dist/log-state.d.ts +89 -0
- package/dist/log-state.js +92 -0
- package/dist/log-switch.d.ts +3 -0
- package/dist/log-switch.js +98 -0
- package/dist/log-types.d.ts +129 -0
- package/dist/log-types.js +8 -0
- package/dist/log.d.ts +8 -0
- package/dist/log.js +62 -0
- package/dist/migrate.d.ts +29 -0
- package/dist/migrate.js +173 -0
- package/dist/normalization-runtime.d.ts +1 -0
- package/dist/normalization-runtime.js +3150 -0
- package/dist/portable.d.ts +9 -0
- package/dist/portable.js +26 -0
- package/dist/readers/canonical.d.ts +13 -0
- package/dist/readers/canonical.js +54 -0
- package/dist/readers/cline.d.ts +10 -0
- package/dist/readers/cline.js +107 -0
- package/dist/readers/cursor.d.ts +20 -0
- package/dist/readers/cursor.js +374 -0
- package/dist/readers/deepagents.d.ts +41 -0
- package/dist/readers/deepagents.js +163 -0
- package/dist/readers/fx.d.ts +11 -0
- package/dist/readers/fx.js +573 -0
- package/dist/readers/github-copilot.d.ts +8 -0
- package/dist/readers/github-copilot.js +216 -0
- package/dist/readers/grok-build.d.ts +12 -0
- package/dist/readers/grok-build.js +391 -0
- package/dist/readers/guards.d.ts +4 -0
- package/dist/readers/guards.js +121 -0
- package/dist/readers/pi-compaction.d.ts +7 -0
- package/dist/readers/pi-compaction.js +112 -0
- package/dist/readers/pi-context.d.ts +13 -0
- package/dist/readers/pi-context.js +64 -0
- package/dist/store.d.ts +25 -0
- package/dist/store.js +218 -0
- package/dist/types.d.ts +91 -0
- package/dist/types.js +1 -0
- package/dist/writers/claude-code.d.ts +5 -0
- package/dist/writers/claude-code.js +51 -0
- package/dist/writers/cline.d.ts +3 -0
- package/dist/writers/cline.js +77 -0
- package/dist/writers/codex.d.ts +3 -0
- package/dist/writers/codex.js +32 -0
- package/dist/writers/cursor.d.ts +3 -0
- package/dist/writers/cursor.js +197 -0
- package/dist/writers/deepagents.d.ts +3 -0
- package/dist/writers/deepagents.js +116 -0
- package/dist/writers/entropy.d.ts +5 -0
- package/dist/writers/entropy.js +33 -0
- package/dist/writers/fx.d.ts +3 -0
- package/dist/writers/fx.js +135 -0
- package/dist/writers/github-copilot.d.ts +3 -0
- package/dist/writers/github-copilot.js +71 -0
- package/dist/writers/grok-build.d.ts +3 -0
- package/dist/writers/grok-build.js +43 -0
- package/dist/writers/helpers.d.ts +17 -0
- package/dist/writers/helpers.js +75 -0
- package/dist/writers/omp.d.ts +3 -0
- package/dist/writers/omp.js +7 -0
- package/dist/writers/opencode.d.ts +3 -0
- package/dist/writers/opencode.js +65 -0
- package/dist/writers/pi.d.ts +3 -0
- package/dist/writers/pi.js +45 -0
- package/docs/cline-native-format.md +43 -0
- package/docs/copilot-native-format.md +78 -0
- package/docs/cursor-native-format.md +103 -0
- package/docs/deepagents-native-format.md +98 -0
- package/docs/evidence-api.md +170 -0
- package/docs/fx-native-format.md +87 -0
- package/docs/harness-parity-bootstrap.md +78 -0
- package/docs/harness-support-parity.md +107 -0
- package/docs/inspirations.md +103 -0
- package/docs/ledger-migration.md +141 -0
- package/docs/pi-compaction-context.md +75 -0
- package/docs/pi-provider-context.md +76 -0
- package/docs/research/2026-10-06-huihua-and-oar.md +176 -0
- package/docs/research/ledger/README.md +96 -0
- package/docs/research/ledger/confer/2026-09-13-round-10/README.md +61 -0
- package/docs/research/ledger/confer/2026-09-13-round-2/cass.md +72 -0
- package/docs/research/ledger/confer/2026-09-13-round-3/README.md +70 -0
- package/docs/research/ledger/confer/2026-09-13-round-5/README.md +61 -0
- package/docs/research/ledger/confer/2026-09-13-round-6/README.md +68 -0
- package/docs/research/ledger/confer/2026-09-13-round-7/README.md +64 -0
- package/docs/research/ledger/confer/2026-09-13-round-9/README.md +59 -0
- package/docs/research/ledger/confer/2026-09-14-wake/wake.md +146 -0
- package/docs/research/ledger/docs/inspirations.md +320 -0
- package/docs/research/ledger/docs/pr2-pr6-integration.md +53 -0
- package/docs/research/ledger/docs/research/2026-09-07-confer-inspirations.md +163 -0
- package/docs/research/ledger/docs/research/2026-09-07-letta-continuous-service.md +104 -0
- package/docs/research/ledger/docs/research/2026-09-08-claude-history-retrieval.md +117 -0
- package/docs/research/ledger/docs/research/2026-09-09-agentsview-native-discovery.md +87 -0
- package/docs/research/ledger/docs/research/2026-09-09-daemon-observation.md +231 -0
- package/docs/research/ledger/docs/research/2026-09-09-discovery-pagination.md +114 -0
- package/docs/research/ledger/docs/research/2026-09-09-harness-transcript-contract.md +145 -0
- package/docs/research/ledger/docs/research/2026-09-09-native-history-discovery.md +180 -0
- package/docs/research/ledger/docs/research/2026-09-09-native-history-implementation.md +137 -0
- package/docs/research/ledger/docs/research/2026-09-09-other-inspirations.md +279 -0
- package/docs/research/ledger/docs/research/2026-09-09-trajectory-and-letta-code.md +128 -0
- package/docs/research/ledger/docs/research/2026-09-13-diagnostics-coverage.md +116 -0
- package/docs/research/ledger/docs/research/2026-09-13-fts-rebuild.md +83 -0
- package/docs/research/ledger/docs/research/2026-09-13-read-path-split.md +74 -0
- package/docs/research/ledger/docs/research/2026-09-13-runtime-rollout.md +102 -0
- package/docs/research/ledger/docs/research/2026-09-13-scan-outcomes.md +80 -0
- package/docs/research/ledger/docs/research/2026-09-13-search-quality.md +105 -0
- package/docs/research/ledger/docs/research/2026-09-13-source-coverage.md +64 -0
- package/docs/research/ledger/docs/research/2026-09-13-sync-wall-budget.md +82 -0
- package/docs/research/ledger/docs/research/2026-09-13-typed-read-gate.md +81 -0
- package/docs/research/ledger/docs/research/2026-09-13-writer-lease.md +84 -0
- package/docs/research/ledger/docs/research/2026-09-16-c1-copy-fallback.md +227 -0
- package/docs/research/ledger/docs/research/2026-09-16-c10-kimi.md +414 -0
- package/docs/research/ledger/docs/research/2026-09-16-c2-derive-counter.md +216 -0
- package/docs/research/ledger/docs/research/2026-09-16-c3-echo-filter.md +305 -0
- package/docs/research/ledger/docs/research/2026-09-16-c4-title-index.md +349 -0
- package/docs/research/ledger/docs/research/2026-09-16-c5-trigram.md +268 -0
- package/docs/research/ledger/docs/research/2026-09-16-c6-recency-prior.md +438 -0
- package/docs/research/ledger/docs/research/2026-09-16-c7-tombstones.md +467 -0
- package/docs/research/ledger/docs/research/2026-09-16-c8-remote-truth.md +390 -0
- package/docs/research/ledger/docs/research/2026-09-16-c9-grok.md +380 -0
- package/docs/research/ledger/docs/research/2026-09-16-wake-port-program.md +38 -0
- package/docs/research/ledger/docs/research/2026-09-17-cursor-blobstore.md +157 -0
- package/docs/research/ledger/manifest.json +540 -0
- package/legacy/NOTICE.md +50 -0
- package/legacy/bin/ledger-core.ts +7 -0
- package/legacy/bin/ledger.ts +7 -0
- package/legacy/bin/traj-bridge.ts +7 -0
- package/legacy/scripts/import-claude-history.ts +130 -0
- package/legacy/scripts/native-history-smoke.ts +52 -0
- package/legacy/scripts/observe-daemon.ts +570 -0
- package/legacy/scripts/retitle-sessions.md +19 -0
- package/legacy/scripts/verify-runtime-rollout.ts +151 -0
- package/legacy/src/canonical.ts +108 -0
- package/legacy/src/cli.ts +1293 -0
- package/legacy/src/db.ts +2254 -0
- package/legacy/src/diagnostics.ts +87 -0
- package/legacy/src/discovery.ts +56 -0
- package/legacy/src/embeddings.ts +382 -0
- package/legacy/src/filter.ts +602 -0
- package/legacy/src/gemini_embeddings.ts +103 -0
- package/legacy/src/identity.ts +9 -0
- package/legacy/src/index.ts +10 -0
- package/legacy/src/ingest.ts +791 -0
- package/legacy/src/managed.ts +178 -0
- package/legacy/src/native/common.ts +68 -0
- package/legacy/src/native/cursor.ts +647 -0
- package/legacy/src/native/grok.ts +540 -0
- package/legacy/src/native/kimi.ts +706 -0
- package/legacy/src/native/opencode.ts +470 -0
- package/legacy/src/native/sqlite-ro.ts +281 -0
- package/legacy/src/native.ts +97 -0
- package/legacy/src/normalizer.ts +457 -0
- package/legacy/src/parentage.ts +562 -0
- package/legacy/src/pressure.ts +162 -0
- package/legacy/src/private-storage.ts +74 -0
- package/legacy/src/reader.ts +1255 -0
- package/legacy/src/scanner.ts +591 -0
- package/legacy/src/schema-gate.ts +263 -0
- package/legacy/src/schema.ts +829 -0
- package/legacy/src/semantic-top-k.ts +71 -0
- package/legacy/src/session-hash.ts +49 -0
- package/legacy/src/store-status.ts +218 -0
- package/legacy/src/sync-preparation.ts +45 -0
- package/legacy/src/sync-truth.ts +843 -0
- package/legacy/src/sync.ts +1233 -0
- package/legacy/src/types.ts +436 -0
- package/legacy/src/vocab.txt +30522 -0
- package/legacy/src/watch.ts +673 -0
- package/legacy/src/writer-lease.ts +310 -0
- package/ops/README.md +150 -0
- package/ops/install-cli.sh +63 -0
- package/ops/install-native.sh +35 -0
- package/ops/install-release.sh +49 -0
- package/ops/linux/install-systemd-user.sh +103 -0
- package/ops/macos/install-launchd.sh +72 -0
- package/ops/sync-settings.sh +37 -0
- package/package.json +21 -5
- package/skills/trajectories/SKILL.md +138 -0
- package/skills/trajectories/scripts/trajectories_recent.sh +26 -0
- package/skills/trajectories/scripts/trajectories_search.sh +26 -0
- package/index.js +0 -1
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
export type PressureSeverity = "unavailable" | "calm" | "constrained" | "critical";
|
|
5
|
+
|
|
6
|
+
export interface PsiWindow {
|
|
7
|
+
some_avg10: number;
|
|
8
|
+
full_avg10: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface PressureSnapshot {
|
|
12
|
+
platform: NodeJS.Platform;
|
|
13
|
+
severity: PressureSeverity;
|
|
14
|
+
cpu: PsiWindow | null;
|
|
15
|
+
memory: PsiWindow | null;
|
|
16
|
+
io: PsiWindow | null;
|
|
17
|
+
cgroup_memory_ratio: number | null;
|
|
18
|
+
cgroup_cpu_cores: number | null;
|
|
19
|
+
reasons: string[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface AdaptivePressureState {
|
|
23
|
+
scale_permille: number;
|
|
24
|
+
calm_runs: number;
|
|
25
|
+
last_severity: PressureSeverity;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type PressureSignals = Omit<PressureSnapshot, "severity" | "reasons">;
|
|
29
|
+
|
|
30
|
+
export const DEFAULT_PRESSURE_STATE: AdaptivePressureState = {
|
|
31
|
+
scale_permille: 1000,
|
|
32
|
+
calm_runs: 0,
|
|
33
|
+
last_severity: "unavailable",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const MIN_SCALE_PERMILLE = 250;
|
|
37
|
+
const RECOVERY_STEP_PERMILLE = 125;
|
|
38
|
+
const CALM_RUNS_PER_RECOVERY_STEP = 2;
|
|
39
|
+
|
|
40
|
+
function finiteNumber(value: string | undefined): number | null {
|
|
41
|
+
if (value === undefined) return null;
|
|
42
|
+
const parsed = Number(value);
|
|
43
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function parsePsi(text: string): PsiWindow | null {
|
|
47
|
+
let some: number | null = null;
|
|
48
|
+
let full: number | null = null;
|
|
49
|
+
for (const line of text.trim().split("\n")) {
|
|
50
|
+
const [kind, ...fields] = line.trim().split(/\s+/);
|
|
51
|
+
const avg10 = finiteNumber(fields.find((field) => field.startsWith("avg10="))?.slice(6));
|
|
52
|
+
if (kind === "some") some = avg10;
|
|
53
|
+
if (kind === "full") full = avg10;
|
|
54
|
+
}
|
|
55
|
+
return some === null || full === null ? null : { some_avg10: some, full_avg10: full };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function readText(path: string): string | null {
|
|
59
|
+
try { return readFileSync(path, "utf8"); }
|
|
60
|
+
catch { return null; }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function cgroupRoot(): string | null {
|
|
64
|
+
const membership = readText("/proc/self/cgroup");
|
|
65
|
+
const relative = membership?.split("\n")
|
|
66
|
+
.map((line) => line.split(":"))
|
|
67
|
+
.find((parts) => parts[0] === "0" && parts[1] === "")?.[2];
|
|
68
|
+
return relative ? join("/sys/fs/cgroup", relative) : null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function cgroupMemoryRatio(root: string | null): number | null {
|
|
72
|
+
if (!root) return null;
|
|
73
|
+
const current = finiteNumber(readText(join(root, "memory.current"))?.trim());
|
|
74
|
+
const rawMax = readText(join(root, "memory.max"))?.trim();
|
|
75
|
+
const max = rawMax === "max" ? null : finiteNumber(rawMax);
|
|
76
|
+
return current === null || max === null || max === 0 ? null : current / max;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function cgroupCpuCores(root: string | null): number | null {
|
|
80
|
+
if (!root) return null;
|
|
81
|
+
const [quotaText, periodText] = (readText(join(root, "cpu.max")) ?? "").trim().split(/\s+/);
|
|
82
|
+
if (quotaText === "max") return null;
|
|
83
|
+
const quota = finiteNumber(quotaText);
|
|
84
|
+
const period = finiteNumber(periodText);
|
|
85
|
+
return quota === null || period === null || period === 0 ? null : quota / period;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function pressureWindow(root: string | null, kind: "cpu" | "memory" | "io"): PsiWindow | null {
|
|
89
|
+
const scoped = root ? readText(join(root, `${kind}.pressure`)) : null;
|
|
90
|
+
return parsePsi(scoped ?? readText(`/proc/pressure/${kind}`) ?? "");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function samplePressure(platform: NodeJS.Platform = process.platform): PressureSnapshot {
|
|
94
|
+
if (platform !== "linux") {
|
|
95
|
+
return { platform, severity: "unavailable", cpu: null, memory: null, io: null,
|
|
96
|
+
cgroup_memory_ratio: null, cgroup_cpu_cores: null, reasons: ["psi_unavailable"] };
|
|
97
|
+
}
|
|
98
|
+
const root = cgroupRoot();
|
|
99
|
+
const cpu = pressureWindow(root, "cpu");
|
|
100
|
+
const memory = pressureWindow(root, "memory");
|
|
101
|
+
const io = pressureWindow(root, "io");
|
|
102
|
+
const ratio = cgroupMemoryRatio(root);
|
|
103
|
+
const cpuCores = cgroupCpuCores(root);
|
|
104
|
+
return classifyPressure({ platform, cpu, memory, io,
|
|
105
|
+
cgroup_memory_ratio: ratio, cgroup_cpu_cores: cpuCores });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function classifyPressure(signals: PressureSignals): PressureSnapshot {
|
|
109
|
+
const { platform, cpu, memory, io, cgroup_memory_ratio: ratio, cgroup_cpu_cores: cpuCores } = signals;
|
|
110
|
+
if (!cpu && !memory && !io && ratio === null) {
|
|
111
|
+
return { ...signals, severity: "unavailable", reasons: ["psi_unavailable"] };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const critical: string[] = [];
|
|
115
|
+
if (memory && memory.full_avg10 >= 1) critical.push("memory_full");
|
|
116
|
+
if (io && io.full_avg10 >= 10) critical.push("io_full");
|
|
117
|
+
if (ratio !== null && ratio >= 0.9) critical.push("cgroup_memory_90pct");
|
|
118
|
+
if (critical.length) return { platform, severity: "critical", cpu, memory, io,
|
|
119
|
+
cgroup_memory_ratio: ratio, cgroup_cpu_cores: cpuCores, reasons: critical };
|
|
120
|
+
|
|
121
|
+
const constrained: string[] = [];
|
|
122
|
+
if (memory && memory.some_avg10 >= 2) constrained.push("memory_some");
|
|
123
|
+
if (io && io.some_avg10 >= 10) constrained.push("io_some");
|
|
124
|
+
if (cpu && cpu.some_avg10 >= 80) constrained.push("cpu_some");
|
|
125
|
+
if (ratio !== null && ratio >= 0.75) constrained.push("cgroup_memory_75pct");
|
|
126
|
+
return { platform, severity: constrained.length ? "constrained" : "calm", cpu, memory, io,
|
|
127
|
+
cgroup_memory_ratio: ratio, cgroup_cpu_cores: cpuCores, reasons: constrained };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function adaptPressure(
|
|
131
|
+
previous: AdaptivePressureState,
|
|
132
|
+
snapshot: PressureSnapshot,
|
|
133
|
+
): { state: AdaptivePressureState; pause: boolean } {
|
|
134
|
+
const scale = Math.min(1000, Math.max(MIN_SCALE_PERMILLE, previous.scale_permille));
|
|
135
|
+
if (snapshot.severity === "critical" || snapshot.severity === "constrained") {
|
|
136
|
+
return {
|
|
137
|
+
state: {
|
|
138
|
+
scale_permille: Math.max(MIN_SCALE_PERMILLE, Math.floor(scale / 2)),
|
|
139
|
+
calm_runs: 0,
|
|
140
|
+
last_severity: snapshot.severity,
|
|
141
|
+
},
|
|
142
|
+
pause: snapshot.severity === "critical",
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
if (snapshot.severity === "unavailable") {
|
|
146
|
+
return { state: { ...previous, scale_permille: scale, last_severity: "unavailable" }, pause: false };
|
|
147
|
+
}
|
|
148
|
+
const calmRuns = previous.calm_runs + 1;
|
|
149
|
+
const recover = calmRuns >= CALM_RUNS_PER_RECOVERY_STEP;
|
|
150
|
+
return {
|
|
151
|
+
state: {
|
|
152
|
+
scale_permille: recover ? Math.min(1000, scale + RECOVERY_STEP_PERMILLE) : scale,
|
|
153
|
+
calm_runs: recover ? 0 : calmRuns,
|
|
154
|
+
last_severity: "calm",
|
|
155
|
+
},
|
|
156
|
+
pause: false,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function scaledLimit(limit: number, scalePermille: number): number {
|
|
161
|
+
return Math.max(1, Math.floor(limit * scalePermille / 1000));
|
|
162
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import {
|
|
2
|
+
chmodSync, closeSync, constants, fstatSync, lstatSync, mkdirSync, openSync, type Stats,
|
|
3
|
+
} from "node:fs";
|
|
4
|
+
|
|
5
|
+
/** Owner-private local storage, not encryption or a same-user sandbox. */
|
|
6
|
+
export class PrivateStorageError extends Error {
|
|
7
|
+
readonly code = "UNSAFE_STORE_PATH";
|
|
8
|
+
constructor(path: string, detail: string) {
|
|
9
|
+
super(`Refusing unsafe Ledger storage at ${path}: ${detail}`);
|
|
10
|
+
this.name = "PrivateStorageError";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function validate(stat: Stats, path: string, directory = false): void {
|
|
15
|
+
if (directory ? !stat.isDirectory() : !stat.isFile()) {
|
|
16
|
+
throw new PrivateStorageError(path, directory ? "not a real directory" : "not a regular, non-symlink file");
|
|
17
|
+
}
|
|
18
|
+
// A journal may be unlinked by a concurrent SQLite commit during stat.
|
|
19
|
+
if (stat.nlink === 0) throw Object.assign(new Error("Storage entry was removed"), { code: "ENOENT" });
|
|
20
|
+
if (!directory && stat.nlink > 1) throw new PrivateStorageError(path, "hard-linked file");
|
|
21
|
+
if (process.getuid && stat.uid !== process.getuid()) throw new PrivateStorageError(path, "owned by another user");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Never open/close an extra descriptor for an existing SQLite inode: POSIX
|
|
26
|
+
* closing ANY descriptor to it can release this process's fcntl locks, even
|
|
27
|
+
* locks owned by a different SQLite handle. lstat/chmod do not release locks.
|
|
28
|
+
* The local namespace is trusted against same-UID races; changed identities
|
|
29
|
+
* are refused, but this is not a sandbox for a hostile process with our UID.
|
|
30
|
+
*/
|
|
31
|
+
function restrictExisting(path: string, directory = false): void {
|
|
32
|
+
const before = lstatSync(path);
|
|
33
|
+
validate(before, path, directory);
|
|
34
|
+
const mode = directory ? 0o700 : 0o600;
|
|
35
|
+
if ((before.mode & mode) !== mode) throw new PrivateStorageError(path, "owner permissions are restricted; repair explicitly before retrying");
|
|
36
|
+
if ((before.mode & 0o777) === mode) return;
|
|
37
|
+
chmodSync(path, mode);
|
|
38
|
+
const after = lstatSync(path);
|
|
39
|
+
validate(after, path, directory);
|
|
40
|
+
if (after.dev !== before.dev || after.ino !== before.ino || (after.mode & 0o777) !== mode) {
|
|
41
|
+
throw new PrivateStorageError(path, "identity or permissions changed during permission repair");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Only use for Ledger's own default directory, never arbitrary parent directories. */
|
|
46
|
+
export function ensurePrivateDirectory(path: string): void {
|
|
47
|
+
mkdirSync(path, { recursive: true, mode: 0o700 });
|
|
48
|
+
restrictExisting(path, true);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function ensurePrivateFile(path: string, create = true): void {
|
|
52
|
+
try {
|
|
53
|
+
restrictExisting(path);
|
|
54
|
+
return;
|
|
55
|
+
} catch (error) {
|
|
56
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
57
|
+
if (!create) return;
|
|
58
|
+
}
|
|
59
|
+
let fd: number;
|
|
60
|
+
try {
|
|
61
|
+
// O_EXCL guarantees this descriptor is never an existing SQLite inode.
|
|
62
|
+
fd = openSync(path, constants.O_RDWR | constants.O_NOFOLLOW | constants.O_CREAT | constants.O_EXCL, 0o600);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
if ((error as NodeJS.ErrnoException).code === "EEXIST") return restrictExisting(path);
|
|
65
|
+
throw new PrivateStorageError(path, (error as NodeJS.ErrnoException).code ?? "cannot create safely");
|
|
66
|
+
}
|
|
67
|
+
try { validate(fstatSync(fd), path); } finally { closeSync(fd); }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** SQLite creates missing sidecars using the primary file's restrictive mode. */
|
|
71
|
+
export function ensurePrivateDatabase(path: string): void {
|
|
72
|
+
ensurePrivateFile(path);
|
|
73
|
+
for (const suffix of ["-wal", "-shm", "-journal"]) ensurePrivateFile(path + suffix, false);
|
|
74
|
+
}
|