@totemsdk/proofgraph 1.0.1 → 1.1.1
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 +202 -21
- package/NOTICE +60 -0
- package/README.md +31 -10
- package/dist/canonical.js +11 -19
- package/dist/cjs/canonical.d.ts +22 -0
- package/dist/cjs/canonical.js +51 -0
- package/dist/cjs/durable-store.d.ts +50 -0
- package/dist/cjs/durable-store.js +178 -0
- package/dist/cjs/evidence.d.ts +89 -0
- package/dist/cjs/evidence.js +130 -0
- package/dist/cjs/graph.d.ts +103 -0
- package/dist/cjs/graph.js +298 -0
- package/dist/cjs/index.d.ts +26 -0
- package/dist/cjs/index.js +84 -0
- package/dist/cjs/io.d.ts +26 -0
- package/dist/cjs/io.js +173 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/query.d.ts +85 -0
- package/dist/cjs/query.js +189 -0
- package/dist/cjs/replicated-evidence.d.ts +42 -0
- package/dist/cjs/replicated-evidence.js +98 -0
- package/dist/cjs/types.d.ts +62 -0
- package/dist/cjs/types.js +7 -0
- package/dist/cjs/verify.d.ts +23 -0
- package/dist/cjs/verify.js +77 -0
- package/dist/durable-store.js +9 -12
- package/dist/evidence.js +14 -19
- package/dist/graph.d.ts +11 -0
- package/dist/graph.js +47 -34
- package/dist/index.d.ts +3 -2
- package/dist/index.js +8 -52
- package/dist/io.js +4 -8
- package/dist/query.d.ts +4 -0
- package/dist/query.js +30 -38
- package/dist/replicated-evidence.d.ts +42 -0
- package/dist/replicated-evidence.js +95 -0
- package/dist/types.d.ts +1 -1
- package/dist/types.js +1 -2
- package/dist/verify.js +3 -40
- package/package.json +25 -15
- package/rust/pkg/package.json +2 -2
- package/rust/pkg/proofgraph_wasm_bg.wasm +0 -0
- package/rust/pkg-node/package.json +2 -2
- package/rust/pkg-node/proofgraph_wasm_bg.wasm +0 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Durable `ProofGraphStoragePort` adapter for @totemsdk/proofgraph.
|
|
4
|
+
*
|
|
5
|
+
* Implements RFC-007 G8 / §4.3: graph + reverse node index are persisted over a
|
|
6
|
+
* shared `StorageAdapter`, with restart recovery and concurrent-update safety
|
|
7
|
+
* (CAS guards on the head pointer and node index, serialized per adapter).
|
|
8
|
+
*
|
|
9
|
+
* Layout (single `StorageAdapter`, prefixed keys):
|
|
10
|
+
* graph:<graphId> { json, savedAt } immutable content-addressed record
|
|
11
|
+
* node:<nodeId> { graphId, savedAt } reverse index (most-recent
|
|
12
|
+
* snapshot that contains the node)
|
|
13
|
+
* head { graphId, savedAt } most-recent saved graph
|
|
14
|
+
*
|
|
15
|
+
* `load()` runs `importProofGraph`, which validates every node/edge and
|
|
16
|
+
* recomputes the graphId — a tampered or truncated record surfaces as
|
|
17
|
+
* `StorageError` `corrupt`, never as `not-found` (RFC-007 §4.2 strict).
|
|
18
|
+
*/
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
exports.createDurableProofGraphStore = createDurableProofGraphStore;
|
|
21
|
+
const types_1 = require("@totemsdk/storage/types");
|
|
22
|
+
const errors_1 = require("@totemsdk/storage/errors");
|
|
23
|
+
const io_js_1 = require("./io.js");
|
|
24
|
+
const DEFAULT_NAMESPACE = 'totem_proofgraph:v1:';
|
|
25
|
+
const GRAPH_PREFIX = 'graph:';
|
|
26
|
+
const NODE_PREFIX = 'node:';
|
|
27
|
+
const HEAD_KEY = 'head';
|
|
28
|
+
/** Per-adapter mutation lock so concurrent saves never interleave (wots-lease pattern). */
|
|
29
|
+
const storageTails = new WeakMap();
|
|
30
|
+
function withStorageLock(storage, fn) {
|
|
31
|
+
const previous = storageTails.get(storage) ?? Promise.resolve();
|
|
32
|
+
let release;
|
|
33
|
+
const gate = new Promise((resolve) => { release = resolve; });
|
|
34
|
+
storageTails.set(storage, previous.then(() => gate));
|
|
35
|
+
return previous.then(() => fn()).finally(release);
|
|
36
|
+
}
|
|
37
|
+
function isCasStore(adapter) {
|
|
38
|
+
return typeof adapter.conditionalUpdate === 'function';
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Create a durable `ProofGraphStoragePort` over a CAS-capable adapter.
|
|
42
|
+
*
|
|
43
|
+
* The head pointer and every reverse-index entry are guarded with the
|
|
44
|
+
* adapter's `conditionalUpdate`, and all mutations run serialized per adapter,
|
|
45
|
+
* so concurrent saves never clobber a newer graph. A non-CAS or low-durability
|
|
46
|
+
* adapter is rejected at construction (RFC-007 §4.2 no-silent-downgrade).
|
|
47
|
+
*/
|
|
48
|
+
function createDurableProofGraphStore(adapter, options = {}) {
|
|
49
|
+
const namespace = options.namespace ?? DEFAULT_NAMESPACE;
|
|
50
|
+
if (typeof adapter.capabilities === 'object') {
|
|
51
|
+
(0, types_1.assertCapabilities)(adapter, {
|
|
52
|
+
acknowledge: options.requireAckMode ?? 'durably-acknowledged',
|
|
53
|
+
atomic: true,
|
|
54
|
+
conditional: true,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
if (!isCasStore(adapter)) {
|
|
58
|
+
throw new Error('createDurableProofGraphStore requires a CAS-capable storage adapter (CasStore.conditionalUpdate)');
|
|
59
|
+
}
|
|
60
|
+
const cas = adapter.conditionalUpdate.bind(adapter);
|
|
61
|
+
const graphKey = (graphId) => `${namespace}${GRAPH_PREFIX}${graphId}`;
|
|
62
|
+
const nodeKey = (nodeId) => `${namespace}${NODE_PREFIX}${nodeId}`;
|
|
63
|
+
const headKey = () => `${namespace}${HEAD_KEY}`;
|
|
64
|
+
/** Strictly-increasing savedAt so concurrent saves have deterministic order. */
|
|
65
|
+
let lastSavedAt = 0;
|
|
66
|
+
const nextSavedAt = () => {
|
|
67
|
+
const now = Date.now();
|
|
68
|
+
lastSavedAt = now > lastSavedAt ? now : lastSavedAt + 1;
|
|
69
|
+
return lastSavedAt;
|
|
70
|
+
};
|
|
71
|
+
function parseGraph(graphId, raw) {
|
|
72
|
+
if (typeof raw !== 'string' && (typeof raw !== 'object' || raw === null || typeof raw.json !== 'string')) {
|
|
73
|
+
throw new errors_1.StorageError(`proofgraph record "${graphId}" is malformed`, 'corrupt', { key: graphKey(graphId) });
|
|
74
|
+
}
|
|
75
|
+
const json = typeof raw === 'string' ? raw : raw.json;
|
|
76
|
+
try {
|
|
77
|
+
return (0, io_js_1.importProofGraph)(json);
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
throw new errors_1.StorageError(`proofgraph record "${graphId}" failed validation: ${err.message}`, 'corrupt', { key: graphKey(graphId), cause: err });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async function loadUnchecked(graphId) {
|
|
84
|
+
const raw = await adapter.get(graphKey(graphId));
|
|
85
|
+
if (raw === null || raw === undefined)
|
|
86
|
+
return null;
|
|
87
|
+
return parseGraph(graphId, raw);
|
|
88
|
+
}
|
|
89
|
+
async function loadGraphForNode(nodeId) {
|
|
90
|
+
const entry = await adapter.get(nodeKey(nodeId));
|
|
91
|
+
if (entry === null || entry === undefined)
|
|
92
|
+
return null;
|
|
93
|
+
return loadUnchecked(entry.graphId);
|
|
94
|
+
}
|
|
95
|
+
async function writeIndexIfNewer(key, next) {
|
|
96
|
+
await cas(key, (current) => {
|
|
97
|
+
if (current && current.savedAt > next.savedAt)
|
|
98
|
+
return { abort: 'older snapshot' };
|
|
99
|
+
return { next };
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
async save(graph) {
|
|
104
|
+
const savedAt = nextSavedAt();
|
|
105
|
+
return withStorageLock(adapter, async () => {
|
|
106
|
+
// Immutable content-addressed record first (idempotent, safe on retry).
|
|
107
|
+
const record = { json: (0, io_js_1.exportProofGraph)(graph), savedAt };
|
|
108
|
+
await adapter.set(graphKey(graph.graphId), record);
|
|
109
|
+
// Reverse index: most-recent snapshot wins (CAS, never clobbers newer).
|
|
110
|
+
for (const node of graph.nodes) {
|
|
111
|
+
await writeIndexIfNewer(nodeKey(node.id), { graphId: graph.graphId, savedAt });
|
|
112
|
+
}
|
|
113
|
+
// Head pointer: does not move backwards.
|
|
114
|
+
await writeIndexIfNewer(headKey(), { graphId: graph.graphId, savedAt });
|
|
115
|
+
});
|
|
116
|
+
},
|
|
117
|
+
async load(graphId) {
|
|
118
|
+
return loadUnchecked(graphId);
|
|
119
|
+
},
|
|
120
|
+
async findByNodeId(id) {
|
|
121
|
+
return loadGraphForNode(id);
|
|
122
|
+
},
|
|
123
|
+
async latest() {
|
|
124
|
+
const head = await adapter.get(headKey());
|
|
125
|
+
if (head === null || head === undefined)
|
|
126
|
+
return null;
|
|
127
|
+
return loadUnchecked(head.graphId);
|
|
128
|
+
},
|
|
129
|
+
async listGraphIds() {
|
|
130
|
+
const prefix = `${namespace}${GRAPH_PREFIX}`;
|
|
131
|
+
const ids = [];
|
|
132
|
+
for (const key of await adapter.keys()) {
|
|
133
|
+
if (!key.startsWith(prefix))
|
|
134
|
+
continue;
|
|
135
|
+
ids.push(key.slice(prefix.length));
|
|
136
|
+
}
|
|
137
|
+
return ids.sort();
|
|
138
|
+
},
|
|
139
|
+
async recover() {
|
|
140
|
+
return withStorageLock(adapter, async () => {
|
|
141
|
+
const report = { recoveredHead: false, cleanedIndexKeys: 0 };
|
|
142
|
+
const graphs = await this.listGraphIds();
|
|
143
|
+
const head = await adapter.get(headKey());
|
|
144
|
+
// 1. Head pointer: if absent but graphs exist, adopt the newest record.
|
|
145
|
+
if ((head === null || head === undefined) && graphs.length > 0) {
|
|
146
|
+
let newest = null;
|
|
147
|
+
for (const graphId of graphs) {
|
|
148
|
+
const record = await adapter.get(graphKey(graphId));
|
|
149
|
+
if (!record)
|
|
150
|
+
continue;
|
|
151
|
+
if (!newest || record.savedAt > newest.savedAt) {
|
|
152
|
+
newest = { graphId, savedAt: record.savedAt };
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (newest) {
|
|
156
|
+
await writeIndexIfNewer(headKey(), newest);
|
|
157
|
+
report.recoveredHead = true;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// 2. Drop reverse-index entries pointing at absent graph records.
|
|
161
|
+
const nodePrefix = `${namespace}${NODE_PREFIX}`;
|
|
162
|
+
for (const key of await adapter.keys()) {
|
|
163
|
+
if (!key.startsWith(nodePrefix))
|
|
164
|
+
continue;
|
|
165
|
+
const entry = await adapter.get(key);
|
|
166
|
+
if (!entry)
|
|
167
|
+
continue;
|
|
168
|
+
const graphRecord = await adapter.get(graphKey(entry.graphId));
|
|
169
|
+
if (graphRecord === null || graphRecord === undefined) {
|
|
170
|
+
await adapter.remove(key);
|
|
171
|
+
report.cleanedIndexKeys++;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return report;
|
|
175
|
+
});
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable evidence-chain owner for @totemsdk/proofgraph (RFC-007 G8 / §4.3).
|
|
3
|
+
*
|
|
4
|
+
* Evidence *bytes* are owned by the minimal `ArtifactStore` over a pluggable
|
|
5
|
+
* backend; graph references alone do not satisfy the flight-evidence gate. This
|
|
6
|
+
* module wires a ProofGraph's `evidence` nodes to content-addressed artifacts,
|
|
7
|
+
* and restores the bytes on demand with commit-time digest verification.
|
|
8
|
+
*
|
|
9
|
+
* Restore semantics (RFC-007 §4.3): `corrupt` (bytes present but digest
|
|
10
|
+
* mismatch — tampered or truncated) is surfaced strictly, never collapsed into
|
|
11
|
+
* `not-found` (bytes absent); `unavailable` (backend unreachable, or the
|
|
12
|
+
* backend deleted a blob the local index still references) is also surfaced.
|
|
13
|
+
*/
|
|
14
|
+
import type { ProofGraph } from './types.js';
|
|
15
|
+
import { ArtifactStore } from '@totemsdk/storage/artifacts';
|
|
16
|
+
import type { ArtifactRef } from '@totemsdk/storage/artifacts';
|
|
17
|
+
import type { StorageAdapter } from '@totemsdk/core';
|
|
18
|
+
/**
|
|
19
|
+
* RFC-007 Amendment A (A3): the enforced binding between a claim's commitment
|
|
20
|
+
* and the artifact that addresses it.
|
|
21
|
+
*
|
|
22
|
+
* Invariant: `contentHash === artifact.digest`. `assertEvidenceBinding` throws
|
|
23
|
+
* `StorageError('corrupt')` when it does not hold, so an unrecoverable
|
|
24
|
+
* commitment cannot be constructed or restored.
|
|
25
|
+
*/
|
|
26
|
+
export interface EvidenceArtifactBinding {
|
|
27
|
+
readonly nodeId: string;
|
|
28
|
+
/** The commitment from the claim (hex, with or without `0x`). */
|
|
29
|
+
readonly contentHash: string;
|
|
30
|
+
/** The content-addressed artifact; `artifact.digest` must equal `contentHash`. */
|
|
31
|
+
readonly artifact: ArtifactRef;
|
|
32
|
+
}
|
|
33
|
+
export declare function normalizeEvidenceHash(hash: string): string;
|
|
34
|
+
/** Throws `StorageError('corrupt')` unless `contentHash === artifact.digest`. */
|
|
35
|
+
export declare function assertEvidenceBinding(binding: EvidenceArtifactBinding): void;
|
|
36
|
+
export interface ProofGraphEvidenceStoreOptions {
|
|
37
|
+
/** Namespace the ArtifactStore writes artifact bytes under. */
|
|
38
|
+
namespace?: string;
|
|
39
|
+
/**
|
|
40
|
+
* true (default) — a `corrupt` or `unavailable` evidence read throws
|
|
41
|
+
* `StorageError` (never fail-open). `not-found` is always returned as a
|
|
42
|
+
* result, never thrown. Set false to return audit-able results instead.
|
|
43
|
+
*/
|
|
44
|
+
strict?: boolean;
|
|
45
|
+
}
|
|
46
|
+
/** Per-evidence-node restore result. */
|
|
47
|
+
export interface ProofGraphEvidenceResult {
|
|
48
|
+
nodeId: string;
|
|
49
|
+
status: 'ok' | 'not-found' | 'corrupt' | 'unavailable';
|
|
50
|
+
ref?: ArtifactRef;
|
|
51
|
+
/** The claim commitment this evidence is bound to, when known. */
|
|
52
|
+
contentHash?: string;
|
|
53
|
+
bytes?: Uint8Array;
|
|
54
|
+
message?: string;
|
|
55
|
+
}
|
|
56
|
+
export interface PutEvidenceOptions {
|
|
57
|
+
/**
|
|
58
|
+
* Explicit commitment per node. Falls back to the evidence node's
|
|
59
|
+
* `data.contentHash` (set by `addProof`). When a commitment is known, the
|
|
60
|
+
* stored bytes MUST hash to it and the artifact digest MUST equal it, or the
|
|
61
|
+
* put throws `StorageError('corrupt')`.
|
|
62
|
+
*/
|
|
63
|
+
contentHashFor?: (nodeId: string) => string | undefined;
|
|
64
|
+
/**
|
|
65
|
+
* When true, every evidence node must have a commitment — a missing
|
|
66
|
+
* commitment throws. Default false (back-compat for nodes without one).
|
|
67
|
+
*/
|
|
68
|
+
requireContentHash?: boolean;
|
|
69
|
+
}
|
|
70
|
+
export interface ProofGraphEvidenceStore {
|
|
71
|
+
/**
|
|
72
|
+
* Persist evidence bytes for every `evidence` node of `graph`.
|
|
73
|
+
* `bytesFor` maps a node id to the bytes to store; nodes with no bytes are
|
|
74
|
+
* skipped. When a commitment is known (node `data.contentHash` or
|
|
75
|
+
* `options.contentHashFor`), the claim→hash→artifact binding is enforced:
|
|
76
|
+
* `sha3_256(bytes) === contentHash === artifact.digest`.
|
|
77
|
+
* Returns the refs persisted, in graph node order.
|
|
78
|
+
*/
|
|
79
|
+
putEvidence(graph: ProofGraph, bytesFor: (nodeId: string) => Uint8Array | undefined, options?: PutEvidenceOptions): Promise<ArtifactRef[]>;
|
|
80
|
+
/**
|
|
81
|
+
* Restore evidence bytes for every `evidence` node of `graph`, verifying
|
|
82
|
+
* each artifact's committed digest on read. With `strict` (default), corrupt
|
|
83
|
+
* or unavailable reads throw; otherwise a per-node result is returned.
|
|
84
|
+
*/
|
|
85
|
+
restoreEvidence(graph: ProofGraph): Promise<ProofGraphEvidenceResult[]>;
|
|
86
|
+
/** Count of local index entries (persisted evidence mappings). */
|
|
87
|
+
listCount(): Promise<number>;
|
|
88
|
+
}
|
|
89
|
+
export declare function createProofGraphEvidenceStore(artifacts: ArtifactStore, index: StorageAdapter, options?: ProofGraphEvidenceStoreOptions): ProofGraphEvidenceStore;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Durable evidence-chain owner for @totemsdk/proofgraph (RFC-007 G8 / §4.3).
|
|
4
|
+
*
|
|
5
|
+
* Evidence *bytes* are owned by the minimal `ArtifactStore` over a pluggable
|
|
6
|
+
* backend; graph references alone do not satisfy the flight-evidence gate. This
|
|
7
|
+
* module wires a ProofGraph's `evidence` nodes to content-addressed artifacts,
|
|
8
|
+
* and restores the bytes on demand with commit-time digest verification.
|
|
9
|
+
*
|
|
10
|
+
* Restore semantics (RFC-007 §4.3): `corrupt` (bytes present but digest
|
|
11
|
+
* mismatch — tampered or truncated) is surfaced strictly, never collapsed into
|
|
12
|
+
* `not-found` (bytes absent); `unavailable` (backend unreachable, or the
|
|
13
|
+
* backend deleted a blob the local index still references) is also surfaced.
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.normalizeEvidenceHash = normalizeEvidenceHash;
|
|
17
|
+
exports.assertEvidenceBinding = assertEvidenceBinding;
|
|
18
|
+
exports.createProofGraphEvidenceStore = createProofGraphEvidenceStore;
|
|
19
|
+
const errors_1 = require("@totemsdk/storage/errors");
|
|
20
|
+
const core_1 = require("@totemsdk/core");
|
|
21
|
+
const DEFAULT_NS = 'totem-proofgraph-evidence';
|
|
22
|
+
const INDEX_PREFIX = 'totem_proofgraph_evidence:v1:';
|
|
23
|
+
function normalizeEvidenceHash(hash) {
|
|
24
|
+
return hash.replace(/^0x/i, '').toLowerCase();
|
|
25
|
+
}
|
|
26
|
+
/** Throws `StorageError('corrupt')` unless `contentHash === artifact.digest`. */
|
|
27
|
+
function assertEvidenceBinding(binding) {
|
|
28
|
+
if (normalizeEvidenceHash(binding.contentHash) !== normalizeEvidenceHash(binding.artifact.digest)) {
|
|
29
|
+
throw new errors_1.StorageError(`evidence binding mismatch for node "${binding.nodeId}": contentHash ${binding.contentHash} !== artifact.digest ${binding.artifact.digest}`, 'corrupt', { key: binding.nodeId });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function evidenceNodeIds(graph) {
|
|
33
|
+
return graph.nodes.filter((node) => node.type === 'evidence').map((node) => node.id);
|
|
34
|
+
}
|
|
35
|
+
/** The claim commitment carried on an evidence node (`data.contentHash`). */
|
|
36
|
+
function nodeContentHash(graph, nodeId) {
|
|
37
|
+
const value = graph.nodes.find((node) => node.id === nodeId)?.data?.contentHash;
|
|
38
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
39
|
+
}
|
|
40
|
+
function createProofGraphEvidenceStore(artifacts, index, options = {}) {
|
|
41
|
+
if (!artifacts.capabilities.writable) {
|
|
42
|
+
throw new Error('createProofGraphEvidenceStore requires a writable artifact store');
|
|
43
|
+
}
|
|
44
|
+
const namespace = options.namespace ?? DEFAULT_NS;
|
|
45
|
+
const strict = options.strict ?? true;
|
|
46
|
+
const indexKey = (nodeId) => `${INDEX_PREFIX}${nodeId}`;
|
|
47
|
+
return {
|
|
48
|
+
async putEvidence(graph, bytesFor, options = {}) {
|
|
49
|
+
const refs = [];
|
|
50
|
+
for (const nodeId of evidenceNodeIds(graph)) {
|
|
51
|
+
const bytes = bytesFor(nodeId);
|
|
52
|
+
if (!bytes)
|
|
53
|
+
continue;
|
|
54
|
+
const contentHash = options.contentHashFor?.(nodeId) ?? nodeContentHash(graph, nodeId);
|
|
55
|
+
if (!contentHash && options.requireContentHash) {
|
|
56
|
+
throw new errors_1.StorageError(`evidence commitment missing for node "${nodeId}"`, 'corrupt', { key: indexKey(nodeId) });
|
|
57
|
+
}
|
|
58
|
+
// Byte-identity invariant: the stored bytes must hash to the commitment.
|
|
59
|
+
const digest = (0, core_1.toHex)((0, core_1.sha3_256)(bytes));
|
|
60
|
+
if (contentHash && normalizeEvidenceHash(contentHash) !== digest) {
|
|
61
|
+
throw new errors_1.StorageError(`evidence commitment mismatch for node "${nodeId}": sha3(bytes) ${digest} !== contentHash ${contentHash}`, 'corrupt', { key: indexKey(nodeId) });
|
|
62
|
+
}
|
|
63
|
+
const receipt = await artifacts.put(namespace, bytes).catch((err) => {
|
|
64
|
+
throw new errors_1.StorageError(`evidence put failed for node "${nodeId}": ${err.message}`, 'write-failed', { key: indexKey(nodeId), cause: err });
|
|
65
|
+
});
|
|
66
|
+
if (contentHash) {
|
|
67
|
+
// The artifact must address exactly the committed hash.
|
|
68
|
+
assertEvidenceBinding({ nodeId, contentHash, artifact: receipt.ref });
|
|
69
|
+
}
|
|
70
|
+
const entry = {
|
|
71
|
+
ref: receipt.ref,
|
|
72
|
+
...(contentHash ? { contentHash: normalizeEvidenceHash(contentHash) } : {}),
|
|
73
|
+
};
|
|
74
|
+
await index.set(indexKey(nodeId), entry);
|
|
75
|
+
refs.push(receipt.ref);
|
|
76
|
+
}
|
|
77
|
+
return refs;
|
|
78
|
+
},
|
|
79
|
+
async restoreEvidence(graph) {
|
|
80
|
+
const results = [];
|
|
81
|
+
const fail = (err) => {
|
|
82
|
+
if (err instanceof errors_1.StorageError)
|
|
83
|
+
throw err;
|
|
84
|
+
throw new errors_1.StorageError(`evidence read failed: ${err.message}`, 'unavailable', { cause: err });
|
|
85
|
+
};
|
|
86
|
+
for (const nodeId of evidenceNodeIds(graph)) {
|
|
87
|
+
const entry = await index.get(indexKey(nodeId));
|
|
88
|
+
if (!entry) {
|
|
89
|
+
results.push({ nodeId, status: 'not-found', message: 'no evidence artifact persisted for node' });
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const { ref, contentHash } = entry;
|
|
93
|
+
if (contentHash) {
|
|
94
|
+
// Index-tamper guard: the enforced binding must still hold.
|
|
95
|
+
assertEvidenceBinding({ nodeId, contentHash, artifact: ref });
|
|
96
|
+
}
|
|
97
|
+
const read = await artifacts.get(ref).catch(fail);
|
|
98
|
+
switch (read.status) {
|
|
99
|
+
case 'ok':
|
|
100
|
+
results.push({ nodeId, status: 'ok', ref, ...(contentHash ? { contentHash } : {}), bytes: read.bytes });
|
|
101
|
+
break;
|
|
102
|
+
case 'corrupt':
|
|
103
|
+
if (strict) {
|
|
104
|
+
throw new errors_1.StorageError(`evidence artifact corrupt for node "${nodeId}": ${read.message ?? 'digest mismatch'}`, 'corrupt', { key: indexKey(nodeId) });
|
|
105
|
+
}
|
|
106
|
+
results.push({ nodeId, status: 'corrupt', ref, message: read.message ?? 'digest mismatch' });
|
|
107
|
+
break;
|
|
108
|
+
case 'unavailable':
|
|
109
|
+
if (strict) {
|
|
110
|
+
throw new errors_1.StorageError(`evidence artifact unavailable for node "${nodeId}": ${read.message ?? 'backend unavailable'}`, 'unavailable', { key: indexKey(nodeId) });
|
|
111
|
+
}
|
|
112
|
+
results.push({ nodeId, status: 'unavailable', ref, message: read.message ?? 'backend unavailable' });
|
|
113
|
+
break;
|
|
114
|
+
case 'not-found':
|
|
115
|
+
results.push({ nodeId, status: 'not-found', ref, message: read.message ?? 'evidence artifact not found' });
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return results;
|
|
120
|
+
},
|
|
121
|
+
async listCount() {
|
|
122
|
+
let count = 0;
|
|
123
|
+
for (const key of await index.keys()) {
|
|
124
|
+
if (key.startsWith(INDEX_PREFIX))
|
|
125
|
+
count++;
|
|
126
|
+
}
|
|
127
|
+
return count;
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Proof graph mutation functions — all pure, all returning NEW graphs.
|
|
3
|
+
*
|
|
4
|
+
* Immutable pattern: every mutation returns a brand-new ProofGraph with a
|
|
5
|
+
* recomputed graphId. Nodes and edges are deduplicated by ID so adding the
|
|
6
|
+
* same proof twice is idempotent.
|
|
7
|
+
*/
|
|
8
|
+
import type { SignedProof } from '@totemsdk/proof';
|
|
9
|
+
import type { SignedIdentityClaim, TotemIdentityDocument } from '@totemsdk/identity';
|
|
10
|
+
import type { SignedManifest } from '@totemsdk/manifest';
|
|
11
|
+
import type { ProofGraph, ProofGraphEdge, ProofGraphEdgeType, ProofGraphNodeType, ReceiptLikeInput, AnchorInput } from './types.js';
|
|
12
|
+
/**
|
|
13
|
+
* Create an empty ProofGraph.
|
|
14
|
+
*/
|
|
15
|
+
export declare function createProofGraph(metadata?: Record<string, unknown>): ProofGraph;
|
|
16
|
+
/**
|
|
17
|
+
* Attach or replace mutable metadata. Does NOT affect graphId.
|
|
18
|
+
*/
|
|
19
|
+
export declare function setGraphMetadata(graph: ProofGraph, metadata: Record<string, unknown>): ProofGraph;
|
|
20
|
+
/**
|
|
21
|
+
* Build a ProofGraphEdge with a deterministic ID from its fields.
|
|
22
|
+
* Convenience helper so callers don't need to import computeEdgeId.
|
|
23
|
+
*/
|
|
24
|
+
export declare function buildEdge(type: ProofGraphEdgeType, from: string, to: string, proofId?: string, data?: Record<string, unknown>): ProofGraphEdge;
|
|
25
|
+
/**
|
|
26
|
+
* Add a node directly (type + refId pair). Idempotent — skipped if the node already exists.
|
|
27
|
+
*/
|
|
28
|
+
export declare function addNode(graph: ProofGraph, type: ProofGraphNodeType, refId: string, data?: Record<string, unknown>): ProofGraph;
|
|
29
|
+
/**
|
|
30
|
+
* Append a ProofGraphEdge and recompute graphId. Idempotent by edge ID.
|
|
31
|
+
* Spec API: addEdge(graph, edge). Use buildEdge() to construct the edge object.
|
|
32
|
+
*/
|
|
33
|
+
export declare function addEdge(graph: ProofGraph, edge: ProofGraphEdge): ProofGraph;
|
|
34
|
+
/**
|
|
35
|
+
* Index a TotemIdentityDocument into the graph.
|
|
36
|
+
* Creates an 'identity' node with refId = doc.id.
|
|
37
|
+
*/
|
|
38
|
+
export declare function addIdentityDocument(graph: ProofGraph, doc: TotemIdentityDocument): ProofGraph;
|
|
39
|
+
/**
|
|
40
|
+
* Index a SignedProof into the graph.
|
|
41
|
+
*
|
|
42
|
+
* Nodes created:
|
|
43
|
+
* proof — proofId (stores the full SignedProof in .data)
|
|
44
|
+
* identity — signature.address (issuer key address)
|
|
45
|
+
* subject — proof.subject.id
|
|
46
|
+
* evidence — ev.id (one per evidence ref)
|
|
47
|
+
* anchor — anchor.hash (if present)
|
|
48
|
+
*
|
|
49
|
+
* Edges created (all referencing the proofId):
|
|
50
|
+
* proves proof → subject
|
|
51
|
+
* issued_by proof → identity (signer)
|
|
52
|
+
* about proof → subject
|
|
53
|
+
* references proof → evidence (one per evidence ref, in array order)
|
|
54
|
+
* anchored_to proof → anchor (if present)
|
|
55
|
+
*/
|
|
56
|
+
export declare function addProof(graph: ProofGraph, signedProof: SignedProof): ProofGraph;
|
|
57
|
+
/**
|
|
58
|
+
* Index a SignedIdentityClaim into the graph.
|
|
59
|
+
*
|
|
60
|
+
* Nodes created:
|
|
61
|
+
* identity-claim — claim.id (stores the full signed claim in .data)
|
|
62
|
+
* address — issuer address
|
|
63
|
+
* address/identity — target address (for delegates_to / rotates_to / revokes)
|
|
64
|
+
*
|
|
65
|
+
* Edges created based on claim.type:
|
|
66
|
+
* all types → issued_by (claim-node → issuer address)
|
|
67
|
+
* delegates_to → delegates_to edge (claim-node → delegated address)
|
|
68
|
+
* revokes → revokes edge (claim-node → subject identity)
|
|
69
|
+
* rotates_to → controls edge (claim-node → new address)
|
|
70
|
+
*/
|
|
71
|
+
export declare function addIdentityClaim(graph: ProofGraph, signedClaim: SignedIdentityClaim): ProofGraph;
|
|
72
|
+
/**
|
|
73
|
+
* Index a SignedManifest into the graph.
|
|
74
|
+
*
|
|
75
|
+
* Nodes created:
|
|
76
|
+
* manifest — computed manifest ID (stores manifest content in .data)
|
|
77
|
+
* address — resolved author/agent/operator address
|
|
78
|
+
*
|
|
79
|
+
* Edge created:
|
|
80
|
+
* manifests_as manifest → address
|
|
81
|
+
*/
|
|
82
|
+
export declare function addManifest(graph: ProofGraph, signedManifest: SignedManifest): ProofGraph;
|
|
83
|
+
/**
|
|
84
|
+
* Add a signed manifest **revision** without collapsing other revisions.
|
|
85
|
+
*
|
|
86
|
+
* Creates:
|
|
87
|
+
* manifest (logical) refId = manifestId
|
|
88
|
+
* manifest (revision) refId = manifestEnvelopeId, data = { manifestId, manifestEnvelopeId, signedManifest }
|
|
89
|
+
* address resolved author/operator address
|
|
90
|
+
* manifests_as manifestId → address
|
|
91
|
+
* revision_of manifestEnvelopeId → manifestId
|
|
92
|
+
*/
|
|
93
|
+
export declare function addManifestRevision(graph: ProofGraph, signedManifest: SignedManifest): ProofGraph;
|
|
94
|
+
/**
|
|
95
|
+
* Add a receipt-like node (payment, subscription, claim receipt) to the graph.
|
|
96
|
+
* If receipt.proofId is set, adds a 'supports' edge from the receipt to that proof.
|
|
97
|
+
*/
|
|
98
|
+
export declare function addReceiptLike(graph: ProofGraph, receipt: ReceiptLikeInput): ProofGraph;
|
|
99
|
+
/**
|
|
100
|
+
* Add a standalone anchor node. If anchor.proofId is set, adds an 'anchored_to'
|
|
101
|
+
* edge from the proof to this anchor.
|
|
102
|
+
*/
|
|
103
|
+
export declare function addAnchor(graph: ProofGraph, anchor: AnchorInput): ProofGraph;
|