botanary-mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +235 -0
- package/dist/bin/botanary-mcp.js +21 -0
- package/dist/bin/botanary-mcp.js.map +1 -0
- package/dist/src/api-client.js +42 -0
- package/dist/src/api-client.js.map +1 -0
- package/dist/src/cli.js +79 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/identity/backends/exec.js +34 -0
- package/dist/src/identity/backends/exec.js.map +1 -0
- package/dist/src/identity/backends/file-fallback.js +58 -0
- package/dist/src/identity/backends/file-fallback.js.map +1 -0
- package/dist/src/identity/backends/libsecret.js +57 -0
- package/dist/src/identity/backends/libsecret.js.map +1 -0
- package/dist/src/identity/backends/macos-keychain.js +63 -0
- package/dist/src/identity/backends/macos-keychain.js.map +1 -0
- package/dist/src/identity/backends/select.js +34 -0
- package/dist/src/identity/backends/select.js.map +1 -0
- package/dist/src/identity/backends/types.js +16 -0
- package/dist/src/identity/backends/types.js.map +1 -0
- package/dist/src/identity/backends/windows-dpapi.js +89 -0
- package/dist/src/identity/backends/windows-dpapi.js.map +1 -0
- package/dist/src/identity/fingerprint.js +47 -0
- package/dist/src/identity/fingerprint.js.map +1 -0
- package/dist/src/identity/keypair.js +40 -0
- package/dist/src/identity/keypair.js.map +1 -0
- package/dist/src/identity/pairing-code.js +60 -0
- package/dist/src/identity/pairing-code.js.map +1 -0
- package/dist/src/identity/redact.js +21 -0
- package/dist/src/identity/redact.js.map +1 -0
- package/dist/src/identity/store.js +249 -0
- package/dist/src/identity/store.js.map +1 -0
- package/dist/src/identity/types.js +2 -0
- package/dist/src/identity/types.js.map +1 -0
- package/dist/src/paths.js +22 -0
- package/dist/src/paths.js.map +1 -0
- package/dist/src/runtime.js +195 -0
- package/dist/src/runtime.js.map +1 -0
- package/dist/src/server.js +36 -0
- package/dist/src/server.js.map +1 -0
- package/dist/src/tools.js +320 -0
- package/dist/src/tools.js.map +1 -0
- package/package.json +51 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { createFileFallbackBackend } from './file-fallback.js';
|
|
2
|
+
import { createLibsecretBackend } from './libsecret.js';
|
|
3
|
+
import { createMacosKeychainBackend } from './macos-keychain.js';
|
|
4
|
+
import { createWindowsDpapiBackend } from './windows-dpapi.js';
|
|
5
|
+
/**
|
|
6
|
+
* Fixed, platform-derived priority order. The SAME list is used for reads and writes (store.ts): a new
|
|
7
|
+
* key is written to the first available candidate, and every candidate is swept in this order when
|
|
8
|
+
* looking for an existing one. `file-fallback` is always last and always available, so this list can
|
|
9
|
+
* never be empty and a caller can never end up with nowhere to store a key.
|
|
10
|
+
*/
|
|
11
|
+
export function defaultCandidates(platform = process.platform) {
|
|
12
|
+
switch (platform) {
|
|
13
|
+
case 'darwin':
|
|
14
|
+
return [createMacosKeychainBackend(), createFileFallbackBackend()];
|
|
15
|
+
case 'linux':
|
|
16
|
+
return [createLibsecretBackend(), createFileFallbackBackend()];
|
|
17
|
+
case 'win32':
|
|
18
|
+
return [createWindowsDpapiBackend(), createFileFallbackBackend()];
|
|
19
|
+
default:
|
|
20
|
+
return [createFileFallbackBackend()];
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/** Pick the first available backend from `candidates`, in order. Used only to decide where a NEW key
|
|
24
|
+
* gets written - see the KeyBackend interface doc for why reads don't use this. */
|
|
25
|
+
export async function selectAvailable(candidates) {
|
|
26
|
+
for (const backend of candidates) {
|
|
27
|
+
if (await backend.isAvailable())
|
|
28
|
+
return backend;
|
|
29
|
+
}
|
|
30
|
+
// Unreachable in practice: every platform list above ends with file-fallback, which is always
|
|
31
|
+
// available. Guarded anyway so a future edit that removes that guarantee fails loudly, not silently.
|
|
32
|
+
throw new Error('no key storage backend available (file-fallback should always be present)');
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=select.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"select.js","sourceRoot":"","sources":["../../../../src/identity/backends/select.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,yBAAyB,EAAE,MAAM,oBAAoB,CAAC;AAC/D,OAAO,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,EAAE,0BAA0B,EAAE,MAAM,qBAAqB,CAAC;AAEjE,OAAO,EAAE,yBAAyB,EAAE,MAAM,oBAAoB,CAAC;AAE/D;;;;;GAKG;AACH,MAAM,UAAU,iBAAiB,CAAC,WAA4B,OAAO,CAAC,QAAQ;IAC5E,QAAQ,QAAQ,EAAE,CAAC;QACjB,KAAK,QAAQ;YACX,OAAO,CAAC,0BAA0B,EAAE,EAAE,yBAAyB,EAAE,CAAC,CAAC;QACrE,KAAK,OAAO;YACV,OAAO,CAAC,sBAAsB,EAAE,EAAE,yBAAyB,EAAE,CAAC,CAAC;QACjE,KAAK,OAAO;YACV,OAAO,CAAC,yBAAyB,EAAE,EAAE,yBAAyB,EAAE,CAAC,CAAC;QACpE;YACE,OAAO,CAAC,yBAAyB,EAAE,CAAC,CAAC;IACzC,CAAC;AACH,CAAC;AAED;oFACoF;AACpF,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,UAAwB;IAC5D,KAAK,MAAM,OAAO,IAAI,UAAU,EAAE,CAAC;QACjC,IAAI,MAAM,OAAO,CAAC,WAAW,EAAE;YAAE,OAAO,OAAO,CAAC;IAClD,CAAC;IACD,8FAA8F;IAC9F,qGAAqG;IACrG,MAAM,IAAI,KAAK,CAAC,2EAA2E,CAAC,CAAC;AAC/F,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export class KeyBackendError extends Error {
|
|
2
|
+
backend;
|
|
3
|
+
operation;
|
|
4
|
+
constructor(backend, operation, message) {
|
|
5
|
+
super(`[${backend}] ${operation} failed: ${message}`);
|
|
6
|
+
this.backend = backend;
|
|
7
|
+
this.operation = operation;
|
|
8
|
+
this.name = 'KeyBackendError';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/** Fixed, non-secret names every real backend stores the identity under. There is exactly one agent
|
|
12
|
+
* identity per machine by design (see README) - a multi-identity store is a bigger feature than this
|
|
13
|
+
* package builds. */
|
|
14
|
+
export const SERVICE_NAME = 'botanary-mcp';
|
|
15
|
+
export const ACCOUNT_NAME = 'agent-identity';
|
|
16
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../../src/identity/backends/types.ts"],"names":[],"mappings":"AA6BA,MAAM,OAAO,eAAgB,SAAQ,KAAK;IAEtB;IACA;IAFlB,YACkB,OAAe,EACf,SAA+C,EAC/D,OAAe;QAEf,KAAK,CAAC,IAAI,OAAO,KAAK,SAAS,YAAY,OAAO,EAAE,CAAC,CAAC;QAJtC,YAAO,GAAP,OAAO,CAAQ;QACf,cAAS,GAAT,SAAS,CAAsC;QAI/D,IAAI,CAAC,IAAI,GAAG,iBAAiB,CAAC;IAChC,CAAC;CACF;AAED;;sBAEsB;AACtB,MAAM,CAAC,MAAM,YAAY,GAAG,cAAc,CAAC;AAC3C,MAAM,CAAC,MAAM,YAAY,GAAG,gBAAgB,CAAC"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { dirname, join } from 'node:path';
|
|
2
|
+
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { nodeExec } from './exec.js';
|
|
4
|
+
import { KeyBackendError } from './types.js';
|
|
5
|
+
import { botanaryMcpHome } from '../../paths.js';
|
|
6
|
+
import { redactSecret } from '../redact.js';
|
|
7
|
+
// Windows has no scriptable equivalent of macOS's `security`/Linux's `secret-tool` that round-trips a
|
|
8
|
+
// secret through the CLI: `cmdkey` can WRITE a Windows Credential Manager entry but deliberately gives
|
|
9
|
+
// no way to read the password back out (by design - Credential Manager entries are meant to be
|
|
10
|
+
// consumed by Windows components with a matching target, not read back by arbitrary scripts).
|
|
11
|
+
//
|
|
12
|
+
// The mechanism Credential Manager itself is built on IS scriptable, though: DPAPI
|
|
13
|
+
// (`ProtectedData.Protect`/`Unprotect`, `DataProtectionScope.CurrentUser`), reachable from PowerShell
|
|
14
|
+
// (built in on every Windows install, no extra dependency). This backend calls PowerShell to do only
|
|
15
|
+
// the encrypt/decrypt step (the secret is piped to it over stdin, never passed as an argument, so it
|
|
16
|
+
// never appears in argv/`Get-Process` output); Node does the file I/O, writing/reading the CIPHERTEXT
|
|
17
|
+
// (base64) - so the file on disk holds only DPAPI-encrypted bytes tied to the current Windows user's
|
|
18
|
+
// login, not a plaintext key. This is genuinely equivalent to what Credential Manager provides
|
|
19
|
+
// (OS-mediated, per-user secret confidentiality), and meaningfully stronger than the plain 0600 file
|
|
20
|
+
// fallback (whose POSIX permission bit is not something Node's `fs` can even express as a real ACL on
|
|
21
|
+
// NTFS) - we call it out as its own backend rather than folding it into file-fallback for that reason.
|
|
22
|
+
const PROTECT_SCRIPT = [
|
|
23
|
+
'Add-Type -AssemblyName System.Security',
|
|
24
|
+
'$text = [Console]::In.ReadToEnd()',
|
|
25
|
+
'$bytes = [System.Text.Encoding]::UTF8.GetBytes($text)',
|
|
26
|
+
'$protected = [System.Security.Cryptography.ProtectedData]::Protect($bytes, $null, [System.Security.Cryptography.DataProtectionScope]::CurrentUser)',
|
|
27
|
+
'[Console]::Out.Write([Convert]::ToBase64String($protected))',
|
|
28
|
+
].join('\n');
|
|
29
|
+
const UNPROTECT_SCRIPT = [
|
|
30
|
+
'Add-Type -AssemblyName System.Security',
|
|
31
|
+
'$b64 = [Console]::In.ReadToEnd()',
|
|
32
|
+
'$bytes = [Convert]::FromBase64String($b64)',
|
|
33
|
+
'$unprotected = [System.Security.Cryptography.ProtectedData]::Unprotect($bytes, $null, [System.Security.Cryptography.DataProtectionScope]::CurrentUser)',
|
|
34
|
+
'[Console]::Out.Write([System.Text.Encoding]::UTF8.GetString($unprotected))',
|
|
35
|
+
].join('\n');
|
|
36
|
+
const PS_ARGS = ['-NoProfile', '-NonInteractive', '-Command'];
|
|
37
|
+
function defaultFilePath() {
|
|
38
|
+
return join(botanaryMcpHome(), 'identity.dpapi');
|
|
39
|
+
}
|
|
40
|
+
export function createWindowsDpapiBackend(exec = nodeExec, filePath = defaultFilePath()) {
|
|
41
|
+
return {
|
|
42
|
+
id: 'windows-dpapi',
|
|
43
|
+
label: 'Windows credential storage (DPAPI, current user)',
|
|
44
|
+
async isAvailable() {
|
|
45
|
+
if (process.platform !== 'win32')
|
|
46
|
+
return false;
|
|
47
|
+
try {
|
|
48
|
+
const r = await exec('powershell', [...PS_ARGS, 'Write-Output ok']);
|
|
49
|
+
return r.code === 0 && r.stdout.trim() === 'ok';
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
async save(secret) {
|
|
56
|
+
const r = await exec('powershell', [...PS_ARGS, PROTECT_SCRIPT], { input: secret });
|
|
57
|
+
if (r.code !== 0 || !r.stdout.trim()) {
|
|
58
|
+
// PowerShell doesn't echo stdin back on error, but redact defensively anyway - see libsecret.ts.
|
|
59
|
+
throw new KeyBackendError('windows-dpapi', 'save', redactSecret(r.stderr || `exit ${r.code}`, secret));
|
|
60
|
+
}
|
|
61
|
+
await mkdir(dirname(filePath), { recursive: true, mode: 0o700 });
|
|
62
|
+
// The file holds DPAPI ciphertext, not the secret - 0600 here is extra caution, not the primary
|
|
63
|
+
// protection (DPAPI itself is what makes the content unreadable off this Windows account).
|
|
64
|
+
await writeFile(filePath, r.stdout.trim(), { encoding: 'utf8', mode: 0o600 });
|
|
65
|
+
},
|
|
66
|
+
async load() {
|
|
67
|
+
let ciphertext;
|
|
68
|
+
try {
|
|
69
|
+
ciphertext = (await readFile(filePath, 'utf8')).trim();
|
|
70
|
+
}
|
|
71
|
+
catch (e) {
|
|
72
|
+
if (e.code === 'ENOENT')
|
|
73
|
+
return null;
|
|
74
|
+
throw new KeyBackendError('windows-dpapi', 'load', e.message);
|
|
75
|
+
}
|
|
76
|
+
if (!ciphertext)
|
|
77
|
+
return null;
|
|
78
|
+
const r = await exec('powershell', [...PS_ARGS, UNPROTECT_SCRIPT], { input: ciphertext });
|
|
79
|
+
if (r.code !== 0) {
|
|
80
|
+
throw new KeyBackendError('windows-dpapi', 'load', r.stderr || `exit ${r.code}`);
|
|
81
|
+
}
|
|
82
|
+
return r.stdout.trim();
|
|
83
|
+
},
|
|
84
|
+
async delete() {
|
|
85
|
+
await rm(filePath, { force: true });
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=windows-dpapi.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"windows-dpapi.js","sourceRoot":"","sources":["../../../../src/identity/backends/windows-dpapi.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { hexToBytes } from 'viem';
|
|
3
|
+
// Crockford Base32: 32 symbols, excludes I/L/O/U so nothing is ambiguous when read aloud or typed by
|
|
4
|
+
// hand (no risk of confusing 0/O, 1/I/L), case-insensitive by convention. This is the same alphabet
|
|
5
|
+
// family used by ULIDs and plenty of other human-facing codes for exactly this reason.
|
|
6
|
+
const ALPHABET = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
|
|
7
|
+
/** Encode `bytes` as Crockford Base32. Not meant to round-trip (no decoder here, and slicing the
|
|
8
|
+
* result to a shorter display length - see below - deliberately drops the tail) - this is a display
|
|
9
|
+
* transform for short human-facing codes, not a data-interchange encoding. */
|
|
10
|
+
export function crockfordBase32(bytes) {
|
|
11
|
+
let bitBuffer = 0;
|
|
12
|
+
let bitCount = 0;
|
|
13
|
+
let output = '';
|
|
14
|
+
for (const byte of bytes) {
|
|
15
|
+
bitBuffer = (bitBuffer << 8) | byte;
|
|
16
|
+
bitCount += 8;
|
|
17
|
+
while (bitCount >= 5) {
|
|
18
|
+
output += ALPHABET[(bitBuffer >>> (bitCount - 5)) & 0x1f];
|
|
19
|
+
bitCount -= 5;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
if (bitCount > 0) {
|
|
23
|
+
output += ALPHABET[(bitBuffer << (5 - bitCount)) & 0x1f];
|
|
24
|
+
}
|
|
25
|
+
return output;
|
|
26
|
+
}
|
|
27
|
+
/** Bytes of address fingerprint / pairing nonce - chosen for "short to type" over "cryptographically
|
|
28
|
+
* weighty", since neither is a secret (see README's "why intercepting the code alone grants nothing").
|
|
29
|
+
* FINGERPRINT_BYTES: ~24 bits, purely a human recognition aid - the full address is always available
|
|
30
|
+
* via get_identity for exact verification, so collisions here cost nothing but a moment's "hm, two
|
|
31
|
+
* agents look similar", not a security property.
|
|
32
|
+
* NONCE_BYTES: ~40 bits of fresh randomness per pairing code, the thing that actually makes each code
|
|
33
|
+
* single-use in practice (combined with the short TTL - see pairing-code.ts). */
|
|
34
|
+
export const FINGERPRINT_BYTES = 3;
|
|
35
|
+
export const NONCE_BYTES = 5;
|
|
36
|
+
/** A short, deterministic, human-typable id for an address - always the same for the same address.
|
|
37
|
+
* Purely a display/recognition aid (see module doc); the address itself is the real identifier. */
|
|
38
|
+
export function deriveFingerprint(address) {
|
|
39
|
+
const bytes = hexToBytes(address);
|
|
40
|
+
return crockfordBase32(bytes.slice(0, FINGERPRINT_BYTES));
|
|
41
|
+
}
|
|
42
|
+
/** A fresh random nonce for one pairing code. Never derived from the key - pure randomness, so a new
|
|
43
|
+
* call can never be predicted from a previous one. */
|
|
44
|
+
export function generatePairingNonce() {
|
|
45
|
+
return crockfordBase32(randomBytes(NONCE_BYTES));
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=fingerprint.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fingerprint.js","sourceRoot":"","sources":["../../../src/identity/fingerprint.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,UAAU,EAAE,MAAM,MAAM,CAAC;AAGlC,qGAAqG;AACrG,oGAAoG;AACpG,uFAAuF;AACvF,MAAM,QAAQ,GAAG,kCAAkC,CAAC;AAEpD;;+EAE+E;AAC/E,MAAM,UAAU,eAAe,CAAC,KAAiB;IAC/C,IAAI,SAAS,GAAG,CAAC,CAAC;IAClB,IAAI,QAAQ,GAAG,CAAC,CAAC;IACjB,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,SAAS,GAAG,CAAC,SAAS,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC;QACpC,QAAQ,IAAI,CAAC,CAAC;QACd,OAAO,QAAQ,IAAI,CAAC,EAAE,CAAC;YACrB,MAAM,IAAI,QAAQ,CAAC,CAAC,SAAS,KAAK,CAAC,QAAQ,GAAG,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;YAC1D,QAAQ,IAAI,CAAC,CAAC;QAChB,CAAC;IACH,CAAC;IACD,IAAI,QAAQ,GAAG,CAAC,EAAE,CAAC;QACjB,MAAM,IAAI,QAAQ,CAAC,CAAC,SAAS,IAAI,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IAC3D,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;;kFAMkF;AAClF,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,CAAC;AACnC,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,CAAC;AAE7B;oGACoG;AACpG,MAAM,UAAU,iBAAiB,CAAC,OAAY;IAC5C,MAAM,KAAK,GAAG,UAAU,CAAC,OAAO,CAAC,CAAC;IAClC,OAAO,eAAe,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,iBAAiB,CAAC,CAAC,CAAC;AAC5D,CAAC;AAED;uDACuD;AACvD,MAAM,UAAU,oBAAoB;IAClC,OAAO,eAAe,CAAC,WAAW,CAAC,WAAW,CAAC,CAAC,CAAC;AACnD,CAAC"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts';
|
|
2
|
+
/** A fresh, cryptographically random secp256k1 keypair. Never logged, never returned from any tool -
|
|
3
|
+
* see store.ts, which is the only place this function is called from. */
|
|
4
|
+
export function generateKeypair() {
|
|
5
|
+
const privateKey = generatePrivateKey();
|
|
6
|
+
return keypairFromPrivateKey(privateKey);
|
|
7
|
+
}
|
|
8
|
+
export function keypairFromPrivateKey(privateKey) {
|
|
9
|
+
const account = privateKeyToAccount(privateKey);
|
|
10
|
+
return { privateKey, address: account.address, publicKey: account.publicKey };
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* EIP-191 personal-message signature ("\x19Ethereum Signed Message:\n" + len + message) - the standard
|
|
14
|
+
* proof-of-possession primitive: "I hold the private key behind this address" without exposing it.
|
|
15
|
+
* Available for a caller that genuinely wants to sign arbitrary human-readable text (`IdentityStore.sign`,
|
|
16
|
+
* used directly by a couple of tests below). NOT what pairing/session/spend signing uses - see
|
|
17
|
+
* `signHashWithPrivateKey`.
|
|
18
|
+
*/
|
|
19
|
+
export async function signMessageWithPrivateKey(privateKey, message) {
|
|
20
|
+
const account = privateKeyToAccount(privateKey);
|
|
21
|
+
return account.signMessage({ message });
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Raw ECDSA signature over an EXACT 32-byte hash - no EIP-191 prefix, no re-hashing. This is what the
|
|
25
|
+
* backend actually verifies for every proof-of-possession this package produces:
|
|
26
|
+
* `agents.service.ts`'s `pair()`/`mintSession()` both recover via `recoverPublicKey({ hash, signature })`
|
|
27
|
+
* over the raw hash, and Smart Sessions' USE-mode envelope (`AgentRuntime.spendUnderGrant`) packs a raw
|
|
28
|
+
* signature next to the permissionId for the SAME reason - neither one wraps or re-hashes first.
|
|
29
|
+
*
|
|
30
|
+
* `signMessageWithPrivateKey` (EIP-191) is the wrong primitive for any of the three: signing a hash
|
|
31
|
+
* through it re-hashes `keccak256("\x19Ethereum Signed Message:\n32" + hash)` and signs THAT instead,
|
|
32
|
+
* producing a signature that recovers to a completely different (still valid-looking) result - wrong
|
|
33
|
+
* silently, never with an error, which is exactly the shape of bug this package's whole reason for
|
|
34
|
+
* existing (verifiable proof-of-possession) cannot afford.
|
|
35
|
+
*/
|
|
36
|
+
export async function signHashWithPrivateKey(privateKey, hash) {
|
|
37
|
+
const account = privateKeyToAccount(privateKey);
|
|
38
|
+
return account.sign({ hash });
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=keypair.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keypair.js","sourceRoot":"","sources":["../../../src/identity/keypair.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAmBxE;0EAC0E;AAC1E,MAAM,UAAU,eAAe;IAC7B,MAAM,UAAU,GAAG,kBAAkB,EAAE,CAAC;IACxC,OAAO,qBAAqB,CAAC,UAAU,CAAC,CAAC;AAC3C,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,UAAe;IACnD,MAAM,OAAO,GAAG,mBAAmB,CAAC,UAAU,CAAC,CAAC;IAChD,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,SAAS,EAAE,OAAO,CAAC,SAAS,EAAE,CAAC;AAChF,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,yBAAyB,CAAC,UAAe,EAAE,OAAe;IAC9E,MAAM,OAAO,GAAG,mBAAmB,CAAC,UAAU,CAAC,CAAC;IAChD,OAAO,OAAO,CAAC,WAAW,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC;AAC1C,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAAC,UAAe,EAAE,IAAS;IACrE,MAAM,OAAO,GAAG,mBAAmB,CAAC,UAAU,CAAC,CAAC;IAChD,OAAO,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC;AAChC,CAAC"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { generatePairingNonce } from './fingerprint.js';
|
|
2
|
+
/** 10 minutes: long enough to alt-tab to the Botanary app and type ~13 characters, short enough that a
|
|
3
|
+
* code nobody redeemed stops being worth anything on its own quickly. See README for the full
|
|
4
|
+
* "why intercepting the code alone grants nothing" argument - the TTL is one layer of it, not the
|
|
5
|
+
* whole thing. */
|
|
6
|
+
export const DEFAULT_PAIRING_CODE_TTL_MS = 10 * 60 * 1000;
|
|
7
|
+
/**
|
|
8
|
+
* Owns exactly one pairing attempt at a time for this process, keyed off a fixed address fingerprint.
|
|
9
|
+
* `current()` is idempotent while the code is still valid - Flow 7d step 1 is "the agent ... shows the
|
|
10
|
+
* short code, and waits", which only makes sense if the code stays put while the owner is busy typing
|
|
11
|
+
* it, rather than rotating out from under them on every tool call. It mints automatically once the
|
|
12
|
+
* previous code expires, and `regenerate()` lets the owner (via the agent) burn a code early - e.g. if
|
|
13
|
+
* they suspect it was seen by someone else - without waiting out the TTL.
|
|
14
|
+
*
|
|
15
|
+
* Deliberately in-memory only: a pairing code is not part of the identity (see store.ts) and is not
|
|
16
|
+
* meant to survive a restart - a fresh process showing a fresh code is the correct behavior, not a bug.
|
|
17
|
+
*/
|
|
18
|
+
export class PairingCodeManager {
|
|
19
|
+
#fingerprint;
|
|
20
|
+
#ttlMs;
|
|
21
|
+
#now;
|
|
22
|
+
#nonce;
|
|
23
|
+
#current = null;
|
|
24
|
+
constructor(fingerprint, options) {
|
|
25
|
+
this.#fingerprint = fingerprint;
|
|
26
|
+
this.#ttlMs = options?.ttlMs ?? DEFAULT_PAIRING_CODE_TTL_MS;
|
|
27
|
+
this.#now = options?.now ?? Date.now;
|
|
28
|
+
this.#nonce = options?.nonce ?? generatePairingNonce;
|
|
29
|
+
}
|
|
30
|
+
/** The active code, minting a fresh one if there is none yet or the previous one expired. */
|
|
31
|
+
current() {
|
|
32
|
+
const t = this.#now();
|
|
33
|
+
if (!this.#current || this.#current.expiresAt <= t) {
|
|
34
|
+
this.#mint(t);
|
|
35
|
+
}
|
|
36
|
+
return this.#toPublic();
|
|
37
|
+
}
|
|
38
|
+
/** Always mints a fresh code, invalidating whatever was active. */
|
|
39
|
+
regenerate() {
|
|
40
|
+
this.#mint(this.#now());
|
|
41
|
+
return this.#toPublic();
|
|
42
|
+
}
|
|
43
|
+
#mint(t) {
|
|
44
|
+
this.#current = { nonce: this.#nonce(), issuedAt: t, expiresAt: t + this.#ttlMs };
|
|
45
|
+
}
|
|
46
|
+
#toPublic() {
|
|
47
|
+
const c = this.#current;
|
|
48
|
+
/* istanbul ignore next -- #mint always runs first in both call sites above */
|
|
49
|
+
if (!c)
|
|
50
|
+
throw new Error('unreachable: no pairing code minted');
|
|
51
|
+
return {
|
|
52
|
+
code: `${this.#fingerprint}-${c.nonce}`,
|
|
53
|
+
fingerprint: this.#fingerprint,
|
|
54
|
+
nonce: c.nonce,
|
|
55
|
+
issuedAt: new Date(c.issuedAt).toISOString(),
|
|
56
|
+
expiresAt: new Date(c.expiresAt).toISOString(),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=pairing-code.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pairing-code.js","sourceRoot":"","sources":["../../../src/identity/pairing-code.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAExD;;;mBAGmB;AACnB,MAAM,CAAC,MAAM,2BAA2B,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AAa1D;;;;;;;;;;GAUG;AACH,MAAM,OAAO,kBAAkB;IACpB,YAAY,CAAS;IACrB,MAAM,CAAS;IACf,IAAI,CAAe;IACnB,MAAM,CAAe;IAC9B,QAAQ,GAAkE,IAAI,CAAC;IAE/E,YACE,WAAmB,EACnB,OAMC;QAED,IAAI,CAAC,YAAY,GAAG,WAAW,CAAC;QAChC,IAAI,CAAC,MAAM,GAAG,OAAO,EAAE,KAAK,IAAI,2BAA2B,CAAC;QAC5D,IAAI,CAAC,IAAI,GAAG,OAAO,EAAE,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC;QACrC,IAAI,CAAC,MAAM,GAAG,OAAO,EAAE,KAAK,IAAI,oBAAoB,CAAC;IACvD,CAAC;IAED,6FAA6F;IAC7F,OAAO;QACL,MAAM,CAAC,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;QACtB,IAAI,CAAC,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,QAAQ,CAAC,SAAS,IAAI,CAAC,EAAE,CAAC;YACnD,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QAChB,CAAC;QACD,OAAO,IAAI,CAAC,SAAS,EAAE,CAAC;IAC1B,CAAC;IAED,mEAAmE;IACnE,UAAU;QACR,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;QACxB,OAAO,IAAI,CAAC,SAAS,EAAE,CAAC;IAC1B,CAAC;IAED,KAAK,CAAC,CAAS;QACb,IAAI,CAAC,QAAQ,GAAG,EAAE,KAAK,EAAE,IAAI,CAAC,MAAM,EAAE,EAAE,QAAQ,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;IACpF,CAAC;IAED,SAAS;QACP,MAAM,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC;QACxB,8EAA8E;QAC9E,IAAI,CAAC,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,qCAAqC,CAAC,CAAC;QAC/D,OAAO;YACL,IAAI,EAAE,GAAG,IAAI,CAAC,YAAY,IAAI,CAAC,CAAC,KAAK,EAAE;YACvC,WAAW,EAAE,IAAI,CAAC,YAAY;YAC9B,KAAK,EAAE,CAAC,CAAC,KAAK;YACd,QAAQ,EAAE,IAAI,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,WAAW,EAAE;YAC5C,SAAS,EAAE,IAAI,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,WAAW,EAAE;SAC/C,CAAC;IACJ,CAAC;CACF"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Defense in depth for the one invariant this whole package exists to protect: the private key must
|
|
2
|
+
// never appear in anything that leaves this process's control - a log line, an error message, a
|
|
3
|
+
// crash report. Nothing here is the PRIMARY guarantee (that's the type boundary in store.ts, which
|
|
4
|
+
// never returns the key from any public method) - this is the belt under that belt, in case a raw
|
|
5
|
+
// error from a shelled-out OS command (security/secret-tool/powershell) ever happened to echo back
|
|
6
|
+
// something it was given on argv or stdin.
|
|
7
|
+
/**
|
|
8
|
+
* Replace every exact occurrence of `secret` in `text` with a fixed placeholder. Safe to call with an
|
|
9
|
+
* empty/undefined secret (a no-op) so call sites never need their own guard.
|
|
10
|
+
*/
|
|
11
|
+
export function redactSecret(text, secret) {
|
|
12
|
+
if (!secret)
|
|
13
|
+
return text;
|
|
14
|
+
return text.split(secret).join('«redacted»');
|
|
15
|
+
}
|
|
16
|
+
/** Same, but for a value that might not be a string yet (an Error, an unknown thrown value, ...). */
|
|
17
|
+
export function redactUnknown(value, secret) {
|
|
18
|
+
const text = value instanceof Error ? (value.stack ?? value.message) : String(value);
|
|
19
|
+
return redactSecret(text, secret);
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=redact.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redact.js","sourceRoot":"","sources":["../../../src/identity/redact.ts"],"names":[],"mappings":"AAAA,oGAAoG;AACpG,gGAAgG;AAChG,mGAAmG;AACnG,kGAAkG;AAClG,mGAAmG;AACnG,2CAA2C;AAE3C;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,IAAY,EAAE,MAAiC;IAC1E,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAC;IACzB,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;AAC/C,CAAC;AAED,qGAAqG;AACrG,MAAM,UAAU,aAAa,CAAC,KAAc,EAAE,MAAiC;IAC7E,MAAM,IAAI,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACrF,OAAO,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;AACpC,CAAC"}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import { publicMetadataPath } from '../paths.js';
|
|
4
|
+
import { deriveFingerprint } from './fingerprint.js';
|
|
5
|
+
import { generateKeypair, keypairFromPrivateKey, signHashWithPrivateKey, signMessageWithPrivateKey, } from './keypair.js';
|
|
6
|
+
import { redactSecret } from './redact.js';
|
|
7
|
+
export class IdentityCorruptError extends Error {
|
|
8
|
+
constructor(message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = 'IdentityCorruptError';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export class IdentityMissingKeyError extends Error {
|
|
14
|
+
constructor(message) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = 'IdentityMissingKeyError';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export class IdentityMismatchError extends Error {
|
|
20
|
+
constructor(message) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.name = 'IdentityMismatchError';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The one boundary the private key ever crosses out of storage. Orchestrates:
|
|
27
|
+
* - the non-secret public metadata file (address/publicKey/fingerprint/createdAt/backend) - read on
|
|
28
|
+
* every `ensure()`/`get_identity` call, which therefore NEVER touches the keychain;
|
|
29
|
+
* - the KeyBackend candidates (see backends/) - only touched by `sign()` (loads) and `#createNew()`/
|
|
30
|
+
* `forget()` (save/delete). No method here caches the private key on `this`; it is read fresh for
|
|
31
|
+
* each `sign()` call and let go out of scope immediately after.
|
|
32
|
+
*
|
|
33
|
+
* Two invariants worth stating explicitly, because they are the whole security story of this class:
|
|
34
|
+
* 1. A new key is written to exactly ONE backend - the first available candidate - never a second
|
|
35
|
+
* "just in case" copy anywhere else. See #saveToFirstAvailable.
|
|
36
|
+
* 2. Reads never invent a replacement key. If metadata exists but no backend holds the matching
|
|
37
|
+
* secret (deleted - see acceptance test "delete the agent's key on its own machine"), or the
|
|
38
|
+
* backend's secret doesn't match the metadata's address, this throws rather than silently
|
|
39
|
+
* generating a new identity that would orphan whatever grant the old address held.
|
|
40
|
+
*/
|
|
41
|
+
export class IdentityStore {
|
|
42
|
+
#candidates;
|
|
43
|
+
#metadataPath;
|
|
44
|
+
#keypairFactory;
|
|
45
|
+
constructor(candidates, options) {
|
|
46
|
+
if (candidates.length === 0)
|
|
47
|
+
throw new Error('IdentityStore needs at least one KeyBackend candidate');
|
|
48
|
+
this.#candidates = candidates;
|
|
49
|
+
this.#metadataPath = options?.metadataPath ?? publicMetadataPath();
|
|
50
|
+
this.#keypairFactory = options?.keypairFactory ?? generateKeypair;
|
|
51
|
+
}
|
|
52
|
+
async hasIdentity() {
|
|
53
|
+
return (await this.#readMetadata()) !== null;
|
|
54
|
+
}
|
|
55
|
+
/** Load the existing identity, or create one on first run. */
|
|
56
|
+
async ensure() {
|
|
57
|
+
const metadata = await this.#readMetadata();
|
|
58
|
+
if (metadata)
|
|
59
|
+
return toIdentity(metadata);
|
|
60
|
+
return this.#createNew();
|
|
61
|
+
}
|
|
62
|
+
/** Sign `message` as an EIP-191 personal message with the agent's private key. One of two methods on
|
|
63
|
+
* this class that ever hold the key in a local variable (the other is `signHash`). */
|
|
64
|
+
async sign(message) {
|
|
65
|
+
const metadata = await this.#readMetadata();
|
|
66
|
+
if (!metadata) {
|
|
67
|
+
throw new Error('no agent identity yet - call ensure() (or the get_identity tool) first');
|
|
68
|
+
}
|
|
69
|
+
const privateKey = await this.#loadSecretForSigning(metadata);
|
|
70
|
+
return signMessageWithPrivateKey(privateKey, message);
|
|
71
|
+
}
|
|
72
|
+
/** Sign a 32-byte hash directly - raw ECDSA, no EIP-191 prefix. What `pair()`, `mintSession()`, and
|
|
73
|
+
* `spendUnderGrant()`'s USE-mode envelope all actually need - see `signHashWithPrivateKey`'s doc
|
|
74
|
+
* comment for why `sign()` above would silently produce the WRONG signature for any of them. */
|
|
75
|
+
async signHash(hash) {
|
|
76
|
+
const metadata = await this.#readMetadata();
|
|
77
|
+
if (!metadata) {
|
|
78
|
+
throw new Error('no agent identity yet - call ensure() (or the get_identity tool) first');
|
|
79
|
+
}
|
|
80
|
+
const privateKey = await this.#loadSecretForSigning(metadata);
|
|
81
|
+
return signHashWithPrivateKey(privateKey, hash);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Deletes the private key from EVERY backend candidate (not only whichever one metadata says holds
|
|
85
|
+
* it - a clean slate regardless of any drift) and clears the public metadata. This is the mechanism
|
|
86
|
+
* behind acceptance test §5-46: after this resolves, `hasIdentity()` is false and `sign()` throws -
|
|
87
|
+
* there is nothing left, anywhere on this machine, that can produce a signature for the old address.
|
|
88
|
+
* The grant a backend may still have on file for that address is untouched by this call - it was
|
|
89
|
+
* never this machine's to revoke (see README).
|
|
90
|
+
*/
|
|
91
|
+
async forget() {
|
|
92
|
+
const errors = [];
|
|
93
|
+
for (const backend of this.#candidates) {
|
|
94
|
+
try {
|
|
95
|
+
await backend.delete();
|
|
96
|
+
}
|
|
97
|
+
catch (e) {
|
|
98
|
+
errors.push(e);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
await rm(this.#metadataPath, { force: true });
|
|
102
|
+
if (errors.length > 0 && errors.length === this.#candidates.length) {
|
|
103
|
+
throw new AggregateError(errors, 'failed to delete the agent key from every backend');
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Test-only accessor, so `no-leak.spec.ts` can assert that the exact secret never appears in an
|
|
108
|
+
* outbound request. GUARDED AT RUNTIME rather than by this comment: the whole point of this package
|
|
109
|
+
* is that the key never leaves the machine, and a doc-comment "never used in production" on a public
|
|
110
|
+
* method that hands out the raw private key is an instruction away from being untrue - an injected
|
|
111
|
+
* prompt reaching the runtime would only have to call it.
|
|
112
|
+
*/
|
|
113
|
+
async exportForTest() {
|
|
114
|
+
if (!process.env.VITEST && process.env.NODE_ENV !== 'test') {
|
|
115
|
+
throw new Error('exportForTest is available only under the test runner.');
|
|
116
|
+
}
|
|
117
|
+
const metadata = await this.#readMetadata();
|
|
118
|
+
if (!metadata) {
|
|
119
|
+
throw new Error('no agent identity yet');
|
|
120
|
+
}
|
|
121
|
+
const privateKey = await this.#loadSecretForSigning(metadata);
|
|
122
|
+
return { privateKey };
|
|
123
|
+
}
|
|
124
|
+
async #createNew() {
|
|
125
|
+
const keypair = this.#keypairFactory();
|
|
126
|
+
const createdAt = new Date().toISOString();
|
|
127
|
+
const payload = { v: 1, privateKey: keypair.privateKey, createdAt };
|
|
128
|
+
const backend = await this.#saveToFirstAvailable(payload, keypair.privateKey);
|
|
129
|
+
const metadata = {
|
|
130
|
+
v: 1,
|
|
131
|
+
address: keypair.address,
|
|
132
|
+
publicKey: keypair.publicKey,
|
|
133
|
+
fingerprint: deriveFingerprint(keypair.address),
|
|
134
|
+
createdAt,
|
|
135
|
+
backend: backend.id,
|
|
136
|
+
};
|
|
137
|
+
await this.#writeMetadata(metadata);
|
|
138
|
+
return toIdentity(metadata);
|
|
139
|
+
}
|
|
140
|
+
/** Tries candidates in order and writes to the FIRST one that is available and accepts the save -
|
|
141
|
+
* and stops there. Only advances to the next candidate if the previous one actually failed (probe
|
|
142
|
+
* said available, save still errored), never as a redundant backup of a successful save. */
|
|
143
|
+
async #saveToFirstAvailable(payload, privateKey) {
|
|
144
|
+
const secret = JSON.stringify(payload);
|
|
145
|
+
let lastError;
|
|
146
|
+
for (const backend of this.#candidates) {
|
|
147
|
+
if (!(await backend.isAvailable()))
|
|
148
|
+
continue;
|
|
149
|
+
try {
|
|
150
|
+
await backend.save(secret);
|
|
151
|
+
return backend;
|
|
152
|
+
}
|
|
153
|
+
catch (e) {
|
|
154
|
+
lastError = e;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const detail = redactSecret(lastError instanceof Error ? lastError.message : String(lastError), privateKey);
|
|
158
|
+
throw new Error(`could not save the agent key to any storage backend: ${detail}`);
|
|
159
|
+
}
|
|
160
|
+
/** Sweeps every candidate in priority order for an existing secret. Distinguishes "nothing here"
|
|
161
|
+
* (keep going) from "this backend errored" (also keep going, but the error is kept so a fully-empty
|
|
162
|
+
* sweep can explain itself instead of just saying "not found" when something was actually broken). */
|
|
163
|
+
async #loadFromAnyBackend() {
|
|
164
|
+
const backendErrors = [];
|
|
165
|
+
for (const backend of this.#candidates) {
|
|
166
|
+
let raw;
|
|
167
|
+
try {
|
|
168
|
+
raw = await backend.load();
|
|
169
|
+
}
|
|
170
|
+
catch (e) {
|
|
171
|
+
backendErrors.push({ backendId: backend.id, message: e instanceof Error ? e.message : String(e) });
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (raw === null)
|
|
175
|
+
continue;
|
|
176
|
+
const payload = parseSecret(raw);
|
|
177
|
+
if (payload)
|
|
178
|
+
return { found: { backend, payload }, backendErrors };
|
|
179
|
+
}
|
|
180
|
+
return { found: null, backendErrors };
|
|
181
|
+
}
|
|
182
|
+
async #loadSecretForSigning(metadata) {
|
|
183
|
+
const { found, backendErrors } = await this.#loadFromAnyBackend();
|
|
184
|
+
if (!found) {
|
|
185
|
+
const hint = backendErrors.length > 0
|
|
186
|
+
? ` (${backendErrors.map((e) => `${e.backendId}: ${e.message}`).join('; ')})`
|
|
187
|
+
: '';
|
|
188
|
+
throw new IdentityMissingKeyError(`identity metadata exists (address ${metadata.address}) but no backend holds its private key${hint} - ` +
|
|
189
|
+
'it may have been deleted (directly via the OS keychain, or via "botanary-mcp forget"). Run ' +
|
|
190
|
+
'"botanary-mcp forget" to clear stale metadata and let a fresh identity be created on next use, ' +
|
|
191
|
+
'or restore the key if you still have it.');
|
|
192
|
+
}
|
|
193
|
+
const keypair = keypairFromPrivateKey(found.payload.privateKey);
|
|
194
|
+
if (keypair.address.toLowerCase() !== metadata.address.toLowerCase()) {
|
|
195
|
+
throw new IdentityMismatchError(`the key found in ${found.backend.label} does not match the stored identity (expected ` +
|
|
196
|
+
`${metadata.address}, found a key for ${keypair.address}) - refusing to sign with it.`);
|
|
197
|
+
}
|
|
198
|
+
return keypair.privateKey;
|
|
199
|
+
}
|
|
200
|
+
async #readMetadata() {
|
|
201
|
+
let raw;
|
|
202
|
+
try {
|
|
203
|
+
raw = await readFile(this.#metadataPath, 'utf8');
|
|
204
|
+
}
|
|
205
|
+
catch (e) {
|
|
206
|
+
if (e.code === 'ENOENT')
|
|
207
|
+
return null;
|
|
208
|
+
throw e;
|
|
209
|
+
}
|
|
210
|
+
const parsed = parseMetadata(raw);
|
|
211
|
+
if (parsed)
|
|
212
|
+
return parsed;
|
|
213
|
+
throw new IdentityCorruptError(`${this.#metadataPath} exists but could not be read as agent identity metadata. Back it up and ` +
|
|
214
|
+
'remove it (or run "botanary-mcp forget") before continuing - refusing to silently overwrite it ' +
|
|
215
|
+
'with a new identity.');
|
|
216
|
+
}
|
|
217
|
+
async #writeMetadata(metadata) {
|
|
218
|
+
await mkdir(dirname(this.#metadataPath), { recursive: true });
|
|
219
|
+
await writeFile(this.#metadataPath, `${JSON.stringify(metadata, null, 2)}\n`, 'utf8');
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
function toIdentity(metadata) {
|
|
223
|
+
return {
|
|
224
|
+
address: metadata.address,
|
|
225
|
+
publicKey: metadata.publicKey,
|
|
226
|
+
fingerprint: metadata.fingerprint,
|
|
227
|
+
createdAt: metadata.createdAt,
|
|
228
|
+
backend: metadata.backend,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
function parseSecret(raw) {
|
|
232
|
+
try {
|
|
233
|
+
const parsed = JSON.parse(raw);
|
|
234
|
+
return parsed && parsed.v === 1 && typeof parsed.privateKey === 'string' ? parsed : null;
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
function parseMetadata(raw) {
|
|
241
|
+
try {
|
|
242
|
+
const parsed = JSON.parse(raw);
|
|
243
|
+
return parsed && parsed.v === 1 && typeof parsed.address === 'string' ? parsed : null;
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.js","sourceRoot":"","sources":["../../../src/identity/store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/identity/types.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* Root directory for everything this package writes locally: the non-secret public identity metadata
|
|
5
|
+
* (identity.json) always, plus - only when no OS keychain/libsecret/DPAPI is available - the file
|
|
6
|
+
* fallback's 0600 secret file. `BOTANARY_MCP_HOME` is an escape hatch for tests and for running this
|
|
7
|
+
* server inside a container/CI box with an isolated home; it is not something a normal install needs
|
|
8
|
+
* to set.
|
|
9
|
+
*/
|
|
10
|
+
export function botanaryMcpHome() {
|
|
11
|
+
return process.env.BOTANARY_MCP_HOME || join(homedir(), '.botanary-mcp');
|
|
12
|
+
}
|
|
13
|
+
/** Public, non-secret identity metadata: address, public key, fingerprint, createdAt, which backend
|
|
14
|
+
* holds the private key. Safe to read on every tool call - loading it never touches the keychain. */
|
|
15
|
+
export function publicMetadataPath() {
|
|
16
|
+
return join(botanaryMcpHome(), 'identity.json');
|
|
17
|
+
}
|
|
18
|
+
/** Where the file-fallback backend stores the secret when no OS keychain is available. 0600. */
|
|
19
|
+
export function fileFallbackSecretPath() {
|
|
20
|
+
return join(botanaryMcpHome(), 'identity.key');
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=paths.js.map
|