blindseal 0.5.0 → 0.5.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/dashboard/assets/blindseal_verifier_bg-BYF7rQG1.wasm +0 -0
- package/dashboard/assets/geist-400-gapTbOY8.woff2 +0 -0
- package/dashboard/assets/geist-500-uokXdC-Q.woff2 +0 -0
- package/dashboard/assets/geist-600-DFOURf8L.woff2 +0 -0
- package/dashboard/assets/geist-mono-400-DTRLJnHl.woff2 +0 -0
- package/dashboard/assets/geist-mono-600-V-KvD_pi.woff2 +0 -0
- package/dashboard/assets/index-ErSVkiAF.css +1 -0
- package/dashboard/assets/index-cOSTL8eT.js +1 -0
- package/dashboard/assets/index-p0eB3k2y.js +70 -0
- package/dashboard/index.html +76 -0
- package/dist/attest-VWYZAOLY.js +346 -0
- package/dist/{audit-C756XFXR.js → audit-G4DDMAXH.js} +1 -1
- package/dist/chunk-6EU4GH4P.js +513 -0
- package/dist/chunk-B44GZXDX.js +136 -0
- package/dist/chunk-CIUQ6WHJ.js +88 -0
- package/dist/{chunk-BTIXDNTV.js → chunk-DJL2E6RF.js} +69 -6
- package/dist/{chunk-PQ457W6T.js → chunk-VSOUJM6X.js} +1 -0
- package/dist/{chunk-JJY62YXJ.js → chunk-ZZTWGFH2.js} +0 -20
- package/dist/dashboard-J6BA7OBL.js +105 -0
- package/dist/decide.js +1 -1
- package/dist/index.js +122 -1307
- package/dist/{init.js → init-ZP3SLHM3.js} +122 -36
- package/dist/run-YQBNEPSD.js +17 -0
- package/dist/{submit-2UALCRX2.js → submit-D6MQTAXT.js} +1 -1
- package/dist/verify-RISAGKXC.js +98 -0
- package/fixtures/attestation-sample.json +25 -0
- package/fixtures/attestation-tampered.json +29 -0
- package/fixtures/attestations.json +174 -0
- package/fixtures/audit.json +1810 -0
- package/fixtures/decisions.json +9899 -0
- package/fixtures/policy.json +216 -0
- package/package.json +8 -8
- package/dist/attest-QLIE2OSQ.js +0 -195
- package/dist/verify-HO4IU7XV.js +0 -45
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
getNullifierStatus
|
|
4
|
+
} from "./chunk-B44GZXDX.js";
|
|
5
|
+
import {
|
|
6
|
+
discloseBundle,
|
|
7
|
+
hashEntry
|
|
8
|
+
} from "./chunk-VSOUJM6X.js";
|
|
9
|
+
import {
|
|
10
|
+
nearestFixturesDir,
|
|
11
|
+
packageRoot,
|
|
12
|
+
resolveDashboardToken,
|
|
13
|
+
shippedDashboardDir,
|
|
14
|
+
shippedFixturesDir
|
|
15
|
+
} from "./chunk-CIUQ6WHJ.js";
|
|
16
|
+
|
|
17
|
+
// src/cli/run.ts
|
|
18
|
+
import { existsSync as existsSync2 } from "fs";
|
|
19
|
+
import path2 from "path";
|
|
20
|
+
|
|
21
|
+
// src/api/mock-server.ts
|
|
22
|
+
import { createServer } from "http";
|
|
23
|
+
import { createHash, randomBytes } from "crypto";
|
|
24
|
+
import { readFileSync, existsSync } from "fs";
|
|
25
|
+
import path from "path";
|
|
26
|
+
import { createPrivateKey, createPublicKey } from "crypto";
|
|
27
|
+
var MOCK_IDENTITY_PKCS8 = "302e020100300506032b657004220420b6c6aa0182de5f17678ed27e9515ed56b438c6bec2ca9d27d653feec4d13da1c";
|
|
28
|
+
var MOCK_PRIVATE_KEY = createPrivateKey({ key: Buffer.from(MOCK_IDENTITY_PKCS8, "hex"), format: "der", type: "pkcs8" });
|
|
29
|
+
var MOCK_PUBKEY_HEX = createPublicKey(MOCK_PRIVATE_KEY).export({ type: "spki", format: "der" }).toString("hex");
|
|
30
|
+
var MIME = {
|
|
31
|
+
".html": "text/html; charset=utf-8",
|
|
32
|
+
".js": "text/javascript; charset=utf-8",
|
|
33
|
+
".css": "text/css; charset=utf-8",
|
|
34
|
+
".wasm": "application/wasm",
|
|
35
|
+
".json": "application/json",
|
|
36
|
+
".svg": "image/svg+xml",
|
|
37
|
+
".png": "image/png",
|
|
38
|
+
".woff2": "font/woff2",
|
|
39
|
+
".woff": "font/woff",
|
|
40
|
+
".ico": "image/x-icon",
|
|
41
|
+
".map": "application/json",
|
|
42
|
+
".txt": "text/plain"
|
|
43
|
+
};
|
|
44
|
+
var DASHBOARD_CSP = [
|
|
45
|
+
"default-src 'self'",
|
|
46
|
+
"script-src 'self' 'wasm-unsafe-eval'",
|
|
47
|
+
"style-src 'self' 'unsafe-inline'",
|
|
48
|
+
"img-src 'self' data:",
|
|
49
|
+
"font-src 'self'",
|
|
50
|
+
"connect-src 'self' ws://127.0.0.1:* ws://localhost:* http://127.0.0.1:* http://localhost:*",
|
|
51
|
+
"object-src 'none'",
|
|
52
|
+
"base-uri 'self'",
|
|
53
|
+
"frame-ancestors 'none'"
|
|
54
|
+
].join("; ");
|
|
55
|
+
function serveDashboard(res, dist, pathname) {
|
|
56
|
+
const rel = pathname === "/" ? "/index.html" : decodeURIComponent(pathname);
|
|
57
|
+
let file = path.normalize(path.join(dist, rel));
|
|
58
|
+
if (!file.startsWith(path.normalize(dist + path.sep)) && file !== path.normalize(dist)) return false;
|
|
59
|
+
if (!existsSync(file)) file = path.join(dist, "index.html");
|
|
60
|
+
if (!existsSync(file)) return false;
|
|
61
|
+
const body = readFileSync(file);
|
|
62
|
+
const type = MIME[path.extname(file).toLowerCase()] ?? "application/octet-stream";
|
|
63
|
+
res.writeHead(200, {
|
|
64
|
+
"content-type": type,
|
|
65
|
+
"content-length": body.length,
|
|
66
|
+
"content-security-policy": DASHBOARD_CSP,
|
|
67
|
+
"x-content-type-options": "nosniff",
|
|
68
|
+
"referrer-policy": "no-referrer",
|
|
69
|
+
"cache-control": file.endsWith("index.html") ? "no-cache" : "public, max-age=31536000, immutable"
|
|
70
|
+
});
|
|
71
|
+
res.end(body);
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
function loadFixtures(dir) {
|
|
75
|
+
const read = (f) => JSON.parse(readFileSync(path.join(dir, f), "utf8"));
|
|
76
|
+
return {
|
|
77
|
+
decisions: read("decisions.json"),
|
|
78
|
+
policy: read("policy.json"),
|
|
79
|
+
audit: read("audit.json"),
|
|
80
|
+
attestations: read("attestations.json"),
|
|
81
|
+
sample: read("attestation-sample.json")
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function resolveToken(explicit) {
|
|
85
|
+
return resolveDashboardToken(explicit);
|
|
86
|
+
}
|
|
87
|
+
var sha = (s) => "0x" + createHash("sha256").update(s).digest("hex");
|
|
88
|
+
function sendJson(res, status, body) {
|
|
89
|
+
const buf = Buffer.from(JSON.stringify(body));
|
|
90
|
+
res.writeHead(status, { "content-type": "application/json", "content-length": buf.length });
|
|
91
|
+
res.end(buf);
|
|
92
|
+
}
|
|
93
|
+
function sendErr(res, status, code, message) {
|
|
94
|
+
sendJson(res, status, { error: { code, message } });
|
|
95
|
+
}
|
|
96
|
+
function bearerOk(req, token, url) {
|
|
97
|
+
const auth = req.headers["authorization"];
|
|
98
|
+
if (typeof auth === "string" && auth === `Bearer ${token}`) return true;
|
|
99
|
+
if (url.searchParams.get("token") === token) return true;
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
async function readBody(req) {
|
|
103
|
+
const chunks = [];
|
|
104
|
+
for await (const c of req) chunks.push(c);
|
|
105
|
+
if (chunks.length === 0) return {};
|
|
106
|
+
try {
|
|
107
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
108
|
+
} catch {
|
|
109
|
+
return void 0;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
var WS_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
|
113
|
+
function wsAccept(key) {
|
|
114
|
+
return createHash("sha1").update(key + WS_GUID).digest("base64");
|
|
115
|
+
}
|
|
116
|
+
function wsFrame(text) {
|
|
117
|
+
const payload = Buffer.from(text);
|
|
118
|
+
const len = payload.length;
|
|
119
|
+
let header;
|
|
120
|
+
if (len < 126) {
|
|
121
|
+
header = Buffer.from([129, len]);
|
|
122
|
+
} else if (len < 65536) {
|
|
123
|
+
header = Buffer.from([129, 126, len >> 8 & 255, len & 255]);
|
|
124
|
+
} else {
|
|
125
|
+
header = Buffer.alloc(10);
|
|
126
|
+
header[0] = 129;
|
|
127
|
+
header[1] = 127;
|
|
128
|
+
header.writeBigUInt64BE(BigInt(len), 2);
|
|
129
|
+
}
|
|
130
|
+
return Buffer.concat([header, payload]);
|
|
131
|
+
}
|
|
132
|
+
function startMockApi(opts) {
|
|
133
|
+
const port = opts.port ?? 7777;
|
|
134
|
+
const host = opts.host ?? "127.0.0.1";
|
|
135
|
+
const token = resolveToken(opts.token);
|
|
136
|
+
const emitIntervalMs = opts.emitIntervalMs ?? 3e3;
|
|
137
|
+
const fx = loadFixtures(opts.fixturesDir);
|
|
138
|
+
const startedAt = Date.now();
|
|
139
|
+
const envState = process.env.BLINDSEAL_MOCK_STATE ?? "";
|
|
140
|
+
const stateOf = (url) => url.searchParams.get("state") ?? envState;
|
|
141
|
+
const TEE_QUOTE = { quote: sha("mock-tee-quote").slice(2), quoteHash: sha("mock-tee-quote-hash") };
|
|
142
|
+
const status = (st) => ({
|
|
143
|
+
version: "0.0.0-mock",
|
|
144
|
+
uptimeSec: Math.floor((Date.now() - startedAt) / 1e3),
|
|
145
|
+
connection: st === "offline" ? "offline" : st === "reconnecting" ? "reconnecting" : "live",
|
|
146
|
+
upstreams: [{ name: "mock-upstream", transport: "stdio", healthy: st !== "upstream-down" }],
|
|
147
|
+
policyRoot: fx.policy.root,
|
|
148
|
+
detectorVersion: fx.decisions[0]?.detectorVersion ?? "0x",
|
|
149
|
+
audit: st === "empty" ? { index: -1, hash: fx.audit.genesis, ts: new Date(startedAt).toISOString() } : fx.audit.head,
|
|
150
|
+
prover: { available: st === "prover-down" ? false : true },
|
|
151
|
+
l3: { mode: st === "tee" ? "tee" : "local" },
|
|
152
|
+
mock: true
|
|
153
|
+
});
|
|
154
|
+
const wsClients = /* @__PURE__ */ new Set();
|
|
155
|
+
const broadcast = (obj) => {
|
|
156
|
+
const frame = wsFrame(JSON.stringify(obj));
|
|
157
|
+
for (const s of wsClients) {
|
|
158
|
+
if (!s.destroyed) s.write(frame);
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
const createdAttestations = /* @__PURE__ */ new Map();
|
|
162
|
+
const liveDecisions = /* @__PURE__ */ new Map();
|
|
163
|
+
const server = createServer(async (req, res) => {
|
|
164
|
+
const url = new URL(req.url ?? "/", `http://${host}:${port}`);
|
|
165
|
+
const p = url.pathname;
|
|
166
|
+
const method = req.method ?? "GET";
|
|
167
|
+
const origin = req.headers["origin"];
|
|
168
|
+
if (typeof origin === "string" && /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/.test(origin)) {
|
|
169
|
+
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
170
|
+
res.setHeader("Access-Control-Allow-Headers", "authorization, content-type, x-confirm");
|
|
171
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, OPTIONS");
|
|
172
|
+
res.setHeader("Vary", "Origin");
|
|
173
|
+
}
|
|
174
|
+
if (method === "OPTIONS") {
|
|
175
|
+
res.writeHead(204);
|
|
176
|
+
return res.end();
|
|
177
|
+
}
|
|
178
|
+
if (!p.startsWith("/api/")) {
|
|
179
|
+
if (opts.dashboardDist && method === "GET" && serveDashboard(res, opts.dashboardDist, p)) return;
|
|
180
|
+
return sendErr(res, 404, "NOT_FOUND", "unknown path");
|
|
181
|
+
}
|
|
182
|
+
if (!bearerOk(req, token, url)) return sendErr(res, 401, "UNAUTHORIZED", "missing or invalid bearer token");
|
|
183
|
+
const seg = p.slice("/api/".length).split("/").filter(Boolean);
|
|
184
|
+
const st = stateOf(url);
|
|
185
|
+
if (st === "unauthorized") return sendErr(res, 401, "UNAUTHORIZED", "missing or invalid dashboard token");
|
|
186
|
+
if (st === "offline" && !(seg[0] === "status")) return sendErr(res, 503, "OFFLINE", "the proxy is unreachable");
|
|
187
|
+
try {
|
|
188
|
+
if (method === "GET" && seg[0] === "status" && seg.length === 1) return sendJson(res, 200, status(st));
|
|
189
|
+
if (method === "GET" && seg[0] === "decisions" && seg.length === 1) {
|
|
190
|
+
const q = url.searchParams;
|
|
191
|
+
let items = st === "empty" ? [] : fx.decisions.slice();
|
|
192
|
+
const vd = q.get("verdict");
|
|
193
|
+
if (vd) items = items.filter((d) => d.verdict === vd);
|
|
194
|
+
const sv = q.get("server");
|
|
195
|
+
if (sv) items = items.filter((d) => d.server === sv);
|
|
196
|
+
const tl = q.get("tool");
|
|
197
|
+
if (tl) items = items.filter((d) => d.tool === tl);
|
|
198
|
+
const from = q.get("from");
|
|
199
|
+
if (from) items = items.filter((d) => d.ts >= from);
|
|
200
|
+
const to = q.get("to");
|
|
201
|
+
if (to) items = items.filter((d) => d.ts <= to);
|
|
202
|
+
const limit = Math.min(Math.max(Number.parseInt(q.get("limit") ?? "50", 10) || 50, 1), 200);
|
|
203
|
+
const cursor = Number.parseInt(q.get("cursor") ?? "0", 10) || 0;
|
|
204
|
+
const page = items.slice(cursor, cursor + limit);
|
|
205
|
+
const nextCursor = cursor + limit < items.length ? String(cursor + limit) : void 0;
|
|
206
|
+
return sendJson(res, 200, { items: page, ...nextCursor ? { nextCursor } : {} });
|
|
207
|
+
}
|
|
208
|
+
if (seg[0] === "decisions" && seg.length >= 2) {
|
|
209
|
+
const d = fx.decisions.find((x) => x.id === seg[1]) ?? liveDecisions.get(seg[1]);
|
|
210
|
+
if (!d) return sendErr(res, 404, "NOT_FOUND", "decision not found");
|
|
211
|
+
if (method === "GET" && seg.length === 2) {
|
|
212
|
+
return sendJson(res, 200, { ...d, payloadRedacted: `[mock redacted payload for ${d.id}]` });
|
|
213
|
+
}
|
|
214
|
+
if (method === "POST" && seg[2] === "reveal" && seg.length === 3) {
|
|
215
|
+
if (req.headers["x-confirm"] !== "reveal") return sendErr(res, 400, "VALIDATION", "X-Confirm: reveal required");
|
|
216
|
+
return sendJson(res, 200, { payload: `[mock raw payload for ${d.id} \u2014 synthetic]` });
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (seg[0] === "audit") {
|
|
220
|
+
if (method === "GET" && seg[1] === "head") {
|
|
221
|
+
if (st === "empty") return sendJson(res, 200, { index: -1, hash: fx.audit.genesis, ts: new Date(startedAt).toISOString() });
|
|
222
|
+
return sendJson(res, 200, fx.audit.head);
|
|
223
|
+
}
|
|
224
|
+
if (method === "GET" && seg[1] === "entries") {
|
|
225
|
+
if (st === "empty") return sendJson(res, 200, []);
|
|
226
|
+
const from = Number.parseInt(url.searchParams.get("from") ?? "0", 10) || 0;
|
|
227
|
+
const to = Number.parseInt(url.searchParams.get("to") ?? String(fx.audit.head.index), 10);
|
|
228
|
+
return sendJson(res, 200, fx.audit.entries.filter((e) => e.index >= from && e.index <= to));
|
|
229
|
+
}
|
|
230
|
+
if (method === "POST" && seg[1] === "verify") {
|
|
231
|
+
if (st === "chain-broken") return sendJson(res, 200, { ok: false, checkedFrom: 0, checkedTo: fx.audit.head.index, brokenAt: 143 });
|
|
232
|
+
let prev = fx.audit.genesis;
|
|
233
|
+
let brokenAt;
|
|
234
|
+
for (const e of fx.audit.entries) {
|
|
235
|
+
const h = hashEntry({ index: e.index, ts: e.ts, decisionId: e.decisionId, verdict: e.verdict, payloadHash: e.payloadHash, prevHash: prev });
|
|
236
|
+
if (h !== e.hash || e.prevHash !== prev) {
|
|
237
|
+
brokenAt = e.index;
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
prev = e.hash;
|
|
241
|
+
}
|
|
242
|
+
return sendJson(res, 200, {
|
|
243
|
+
ok: brokenAt === void 0,
|
|
244
|
+
checkedFrom: 0,
|
|
245
|
+
checkedTo: fx.audit.head.index,
|
|
246
|
+
...brokenAt !== void 0 ? { brokenAt } : {}
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
if (method === "POST" && seg[1] === "disclose") {
|
|
250
|
+
const body = await readBody(req);
|
|
251
|
+
if (!body || typeof body.from !== "number" || typeof body.to !== "number") return sendErr(res, 400, "VALIDATION", "from/to required");
|
|
252
|
+
if (st === "chain-broken" && body.from <= 143 && body.to >= 143)
|
|
253
|
+
return sendErr(res, 409, "CHAIN_BROKEN", "the chain is broken at #143; cannot disclose a range over it");
|
|
254
|
+
const entries = fx.audit.entries.filter((e) => e.index >= body.from && e.index <= body.to);
|
|
255
|
+
return sendJson(res, 200, discloseBundle({
|
|
256
|
+
from: body.from,
|
|
257
|
+
to: body.to,
|
|
258
|
+
head: fx.audit.head,
|
|
259
|
+
entries,
|
|
260
|
+
privateKey: MOCK_PRIVATE_KEY,
|
|
261
|
+
pubkeyHex: MOCK_PUBKEY_HEX
|
|
262
|
+
}));
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (seg[0] === "policy") {
|
|
266
|
+
if (method === "GET" && seg.length === 1) return sendJson(res, 200, fx.policy);
|
|
267
|
+
if (method === "PUT" && seg.length === 1) {
|
|
268
|
+
const body = await readBody(req);
|
|
269
|
+
if (!body || !Array.isArray(body.rules)) return sendErr(res, 400, "VALIDATION", "rules[] required");
|
|
270
|
+
const root = sha("policy:" + JSON.stringify(body.rules));
|
|
271
|
+
return sendJson(res, 200, { ...fx.policy, rules: body.rules, root, version: (fx.policy.version ?? 1) + 1, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
272
|
+
}
|
|
273
|
+
if (method === "POST" && seg[1] === "dry-run") {
|
|
274
|
+
const body = await readBody(req);
|
|
275
|
+
if (body === void 0) return sendErr(res, 400, "VALIDATION", "invalid JSON body");
|
|
276
|
+
const d = { ...fx.decisions[0], id: "dryrun_" + Date.now(), ts: (/* @__PURE__ */ new Date()).toISOString() };
|
|
277
|
+
return sendJson(res, 200, d);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (seg[0] === "attestations") {
|
|
281
|
+
if (method === "GET" && seg.length === 1) {
|
|
282
|
+
if (st === "empty") return sendJson(res, 200, []);
|
|
283
|
+
const statusFilter = url.searchParams.get("status");
|
|
284
|
+
const items = statusFilter ? fx.attestations.filter((a) => a.status === statusFilter) : fx.attestations;
|
|
285
|
+
return sendJson(res, 200, items);
|
|
286
|
+
}
|
|
287
|
+
if (method === "POST" && seg.length === 1) {
|
|
288
|
+
if (st === "prover-down") return sendErr(res, 503, "PROVER_UNAVAILABLE", "the local prover is not running");
|
|
289
|
+
const body = await readBody(req);
|
|
290
|
+
if (!body || typeof body.decisionId !== "string") return sendErr(res, 400, "VALIDATION", "decisionId required");
|
|
291
|
+
const d = fx.decisions.find((x) => x.id === body.decisionId);
|
|
292
|
+
if (!d) return sendErr(res, 404, "NOT_FOUND", "decision not found");
|
|
293
|
+
if (!d.action) return sendErr(res, 422, "NO_ACTION_FIELDS", "decision has no on-chain action to attest");
|
|
294
|
+
const id = "att_" + randomBytes(5).toString("hex");
|
|
295
|
+
createdAttestations.set(id, {
|
|
296
|
+
...fx.sample,
|
|
297
|
+
id,
|
|
298
|
+
decisionId: d.id,
|
|
299
|
+
status: "generating",
|
|
300
|
+
progressPct: 50,
|
|
301
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
302
|
+
});
|
|
303
|
+
setTimeout(() => broadcast({ type: "attestation", data: { id, status: "generating", progressPct: 50 } }), 50);
|
|
304
|
+
setTimeout(() => {
|
|
305
|
+
const a = createdAttestations.get(id);
|
|
306
|
+
if (a) {
|
|
307
|
+
a.status = "generated";
|
|
308
|
+
a.progressPct = 100;
|
|
309
|
+
}
|
|
310
|
+
broadcast({ type: "attestation", data: { id, status: "generated", progressPct: 100 } });
|
|
311
|
+
}, 150);
|
|
312
|
+
return sendJson(res, 202, { id });
|
|
313
|
+
}
|
|
314
|
+
if (seg.length >= 2) {
|
|
315
|
+
const a = fx.attestations.find((x) => x.id === seg[1]) ?? createdAttestations.get(seg[1]) ?? (seg[1] === "sample" ? fx.sample : void 0);
|
|
316
|
+
if (method === "GET" && seg.length === 2) {
|
|
317
|
+
if (!a) return sendErr(res, 404, "NOT_FOUND", "attestation not found");
|
|
318
|
+
return sendJson(res, 200, a);
|
|
319
|
+
}
|
|
320
|
+
if (method === "POST" && seg[2] === "submit") {
|
|
321
|
+
const body = await readBody(req);
|
|
322
|
+
if (!body || body.cluster !== "mainnet-beta") return sendErr(res, 400, "VALIDATION", "cluster must be mainnet-beta");
|
|
323
|
+
return sendJson(res, 200, { txSig: sha("mock-txsig:" + seg[1]).slice(2, 90) });
|
|
324
|
+
}
|
|
325
|
+
if (method === "GET" && seg[2] === "export") {
|
|
326
|
+
if (!a) return sendErr(res, 404, "NOT_FOUND", "attestation not found");
|
|
327
|
+
const buf = Buffer.from(JSON.stringify(a, null, 2));
|
|
328
|
+
res.writeHead(200, { "content-type": "application/json", "content-disposition": `attachment; filename="attestation-${seg[1]}.json"`, "content-length": buf.length });
|
|
329
|
+
return res.end(buf);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
if (method === "GET" && seg[0] === "tee" && seg[1] === "quote") {
|
|
334
|
+
if (st === "tee") return sendJson(res, 200, { mode: "tee", quote: TEE_QUOTE.quote, quoteHash: TEE_QUOTE.quoteHash });
|
|
335
|
+
return sendJson(res, 200, { mode: "local" });
|
|
336
|
+
}
|
|
337
|
+
if (seg[0] === "nullifiers" && seg[1] && method === "GET") {
|
|
338
|
+
try {
|
|
339
|
+
return sendJson(res, 200, await getNullifierStatus(seg[1]));
|
|
340
|
+
} catch (e) {
|
|
341
|
+
return sendErr(res, 502, "RPC_UNAVAILABLE", e instanceof Error ? e.message : String(e));
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return sendErr(res, 404, "NOT_FOUND", `no route for ${method} ${p}`);
|
|
345
|
+
} catch (e) {
|
|
346
|
+
return sendErr(res, 500, "VALIDATION", e.message);
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
server.on("upgrade", (req, socket) => {
|
|
350
|
+
const url = new URL(req.url ?? "/", `http://${host}:${port}`);
|
|
351
|
+
const isEvents = url.pathname === "/api/events" || url.pathname === "/events";
|
|
352
|
+
if (!isEvents || !bearerOk(req, token, url)) {
|
|
353
|
+
socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n");
|
|
354
|
+
socket.destroy();
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
const key = req.headers["sec-websocket-key"];
|
|
358
|
+
if (typeof key !== "string") {
|
|
359
|
+
socket.destroy();
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
socket.write(
|
|
363
|
+
`HTTP/1.1 101 Switching Protocols\r
|
|
364
|
+
Upgrade: websocket\r
|
|
365
|
+
Connection: Upgrade\r
|
|
366
|
+
Sec-WebSocket-Accept: ${wsAccept(key)}\r
|
|
367
|
+
\r
|
|
368
|
+
`
|
|
369
|
+
);
|
|
370
|
+
wsClients.add(socket);
|
|
371
|
+
socket.write(wsFrame(JSON.stringify({ type: "status", data: status(envState) })));
|
|
372
|
+
socket.on("close", () => wsClients.delete(socket));
|
|
373
|
+
socket.on("error", () => wsClients.delete(socket));
|
|
374
|
+
});
|
|
375
|
+
let idx = 0;
|
|
376
|
+
const timer = setInterval(() => {
|
|
377
|
+
if (wsClients.size === 0) return;
|
|
378
|
+
const base = fx.decisions[idx % fx.decisions.length];
|
|
379
|
+
idx++;
|
|
380
|
+
const live = { ...base, id: base.id + "_live" + idx, ts: (/* @__PURE__ */ new Date()).toISOString() };
|
|
381
|
+
liveDecisions.set(live.id, live);
|
|
382
|
+
if (liveDecisions.size > 100) liveDecisions.delete(liveDecisions.keys().next().value);
|
|
383
|
+
broadcast({ type: "decision", data: live });
|
|
384
|
+
}, emitIntervalMs);
|
|
385
|
+
timer.unref?.();
|
|
386
|
+
return new Promise((resolve) => {
|
|
387
|
+
server.listen(port, host, () => {
|
|
388
|
+
const addr = server.address();
|
|
389
|
+
const actualPort = typeof addr === "object" && addr ? addr.port : port;
|
|
390
|
+
resolve({
|
|
391
|
+
url: `http://${host}:${actualPort}/api`,
|
|
392
|
+
token,
|
|
393
|
+
port: actualPort,
|
|
394
|
+
close: () => new Promise((r) => {
|
|
395
|
+
clearInterval(timer);
|
|
396
|
+
for (const s of wsClients) s.destroy();
|
|
397
|
+
wsClients.clear();
|
|
398
|
+
server.close(() => r());
|
|
399
|
+
})
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// src/cli/run.ts
|
|
406
|
+
var HELP = `blindseal run \u2014 start the local API + WS and serve the dashboard
|
|
407
|
+
|
|
408
|
+
Usage:
|
|
409
|
+
blindseal run --mock [flags]
|
|
410
|
+
|
|
411
|
+
Flags
|
|
412
|
+
--mock serve the bundled sample data with a fake decision stream
|
|
413
|
+
--port <n> listen port (default 7777)
|
|
414
|
+
--host <addr> listen host (default 127.0.0.1)
|
|
415
|
+
--fixtures <dir> sample data directory (default: the fixtures/ shipped in the package,
|
|
416
|
+
else the nearest fixtures/ above the current directory; $BLINDSEAL_FIXTURES)
|
|
417
|
+
--dashboard <dir> built dashboard to serve at / (default: the dashboard/ shipped in the
|
|
418
|
+
package; $BLINDSEAL_DASHBOARD_DIST)
|
|
419
|
+
--help, -h this help
|
|
420
|
+
|
|
421
|
+
Then open the dashboard: blindseal dashboard (or the http://127.0.0.1:7777/#token=\u2026 URL printed).
|
|
422
|
+
`;
|
|
423
|
+
function parseRunArgs(argv) {
|
|
424
|
+
const out = { mock: process.env.BLINDSEAL_MOCK === "1" };
|
|
425
|
+
for (let i = 0; i < argv.length; i++) {
|
|
426
|
+
const a = argv[i];
|
|
427
|
+
if (a === "--help" || a === "-h") out.help = true;
|
|
428
|
+
else if (a === "--mock") out.mock = true;
|
|
429
|
+
else if (a === "--port") out.port = Number.parseInt(argv[++i] ?? "", 10);
|
|
430
|
+
else if (a === "--host") out.host = argv[++i];
|
|
431
|
+
else if (a === "--fixtures") out.fixturesDir = argv[++i];
|
|
432
|
+
else if (a === "--dashboard") out.dashboard = argv[++i];
|
|
433
|
+
else if (a && a.startsWith("--")) throw new Error(`unknown flag: ${a}`);
|
|
434
|
+
}
|
|
435
|
+
return out;
|
|
436
|
+
}
|
|
437
|
+
function resolveFixturesDir(explicit) {
|
|
438
|
+
if (explicit) return explicit;
|
|
439
|
+
if (process.env.BLINDSEAL_FIXTURES) return process.env.BLINDSEAL_FIXTURES;
|
|
440
|
+
const found = shippedFixturesDir() ?? nearestFixturesDir();
|
|
441
|
+
if (found) return found;
|
|
442
|
+
throw new Error(
|
|
443
|
+
`sample data not found: this install has no ${path2.join(packageRoot(), "fixtures")} and no fixtures/ above ${process.cwd()}; pass --fixtures <dir> or set BLINDSEAL_FIXTURES`
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
function resolveDashboardDir(explicit) {
|
|
447
|
+
const picked = explicit ?? process.env.BLINDSEAL_DASHBOARD_DIST;
|
|
448
|
+
if (picked) return picked;
|
|
449
|
+
const shipped = shippedDashboardDir();
|
|
450
|
+
if (shipped) return shipped;
|
|
451
|
+
const sibling = path2.join(packageRoot(), "..", "dashboard", "dist");
|
|
452
|
+
return existsSync2(path2.join(sibling, "index.html")) ? sibling : void 0;
|
|
453
|
+
}
|
|
454
|
+
async function runRun(argv) {
|
|
455
|
+
let opts;
|
|
456
|
+
try {
|
|
457
|
+
opts = parseRunArgs(argv);
|
|
458
|
+
} catch (e) {
|
|
459
|
+
process.stderr.write(`blindseal run: ${e.message}
|
|
460
|
+
`);
|
|
461
|
+
return 1;
|
|
462
|
+
}
|
|
463
|
+
if (opts.help) {
|
|
464
|
+
process.stdout.write(HELP);
|
|
465
|
+
return 0;
|
|
466
|
+
}
|
|
467
|
+
if (!opts.mock) {
|
|
468
|
+
process.stderr.write(
|
|
469
|
+
"blindseal run: the engine-backed local API is not available in this release.\nUse `blindseal run --mock` for the dashboard over sample data, or `blindseal -- <server>` to wrap an MCP server.\n"
|
|
470
|
+
);
|
|
471
|
+
return 1;
|
|
472
|
+
}
|
|
473
|
+
let fixturesDir;
|
|
474
|
+
try {
|
|
475
|
+
fixturesDir = resolveFixturesDir(opts.fixturesDir);
|
|
476
|
+
} catch (e) {
|
|
477
|
+
process.stderr.write(`blindseal run: ${e.message}
|
|
478
|
+
`);
|
|
479
|
+
return 1;
|
|
480
|
+
}
|
|
481
|
+
const dashboardDist = resolveDashboardDir(opts.dashboard);
|
|
482
|
+
if (dashboardDist && !existsSync2(path2.join(dashboardDist, "index.html"))) {
|
|
483
|
+
process.stderr.write(`blindseal run: dashboard directory ${dashboardDist} has no index.html
|
|
484
|
+
`);
|
|
485
|
+
return 1;
|
|
486
|
+
}
|
|
487
|
+
const handle = await startMockApi({ port: opts.port, host: opts.host, fixturesDir, dashboardDist });
|
|
488
|
+
const base = handle.url.replace(/\/api$/, "");
|
|
489
|
+
process.stdout.write(
|
|
490
|
+
`blindseal (MOCK): local API on ${handle.url}
|
|
491
|
+
token: ${handle.token}
|
|
492
|
+
` + (dashboardDist ? ` dashboard: ${base}/#token=${handle.token} (served from ${dashboardDist})
|
|
493
|
+
` : ` dashboard: NOT SERVED \u2014 this install has no ${path2.join(packageRoot(), "dashboard")}; pass --dashboard <dir> or set BLINDSEAL_DASHBOARD_DIST
|
|
494
|
+
`) + ` sample data: ${fixturesDir}
|
|
495
|
+
WS: ${base.replace(/^http/, "ws")}/api/events?token=${handle.token}
|
|
496
|
+
Ctrl-C to stop.
|
|
497
|
+
`
|
|
498
|
+
);
|
|
499
|
+
return new Promise((resolve) => {
|
|
500
|
+
const shutdown = () => {
|
|
501
|
+
void handle.close().then(() => resolve(0));
|
|
502
|
+
};
|
|
503
|
+
process.on("SIGINT", shutdown);
|
|
504
|
+
process.on("SIGTERM", shutdown);
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
export {
|
|
509
|
+
parseRunArgs,
|
|
510
|
+
resolveFixturesDir,
|
|
511
|
+
resolveDashboardDir,
|
|
512
|
+
runRun
|
|
513
|
+
};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// ../chain-reads/src/index.ts
|
|
4
|
+
import { createHash } from "crypto";
|
|
5
|
+
var VERIFY_PROGRAM_ID = "merZNYKpKwwX4CGgzvma3jPvLfrMTkZcb1cSXUKANcb";
|
|
6
|
+
var PUBLIC_MAINNET_RPC = "https://api.mainnet-beta.solana.com";
|
|
7
|
+
var warned = false;
|
|
8
|
+
function rpcUrl() {
|
|
9
|
+
const u = process.env.BLINDSEAL_RPC_URL;
|
|
10
|
+
if (u === "off") return null;
|
|
11
|
+
if (u) return u;
|
|
12
|
+
if (!warned) {
|
|
13
|
+
warned = true;
|
|
14
|
+
process.stderr.write("blindseal: BLINDSEAL_RPC_URL unset \u2014 using the public mainnet RPC (rate-limited; ~100 req/10s per IP). Set your own endpoint for production.\n");
|
|
15
|
+
}
|
|
16
|
+
return PUBLIC_MAINNET_RPC;
|
|
17
|
+
}
|
|
18
|
+
async function rpc(method, params) {
|
|
19
|
+
const url = rpcUrl();
|
|
20
|
+
if (!url) throw new Error("rpc disabled (BLINDSEAL_RPC_URL=off)");
|
|
21
|
+
const res = await fetch(url, {
|
|
22
|
+
method: "POST",
|
|
23
|
+
headers: { "content-type": "application/json" },
|
|
24
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),
|
|
25
|
+
signal: AbortSignal.timeout(1e4)
|
|
26
|
+
});
|
|
27
|
+
if (res.status === 429) throw new Error("rpc rate-limited (429) \u2014 set BLINDSEAL_RPC_URL");
|
|
28
|
+
const j = await res.json();
|
|
29
|
+
if (j.error) throw new Error(`rpc ${method}: ${j.error.message}`);
|
|
30
|
+
return j.result;
|
|
31
|
+
}
|
|
32
|
+
var disc = (name) => createHash("sha256").update(`account:${name}`).digest().subarray(0, 8);
|
|
33
|
+
var b58 = (bytes) => bs58encode(bytes);
|
|
34
|
+
var ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
35
|
+
function bs58encode(bytes) {
|
|
36
|
+
let zeros = 0;
|
|
37
|
+
while (zeros < bytes.length && bytes[zeros] === 0) zeros++;
|
|
38
|
+
const digits = [];
|
|
39
|
+
for (const byte of bytes) {
|
|
40
|
+
let carry = byte;
|
|
41
|
+
for (let j = 0; j < digits.length; j++) {
|
|
42
|
+
carry += digits[j] << 8;
|
|
43
|
+
digits[j] = carry % 58;
|
|
44
|
+
carry = carry / 58 | 0;
|
|
45
|
+
}
|
|
46
|
+
while (carry > 0) {
|
|
47
|
+
digits.push(carry % 58);
|
|
48
|
+
carry = carry / 58 | 0;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return "1".repeat(zeros) + digits.reverse().map((d) => ALPHABET[d]).join("");
|
|
52
|
+
}
|
|
53
|
+
function bs58decode(s) {
|
|
54
|
+
const bytes = [];
|
|
55
|
+
for (const ch of s) {
|
|
56
|
+
let carry = ALPHABET.indexOf(ch);
|
|
57
|
+
if (carry < 0) throw new Error("bad base58");
|
|
58
|
+
for (let j = 0; j < bytes.length; j++) {
|
|
59
|
+
carry += bytes[j] * 58;
|
|
60
|
+
bytes[j] = carry & 255;
|
|
61
|
+
carry >>= 8;
|
|
62
|
+
}
|
|
63
|
+
while (carry > 0) {
|
|
64
|
+
bytes.push(carry & 255);
|
|
65
|
+
carry >>= 8;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
let zeros = 0;
|
|
69
|
+
while (zeros < s.length && s[zeros] === "1") zeros++;
|
|
70
|
+
return Uint8Array.from([...new Array(zeros).fill(0), ...bytes.reverse()]);
|
|
71
|
+
}
|
|
72
|
+
var P = (1n << 255n) - 19n;
|
|
73
|
+
var D = 37095705934669439343138083508754565189542113879843219016388785533085940283555n;
|
|
74
|
+
var SQRT_M1 = 19681161376707505956807079304988542015446066515923890162744021073123829784752n;
|
|
75
|
+
function modPow(b, e, m) {
|
|
76
|
+
let r = 1n;
|
|
77
|
+
b %= m;
|
|
78
|
+
while (e > 0n) {
|
|
79
|
+
if (e & 1n) r = r * b % m;
|
|
80
|
+
b = b * b % m;
|
|
81
|
+
e >>= 1n;
|
|
82
|
+
}
|
|
83
|
+
return r;
|
|
84
|
+
}
|
|
85
|
+
function isOnCurve(pk) {
|
|
86
|
+
if (pk.length !== 32) return false;
|
|
87
|
+
let y = 0n;
|
|
88
|
+
for (let i = 31; i >= 0; i--) y = y << 8n | BigInt(pk[i]);
|
|
89
|
+
const sign = y >> 255n & 1n;
|
|
90
|
+
y &= (1n << 255n) - 1n;
|
|
91
|
+
if (y >= P) return false;
|
|
92
|
+
const y2 = y * y % P, u = (y2 - 1n + P) % P, v = (D * y2 + 1n) % P;
|
|
93
|
+
const v3 = v * v % P * v % P, v7 = v3 * v3 % P * v % P;
|
|
94
|
+
let x = u * v3 % P * modPow(u * v7 % P, (P - 5n) / 8n, P) % P;
|
|
95
|
+
const vx2 = v * (x * x % P) % P;
|
|
96
|
+
if (vx2 === u) {
|
|
97
|
+
} else if (vx2 === (P - u) % P) x = x * SQRT_M1 % P;
|
|
98
|
+
else return false;
|
|
99
|
+
if (x === 0n && sign === 1n) return false;
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
function findProgramAddress(seeds, programId) {
|
|
103
|
+
const prog = bs58decode(programId);
|
|
104
|
+
for (let bump = 255; bump >= 0; bump--) {
|
|
105
|
+
const h = createHash("sha256");
|
|
106
|
+
for (const s of seeds) h.update(s);
|
|
107
|
+
h.update(Uint8Array.of(bump));
|
|
108
|
+
h.update(prog);
|
|
109
|
+
h.update("ProgramDerivedAddress");
|
|
110
|
+
const cand = h.digest();
|
|
111
|
+
if (!isOnCurve(cand)) return b58(cand);
|
|
112
|
+
}
|
|
113
|
+
throw new Error("no PDA bump");
|
|
114
|
+
}
|
|
115
|
+
async function getNullifierStatus(nullifierHex) {
|
|
116
|
+
const nf = Buffer.from(nullifierHex.replace(/^0x/, ""), "hex");
|
|
117
|
+
if (nf.length !== 32) throw new Error("nullifier must be 32 bytes");
|
|
118
|
+
const pda = findProgramAddress([Buffer.from("nullifier"), nf], VERIFY_PROGRAM_ID);
|
|
119
|
+
const r = await rpc("getAccountInfo", [pda, { encoding: "base64", commitment: "confirmed" }]);
|
|
120
|
+
const base = { nullifier: nullifierHex, consumed: false, pda, cluster: "mainnet-beta", rpc: process.env.BLINDSEAL_RPC_URL ? "configured" : "public" };
|
|
121
|
+
if (!r.value) return base;
|
|
122
|
+
const data = Buffer.from(r.value.data[0], "base64");
|
|
123
|
+
if (data.length < 81 || !data.subarray(0, 8).equals(disc("Nullifier"))) return base;
|
|
124
|
+
return {
|
|
125
|
+
...base,
|
|
126
|
+
consumed: true,
|
|
127
|
+
consumedAtSlot: Number(data.readBigUInt64LE(8)),
|
|
128
|
+
consumerProgram: b58(data.subarray(16, 48)),
|
|
129
|
+
actionHash: "0x" + data.subarray(48, 80).toString("hex")
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export {
|
|
134
|
+
rpcUrl,
|
|
135
|
+
getNullifierStatus
|
|
136
|
+
};
|