pactium 0.2.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/LICENSE +674 -0
- package/README.md +92 -0
- package/README.zh-CN.md +90 -0
- package/SECURITY.md +7 -0
- package/bin/pactium.mjs +121 -0
- package/docs/LICOLITE-ASPECT.md +57 -0
- package/docs/README.md +13 -0
- package/docs/TERM.md +289 -0
- package/docs/architecture/ARCHITECTURE.md +62 -0
- package/docs/protocols/PROFILE.md +124 -0
- package/docs/protocols/PROTOCOLS.md +62 -0
- package/examples/record-operation.mjs +26 -0
- package/package.json +69 -0
- package/src/README.md +13 -0
- package/src/aspects/licolite/aspect.js +278 -0
- package/src/aspects/licolite/constants.js +13 -0
- package/src/aspects/licolite/evidence.js +47 -0
- package/src/aspects/licolite/index.d.ts +51 -0
- package/src/aspects/licolite/index.js +19 -0
- package/src/aspects/licolite/signing.js +78 -0
- package/src/canonical/value.js +40 -0
- package/src/core/append-condition.js +102 -0
- package/src/core/pactium-core.js +1073 -0
- package/src/core/tracking-cursor.js +68 -0
- package/src/http.js +99 -0
- package/src/index-engine/snapshot-merkle-index.js +994 -0
- package/src/index.d.ts +244 -0
- package/src/index.js +73 -0
- package/src/ledger/signed-head.js +204 -0
- package/src/ledger/transparency-log.js +702 -0
- package/src/maintenance/task-engine.js +36 -0
- package/src/proof/bundle-format.js +265 -0
- package/src/proof/bundle.js +77 -0
- package/src/proof/envelope.js +548 -0
- package/src/proof/registry.js +18 -0
- package/src/protocol/constants.js +69 -0
- package/src/protocol/hashing.js +47 -0
- package/src/quality/profile-runner.js +291 -0
- package/src/repair/planner.js +62 -0
- package/src/shared/records.js +32 -0
- package/src/storage/local-json-storage-port.js +360 -0
- package/src/verification/failure.js +31 -0
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
import { PACTIUM_PROTOCOL, PACTIUM_SCHEMA_VERSION } from "../protocol/constants.js";
|
|
7
|
+
import { canonicalEncode, normalizeCanonicalValue } from "../canonical/value.js";
|
|
8
|
+
import { cidForBytes, hashBytes, hexFromCid } from "../protocol/hashing.js";
|
|
9
|
+
import { asArray, nowIso, safeToken } from "../shared/records.js";
|
|
10
|
+
|
|
11
|
+
export function defaultPactiumDataDir() {
|
|
12
|
+
return path.join(os.homedir(), ".pactium");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function resolveDataDir(dataDir = "") {
|
|
16
|
+
return path.resolve(String(dataDir || process.env.PACTIUM_DATA_DIR || defaultPactiumDataDir()));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function resolveWithin(root, ...segments) {
|
|
20
|
+
const base = path.resolve(String(root || defaultPactiumDataDir()));
|
|
21
|
+
const target = path.resolve(base, ...segments.map((segment) => String(segment || "")));
|
|
22
|
+
if (target !== base && !target.startsWith(`${base}${path.sep}`)) {
|
|
23
|
+
throw new Error(`Path escapes Pactium data directory: ${target}`);
|
|
24
|
+
}
|
|
25
|
+
return target;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function fileExists(filePath) {
|
|
29
|
+
try {
|
|
30
|
+
await fs.access(filePath);
|
|
31
|
+
return true;
|
|
32
|
+
} catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function readJson(filePath, fallback = null) {
|
|
38
|
+
try {
|
|
39
|
+
return JSON.parse(await fs.readFile(filePath, "utf8"));
|
|
40
|
+
} catch (error) {
|
|
41
|
+
if (error?.code === "ENOENT") return fallback;
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function writeJsonAtomic(filePath, value) {
|
|
47
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
48
|
+
const tmpPath = `${filePath}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
49
|
+
await fs.writeFile(tmpPath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
50
|
+
await fs.rename(tmpPath, filePath);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function sleep(ms) {
|
|
54
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function processIsAlive(pid) {
|
|
58
|
+
if (!pid || pid === process.pid) return Boolean(pid);
|
|
59
|
+
try {
|
|
60
|
+
process.kill(pid, 0);
|
|
61
|
+
return true;
|
|
62
|
+
} catch (error) {
|
|
63
|
+
return error?.code === "EPERM";
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function createStoragePort({ dataDir = "", userDataPath = "", inMemory = false } = {}) {
|
|
68
|
+
const resolvedDataDir = resolveDataDir(dataDir || userDataPath);
|
|
69
|
+
const memoryBlocks = new Map();
|
|
70
|
+
const memoryObjects = new Map();
|
|
71
|
+
let initialized = false;
|
|
72
|
+
|
|
73
|
+
function manifestPath() {
|
|
74
|
+
return resolveWithin(resolvedDataDir, "pactium-manifest.json");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function blockPath(cid) {
|
|
78
|
+
const hex = hexFromCid(cid);
|
|
79
|
+
return resolveWithin(resolvedDataDir, "cas", hex.slice(0, 2), `${hex}.json`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function objectPath(scope, key) {
|
|
83
|
+
return resolveWithin(resolvedDataDir, "protocol", safeToken(scope), `${safeToken(key)}.json`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function lockPath(name = "write") {
|
|
87
|
+
return resolveWithin(resolvedDataDir, "locks", `${safeToken(name)}.lock`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function lockOwnerPath(lockDir) {
|
|
91
|
+
return path.join(lockDir, "owner.json");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function ensureInitialized() {
|
|
95
|
+
if (initialized) return;
|
|
96
|
+
initialized = true;
|
|
97
|
+
if (inMemory) return;
|
|
98
|
+
await fs.mkdir(resolvedDataDir, { recursive: true });
|
|
99
|
+
const manifest = await readJson(manifestPath());
|
|
100
|
+
if (manifest) {
|
|
101
|
+
if (manifest.protocol !== PACTIUM_PROTOCOL || manifest.schema !== PACTIUM_SCHEMA_VERSION) {
|
|
102
|
+
throw new Error("Pactium latest-schema-only boundary rejected a non-current protocol data directory.");
|
|
103
|
+
}
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const historicalLayoutHints = [
|
|
107
|
+
resolveWithin(resolvedDataDir, "operation-ledger"),
|
|
108
|
+
resolveWithin(resolvedDataDir, "checkpoint-trees"),
|
|
109
|
+
resolveWithin(resolvedDataDir, "state-substrate")
|
|
110
|
+
];
|
|
111
|
+
for (const historicalPath of historicalLayoutHints) {
|
|
112
|
+
if (await fileExists(historicalPath)) {
|
|
113
|
+
throw new Error("Historical Pactium data directory detected. Pactium performs no data migration.");
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
await writeJsonAtomic(manifestPath(), {
|
|
117
|
+
protocol: PACTIUM_PROTOCOL,
|
|
118
|
+
schema: PACTIUM_SCHEMA_VERSION,
|
|
119
|
+
createdAt: nowIso(),
|
|
120
|
+
latestSchemaOnly: true,
|
|
121
|
+
historicalMigration: false
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function putBlock(value, { codec = "pactium-canonical", kind = "protocol-material", refs = [] } = {}) {
|
|
126
|
+
await ensureInitialized();
|
|
127
|
+
const bytes = codec === "raw"
|
|
128
|
+
? Buffer.from(value || "")
|
|
129
|
+
: Buffer.from(canonicalEncode(value));
|
|
130
|
+
const cid = cidForBytes(bytes);
|
|
131
|
+
const payloadHash = `sha256:${hashBytes(bytes)}`;
|
|
132
|
+
const record = {
|
|
133
|
+
protocol: PACTIUM_PROTOCOL,
|
|
134
|
+
schema: PACTIUM_SCHEMA_VERSION,
|
|
135
|
+
cid,
|
|
136
|
+
codec,
|
|
137
|
+
kind,
|
|
138
|
+
refs: [...new Set(asArray(refs).map((ref) => String(ref || "").trim()).filter(Boolean))],
|
|
139
|
+
byteLength: bytes.length,
|
|
140
|
+
payloadHash,
|
|
141
|
+
payloadBase64: bytes.toString("base64"),
|
|
142
|
+
createdAt: nowIso()
|
|
143
|
+
};
|
|
144
|
+
const existing = await getBlock(cid);
|
|
145
|
+
if (existing) {
|
|
146
|
+
if (existing.payloadHash !== payloadHash || existing.payloadBase64 !== record.payloadBase64) {
|
|
147
|
+
throw new Error(`CAS collision or replacement attempt for ${cid}`);
|
|
148
|
+
}
|
|
149
|
+
return { ...existing, deduped: true };
|
|
150
|
+
}
|
|
151
|
+
memoryBlocks.set(cid, record);
|
|
152
|
+
if (!inMemory) await writeJsonAtomic(blockPath(cid), record);
|
|
153
|
+
return { ...record, deduped: false };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function getBlock(cid) {
|
|
157
|
+
await ensureInitialized();
|
|
158
|
+
if (memoryBlocks.has(cid)) return { ...memoryBlocks.get(cid), bytes: Buffer.from(memoryBlocks.get(cid).payloadBase64, "base64") };
|
|
159
|
+
if (inMemory) return null;
|
|
160
|
+
const record = await readJson(blockPath(cid));
|
|
161
|
+
if (!record) return null;
|
|
162
|
+
const bytes = Buffer.from(String(record.payloadBase64 || ""), "base64");
|
|
163
|
+
const payloadHash = `sha256:${hashBytes(bytes)}`;
|
|
164
|
+
if (payloadHash !== record.payloadHash || cidForBytes(bytes) !== record.cid) {
|
|
165
|
+
throw new Error(`CAS block integrity failure for ${cid}`);
|
|
166
|
+
}
|
|
167
|
+
memoryBlocks.set(cid, record);
|
|
168
|
+
return { ...record, bytes };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function hasBlock(cid) {
|
|
172
|
+
return Boolean(await getBlock(cid));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function walk(rootCid) {
|
|
176
|
+
const missing = [];
|
|
177
|
+
const blocks = [];
|
|
178
|
+
const seen = new Set();
|
|
179
|
+
const stack = [String(rootCid || "").trim()].filter(Boolean);
|
|
180
|
+
while (stack.length > 0) {
|
|
181
|
+
const cid = stack.pop();
|
|
182
|
+
if (!cid || seen.has(cid)) continue;
|
|
183
|
+
seen.add(cid);
|
|
184
|
+
const block = await getBlock(cid);
|
|
185
|
+
if (!block) {
|
|
186
|
+
missing.push(cid);
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
blocks.push(block);
|
|
190
|
+
for (const ref of [...asArray(block.refs)].reverse()) {
|
|
191
|
+
if (!seen.has(ref)) stack.push(ref);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return { protocol: PACTIUM_PROTOCOL, rootCid, blockCount: blocks.length, missing, blocks };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function putProtocolObject(scope, key, value) {
|
|
198
|
+
await ensureInitialized();
|
|
199
|
+
const normalizedScope = safeToken(scope);
|
|
200
|
+
const normalizedKey = safeToken(key);
|
|
201
|
+
const storedValue = inMemory ? value : normalizeCanonicalValue(value);
|
|
202
|
+
const stored = {
|
|
203
|
+
protocol: PACTIUM_PROTOCOL,
|
|
204
|
+
schema: PACTIUM_SCHEMA_VERSION,
|
|
205
|
+
value: storedValue,
|
|
206
|
+
updatedAt: nowIso()
|
|
207
|
+
};
|
|
208
|
+
memoryObjects.set(`${normalizedScope}/${normalizedKey}`, storedValue);
|
|
209
|
+
if (!inMemory) await writeJsonAtomic(objectPath(normalizedScope, normalizedKey), stored);
|
|
210
|
+
return storedValue;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function clearCache() {
|
|
214
|
+
memoryObjects.clear();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function readLockOwner(lockDir) {
|
|
218
|
+
const owner = await readJson(lockOwnerPath(lockDir), null);
|
|
219
|
+
const stats = await fs.stat(lockDir).catch(() => null);
|
|
220
|
+
return {
|
|
221
|
+
owner,
|
|
222
|
+
mtimeMs: Number(stats?.mtimeMs || 0)
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function removeStaleLock(lockDir, staleMs) {
|
|
227
|
+
const { owner, mtimeMs } = await readLockOwner(lockDir);
|
|
228
|
+
if (!mtimeMs) return false;
|
|
229
|
+
const pid = Number(owner?.pid || 0);
|
|
230
|
+
const ageMs = Date.now() - (Number(owner?.createdAtMs || 0) || mtimeMs);
|
|
231
|
+
if (!owner) {
|
|
232
|
+
if (ageMs < staleMs) return false;
|
|
233
|
+
} else if (ageMs < staleMs && processIsAlive(pid)) {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
const latest = await readLockOwner(lockDir);
|
|
237
|
+
const sameOwner = String(latest.owner?.ownerId || "") === String(owner?.ownerId || "") &&
|
|
238
|
+
Number(latest.owner?.createdAtMs || 0) === Number(owner?.createdAtMs || 0) &&
|
|
239
|
+
Number(latest.mtimeMs || 0) === Number(mtimeMs || 0);
|
|
240
|
+
if (!sameOwner) return false;
|
|
241
|
+
await fs.rm(lockDir, { recursive: true, force: true });
|
|
242
|
+
return true;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async function withWriteLock(task, {
|
|
246
|
+
name = "write",
|
|
247
|
+
timeoutMs = 10000,
|
|
248
|
+
retryMs = 25,
|
|
249
|
+
staleMs = 30000
|
|
250
|
+
} = {}) {
|
|
251
|
+
await ensureInitialized();
|
|
252
|
+
if (inMemory) return task();
|
|
253
|
+
const locksDir = resolveWithin(resolvedDataDir, "locks");
|
|
254
|
+
await fs.mkdir(locksDir, { recursive: true });
|
|
255
|
+
const targetLockPath = lockPath(name);
|
|
256
|
+
const ownerId = crypto.randomUUID();
|
|
257
|
+
const startedAt = Date.now();
|
|
258
|
+
let acquired = false;
|
|
259
|
+
while (!acquired) {
|
|
260
|
+
try {
|
|
261
|
+
await fs.mkdir(targetLockPath);
|
|
262
|
+
acquired = true;
|
|
263
|
+
} catch (error) {
|
|
264
|
+
if (error?.code !== "EEXIST") throw error;
|
|
265
|
+
await removeStaleLock(targetLockPath, staleMs).catch(() => false);
|
|
266
|
+
if (Date.now() - startedAt >= timeoutMs) {
|
|
267
|
+
const lockError = new Error(`Timed out acquiring Pactium data directory write lock: ${targetLockPath}`);
|
|
268
|
+
lockError.code = "PACTIUM_WRITE_LOCK_TIMEOUT";
|
|
269
|
+
lockError.details = {
|
|
270
|
+
protocol: PACTIUM_PROTOCOL,
|
|
271
|
+
lockType: "pactium.write-lock",
|
|
272
|
+
dataDir: resolvedDataDir,
|
|
273
|
+
lockPath: targetLockPath,
|
|
274
|
+
timeoutMs,
|
|
275
|
+
retryMs,
|
|
276
|
+
staleMs
|
|
277
|
+
};
|
|
278
|
+
throw lockError;
|
|
279
|
+
}
|
|
280
|
+
await sleep(retryMs);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
await writeJsonAtomic(lockOwnerPath(targetLockPath), {
|
|
284
|
+
protocol: PACTIUM_PROTOCOL,
|
|
285
|
+
schema: PACTIUM_SCHEMA_VERSION,
|
|
286
|
+
lockType: "pactium.write-lock",
|
|
287
|
+
ownerId,
|
|
288
|
+
pid: process.pid,
|
|
289
|
+
createdAt: nowIso(),
|
|
290
|
+
createdAtMs: Date.now()
|
|
291
|
+
});
|
|
292
|
+
try {
|
|
293
|
+
return await task();
|
|
294
|
+
} finally {
|
|
295
|
+
const owner = await readJson(lockOwnerPath(targetLockPath), null).catch(() => null);
|
|
296
|
+
if (owner?.ownerId === ownerId) {
|
|
297
|
+
await fs.rm(targetLockPath, { recursive: true, force: true });
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function pruneBlocks(predicate = () => false) {
|
|
303
|
+
if (!inMemory) return 0;
|
|
304
|
+
let pruned = 0;
|
|
305
|
+
for (const [cid, record] of memoryBlocks.entries()) {
|
|
306
|
+
if (predicate(record)) {
|
|
307
|
+
memoryBlocks.delete(cid);
|
|
308
|
+
pruned += 1;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return pruned;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function pruneProtocolObjects(predicate = () => false) {
|
|
315
|
+
if (!inMemory) return 0;
|
|
316
|
+
let pruned = 0;
|
|
317
|
+
for (const [compoundKey, value] of memoryObjects.entries()) {
|
|
318
|
+
const [scope = "", key = ""] = compoundKey.split("/");
|
|
319
|
+
if (predicate({ scope, key, value })) {
|
|
320
|
+
memoryObjects.delete(compoundKey);
|
|
321
|
+
pruned += 1;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return pruned;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async function getProtocolObject(scope, key, fallback = null) {
|
|
328
|
+
await ensureInitialized();
|
|
329
|
+
const normalizedScope = safeToken(scope);
|
|
330
|
+
const normalizedKey = safeToken(key);
|
|
331
|
+
const memoryKey = `${normalizedScope}/${normalizedKey}`;
|
|
332
|
+
if (memoryObjects.has(memoryKey)) return memoryObjects.get(memoryKey);
|
|
333
|
+
if (inMemory) return fallback;
|
|
334
|
+
const stored = await readJson(objectPath(normalizedScope, normalizedKey));
|
|
335
|
+
if (!stored) return fallback;
|
|
336
|
+
if (stored.protocol !== PACTIUM_PROTOCOL || stored.schema !== PACTIUM_SCHEMA_VERSION) {
|
|
337
|
+
throw new Error("Pactium latest-schema-only boundary rejected protocol material.");
|
|
338
|
+
}
|
|
339
|
+
memoryObjects.set(memoryKey, stored.value);
|
|
340
|
+
return stored.value;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return Object.freeze({
|
|
344
|
+
protocol: PACTIUM_PROTOCOL,
|
|
345
|
+
schema: PACTIUM_SCHEMA_VERSION,
|
|
346
|
+
dataDir: resolvedDataDir,
|
|
347
|
+
inMemory,
|
|
348
|
+
initialize: ensureInitialized,
|
|
349
|
+
putBlock,
|
|
350
|
+
getBlock,
|
|
351
|
+
hasBlock,
|
|
352
|
+
walk,
|
|
353
|
+
putProtocolObject,
|
|
354
|
+
getProtocolObject,
|
|
355
|
+
clearCache,
|
|
356
|
+
withWriteLock,
|
|
357
|
+
pruneBlocks,
|
|
358
|
+
pruneProtocolObjects
|
|
359
|
+
});
|
|
360
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { PACTIUM_PROTOCOL } from "../protocol/constants.js";
|
|
2
|
+
import { asRecord, compactObject } from "../shared/records.js";
|
|
3
|
+
|
|
4
|
+
export function createVerificationFailure({
|
|
5
|
+
layer = "core",
|
|
6
|
+
code = "verification_failed",
|
|
7
|
+
severity = "error",
|
|
8
|
+
message = "",
|
|
9
|
+
evidenceRef = "",
|
|
10
|
+
repairable = false,
|
|
11
|
+
details = {}
|
|
12
|
+
} = {}) {
|
|
13
|
+
return compactObject({
|
|
14
|
+
protocol: PACTIUM_PROTOCOL,
|
|
15
|
+
layer,
|
|
16
|
+
code,
|
|
17
|
+
severity,
|
|
18
|
+
message,
|
|
19
|
+
evidenceRef,
|
|
20
|
+
repairable,
|
|
21
|
+
details: Object.keys(asRecord(details)).length > 0 ? asRecord(details) : undefined
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class PactiumLifecycleError extends Error {
|
|
26
|
+
constructor(message, failure) {
|
|
27
|
+
super(message);
|
|
28
|
+
this.name = "PactiumLifecycleError";
|
|
29
|
+
this.failure = failure;
|
|
30
|
+
}
|
|
31
|
+
}
|