@totemsdk/proofgraph 1.0.0 → 1.1.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 +202 -21
- package/NOTICE +60 -0
- package/README.md +1 -1
- 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.d.ts +50 -0
- package/dist/durable-store.js +175 -0
- package/dist/evidence.d.ts +89 -0
- package/dist/evidence.js +125 -0
- package/dist/graph.d.ts +11 -0
- package/dist/graph.js +49 -33
- package/dist/index.d.ts +7 -2
- package/dist/index.js +8 -46
- package/dist/io.js +4 -8
- package/dist/query.d.ts +4 -0
- package/dist/query.js +28 -36
- 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 +22 -14
- 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,98 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* RFC-057 Phase 4 — replicated proofgraph evidence adapter.
|
|
4
|
+
*
|
|
5
|
+
* Keeps the authoritative proofgraph evidence store local while allowing a
|
|
6
|
+
* replicated index to locate the same digest-verified artifacts on other
|
|
7
|
+
* peers. The replicated index is never trusted for bytes: every read
|
|
8
|
+
* re-verifies the artifact digest.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.createReplicatedProofGraphEvidenceStore = createReplicatedProofGraphEvidenceStore;
|
|
12
|
+
const core_1 = require("@totemsdk/core");
|
|
13
|
+
const errors_1 = require("@totemsdk/storage/errors");
|
|
14
|
+
const KEY_PREFIX = 'proofgraph:evidence:';
|
|
15
|
+
const DEFAULT_NAMESPACE = 'totem-proofgraph-evidence';
|
|
16
|
+
function normalizeHash(value) {
|
|
17
|
+
return value.replace(/^0x/i, '').toLowerCase();
|
|
18
|
+
}
|
|
19
|
+
function evidenceNodes(graph) {
|
|
20
|
+
return graph.nodes.filter((node) => node.type === 'evidence');
|
|
21
|
+
}
|
|
22
|
+
function contentHashOf(node) {
|
|
23
|
+
const value = node.data?.contentHash;
|
|
24
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
25
|
+
}
|
|
26
|
+
function createReplicatedProofGraphEvidenceStore(options) {
|
|
27
|
+
if (!options.artifacts.capabilities.writable) {
|
|
28
|
+
throw new Error('replicated evidence store requires a writable artifact store');
|
|
29
|
+
}
|
|
30
|
+
const namespace = options.namespace ?? DEFAULT_NAMESPACE;
|
|
31
|
+
const key = (nodeId) => `${KEY_PREFIX}${nodeId}`;
|
|
32
|
+
return {
|
|
33
|
+
async putEvidence(graph, bytesFor) {
|
|
34
|
+
const refs = [];
|
|
35
|
+
for (const node of evidenceNodes(graph)) {
|
|
36
|
+
const bytes = bytesFor(node.id);
|
|
37
|
+
if (!bytes)
|
|
38
|
+
continue;
|
|
39
|
+
const digest = (0, core_1.toHex)((0, core_1.sha3_256)(bytes));
|
|
40
|
+
const contentHash = contentHashOf(node);
|
|
41
|
+
if (contentHash && normalizeHash(contentHash) !== digest) {
|
|
42
|
+
throw new errors_1.StorageError(`evidence commitment mismatch for node "${node.id}"`, 'corrupt', {
|
|
43
|
+
key: key(node.id),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
const receipt = await options.artifacts.put(namespace, bytes);
|
|
47
|
+
if (contentHash && normalizeHash(contentHash) !== receipt.ref.digest) {
|
|
48
|
+
throw new errors_1.StorageError(`artifact digest mismatch for node "${node.id}"`, 'corrupt', {
|
|
49
|
+
key: key(node.id),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
await options.index.put(key(node.id), {
|
|
53
|
+
nodeId: node.id,
|
|
54
|
+
ref: receipt.ref,
|
|
55
|
+
contentHash: contentHash ?? receipt.ref.digest,
|
|
56
|
+
storedAt: Date.now(),
|
|
57
|
+
});
|
|
58
|
+
refs.push(receipt.ref);
|
|
59
|
+
}
|
|
60
|
+
return refs;
|
|
61
|
+
},
|
|
62
|
+
async getEvidence(nodeId) {
|
|
63
|
+
const raw = await options.index.get(key(nodeId));
|
|
64
|
+
if (!raw)
|
|
65
|
+
return { nodeId, status: 'not-found' };
|
|
66
|
+
const entry = raw;
|
|
67
|
+
const read = await options.artifacts.get(entry.ref);
|
|
68
|
+
if (read.status !== 'ok' || !read.bytes) {
|
|
69
|
+
return {
|
|
70
|
+
nodeId,
|
|
71
|
+
status: read.status,
|
|
72
|
+
ref: entry.ref,
|
|
73
|
+
...(read.message ? { message: read.message } : {}),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
const digest = (0, core_1.toHex)((0, core_1.sha3_256)(read.bytes));
|
|
77
|
+
if (normalizeHash(entry.contentHash) !== digest) {
|
|
78
|
+
return {
|
|
79
|
+
nodeId,
|
|
80
|
+
status: 'corrupt',
|
|
81
|
+
ref: entry.ref,
|
|
82
|
+
message: `evidence digest mismatch: ${digest} !== ${entry.contentHash}`,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return { nodeId, status: 'ok', ref: entry.ref, bytes: read.bytes };
|
|
86
|
+
},
|
|
87
|
+
async verifyEvidence(nodeId) {
|
|
88
|
+
return (await this.getEvidence(nodeId)).status === 'ok';
|
|
89
|
+
},
|
|
90
|
+
async *listEvidence() {
|
|
91
|
+
if (!options.index.list)
|
|
92
|
+
return;
|
|
93
|
+
for await (const entry of options.index.list(KEY_PREFIX)) {
|
|
94
|
+
yield entry.value;
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @totemsdk/proofgraph — Type definitions
|
|
3
|
+
*
|
|
4
|
+
* Pure schema — no network, no DHT, no blockchain submission, no crypto.
|
|
5
|
+
*/
|
|
6
|
+
export type ProofGraphNodeType = 'proof' | 'identity' | 'identity-claim' | 'manifest' | 'address' | 'subject' | 'evidence' | 'anchor' | 'receipt' | 'payment' | 'device' | 'service' | 'policy' | 'custom';
|
|
7
|
+
export type ProofGraphEdgeType = 'proves' | 'issued_by' | 'signed_by' | 'about' | 'references' | 'derived_from' | 'supports' | 'contradicts' | 'supersedes' | 'revokes' | 'anchored_to' | 'delegates_to' | 'controls' | 'depends_on' | 'manifests_as' | 'revision_of' | 'conflicts_with';
|
|
8
|
+
export interface ProofGraphNode {
|
|
9
|
+
id: string;
|
|
10
|
+
type: ProofGraphNodeType;
|
|
11
|
+
refId: string;
|
|
12
|
+
data?: Record<string, unknown>;
|
|
13
|
+
createdAt: number;
|
|
14
|
+
}
|
|
15
|
+
export interface ProofGraphEdge {
|
|
16
|
+
id: string;
|
|
17
|
+
type: ProofGraphEdgeType;
|
|
18
|
+
from: string;
|
|
19
|
+
to: string;
|
|
20
|
+
proofId?: string;
|
|
21
|
+
data?: Record<string, unknown>;
|
|
22
|
+
}
|
|
23
|
+
export interface ProofGraph {
|
|
24
|
+
graphId: string;
|
|
25
|
+
nodes: ProofGraphNode[];
|
|
26
|
+
edges: ProofGraphEdge[];
|
|
27
|
+
createdAt: number;
|
|
28
|
+
metadata?: Record<string, unknown>;
|
|
29
|
+
}
|
|
30
|
+
export interface ProofGraphVerifyResult {
|
|
31
|
+
valid: boolean;
|
|
32
|
+
invalidProofs: string[];
|
|
33
|
+
reason?: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Input object for addReceiptLike.
|
|
37
|
+
*/
|
|
38
|
+
export interface ReceiptLikeInput {
|
|
39
|
+
id: string;
|
|
40
|
+
data?: Record<string, unknown>;
|
|
41
|
+
proofId?: string;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Input object for addAnchor.
|
|
45
|
+
*/
|
|
46
|
+
export interface AnchorInput {
|
|
47
|
+
hash: string;
|
|
48
|
+
provider?: string;
|
|
49
|
+
txId?: string;
|
|
50
|
+
confirmedAt?: number;
|
|
51
|
+
metadata?: Record<string, unknown>;
|
|
52
|
+
proofId?: string;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Storage port — interface only, no concrete implementation in this package.
|
|
56
|
+
* Adapters (SQLite, LevelDB, in-memory) live in consumer packages.
|
|
57
|
+
*/
|
|
58
|
+
export interface ProofGraphStoragePort {
|
|
59
|
+
save(graph: ProofGraph): Promise<void>;
|
|
60
|
+
load(graphId: string): Promise<ProofGraph | null>;
|
|
61
|
+
findByNodeId(id: string): Promise<ProofGraph | null>;
|
|
62
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Graph-level verification.
|
|
3
|
+
*
|
|
4
|
+
* verifyProofGraph: iterates proof nodes, recovers SignedProof from node.data,
|
|
5
|
+
* and delegates to @totemsdk/proof's verifyProof for each one.
|
|
6
|
+
* Never reimplements cryptography.
|
|
7
|
+
*
|
|
8
|
+
* verifyGraphProofs: same logic, returns the list of invalid proof IDs (string[]).
|
|
9
|
+
*/
|
|
10
|
+
import type { ProofGraph, ProofGraphVerifyResult } from './types.js';
|
|
11
|
+
/**
|
|
12
|
+
* Verify every proof node stored in the graph.
|
|
13
|
+
*
|
|
14
|
+
* Iterates nodes of type 'proof', casts node.data back to SignedProof,
|
|
15
|
+
* and calls proofModule.verifyProof() from @totemsdk/proof.
|
|
16
|
+
* Returns { valid: true } if ALL proofs pass; otherwise lists the failing proofIds.
|
|
17
|
+
*/
|
|
18
|
+
export declare function verifyProofGraph(graph: ProofGraph, _options?: Record<string, unknown>): ProofGraphVerifyResult;
|
|
19
|
+
/**
|
|
20
|
+
* Returns the list of invalid proof IDs in the graph.
|
|
21
|
+
* Equivalent to verifyProofGraph(graph).invalidProofs.
|
|
22
|
+
*/
|
|
23
|
+
export declare function verifyGraphProofs(graph: ProofGraph): string[];
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Graph-level verification.
|
|
4
|
+
*
|
|
5
|
+
* verifyProofGraph: iterates proof nodes, recovers SignedProof from node.data,
|
|
6
|
+
* and delegates to @totemsdk/proof's verifyProof for each one.
|
|
7
|
+
* Never reimplements cryptography.
|
|
8
|
+
*
|
|
9
|
+
* verifyGraphProofs: same logic, returns the list of invalid proof IDs (string[]).
|
|
10
|
+
*/
|
|
11
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
12
|
+
if (k2 === undefined) k2 = k;
|
|
13
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
14
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
15
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
16
|
+
}
|
|
17
|
+
Object.defineProperty(o, k2, desc);
|
|
18
|
+
}) : (function(o, m, k, k2) {
|
|
19
|
+
if (k2 === undefined) k2 = k;
|
|
20
|
+
o[k2] = m[k];
|
|
21
|
+
}));
|
|
22
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
23
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
24
|
+
}) : function(o, v) {
|
|
25
|
+
o["default"] = v;
|
|
26
|
+
});
|
|
27
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
28
|
+
var ownKeys = function(o) {
|
|
29
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
30
|
+
var ar = [];
|
|
31
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
32
|
+
return ar;
|
|
33
|
+
};
|
|
34
|
+
return ownKeys(o);
|
|
35
|
+
};
|
|
36
|
+
return function (mod) {
|
|
37
|
+
if (mod && mod.__esModule) return mod;
|
|
38
|
+
var result = {};
|
|
39
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
40
|
+
__setModuleDefault(result, mod);
|
|
41
|
+
return result;
|
|
42
|
+
};
|
|
43
|
+
})();
|
|
44
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
|
+
exports.verifyProofGraph = verifyProofGraph;
|
|
46
|
+
exports.verifyGraphProofs = verifyGraphProofs;
|
|
47
|
+
const proofModule = __importStar(require("@totemsdk/proof"));
|
|
48
|
+
/**
|
|
49
|
+
* Verify every proof node stored in the graph.
|
|
50
|
+
*
|
|
51
|
+
* Iterates nodes of type 'proof', casts node.data back to SignedProof,
|
|
52
|
+
* and calls proofModule.verifyProof() from @totemsdk/proof.
|
|
53
|
+
* Returns { valid: true } if ALL proofs pass; otherwise lists the failing proofIds.
|
|
54
|
+
*/
|
|
55
|
+
function verifyProofGraph(graph, _options) {
|
|
56
|
+
const invalidProofs = [];
|
|
57
|
+
for (const node of graph.nodes) {
|
|
58
|
+
if (node.type !== 'proof' || !node.data)
|
|
59
|
+
continue;
|
|
60
|
+
const signedProof = node.data;
|
|
61
|
+
const result = proofModule.verifyProof(signedProof);
|
|
62
|
+
if (!result.valid) {
|
|
63
|
+
invalidProofs.push(node.refId);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
valid: invalidProofs.length === 0,
|
|
68
|
+
invalidProofs,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Returns the list of invalid proof IDs in the graph.
|
|
73
|
+
* Equivalent to verifyProofGraph(graph).invalidProofs.
|
|
74
|
+
*/
|
|
75
|
+
function verifyGraphProofs(graph) {
|
|
76
|
+
return verifyProofGraph(graph).invalidProofs;
|
|
77
|
+
}
|
|
@@ -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,175 @@
|
|
|
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 { assertCapabilities } from '@totemsdk/storage/types';
|
|
19
|
+
import { StorageError } from '@totemsdk/storage/errors';
|
|
20
|
+
import { exportProofGraph, importProofGraph } from './io.js';
|
|
21
|
+
const DEFAULT_NAMESPACE = 'totem_proofgraph:v1:';
|
|
22
|
+
const GRAPH_PREFIX = 'graph:';
|
|
23
|
+
const NODE_PREFIX = 'node:';
|
|
24
|
+
const HEAD_KEY = 'head';
|
|
25
|
+
/** Per-adapter mutation lock so concurrent saves never interleave (wots-lease pattern). */
|
|
26
|
+
const storageTails = new WeakMap();
|
|
27
|
+
function withStorageLock(storage, fn) {
|
|
28
|
+
const previous = storageTails.get(storage) ?? Promise.resolve();
|
|
29
|
+
let release;
|
|
30
|
+
const gate = new Promise((resolve) => { release = resolve; });
|
|
31
|
+
storageTails.set(storage, previous.then(() => gate));
|
|
32
|
+
return previous.then(() => fn()).finally(release);
|
|
33
|
+
}
|
|
34
|
+
function isCasStore(adapter) {
|
|
35
|
+
return typeof adapter.conditionalUpdate === 'function';
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Create a durable `ProofGraphStoragePort` over a CAS-capable adapter.
|
|
39
|
+
*
|
|
40
|
+
* The head pointer and every reverse-index entry are guarded with the
|
|
41
|
+
* adapter's `conditionalUpdate`, and all mutations run serialized per adapter,
|
|
42
|
+
* so concurrent saves never clobber a newer graph. A non-CAS or low-durability
|
|
43
|
+
* adapter is rejected at construction (RFC-007 §4.2 no-silent-downgrade).
|
|
44
|
+
*/
|
|
45
|
+
export function createDurableProofGraphStore(adapter, options = {}) {
|
|
46
|
+
const namespace = options.namespace ?? DEFAULT_NAMESPACE;
|
|
47
|
+
if (typeof adapter.capabilities === 'object') {
|
|
48
|
+
assertCapabilities(adapter, {
|
|
49
|
+
acknowledge: options.requireAckMode ?? 'durably-acknowledged',
|
|
50
|
+
atomic: true,
|
|
51
|
+
conditional: true,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
if (!isCasStore(adapter)) {
|
|
55
|
+
throw new Error('createDurableProofGraphStore requires a CAS-capable storage adapter (CasStore.conditionalUpdate)');
|
|
56
|
+
}
|
|
57
|
+
const cas = adapter.conditionalUpdate.bind(adapter);
|
|
58
|
+
const graphKey = (graphId) => `${namespace}${GRAPH_PREFIX}${graphId}`;
|
|
59
|
+
const nodeKey = (nodeId) => `${namespace}${NODE_PREFIX}${nodeId}`;
|
|
60
|
+
const headKey = () => `${namespace}${HEAD_KEY}`;
|
|
61
|
+
/** Strictly-increasing savedAt so concurrent saves have deterministic order. */
|
|
62
|
+
let lastSavedAt = 0;
|
|
63
|
+
const nextSavedAt = () => {
|
|
64
|
+
const now = Date.now();
|
|
65
|
+
lastSavedAt = now > lastSavedAt ? now : lastSavedAt + 1;
|
|
66
|
+
return lastSavedAt;
|
|
67
|
+
};
|
|
68
|
+
function parseGraph(graphId, raw) {
|
|
69
|
+
if (typeof raw !== 'string' && (typeof raw !== 'object' || raw === null || typeof raw.json !== 'string')) {
|
|
70
|
+
throw new StorageError(`proofgraph record "${graphId}" is malformed`, 'corrupt', { key: graphKey(graphId) });
|
|
71
|
+
}
|
|
72
|
+
const json = typeof raw === 'string' ? raw : raw.json;
|
|
73
|
+
try {
|
|
74
|
+
return importProofGraph(json);
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
throw new StorageError(`proofgraph record "${graphId}" failed validation: ${err.message}`, 'corrupt', { key: graphKey(graphId), cause: err });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async function loadUnchecked(graphId) {
|
|
81
|
+
const raw = await adapter.get(graphKey(graphId));
|
|
82
|
+
if (raw === null || raw === undefined)
|
|
83
|
+
return null;
|
|
84
|
+
return parseGraph(graphId, raw);
|
|
85
|
+
}
|
|
86
|
+
async function loadGraphForNode(nodeId) {
|
|
87
|
+
const entry = await adapter.get(nodeKey(nodeId));
|
|
88
|
+
if (entry === null || entry === undefined)
|
|
89
|
+
return null;
|
|
90
|
+
return loadUnchecked(entry.graphId);
|
|
91
|
+
}
|
|
92
|
+
async function writeIndexIfNewer(key, next) {
|
|
93
|
+
await cas(key, (current) => {
|
|
94
|
+
if (current && current.savedAt > next.savedAt)
|
|
95
|
+
return { abort: 'older snapshot' };
|
|
96
|
+
return { next };
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
async save(graph) {
|
|
101
|
+
const savedAt = nextSavedAt();
|
|
102
|
+
return withStorageLock(adapter, async () => {
|
|
103
|
+
// Immutable content-addressed record first (idempotent, safe on retry).
|
|
104
|
+
const record = { json: exportProofGraph(graph), savedAt };
|
|
105
|
+
await adapter.set(graphKey(graph.graphId), record);
|
|
106
|
+
// Reverse index: most-recent snapshot wins (CAS, never clobbers newer).
|
|
107
|
+
for (const node of graph.nodes) {
|
|
108
|
+
await writeIndexIfNewer(nodeKey(node.id), { graphId: graph.graphId, savedAt });
|
|
109
|
+
}
|
|
110
|
+
// Head pointer: does not move backwards.
|
|
111
|
+
await writeIndexIfNewer(headKey(), { graphId: graph.graphId, savedAt });
|
|
112
|
+
});
|
|
113
|
+
},
|
|
114
|
+
async load(graphId) {
|
|
115
|
+
return loadUnchecked(graphId);
|
|
116
|
+
},
|
|
117
|
+
async findByNodeId(id) {
|
|
118
|
+
return loadGraphForNode(id);
|
|
119
|
+
},
|
|
120
|
+
async latest() {
|
|
121
|
+
const head = await adapter.get(headKey());
|
|
122
|
+
if (head === null || head === undefined)
|
|
123
|
+
return null;
|
|
124
|
+
return loadUnchecked(head.graphId);
|
|
125
|
+
},
|
|
126
|
+
async listGraphIds() {
|
|
127
|
+
const prefix = `${namespace}${GRAPH_PREFIX}`;
|
|
128
|
+
const ids = [];
|
|
129
|
+
for (const key of await adapter.keys()) {
|
|
130
|
+
if (!key.startsWith(prefix))
|
|
131
|
+
continue;
|
|
132
|
+
ids.push(key.slice(prefix.length));
|
|
133
|
+
}
|
|
134
|
+
return ids.sort();
|
|
135
|
+
},
|
|
136
|
+
async recover() {
|
|
137
|
+
return withStorageLock(adapter, async () => {
|
|
138
|
+
const report = { recoveredHead: false, cleanedIndexKeys: 0 };
|
|
139
|
+
const graphs = await this.listGraphIds();
|
|
140
|
+
const head = await adapter.get(headKey());
|
|
141
|
+
// 1. Head pointer: if absent but graphs exist, adopt the newest record.
|
|
142
|
+
if ((head === null || head === undefined) && graphs.length > 0) {
|
|
143
|
+
let newest = null;
|
|
144
|
+
for (const graphId of graphs) {
|
|
145
|
+
const record = await adapter.get(graphKey(graphId));
|
|
146
|
+
if (!record)
|
|
147
|
+
continue;
|
|
148
|
+
if (!newest || record.savedAt > newest.savedAt) {
|
|
149
|
+
newest = { graphId, savedAt: record.savedAt };
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (newest) {
|
|
153
|
+
await writeIndexIfNewer(headKey(), newest);
|
|
154
|
+
report.recoveredHead = true;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// 2. Drop reverse-index entries pointing at absent graph records.
|
|
158
|
+
const nodePrefix = `${namespace}${NODE_PREFIX}`;
|
|
159
|
+
for (const key of await adapter.keys()) {
|
|
160
|
+
if (!key.startsWith(nodePrefix))
|
|
161
|
+
continue;
|
|
162
|
+
const entry = await adapter.get(key);
|
|
163
|
+
if (!entry)
|
|
164
|
+
continue;
|
|
165
|
+
const graphRecord = await adapter.get(graphKey(entry.graphId));
|
|
166
|
+
if (graphRecord === null || graphRecord === undefined) {
|
|
167
|
+
await adapter.remove(key);
|
|
168
|
+
report.cleanedIndexKeys++;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return report;
|
|
172
|
+
});
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
}
|
|
@@ -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;
|