0nmcp 1.7.0 → 2.0.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/README.md +24 -23
- package/cli.js +431 -1
- package/command-runner.js +224 -0
- package/commands.js +115 -0
- package/index.js +8 -1
- package/lib/stats.json +1 -1
- package/package.json +26 -3
- package/vault/container.js +479 -0
- package/vault/crypto-container.js +278 -0
- package/vault/escrow.js +227 -0
- package/vault/layers.js +254 -0
- package/vault/registry.js +159 -0
- package/vault/seal.js +74 -0
- package/vault/tools-container.js +356 -0
package/vault/layers.js
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// 0nMCP — Vault: Semantic Layer Manager
|
|
3
|
+
// ============================================================
|
|
4
|
+
// 7 named layers, each independently encrypted with AES-256-GCM.
|
|
5
|
+
// The credentials layer (layer 2) uses double-encryption via
|
|
6
|
+
// Argon2id for maximum protection of API keys and secrets.
|
|
7
|
+
//
|
|
8
|
+
// Patent Pending: US Provisional Patent Application #63/990,046
|
|
9
|
+
// ============================================================
|
|
10
|
+
|
|
11
|
+
import { encryptAES, decryptAES, deriveKeyArgon2, encryptAESRaw, decryptAESRaw, SALT_LENGTH } from "./crypto-container.js";
|
|
12
|
+
import { randomBytes } from "crypto";
|
|
13
|
+
|
|
14
|
+
// ── Layer Definitions ────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
export const LAYER_NAMES = [
|
|
17
|
+
"workflows", // 1: .0n workflow definitions
|
|
18
|
+
"credentials", // 2: API keys, tokens, secrets (double-encrypted)
|
|
19
|
+
"env_vars", // 3: Environment variables
|
|
20
|
+
"mcp_configs", // 4: MCP server configurations
|
|
21
|
+
"site_profiles", // 5: CRO9/site configs
|
|
22
|
+
"ai_brain", // 6: AI agent data, prompts, memory
|
|
23
|
+
"audit_trail", // 7: Execution logs, timestamps (append-only)
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
export const LAYER_COUNT = LAYER_NAMES.length;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Check if a layer name is valid.
|
|
30
|
+
* @param {string} name
|
|
31
|
+
* @returns {boolean}
|
|
32
|
+
*/
|
|
33
|
+
export function isValidLayer(name) {
|
|
34
|
+
return LAYER_NAMES.includes(name);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Get the index of a layer by name.
|
|
39
|
+
* @param {string} name
|
|
40
|
+
* @returns {number} 0-based index, or -1 if not found
|
|
41
|
+
*/
|
|
42
|
+
export function getLayerIndex(name) {
|
|
43
|
+
return LAYER_NAMES.indexOf(name);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ── Layer Encryption ─────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Seal (encrypt) a single layer's data.
|
|
50
|
+
*
|
|
51
|
+
* @param {string} name - Layer name
|
|
52
|
+
* @param {any} data - Data to encrypt (will be JSON.stringify'd)
|
|
53
|
+
* @param {string} passphrase - Encryption passphrase
|
|
54
|
+
* @returns {{ name: string, ciphertext: Buffer, iv: Buffer, salt: Buffer, tag: Buffer }}
|
|
55
|
+
*/
|
|
56
|
+
export function sealLayer(name, data, passphrase) {
|
|
57
|
+
if (!isValidLayer(name)) {
|
|
58
|
+
throw new Error(`Invalid layer name: "${name}". Valid: ${LAYER_NAMES.join(", ")}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const plaintext = JSON.stringify(data);
|
|
62
|
+
const result = encryptAES(plaintext, passphrase);
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
name,
|
|
66
|
+
ciphertext: result.ciphertext,
|
|
67
|
+
iv: result.iv,
|
|
68
|
+
salt: result.salt,
|
|
69
|
+
tag: result.tag,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Unseal (decrypt) a single layer's data.
|
|
75
|
+
*
|
|
76
|
+
* @param {string} name - Layer name
|
|
77
|
+
* @param {Buffer} ciphertext
|
|
78
|
+
* @param {string} passphrase
|
|
79
|
+
* @param {Buffer} iv
|
|
80
|
+
* @param {Buffer} salt
|
|
81
|
+
* @param {Buffer} tag
|
|
82
|
+
* @returns {any} Parsed JSON data
|
|
83
|
+
*/
|
|
84
|
+
export function unsealLayer(name, ciphertext, passphrase, iv, salt, tag) {
|
|
85
|
+
if (!isValidLayer(name)) {
|
|
86
|
+
throw new Error(`Invalid layer name: "${name}"`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const decrypted = decryptAES(ciphertext, passphrase, iv, salt, tag);
|
|
90
|
+
return JSON.parse(decrypted.toString("utf8"));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ── Credentials Double-Encryption ────────────────────────────
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Seal credentials with double encryption:
|
|
97
|
+
* 1. First layer: AES-256-GCM with passphrase
|
|
98
|
+
* 2. Second layer: AES-256-GCM with Argon2id-derived key
|
|
99
|
+
*
|
|
100
|
+
* @param {any} data - Credentials data
|
|
101
|
+
* @param {string} passphrase - Encryption passphrase
|
|
102
|
+
* @returns {Promise<{ ciphertext: Buffer, iv: Buffer, salt: Buffer, tag: Buffer, argonSalt: Buffer, innerIv: Buffer, innerTag: Buffer }>}
|
|
103
|
+
*/
|
|
104
|
+
export async function sealCredentials(data, passphrase) {
|
|
105
|
+
const plaintext = Buffer.from(JSON.stringify(data), "utf8");
|
|
106
|
+
|
|
107
|
+
// Inner layer: Argon2id-derived key
|
|
108
|
+
const argonSalt = randomBytes(SALT_LENGTH);
|
|
109
|
+
const argonKey = await deriveKeyArgon2(passphrase, argonSalt);
|
|
110
|
+
const inner = encryptAESRaw(plaintext, argonKey);
|
|
111
|
+
|
|
112
|
+
// Outer layer: standard AES-256-GCM with passphrase
|
|
113
|
+
// Combine inner ciphertext + iv + tag for outer encryption
|
|
114
|
+
const innerPayload = Buffer.concat([inner.iv, inner.tag, inner.ciphertext]);
|
|
115
|
+
const outer = encryptAES(innerPayload, passphrase);
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
ciphertext: outer.ciphertext,
|
|
119
|
+
iv: outer.iv,
|
|
120
|
+
salt: outer.salt,
|
|
121
|
+
tag: outer.tag,
|
|
122
|
+
argonSalt,
|
|
123
|
+
innerIv: inner.iv,
|
|
124
|
+
innerTag: inner.tag,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Unseal double-encrypted credentials.
|
|
130
|
+
*
|
|
131
|
+
* @param {Buffer} ciphertext
|
|
132
|
+
* @param {string} passphrase
|
|
133
|
+
* @param {Buffer} iv
|
|
134
|
+
* @param {Buffer} salt
|
|
135
|
+
* @param {Buffer} tag
|
|
136
|
+
* @param {Buffer} argonSalt
|
|
137
|
+
* @returns {Promise<any>} Parsed credentials
|
|
138
|
+
*/
|
|
139
|
+
export async function unsealCredentials(ciphertext, passphrase, iv, salt, tag, argonSalt) {
|
|
140
|
+
// Outer layer: decrypt with passphrase
|
|
141
|
+
const innerPayload = decryptAES(ciphertext, passphrase, iv, salt, tag);
|
|
142
|
+
|
|
143
|
+
// Extract inner components (iv:12 + tag:16 + ciphertext:rest)
|
|
144
|
+
const innerIv = innerPayload.subarray(0, 12);
|
|
145
|
+
const innerTag = innerPayload.subarray(12, 28);
|
|
146
|
+
const innerCiphertext = innerPayload.subarray(28);
|
|
147
|
+
|
|
148
|
+
// Inner layer: Argon2id-derived key
|
|
149
|
+
const argonKey = await deriveKeyArgon2(passphrase, argonSalt);
|
|
150
|
+
const decrypted = decryptAESRaw(innerCiphertext, argonKey, innerIv, innerTag);
|
|
151
|
+
|
|
152
|
+
return JSON.parse(decrypted.toString("utf8"));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ── Bulk Layer Operations ────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Seal multiple layers at once.
|
|
159
|
+
*
|
|
160
|
+
* @param {Object<string, any>} layerData - { layerName: data }
|
|
161
|
+
* @param {string} passphrase
|
|
162
|
+
* @returns {Promise<Map<string, object>>} Sealed layers
|
|
163
|
+
*/
|
|
164
|
+
export async function sealAllLayers(layerData, passphrase) {
|
|
165
|
+
const sealed = new Map();
|
|
166
|
+
|
|
167
|
+
for (const [name, data] of Object.entries(layerData)) {
|
|
168
|
+
if (!isValidLayer(name)) {
|
|
169
|
+
throw new Error(`Invalid layer: "${name}"`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (name === "credentials") {
|
|
173
|
+
const result = await sealCredentials(data, passphrase);
|
|
174
|
+
sealed.set(name, { ...result, doubleEncrypted: true });
|
|
175
|
+
} else {
|
|
176
|
+
const result = sealLayer(name, data, passphrase);
|
|
177
|
+
sealed.set(name, result);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return sealed;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Unseal multiple layers at once.
|
|
186
|
+
*
|
|
187
|
+
* @param {Map<string, object>} sealedLayers
|
|
188
|
+
* @param {string} passphrase
|
|
189
|
+
* @param {string[]} [onlyLayers] - Optional filter: only unseal these layers
|
|
190
|
+
* @returns {Promise<Object<string, any>>} Unsealed layer data
|
|
191
|
+
*/
|
|
192
|
+
export async function unsealAllLayers(sealedLayers, passphrase, onlyLayers = null) {
|
|
193
|
+
const result = {};
|
|
194
|
+
|
|
195
|
+
for (const [name, layerInfo] of sealedLayers) {
|
|
196
|
+
if (onlyLayers && !onlyLayers.includes(name)) continue;
|
|
197
|
+
|
|
198
|
+
if (name === "credentials" && layerInfo.doubleEncrypted) {
|
|
199
|
+
result[name] = await unsealCredentials(
|
|
200
|
+
layerInfo.ciphertext, passphrase,
|
|
201
|
+
layerInfo.iv, layerInfo.salt, layerInfo.tag,
|
|
202
|
+
layerInfo.argonSalt
|
|
203
|
+
);
|
|
204
|
+
} else {
|
|
205
|
+
result[name] = unsealLayer(
|
|
206
|
+
name, layerInfo.ciphertext, passphrase,
|
|
207
|
+
layerInfo.iv, layerInfo.salt, layerInfo.tag
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return result;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ── Layer Access Matrix ──────────────────────────────────────
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Create a layer access matrix for escrow parties.
|
|
219
|
+
*
|
|
220
|
+
* @param {Object<string, string[]>} partyAccess - { partyId: [layerNames] }
|
|
221
|
+
* @returns {Object<string, Object<string, boolean>>} Full matrix
|
|
222
|
+
*/
|
|
223
|
+
export function createAccessMatrix(partyAccess) {
|
|
224
|
+
const matrix = {};
|
|
225
|
+
|
|
226
|
+
for (const [partyId, layers] of Object.entries(partyAccess)) {
|
|
227
|
+
matrix[partyId] = {};
|
|
228
|
+
for (const layerName of LAYER_NAMES) {
|
|
229
|
+
matrix[partyId][layerName] = layers.includes(layerName);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return matrix;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Validate an access matrix.
|
|
238
|
+
*
|
|
239
|
+
* @param {Object} matrix
|
|
240
|
+
* @returns {{ valid: boolean, errors: string[] }}
|
|
241
|
+
*/
|
|
242
|
+
export function validateAccessMatrix(matrix) {
|
|
243
|
+
const errors = [];
|
|
244
|
+
|
|
245
|
+
for (const [partyId, access] of Object.entries(matrix)) {
|
|
246
|
+
for (const layerName of Object.keys(access)) {
|
|
247
|
+
if (!isValidLayer(layerName)) {
|
|
248
|
+
errors.push(`Party "${partyId}": invalid layer "${layerName}"`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return { valid: errors.length === 0, errors };
|
|
254
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// 0nMCP — Vault: Transfer Registry
|
|
3
|
+
// ============================================================
|
|
4
|
+
// In-memory + file-backed registry for tracking vault transfers.
|
|
5
|
+
// Prevents replay attacks by rejecting duplicate transfer IDs.
|
|
6
|
+
//
|
|
7
|
+
// Patent Pending: US Provisional Patent Application #63/990,046
|
|
8
|
+
// ============================================================
|
|
9
|
+
|
|
10
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
11
|
+
import { join } from "path";
|
|
12
|
+
import { homedir } from "os";
|
|
13
|
+
|
|
14
|
+
const VAULT_DIR = join(homedir(), ".0n", "vault");
|
|
15
|
+
const REGISTRY_PATH = join(VAULT_DIR, "transfers.json");
|
|
16
|
+
|
|
17
|
+
// In-memory cache
|
|
18
|
+
let registry = null;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Ensure vault directory exists and load registry from disk.
|
|
22
|
+
*/
|
|
23
|
+
function ensureRegistry() {
|
|
24
|
+
if (registry !== null) return;
|
|
25
|
+
|
|
26
|
+
if (!existsSync(VAULT_DIR)) {
|
|
27
|
+
mkdirSync(VAULT_DIR, { recursive: true });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (existsSync(REGISTRY_PATH)) {
|
|
31
|
+
try {
|
|
32
|
+
registry = JSON.parse(readFileSync(REGISTRY_PATH, "utf-8"));
|
|
33
|
+
} catch {
|
|
34
|
+
registry = { transfers: {} };
|
|
35
|
+
}
|
|
36
|
+
} else {
|
|
37
|
+
registry = { transfers: {} };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Save registry to disk.
|
|
43
|
+
*/
|
|
44
|
+
function saveRegistry() {
|
|
45
|
+
if (!existsSync(VAULT_DIR)) {
|
|
46
|
+
mkdirSync(VAULT_DIR, { recursive: true });
|
|
47
|
+
}
|
|
48
|
+
writeFileSync(REGISTRY_PATH, JSON.stringify(registry, null, 2));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Register a new transfer.
|
|
53
|
+
*
|
|
54
|
+
* @param {string} transferId - UUID v4
|
|
55
|
+
* @param {string} sealHex - Seal of Truth hex string
|
|
56
|
+
* @param {Object} metadata - Additional metadata
|
|
57
|
+
* @returns {{ success: boolean, error?: string }}
|
|
58
|
+
*/
|
|
59
|
+
export function registerTransfer(transferId, sealHex, metadata = {}) {
|
|
60
|
+
ensureRegistry();
|
|
61
|
+
|
|
62
|
+
// Replay prevention
|
|
63
|
+
if (registry.transfers[transferId]) {
|
|
64
|
+
return {
|
|
65
|
+
success: false,
|
|
66
|
+
error: `Transfer ID "${transferId}" already registered — replay rejected`,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
registry.transfers[transferId] = {
|
|
71
|
+
seal: sealHex,
|
|
72
|
+
registered_at: new Date().toISOString(),
|
|
73
|
+
status: "active",
|
|
74
|
+
...metadata,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
saveRegistry();
|
|
78
|
+
|
|
79
|
+
return { success: true, transferId };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Look up a transfer by ID.
|
|
84
|
+
*
|
|
85
|
+
* @param {string} transferId
|
|
86
|
+
* @returns {{ found: boolean, transfer?: Object }}
|
|
87
|
+
*/
|
|
88
|
+
export function lookupTransfer(transferId) {
|
|
89
|
+
ensureRegistry();
|
|
90
|
+
|
|
91
|
+
const transfer = registry.transfers[transferId];
|
|
92
|
+
if (!transfer) {
|
|
93
|
+
return { found: false };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return { found: true, transfer: { transferId, ...transfer } };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Revoke a transfer.
|
|
101
|
+
*
|
|
102
|
+
* @param {string} transferId
|
|
103
|
+
* @returns {{ success: boolean, error?: string }}
|
|
104
|
+
*/
|
|
105
|
+
export function revokeTransfer(transferId) {
|
|
106
|
+
ensureRegistry();
|
|
107
|
+
|
|
108
|
+
if (!registry.transfers[transferId]) {
|
|
109
|
+
return { success: false, error: `Transfer ID "${transferId}" not found` };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (registry.transfers[transferId].status === "revoked") {
|
|
113
|
+
return { success: false, error: `Transfer "${transferId}" is already revoked` };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
registry.transfers[transferId].status = "revoked";
|
|
117
|
+
registry.transfers[transferId].revoked_at = new Date().toISOString();
|
|
118
|
+
|
|
119
|
+
saveRegistry();
|
|
120
|
+
|
|
121
|
+
return { success: true, transferId, message: `Transfer "${transferId}" has been revoked` };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* List all transfers.
|
|
126
|
+
*
|
|
127
|
+
* @param {string} [status] - Optional filter: "active", "revoked"
|
|
128
|
+
* @returns {Array<Object>}
|
|
129
|
+
*/
|
|
130
|
+
export function listTransfers(status = null) {
|
|
131
|
+
ensureRegistry();
|
|
132
|
+
|
|
133
|
+
const transfers = [];
|
|
134
|
+
for (const [id, data] of Object.entries(registry.transfers)) {
|
|
135
|
+
if (status && data.status !== status) continue;
|
|
136
|
+
transfers.push({ transferId: id, ...data });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return transfers;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Check if a transfer ID has been used (for replay prevention).
|
|
144
|
+
*
|
|
145
|
+
* @param {string} transferId
|
|
146
|
+
* @returns {boolean}
|
|
147
|
+
*/
|
|
148
|
+
export function isTransferUsed(transferId) {
|
|
149
|
+
ensureRegistry();
|
|
150
|
+
return !!registry.transfers[transferId];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Reset registry (for testing only).
|
|
155
|
+
*/
|
|
156
|
+
export function resetRegistry() {
|
|
157
|
+
registry = { transfers: {} };
|
|
158
|
+
saveRegistry();
|
|
159
|
+
}
|
package/vault/seal.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// 0nMCP — Vault: Seal of Truth
|
|
3
|
+
// ============================================================
|
|
4
|
+
// SHA3-256 based content certification hash.
|
|
5
|
+
// Publicly verifiable — anyone can confirm container integrity
|
|
6
|
+
// without decrypting any layer data.
|
|
7
|
+
//
|
|
8
|
+
// Formula:
|
|
9
|
+
// SHA3-256(transfer_id || timestamp || pubkey || SHA3-256(concat(all_ciphertexts)))
|
|
10
|
+
//
|
|
11
|
+
// Patent Pending: US Provisional Patent Application #63/990,046
|
|
12
|
+
// ============================================================
|
|
13
|
+
|
|
14
|
+
import { sha3, sha3Hex } from "./crypto-container.js";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Create a Seal of Truth for a vault container.
|
|
18
|
+
*
|
|
19
|
+
* @param {string} transferId - UUID v4 transfer ID
|
|
20
|
+
* @param {number} timestamp - Unix timestamp (ms)
|
|
21
|
+
* @param {Buffer} pubkey - Ed25519 public key (32 bytes)
|
|
22
|
+
* @param {Buffer[]} layerCiphertexts - Array of encrypted layer data
|
|
23
|
+
* @returns {{ seal: Buffer, sealHex: string }}
|
|
24
|
+
*/
|
|
25
|
+
export function createSeal(transferId, timestamp, pubkey, layerCiphertexts) {
|
|
26
|
+
// Hash all ciphertexts together
|
|
27
|
+
const contentHash = sha3(Buffer.concat(layerCiphertexts));
|
|
28
|
+
|
|
29
|
+
// Combine components: transferId || timestamp || pubkey || contentHash
|
|
30
|
+
const transferIdBuf = Buffer.from(transferId, "utf8");
|
|
31
|
+
const timestampBuf = Buffer.alloc(8);
|
|
32
|
+
timestampBuf.writeBigUInt64BE(BigInt(timestamp));
|
|
33
|
+
|
|
34
|
+
const sealInput = Buffer.concat([transferIdBuf, timestampBuf, pubkey, contentHash]);
|
|
35
|
+
const seal = sha3(sealInput);
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
seal,
|
|
39
|
+
sealHex: seal.toString("hex"),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Verify a Seal of Truth.
|
|
45
|
+
*
|
|
46
|
+
* @param {Buffer} seal - 32-byte seal hash
|
|
47
|
+
* @param {string} transferId
|
|
48
|
+
* @param {number} timestamp
|
|
49
|
+
* @param {Buffer} pubkey
|
|
50
|
+
* @param {Buffer[]} layerCiphertexts
|
|
51
|
+
* @returns {boolean} True if seal matches
|
|
52
|
+
*/
|
|
53
|
+
export function verifySeal(seal, transferId, timestamp, pubkey, layerCiphertexts) {
|
|
54
|
+
const expected = createSeal(transferId, timestamp, pubkey, layerCiphertexts);
|
|
55
|
+
return Buffer.compare(seal, expected.seal) === 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Generate a human-readable seal summary.
|
|
60
|
+
*
|
|
61
|
+
* @param {Buffer} seal
|
|
62
|
+
* @param {string} transferId
|
|
63
|
+
* @param {number} timestamp
|
|
64
|
+
* @returns {string}
|
|
65
|
+
*/
|
|
66
|
+
export function sealSummary(seal, transferId, timestamp) {
|
|
67
|
+
return [
|
|
68
|
+
`Seal of Truth: ${seal.toString("hex")}`,
|
|
69
|
+
`Transfer ID: ${transferId}`,
|
|
70
|
+
`Timestamp: ${new Date(timestamp).toISOString()}`,
|
|
71
|
+
`Algorithm: SHA3-256`,
|
|
72
|
+
`Status: Verified ✓`,
|
|
73
|
+
].join("\n");
|
|
74
|
+
}
|