@fiscalmindset/blindfold 0.4.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 +21 -0
- package/README.md +105 -0
- package/assets/SKILL.md +205 -0
- package/assets/blindfold_proxy.wasm +0 -0
- package/bin/blindfold.ts +90 -0
- package/bin/cli-shared.ts +72 -0
- package/bin/cmd-auth.ts +193 -0
- package/bin/cmd-enclave.ts +373 -0
- package/bin/cmd-lifecycle.ts +151 -0
- package/bin/cmd-secrets.ts +136 -0
- package/bin/cmd-serve.ts +165 -0
- package/bin/cmd-tenant.ts +84 -0
- package/dist/cli.mjs +4317 -0
- package/dist/lib/index.mjs +2362 -0
- package/dist/lib/proxy.mjs +1078 -0
- package/dist/lib/register.mjs +756 -0
- package/dist/lib/wrap.mjs +81 -0
- package/package.json +48 -0
- package/postinstall.mjs +21 -0
- package/src/attest.ts +235 -0
- package/src/color.ts +31 -0
- package/src/compat.ts +217 -0
- package/src/constants.ts +24 -0
- package/src/dashboard.ts +785 -0
- package/src/env.ts +261 -0
- package/src/index.ts +11 -0
- package/src/init.ts +391 -0
- package/src/keychain.ts +155 -0
- package/src/log.ts +51 -0
- package/src/migrate.ts +135 -0
- package/src/prompt.ts +114 -0
- package/src/providers.ts +224 -0
- package/src/proxy.ts +454 -0
- package/src/register.ts +81 -0
- package/src/release.ts +82 -0
- package/src/sealed-ledger.ts +158 -0
- package/src/t3-client.ts +580 -0
- package/src/types.ts +50 -0
- package/src/usage-log.ts +149 -0
- package/src/versions.ts +64 -0
- package/src/wrap.ts +122 -0
|
@@ -0,0 +1,2362 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
4
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
5
|
+
}) : x)(function(x) {
|
|
6
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
7
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
8
|
+
});
|
|
9
|
+
var __esm = (fn, res) => function __init() {
|
|
10
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
11
|
+
};
|
|
12
|
+
var __export = (target, all) => {
|
|
13
|
+
for (var name in all)
|
|
14
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// src/constants.ts
|
|
18
|
+
var SENTINEL, DEFAULT_PORT, CONTRACT_TAIL, CONTRACT_VERSION, DEFAULT_DASHBOARD_PORT;
|
|
19
|
+
var init_constants = __esm({
|
|
20
|
+
"src/constants.ts"() {
|
|
21
|
+
"use strict";
|
|
22
|
+
SENTINEL = "__BLINDFOLD__";
|
|
23
|
+
DEFAULT_PORT = 8787;
|
|
24
|
+
CONTRACT_TAIL = "blindfold-proxy";
|
|
25
|
+
CONTRACT_VERSION = "0.5.6";
|
|
26
|
+
DEFAULT_DASHBOARD_PORT = 8799;
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// src/keychain.ts
|
|
31
|
+
import { spawnSync } from "node:child_process";
|
|
32
|
+
function has(cmd) {
|
|
33
|
+
const finder = isWin ? "where" : "which";
|
|
34
|
+
const r = spawnSync(finder, [cmd], { stdio: "ignore" });
|
|
35
|
+
return r.status === 0;
|
|
36
|
+
}
|
|
37
|
+
function winPS(op, target, secret) {
|
|
38
|
+
let body;
|
|
39
|
+
if (op === "set") {
|
|
40
|
+
body = `$b=[System.Text.Encoding]::UTF8.GetBytes($env:BF_SECRET);$p=[Runtime.InteropServices.Marshal]::AllocHGlobal($b.Length);[Runtime.InteropServices.Marshal]::Copy($b,0,$p,$b.Length);$c=New-Object BFCred+CREDENTIAL;$c.Type=1;$c.TargetName=$env:BF_TARGET;$c.UserName=$env:BF_TARGET;$c.CredentialBlobSize=$b.Length;$c.CredentialBlob=$p;$c.Persist=2;$ok=[BFCred]::CredWriteW([ref]$c,0);[Runtime.InteropServices.Marshal]::FreeHGlobal($p);if($ok){[Console]::Out.Write('BFOK')}`;
|
|
41
|
+
} else if (op === "get") {
|
|
42
|
+
body = `$ptr=[IntPtr]::Zero;if([BFCred]::CredReadW($env:BF_TARGET,1,0,[ref]$ptr)){$c=[Runtime.InteropServices.Marshal]::PtrToStructure($ptr,[BFCred+CREDENTIAL]);$n=[int]$c.CredentialBlobSize;$b=New-Object byte[] $n;[Runtime.InteropServices.Marshal]::Copy([IntPtr]$c.CredentialBlob,$b,0,$n);[BFCred]::CredFree($ptr);[Console]::Out.Write([System.Text.Encoding]::UTF8.GetString($b))}else{exit 1}`;
|
|
43
|
+
} else {
|
|
44
|
+
body = `if([BFCred]::CredDeleteW($env:BF_TARGET,1,0)){[Console]::Out.Write('BFOK')}`;
|
|
45
|
+
}
|
|
46
|
+
const env = { ...process.env, BF_TARGET: target };
|
|
47
|
+
if (secret !== void 0) env.BF_SECRET = secret;
|
|
48
|
+
const r = spawnSync(
|
|
49
|
+
"powershell",
|
|
50
|
+
["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", "-"],
|
|
51
|
+
{ input: PS_HEADER + body, env, encoding: "utf8" }
|
|
52
|
+
);
|
|
53
|
+
return { status: r.status, stdout: typeof r.stdout === "string" ? r.stdout : "" };
|
|
54
|
+
}
|
|
55
|
+
function winTarget(account) {
|
|
56
|
+
return `${SERVICE}:${account}`;
|
|
57
|
+
}
|
|
58
|
+
function keychainAvailable() {
|
|
59
|
+
if (process.platform === "darwin") return has("security");
|
|
60
|
+
if (process.platform === "linux") return has("secret-tool");
|
|
61
|
+
if (isWin) return has("powershell");
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
function keychainGet(account) {
|
|
65
|
+
if (process.platform === "darwin") {
|
|
66
|
+
const r = spawnSync("security", ["find-generic-password", "-a", account, "-s", SERVICE, "-w"], { encoding: "utf8" });
|
|
67
|
+
if (r.status === 0 && typeof r.stdout === "string") return r.stdout.replace(/\n$/, "");
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
if (process.platform === "linux") {
|
|
71
|
+
const r = spawnSync("secret-tool", ["lookup", "service", SERVICE, "account", account], { encoding: "utf8" });
|
|
72
|
+
if (r.status === 0 && r.stdout) return r.stdout.replace(/\n$/, "");
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
if (isWin) {
|
|
76
|
+
const r = winPS("get", winTarget(account));
|
|
77
|
+
if (r.status === 0 && r.stdout) return r.stdout.replace(/\r?\n$/, "");
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
var SERVICE, isWin, WIN_CS, PS_HEADER;
|
|
83
|
+
var init_keychain = __esm({
|
|
84
|
+
"src/keychain.ts"() {
|
|
85
|
+
"use strict";
|
|
86
|
+
SERVICE = "blindfold";
|
|
87
|
+
isWin = process.platform === "win32";
|
|
88
|
+
WIN_CS = `
|
|
89
|
+
using System;
|
|
90
|
+
using System.Runtime.InteropServices;
|
|
91
|
+
public static class BFCred {
|
|
92
|
+
[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)]
|
|
93
|
+
public struct CREDENTIAL {
|
|
94
|
+
public UInt32 Flags; public UInt32 Type;
|
|
95
|
+
[MarshalAs(UnmanagedType.LPWStr)] public string TargetName;
|
|
96
|
+
[MarshalAs(UnmanagedType.LPWStr)] public string Comment;
|
|
97
|
+
public System.Runtime.InteropServices.ComTypes.FILETIME LastWritten;
|
|
98
|
+
public UInt32 CredentialBlobSize; public IntPtr CredentialBlob; public UInt32 Persist;
|
|
99
|
+
public UInt32 AttributeCount; public IntPtr Attributes;
|
|
100
|
+
[MarshalAs(UnmanagedType.LPWStr)] public string TargetAlias;
|
|
101
|
+
[MarshalAs(UnmanagedType.LPWStr)] public string UserName;
|
|
102
|
+
}
|
|
103
|
+
[DllImport("advapi32.dll", CharSet=CharSet.Unicode, SetLastError=true)] public static extern bool CredWriteW(ref CREDENTIAL c, UInt32 f);
|
|
104
|
+
[DllImport("advapi32.dll", CharSet=CharSet.Unicode, SetLastError=true)] public static extern bool CredReadW(string t, UInt32 ty, UInt32 f, out IntPtr c);
|
|
105
|
+
[DllImport("advapi32.dll", CharSet=CharSet.Unicode, SetLastError=true)] public static extern bool CredDeleteW(string t, UInt32 ty, UInt32 f);
|
|
106
|
+
[DllImport("advapi32.dll")] public static extern void CredFree(IntPtr c);
|
|
107
|
+
}
|
|
108
|
+
`;
|
|
109
|
+
PS_HEADER = `$ErrorActionPreference='Stop'
|
|
110
|
+
Add-Type -TypeDefinition @"
|
|
111
|
+
${WIN_CS}
|
|
112
|
+
"@
|
|
113
|
+
`;
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// src/env.ts
|
|
118
|
+
import fs from "node:fs";
|
|
119
|
+
import os from "node:os";
|
|
120
|
+
import path from "node:path";
|
|
121
|
+
import { fileURLToPath } from "node:url";
|
|
122
|
+
function repoRoot() {
|
|
123
|
+
if (_repoRoot) return _repoRoot;
|
|
124
|
+
let dir = process.cwd();
|
|
125
|
+
for (let i = 0; i < 12; i++) {
|
|
126
|
+
if (fs.existsSync(path.join(dir, ".env")) || fs.existsSync(path.join(dir, ".blindfold")) || fs.existsSync(path.join(dir, ".git"))) {
|
|
127
|
+
_repoRoot = dir;
|
|
128
|
+
return dir;
|
|
129
|
+
}
|
|
130
|
+
const parent = path.dirname(dir);
|
|
131
|
+
if (parent === dir) break;
|
|
132
|
+
dir = parent;
|
|
133
|
+
}
|
|
134
|
+
_repoRoot = SRC_RELATIVE_ROOT;
|
|
135
|
+
return _repoRoot;
|
|
136
|
+
}
|
|
137
|
+
function homeDir() {
|
|
138
|
+
return path.join(os.homedir(), ".blindfold");
|
|
139
|
+
}
|
|
140
|
+
function configPath() {
|
|
141
|
+
return path.join(homeDir(), "config.json");
|
|
142
|
+
}
|
|
143
|
+
function stateDir() {
|
|
144
|
+
const override = process.env.BLINDFOLD_STATE_DIR;
|
|
145
|
+
if (override && override.trim()) return path.resolve(override.trim());
|
|
146
|
+
const home = homeDir();
|
|
147
|
+
if (!fs.existsSync(home)) {
|
|
148
|
+
try {
|
|
149
|
+
const legacy = path.join(repoRoot(), ".blindfold");
|
|
150
|
+
if (fs.existsSync(legacy) && fs.statSync(legacy).isDirectory()) {
|
|
151
|
+
fs.cpSync(legacy, home, { recursive: true });
|
|
152
|
+
} else {
|
|
153
|
+
fs.mkdirSync(home, { recursive: true });
|
|
154
|
+
}
|
|
155
|
+
} catch {
|
|
156
|
+
try {
|
|
157
|
+
fs.mkdirSync(home, { recursive: true });
|
|
158
|
+
} catch {
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return home;
|
|
163
|
+
}
|
|
164
|
+
function assertSafeOverrideUrl(raw, label) {
|
|
165
|
+
let u;
|
|
166
|
+
try {
|
|
167
|
+
u = new URL(raw);
|
|
168
|
+
} catch {
|
|
169
|
+
throw new Error(`${label} is not a valid URL: ${raw}`);
|
|
170
|
+
}
|
|
171
|
+
const isLocal = ["localhost", "127.0.0.1", "::1", "[::1]"].includes(u.hostname);
|
|
172
|
+
if (u.protocol !== "https:" && !isLocal) {
|
|
173
|
+
throw new Error(`${label} must be https (got ${u.protocol}//${u.hostname}); refusing to trust an insecure endpoint`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
function withFileLockSync(target, fn) {
|
|
177
|
+
const lockPath = `${target}.lock`;
|
|
178
|
+
const deadline = Date.now() + 1e4;
|
|
179
|
+
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
|
180
|
+
const sleep = (ms) => {
|
|
181
|
+
try {
|
|
182
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
183
|
+
} catch {
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
for (; ; ) {
|
|
187
|
+
try {
|
|
188
|
+
const fd = fs.openSync(lockPath, "wx");
|
|
189
|
+
fs.writeSync(fd, String(process.pid));
|
|
190
|
+
fs.closeSync(fd);
|
|
191
|
+
break;
|
|
192
|
+
} catch (e) {
|
|
193
|
+
if (e?.code !== "EEXIST") throw e;
|
|
194
|
+
try {
|
|
195
|
+
const st = fs.statSync(lockPath);
|
|
196
|
+
if (Date.now() - st.mtimeMs > 1e4) {
|
|
197
|
+
fs.rmSync(lockPath, { force: true });
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
} catch {
|
|
201
|
+
}
|
|
202
|
+
if (Date.now() > deadline) throw new Error(`timed out acquiring lock ${lockPath}`);
|
|
203
|
+
sleep(25);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
try {
|
|
207
|
+
return fn();
|
|
208
|
+
} finally {
|
|
209
|
+
fs.rmSync(lockPath, { force: true });
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
function defaultEnvPath() {
|
|
213
|
+
return path.join(repoRoot(), ".env");
|
|
214
|
+
}
|
|
215
|
+
function loadEnvFromFile(envPath = path.join(repoRoot(), ".env")) {
|
|
216
|
+
if (!fs.existsSync(envPath)) return;
|
|
217
|
+
const text = fs.readFileSync(envPath, "utf8");
|
|
218
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
219
|
+
const line = raw.trim();
|
|
220
|
+
if (!line || line.startsWith("#")) continue;
|
|
221
|
+
const eq = line.indexOf("=");
|
|
222
|
+
if (eq <= 0) continue;
|
|
223
|
+
const key = line.slice(0, eq).trim();
|
|
224
|
+
let val = line.slice(eq + 1).trim();
|
|
225
|
+
if (val.startsWith('"') && val.endsWith('"') || val.startsWith("'") && val.endsWith("'")) {
|
|
226
|
+
val = val.slice(1, -1);
|
|
227
|
+
}
|
|
228
|
+
if (process.env[key] === void 0) process.env[key] = val;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
function loadHomeConfig() {
|
|
232
|
+
try {
|
|
233
|
+
const cfg = configPath();
|
|
234
|
+
if (fs.existsSync(cfg)) {
|
|
235
|
+
const obj = JSON.parse(fs.readFileSync(cfg, "utf8"));
|
|
236
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
237
|
+
if (k === "T3N_API_KEY_STORE") continue;
|
|
238
|
+
if (typeof v === "string" && process.env[k] === void 0) process.env[k] = v;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
} catch {
|
|
242
|
+
}
|
|
243
|
+
loadEnvFromFile(path.join(homeDir(), ".env"));
|
|
244
|
+
if (process.env.T3N_API_KEY === void 0 && process.env.DID && keychainAvailable()) {
|
|
245
|
+
const k = keychainGet(process.env.DID);
|
|
246
|
+
if (k) process.env.T3N_API_KEY = k;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
function loadBlindfoldEnv() {
|
|
250
|
+
loadEnvFromFile();
|
|
251
|
+
loadHomeConfig();
|
|
252
|
+
const t3nApiKey = process.env.T3N_API_KEY ?? "";
|
|
253
|
+
const did = process.env.DID ?? "";
|
|
254
|
+
const port = Number.parseInt(process.env.BLINDFOLD_PORT ?? "8787", 10);
|
|
255
|
+
const t3EnvRaw = (process.env.BLINDFOLD_T3_ENV ?? "testnet").toLowerCase();
|
|
256
|
+
const t3Env = t3EnvRaw === "production" ? "production" : "testnet";
|
|
257
|
+
const t3BaseUrl = (process.env.T3_BASE_URL ?? process.env.BLINDFOLD_BASE_URL ?? "").trim();
|
|
258
|
+
if (t3BaseUrl) {
|
|
259
|
+
assertSafeOverrideUrl(t3BaseUrl, process.env.T3_BASE_URL ? "T3_BASE_URL" : "BLINDFOLD_BASE_URL");
|
|
260
|
+
}
|
|
261
|
+
const mock = process.env.BLINDFOLD_MOCK === "1";
|
|
262
|
+
return { t3nApiKey, did, port, t3Env, t3BaseUrl, mock };
|
|
263
|
+
}
|
|
264
|
+
function assertRealReady(env) {
|
|
265
|
+
if (env.mock) return;
|
|
266
|
+
const missing = [];
|
|
267
|
+
if (!env.t3nApiKey) missing.push("T3N_API_KEY");
|
|
268
|
+
if (!env.did) missing.push("DID");
|
|
269
|
+
if (missing.length > 0) {
|
|
270
|
+
throw new Error(
|
|
271
|
+
`Missing required env: ${missing.join(", ")}. Claim them at https://docs.terminal3.io/developers/adk/get-started/prerequisites/request-test-tokens, then put them in .env. Or set BLINDFOLD_MOCK=1 to use mock mode for the demo only.`
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
function pluckSecret(envName) {
|
|
276
|
+
const v = process.env[envName];
|
|
277
|
+
if (!v) {
|
|
278
|
+
throw new Error(`environment variable ${envName} is unset or empty`);
|
|
279
|
+
}
|
|
280
|
+
return v;
|
|
281
|
+
}
|
|
282
|
+
var HERE, SRC_RELATIVE_ROOT, _repoRoot;
|
|
283
|
+
var init_env = __esm({
|
|
284
|
+
"src/env.ts"() {
|
|
285
|
+
"use strict";
|
|
286
|
+
init_keychain();
|
|
287
|
+
HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
288
|
+
SRC_RELATIVE_ROOT = path.resolve(HERE, "..", "..", "..");
|
|
289
|
+
_repoRoot = null;
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// src/log.ts
|
|
294
|
+
function safeLog(level, obj) {
|
|
295
|
+
const safe = typeof obj === "string" ? { msg: obj } : redact(obj);
|
|
296
|
+
const line = JSON.stringify({ t: (/* @__PURE__ */ new Date()).toISOString(), level, ...safe });
|
|
297
|
+
process.stderr.write(line + "\n");
|
|
298
|
+
}
|
|
299
|
+
function redact(input) {
|
|
300
|
+
const out = {};
|
|
301
|
+
for (const [k, v] of Object.entries(input)) {
|
|
302
|
+
if (k === "headers" && v && typeof v === "object") {
|
|
303
|
+
out[k] = redactHeaders(v);
|
|
304
|
+
} else if (HEADER_BLOCKLIST.has(k.toLowerCase())) {
|
|
305
|
+
out[k] = "[redacted]";
|
|
306
|
+
} else {
|
|
307
|
+
out[k] = v;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return out;
|
|
311
|
+
}
|
|
312
|
+
function redactHeaders(h) {
|
|
313
|
+
if (Array.isArray(h)) {
|
|
314
|
+
return h.map(([k, v]) => [k, HEADER_BLOCKLIST.has(k.toLowerCase()) ? "[redacted]" : v]);
|
|
315
|
+
}
|
|
316
|
+
const out = {};
|
|
317
|
+
for (const [k, v] of Object.entries(h)) {
|
|
318
|
+
out[k] = HEADER_BLOCKLIST.has(k.toLowerCase()) ? "[redacted]" : String(v);
|
|
319
|
+
}
|
|
320
|
+
return out;
|
|
321
|
+
}
|
|
322
|
+
var HEADER_BLOCKLIST;
|
|
323
|
+
var init_log = __esm({
|
|
324
|
+
"src/log.ts"() {
|
|
325
|
+
"use strict";
|
|
326
|
+
HEADER_BLOCKLIST = /* @__PURE__ */ new Set([
|
|
327
|
+
"authorization",
|
|
328
|
+
"proxy-authorization",
|
|
329
|
+
"x-api-key",
|
|
330
|
+
"cookie",
|
|
331
|
+
"set-cookie"
|
|
332
|
+
]);
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
// src/t3-client.ts
|
|
337
|
+
var t3_client_exports = {};
|
|
338
|
+
__export(t3_client_exports, {
|
|
339
|
+
SignupEmailTakenError: () => SignupEmailTakenError,
|
|
340
|
+
T3TimeoutError: () => T3TimeoutError,
|
|
341
|
+
loadEgressHosts: () => loadEgressHosts,
|
|
342
|
+
openT3Client: () => openT3Client,
|
|
343
|
+
signupTenant: () => signupTenant
|
|
344
|
+
});
|
|
345
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
346
|
+
import fs2 from "node:fs";
|
|
347
|
+
import path2 from "node:path";
|
|
348
|
+
function withDeadline(promise, label, ms = T3_TIMEOUT_MS) {
|
|
349
|
+
return new Promise((resolve, reject) => {
|
|
350
|
+
const timer = setTimeout(() => reject(new T3TimeoutError(`T3 ${label} timed out after ${ms}ms`)), ms);
|
|
351
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
352
|
+
promise.then(
|
|
353
|
+
(v) => {
|
|
354
|
+
clearTimeout(timer);
|
|
355
|
+
resolve(v);
|
|
356
|
+
},
|
|
357
|
+
(e) => {
|
|
358
|
+
clearTimeout(timer);
|
|
359
|
+
reject(e);
|
|
360
|
+
}
|
|
361
|
+
);
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
function egressCachePath() {
|
|
365
|
+
return process.env.BLINDFOLD_EGRESS_CACHE ?? path2.join(stateDir(), "egress-hosts.json");
|
|
366
|
+
}
|
|
367
|
+
function loadEgressHosts(did) {
|
|
368
|
+
try {
|
|
369
|
+
const all = JSON.parse(fs2.readFileSync(egressCachePath(), "utf8"));
|
|
370
|
+
return Array.isArray(all[did]) ? all[did] : [];
|
|
371
|
+
} catch {
|
|
372
|
+
return [];
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
function saveEgressHosts(did, hosts) {
|
|
376
|
+
const p = egressCachePath();
|
|
377
|
+
withFileLockSync(p, () => {
|
|
378
|
+
let all = {};
|
|
379
|
+
try {
|
|
380
|
+
all = JSON.parse(fs2.readFileSync(p, "utf8"));
|
|
381
|
+
} catch {
|
|
382
|
+
}
|
|
383
|
+
const existing = Array.isArray(all[did]) ? all[did] : [];
|
|
384
|
+
all[did] = Array.from(/* @__PURE__ */ new Set([...existing, ...hosts])).sort();
|
|
385
|
+
fs2.mkdirSync(path2.dirname(p), { recursive: true });
|
|
386
|
+
fs2.writeFileSync(p, JSON.stringify(all, null, 2));
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
async function loadSdk() {
|
|
390
|
+
if (sdkCache) return sdkCache;
|
|
391
|
+
try {
|
|
392
|
+
const mod = await import("@terminal3/t3n-sdk");
|
|
393
|
+
sdkCache = mod;
|
|
394
|
+
return mod;
|
|
395
|
+
} catch {
|
|
396
|
+
throw new Error(
|
|
397
|
+
"@terminal3/t3n-sdk not installed. Run `npm install @terminal3/t3n-sdk` for REAL T3 mode, or set BLINDFOLD_MOCK=1 to keep using mock mode."
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
async function signupTenant(opts) {
|
|
402
|
+
const sdk = await loadSdk();
|
|
403
|
+
sdk.setEnvironment(opts.env);
|
|
404
|
+
const baseUrl = opts.baseUrl || sdk.NODE_URLS[opts.env];
|
|
405
|
+
const wasmComponent = await sdk.loadWasmComponent();
|
|
406
|
+
const key = "0x" + randomBytes(32).toString("hex");
|
|
407
|
+
const address = sdk.eth_get_address(key);
|
|
408
|
+
const t3n = new sdk.T3nClient({
|
|
409
|
+
baseUrl,
|
|
410
|
+
wasmComponent,
|
|
411
|
+
handlers: { EthSign: sdk.metamask_sign(address, void 0, key) }
|
|
412
|
+
});
|
|
413
|
+
await t3n.handshake();
|
|
414
|
+
const did = String(await t3n.authenticate(sdk.createEthAuthInput(address)));
|
|
415
|
+
const channel = { emailChannel: { emailAddress: opts.email } };
|
|
416
|
+
await t3n.otpRequest(channel);
|
|
417
|
+
const code = String(await opts.getOtpCode(opts.email, "email")).trim();
|
|
418
|
+
const verify = await t3n.otpVerify({ otpCode: code, request: channel });
|
|
419
|
+
if (verify.mergeSuggestion) {
|
|
420
|
+
throw new SignupEmailTakenError(opts.email, verify.mergeSuggestion.existingDid);
|
|
421
|
+
}
|
|
422
|
+
if (verify.status === "otp_failed" || verify.status === "otp_expired") {
|
|
423
|
+
throw new Error(
|
|
424
|
+
verify.status === "otp_expired" ? "the code expired \u2014 run `blindfold signup` again to get a fresh one" : "that code was incorrect \u2014 re-run `blindfold signup` and enter the emailed code"
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
if (opts.onKeyReady) await opts.onKeyReady(key, did);
|
|
428
|
+
const result = await t3n.submitUserInput({
|
|
429
|
+
profile: { email_address: opts.email, ...opts.profile ?? {} },
|
|
430
|
+
becomeDevTenant: true
|
|
431
|
+
});
|
|
432
|
+
const admit = result.tenantAdmit;
|
|
433
|
+
return {
|
|
434
|
+
key,
|
|
435
|
+
did,
|
|
436
|
+
address,
|
|
437
|
+
env: opts.env,
|
|
438
|
+
admitStatus: admit?.status ?? "unknown",
|
|
439
|
+
grantedCredits: admit?.grantedCredits,
|
|
440
|
+
refusedReason: admit?.reason,
|
|
441
|
+
refusedDetail: admit?.detail
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
async function openT3Client(env) {
|
|
445
|
+
if (env.mock) return openMockClient();
|
|
446
|
+
assertRealReady(env);
|
|
447
|
+
return openRealClient(env);
|
|
448
|
+
}
|
|
449
|
+
async function openRealClient(env) {
|
|
450
|
+
if (!/^0x[0-9a-fA-F]{64}$/.test(env.t3nApiKey)) {
|
|
451
|
+
throw new Error("T3N_API_KEY must be a 0x-prefixed 32-byte hex (secp256k1 private key).");
|
|
452
|
+
}
|
|
453
|
+
if (!/^did:t3n:[0-9a-fA-F]+$/.test(env.did)) {
|
|
454
|
+
throw new Error('DID must look like "did:t3n:<hex>".');
|
|
455
|
+
}
|
|
456
|
+
const sdk = await loadSdk();
|
|
457
|
+
sdk.setEnvironment(env.t3Env);
|
|
458
|
+
const baseUrl = env.t3BaseUrl || sdk.NODE_URLS[env.t3Env];
|
|
459
|
+
const wasmComponent = await sdk.loadWasmComponent();
|
|
460
|
+
const address = sdk.eth_get_address(env.t3nApiKey);
|
|
461
|
+
const t3n = new sdk.T3nClient({
|
|
462
|
+
baseUrl,
|
|
463
|
+
wasmComponent,
|
|
464
|
+
handlers: { EthSign: sdk.metamask_sign(address, void 0, env.t3nApiKey) }
|
|
465
|
+
});
|
|
466
|
+
await t3n.handshake();
|
|
467
|
+
await t3n.authenticate(sdk.createEthAuthInput(address));
|
|
468
|
+
const tenant = new sdk.TenantClient({
|
|
469
|
+
environment: env.t3Env,
|
|
470
|
+
baseUrl,
|
|
471
|
+
tenantDid: env.did,
|
|
472
|
+
t3n
|
|
473
|
+
});
|
|
474
|
+
const seedSecret = async (name, value) => {
|
|
475
|
+
await tenant.executeControl("map-entry-set", {
|
|
476
|
+
map_name: tenant.canonicalName("secrets"),
|
|
477
|
+
key: name,
|
|
478
|
+
value
|
|
479
|
+
});
|
|
480
|
+
safeLog("info", { msg: "seeded", name });
|
|
481
|
+
};
|
|
482
|
+
const registerContract2 = async (wasm) => {
|
|
483
|
+
const r = await tenant.contracts.register({
|
|
484
|
+
tail: CONTRACT_TAIL,
|
|
485
|
+
version: CONTRACT_VERSION,
|
|
486
|
+
wasm
|
|
487
|
+
});
|
|
488
|
+
return {
|
|
489
|
+
contractId: r.contract_id ?? r.contractId ?? "(unknown)"
|
|
490
|
+
};
|
|
491
|
+
};
|
|
492
|
+
const invokeForward = async (req) => {
|
|
493
|
+
const raw = await withDeadline(tenant.contracts.execute(CONTRACT_TAIL, {
|
|
494
|
+
version: CONTRACT_VERSION,
|
|
495
|
+
functionName: "forward",
|
|
496
|
+
input: req
|
|
497
|
+
}), "forward");
|
|
498
|
+
if (typeof raw.status === "number" && Array.isArray(raw.headers)) {
|
|
499
|
+
return { status: raw.status, headers: raw.headers, body: raw.body ?? "" };
|
|
500
|
+
}
|
|
501
|
+
return {
|
|
502
|
+
status: raw.code ?? 502,
|
|
503
|
+
headers: [["content-type", "application/json"]],
|
|
504
|
+
body: raw.body ?? ""
|
|
505
|
+
};
|
|
506
|
+
};
|
|
507
|
+
const decodeSecret = (v) => {
|
|
508
|
+
if (typeof v === "string") return v;
|
|
509
|
+
if (Array.isArray(v)) return Buffer.from(v).toString("utf8");
|
|
510
|
+
if (v && typeof v === "object") {
|
|
511
|
+
const o = v;
|
|
512
|
+
if (typeof o.value === "string") return o.value;
|
|
513
|
+
if (Array.isArray(o.value)) return Buffer.from(o.value).toString("utf8");
|
|
514
|
+
const entry = o.entry;
|
|
515
|
+
if (entry && typeof entry.value === "string") return entry.value;
|
|
516
|
+
}
|
|
517
|
+
return "";
|
|
518
|
+
};
|
|
519
|
+
const releaseSecret = async (name) => {
|
|
520
|
+
try {
|
|
521
|
+
const r = await withDeadline(tenant.contracts.execute(CONTRACT_TAIL, {
|
|
522
|
+
version: CONTRACT_VERSION,
|
|
523
|
+
functionName: "release-to-tenant",
|
|
524
|
+
input: { secret_key: name }
|
|
525
|
+
}), "release-to-tenant");
|
|
526
|
+
if (r && r.ok && r.value) return r.value;
|
|
527
|
+
} catch (e) {
|
|
528
|
+
if (e instanceof T3TimeoutError) throw e;
|
|
529
|
+
}
|
|
530
|
+
const direct = decodeSecret(
|
|
531
|
+
await withDeadline(tenant.executeControl("map-entry-get", {
|
|
532
|
+
map_name: tenant.canonicalName("secrets"),
|
|
533
|
+
key: name
|
|
534
|
+
}), "map-entry-get")
|
|
535
|
+
);
|
|
536
|
+
if (!direct) throw new Error(`secret "${name}" not found in the secrets map`);
|
|
537
|
+
return direct;
|
|
538
|
+
};
|
|
539
|
+
const me = async () => {
|
|
540
|
+
const info = await tenant.tenant.me();
|
|
541
|
+
return { tenant: String(info.tenant ?? ""), status: info.status };
|
|
542
|
+
};
|
|
543
|
+
const agentAuthUpdate = async (agentDid, hosts, functions) => {
|
|
544
|
+
let ucv = "0.1.0";
|
|
545
|
+
if (typeof sdk.getScriptVersion === "function") {
|
|
546
|
+
try {
|
|
547
|
+
const v = await sdk.getScriptVersion(baseUrl, "tee:user/contracts");
|
|
548
|
+
if (typeof v === "string" && /^\d/.test(v)) ucv = v;
|
|
549
|
+
} catch {
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
const didHex = env.did.replace(/^did:t3n:/, "");
|
|
553
|
+
const revoking = functions.length === 0 && hosts.length === 0;
|
|
554
|
+
const scripts = [{
|
|
555
|
+
scriptName: `z:${didHex}:${CONTRACT_TAIL}`,
|
|
556
|
+
versionReq: `>=${CONTRACT_VERSION}`,
|
|
557
|
+
functions: revoking ? ["forward"] : functions,
|
|
558
|
+
allowedHosts: revoking ? [] : hosts
|
|
559
|
+
}];
|
|
560
|
+
await t3n.execute({
|
|
561
|
+
script_name: "tee:user/contracts",
|
|
562
|
+
script_version: ucv,
|
|
563
|
+
function_name: "agent-auth-update",
|
|
564
|
+
input: { agents: [{ agentDid, scripts }] }
|
|
565
|
+
});
|
|
566
|
+
};
|
|
567
|
+
const grantEgress = async (hosts, opts) => {
|
|
568
|
+
const prev = opts?.replace ? [] : loadEgressHosts(env.did);
|
|
569
|
+
const merged = Array.from(/* @__PURE__ */ new Set([...prev, ...hosts])).sort();
|
|
570
|
+
await agentAuthUpdate(env.did, merged, ["forward", "release-to-tenant"]);
|
|
571
|
+
saveEgressHosts(env.did, merged);
|
|
572
|
+
return merged;
|
|
573
|
+
};
|
|
574
|
+
const setAgentGrant = (agentDid, hosts, functions) => agentAuthUpdate(agentDid, hosts, functions);
|
|
575
|
+
const verifySecret = async (name) => {
|
|
576
|
+
try {
|
|
577
|
+
const value = await releaseSecret(name);
|
|
578
|
+
return { present: true, length: value.length, fingerprint: createHash("sha256").update(value).digest("hex").slice(0, 8) };
|
|
579
|
+
} catch {
|
|
580
|
+
return { present: false, length: 0, fingerprint: "" };
|
|
581
|
+
}
|
|
582
|
+
};
|
|
583
|
+
const getBalance = async () => {
|
|
584
|
+
const page = await withDeadline(tenant.token.getUsage(), "token.getUsage");
|
|
585
|
+
const b = page.balance ?? {};
|
|
586
|
+
return {
|
|
587
|
+
available: Number(b.available ?? 0),
|
|
588
|
+
reserved: Number(b.reserved ?? 0),
|
|
589
|
+
creditExhausted: Boolean(b.credit_exhausted),
|
|
590
|
+
storageDeposit: b.storage_deposit != null ? Number(b.storage_deposit) : void 0
|
|
591
|
+
};
|
|
592
|
+
};
|
|
593
|
+
return {
|
|
594
|
+
close: async () => {
|
|
595
|
+
},
|
|
596
|
+
seedSecret,
|
|
597
|
+
invokeForward,
|
|
598
|
+
registerContract: registerContract2,
|
|
599
|
+
releaseSecret,
|
|
600
|
+
me,
|
|
601
|
+
grantEgress,
|
|
602
|
+
setAgentGrant,
|
|
603
|
+
verifySecret,
|
|
604
|
+
getBalance,
|
|
605
|
+
isReal: true
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
function openMockClient() {
|
|
609
|
+
return {
|
|
610
|
+
close: async () => {
|
|
611
|
+
},
|
|
612
|
+
async seedSecret(name, value) {
|
|
613
|
+
if (!value || value.length === 0) throw new Error(`secret ${name} is empty`);
|
|
614
|
+
safeLog("info", { msg: "mock-seed (value dropped, length only)", name, length: value.length });
|
|
615
|
+
},
|
|
616
|
+
async registerContract(wasm) {
|
|
617
|
+
safeLog("info", { msg: "mock-register-contract", wasmBytes: wasm.byteLength });
|
|
618
|
+
return { contractId: "mock-contract-1" };
|
|
619
|
+
},
|
|
620
|
+
async invokeForward(req) {
|
|
621
|
+
safeLog("info", {
|
|
622
|
+
msg: "mock-forward",
|
|
623
|
+
method: req.method,
|
|
624
|
+
url: schemeAndHost(req.url),
|
|
625
|
+
secret_key: req.secret_key
|
|
626
|
+
});
|
|
627
|
+
const body = `{"mock":true,"note":"Blindfold mock mode \u2014 no real call made.","echo":{"url":${JSON.stringify(req.url)}}}`;
|
|
628
|
+
return { status: 200, headers: [["content-type", "application/json"]], body };
|
|
629
|
+
},
|
|
630
|
+
async releaseSecret(name) {
|
|
631
|
+
safeLog("info", { msg: "mock-release", name });
|
|
632
|
+
return `mock-released:${name}`;
|
|
633
|
+
},
|
|
634
|
+
async me() {
|
|
635
|
+
return { tenant: "did:t3n:mock", status: "active" };
|
|
636
|
+
},
|
|
637
|
+
async grantEgress(hosts) {
|
|
638
|
+
safeLog("info", { msg: "mock-grant-egress", hosts });
|
|
639
|
+
return hosts;
|
|
640
|
+
},
|
|
641
|
+
async setAgentGrant(agentDid, hosts, functions) {
|
|
642
|
+
safeLog("info", { msg: "mock-set-agent-grant", agentDid, hosts, functions });
|
|
643
|
+
},
|
|
644
|
+
async verifySecret(name) {
|
|
645
|
+
return { present: true, length: 0, fingerprint: `mock-${name}`.slice(0, 8) };
|
|
646
|
+
},
|
|
647
|
+
async getBalance() {
|
|
648
|
+
return { available: 1e9, reserved: 0, creditExhausted: false, mock: true };
|
|
649
|
+
},
|
|
650
|
+
isReal: false
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
function schemeAndHost(url) {
|
|
654
|
+
try {
|
|
655
|
+
const u = new URL(url);
|
|
656
|
+
return `${u.protocol}//${u.host}`;
|
|
657
|
+
} catch {
|
|
658
|
+
return "<bad-url>";
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
var T3_TIMEOUT_MS, T3TimeoutError, sdkCache, SignupEmailTakenError;
|
|
662
|
+
var init_t3_client = __esm({
|
|
663
|
+
"src/t3-client.ts"() {
|
|
664
|
+
"use strict";
|
|
665
|
+
init_constants();
|
|
666
|
+
init_env();
|
|
667
|
+
init_log();
|
|
668
|
+
T3_TIMEOUT_MS = Number(process.env.BLINDFOLD_T3_TIMEOUT_MS) || 3e4;
|
|
669
|
+
T3TimeoutError = class extends Error {
|
|
670
|
+
};
|
|
671
|
+
sdkCache = null;
|
|
672
|
+
SignupEmailTakenError = class extends Error {
|
|
673
|
+
constructor(email, existingDid) {
|
|
674
|
+
super(`"${email}" is already registered to Terminal 3 tenant ${existingDid}.`);
|
|
675
|
+
this.email = email;
|
|
676
|
+
this.existingDid = existingDid;
|
|
677
|
+
this.name = "SignupEmailTakenError";
|
|
678
|
+
}
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
// src/index.ts
|
|
684
|
+
init_constants();
|
|
685
|
+
|
|
686
|
+
// src/proxy.ts
|
|
687
|
+
init_constants();
|
|
688
|
+
init_env();
|
|
689
|
+
init_log();
|
|
690
|
+
init_t3_client();
|
|
691
|
+
import http from "node:http";
|
|
692
|
+
import crypto from "node:crypto";
|
|
693
|
+
import fs4 from "node:fs";
|
|
694
|
+
import net from "node:net";
|
|
695
|
+
|
|
696
|
+
// src/usage-log.ts
|
|
697
|
+
init_env();
|
|
698
|
+
import fs3 from "node:fs";
|
|
699
|
+
import path3 from "node:path";
|
|
700
|
+
function defaultLogPath() {
|
|
701
|
+
return process.env.BLINDFOLD_USAGE_LOG ?? path3.join(stateDir(), "usage.jsonl");
|
|
702
|
+
}
|
|
703
|
+
var USAGE_MAX_BYTES = Number(process.env.BLINDFOLD_USAGE_MAX_BYTES) || 10 * 1024 * 1024;
|
|
704
|
+
function rotateIfNeeded(p) {
|
|
705
|
+
try {
|
|
706
|
+
const size = fs3.statSync(p).size;
|
|
707
|
+
if (size < USAGE_MAX_BYTES) return;
|
|
708
|
+
if (fs3.existsSync(`${p}.1`)) fs3.renameSync(`${p}.1`, `${p}.2`);
|
|
709
|
+
fs3.renameSync(p, `${p}.1`);
|
|
710
|
+
} catch {
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
var ensuredDirs = /* @__PURE__ */ new Set();
|
|
714
|
+
var ROTATE_CHECK_EVERY = 500;
|
|
715
|
+
var writesSinceRotateCheck = ROTATE_CHECK_EVERY;
|
|
716
|
+
function logUsage(event) {
|
|
717
|
+
const p = defaultLogPath();
|
|
718
|
+
try {
|
|
719
|
+
const dir = path3.dirname(p);
|
|
720
|
+
if (!ensuredDirs.has(dir)) {
|
|
721
|
+
fs3.mkdirSync(dir, { recursive: true });
|
|
722
|
+
ensuredDirs.add(dir);
|
|
723
|
+
}
|
|
724
|
+
if (++writesSinceRotateCheck >= ROTATE_CHECK_EVERY) {
|
|
725
|
+
writesSinceRotateCheck = 0;
|
|
726
|
+
rotateIfNeeded(p);
|
|
727
|
+
}
|
|
728
|
+
fs3.appendFile(p, JSON.stringify(event) + "\n", () => {
|
|
729
|
+
});
|
|
730
|
+
} catch {
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
function readUsage() {
|
|
734
|
+
const p = defaultLogPath();
|
|
735
|
+
if (!fs3.existsSync(p)) return [];
|
|
736
|
+
return fs3.readFileSync(p, "utf8").split("\n").filter(Boolean).map((line) => {
|
|
737
|
+
try {
|
|
738
|
+
return JSON.parse(line);
|
|
739
|
+
} catch {
|
|
740
|
+
return null;
|
|
741
|
+
}
|
|
742
|
+
}).filter((e) => e !== null);
|
|
743
|
+
}
|
|
744
|
+
function readUsageTail(n = 500) {
|
|
745
|
+
const p = defaultLogPath();
|
|
746
|
+
if (!fs3.existsSync(p)) return [];
|
|
747
|
+
const fd = fs3.openSync(p, "r");
|
|
748
|
+
try {
|
|
749
|
+
const size = fs3.fstatSync(fd).size;
|
|
750
|
+
const readLen = Math.min(size, Math.max(64 * 1024, n * 2048));
|
|
751
|
+
const buf = Buffer.alloc(readLen);
|
|
752
|
+
fs3.readSync(fd, buf, 0, readLen, size - readLen);
|
|
753
|
+
let text = buf.toString("utf8");
|
|
754
|
+
if (readLen < size) {
|
|
755
|
+
const nl = text.indexOf("\n");
|
|
756
|
+
if (nl >= 0) text = text.slice(nl + 1);
|
|
757
|
+
}
|
|
758
|
+
const events = text.split("\n").filter(Boolean).map((line) => {
|
|
759
|
+
try {
|
|
760
|
+
return JSON.parse(line);
|
|
761
|
+
} catch {
|
|
762
|
+
return null;
|
|
763
|
+
}
|
|
764
|
+
}).filter((e) => e !== null);
|
|
765
|
+
return events.slice(-n);
|
|
766
|
+
} finally {
|
|
767
|
+
fs3.closeSync(fd);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
function clearUsage() {
|
|
771
|
+
const p = defaultLogPath();
|
|
772
|
+
if (fs3.existsSync(p)) fs3.unlinkSync(p);
|
|
773
|
+
}
|
|
774
|
+
function providerForUpstream(upstream) {
|
|
775
|
+
try {
|
|
776
|
+
const u = new URL(upstream);
|
|
777
|
+
const h = u.hostname;
|
|
778
|
+
if (h.endsWith("openai.com")) return "openai";
|
|
779
|
+
if (h.endsWith("anthropic.com")) return "anthropic";
|
|
780
|
+
if (h.endsWith("googleapis.com")) return "google";
|
|
781
|
+
if (h.endsWith("x.ai")) return "xai";
|
|
782
|
+
if (h.endsWith("groq.com")) return "groq";
|
|
783
|
+
return h;
|
|
784
|
+
} catch {
|
|
785
|
+
return "unknown";
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// src/providers.ts
|
|
790
|
+
init_constants();
|
|
791
|
+
function amzDate(now = /* @__PURE__ */ new Date()) {
|
|
792
|
+
const p = (n) => String(n).padStart(2, "0");
|
|
793
|
+
return `${now.getUTCFullYear()}${p(now.getUTCMonth() + 1)}${p(now.getUTCDate())}T${p(now.getUTCHours())}${p(now.getUTCMinutes())}${p(now.getUTCSeconds())}Z`;
|
|
794
|
+
}
|
|
795
|
+
var awsRegion = () => process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION || "us-east-1";
|
|
796
|
+
var awsAccessKeyId = () => process.env.AWS_ACCESS_KEY_ID || "";
|
|
797
|
+
function stripPrefix(path6, prefix) {
|
|
798
|
+
const rest = path6.slice(prefix.length - 1);
|
|
799
|
+
return rest.startsWith("/") ? rest : `/${rest}`;
|
|
800
|
+
}
|
|
801
|
+
var PROVIDERS = [
|
|
802
|
+
// ---- LLM providers (OpenAI-shaped, bearer). Back-compatible. --------------
|
|
803
|
+
{ id: "openai", prefix: "/v1/", upstream: (p) => `https://api.openai.com${p}`, auth: () => ({ scheme: "bearer" }) },
|
|
804
|
+
{ id: "openai", prefix: "/openai/", upstream: (p) => `https://api.openai.com${stripPrefix(p, "/openai/")}`, auth: () => ({ scheme: "bearer" }) },
|
|
805
|
+
// Anthropic REQUIRES the anthropic-version header on the raw REST API.
|
|
806
|
+
{ id: "anthropic", prefix: "/anthropic/", upstream: (p) => `https://api.anthropic.com${stripPrefix(p, "/anthropic/")}`, auth: () => ({ scheme: "bearer" }), defaultHeaders: { "anthropic-version": "2023-06-01" } },
|
|
807
|
+
{ id: "xai", prefix: "/x/", upstream: (p) => `https://api.x.ai${stripPrefix(p, "/x/")}`, auth: () => ({ scheme: "bearer" }) },
|
|
808
|
+
{ id: "groq", prefix: "/groq/", upstream: (p) => `https://api.groq.com/openai${stripPrefix(p, "/groq/")}`, auth: () => ({ scheme: "bearer" }) },
|
|
809
|
+
// ---- LLM: Google Gemini (native API — key rides in `x-goog-api-key`). ------
|
|
810
|
+
// Not OpenAI-shaped and NOT `Authorization: Bearer`. The sentinel is planted
|
|
811
|
+
// in Google's provider-specific header and swapped for the sealed key inside
|
|
812
|
+
// the enclave — proving Blindfold handles a provider's real auth convention,
|
|
813
|
+
// not just the one bearer shape.
|
|
814
|
+
{
|
|
815
|
+
id: "gemini",
|
|
816
|
+
prefix: "/gemini/",
|
|
817
|
+
upstream: (p) => `https://generativelanguage.googleapis.com${stripPrefix(p, "/gemini/")}`,
|
|
818
|
+
secretKey: "gemini_api_key",
|
|
819
|
+
auth: () => ({ scheme: "bearer" }),
|
|
820
|
+
sentinelHeader: { name: "x-goog-api-key", prefix: "" }
|
|
821
|
+
},
|
|
822
|
+
// ---- Payments: Stripe (bearer; pins the API version so behaviour is stable). -----
|
|
823
|
+
{
|
|
824
|
+
id: "stripe",
|
|
825
|
+
prefix: "/stripe/",
|
|
826
|
+
upstream: (p) => `https://api.stripe.com${stripPrefix(p, "/stripe/")}`,
|
|
827
|
+
secretKey: "stripe_secret_key",
|
|
828
|
+
auth: () => ({ scheme: "bearer" }),
|
|
829
|
+
defaultHeaders: { "stripe-version": "2024-06-20", "content-type": "application/x-www-form-urlencoded" }
|
|
830
|
+
},
|
|
831
|
+
// ---- Dev infra: GitHub. GitHub REJECTS requests with no User-Agent (403),
|
|
832
|
+
// and best practice is to pin the REST API version + set the Accept type.
|
|
833
|
+
{
|
|
834
|
+
id: "github",
|
|
835
|
+
prefix: "/github/",
|
|
836
|
+
upstream: (p) => `https://api.github.com${stripPrefix(p, "/github/")}`,
|
|
837
|
+
secretKey: "github_token",
|
|
838
|
+
auth: () => ({ scheme: "bearer" }),
|
|
839
|
+
defaultHeaders: {
|
|
840
|
+
accept: "application/vnd.github+json",
|
|
841
|
+
"x-github-api-version": "2022-11-28",
|
|
842
|
+
"user-agent": "blindfold"
|
|
843
|
+
}
|
|
844
|
+
},
|
|
845
|
+
// ---- Email: SendGrid (bearer; JSON v3 API). -------------------------------
|
|
846
|
+
{
|
|
847
|
+
id: "sendgrid",
|
|
848
|
+
prefix: "/sendgrid/",
|
|
849
|
+
upstream: (p) => `https://api.sendgrid.com${stripPrefix(p, "/sendgrid/")}`,
|
|
850
|
+
secretKey: "sendgrid_api_key",
|
|
851
|
+
auth: () => ({ scheme: "bearer" }),
|
|
852
|
+
defaultHeaders: { "content-type": "application/json" }
|
|
853
|
+
},
|
|
854
|
+
// ---- Comms: Slack (bearer bot token; Web API wants JSON+charset on POST). --
|
|
855
|
+
{
|
|
856
|
+
id: "slack",
|
|
857
|
+
prefix: "/slack/",
|
|
858
|
+
upstream: (p) => `https://slack.com/api${stripPrefix(p, "/slack/")}`,
|
|
859
|
+
secretKey: "slack_bot_token",
|
|
860
|
+
auth: () => ({ scheme: "bearer" }),
|
|
861
|
+
defaultHeaders: { "content-type": "application/json; charset=utf-8" }
|
|
862
|
+
},
|
|
863
|
+
// ---- Telephony: Twilio (HTTP Basic — base64 computed IN the enclave). -----
|
|
864
|
+
// Username = Account SID (not secret; also appears in the URL path). The
|
|
865
|
+
// sealed secret is the Auth Token. A generic proxy CANNOT do this: the
|
|
866
|
+
// base64 must be computed after the secret is joined, inside TDX.
|
|
867
|
+
{
|
|
868
|
+
id: "twilio",
|
|
869
|
+
prefix: "/twilio/",
|
|
870
|
+
upstream: (p) => `https://api.twilio.com${stripPrefix(p, "/twilio/")}`,
|
|
871
|
+
secretKey: "twilio_auth_token",
|
|
872
|
+
auth: () => ({ scheme: "basic", username: process.env.TWILIO_ACCOUNT_SID || "" }),
|
|
873
|
+
defaultHeaders: { "content-type": "application/x-www-form-urlencoded" }
|
|
874
|
+
},
|
|
875
|
+
// ---- Cloud: AWS SES (SigV4 — secret SIGNS, never transmitted). ------------
|
|
876
|
+
{
|
|
877
|
+
id: "aws-ses",
|
|
878
|
+
prefix: "/aws/ses/",
|
|
879
|
+
upstream: (p) => `https://email.${awsRegion()}.amazonaws.com${stripPrefix(p, "/aws/ses/")}`,
|
|
880
|
+
secretKey: "aws_secret_access_key",
|
|
881
|
+
auth: () => ({ scheme: "sigv4", access_key_id: awsAccessKeyId(), region: awsRegion(), service: "ses", amz_date: amzDate() })
|
|
882
|
+
},
|
|
883
|
+
// ---- Cloud: AWS S3 (SigV4). -----------------------------------------------
|
|
884
|
+
{
|
|
885
|
+
id: "aws-s3",
|
|
886
|
+
prefix: "/aws/s3/",
|
|
887
|
+
upstream: (p) => `https://s3.${awsRegion()}.amazonaws.com${stripPrefix(p, "/aws/s3/")}`,
|
|
888
|
+
secretKey: "aws_secret_access_key",
|
|
889
|
+
auth: () => ({ scheme: "sigv4", access_key_id: awsAccessKeyId(), region: awsRegion(), service: "s3", amz_date: amzDate() })
|
|
890
|
+
},
|
|
891
|
+
// ---- Webhook: Discord. The SECRET IS THE URL — the enclave substitutes the
|
|
892
|
+
// sealed webhook URL for the sentinel, so the agent POSTs to /discord
|
|
893
|
+
// with only a JSON body and never holds the URL. Needs egress for
|
|
894
|
+
// discord.com (`blindfold grant --host discord.com`).
|
|
895
|
+
{
|
|
896
|
+
id: "discord",
|
|
897
|
+
prefix: "/discord",
|
|
898
|
+
upstream: () => SENTINEL,
|
|
899
|
+
// enclave replaces this with the sealed webhook URL
|
|
900
|
+
secretKey: "webhook_discord_url",
|
|
901
|
+
auth: () => ({ scheme: "webhook" })
|
|
902
|
+
}
|
|
903
|
+
];
|
|
904
|
+
function resolveProvider(path6) {
|
|
905
|
+
const def = PROVIDERS.filter((d) => path6.startsWith(d.prefix)).sort((a, b) => b.prefix.length - a.prefix.length)[0];
|
|
906
|
+
if (!def) return null;
|
|
907
|
+
return {
|
|
908
|
+
id: def.id,
|
|
909
|
+
upstream: def.upstream(path6),
|
|
910
|
+
secretKey: def.secretKey,
|
|
911
|
+
auth: def.auth(),
|
|
912
|
+
sentinelHeader: def.sentinelHeader,
|
|
913
|
+
defaultHeaders: def.defaultHeaders
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
// src/proxy.ts
|
|
918
|
+
var MAX_BODY_BYTES = Number(process.env.BLINDFOLD_MAX_BODY_BYTES) || 5 * 1024 * 1024;
|
|
919
|
+
var BODY_IDLE_MS = Number(process.env.BLINDFOLD_BODY_IDLE_MS) || 3e4;
|
|
920
|
+
var MAX_INFLIGHT = Number(process.env.BLINDFOLD_MAX_INFLIGHT) || 64;
|
|
921
|
+
var inflight = 0;
|
|
922
|
+
var PayloadTooLargeError = class extends Error {
|
|
923
|
+
};
|
|
924
|
+
function tokenMatches(provided, expected) {
|
|
925
|
+
const a = Buffer.from(provided);
|
|
926
|
+
const b = Buffer.from(expected);
|
|
927
|
+
if (a.length !== b.length) return false;
|
|
928
|
+
return crypto.timingSafeEqual(a, b);
|
|
929
|
+
}
|
|
930
|
+
var PROXY_TOKEN_HEADER = "x-blindfold-token";
|
|
931
|
+
function isSocketLive(socketPath) {
|
|
932
|
+
return new Promise((resolve) => {
|
|
933
|
+
if (!fs4.existsSync(socketPath)) {
|
|
934
|
+
resolve(false);
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
const c = net.connect(socketPath);
|
|
938
|
+
const done = (live) => {
|
|
939
|
+
try {
|
|
940
|
+
c.destroy();
|
|
941
|
+
} catch {
|
|
942
|
+
}
|
|
943
|
+
resolve(live);
|
|
944
|
+
};
|
|
945
|
+
c.once("connect", () => done(true));
|
|
946
|
+
c.once("error", () => done(false));
|
|
947
|
+
c.setTimeout(500, () => done(false));
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
async function startProxy(opts = {}) {
|
|
951
|
+
const env = loadBlindfoldEnv();
|
|
952
|
+
const port = opts.port ?? env.port ?? 8787;
|
|
953
|
+
const secretKey = opts.secretKey ?? "openai_api_key";
|
|
954
|
+
const token = (opts.token ?? process.env.BLINDFOLD_PROXY_TOKEN ?? "").trim() || void 0;
|
|
955
|
+
const socketPath = opts.socket?.trim() || void 0;
|
|
956
|
+
if (socketPath && await isSocketLive(socketPath)) {
|
|
957
|
+
throw new Error(`a Blindfold proxy is already listening on ${socketPath} \u2014 stop it first, or use a different --socket path`);
|
|
958
|
+
}
|
|
959
|
+
const t3 = await openT3Client(env);
|
|
960
|
+
const server = http.createServer((req, res) => {
|
|
961
|
+
handle(req, res, t3, secretKey, env, token).catch((e) => {
|
|
962
|
+
if (e instanceof PayloadTooLargeError) {
|
|
963
|
+
if (!res.headersSent) res.writeHead(413, { "content-type": "text/plain", "connection": "close" });
|
|
964
|
+
res.end("request body too large");
|
|
965
|
+
req.destroy();
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
safeLog("error", { msg: "proxy_unhandled", error: e.message });
|
|
969
|
+
if (!res.headersSent) res.writeHead(500, { "content-type": "text/plain" });
|
|
970
|
+
res.end("internal proxy error");
|
|
971
|
+
});
|
|
972
|
+
});
|
|
973
|
+
return await new Promise((resolve, reject) => {
|
|
974
|
+
server.once("error", reject);
|
|
975
|
+
let prevUmask;
|
|
976
|
+
const onListening = () => {
|
|
977
|
+
let url;
|
|
978
|
+
let bound = 0;
|
|
979
|
+
if (socketPath) {
|
|
980
|
+
try {
|
|
981
|
+
fs4.chmodSync(socketPath, 384);
|
|
982
|
+
} catch {
|
|
983
|
+
}
|
|
984
|
+
if (prevUmask !== void 0) {
|
|
985
|
+
try {
|
|
986
|
+
process.umask(prevUmask);
|
|
987
|
+
} catch {
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
url = `unix:${socketPath}`;
|
|
991
|
+
} else {
|
|
992
|
+
bound = server.address().port;
|
|
993
|
+
url = `http://127.0.0.1:${bound}`;
|
|
994
|
+
}
|
|
995
|
+
safeLog("info", { msg: "proxy_listening", url, mock: env.mock });
|
|
996
|
+
resolve({
|
|
997
|
+
url,
|
|
998
|
+
port: bound,
|
|
999
|
+
token,
|
|
1000
|
+
socket: socketPath,
|
|
1001
|
+
close: async () => {
|
|
1002
|
+
await new Promise((r) => server.close(() => r()));
|
|
1003
|
+
if (socketPath) {
|
|
1004
|
+
try {
|
|
1005
|
+
fs4.rmSync(socketPath, { force: true });
|
|
1006
|
+
} catch {
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
await t3.close();
|
|
1010
|
+
}
|
|
1011
|
+
});
|
|
1012
|
+
};
|
|
1013
|
+
if (socketPath) {
|
|
1014
|
+
try {
|
|
1015
|
+
fs4.rmSync(socketPath, { force: true });
|
|
1016
|
+
} catch {
|
|
1017
|
+
}
|
|
1018
|
+
prevUmask = process.umask(127);
|
|
1019
|
+
server.listen(socketPath, onListening);
|
|
1020
|
+
} else {
|
|
1021
|
+
server.listen(port, "127.0.0.1", onListening);
|
|
1022
|
+
}
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
async function handle(req, res, t3, secretKey, env, token) {
|
|
1026
|
+
if (req.url === "/health") {
|
|
1027
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
1028
|
+
res.end(JSON.stringify({ ok: true, mock: env.mock, auth: Boolean(token) }));
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
if (token) {
|
|
1032
|
+
const provided = String(req.headers[PROXY_TOKEN_HEADER] ?? "");
|
|
1033
|
+
if (!provided || !tokenMatches(provided, token)) {
|
|
1034
|
+
safeLog("warn", { msg: "proxy_unauthorized", path: req.url });
|
|
1035
|
+
res.writeHead(401, { "content-type": "text/plain" });
|
|
1036
|
+
res.end(`unauthorized: missing or invalid ${PROXY_TOKEN_HEADER} header`);
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
const provider = resolveProvider(req.url ?? "/");
|
|
1041
|
+
if (!provider) {
|
|
1042
|
+
res.writeHead(404, { "content-type": "text/plain" });
|
|
1043
|
+
res.end(`no upstream mapping for ${req.url}`);
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
const upstream = provider.upstream;
|
|
1047
|
+
const providerSecretKey = provider.secretKey ?? secretKey;
|
|
1048
|
+
const body = await readBody(req, MAX_BODY_BYTES);
|
|
1049
|
+
const agentSuppliedAuth = Object.keys(req.headers).some((k) => k.toLowerCase() === "authorization");
|
|
1050
|
+
const headers = forwardableHeaders(req.headers);
|
|
1051
|
+
if (provider.auth.scheme === "bearer") {
|
|
1052
|
+
const sh = provider.sentinelHeader ?? { name: "authorization", prefix: "Bearer " };
|
|
1053
|
+
removeHeader(headers, "authorization");
|
|
1054
|
+
ensureHeader(headers, sh.name, `${sh.prefix}${SENTINEL}`);
|
|
1055
|
+
} else {
|
|
1056
|
+
removeHeader(headers, "authorization");
|
|
1057
|
+
}
|
|
1058
|
+
for (const [name, value] of Object.entries(provider.defaultHeaders ?? {})) {
|
|
1059
|
+
if (!headers.some(([k]) => k.toLowerCase() === name.toLowerCase())) {
|
|
1060
|
+
headers.push([name, value]);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
let outboundUrl = upstream;
|
|
1064
|
+
let outboundBody = body.length ? body.toString("utf8") : void 0;
|
|
1065
|
+
const contentType = (headers.find(([k]) => k.toLowerCase() === "content-type")?.[1] ?? "").toLowerCase();
|
|
1066
|
+
if (provider.auth.scheme !== "webhook" && outboundBody && contentType.includes("application/x-www-form-urlencoded")) {
|
|
1067
|
+
const u = new URL(outboundUrl);
|
|
1068
|
+
for (const [k, v] of new URLSearchParams(outboundBody)) u.searchParams.append(k, v);
|
|
1069
|
+
outboundUrl = u.toString();
|
|
1070
|
+
outboundBody = void 0;
|
|
1071
|
+
safeLog("info", { msg: "form_body_to_query", provider: provider.id });
|
|
1072
|
+
}
|
|
1073
|
+
const forwardReq = {
|
|
1074
|
+
method: req.method ?? "GET",
|
|
1075
|
+
url: outboundUrl,
|
|
1076
|
+
headers,
|
|
1077
|
+
body: outboundBody,
|
|
1078
|
+
secret_key: providerSecretKey,
|
|
1079
|
+
auth: provider.auth
|
|
1080
|
+
};
|
|
1081
|
+
safeLog("info", {
|
|
1082
|
+
msg: "proxy_forward",
|
|
1083
|
+
method: forwardReq.method,
|
|
1084
|
+
upstream: upstream.replace(/\?.*$/, "")
|
|
1085
|
+
});
|
|
1086
|
+
if (inflight >= MAX_INFLIGHT) {
|
|
1087
|
+
safeLog("warn", { msg: "proxy_saturated", inflight, max: MAX_INFLIGHT });
|
|
1088
|
+
if (!res.headersSent) res.writeHead(503, { "content-type": "text/plain", "retry-after": "1" });
|
|
1089
|
+
res.end("proxy saturated: too many concurrent enclave calls \u2014 retry shortly");
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
const startedAt = Date.now();
|
|
1093
|
+
let result;
|
|
1094
|
+
inflight++;
|
|
1095
|
+
try {
|
|
1096
|
+
result = await t3.invokeForward(forwardReq);
|
|
1097
|
+
} catch (e) {
|
|
1098
|
+
const { status, body: body2 } = explainForwardError(e);
|
|
1099
|
+
safeLog("error", { msg: "proxy_forward_failed", status, provider: provider.id, error: e.message });
|
|
1100
|
+
if (!res.headersSent) res.writeHead(status, { "content-type": "application/json" });
|
|
1101
|
+
res.end(body2);
|
|
1102
|
+
return;
|
|
1103
|
+
} finally {
|
|
1104
|
+
inflight--;
|
|
1105
|
+
}
|
|
1106
|
+
const latency = Date.now() - startedAt;
|
|
1107
|
+
logUsage({
|
|
1108
|
+
t: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1109
|
+
mode: env.mock ? "mock" : "real",
|
|
1110
|
+
provider: provider.id || providerForUpstream(upstream),
|
|
1111
|
+
method: forwardReq.method,
|
|
1112
|
+
path: req.url ?? "/",
|
|
1113
|
+
upstream: upstream.replace(/\?.*$/, ""),
|
|
1114
|
+
status: result.status,
|
|
1115
|
+
latency_ms: latency,
|
|
1116
|
+
agent_supplied_auth: agentSuppliedAuth,
|
|
1117
|
+
auth_scheme: provider.auth.scheme,
|
|
1118
|
+
sentinel_in_outbound: forwardReq.headers.some(([k, v]) => k.toLowerCase() === "authorization" && v.includes(SENTINEL)),
|
|
1119
|
+
via: "proxy",
|
|
1120
|
+
secret_key: providerSecretKey
|
|
1121
|
+
});
|
|
1122
|
+
res.writeHead(result.status, headersFromTuple(result.headers));
|
|
1123
|
+
const bodyBytes = typeof result.body === "string" ? Buffer.from(result.body, "utf8") : Buffer.from(result.body);
|
|
1124
|
+
res.end(bodyBytes);
|
|
1125
|
+
}
|
|
1126
|
+
function readBody(req, maxBytes) {
|
|
1127
|
+
return new Promise((resolve, reject) => {
|
|
1128
|
+
const chunks = [];
|
|
1129
|
+
let size = 0;
|
|
1130
|
+
let aborted = false;
|
|
1131
|
+
let timer;
|
|
1132
|
+
const fail = (e) => {
|
|
1133
|
+
aborted = true;
|
|
1134
|
+
clearTimeout(timer);
|
|
1135
|
+
reject(e);
|
|
1136
|
+
};
|
|
1137
|
+
const arm = () => {
|
|
1138
|
+
clearTimeout(timer);
|
|
1139
|
+
timer = setTimeout(() => {
|
|
1140
|
+
if (!aborted) fail(new Error(`request body read timed out after ${BODY_IDLE_MS}ms`));
|
|
1141
|
+
}, BODY_IDLE_MS);
|
|
1142
|
+
};
|
|
1143
|
+
arm();
|
|
1144
|
+
req.on("data", (c) => {
|
|
1145
|
+
if (aborted) return;
|
|
1146
|
+
arm();
|
|
1147
|
+
size += c.length;
|
|
1148
|
+
if (size > maxBytes) {
|
|
1149
|
+
fail(new PayloadTooLargeError(`request body exceeds ${maxBytes} bytes`));
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
chunks.push(c);
|
|
1153
|
+
});
|
|
1154
|
+
req.on("end", () => {
|
|
1155
|
+
clearTimeout(timer);
|
|
1156
|
+
if (!aborted) resolve(Buffer.concat(chunks));
|
|
1157
|
+
});
|
|
1158
|
+
req.on("error", (e) => {
|
|
1159
|
+
if (!aborted) fail(e);
|
|
1160
|
+
});
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1163
|
+
var STRIPPED = /* @__PURE__ */ new Set(["host", "content-length", "connection", "transfer-encoding"]);
|
|
1164
|
+
function forwardableHeaders(h) {
|
|
1165
|
+
const out = [];
|
|
1166
|
+
for (const [k, v] of Object.entries(h)) {
|
|
1167
|
+
if (v === void 0) continue;
|
|
1168
|
+
if (STRIPPED.has(k.toLowerCase())) continue;
|
|
1169
|
+
out.push([k, Array.isArray(v) ? v.join(", ") : v]);
|
|
1170
|
+
}
|
|
1171
|
+
return out;
|
|
1172
|
+
}
|
|
1173
|
+
function ensureHeader(h, name, value) {
|
|
1174
|
+
const lower = name.toLowerCase();
|
|
1175
|
+
const idx = h.findIndex(([k]) => k.toLowerCase() === lower);
|
|
1176
|
+
if (idx >= 0) h[idx] = [name, value];
|
|
1177
|
+
else h.push([name, value]);
|
|
1178
|
+
}
|
|
1179
|
+
function removeHeader(h, name) {
|
|
1180
|
+
const lower = name.toLowerCase();
|
|
1181
|
+
for (let i = h.length - 1; i >= 0; i--) {
|
|
1182
|
+
if (h[i][0].toLowerCase() === lower) h.splice(i, 1);
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
function headersFromTuple(t) {
|
|
1186
|
+
const out = {};
|
|
1187
|
+
for (const [k, v] of t) out[k] = v;
|
|
1188
|
+
return out;
|
|
1189
|
+
}
|
|
1190
|
+
function explainForwardError(err) {
|
|
1191
|
+
const msg = err?.message ?? String(err);
|
|
1192
|
+
if (err instanceof T3TimeoutError) {
|
|
1193
|
+
return {
|
|
1194
|
+
status: 504,
|
|
1195
|
+
body: JSON.stringify({
|
|
1196
|
+
error: "blindfold_forward_failed",
|
|
1197
|
+
status: 504,
|
|
1198
|
+
code: "t3_timeout",
|
|
1199
|
+
detail: msg,
|
|
1200
|
+
hint: "The T3 node did not respond within the deadline (BLINDFOLD_T3_TIMEOUT_MS). It may be slow or unreachable; retry, or point T3_BASE_URL at a healthy node."
|
|
1201
|
+
}, null, 2)
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
let status = Number(msg.match(/HTTP (\d{3})/)?.[1]) || 502;
|
|
1205
|
+
let code = "";
|
|
1206
|
+
let detail = msg;
|
|
1207
|
+
let requestId = "";
|
|
1208
|
+
const jsonM = msg.match(/\((\{.*\})\)\s*$/);
|
|
1209
|
+
if (jsonM && jsonM[1]) {
|
|
1210
|
+
try {
|
|
1211
|
+
const o = JSON.parse(jsonM[1]);
|
|
1212
|
+
code = o.code ?? "";
|
|
1213
|
+
detail = o.detail ?? detail;
|
|
1214
|
+
requestId = o.request_id ?? "";
|
|
1215
|
+
} catch {
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
let hint = "";
|
|
1219
|
+
if (/egress_denied|authorised_hosts|allowlist/i.test(detail)) {
|
|
1220
|
+
const host = detail.match(/host '([^']+)'/)?.[1];
|
|
1221
|
+
hint = `Egress is not authorized${host ? ` for '${host}'` : ""}. Run: blindfold grant --host ${host ?? "<host>"} \u2014 list every host you use in ONE command (grant replaces the allowlist unless merged).`;
|
|
1222
|
+
if (status < 400) status = 403;
|
|
1223
|
+
} else if (/fuel_per_minute|too_many_requests|rate limit/i.test(detail)) {
|
|
1224
|
+
hint = "Rate limited by the testnet per-minute compute quota (fuel_per_minute). Retry in ~60s and space calls out \u2014 this is not an outage.";
|
|
1225
|
+
status = 429;
|
|
1226
|
+
} else if (/cannot read map|:secrets/i.test(detail)) {
|
|
1227
|
+
hint = "The contract isn't authorized to read your secrets map (common right after publishing a new contract id). Run: blindfold init (re-grants the secrets read ACL).";
|
|
1228
|
+
} else if (/parse_payload|expected value at line/i.test(detail)) {
|
|
1229
|
+
hint = "The T3 host egress parses request bodies as JSON. For form-encoded APIs (Stripe/Twilio), send params in the query string with an empty body.";
|
|
1230
|
+
if (status < 400) status = 400;
|
|
1231
|
+
} else if (/secret .* not found|not found in the secrets map/i.test(detail)) {
|
|
1232
|
+
hint = "That sealed secret name doesn't exist. Seal it: blindfold register --name <name> --from-env <ENV_VAR>.";
|
|
1233
|
+
}
|
|
1234
|
+
const payload = {
|
|
1235
|
+
error: "blindfold_forward_failed",
|
|
1236
|
+
status,
|
|
1237
|
+
code: code || void 0,
|
|
1238
|
+
detail,
|
|
1239
|
+
request_id: requestId || void 0,
|
|
1240
|
+
hint: hint || void 0
|
|
1241
|
+
};
|
|
1242
|
+
return { status, body: JSON.stringify(payload, null, 2) };
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
// src/dashboard.ts
|
|
1246
|
+
init_constants();
|
|
1247
|
+
init_env();
|
|
1248
|
+
import fs6 from "node:fs";
|
|
1249
|
+
import http2 from "node:http";
|
|
1250
|
+
import path5 from "node:path";
|
|
1251
|
+
import { timingSafeEqual } from "node:crypto";
|
|
1252
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
1253
|
+
|
|
1254
|
+
// src/sealed-ledger.ts
|
|
1255
|
+
init_env();
|
|
1256
|
+
import fs5 from "node:fs";
|
|
1257
|
+
import path4 from "node:path";
|
|
1258
|
+
import { createHash as createHash2, createHmac, randomBytes as randomBytes2 } from "node:crypto";
|
|
1259
|
+
function defaultSealedLogPath() {
|
|
1260
|
+
return process.env.BLINDFOLD_SEALED_LOG ?? path4.join(stateDir(), "sealed.jsonl");
|
|
1261
|
+
}
|
|
1262
|
+
function ledgerKey() {
|
|
1263
|
+
try {
|
|
1264
|
+
const keyPath = path4.join(stateDir(), "ledger.key");
|
|
1265
|
+
if (fs5.existsSync(keyPath)) return Buffer.from(fs5.readFileSync(keyPath, "utf8").trim(), "hex");
|
|
1266
|
+
fs5.mkdirSync(path4.dirname(keyPath), { recursive: true });
|
|
1267
|
+
const key = randomBytes2(32);
|
|
1268
|
+
fs5.writeFileSync(keyPath, key.toString("hex"), { mode: 384 });
|
|
1269
|
+
try {
|
|
1270
|
+
fs5.chmodSync(keyPath, 384);
|
|
1271
|
+
} catch {
|
|
1272
|
+
}
|
|
1273
|
+
return key;
|
|
1274
|
+
} catch {
|
|
1275
|
+
return null;
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
function coreString(e) {
|
|
1279
|
+
return JSON.stringify({
|
|
1280
|
+
t: e.t,
|
|
1281
|
+
name: e.name,
|
|
1282
|
+
source: e.source,
|
|
1283
|
+
length: e.length,
|
|
1284
|
+
mode: e.mode,
|
|
1285
|
+
tenant_did: e.tenant_did,
|
|
1286
|
+
map_name: e.map_name
|
|
1287
|
+
});
|
|
1288
|
+
}
|
|
1289
|
+
function sha(s) {
|
|
1290
|
+
return createHash2("sha256").update(s).digest("hex");
|
|
1291
|
+
}
|
|
1292
|
+
function chainHash(key, prev, core) {
|
|
1293
|
+
if (key) return { hash: createHmac("sha256", key).update(`${prev}
|
|
1294
|
+
${core}`).digest("hex"), alg: "hmac-sha256" };
|
|
1295
|
+
return { hash: sha(`${prev}
|
|
1296
|
+
${core}`) };
|
|
1297
|
+
}
|
|
1298
|
+
function readLastLine(p) {
|
|
1299
|
+
if (!fs5.existsSync(p)) return null;
|
|
1300
|
+
const fd = fs5.openSync(p, "r");
|
|
1301
|
+
try {
|
|
1302
|
+
const size = fs5.fstatSync(fd).size;
|
|
1303
|
+
if (size === 0) return null;
|
|
1304
|
+
const readLen = Math.min(size, 64 * 1024);
|
|
1305
|
+
const buf = Buffer.alloc(readLen);
|
|
1306
|
+
fs5.readSync(fd, buf, 0, readLen, size - readLen);
|
|
1307
|
+
const lines = buf.toString("utf8").split("\n").filter((l) => l.trim().length > 0);
|
|
1308
|
+
return lines.length ? lines[lines.length - 1] : null;
|
|
1309
|
+
} finally {
|
|
1310
|
+
fs5.closeSync(fd);
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
function recordSealed(entry) {
|
|
1314
|
+
const p = defaultSealedLogPath();
|
|
1315
|
+
try {
|
|
1316
|
+
withFileLockSync(p, () => {
|
|
1317
|
+
let prevHash = "";
|
|
1318
|
+
const last = readLastLine(p);
|
|
1319
|
+
if (last) {
|
|
1320
|
+
try {
|
|
1321
|
+
prevHash = JSON.parse(last).hash ?? "";
|
|
1322
|
+
} catch {
|
|
1323
|
+
prevHash = "";
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
const { hash, alg } = chainHash(ledgerKey(), prevHash, coreString(entry));
|
|
1327
|
+
const chained = { ...entry, prev: prevHash, hash, ...alg ? { alg } : {} };
|
|
1328
|
+
fs5.mkdirSync(path4.dirname(p), { recursive: true });
|
|
1329
|
+
fs5.appendFileSync(p, JSON.stringify(chained) + "\n");
|
|
1330
|
+
});
|
|
1331
|
+
} catch {
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
function readSealed() {
|
|
1335
|
+
const p = defaultSealedLogPath();
|
|
1336
|
+
if (!fs5.existsSync(p)) return [];
|
|
1337
|
+
return fs5.readFileSync(p, "utf8").split("\n").filter(Boolean).map((l) => {
|
|
1338
|
+
try {
|
|
1339
|
+
return JSON.parse(l);
|
|
1340
|
+
} catch {
|
|
1341
|
+
return null;
|
|
1342
|
+
}
|
|
1343
|
+
}).filter((e) => e !== null);
|
|
1344
|
+
}
|
|
1345
|
+
function verifyLedgerChain() {
|
|
1346
|
+
const entries = readSealed();
|
|
1347
|
+
const key = ledgerKey();
|
|
1348
|
+
let runningPrev = "";
|
|
1349
|
+
let legacy = 0;
|
|
1350
|
+
let firstBrokenIndex = -1;
|
|
1351
|
+
entries.forEach((e, i) => {
|
|
1352
|
+
if (!e.hash) {
|
|
1353
|
+
legacy++;
|
|
1354
|
+
return;
|
|
1355
|
+
}
|
|
1356
|
+
if (e.alg === "hmac-sha256") {
|
|
1357
|
+
const expected = key ? createHmac("sha256", key).update(`${runningPrev}
|
|
1358
|
+
${coreString(e)}`).digest("hex") : null;
|
|
1359
|
+
if (firstBrokenIndex < 0 && (e.prev !== runningPrev || expected !== null && e.hash !== expected)) {
|
|
1360
|
+
firstBrokenIndex = i;
|
|
1361
|
+
}
|
|
1362
|
+
if (expected === null) legacy++;
|
|
1363
|
+
} else {
|
|
1364
|
+
legacy++;
|
|
1365
|
+
}
|
|
1366
|
+
runningPrev = e.hash;
|
|
1367
|
+
});
|
|
1368
|
+
return { ok: firstBrokenIndex < 0, total: entries.length, legacy, firstBrokenIndex };
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
// src/dashboard.ts
|
|
1372
|
+
async function startDashboard(opts = {}) {
|
|
1373
|
+
const port = opts.port ?? DEFAULT_DASHBOARD_PORT;
|
|
1374
|
+
const TOKEN = process.env.BLINDFOLD_DASHBOARD_TOKEN || "";
|
|
1375
|
+
const tokenEq = (candidate) => {
|
|
1376
|
+
const a = Buffer.from(candidate);
|
|
1377
|
+
const b = Buffer.from(TOKEN);
|
|
1378
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
1379
|
+
};
|
|
1380
|
+
const authed = (req, url) => {
|
|
1381
|
+
if (!TOKEN) return true;
|
|
1382
|
+
const auth = req.headers.authorization || "";
|
|
1383
|
+
if (auth.startsWith("Bearer ") && tokenEq(auth.slice(7))) return true;
|
|
1384
|
+
try {
|
|
1385
|
+
const t = new URL(url, "http://x").searchParams.get("token");
|
|
1386
|
+
if (t && tokenEq(t)) return true;
|
|
1387
|
+
} catch {
|
|
1388
|
+
}
|
|
1389
|
+
return false;
|
|
1390
|
+
};
|
|
1391
|
+
const server = http2.createServer(async (req, res) => {
|
|
1392
|
+
const url = req.url ?? "/";
|
|
1393
|
+
const pathname = url.split("?")[0];
|
|
1394
|
+
if (!authed(req, url)) {
|
|
1395
|
+
res.writeHead(401, { "content-type": "text/plain" });
|
|
1396
|
+
res.end("unauthorized \u2014 append ?token=<BLINDFOLD_DASHBOARD_TOKEN>");
|
|
1397
|
+
return;
|
|
1398
|
+
}
|
|
1399
|
+
if (req.method === "GET" && pathname === "/logo.png") {
|
|
1400
|
+
try {
|
|
1401
|
+
const HERE2 = path5.dirname(fileURLToPath2(import.meta.url));
|
|
1402
|
+
const candidates = [
|
|
1403
|
+
path5.resolve(HERE2, "..", "..", "..", "assets", "logo.png"),
|
|
1404
|
+
path5.resolve(HERE2, "..", "assets", "logo.png")
|
|
1405
|
+
];
|
|
1406
|
+
const logo = candidates.find((p) => fs6.existsSync(p));
|
|
1407
|
+
if (logo) {
|
|
1408
|
+
res.writeHead(200, { "content-type": "image/png", "cache-control": "max-age=3600" });
|
|
1409
|
+
res.end(fs6.readFileSync(logo));
|
|
1410
|
+
} else {
|
|
1411
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#8b5cf6"/><stop offset="1" stop-color="#ff8c2b"/></linearGradient></defs><rect width="100" height="100" rx="22" fill="#161b22"/><path d="M50 14l28 10v22c0 20-13 33-28 40-15-7-28-20-28-40V24z" fill="url(#g)" opacity="0.92"/><rect x="34" y="46" width="32" height="24" rx="5" fill="#0b0e14"/><rect x="42" y="36" width="16" height="16" rx="8" fill="none" stroke="#0b0e14" stroke-width="5"/></svg>`;
|
|
1412
|
+
res.writeHead(200, { "content-type": "image/svg+xml", "cache-control": "max-age=3600" });
|
|
1413
|
+
res.end(svg);
|
|
1414
|
+
}
|
|
1415
|
+
} catch {
|
|
1416
|
+
res.writeHead(404, { "content-type": "text/plain" }).end("logo not found");
|
|
1417
|
+
}
|
|
1418
|
+
return;
|
|
1419
|
+
}
|
|
1420
|
+
if (req.method === "GET" && (pathname === "/" || pathname === "/index.html")) {
|
|
1421
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
1422
|
+
res.end(HTML);
|
|
1423
|
+
return;
|
|
1424
|
+
}
|
|
1425
|
+
if (req.method === "GET" && pathname === "/api/events") {
|
|
1426
|
+
const limit = Math.min(Number(new URL(url, "http://x").searchParams.get("limit")) || 500, 5e3);
|
|
1427
|
+
const events = readUsageTail(limit);
|
|
1428
|
+
const counters = {};
|
|
1429
|
+
for (const e of events) counters[e.provider] = (counters[e.provider] ?? 0) + 1;
|
|
1430
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
1431
|
+
res.end(JSON.stringify({ events, counters, returned: events.length, source: defaultLogPath() }));
|
|
1432
|
+
return;
|
|
1433
|
+
}
|
|
1434
|
+
if (req.method === "GET" && pathname === "/api/sealed") {
|
|
1435
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
1436
|
+
res.end(JSON.stringify({ entries: readSealed(), source: defaultSealedLogPath() }));
|
|
1437
|
+
return;
|
|
1438
|
+
}
|
|
1439
|
+
if (req.method === "GET" && pathname === "/api/stream") {
|
|
1440
|
+
res.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-cache", connection: "keep-alive" });
|
|
1441
|
+
res.write(": connected\n\n");
|
|
1442
|
+
const send = () => {
|
|
1443
|
+
try {
|
|
1444
|
+
res.write("event: change\ndata: {}\n\n");
|
|
1445
|
+
} catch {
|
|
1446
|
+
}
|
|
1447
|
+
};
|
|
1448
|
+
const watchers = [];
|
|
1449
|
+
for (const p of [defaultLogPath(), defaultSealedLogPath()]) {
|
|
1450
|
+
try {
|
|
1451
|
+
watchers.push(fs6.watch(path5.dirname(p), () => send()));
|
|
1452
|
+
} catch {
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
const hb = setInterval(() => {
|
|
1456
|
+
try {
|
|
1457
|
+
res.write(": ping\n\n");
|
|
1458
|
+
} catch {
|
|
1459
|
+
}
|
|
1460
|
+
}, 15e3);
|
|
1461
|
+
req.on("close", () => {
|
|
1462
|
+
clearInterval(hb);
|
|
1463
|
+
for (const w of watchers) {
|
|
1464
|
+
try {
|
|
1465
|
+
w.close();
|
|
1466
|
+
} catch {
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
});
|
|
1470
|
+
return;
|
|
1471
|
+
}
|
|
1472
|
+
if (req.method === "GET" && pathname === "/api/audit/full") {
|
|
1473
|
+
const env = loadBlindfoldEnv();
|
|
1474
|
+
const sealed = readSealed();
|
|
1475
|
+
const latest = /* @__PURE__ */ new Map();
|
|
1476
|
+
for (const s of sealed) latest.set(s.name, s);
|
|
1477
|
+
const results = [];
|
|
1478
|
+
if (!env.mock) {
|
|
1479
|
+
try {
|
|
1480
|
+
const { openT3Client: openT3Client2 } = await Promise.resolve().then(() => (init_t3_client(), t3_client_exports));
|
|
1481
|
+
const client = await openT3Client2(env);
|
|
1482
|
+
try {
|
|
1483
|
+
for (const e of latest.values()) {
|
|
1484
|
+
const v = await client.verifySecret(e.name);
|
|
1485
|
+
results.push({ name: e.name, present: v.present, enclave_len: v.length, ledger_len: e.length, fingerprint: v.fingerprint, ok: v.present && v.length === e.length });
|
|
1486
|
+
}
|
|
1487
|
+
} finally {
|
|
1488
|
+
await client.close();
|
|
1489
|
+
}
|
|
1490
|
+
} catch (err) {
|
|
1491
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
1492
|
+
res.end(JSON.stringify({ mock: env.mock, error: err.message, results: [] }));
|
|
1493
|
+
return;
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
1497
|
+
res.end(JSON.stringify({ mock: env.mock, results }));
|
|
1498
|
+
return;
|
|
1499
|
+
}
|
|
1500
|
+
if (req.method === "GET" && pathname === "/api/status") {
|
|
1501
|
+
const env = loadBlindfoldEnv();
|
|
1502
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
1503
|
+
res.end(JSON.stringify({
|
|
1504
|
+
mode: env.mock ? "mock" : "real",
|
|
1505
|
+
tenant_did: env.did || null,
|
|
1506
|
+
tenant_did_short: env.did ? shortenDid(env.did) : null,
|
|
1507
|
+
t3_env: env.t3Env,
|
|
1508
|
+
contract_version: CONTRACT_VERSION,
|
|
1509
|
+
proxy_port: env.port,
|
|
1510
|
+
sdk_installed: isSdkInstalled()
|
|
1511
|
+
}));
|
|
1512
|
+
return;
|
|
1513
|
+
}
|
|
1514
|
+
if (req.method === "GET" && pathname === "/api/audit") {
|
|
1515
|
+
const sealed = readSealed();
|
|
1516
|
+
const envKeys = new Set(readEnvKeyNames());
|
|
1517
|
+
const latestByName = /* @__PURE__ */ new Map();
|
|
1518
|
+
for (const s of sealed) latestByName.set(s.name, s);
|
|
1519
|
+
const exposed = Array.from(latestByName.values()).filter((latest) => envKeys.has(latest.name)).map((latest) => ({ name: latest.name, length: latest.length, sealed_at: latest.t }));
|
|
1520
|
+
const uniqueSealed = latestByName.size;
|
|
1521
|
+
const chain = verifyLedgerChain();
|
|
1522
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
1523
|
+
res.end(JSON.stringify({
|
|
1524
|
+
exposed_in_env: exposed,
|
|
1525
|
+
env_keys: Array.from(envKeys),
|
|
1526
|
+
sealed_count: uniqueSealed,
|
|
1527
|
+
ledger_chain: chain
|
|
1528
|
+
// { ok, total, legacy, firstBrokenIndex }
|
|
1529
|
+
}));
|
|
1530
|
+
return;
|
|
1531
|
+
}
|
|
1532
|
+
if ((req.method === "POST" || req.method === "DELETE") && pathname === "/api/clear") {
|
|
1533
|
+
clearUsage();
|
|
1534
|
+
res.writeHead(204).end();
|
|
1535
|
+
return;
|
|
1536
|
+
}
|
|
1537
|
+
res.writeHead(404, { "content-type": "text/plain" });
|
|
1538
|
+
res.end("not found");
|
|
1539
|
+
});
|
|
1540
|
+
return await new Promise((resolve, reject) => {
|
|
1541
|
+
server.once("error", reject);
|
|
1542
|
+
server.listen(port, "127.0.0.1", () => {
|
|
1543
|
+
const { port: bound } = server.address();
|
|
1544
|
+
resolve({
|
|
1545
|
+
url: `http://127.0.0.1:${bound}`,
|
|
1546
|
+
port: bound,
|
|
1547
|
+
close: () => new Promise((r) => server.close(() => r()))
|
|
1548
|
+
});
|
|
1549
|
+
});
|
|
1550
|
+
});
|
|
1551
|
+
}
|
|
1552
|
+
function shortenDid(did) {
|
|
1553
|
+
const prefix = "did:t3n:";
|
|
1554
|
+
if (!did.startsWith(prefix)) return did;
|
|
1555
|
+
const tail = did.slice(prefix.length);
|
|
1556
|
+
return tail.length <= 14 ? did : `${prefix}${tail.slice(0, 6)}\u2026${tail.slice(-4)}`;
|
|
1557
|
+
}
|
|
1558
|
+
function readEnvKeyNames() {
|
|
1559
|
+
const ENV_PATH = defaultEnvPath();
|
|
1560
|
+
if (!fs6.existsSync(ENV_PATH)) return [];
|
|
1561
|
+
return fs6.readFileSync(ENV_PATH, "utf8").split(/\r?\n/).map((line) => line.trim()).filter((line) => line && !line.startsWith("#") && line.includes("=")).map((line) => line.split("=", 1)[0]?.trim() ?? "").filter(Boolean);
|
|
1562
|
+
}
|
|
1563
|
+
function isSdkInstalled() {
|
|
1564
|
+
try {
|
|
1565
|
+
__require.resolve?.("@terminal3/t3n-sdk");
|
|
1566
|
+
return true;
|
|
1567
|
+
} catch {
|
|
1568
|
+
try {
|
|
1569
|
+
const HERE2 = path5.dirname(fileURLToPath2(import.meta.url));
|
|
1570
|
+
return fs6.existsSync(path5.resolve(HERE2, "..", "..", "..", "node_modules", "@terminal3", "t3n-sdk"));
|
|
1571
|
+
} catch {
|
|
1572
|
+
return false;
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
var HTML = `<!DOCTYPE html>
|
|
1577
|
+
<html lang="en" data-theme="dark">
|
|
1578
|
+
<head>
|
|
1579
|
+
<meta charset="utf-8" />
|
|
1580
|
+
<title>Blindfold \u2014 Dashboard</title>
|
|
1581
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
1582
|
+
<link rel="icon" type="image/png" href="/logo.png" />
|
|
1583
|
+
<style>
|
|
1584
|
+
:root {
|
|
1585
|
+
--bg:#0b0e14; --bg2:#0e1117; --card:#161b22; --card2:#1c2230; --line:#2a313c;
|
|
1586
|
+
--fg:#e6edf3; --dim:#8b949e; --dim2:#6b7280;
|
|
1587
|
+
--ok:#3fb950; --warn:#d29922; --bad:#f85149; --info:#58a6ff;
|
|
1588
|
+
--accent:#8b5cf6; --accent2:#58a6ff;
|
|
1589
|
+
--orange:#ff8c2b; --orange2:#ff6a3d; --brand:#ff8c2b;
|
|
1590
|
+
--c1:#8b5cf6; --c2:#58a6ff; --c3:#3fb950; --c4:#ff8c2b; --c5:#f85149; --c6:#ff7b72; --c7:#39c5cf; --c8:#f778ba;
|
|
1591
|
+
--line:#333b47; --line-strong:#465061;
|
|
1592
|
+
--shadow:0 1px 3px rgba(0,0,0,.4), 0 8px 24px rgba(0,0,0,.18);
|
|
1593
|
+
}
|
|
1594
|
+
[data-theme="light"] {
|
|
1595
|
+
--bg:#f6f8fa; --bg2:#fff; --card:#fff; --card2:#f3f4f6; --line:#d8dee4; --line-strong:#c2cad3;
|
|
1596
|
+
--fg:#1f2328; --dim:#57606a; --dim2:#8b949e;
|
|
1597
|
+
--shadow:0 1px 2px rgba(0,0,0,.06), 0 8px 24px rgba(0,0,0,.06);
|
|
1598
|
+
}
|
|
1599
|
+
* { box-sizing:border-box; }
|
|
1600
|
+
html,body { max-width:100vw; overflow-x:hidden; }
|
|
1601
|
+
body {
|
|
1602
|
+
margin:0; padding:20px clamp(12px,4vw,40px);
|
|
1603
|
+
font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","Inter",system-ui,sans-serif;
|
|
1604
|
+
background:radial-gradient(1200px 600px at 20% -10%, rgba(139,92,246,.10), transparent 60%), var(--bg);
|
|
1605
|
+
color:var(--fg); line-height:1.5; transition:background .2s,color .2s;
|
|
1606
|
+
}
|
|
1607
|
+
a { color:var(--accent2); }
|
|
1608
|
+
.topbar { display:flex; justify-content:space-between; align-items:center; gap:14px; flex-wrap:wrap; margin-bottom:18px;
|
|
1609
|
+
border:1px solid var(--line-strong); border-radius:16px; padding:16px 20px;
|
|
1610
|
+
background:linear-gradient(180deg,var(--card),var(--card2)); box-shadow:var(--shadow); }
|
|
1611
|
+
.brandwrap { display:flex; align-items:center; gap:16px; min-width:0; }
|
|
1612
|
+
.logo-img { width:clamp(76px,11vw,104px); height:clamp(76px,11vw,104px); border-radius:18px; object-fit:cover; background:var(--card); border:1px solid var(--line-strong); padding:0; box-shadow:var(--shadow); flex:none; animation:logoIn .6s cubic-bezier(.2,.8,.2,1) both; }
|
|
1613
|
+
@keyframes logoIn { from{transform:scale(.7) rotate(-8deg);opacity:0;} to{transform:scale(1) rotate(0);opacity:1;} }
|
|
1614
|
+
h1 { margin:0; font-size:clamp(24px,4.5vw,34px); font-weight:800; letter-spacing:-.5px; display:flex; align-items:baseline; gap:10px; flex-wrap:wrap; }
|
|
1615
|
+
/* wordmark: ribbon-wrap animation \u2014 pure CSS, no runtime deps */
|
|
1616
|
+
.brand { position:relative; display:inline-block; padding-bottom:6px; overflow:hidden; }
|
|
1617
|
+
.brand::after { content:""; position:absolute; left:0; bottom:0; height:3px; width:100%; background:linear-gradient(90deg,var(--orange),var(--orange2)); border-radius:3px; transform-origin:left; transform:scaleX(0); opacity:0; animation:underlineIn .5s 2.2s cubic-bezier(.2,.8,.2,1) both; }
|
|
1618
|
+
@keyframes underlineIn { from{transform:scaleX(0);opacity:.3;} to{transform:scaleX(1);opacity:1;} }
|
|
1619
|
+
.letter { display:inline-block; position:relative; z-index:1; color:var(--fg); text-shadow:none; animation:protectLetter .35s cubic-bezier(.2,.8,.2,1) both; }
|
|
1620
|
+
@keyframes protectLetter { to{ color:#bdc7d4; text-shadow:0 1px 4px rgba(139,92,246,.12); transform:translateY(-0.5px); } }
|
|
1621
|
+
[data-theme="light"] .letter { animation:protectLetterLight .35s cubic-bezier(.2,.8,.2,1) both; }
|
|
1622
|
+
@keyframes protectLetterLight { to{ color:#5a6478; text-shadow:0 1px 4px rgba(139,92,246,.10); transform:translateY(-0.5px); } }
|
|
1623
|
+
.brand > .letter:nth-of-type(5) { animation-delay:0.90s; } .brand > .letter:nth-of-type(4) { animation-delay:0.94s; }
|
|
1624
|
+
.brand > .letter:nth-of-type(6) { animation-delay:0.98s; } .brand > .letter:nth-of-type(3) { animation-delay:1.02s; }
|
|
1625
|
+
.brand > .letter:nth-of-type(7) { animation-delay:1.06s; } .brand > .letter:nth-of-type(2) { animation-delay:1.10s; }
|
|
1626
|
+
.brand > .letter:nth-of-type(8) { animation-delay:1.14s; } .brand > .letter:nth-of-type(1) { animation-delay:1.18s; }
|
|
1627
|
+
.brand > .letter:nth-of-type(9) { animation-delay:1.22s; }
|
|
1628
|
+
.ribbon { position:absolute; top:-25%; height:150%; width:55%; background:linear-gradient(180deg, rgba(50,54,62,0) 0%, rgba(38,42,50,.6) 12%, rgba(28,30,38,.92) 32%, rgba(22,24,30,1) 48%, rgba(28,30,38,.92) 64%, rgba(38,42,50,.6) 84%, rgba(50,54,62,0) 100%); border-radius:4px; z-index:2; pointer-events:none; will-change:transform; transform-origin:left center; animation:ribbonSweep 1.0s .5s cubic-bezier(.65,0,.35,1) both, ribbonSettle .7s 1.5s cubic-bezier(.34,1.56,.64,1) both, ribbonBreeze 3s 2.2s ease-in-out infinite; }
|
|
1629
|
+
@keyframes ribbonSweep { 0%{ transform:translateX(100%) scaleX(.01); } 100%{ transform:translateX(22%) scaleX(1); } }
|
|
1630
|
+
@keyframes ribbonSettle { 0%{ transform:translateX(22%) scaleX(1); } 100%{ transform:translateX(-6%) scaleX(.65) rotate(-1.5deg); } }
|
|
1631
|
+
@keyframes ribbonBreeze { 0%,100%{ transform:translateX(-6%) scaleX(.65) rotate(-1.5deg); } 50%{ transform:translateX(-4%) scaleX(.68) rotate(.8deg); } }
|
|
1632
|
+
@keyframes logoSeal { 0%{ filter:brightness(1) saturate(1); } 25%{ filter:brightness(1.06) saturate(1.1); box-shadow:0 0 0 1px rgba(139,92,246,.15); } 60%{ filter:brightness(1.03) saturate(1.05); } 100%{ filter:brightness(1) saturate(1); } }
|
|
1633
|
+
.logo-img { animation:logoIn .6s cubic-bezier(.2,.8,.2,1) both, logoSeal 2.2s .6s ease both; }
|
|
1634
|
+
.eyebrow { font-size:.42em; font-weight:600; color:var(--dim); text-transform:uppercase; letter-spacing:1.5px; }
|
|
1635
|
+
.tagline { font-size:12px; color:var(--dim); margin-top:7px; display:flex; align-items:center; gap:6px; }
|
|
1636
|
+
.tagline .lock { color:var(--orange); display:inline-block; animation:clack .5s .45s cubic-bezier(.3,1.4,.5,1) both; }
|
|
1637
|
+
@keyframes clack { from{transform:rotate(-35deg) translateY(-2px);opacity:0;} to{transform:rotate(0) translateY(0);opacity:1;} }
|
|
1638
|
+
/* --- Distinctive "decrypt \u2192 seal" wordmark animation (JS-driven) --- */
|
|
1639
|
+
.letter.scrambling { color:var(--accent); font-family:ui-monospace,SFMono-Regular,Menlo,monospace; opacity:.92; }
|
|
1640
|
+
.letter.sealed { animation:sealSnap .55s cubic-bezier(.2,1.4,.4,1) both; }
|
|
1641
|
+
@keyframes sealSnap {
|
|
1642
|
+
0% { color:var(--accent); text-shadow:0 0 14px rgba(139,92,246,.95),0 0 4px rgba(139,92,246,.8); transform:translateY(-3px) scale(1.18); }
|
|
1643
|
+
55% { color:var(--fg); text-shadow:0 0 8px rgba(139,92,246,.5); transform:translateY(1px) scale(.97); }
|
|
1644
|
+
100% { color:var(--fg); text-shadow:none; transform:none; }
|
|
1645
|
+
}
|
|
1646
|
+
/* seal sweep: a vertical light bar that passes across the word once as it seals */
|
|
1647
|
+
.sealbar { position:absolute; top:-22%; height:144%; width:16px; left:-8%; z-index:3; pointer-events:none; border-radius:9px; opacity:0;
|
|
1648
|
+
background:linear-gradient(90deg,transparent, rgba(139,92,246,.55) 45%, rgba(88,166,255,.35) 60%, transparent); filter:blur(2px); }
|
|
1649
|
+
.sealbar.run { animation:sealSweep 1.15s cubic-bezier(.5,0,.3,1) both; }
|
|
1650
|
+
@keyframes sealSweep { 0%{ left:-8%; opacity:0;} 12%{opacity:1;} 86%{opacity:1;} 100%{ left:104%; opacity:0;} }
|
|
1651
|
+
@media (prefers-reduced-motion:reduce){ .logo-img,.letter,.ribbon,.brand::after,.tagline .lock,.sealbar{ animation:none!important; } .ribbon,.sealbar{ display:none; } .letter{ color:var(--fg)!important; transform:none!important; text-shadow:none!important; } .brand::after{ transform:scaleX(1)!important; opacity:1!important; } }
|
|
1652
|
+
.tagline b { color:var(--fg); font-weight:600; }
|
|
1653
|
+
.tags { display:flex; gap:6px; margin-top:8px; flex-wrap:wrap; align-items:center; }
|
|
1654
|
+
.tag { display:inline-flex; align-items:center; gap:5px; font-size:11px; color:var(--dim); background:var(--card); border:1px solid var(--line); border-radius:20px; padding:3px 10px; max-width:100%; }
|
|
1655
|
+
.tag.live { color:var(--ok); border-color:rgba(63,185,80,.45); background:rgba(63,185,80,.08); font-weight:600; }
|
|
1656
|
+
.tag.orange { color:var(--orange); border-color:rgba(255,140,43,.45); background:rgba(255,140,43,.08); }
|
|
1657
|
+
.tag.path { color:var(--dim); }
|
|
1658
|
+
.tag.path code { background:none; padding:0; max-width:min(52vw,420px); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; display:inline-block; vertical-align:bottom; }
|
|
1659
|
+
.live-dot { width:7px; height:7px; border-radius:50%; background:var(--ok); box-shadow:0 0 0 0 rgba(63,185,80,.6); animation:pulse 2s infinite; }
|
|
1660
|
+
@keyframes pulse { 0%{box-shadow:0 0 0 0 rgba(63,185,80,.5);} 70%{box-shadow:0 0 0 6px rgba(63,185,80,0);} 100%{box-shadow:0 0 0 0 rgba(63,185,80,0);} }
|
|
1661
|
+
.controls { display:flex; gap:8px; align-items:center; flex-wrap:wrap; }
|
|
1662
|
+
.controls label { font-size:12px; color:var(--dim); display:flex; align-items:center; gap:4px; }
|
|
1663
|
+
select,button { background:var(--card); color:var(--fg); border:1px solid var(--line-strong); border-radius:8px; padding:6px 10px; font-size:12px; cursor:pointer; transition:border-color .15s,background .15s,transform .05s; }
|
|
1664
|
+
button:hover,select:hover { border-color:var(--orange); color:var(--orange); }
|
|
1665
|
+
button:active { transform:translateY(1px); }
|
|
1666
|
+
.copybtn { background:transparent; border:1px solid var(--line); border-radius:6px; padding:2px 9px; font-size:11px; cursor:pointer; color:var(--dim); transition:border-color .15s,color .15s; }
|
|
1667
|
+
.copybtn:hover { border-color:var(--orange); color:var(--orange); }
|
|
1668
|
+
.copybtn.done { color:var(--ok); border-color:var(--ok); }
|
|
1669
|
+
.copybtn.err { color:var(--bad); border-color:var(--bad); }
|
|
1670
|
+
.copybtn.ref { color:var(--orange); border-color:rgba(255,140,43,.4); }
|
|
1671
|
+
.copybtn.ref:hover { background:rgba(255,140,43,.08); }
|
|
1672
|
+
code.sentinel { color:var(--orange); background:rgba(255,140,43,.10); border:1px solid rgba(255,140,43,.22); font-weight:600; }
|
|
1673
|
+
.cmdname { color:var(--accent2); font-weight:600; }
|
|
1674
|
+
@media (max-width:620px){
|
|
1675
|
+
.topbar { flex-direction:column; align-items:stretch; }
|
|
1676
|
+
.controls { display:grid; grid-template-columns:1fr 1fr; gap:8px; }
|
|
1677
|
+
.controls label { width:100%; }
|
|
1678
|
+
.controls label select { flex:1; width:100%; }
|
|
1679
|
+
.controls > button { width:100%; }
|
|
1680
|
+
}
|
|
1681
|
+
.grid { display:grid; gap:12px; grid-template-columns:repeat(auto-fit,minmax(170px,1fr)); margin-bottom:14px; }
|
|
1682
|
+
.two-col { display:grid; gap:12px; grid-template-columns:1fr 1fr; margin-bottom:4px; }
|
|
1683
|
+
@media (max-width:860px){ .two-col{ grid-template-columns:1fr; } }
|
|
1684
|
+
.card {
|
|
1685
|
+
background:linear-gradient(180deg,var(--card),var(--card2)); border:1px solid var(--line);
|
|
1686
|
+
border-radius:12px; padding:14px 16px; min-width:0; box-shadow:var(--shadow); position:relative; overflow:hidden;
|
|
1687
|
+
transition:transform .18s cubic-bezier(.2,.8,.2,1), border-color .18s, box-shadow .18s;
|
|
1688
|
+
}
|
|
1689
|
+
.card:hover { transform:translateY(-3px); border-color:var(--line-strong); box-shadow:0 6px 22px rgba(0,0,0,.28), 0 0 0 1px rgba(139,92,246,.08); }
|
|
1690
|
+
/* KPI cards get a soft accent glow that brightens on hover */
|
|
1691
|
+
.card.kpi::before { content:""; position:absolute; left:0; top:0; bottom:0; width:3px; background:var(--kpi,var(--accent)); box-shadow:0 0 12px var(--kpi,var(--accent)); opacity:.55; transition:opacity .18s; }
|
|
1692
|
+
.card.kpi:hover::before { opacity:1; }
|
|
1693
|
+
.card .label { font-size:10.5px; color:var(--dim); text-transform:uppercase; letter-spacing:.6px; display:flex; align-items:center; gap:5px; }
|
|
1694
|
+
.card .value { font-size:clamp(20px,3.5vw,26px); font-weight:700; margin-top:5px; overflow-wrap:anywhere; }
|
|
1695
|
+
.card .sub2 { font-size:11.5px; color:var(--dim); margin-top:3px; overflow-wrap:anywhere; }
|
|
1696
|
+
.trend { font-size:11px; font-weight:600; padding:1px 6px; border-radius:20px; margin-left:6px; }
|
|
1697
|
+
.trend.up { color:var(--ok); background:rgba(63,185,80,.12); }
|
|
1698
|
+
.trend.down { color:var(--bad); background:rgba(248,81,73,.12); }
|
|
1699
|
+
.trend.flat { color:var(--dim); background:rgba(139,148,158,.12); }
|
|
1700
|
+
.section-title { font-size:11px; color:var(--dim); text-transform:uppercase; letter-spacing:.6px; margin:22px 0 8px; display:flex; align-items:center; gap:8px; cursor:pointer; user-select:none; }
|
|
1701
|
+
.section-title .chev { transition:transform .15s; font-size:9px; color:var(--dim2); }
|
|
1702
|
+
.section-title.collapsed .chev { transform:rotate(-90deg); }
|
|
1703
|
+
.collapsed + * , .collapsed + * + * { display:none !important; }
|
|
1704
|
+
.scroll { background:var(--card); border:1px solid var(--line); border-radius:12px; overflow-x:auto; -webkit-overflow-scrolling:touch; box-shadow:var(--shadow); }
|
|
1705
|
+
table { width:100%; border-collapse:collapse; min-width:520px; }
|
|
1706
|
+
th,td { text-align:left; padding:9px 12px; border-bottom:1px solid var(--line); font-size:13px; vertical-align:top; }
|
|
1707
|
+
th { color:var(--dim); font-weight:600; text-transform:uppercase; font-size:10.5px; letter-spacing:.5px; white-space:nowrap; cursor:pointer; }
|
|
1708
|
+
th:hover { color:var(--fg); }
|
|
1709
|
+
tr:last-child td { border-bottom:none; }
|
|
1710
|
+
tbody tr:hover { background:rgba(139,92,246,.05); }
|
|
1711
|
+
code { background:rgba(127,127,127,.12); padding:2px 5px; border-radius:4px; font-size:12px; overflow-wrap:anywhere; }
|
|
1712
|
+
.pill { display:inline-block; padding:2px 8px; border-radius:20px; font-size:11px; font-weight:600; }
|
|
1713
|
+
.pill-ok{background:rgba(63,185,80,.15);color:var(--ok);} .pill-bad{background:rgba(248,81,73,.15);color:var(--bad);}
|
|
1714
|
+
.pill-warn{background:rgba(210,153,34,.15);color:var(--warn);} .pill-info{background:rgba(88,166,255,.15);color:var(--info);}
|
|
1715
|
+
.pill-mock{background:rgba(139,92,246,.15);color:var(--accent);} .pill-real{background:rgba(63,185,80,.15);color:var(--ok);}
|
|
1716
|
+
.pill-dim{background:rgba(127,127,127,.12);color:var(--dim);}
|
|
1717
|
+
.empty { background:var(--card); border:1px dashed var(--line); border-radius:12px; padding:22px; text-align:center; color:var(--dim); }
|
|
1718
|
+
.alert { border-radius:12px; padding:12px 16px; margin-bottom:14px; border:1px solid; box-shadow:var(--shadow); }
|
|
1719
|
+
.alert .head { font-weight:700; margin-bottom:4px; }
|
|
1720
|
+
.alert.ok { background:rgba(63,185,80,.07); border-color:rgba(63,185,80,.3); color:var(--ok); }
|
|
1721
|
+
.footer { margin-top:24px; padding:14px 16px; background:var(--card); border:1px solid var(--line); border-radius:12px; font-size:12px; color:var(--dim); }
|
|
1722
|
+
.footer b { color:var(--fg); }
|
|
1723
|
+
/* donut + legend */
|
|
1724
|
+
.donut-wrap { display:flex; align-items:center; gap:18px; flex-wrap:wrap; }
|
|
1725
|
+
.legend { display:flex; flex-direction:column; gap:5px; font-size:12px; flex:1; min-width:120px; }
|
|
1726
|
+
.legend .li { display:flex; align-items:center; gap:7px; cursor:default; }
|
|
1727
|
+
.legend .dot { width:10px; height:10px; border-radius:3px; flex:none; }
|
|
1728
|
+
.legend .li .n { margin-left:auto; color:var(--dim); }
|
|
1729
|
+
/* segmented bar */
|
|
1730
|
+
.segbar { display:flex; height:22px; border-radius:7px; overflow:hidden; background:rgba(127,127,127,.1); }
|
|
1731
|
+
.segbar > div { transition:width .3s; }
|
|
1732
|
+
/* horizontal bars */
|
|
1733
|
+
.bar-row { display:flex; align-items:center; gap:8px; margin:7px 0; font-size:12px; }
|
|
1734
|
+
.bar-row .name { width:120px; color:var(--dim); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
1735
|
+
.bar-row .track { flex:1; background:rgba(127,127,127,.1); border-radius:5px; height:16px; overflow:hidden; }
|
|
1736
|
+
.bar-row .fill { height:100%; border-radius:5px; transition:width .3s; }
|
|
1737
|
+
.bar-row .n { width:58px; text-align:right; }
|
|
1738
|
+
/* sparkline */
|
|
1739
|
+
.spark { display:flex; align-items:flex-end; gap:1px; height:60px; background:var(--card); border:1px solid var(--line); border-radius:12px; padding:8px; box-shadow:var(--shadow); }
|
|
1740
|
+
.spark > .b { background:linear-gradient(180deg,var(--c2),var(--c1)); flex:1 1 0; min-width:1px; border-radius:2px 2px 0 0; transition:height .2s; }
|
|
1741
|
+
.spark > .b.zero { background:var(--line); min-height:1px; }
|
|
1742
|
+
.stat-row { display:flex; gap:14px; flex-wrap:wrap; font-size:12px; color:var(--dim); margin-top:7px; }
|
|
1743
|
+
.stat-row span { color:var(--fg); font-weight:600; }
|
|
1744
|
+
.filter { background:var(--card); color:var(--fg); border:1px solid var(--line); border-radius:8px; padding:5px 9px; font-size:12px; margin-left:auto; min-width:180px; }
|
|
1745
|
+
#tooltip { position:fixed; pointer-events:none; background:var(--bg2); border:1px solid var(--line); border-radius:8px; padding:6px 9px; font-size:12px; box-shadow:var(--shadow); opacity:0; transition:opacity .1s; z-index:50; white-space:nowrap; }
|
|
1746
|
+
@media (max-width:560px){ body{padding:12px;} .grid{grid-template-columns:1fr 1fr;gap:8px;} th,td{padding:7px 9px;font-size:12px;} .filter{min-width:120px;} }
|
|
1747
|
+
</style>
|
|
1748
|
+
</head>
|
|
1749
|
+
<body>
|
|
1750
|
+
<div class="topbar">
|
|
1751
|
+
<div class="brandwrap">
|
|
1752
|
+
<img id="logo" class="logo-img" alt="Blindfold" />
|
|
1753
|
+
<div>
|
|
1754
|
+
<h1><span class="brand"><span class="letter">B</span><span class="letter">l</span><span class="letter">i</span><span class="letter">n</span><span class="letter">d</span><span class="letter">f</span><span class="letter">o</span><span class="letter">l</span><span class="letter">d</span><div class="ribbon" id="brand-ribbon"></div><div class="sealbar" id="brand-sealbar"></div></span><span class="eyebrow">Dashboard</span></h1>
|
|
1755
|
+
<div class="tagline"><span class="lock">\u{1F512}</span> Secrets sealed in a <b>TEE</b> \u2014 keys you can't leak</div>
|
|
1756
|
+
<div class="tags" id="tags"></div>
|
|
1757
|
+
</div>
|
|
1758
|
+
</div>
|
|
1759
|
+
<div class="controls">
|
|
1760
|
+
<label>range
|
|
1761
|
+
<select id="range" onchange="setRange()">
|
|
1762
|
+
<option value="15">15m</option><option value="60" selected>1h</option>
|
|
1763
|
+
<option value="1440">24h</option><option value="0">all</option>
|
|
1764
|
+
</select>
|
|
1765
|
+
</label>
|
|
1766
|
+
<label>refresh
|
|
1767
|
+
<select id="refresh" onchange="setRefresh()">
|
|
1768
|
+
<option value="2000">2s</option><option value="5000">5s</option>
|
|
1769
|
+
<option value="10000">10s</option><option value="0">paused</option>
|
|
1770
|
+
</select>
|
|
1771
|
+
</label>
|
|
1772
|
+
<button onclick="toggleTheme()" id="themeBtn" title="Toggle theme">\u{1F319}</button>
|
|
1773
|
+
<button onclick="exportJson()">\u2B07 Export</button>
|
|
1774
|
+
<button onclick="clearLog()">Clear</button>
|
|
1775
|
+
</div>
|
|
1776
|
+
</div>
|
|
1777
|
+
|
|
1778
|
+
<div id="alert-banner"></div>
|
|
1779
|
+
|
|
1780
|
+
<div class="grid" id="kpis"></div>
|
|
1781
|
+
|
|
1782
|
+
<div class="section-title" onclick="toggleSection(this)"><span class="chev">\u25BC</span>Security posture
|
|
1783
|
+
<button style="margin-left:auto" onclick="event.stopPropagation();runFullAudit()">\u{1F50E} Run full audit (live)</button>
|
|
1784
|
+
</div>
|
|
1785
|
+
<div class="grid" id="posture-cards"></div>
|
|
1786
|
+
<div id="full-audit"></div>
|
|
1787
|
+
|
|
1788
|
+
<div class="two-col">
|
|
1789
|
+
<div>
|
|
1790
|
+
<div class="section-title" onclick="toggleSection(this)"><span class="chev">\u25BC</span>Providers</div>
|
|
1791
|
+
<div class="card" id="provider-chart"></div>
|
|
1792
|
+
</div>
|
|
1793
|
+
<div>
|
|
1794
|
+
<div class="section-title" onclick="toggleSection(this)"><span class="chev">\u25BC</span>Status codes</div>
|
|
1795
|
+
<div class="card" id="status-chart"></div>
|
|
1796
|
+
</div>
|
|
1797
|
+
</div>
|
|
1798
|
+
|
|
1799
|
+
<div class="two-col">
|
|
1800
|
+
<div>
|
|
1801
|
+
<div class="section-title" onclick="toggleSection(this)"><span class="chev">\u25BC</span>p95 latency trend</div>
|
|
1802
|
+
<div class="card" id="trend-latency"></div>
|
|
1803
|
+
</div>
|
|
1804
|
+
<div>
|
|
1805
|
+
<div class="section-title" onclick="toggleSection(this)"><span class="chev">\u25BC</span>Success rate trend</div>
|
|
1806
|
+
<div class="card" id="trend-success"></div>
|
|
1807
|
+
</div>
|
|
1808
|
+
</div>
|
|
1809
|
+
|
|
1810
|
+
<div class="section-title" onclick="toggleSection(this)"><span class="chev">\u25BC</span>Requests per minute</div>
|
|
1811
|
+
<div class="spark" id="spark"></div>
|
|
1812
|
+
<div class="stat-row" id="spark-stats"></div>
|
|
1813
|
+
|
|
1814
|
+
<div class="section-title" onclick="toggleSection(this)"><span class="chev">\u25BC</span>System</div>
|
|
1815
|
+
<div class="grid" id="status-cards"></div>
|
|
1816
|
+
|
|
1817
|
+
<div class="section-title" onclick="toggleSection(this)"><span class="chev">\u25BC</span>Sealed keys (metadata only)</div>
|
|
1818
|
+
<div id="sealed-table-wrap"></div>
|
|
1819
|
+
|
|
1820
|
+
<div class="section-title" onclick="toggleSection(this)"><span class="chev">\u25BC</span>Per-secret usage</div>
|
|
1821
|
+
<div id="per-secret-wrap"></div>
|
|
1822
|
+
|
|
1823
|
+
<div class="section-title" onclick="toggleSection(this)"><span class="chev">\u25BC</span>Blindfold commands</div>
|
|
1824
|
+
<div id="commands-wrap"></div>
|
|
1825
|
+
|
|
1826
|
+
<div class="section-title" onclick="toggleSection(this)"><span class="chev">\u25BC</span>Recent activity
|
|
1827
|
+
<input id="filter" class="filter" placeholder="filter\u2026" oninput="renderTable(rangeFiltered())" onclick="event.stopPropagation()" />
|
|
1828
|
+
</div>
|
|
1829
|
+
<div id="table-wrap"></div>
|
|
1830
|
+
|
|
1831
|
+
<div class="footer"><b>Privacy by design.</b> Metadata only \u2014 no request/response bodies, no header values, no secret values. See <code>usage-log.ts</code> + <code>sealed-ledger.ts</code>.</div>
|
|
1832
|
+
<div id="tooltip"></div>
|
|
1833
|
+
|
|
1834
|
+
<script>
|
|
1835
|
+
/* Distinctive wordmark: each letter scrambles through cipher glyphs, then
|
|
1836
|
+
snap-seals into place \u2014 mirroring how a real key becomes __BLINDFOLD__.
|
|
1837
|
+
Purely decorative; respects prefers-reduced-motion. */
|
|
1838
|
+
(function sealWordmark(){
|
|
1839
|
+
try {
|
|
1840
|
+
if (window.matchMedia && window.matchMedia('(prefers-reduced-motion:reduce)').matches) return;
|
|
1841
|
+
var letters = Array.prototype.slice.call(document.querySelectorAll('.brand > .letter'));
|
|
1842
|
+
if (!letters.length) return;
|
|
1843
|
+
var GLYPHS = '\u2593\u2588\u2592\u2591#@$%&*/\\<>=+\xB7:';
|
|
1844
|
+
var finals = letters.map(function(el){ return el.textContent; });
|
|
1845
|
+
letters.forEach(function(el){ el.classList.add('scrambling'); });
|
|
1846
|
+
var bar = document.getElementById('brand-sealbar');
|
|
1847
|
+
if (bar) { bar.classList.add('run'); }
|
|
1848
|
+
var start = performance.now();
|
|
1849
|
+
var settleAt = letters.map(function(_, i){ return 260 + i * 95; }); // staggered lock, left\u2192right
|
|
1850
|
+
function frame(now){
|
|
1851
|
+
var t = now - start, done = 0;
|
|
1852
|
+
letters.forEach(function(el, i){
|
|
1853
|
+
if (el.dataset.sealed) { done++; return; }
|
|
1854
|
+
if (t >= settleAt[i]) {
|
|
1855
|
+
el.textContent = finals[i];
|
|
1856
|
+
el.classList.remove('scrambling');
|
|
1857
|
+
el.classList.add('sealed');
|
|
1858
|
+
el.dataset.sealed = '1';
|
|
1859
|
+
done++;
|
|
1860
|
+
} else if (t % 60 < 20) {
|
|
1861
|
+
el.textContent = GLYPHS[(Math.random() * GLYPHS.length) | 0];
|
|
1862
|
+
}
|
|
1863
|
+
});
|
|
1864
|
+
if (done < letters.length) requestAnimationFrame(frame);
|
|
1865
|
+
}
|
|
1866
|
+
requestAnimationFrame(frame);
|
|
1867
|
+
} catch (e) { /* animation must never break the dashboard */ }
|
|
1868
|
+
})();
|
|
1869
|
+
var PALETTE=['#8b5cf6','#58a6ff','#3fb950','#d29922','#f85149','#ff7b72','#39c5cf','#f778ba'];
|
|
1870
|
+
function hashIdx(s){var h=0;for(var i=0;i<s.length;i++)h=(h*31+s.charCodeAt(i))>>>0;return h%PALETTE.length;}
|
|
1871
|
+
function colorFor(name){return PALETTE[hashIdx(String(name||'?'))];}
|
|
1872
|
+
var TOK=new URLSearchParams(location.search).get('token');
|
|
1873
|
+
function api(p){return TOK?p+(p.indexOf('?')>=0?'&':'?')+'token='+encodeURIComponent(TOK):p;}
|
|
1874
|
+
function esc(s){return String(s).replace(/[&<>"']/g,function(c){return({'&':'&','<':'<','>':'>','"':'"',"'":'''})[c];});}
|
|
1875
|
+
function timeAgo(iso){var ms=Date.now()-new Date(iso).getTime();if(ms<1000)return'just now';var s=(ms/1000)|0;if(s<60)return s+'s ago';var m=(s/60)|0;if(m<60)return m+'m ago';var h=(m/60)|0;if(h<24)return h+'h ago';return((h/24)|0)+'d ago';}
|
|
1876
|
+
function pillStatus(s){if(s>=200&&s<300)return'<span class="pill pill-ok">'+s+'</span>';if(s>=400)return'<span class="pill pill-bad">'+s+'</span>';return'<span class="pill pill-warn">'+s+'</span>';}
|
|
1877
|
+
function pillMode(m){return'<span class="pill pill-'+m+'">'+m+'</span>';}
|
|
1878
|
+
function pct(arr,p){if(!arr.length)return 0;var s=arr.slice().sort(function(a,b){return a-b;});return s[Math.min(s.length-1,Math.floor(p/100*s.length))];}
|
|
1879
|
+
|
|
1880
|
+
// shared tooltip
|
|
1881
|
+
var TIP=null;
|
|
1882
|
+
function tip(ev,html){if(!TIP)TIP=document.getElementById('tooltip');TIP.innerHTML=html;TIP.style.opacity='1';TIP.style.left=(ev.clientX+12)+'px';TIP.style.top=(ev.clientY+12)+'px';}
|
|
1883
|
+
function tipHide(){if(TIP)TIP.style.opacity='0';}
|
|
1884
|
+
|
|
1885
|
+
// state
|
|
1886
|
+
window._events=[]; window._sort={key:'t',dir:-1};
|
|
1887
|
+
function rangeMin(){return Number(document.getElementById('range').value);}
|
|
1888
|
+
function rangeFiltered(){var m=rangeMin();if(!m)return window._events;var cut=Date.now()-m*60000;return window._events.filter(function(e){return new Date(e.t).getTime()>=cut;});}
|
|
1889
|
+
|
|
1890
|
+
function toggleTheme(){var h=document.documentElement;var d=h.getAttribute('data-theme')==='dark';h.setAttribute('data-theme',d?'light':'dark');document.getElementById('themeBtn').textContent=d?'\u2600':'\u{1F319}';try{localStorage.setItem('bf-theme',d?'light':'dark');}catch(e){}}
|
|
1891
|
+
(function(){try{var t=localStorage.getItem('bf-theme');if(t){document.documentElement.setAttribute('data-theme',t);document.getElementById('themeBtn').textContent=t==='light'?'\u2600':'\u{1F319}';}}catch(e){}})();
|
|
1892
|
+
function toggleSection(el){el.classList.toggle('collapsed');}
|
|
1893
|
+
function setRange(){poll();}
|
|
1894
|
+
function setRefresh(){var v=Number(document.getElementById('refresh').value);if(window._timer)clearInterval(window._timer);if(v>0)window._timer=setInterval(poll,v);}
|
|
1895
|
+
function exportJson(){var b=new Blob([JSON.stringify(window._events||[],null,2)],{type:'application/json'});var a=document.createElement('a');a.href=URL.createObjectURL(b);a.download='blindfold-usage-'+new Date().toISOString().slice(0,19).replace(/[:T]/g,'-')+'.json';a.click();}
|
|
1896
|
+
async function clearLog(){await fetch(api('/api/clear'),{method:'POST'});poll();}
|
|
1897
|
+
|
|
1898
|
+
async function poll(){
|
|
1899
|
+
try{
|
|
1900
|
+
var r=await Promise.all([fetch(api('/api/status')).then(function(x){return x.json();}),fetch(api('/api/sealed')).then(function(x){return x.json();}),fetch(api('/api/events')).then(function(x){return x.json();}),fetch(api('/api/audit')).then(function(x){return x.json();})]);
|
|
1901
|
+
var statusR=r[0],sealedR=r[1],eventsR=r[2],auditR=r[3];
|
|
1902
|
+
window._events=eventsR.events||[];
|
|
1903
|
+
var ev=rangeFiltered();
|
|
1904
|
+
document.getElementById('tags').innerHTML=
|
|
1905
|
+
'<span class="tag live"><span class="live-dot"></span>live</span>'
|
|
1906
|
+
+'<span class="tag orange">'+new Date().toLocaleTimeString()+'</span>'
|
|
1907
|
+
+'<span class="tag">'+(statusR.mode==='real'?'REAL \xB7 '+esc(statusR.t3_env||''):'MOCK')+'</span>'
|
|
1908
|
+
+'<span class="tag path">usage <code title="'+esc(eventsR.source)+'">'+esc(eventsR.source)+'</code></span>';
|
|
1909
|
+
renderAlert(auditR,ev);
|
|
1910
|
+
renderKpis(ev,auditR,statusR);
|
|
1911
|
+
renderPosture(auditR,statusR);
|
|
1912
|
+
renderStatus(statusR);
|
|
1913
|
+
renderDonut(ev);
|
|
1914
|
+
renderStatusBar(ev);
|
|
1915
|
+
renderTrends(ev);
|
|
1916
|
+
renderSpark(ev);
|
|
1917
|
+
renderSealed(sealedR.entries||[]);
|
|
1918
|
+
renderPerSecret(ev);
|
|
1919
|
+
renderTable(ev);
|
|
1920
|
+
}catch(e){}
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
function trendBadge(cur,prev){if(prev===0&&cur===0)return'<span class="trend flat">\u2014</span>';if(prev===0)return'<span class="trend up">new</span>';var d=Math.round((cur-prev)/prev*100);if(Math.abs(d)<2)return'<span class="trend flat">0%</span>';return'<span class="trend '+(d>0?'up':'down')+'">'+(d>0?'\u25B2':'\u25BC')+Math.abs(d)+'%</span>';}
|
|
1924
|
+
|
|
1925
|
+
function renderKpis(ev,a,s){
|
|
1926
|
+
var half=ev.slice().sort(function(x,y){return new Date(x.t)-new Date(y.t);});
|
|
1927
|
+
var mid=Math.floor(half.length/2);var prevHalf=half.slice(0,mid),curHalf=half.slice(mid);
|
|
1928
|
+
function ok(arr){return arr.filter(function(e){return e.status>=200&&e.status<300;}).length;}
|
|
1929
|
+
var total=ev.length;
|
|
1930
|
+
var succ=total?Math.round(ok(ev)/total*100):0;
|
|
1931
|
+
var prevSucc=prevHalf.length?Math.round(ok(prevHalf)/prevHalf.length*100):0;
|
|
1932
|
+
var curSucc=curHalf.length?Math.round(ok(curHalf)/curHalf.length*100):0;
|
|
1933
|
+
var p95=pct(ev.map(function(e){return e.latency_ms||0;}),95);
|
|
1934
|
+
var p95p=pct(prevHalf.map(function(e){return e.latency_ms||0;}),95);
|
|
1935
|
+
var p95c=pct(curHalf.map(function(e){return e.latency_ms||0;}),95);
|
|
1936
|
+
var sealed=(a&&a.sealed_count)||0;
|
|
1937
|
+
var posture=postureScore(a,s).score;
|
|
1938
|
+
var cards=[
|
|
1939
|
+
['Requests',total,'',trendBadge(curHalf.length,prevHalf.length),'var(--c1)'],
|
|
1940
|
+
['Success rate',succ+'%',ok(ev)+'/'+total+' 2xx',trendBadge(curSucc,prevSucc),succ>=95?'var(--ok)':succ>=80?'var(--warn)':'var(--bad)'],
|
|
1941
|
+
['p95 latency',p95+' ms',ev.length+' samples',trendBadge(p95c,p95p),'var(--c2)'],
|
|
1942
|
+
['Sealed secrets',sealed,'in the enclave','','var(--c7)'],
|
|
1943
|
+
['Posture',posture+'<span style="font-size:13px;color:var(--dim)">/100</span>','security score','',posture>=90?'var(--ok)':posture>=60?'var(--warn)':'var(--bad)']
|
|
1944
|
+
];
|
|
1945
|
+
document.getElementById('kpis').innerHTML=cards.map(function(c){return '<div class="card kpi" style="--kpi:'+c[4]+'"><div class="label">'+c[0]+'</div><div class="value">'+c[1]+c[3]+'</div><div class="sub2">'+c[2]+'</div></div>';}).join('');
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
function postureScore(a,s){var exposed=((a&&a.exposed_in_env)||[]).length;var chain=(a&&a.ledger_chain)||{ok:true,total:0};var sealed=(a&&a.sealed_count)||0;var score=100;var notes=[];if(exposed>0){score-=30;notes.push(exposed+' key(s) in .env');}if(chain.total>0&&!chain.ok){score-=40;notes.push('ledger TAMPERED');}if(s&&s.mode!=='real'){score-=10;notes.push('MOCK mode');}if(s&&!s.sdk_installed){score-=10;notes.push('SDK missing');}if(sealed===0){score-=10;notes.push('nothing sealed');}return{score:Math.max(0,score),notes:notes};}
|
|
1949
|
+
|
|
1950
|
+
function renderAlert(a,ev){
|
|
1951
|
+
var msgs=[];var chain=(a&&a.ledger_chain)||{};
|
|
1952
|
+
if(chain.total>0&&!chain.ok)msgs.push(['bad','\u{1F534} Ledger TAMPERED \u2014 a sealed-keys line was edited or removed']);
|
|
1953
|
+
if(((a&&a.exposed_in_env)||[]).length>0)msgs.push(['warn','\u{1F7E0} '+a.exposed_in_env.length+' sealed key(s) still in .env \u2014 delete them']);
|
|
1954
|
+
var recent=(ev||[]).filter(function(e){return Date.now()-new Date(e.t).getTime()<300000;});
|
|
1955
|
+
var errs=recent.filter(function(e){return(e.status||0)>=400;}).length;
|
|
1956
|
+
if(recent.length>=5&&errs/recent.length>0.3)msgs.push(['warn','\u{1F7E0} '+Math.round(errs/recent.length*100)+'% errors in the last 5 min ('+errs+'/'+recent.length+')']);
|
|
1957
|
+
var el=document.getElementById('alert-banner');
|
|
1958
|
+
if(!msgs.length){el.innerHTML='';return;}
|
|
1959
|
+
var bad=msgs.some(function(m){return m[0]==='bad';});
|
|
1960
|
+
el.innerHTML='<div class="alert" style="background:'+(bad?'rgba(248,81,73,.08)':'rgba(210,153,34,.08)')+';border-color:'+(bad?'rgba(248,81,73,.4)':'rgba(210,153,34,.4)')+';color:'+(bad?'var(--bad)':'var(--warn)')+'"><div class="head">\u26A0 Attention</div>'+msgs.map(function(m){return esc(m[1]);}).join('<br/>')+'</div>';
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
function renderPosture(a,s){
|
|
1964
|
+
var p=postureScore(a,s);var exposed=((a&&a.exposed_in_env)||[]).length;var chain=(a&&a.ledger_chain)||{ok:true,total:0,legacy:0};
|
|
1965
|
+
var chainTxt=chain.total===0?'empty':(chain.ok?'intact':'TAMPERED');
|
|
1966
|
+
var cards=[
|
|
1967
|
+
['.env leak surface',exposed===0?'0 \u2705':exposed+' \u26A0','sealed keys still in .env',exposed===0?'var(--ok)':'var(--warn)'],
|
|
1968
|
+
['Ledger integrity',(chainTxt==='TAMPERED'?'\u2716 ':chainTxt==='intact'?'\u2705 ':'')+chainTxt,(chain.legacy||0)+' legacy entries',chainTxt==='TAMPERED'?'var(--bad)':'var(--ok)'],
|
|
1969
|
+
['Mode',s&&s.mode==='real'?'REAL':'MOCK',s&&s.mode==='real'?'connected to '+esc(s.t3_env||''):'BLINDFOLD_MOCK=1','var(--c1)'],
|
|
1970
|
+
['Issues',p.notes.length,p.notes.length?esc(p.notes.join(' \xB7 ')):'all clear',p.notes.length?'var(--warn)':'var(--ok)']
|
|
1971
|
+
];
|
|
1972
|
+
document.getElementById('posture-cards').innerHTML=cards.map(function(c){return '<div class="card kpi" style="--kpi:'+c[3]+'"><div class="label">'+c[0]+'</div><div class="value">'+c[1]+'</div><div class="sub2">'+c[2]+'</div></div>';}).join('');
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
function renderStatus(s){
|
|
1976
|
+
var cards=[
|
|
1977
|
+
[s.mode==='real'?'REAL':'MOCK','Mode',s.mode==='real'?'T3 '+esc(s.t3_env):'mock'],
|
|
1978
|
+
[esc(s.contract_version||'\u2014'),'Contract','blindfold-proxy'],
|
|
1979
|
+
[esc(s.tenant_did_short||'(none)'),'Tenant',s.tenant_did?'full in title':'set creds'],
|
|
1980
|
+
[s.proxy_port,'Proxy port','127.0.0.1:'+s.proxy_port],
|
|
1981
|
+
[s.sdk_installed?'\u2713':'\u2717','SDK',s.sdk_installed?'installed':'missing']
|
|
1982
|
+
];
|
|
1983
|
+
document.getElementById('status-cards').innerHTML=cards.map(function(c){return '<div class="card"><div class="label">'+esc(c[1])+'</div><div class="value">'+esc(c[0])+'</div><div class="sub2">'+esc(c[2])+'</div></div>';}).join('');
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1986
|
+
function donutSvg(rows,total){
|
|
1987
|
+
var r=42,c=2*Math.PI*r,off=0,seg='';
|
|
1988
|
+
rows.forEach(function(row){var frac=total?row[1]/total:0;var len=frac*c;var col=colorFor(row[0]);seg+='<circle cx="60" cy="60" r="'+r+'" fill="none" stroke="'+col+'" stroke-width="16" stroke-dasharray="'+len.toFixed(2)+' '+(c-len).toFixed(2)+'" stroke-dashoffset="'+(-off).toFixed(2)+'" transform="rotate(-90 60 60)"><title>'+esc(row[0])+': '+row[1]+'</title></circle>';off+=len;});
|
|
1989
|
+
return '<svg viewBox="0 0 120 120" style="width:120px;height:120px;flex:none">'+seg+'<text x="60" y="56" text-anchor="middle" fill="var(--fg)" font-size="22" font-weight="700">'+total+'</text><text x="60" y="74" text-anchor="middle" fill="var(--dim)" font-size="9">requests</text></svg>';
|
|
1990
|
+
}
|
|
1991
|
+
function renderDonut(ev){
|
|
1992
|
+
var by={};ev.forEach(function(e){var k=e.provider||'(unknown)';by[k]=(by[k]||0)+1;});
|
|
1993
|
+
var rows=Object.keys(by).map(function(k){return[k,by[k]];}).sort(function(a,b){return b[1]-a[1];});
|
|
1994
|
+
var el=document.getElementById('provider-chart');
|
|
1995
|
+
if(!rows.length){el.innerHTML='<div style="color:var(--dim);font-size:13px">No traffic yet.</div>';return;}
|
|
1996
|
+
var legend=rows.map(function(row){return '<div class="li"><span class="dot" style="background:'+colorFor(row[0])+'"></span>'+esc(row[0])+'<span class="n">'+row[1]+'</span></div>';}).join('');
|
|
1997
|
+
el.innerHTML='<div class="donut-wrap">'+donutSvg(rows,ev.length)+'<div class="legend">'+legend+'</div></div>';
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
function renderStatusBar(ev){
|
|
2001
|
+
var b={'2xx':0,'3xx':0,'4xx':0,'5xx':0};ev.forEach(function(e){var k=Math.floor((e.status||0)/100)+'xx';if(b[k]!==undefined)b[k]++;});
|
|
2002
|
+
var col={'2xx':'var(--ok)','3xx':'var(--info)','4xx':'var(--warn)','5xx':'var(--bad)'};
|
|
2003
|
+
var total=ev.length;var el=document.getElementById('status-chart');
|
|
2004
|
+
if(!total){el.innerHTML='<div style="color:var(--dim);font-size:13px">No traffic yet.</div>';return;}
|
|
2005
|
+
var segs=Object.keys(b).filter(function(k){return b[k]>0;}).map(function(k){return '<div style="width:'+(b[k]/total*100)+'%;background:'+col[k]+'" title="'+k+': '+b[k]+'"></div>';}).join('');
|
|
2006
|
+
var legend=Object.keys(b).map(function(k){return '<div class="li"><span class="dot" style="background:'+col[k]+'"></span>'+k+'<span class="n">'+b[k]+'</span></div>';}).join('');
|
|
2007
|
+
el.innerHTML='<div class="segbar">'+segs+'</div><div class="legend" style="flex-direction:row;flex-wrap:wrap;gap:14px;margin-top:12px">'+legend+'</div>';
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
function areaSvg(values,color,maxOpt){
|
|
2011
|
+
var w=300,h=70,n=values.length;if(n<2)values=values.concat(values);n=values.length;
|
|
2012
|
+
var max=Math.max(maxOpt||0,Math.max.apply(null,values),1);
|
|
2013
|
+
var pts=values.map(function(v,i){return (i/(n-1)*w).toFixed(1)+','+(h-(v/max)*h).toFixed(1);});
|
|
2014
|
+
var gid='g'+Math.floor(Math.random()*1e6);
|
|
2015
|
+
var poly=pts.join(' ');var area='0,'+h+' '+poly+' '+w+','+h;
|
|
2016
|
+
return '<svg viewBox="0 0 '+w+' '+h+'" preserveAspectRatio="none" style="width:100%;height:72px;display:block"><defs><linearGradient id="'+gid+'" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="'+color+'" stop-opacity="0.35"/><stop offset="100%" stop-color="'+color+'" stop-opacity="0"/></linearGradient></defs><polygon points="'+area+'" fill="url(#'+gid+')"/><polyline points="'+poly+'" fill="none" stroke="'+color+'" stroke-width="2" vector-effect="non-scaling-stroke"/></svg>';
|
|
2017
|
+
}
|
|
2018
|
+
function renderTrends(ev){
|
|
2019
|
+
var now=Date.now();var lat=[],ok=[],tot=[];for(var i=0;i<60;i++){lat.push([]);ok.push(0);tot.push(0);}
|
|
2020
|
+
ev.forEach(function(e){var ageMin=Math.floor((now-new Date(e.t).getTime())/60000);if(ageMin>=0&&ageMin<60){var idx=59-ageMin;lat[idx].push(e.latency_ms||0);tot[idx]++;if((e.status||0)>=200&&(e.status||0)<300)ok[idx]++;}});
|
|
2021
|
+
var p95=lat.map(function(a){return a.length?pct(a,95):0;});
|
|
2022
|
+
var succ=tot.map(function(t,i){return t?Math.round(ok[i]/t*100):0;});
|
|
2023
|
+
document.getElementById('trend-latency').innerHTML=areaSvg(p95,'var(--c1)')+'<div class="stat-row">peak p95 <span>'+Math.max.apply(null,p95.concat(0))+' ms</span></div>';
|
|
2024
|
+
document.getElementById('trend-success').innerHTML=areaSvg(succ,'var(--ok)',100)+'<div class="stat-row">latest <span>'+(succ[59]||0)+'%</span></div>';
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
function renderSpark(ev){
|
|
2028
|
+
var now=Date.now();var b=[];for(var i=0;i<60;i++)b.push(0);
|
|
2029
|
+
ev.forEach(function(e){var ageMin=Math.floor((now-new Date(e.t).getTime())/60000);if(ageMin>=0&&ageMin<60)b[59-ageMin]++;});
|
|
2030
|
+
var max=Math.max.apply(null,b.concat(1));
|
|
2031
|
+
document.getElementById('spark').innerHTML=b.map(function(v,i){var hh=Math.max(1,Math.round(v/max*44));return '<div class="b '+(v===0?'zero':'')+'" style="height:'+hh+'px" title="'+v+' req \xB7 '+(59-i)+'m ago"></div>';}).join('');
|
|
2032
|
+
var sum=b.reduce(function(a,c){return a+c;},0);
|
|
2033
|
+
document.getElementById('spark-stats').innerHTML='last 60 min <span>'+sum+'</span> \xB7 peak minute <span>'+max+'</span>';
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
function renderSealed(entries){
|
|
2037
|
+
if(!entries.length){document.getElementById('sealed-table-wrap').innerHTML='<div class="empty">No keys sealed yet. <code>blindfold register --name <K></code></div>';return;}
|
|
2038
|
+
var latest={};entries.forEach(function(e){latest[e.name]=e;});
|
|
2039
|
+
var rows=Object.keys(latest).map(function(k){return latest[k];}).sort(function(a,b){return a.t<b.t?1:-1;}).map(function(e){return '<tr><td><span class="dot" style="display:inline-block;width:8px;height:8px;border-radius:2px;margin-right:6px;background:'+colorFor(e.name)+'"></span><code>'+esc(e.name)+'</code></td><td>'+e.length+' B</td><td>'+pillMode(e.mode)+'</td><td title="'+esc(e.t)+'">'+timeAgo(e.t)+'</td><td><code class="sentinel">__BLINDFOLD__</code></td><td style="white-space:nowrap"><button class="copybtn cmd" data-name="'+esc(e.name)+'" title="Copy a ready-to-run verify command">\u29C9 cmd</button> <button class="copybtn ref" data-name="'+esc(e.name)+'" title="Copy the Blindfold sentinel \u2014 put this where the real key would go; the enclave substitutes the sealed value at call time">\u29C9 sealed token</button></td></tr>';}).join('');
|
|
2040
|
+
document.getElementById('sealed-table-wrap').innerHTML='<div class="scroll"><table><thead><tr><th>Name</th><th>Bytes</th><th>Mode</th><th>Sealed</th><th>Sealed token</th><th>Copy</th></tr></thead><tbody>'+rows+'</tbody></table></div>';
|
|
2041
|
+
function flash(b,txt,cls){var o=b.textContent;b.textContent=txt;b.classList.add(cls||'done');setTimeout(function(){b.textContent=o;b.classList.remove('done','err');},1400);}
|
|
2042
|
+
var cmds=document.querySelectorAll('#sealed-table-wrap .copybtn.cmd');
|
|
2043
|
+
for(var i=0;i<cmds.length;i++){(function(b){b.onclick=function(){var cmd='npm run blindfold -- use --name '+b.getAttribute('data-name')+' --check';try{navigator.clipboard.writeText(cmd);}catch(e){}flash(b,'\u2713 copied');};})(cmds[i]);}
|
|
2044
|
+
var refs=document.querySelectorAll('#sealed-table-wrap .copybtn.ref');
|
|
2045
|
+
for(var j=0;j<refs.length;j++){(function(b){b.onclick=function(){try{navigator.clipboard.writeText('__BLINDFOLD__');}catch(e){}flash(b,'\u2713 sentinel copied');};})(refs[j]);}
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
function renderPerSecret(ev){
|
|
2049
|
+
var by={};ev.forEach(function(e){var k=e.secret_key||'(unknown)';by[k]=(by[k]||0)+1;});
|
|
2050
|
+
var rows=Object.keys(by).map(function(k){return[k,by[k]];}).sort(function(a,b){return b[1]-a[1];});
|
|
2051
|
+
var el=document.getElementById('per-secret-wrap');
|
|
2052
|
+
if(!rows.length){el.innerHTML='<div class="empty">No secret usage yet. Try <code>blindfold use --name <X> -- <cmd></code> or the proxy.</div>';return;}
|
|
2053
|
+
var max=Math.max.apply(null,rows.map(function(r){return r[1];}).concat(1));
|
|
2054
|
+
el.innerHTML='<div class="card">'+rows.map(function(r){return '<div class="bar-row"><div class="name" title="'+esc(r[0])+'">'+esc(r[0])+'</div><div class="track"><div class="fill" style="width:'+(r[1]/max*100)+'%;background:'+colorFor(r[0])+'"></div></div><div class="n">'+r[1]+'</div></div>';}).join('')+'</div>';
|
|
2055
|
+
}
|
|
2056
|
+
|
|
2057
|
+
function sortBy(k){if(window._sort.key===k)window._sort.dir*=-1;else window._sort={key:k,dir:-1};renderTable(rangeFiltered());}
|
|
2058
|
+
function renderTable(ev){
|
|
2059
|
+
var q=(document.getElementById('filter').value||'').toLowerCase().trim();
|
|
2060
|
+
var list=ev;
|
|
2061
|
+
if(q)list=ev.filter(function(e){return(e.provider||'').toLowerCase().indexOf(q)>=0||(e.path||'').toLowerCase().indexOf(q)>=0||(e.via||'').toLowerCase().indexOf(q)>=0||String(e.status||'').indexOf(q)>=0||(e.mode||'').toLowerCase().indexOf(q)>=0;});
|
|
2062
|
+
var k=window._sort.key,dir=window._sort.dir;
|
|
2063
|
+
list=list.slice().sort(function(a,b){var x=a[k],y=b[k];if(k==='t'){x=new Date(a.t).getTime();y=new Date(b.t).getTime();}return x<y?-dir:x>y?dir:0;}).slice(0,80);
|
|
2064
|
+
if(!list.length){document.getElementById('table-wrap').innerHTML='<div class="empty">'+(q?'No requests match "'+esc(q)+'".':'No traffic in range. Use a secret or the proxy.')+'</div>';return;}
|
|
2065
|
+
var rows=list.map(function(e){return '<tr><td>'+timeAgo(e.t)+'</td><td><span class="dot" style="display:inline-block;width:8px;height:8px;border-radius:2px;margin-right:6px;background:'+colorFor(e.provider)+'"></span>'+esc(e.provider)+'</td><td><span class="pill pill-info">'+esc(e.via||'proxy')+'</span></td><td><code>'+esc(e.method)+' '+esc(e.path)+'</code></td><td>'+pillStatus(e.status)+'</td><td>'+e.latency_ms+' ms</td><td>'+pillMode(e.mode)+'</td></tr>';}).join('');
|
|
2066
|
+
document.getElementById('table-wrap').innerHTML='<div class="scroll"><table><thead><tr><th data-k="t">When</th><th data-k="provider">Provider</th><th data-k="via">Via</th><th>Request</th><th data-k="status">Status</th><th data-k="latency_ms">Latency</th><th data-k="mode">Mode</th></tr></thead><tbody>'+rows+'</tbody></table></div>';
|
|
2067
|
+
var ths=document.querySelectorAll('#table-wrap th[data-k]');for(var ti=0;ti<ths.length;ti++){(function(th){th.onclick=function(){sortBy(th.getAttribute('data-k'));};})(ths[ti]);}
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
async function runFullAudit(){
|
|
2071
|
+
var el=document.getElementById('full-audit');el.innerHTML='<div class="empty">Running live enclave reconciliation\u2026</div>';
|
|
2072
|
+
try{
|
|
2073
|
+
var r=await fetch(api('/api/audit/full')).then(function(x){return x.json();});
|
|
2074
|
+
if(r.mock){el.innerHTML='<div class="alert ok"><div class="head">MOCK mode</div>Reconciliation only runs in REAL mode.</div>';return;}
|
|
2075
|
+
if(r.error){el.innerHTML='<div class="alert" style="border-color:rgba(248,81,73,.4);color:var(--bad)"><div class="head">Audit error</div>'+esc(r.error)+'</div>';return;}
|
|
2076
|
+
var rows=(r.results||[]).map(function(x){return '<tr><td><code>'+esc(x.name)+'</code></td><td>'+(x.present?'<span class="pill pill-ok">present</span>':'<span class="pill pill-bad">MISSING</span>')+'</td><td>'+x.enclave_len+' B</td><td>'+x.ledger_len+' B</td><td>'+(x.ok?'<span class="pill pill-ok">ok</span>':'<span class="pill pill-warn">drift</span>')+'</td><td><code>'+esc(x.fingerprint||'')+'</code></td></tr>';}).join('');
|
|
2077
|
+
var okN=(r.results||[]).filter(function(x){return x.ok;}).length;
|
|
2078
|
+
el.innerHTML='<div class="scroll" style="margin-bottom:6px"><table><thead><tr><th>Secret</th><th>Enclave</th><th>Enc bytes</th><th>Ledger bytes</th><th>Match</th><th>Fingerprint</th></tr></thead><tbody>'+rows+'</tbody></table></div><div class="stat-row">'+okN+'/'+(r.results||[]).length+' verified against the enclave</div>';
|
|
2079
|
+
}catch(e){el.innerHTML='<div class="alert" style="border-color:rgba(248,81,73,.4);color:var(--bad)"><div class="head">Audit failed</div>'+esc(String(e))+'</div>';}
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
function startStream(){try{var es=new EventSource(api('/api/stream'));es.addEventListener('change',function(){poll();});es.onerror=function(){};}catch(e){}}
|
|
2083
|
+
|
|
2084
|
+
function renderCommands(){
|
|
2085
|
+
var C=[
|
|
2086
|
+
['doctor','doctor','Check your key + tenant are healthy'],
|
|
2087
|
+
['status','status','Mode, tenant, and every sealed secret'],
|
|
2088
|
+
['audit','audit','Verify the ledger + reconcile against the enclave'],
|
|
2089
|
+
['migrate','migrate','Seal every secret in .env in one shot'],
|
|
2090
|
+
['register','register --name NAME --from-env NAME','Seal a secret into the enclave'],
|
|
2091
|
+
['use','use --name NAME -- COMMAND','Run any tool with a sealed secret injected'],
|
|
2092
|
+
['use --check','use --name NAME --check','Confirm a sealed secret is usable'],
|
|
2093
|
+
['rotate','rotate --name NAME --from-env NAME','Replace a secret value (rollback-safe)'],
|
|
2094
|
+
['rollback','rollback --name NAME','Restore the previous value'],
|
|
2095
|
+
['versions','versions --name NAME','List rollback snapshots'],
|
|
2096
|
+
['grant','grant --host api.openai.com','Authorize the contract to call a host'],
|
|
2097
|
+
['share','share --to DID --host HOST','Let a teammate agent use your keys'],
|
|
2098
|
+
['revoke','revoke --to DID','Remove a teammate access'],
|
|
2099
|
+
['proxy','proxy','Run the local OpenAI/Anthropic proxy'],
|
|
2100
|
+
['publish','publish','Publish the contract to T3'],
|
|
2101
|
+
['sealed','sealed','List sealed keys (metadata only)'],
|
|
2102
|
+
['dashboard','dashboard','Launch this dashboard'],
|
|
2103
|
+
['export','export --name NAME','CI: inject a sealed secret into the job env']
|
|
2104
|
+
];
|
|
2105
|
+
var rows=C.map(function(c){return '<tr><td><span class="cmdname">'+esc(c[0])+'</span></td><td><code>npm run blindfold -- '+esc(c[1])+'</code></td><td>'+esc(c[2])+'</td><td><button class="copybtn" data-cmd="npm run blindfold -- '+esc(c[1])+'">\u29C9 copy</button></td></tr>';}).join('');
|
|
2106
|
+
document.getElementById('commands-wrap').innerHTML='<div class="scroll"><table><thead><tr><th>Command</th><th>Run</th><th>What it does</th><th>Copy</th></tr></thead><tbody>'+rows+'</tbody></table></div>';
|
|
2107
|
+
var btns=document.querySelectorAll('#commands-wrap .copybtn');
|
|
2108
|
+
for(var i=0;i<btns.length;i++){(function(b){b.onclick=function(){try{navigator.clipboard.writeText(b.getAttribute('data-cmd'));}catch(e){}var o=b.textContent;b.textContent='\u2713 copied';b.classList.add('done');setTimeout(function(){b.textContent=o;b.classList.remove('done');},1300);};})(btns[i]);}
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
document.getElementById('logo').src=api('/logo.png');
|
|
2112
|
+
renderCommands();
|
|
2113
|
+
poll();setRefresh();startStream();
|
|
2114
|
+
</script>
|
|
2115
|
+
</body>
|
|
2116
|
+
</html>
|
|
2117
|
+
`;
|
|
2118
|
+
|
|
2119
|
+
// src/register.ts
|
|
2120
|
+
init_env();
|
|
2121
|
+
init_log();
|
|
2122
|
+
|
|
2123
|
+
// src/prompt.ts
|
|
2124
|
+
async function readSecretLine(prompt) {
|
|
2125
|
+
process.stderr.write(prompt);
|
|
2126
|
+
const stdin = process.stdin;
|
|
2127
|
+
if (!stdin.isTTY) {
|
|
2128
|
+
return await readOneLine(stdin);
|
|
2129
|
+
}
|
|
2130
|
+
return await new Promise((resolve, reject) => {
|
|
2131
|
+
let buf = "";
|
|
2132
|
+
stdin.setRawMode(true);
|
|
2133
|
+
stdin.resume();
|
|
2134
|
+
stdin.setEncoding("utf8");
|
|
2135
|
+
const cleanup = () => {
|
|
2136
|
+
stdin.removeListener("data", onData);
|
|
2137
|
+
stdin.setRawMode(false);
|
|
2138
|
+
stdin.pause();
|
|
2139
|
+
};
|
|
2140
|
+
const onData = (chunk) => {
|
|
2141
|
+
const s = typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
2142
|
+
for (const ch of s) {
|
|
2143
|
+
if (ch === "\n" || ch === "\r") {
|
|
2144
|
+
cleanup();
|
|
2145
|
+
process.stderr.write("\n");
|
|
2146
|
+
resolve(buf);
|
|
2147
|
+
return;
|
|
2148
|
+
}
|
|
2149
|
+
if (ch === "") {
|
|
2150
|
+
cleanup();
|
|
2151
|
+
process.stderr.write("\n");
|
|
2152
|
+
reject(new Error("aborted by user"));
|
|
2153
|
+
return;
|
|
2154
|
+
}
|
|
2155
|
+
if (ch === "\x7F" || ch === "\b") {
|
|
2156
|
+
if (buf.length > 0) buf = buf.slice(0, -1);
|
|
2157
|
+
continue;
|
|
2158
|
+
}
|
|
2159
|
+
if (ch >= " " || ch === " ") buf += ch;
|
|
2160
|
+
}
|
|
2161
|
+
};
|
|
2162
|
+
stdin.on("data", onData);
|
|
2163
|
+
});
|
|
2164
|
+
}
|
|
2165
|
+
function readOneLine(stdin) {
|
|
2166
|
+
return new Promise((resolve, reject) => {
|
|
2167
|
+
let buf = "";
|
|
2168
|
+
stdin.setEncoding("utf8");
|
|
2169
|
+
stdin.on("data", (d) => {
|
|
2170
|
+
buf += d;
|
|
2171
|
+
});
|
|
2172
|
+
stdin.on("end", () => resolve(buf.replace(/\r?\n$/, "")));
|
|
2173
|
+
stdin.on("error", reject);
|
|
2174
|
+
});
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
// src/register.ts
|
|
2178
|
+
init_t3_client();
|
|
2179
|
+
init_constants();
|
|
2180
|
+
async function registerSecret(opts) {
|
|
2181
|
+
const env = loadBlindfoldEnv();
|
|
2182
|
+
const t3 = await openT3Client(env);
|
|
2183
|
+
try {
|
|
2184
|
+
let source = "stdin";
|
|
2185
|
+
let value;
|
|
2186
|
+
if (opts.value !== void 0) {
|
|
2187
|
+
value = opts.value;
|
|
2188
|
+
source = "explicit";
|
|
2189
|
+
} else if (opts.fromEnv) {
|
|
2190
|
+
value = pluckSecret(opts.fromEnv);
|
|
2191
|
+
source = `env:${opts.fromEnv}`;
|
|
2192
|
+
} else {
|
|
2193
|
+
value = await readSecretLine(` Value for "${opts.name}" (input is hidden): `);
|
|
2194
|
+
if (!value) throw new Error("empty value");
|
|
2195
|
+
}
|
|
2196
|
+
if (value.includes(SENTINEL)) {
|
|
2197
|
+
throw new Error(`Secret value contains the Blindfold sentinel string "${SENTINEL}" \u2014 this would cause infinite substitution. Use a different value.`);
|
|
2198
|
+
}
|
|
2199
|
+
await t3.seedSecret(opts.name, value);
|
|
2200
|
+
const length = value.length;
|
|
2201
|
+
const mode = env.mock ? "mock" : "real";
|
|
2202
|
+
recordSealed({
|
|
2203
|
+
t: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2204
|
+
name: opts.name,
|
|
2205
|
+
source,
|
|
2206
|
+
length,
|
|
2207
|
+
mode,
|
|
2208
|
+
tenant_did: env.did,
|
|
2209
|
+
map_name: env.did ? `z:${env.did.replace(/^did:t3n:/, "")}:secrets` : "(mock)"
|
|
2210
|
+
});
|
|
2211
|
+
safeLog("info", { msg: "registered", name: opts.name, source, length, mode });
|
|
2212
|
+
} finally {
|
|
2213
|
+
await t3.close();
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
async function registerContract(wasm) {
|
|
2217
|
+
const env = loadBlindfoldEnv();
|
|
2218
|
+
const t3 = await openT3Client(env);
|
|
2219
|
+
try {
|
|
2220
|
+
return await t3.registerContract(wasm);
|
|
2221
|
+
} finally {
|
|
2222
|
+
await t3.close();
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
// src/release.ts
|
|
2227
|
+
init_env();
|
|
2228
|
+
init_t3_client();
|
|
2229
|
+
var clientCache = /* @__PURE__ */ new Map();
|
|
2230
|
+
function sharedClient(env) {
|
|
2231
|
+
const key = `${env.did}|${env.t3Env}|${env.t3BaseUrl}|${env.mock ? 1 : 0}`;
|
|
2232
|
+
let c = clientCache.get(key);
|
|
2233
|
+
if (!c) {
|
|
2234
|
+
c = openT3Client(env).catch((e) => {
|
|
2235
|
+
clientCache.delete(key);
|
|
2236
|
+
throw e;
|
|
2237
|
+
});
|
|
2238
|
+
clientCache.set(key, c);
|
|
2239
|
+
}
|
|
2240
|
+
return c;
|
|
2241
|
+
}
|
|
2242
|
+
async function release(name, opts) {
|
|
2243
|
+
const env = opts?.env ?? loadBlindfoldEnv();
|
|
2244
|
+
const useShared = !opts?.env;
|
|
2245
|
+
const t3 = useShared ? await sharedClient(env) : await openT3Client(env);
|
|
2246
|
+
const startedAt = Date.now();
|
|
2247
|
+
try {
|
|
2248
|
+
const value = await t3.releaseSecret(name);
|
|
2249
|
+
logUsage({
|
|
2250
|
+
t: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2251
|
+
mode: env.mock ? "mock" : "real",
|
|
2252
|
+
provider: "(enclave)",
|
|
2253
|
+
method: "RELEASE",
|
|
2254
|
+
path: name,
|
|
2255
|
+
upstream: "t3-enclave",
|
|
2256
|
+
status: 200,
|
|
2257
|
+
latency_ms: Date.now() - startedAt,
|
|
2258
|
+
agent_supplied_auth: false,
|
|
2259
|
+
sentinel_in_outbound: false,
|
|
2260
|
+
via: opts?.via ?? "release",
|
|
2261
|
+
secret_key: name
|
|
2262
|
+
});
|
|
2263
|
+
return value;
|
|
2264
|
+
} finally {
|
|
2265
|
+
if (!useShared) await t3.close();
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
|
|
2269
|
+
// src/wrap.ts
|
|
2270
|
+
init_constants();
|
|
2271
|
+
import http3 from "node:http";
|
|
2272
|
+
var PROXY_TOKEN_HEADER2 = "x-blindfold-token";
|
|
2273
|
+
function socketFetch(socketPath, token) {
|
|
2274
|
+
return async (input, init = {}) => {
|
|
2275
|
+
const reqObj = typeof Request !== "undefined" && input instanceof Request ? input : void 0;
|
|
2276
|
+
const rawUrl = reqObj ? reqObj.url : String(input);
|
|
2277
|
+
const url = new URL(rawUrl);
|
|
2278
|
+
const method = init?.method ?? reqObj?.method ?? "GET";
|
|
2279
|
+
const headers = new Headers(init?.headers ?? reqObj?.headers);
|
|
2280
|
+
if (token) headers.set(PROXY_TOKEN_HEADER2, token);
|
|
2281
|
+
const outHeaders = {};
|
|
2282
|
+
headers.forEach((v, k) => {
|
|
2283
|
+
outHeaders[k] = v;
|
|
2284
|
+
});
|
|
2285
|
+
return await new Promise((resolve, reject) => {
|
|
2286
|
+
const req = http3.request(
|
|
2287
|
+
{ socketPath, path: url.pathname + url.search, method, headers: outHeaders },
|
|
2288
|
+
(res) => {
|
|
2289
|
+
const chunks = [];
|
|
2290
|
+
res.on("data", (c) => chunks.push(c));
|
|
2291
|
+
res.on("end", () => {
|
|
2292
|
+
const respHeaders = new Headers();
|
|
2293
|
+
for (const [k, v] of Object.entries(res.headers)) {
|
|
2294
|
+
if (typeof v === "string") respHeaders.set(k, v);
|
|
2295
|
+
else if (Array.isArray(v)) respHeaders.set(k, v.join(", "));
|
|
2296
|
+
}
|
|
2297
|
+
resolve(new Response(Buffer.concat(chunks), {
|
|
2298
|
+
status: res.statusCode ?? 502,
|
|
2299
|
+
statusText: res.statusMessage ?? "",
|
|
2300
|
+
headers: respHeaders
|
|
2301
|
+
}));
|
|
2302
|
+
});
|
|
2303
|
+
}
|
|
2304
|
+
);
|
|
2305
|
+
req.on("error", reject);
|
|
2306
|
+
const body = init?.body;
|
|
2307
|
+
if (body != null) {
|
|
2308
|
+
if (typeof body === "string" || body instanceof Buffer || body instanceof Uint8Array) req.write(body);
|
|
2309
|
+
else if (typeof URLSearchParams !== "undefined" && body instanceof URLSearchParams) req.write(body.toString());
|
|
2310
|
+
else if (ArrayBuffer.isView(body)) {
|
|
2311
|
+
const v = body;
|
|
2312
|
+
req.write(Buffer.from(v.buffer, v.byteOffset, v.byteLength));
|
|
2313
|
+
} else if (body instanceof ArrayBuffer) req.write(Buffer.from(body));
|
|
2314
|
+
else {
|
|
2315
|
+
req.destroy();
|
|
2316
|
+
reject(new Error("socketFetch: unsupported body type (use string/Buffer/typed-array/URLSearchParams)"));
|
|
2317
|
+
return;
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
req.end();
|
|
2321
|
+
});
|
|
2322
|
+
};
|
|
2323
|
+
}
|
|
2324
|
+
function wrap(client, opts = {}) {
|
|
2325
|
+
const baseUrl = opts.baseUrl ?? `http://127.0.0.1:${DEFAULT_PORT}/v1`;
|
|
2326
|
+
client.baseURL = baseUrl;
|
|
2327
|
+
client.apiKey = SENTINEL;
|
|
2328
|
+
const token = (opts.token ?? process.env.BLINDFOLD_PROXY_TOKEN ?? "").trim() || void 0;
|
|
2329
|
+
const socket = (opts.socket ?? process.env.BLINDFOLD_PROXY_SOCKET ?? "").trim() || void 0;
|
|
2330
|
+
if (socket) {
|
|
2331
|
+
client.fetch = socketFetch(socket, token);
|
|
2332
|
+
} else if (token) {
|
|
2333
|
+
const base = client.fetch ?? fetch;
|
|
2334
|
+
client.fetch = (input, init = {}) => {
|
|
2335
|
+
const headers = new Headers(init?.headers);
|
|
2336
|
+
headers.set(PROXY_TOKEN_HEADER2, token);
|
|
2337
|
+
return base(input, { ...init, headers });
|
|
2338
|
+
};
|
|
2339
|
+
}
|
|
2340
|
+
return client;
|
|
2341
|
+
}
|
|
2342
|
+
|
|
2343
|
+
// src/index.ts
|
|
2344
|
+
init_env();
|
|
2345
|
+
export {
|
|
2346
|
+
CONTRACT_TAIL,
|
|
2347
|
+
CONTRACT_VERSION,
|
|
2348
|
+
DEFAULT_DASHBOARD_PORT,
|
|
2349
|
+
DEFAULT_PORT,
|
|
2350
|
+
SENTINEL,
|
|
2351
|
+
assertRealReady,
|
|
2352
|
+
clearUsage,
|
|
2353
|
+
defaultLogPath,
|
|
2354
|
+
loadBlindfoldEnv,
|
|
2355
|
+
readUsage,
|
|
2356
|
+
registerContract,
|
|
2357
|
+
registerSecret,
|
|
2358
|
+
release,
|
|
2359
|
+
startDashboard,
|
|
2360
|
+
startProxy,
|
|
2361
|
+
wrap
|
|
2362
|
+
};
|