@oracle-agent/oracle 0.4.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -17
- package/SETUP.md +13 -0
- package/bin/oracle-data-mcp.mjs +1 -1
- package/bin/oracle.mjs +33 -0
- package/docs/cli.md +19 -0
- package/docs/connectors.md +13 -0
- package/package.json +7 -3
- package/profiles/robinhood-agent/SOUL.md +5 -3
- package/public/agent-connect/app.js +16 -13
- package/public/oracle-splash/assets/wordmarks/across.svg +4 -0
- package/public/oracle-splash/assets/wordmarks/aerodrome.svg +23 -0
- package/public/oracle-splash/assets/wordmarks/balancer.svg +3 -0
- package/public/oracle-splash/assets/wordmarks/cowswap.svg +1 -0
- package/public/oracle-splash/assets/wordmarks/gmx.svg +13 -0
- package/public/oracle-splash/assets/wordmarks/hyperliquid.svg +21 -0
- package/public/oracle-splash/assets/wordmarks/hyperswap.svg +13 -0
- package/public/oracle-splash/assets/wordmarks/jupiter.svg +35 -0
- package/public/oracle-splash/assets/wordmarks/lifi.png +0 -0
- package/public/oracle-splash/assets/wordmarks/magic-eden.svg +1 -0
- package/public/oracle-splash/assets/wordmarks/morpho.svg +1 -0
- package/public/oracle-splash/assets/wordmarks/odos.svg +3 -0
- package/public/oracle-splash/assets/wordmarks/oneinch.svg +6 -0
- package/public/oracle-splash/assets/wordmarks/opensea.svg +1 -0
- package/public/oracle-splash/assets/wordmarks/pancakeswap.svg +18 -0
- package/public/oracle-splash/assets/wordmarks/paraswap.svg +16 -0
- package/public/oracle-splash/assets/wordmarks/pendle.png +0 -0
- package/public/oracle-splash/assets/wordmarks/polymarket.png +0 -0
- package/public/oracle-splash/assets/wordmarks/quickswap.png +0 -0
- package/public/oracle-splash/assets/wordmarks/relay.svg +8 -0
- package/public/oracle-splash/assets/wordmarks/stargate.svg +15 -0
- package/public/oracle-splash/assets/wordmarks/uniswap.svg +18 -0
- package/public/oracle-splash/assets/wordmarks/velodrome.svg +84 -0
- package/public/oracle-splash/index.html +789 -485
- package/scripts/check-doc-drift.mjs +111 -0
- package/src/cli/commands/credential.mjs +9 -0
- package/src/cli/commands/data-mcp.mjs +18 -0
- package/src/cli/commands/data.mjs +73 -0
- package/src/cli/commands/doctor.mjs +131 -0
- package/src/cli/commands/help.mjs +7 -0
- package/src/cli/commands/init.mjs +27 -0
- package/src/cli/commands/mcp.mjs +142 -0
- package/src/cli/commands/prepare.mjs +12 -0
- package/src/cli/commands/public.mjs +20 -0
- package/src/cli/commands/route.mjs +12 -0
- package/src/cli/commands/runner.mjs +9 -0
- package/src/cli/commands/scan.mjs +12 -0
- package/src/cli/commands/sign.mjs +21 -0
- package/src/cli/commands/signer.mjs +9 -0
- package/src/cli/commands/upgrade.mjs +12 -0
- package/src/cli/commands/vault.mjs +9 -0
- package/src/cli/commands/version.mjs +22 -0
- package/src/cli/first-run.mjs +20 -0
- package/src/cli/kernel.mjs +198 -0
- package/src/cli/mcp-targets/chatgpt.mjs +51 -0
- package/src/cli/mcp-targets/claude-code.mjs +39 -0
- package/src/cli/mcp-targets/claude-desktop.mjs +24 -0
- package/src/cli/mcp-targets/codex.mjs +41 -0
- package/src/cli/mcp-targets/shared.mjs +61 -0
- package/src/cli/operator-dispatch.mjs +258 -0
- package/src/cli/paths.mjs +78 -0
- package/src/cli/spawn-child.mjs +43 -0
- package/src/public-api/connect-agent.mjs +29 -10
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const PACKAGE_ROOT = path.resolve(HERE, "../..");
|
|
9
|
+
|
|
10
|
+
export const OPERATOR_BIN_NAMES = Object.freeze([
|
|
11
|
+
"oracle-vault",
|
|
12
|
+
"oracle-credential",
|
|
13
|
+
"oracle-signer",
|
|
14
|
+
"oracle-runner",
|
|
15
|
+
"oracle-agentic-init",
|
|
16
|
+
"oracle-agentic-doctor",
|
|
17
|
+
"oracle-control-mcp",
|
|
18
|
+
"oracle-operator-caps",
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
export const SIGN_NOUNS = Object.freeze([
|
|
22
|
+
"vault",
|
|
23
|
+
"credential",
|
|
24
|
+
"signer",
|
|
25
|
+
"runner",
|
|
26
|
+
"sign",
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
export const OPERATOR_NOT_INSTALLED_MESSAGE = `oracle: signing lives in @oracle-agent/operator, which is not installed.
|
|
30
|
+
|
|
31
|
+
The public 'oracle' package is prepare-only and never handles keys.
|
|
32
|
+
To enable local signing on THIS machine:
|
|
33
|
+
|
|
34
|
+
npm i -g @oracle-agent/operator # or add it to this project
|
|
35
|
+
oracle sign init # provision keys + policy (interactive)
|
|
36
|
+
|
|
37
|
+
Nothing about your read/research setup is affected.
|
|
38
|
+
`;
|
|
39
|
+
|
|
40
|
+
export const SIGN_HINT =
|
|
41
|
+
"hint: run 'oracle sign init' to provision keys, or 'oracle sign doctor --json' for detail.";
|
|
42
|
+
|
|
43
|
+
function whichOnPath(binName) {
|
|
44
|
+
const pathEnv = process.env.PATH || "";
|
|
45
|
+
const parts = pathEnv.split(path.delimiter).filter(Boolean);
|
|
46
|
+
const exts = process.platform === "win32" ? ["", ".cmd", ".exe", ".bat"] : [""];
|
|
47
|
+
for (const dir of parts) {
|
|
48
|
+
for (const ext of exts) {
|
|
49
|
+
const candidate = path.join(dir, binName + ext);
|
|
50
|
+
try {
|
|
51
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
|
|
52
|
+
return candidate;
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
55
|
+
// ignore
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function binsFromDir(binDir) {
|
|
63
|
+
const bins = {};
|
|
64
|
+
for (const name of OPERATOR_BIN_NAMES) {
|
|
65
|
+
const candidates = [
|
|
66
|
+
path.join(binDir, name),
|
|
67
|
+
path.join(binDir, `${name}.mjs`),
|
|
68
|
+
path.join(binDir, `${name}.js`),
|
|
69
|
+
];
|
|
70
|
+
for (const c of candidates) {
|
|
71
|
+
if (fs.existsSync(c) && fs.statSync(c).isFile()) {
|
|
72
|
+
bins[name] = c;
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return bins;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function findPackageRoot(entryPath) {
|
|
81
|
+
let dir = path.dirname(entryPath);
|
|
82
|
+
while (dir !== path.dirname(dir)) {
|
|
83
|
+
const pkgJson = path.join(dir, "package.json");
|
|
84
|
+
if (fs.existsSync(pkgJson)) {
|
|
85
|
+
try {
|
|
86
|
+
const pkg = JSON.parse(fs.readFileSync(pkgJson, "utf8"));
|
|
87
|
+
if (pkg.name === "@oracle-agent/operator") return { root: dir, pkg };
|
|
88
|
+
} catch {
|
|
89
|
+
// continue upward
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
dir = path.dirname(dir);
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function tryResolvePackage(fromPaths) {
|
|
98
|
+
for (const from of fromPaths) {
|
|
99
|
+
try {
|
|
100
|
+
const req = createRequire(path.join(from, "package.json"));
|
|
101
|
+
let found;
|
|
102
|
+
try {
|
|
103
|
+
const pkgJson = req.resolve("@oracle-agent/operator/package.json");
|
|
104
|
+
found = {
|
|
105
|
+
root: path.dirname(pkgJson),
|
|
106
|
+
pkg: JSON.parse(fs.readFileSync(pkgJson, "utf8")),
|
|
107
|
+
};
|
|
108
|
+
} catch {
|
|
109
|
+
const entry = req.resolve("@oracle-agent/operator");
|
|
110
|
+
found = findPackageRoot(entry);
|
|
111
|
+
}
|
|
112
|
+
if (!found) continue;
|
|
113
|
+
const { root, pkg } = found;
|
|
114
|
+
const binField = pkg.bin || {};
|
|
115
|
+
const bins = {};
|
|
116
|
+
for (const [name, rel] of Object.entries(binField)) {
|
|
117
|
+
bins[name] = path.resolve(root, rel);
|
|
118
|
+
}
|
|
119
|
+
const extra = binsFromDir(path.join(root, "bin"));
|
|
120
|
+
for (const [k, v] of Object.entries(extra)) {
|
|
121
|
+
if (!bins[k]) bins[k] = v;
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
ok: true,
|
|
125
|
+
binDir: path.join(root, "bin"),
|
|
126
|
+
version: pkg.version || "unknown",
|
|
127
|
+
bins,
|
|
128
|
+
source: `require:${from}`,
|
|
129
|
+
};
|
|
130
|
+
} catch {
|
|
131
|
+
// continue
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function tryCapsOnPath() {
|
|
138
|
+
const capsBin = whichOnPath("oracle-operator-caps");
|
|
139
|
+
if (!capsBin) return null;
|
|
140
|
+
const r = spawnSync(capsBin, [], {
|
|
141
|
+
encoding: "utf8",
|
|
142
|
+
timeout: 5000,
|
|
143
|
+
env: process.env,
|
|
144
|
+
});
|
|
145
|
+
if (r.status !== 0 || !r.stdout) return null;
|
|
146
|
+
try {
|
|
147
|
+
const report = JSON.parse(r.stdout);
|
|
148
|
+
if (!report || typeof report !== "object" || !report.bins) return null;
|
|
149
|
+
return {
|
|
150
|
+
ok: true,
|
|
151
|
+
binDir: path.dirname(Object.values(report.bins)[0] || capsBin),
|
|
152
|
+
version: report.version || "unknown",
|
|
153
|
+
bins: report.bins,
|
|
154
|
+
source: "caps-path",
|
|
155
|
+
controlMcp: report.controlMcp,
|
|
156
|
+
};
|
|
157
|
+
} catch {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function tryPathProbe() {
|
|
163
|
+
const bins = {};
|
|
164
|
+
for (const name of OPERATOR_BIN_NAMES) {
|
|
165
|
+
const hit = whichOnPath(name);
|
|
166
|
+
if (hit) bins[name] = hit;
|
|
167
|
+
}
|
|
168
|
+
if (!Object.keys(bins).length) return null;
|
|
169
|
+
return {
|
|
170
|
+
ok: true,
|
|
171
|
+
binDir: path.dirname(Object.values(bins)[0]),
|
|
172
|
+
version: "unknown",
|
|
173
|
+
bins,
|
|
174
|
+
source: "path-probe",
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function resolveOperator() {
|
|
179
|
+
const envDir = process.env.ORACLE_OPERATOR_BIN_DIR;
|
|
180
|
+
if (envDir) {
|
|
181
|
+
const abs = path.resolve(envDir);
|
|
182
|
+
if (!fs.existsSync(abs)) return { ok: false };
|
|
183
|
+
const bins = binsFromDir(abs);
|
|
184
|
+
if (!Object.keys(bins).length) return { ok: false };
|
|
185
|
+
let version = "unknown";
|
|
186
|
+
const pkgCandidate = path.resolve(abs, "../package.json");
|
|
187
|
+
if (fs.existsSync(pkgCandidate)) {
|
|
188
|
+
try {
|
|
189
|
+
version = JSON.parse(fs.readFileSync(pkgCandidate, "utf8")).version || version;
|
|
190
|
+
} catch {
|
|
191
|
+
// ignore
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return { ok: true, binDir: abs, version, bins, source: "env:ORACLE_OPERATOR_BIN_DIR" };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const fromPkg =
|
|
198
|
+
tryResolvePackage([process.cwd(), PACKAGE_ROOT]) ||
|
|
199
|
+
tryCapsOnPath() ||
|
|
200
|
+
tryPathProbe();
|
|
201
|
+
return fromPkg || { ok: false };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function printOperatorNotInstalled(stream = process.stderr) {
|
|
205
|
+
stream.write(OPERATOR_NOT_INSTALLED_MESSAGE);
|
|
206
|
+
if (!OPERATOR_NOT_INSTALLED_MESSAGE.endsWith("\n")) stream.write("\n");
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function dispatchOperator(binName, argv = [], opts = {}) {
|
|
210
|
+
const resolved = resolveOperator();
|
|
211
|
+
if (!resolved.ok) {
|
|
212
|
+
printOperatorNotInstalled(opts.stderr || process.stderr);
|
|
213
|
+
return 3;
|
|
214
|
+
}
|
|
215
|
+
const binPath = resolved.bins[binName];
|
|
216
|
+
if (!binPath) {
|
|
217
|
+
const err = opts.stderr || process.stderr;
|
|
218
|
+
err.write(`oracle: operator bin '${binName}' not found in resolved operator (${resolved.source})\n`);
|
|
219
|
+
return 3;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const stdio = opts.stdio || "inherit";
|
|
223
|
+
const isJs = /\.(mjs|cjs|js)$/.test(binPath);
|
|
224
|
+
const command = isJs ? process.execPath : binPath;
|
|
225
|
+
const args = isJs ? [binPath, ...argv] : [...argv];
|
|
226
|
+
const r = spawnSync(command, args, {
|
|
227
|
+
stdio,
|
|
228
|
+
env: opts.env || process.env,
|
|
229
|
+
cwd: opts.cwd || process.cwd(),
|
|
230
|
+
encoding: stdio === "pipe" ? "utf8" : undefined,
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
if (r.error) {
|
|
234
|
+
const err = opts.stderr || process.stderr;
|
|
235
|
+
err.write(`oracle: failed to spawn ${binName}: ${r.error.message}\n`);
|
|
236
|
+
return 1;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const code = typeof r.status === "number" ? r.status : 1;
|
|
240
|
+
if (code !== 0 && opts.appendHintOnError) {
|
|
241
|
+
const err = opts.stderr || process.stderr;
|
|
242
|
+
err.write(SIGN_HINT + "\n");
|
|
243
|
+
}
|
|
244
|
+
if (stdio === "pipe") {
|
|
245
|
+
return { code, stdout: r.stdout || "", stderr: r.stderr || "" };
|
|
246
|
+
}
|
|
247
|
+
return code;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export default {
|
|
251
|
+
resolveOperator,
|
|
252
|
+
dispatchOperator,
|
|
253
|
+
printOperatorNotInstalled,
|
|
254
|
+
OPERATOR_NOT_INSTALLED_MESSAGE,
|
|
255
|
+
SIGN_HINT,
|
|
256
|
+
SIGN_NOUNS,
|
|
257
|
+
OPERATOR_BIN_NAMES,
|
|
258
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
export const PACKAGE_ROOT = path.resolve(HERE, "../..");
|
|
8
|
+
|
|
9
|
+
export function homeDir() {
|
|
10
|
+
return process.env.ORACLE_FAKE_HOME || os.homedir();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function oracleConfigDir() {
|
|
14
|
+
return path.join(homeDir(), ".config", "oracle");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function oracleExecEnvPath() {
|
|
18
|
+
return path.join(oracleConfigDir(), "exec.env");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function hermesRoot() {
|
|
22
|
+
if (process.env.HERMES_HOME) return path.resolve(process.env.HERMES_HOME);
|
|
23
|
+
return path.join(homeDir(), ".hermes");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function claudeCodeUserConfigPath() {
|
|
27
|
+
return path.join(homeDir(), ".claude.json");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function claudeCodeProjectMcpPath(cwd = process.cwd()) {
|
|
31
|
+
return path.join(cwd, ".mcp.json");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function claudeDesktopConfigPath() {
|
|
35
|
+
if (process.platform === "darwin") {
|
|
36
|
+
return path.join(homeDir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
37
|
+
}
|
|
38
|
+
return path.join(homeDir(), ".config", "Claude", "claude_desktop_config.json");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function codexConfigPath() {
|
|
42
|
+
return path.join(homeDir(), ".codex", "config.toml");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function chatgptConnectorDir() {
|
|
46
|
+
return path.join(oracleConfigDir(), "connectors");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function chatgptOpenApiPath() {
|
|
50
|
+
return path.join(chatgptConnectorDir(), "chatgpt-openapi.json");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function packageBin(name) {
|
|
54
|
+
return path.join(PACKAGE_ROOT, "bin", name);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function ensureDir(dir) {
|
|
58
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
59
|
+
return dir;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const paths = {
|
|
63
|
+
homeDir,
|
|
64
|
+
oracleConfigDir,
|
|
65
|
+
oracleExecEnvPath,
|
|
66
|
+
hermesRoot,
|
|
67
|
+
claudeCodeUserConfigPath,
|
|
68
|
+
claudeCodeProjectMcpPath,
|
|
69
|
+
claudeDesktopConfigPath,
|
|
70
|
+
codexConfigPath,
|
|
71
|
+
chatgptConnectorDir,
|
|
72
|
+
chatgptOpenApiPath,
|
|
73
|
+
packageBin,
|
|
74
|
+
ensureDir,
|
|
75
|
+
PACKAGE_ROOT,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export default paths;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
const FORWARDED_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
4
|
+
|
|
5
|
+
export function spawnChild(command, args = [], options = {}, label = command) {
|
|
6
|
+
return new Promise((resolve) => {
|
|
7
|
+
const child = spawn(command, args, options);
|
|
8
|
+
let settled = false;
|
|
9
|
+
let forwardedSignal = null;
|
|
10
|
+
|
|
11
|
+
const handlers = new Map(
|
|
12
|
+
FORWARDED_SIGNALS.map((signal) => [signal, () => {
|
|
13
|
+
forwardedSignal ||= signal;
|
|
14
|
+
if (child.exitCode === null && child.signalCode === null) child.kill(signal);
|
|
15
|
+
}]),
|
|
16
|
+
);
|
|
17
|
+
for (const [signal, handler] of handlers) process.on(signal, handler);
|
|
18
|
+
|
|
19
|
+
const cleanup = () => {
|
|
20
|
+
for (const [signal, handler] of handlers) process.off(signal, handler);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
child.once("error", (error) => {
|
|
24
|
+
if (settled) return;
|
|
25
|
+
settled = true;
|
|
26
|
+
cleanup();
|
|
27
|
+
process.stderr.write(`oracle: could not start ${label}: ${error.message}\n`);
|
|
28
|
+
resolve(1);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
child.once("exit", (code, signal) => {
|
|
32
|
+
if (settled) return;
|
|
33
|
+
settled = true;
|
|
34
|
+
cleanup();
|
|
35
|
+
const propagated = forwardedSignal || signal;
|
|
36
|
+
if (propagated) {
|
|
37
|
+
process.kill(process.pid, propagated);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
resolve(code ?? 1);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
}
|
|
@@ -53,6 +53,18 @@ export class SecretLeakError extends Error {
|
|
|
53
53
|
const FORBIDDEN_KEY_RE =
|
|
54
54
|
/(private[_-]?key|secret|bearer|keystore|mnemonic|seed[_-]?phrase|passphrase|password|api[_-]?key|access[_-]?token|auth[_-]?token|session[_-]?token|signer[_-]?url|signature)/i;
|
|
55
55
|
|
|
56
|
+
/** Short wallet-export aliases need exact normalized matching. Substring
|
|
57
|
+
* matching `wif`, for example, would incorrectly reject an unrelated key such
|
|
58
|
+
* as `swift`. Removing common separators catches snake/kebab/spaced variants
|
|
59
|
+
* without widening those short aliases into arbitrary words. */
|
|
60
|
+
const FORBIDDEN_KEY_ALIASES = Object.freeze(["privkey", "wif", "xprv", "seed"]);
|
|
61
|
+
|
|
62
|
+
function isForbiddenKeyName(key) {
|
|
63
|
+
const text = String(key);
|
|
64
|
+
const normalized = text.toLowerCase().replace(/[\s._-]+/g, "");
|
|
65
|
+
return FORBIDDEN_KEY_RE.test(text) || FORBIDDEN_KEY_ALIASES.includes(normalized);
|
|
66
|
+
}
|
|
67
|
+
|
|
56
68
|
/** String-value shapes that must never appear in any object we return. */
|
|
57
69
|
const FORBIDDEN_VALUE_RULES = Object.freeze([
|
|
58
70
|
// 32-byte hex — the shape of a raw EVM private key / session secret. Public
|
|
@@ -69,8 +81,12 @@ const FORBIDDEN_VALUE_RULES = Object.freeze([
|
|
|
69
81
|
// gets disabled. Smuggling a key inside a longer string is still caught at
|
|
70
82
|
// the serialized layer for any non-allowlisted field.
|
|
71
83
|
{ rule: "raw-32-byte-hex-bare", re: /^[0-9a-fA-F]{64}$/ },
|
|
72
|
-
//
|
|
73
|
-
|
|
84
|
+
// BIP-32 private extended keys. Public xpub/tpub/ypub/zpub identifiers are
|
|
85
|
+
// intentionally not matched; only their private-key counterparts fail.
|
|
86
|
+
{ rule: "extended-private-key", re: /\b(?:xprv|tprv|yprv|zprv)[1-9A-HJ-NP-Za-km-z]{107}\b/ },
|
|
87
|
+
// Bitcoin WIF (mainnet 5/K/L, testnet uncompressed 9 or compressed c) —
|
|
88
|
+
// base58, 51-52 chars.
|
|
89
|
+
{ rule: "bitcoin-wif", re: /\b[59KLc][1-9A-HJ-NP-Za-km-z]{50,51}\b/ },
|
|
74
90
|
// BIP-39 mnemonic: 12/15/18/21/24 lowercase words. Detect a long run of
|
|
75
91
|
// space-separated short alpha words rather than shipping the wordlist.
|
|
76
92
|
{ rule: "bip39-mnemonic", re: /\b(?:[a-z]{3,8}\s+){11,23}[a-z]{3,8}\b/ },
|
|
@@ -169,12 +185,15 @@ export function assertSerializedNoSecrets(value) {
|
|
|
169
185
|
if (embedded.test(scanned)) {
|
|
170
186
|
throw new SecretLeakError("$<serialized>", "raw-32-byte-hex-embedded");
|
|
171
187
|
}
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
188
|
+
// Parse the exact serialized bytes with a reviver so escaped or toJSON-made
|
|
189
|
+
// key names receive the same normalization as real object properties. This
|
|
190
|
+
// avoids maintaining a second, inevitably drifting key-name regex.
|
|
191
|
+
JSON.parse(json, (key, item) => {
|
|
192
|
+
if (key && isForbiddenKeyName(key)) {
|
|
193
|
+
throw new SecretLeakError(`$<serialized>.${key}`, "forbidden-key-name");
|
|
194
|
+
}
|
|
195
|
+
return item;
|
|
196
|
+
});
|
|
178
197
|
return json;
|
|
179
198
|
}
|
|
180
199
|
|
|
@@ -207,7 +226,7 @@ export function assertNoSecretMaterial(value, path = "$", seen = new Set(), keyH
|
|
|
207
226
|
if (ArrayBuffer.isView(value) || value instanceof ArrayBuffer) return value;
|
|
208
227
|
if (value instanceof Map) {
|
|
209
228
|
for (const [k, v] of value.entries()) {
|
|
210
|
-
if (typeof k === "string" &&
|
|
229
|
+
if (typeof k === "string" && isForbiddenKeyName(k)) {
|
|
211
230
|
throw new SecretLeakError(`${path}[${JSON.stringify(k)}]`, "forbidden-key-name");
|
|
212
231
|
}
|
|
213
232
|
assertNoSecretMaterial(k, `${path}[key]`, seen);
|
|
@@ -225,7 +244,7 @@ export function assertNoSecretMaterial(value, path = "$", seen = new Set(), keyH
|
|
|
225
244
|
return value;
|
|
226
245
|
}
|
|
227
246
|
for (const [k, v] of Object.entries(value)) {
|
|
228
|
-
if (
|
|
247
|
+
if (isForbiddenKeyName(k)) {
|
|
229
248
|
throw new SecretLeakError(`${path}.${k}`, "forbidden-key-name");
|
|
230
249
|
}
|
|
231
250
|
assertNoSecretMaterial(v, `${path}.${k}`, seen, k);
|