@totemsdk/proofgraph 0.1.2 → 1.0.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/dist/durable-store.d.ts +50 -0
- package/dist/durable-store.js +178 -0
- package/dist/evidence.d.ts +89 -0
- package/dist/evidence.js +130 -0
- package/dist/graph.js +4 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +7 -1
- package/package.json +9 -8
- package/rust/pkg/package.json +1 -1
- package/rust/pkg/proofgraph_wasm_bg.wasm +0 -0
- package/rust/pkg-node/package.json +1 -1
- package/rust/pkg-node/proofgraph_wasm_bg.wasm +0 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable `ProofGraphStoragePort` adapter for @totemsdk/proofgraph.
|
|
3
|
+
*
|
|
4
|
+
* Implements RFC-007 G8 / §4.3: graph + reverse node index are persisted over a
|
|
5
|
+
* shared `StorageAdapter`, with restart recovery and concurrent-update safety
|
|
6
|
+
* (CAS guards on the head pointer and node index, serialized per adapter).
|
|
7
|
+
*
|
|
8
|
+
* Layout (single `StorageAdapter`, prefixed keys):
|
|
9
|
+
* graph:<graphId> { json, savedAt } immutable content-addressed record
|
|
10
|
+
* node:<nodeId> { graphId, savedAt } reverse index (most-recent
|
|
11
|
+
* snapshot that contains the node)
|
|
12
|
+
* head { graphId, savedAt } most-recent saved graph
|
|
13
|
+
*
|
|
14
|
+
* `load()` runs `importProofGraph`, which validates every node/edge and
|
|
15
|
+
* recomputes the graphId — a tampered or truncated record surfaces as
|
|
16
|
+
* `StorageError` `corrupt`, never as `not-found` (RFC-007 §4.2 strict).
|
|
17
|
+
*/
|
|
18
|
+
import type { StorageAdapter } from '@totemsdk/core';
|
|
19
|
+
import type { WriteAckMode } from '@totemsdk/storage/types';
|
|
20
|
+
import type { ProofGraph, ProofGraphStoragePort } from './types.js';
|
|
21
|
+
export interface DurableProofGraphStoreOptions {
|
|
22
|
+
/** Key prefix (default 'totem_proofgraph:v1:'). */
|
|
23
|
+
namespace?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Required write-ack level the backing adapter must satisfy (default
|
|
26
|
+
* 'durably-acknowledged').
|
|
27
|
+
*/
|
|
28
|
+
requireAckMode?: WriteAckMode;
|
|
29
|
+
}
|
|
30
|
+
export interface ProofGraphRecoveryReport {
|
|
31
|
+
recoveredHead: boolean;
|
|
32
|
+
cleanedIndexKeys: number;
|
|
33
|
+
}
|
|
34
|
+
export type DurableProofGraphStore = ProofGraphStoragePort & {
|
|
35
|
+
/** Load the most-recent saved graph (the head). */
|
|
36
|
+
latest(): Promise<ProofGraph | null>;
|
|
37
|
+
/** List all persisted graphIds. */
|
|
38
|
+
listGraphIds(): Promise<string[]>;
|
|
39
|
+
/** Reconcile head + index after a crash/interruption. */
|
|
40
|
+
recover(): Promise<ProofGraphRecoveryReport>;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Create a durable `ProofGraphStoragePort` over a CAS-capable adapter.
|
|
44
|
+
*
|
|
45
|
+
* The head pointer and every reverse-index entry are guarded with the
|
|
46
|
+
* adapter's `conditionalUpdate`, and all mutations run serialized per adapter,
|
|
47
|
+
* so concurrent saves never clobber a newer graph. A non-CAS or low-durability
|
|
48
|
+
* adapter is rejected at construction (RFC-007 §4.2 no-silent-downgrade).
|
|
49
|
+
*/
|
|
50
|
+
export declare function createDurableProofGraphStore(adapter: StorageAdapter, options?: DurableProofGraphStoreOptions): DurableProofGraphStore;
|
|
@@ -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;
|
package/dist/evidence.js
ADDED
|
@@ -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
|
+
}
|
package/dist/graph.js
CHANGED
|
@@ -20,6 +20,7 @@ exports.addReceiptLike = addReceiptLike;
|
|
|
20
20
|
exports.addAnchor = addAnchor;
|
|
21
21
|
const manifest_1 = require("@totemsdk/manifest");
|
|
22
22
|
const canonical_js_1 = require("./canonical.js");
|
|
23
|
+
const evidence_js_1 = require("./evidence.js");
|
|
23
24
|
function makeNode(type, refId, data) {
|
|
24
25
|
return {
|
|
25
26
|
id: (0, canonical_js_1.computeNodeId)(type, refId),
|
|
@@ -150,7 +151,9 @@ function addProof(graph, signedProof) {
|
|
|
150
151
|
];
|
|
151
152
|
if (signedProof.evidence) {
|
|
152
153
|
for (const ev of signedProof.evidence) {
|
|
153
|
-
|
|
154
|
+
// RFC-007 Amendment A (A3): carry the claim commitment on the evidence
|
|
155
|
+
// node so the evidence store can enforce claim→contentHash→artifact.
|
|
156
|
+
newNodes.push(makeNode('evidence', ev.id, ev.hash ? { contentHash: (0, evidence_js_1.normalizeEvidenceHash)(ev.hash) } : undefined));
|
|
154
157
|
newEdges.push(makeEdge('references', signedProof.proofId, ev.id, signedProof.proofId));
|
|
155
158
|
}
|
|
156
159
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -14,6 +14,10 @@
|
|
|
14
14
|
* const result = verifyProofGraph(g1);
|
|
15
15
|
*/
|
|
16
16
|
export type { ProofGraphNodeType, ProofGraphEdgeType, ProofGraphNode, ProofGraphEdge, ProofGraph, ProofGraphVerifyResult, ProofGraphStoragePort, ReceiptLikeInput, AnchorInput, } from './types.js';
|
|
17
|
+
export { createDurableProofGraphStore, } from './durable-store.js';
|
|
18
|
+
export type { DurableProofGraphStore, DurableProofGraphStoreOptions, ProofGraphRecoveryReport, } from './durable-store.js';
|
|
19
|
+
export { createProofGraphEvidenceStore, assertEvidenceBinding, normalizeEvidenceHash, } from './evidence.js';
|
|
20
|
+
export type { ProofGraphEvidenceStore, ProofGraphEvidenceStoreOptions, ProofGraphEvidenceResult, EvidenceArtifactBinding, PutEvidenceOptions, } from './evidence.js';
|
|
17
21
|
export { toHex, canonicalJson, computeNodeId, computeEdgeId, computeProofGraphId, recomputeGraphId, } from './canonical.js';
|
|
18
22
|
export { createProofGraph, setGraphMetadata, buildEdge, addNode, addEdge, addIdentityDocument, addProof, addIdentityClaim, addManifest, addReceiptLike, addAnchor, } from './graph.js';
|
|
19
23
|
export { findNode, getEdgesFrom, getEdgesTo, getEdgesByType, getProofNodes, findProofsBySubject, findProofsByIssuer, findAnchorsForProof, findRevocations, findSupersessions, findConflicts, getEvidenceTrail, getProofLineage, resolveCurrentProofSet, getManifestsForAddress, getEdgesBetween, reachableFrom, } from './query.js';
|
package/dist/index.js
CHANGED
|
@@ -15,7 +15,13 @@
|
|
|
15
15
|
* const result = verifyProofGraph(g1);
|
|
16
16
|
*/
|
|
17
17
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
-
exports.importProofGraph = exports.exportProofGraph = exports.verifyGraphProofs = exports.verifyProofGraph = exports.reachableFrom = exports.getEdgesBetween = exports.getManifestsForAddress = exports.resolveCurrentProofSet = exports.getProofLineage = exports.getEvidenceTrail = exports.findConflicts = exports.findSupersessions = exports.findRevocations = exports.findAnchorsForProof = exports.findProofsByIssuer = exports.findProofsBySubject = exports.getProofNodes = exports.getEdgesByType = exports.getEdgesTo = exports.getEdgesFrom = exports.findNode = exports.addAnchor = exports.addReceiptLike = exports.addManifest = exports.addIdentityClaim = exports.addProof = exports.addIdentityDocument = exports.addEdge = exports.addNode = exports.buildEdge = exports.setGraphMetadata = exports.createProofGraph = exports.recomputeGraphId = exports.computeProofGraphId = exports.computeEdgeId = exports.computeNodeId = exports.canonicalJson = exports.toHex = void 0;
|
|
18
|
+
exports.importProofGraph = exports.exportProofGraph = exports.verifyGraphProofs = exports.verifyProofGraph = exports.reachableFrom = exports.getEdgesBetween = exports.getManifestsForAddress = exports.resolveCurrentProofSet = exports.getProofLineage = exports.getEvidenceTrail = exports.findConflicts = exports.findSupersessions = exports.findRevocations = exports.findAnchorsForProof = exports.findProofsByIssuer = exports.findProofsBySubject = exports.getProofNodes = exports.getEdgesByType = exports.getEdgesTo = exports.getEdgesFrom = exports.findNode = exports.addAnchor = exports.addReceiptLike = exports.addManifest = exports.addIdentityClaim = exports.addProof = exports.addIdentityDocument = exports.addEdge = exports.addNode = exports.buildEdge = exports.setGraphMetadata = exports.createProofGraph = exports.recomputeGraphId = exports.computeProofGraphId = exports.computeEdgeId = exports.computeNodeId = exports.canonicalJson = exports.toHex = exports.normalizeEvidenceHash = exports.assertEvidenceBinding = exports.createProofGraphEvidenceStore = exports.createDurableProofGraphStore = void 0;
|
|
19
|
+
var durable_store_js_1 = require("./durable-store.js");
|
|
20
|
+
Object.defineProperty(exports, "createDurableProofGraphStore", { enumerable: true, get: function () { return durable_store_js_1.createDurableProofGraphStore; } });
|
|
21
|
+
var evidence_js_1 = require("./evidence.js");
|
|
22
|
+
Object.defineProperty(exports, "createProofGraphEvidenceStore", { enumerable: true, get: function () { return evidence_js_1.createProofGraphEvidenceStore; } });
|
|
23
|
+
Object.defineProperty(exports, "assertEvidenceBinding", { enumerable: true, get: function () { return evidence_js_1.assertEvidenceBinding; } });
|
|
24
|
+
Object.defineProperty(exports, "normalizeEvidenceHash", { enumerable: true, get: function () { return evidence_js_1.normalizeEvidenceHash; } });
|
|
19
25
|
var canonical_js_1 = require("./canonical.js"); // canonicalJson / toHex re-exported from @totemsdk/proof
|
|
20
26
|
Object.defineProperty(exports, "toHex", { enumerable: true, get: function () { return canonical_js_1.toHex; } });
|
|
21
27
|
Object.defineProperty(exports, "canonicalJson", { enumerable: true, get: function () { return canonical_js_1.canonicalJson; } });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@totemsdk/proofgraph",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Local deterministic proof relationship graph for Totem Edge — indexes proofs, identities, manifests, and subjects into a queryable content-addressed DAG",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -19,10 +19,11 @@
|
|
|
19
19
|
"LICENSE"
|
|
20
20
|
],
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@totemsdk/core": "^1.2.
|
|
23
|
-
"@totemsdk/proof": "^0.
|
|
24
|
-
"@totemsdk/
|
|
25
|
-
"@totemsdk/
|
|
22
|
+
"@totemsdk/core": "^1.2.11",
|
|
23
|
+
"@totemsdk/proof": "^1.0.0",
|
|
24
|
+
"@totemsdk/storage": "^0.1.0",
|
|
25
|
+
"@totemsdk/manifest": "^1.0.0",
|
|
26
|
+
"@totemsdk/identity": "^1.0.1"
|
|
26
27
|
},
|
|
27
28
|
"devDependencies": {
|
|
28
29
|
"@types/jest": "^30.0.0",
|
|
@@ -39,13 +40,13 @@
|
|
|
39
40
|
},
|
|
40
41
|
"license": "MIT",
|
|
41
42
|
"author": "Totem SDK",
|
|
42
|
-
"homepage": "https://totem.ing",
|
|
43
|
+
"homepage": "https://docs.totem.ing",
|
|
43
44
|
"bugs": {
|
|
44
|
-
"url": "https://github.com/
|
|
45
|
+
"url": "https://github.com/Totem-Edge/totem-sdk/issues"
|
|
45
46
|
},
|
|
46
47
|
"repository": {
|
|
47
48
|
"type": "git",
|
|
48
|
-
"url": "git+https://github.com/
|
|
49
|
+
"url": "git+https://github.com/Totem-Edge/totem-sdk.git",
|
|
49
50
|
"directory": "packages/proofgraph"
|
|
50
51
|
},
|
|
51
52
|
"keywords": [
|
package/rust/pkg/package.json
CHANGED
|
Binary file
|
|
Binary file
|