@shroud-fi/agent-runtime 0.1.4 → 0.1.6
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/package.json +11 -8
- package/src/agent.ts +829 -0
- package/src/browser.ts +126 -0
- package/src/constants.ts +20 -0
- package/src/contacts.ts +174 -0
- package/src/errors.ts +205 -0
- package/src/factory.ts +51 -0
- package/src/index.ts +57 -0
- package/src/types.ts +176 -0
- package/tsconfig.json +9 -0
- package/dist/cjs/agent.d.ts.map +0 -1
- package/dist/cjs/agent.js.map +0 -1
- package/dist/cjs/browser.d.ts.map +0 -1
- package/dist/cjs/browser.js.map +0 -1
- package/dist/cjs/constants.d.ts.map +0 -1
- package/dist/cjs/constants.js.map +0 -1
- package/dist/cjs/contacts.d.ts.map +0 -1
- package/dist/cjs/contacts.js.map +0 -1
- package/dist/cjs/errors.d.ts.map +0 -1
- package/dist/cjs/errors.js.map +0 -1
- package/dist/cjs/factory.d.ts.map +0 -1
- package/dist/cjs/factory.js.map +0 -1
- package/dist/cjs/index.d.ts.map +0 -1
- package/dist/cjs/index.js.map +0 -1
- package/dist/cjs/types.d.ts.map +0 -1
- package/dist/cjs/types.js.map +0 -1
- package/dist/esm/agent.d.ts.map +0 -1
- package/dist/esm/agent.js.map +0 -1
- package/dist/esm/browser.d.ts.map +0 -1
- package/dist/esm/browser.js.map +0 -1
- package/dist/esm/constants.d.ts.map +0 -1
- package/dist/esm/constants.js.map +0 -1
- package/dist/esm/contacts.d.ts.map +0 -1
- package/dist/esm/contacts.js.map +0 -1
- package/dist/esm/errors.d.ts.map +0 -1
- package/dist/esm/errors.js.map +0 -1
- package/dist/esm/factory.d.ts.map +0 -1
- package/dist/esm/factory.js.map +0 -1
- package/dist/esm/index.d.ts.map +0 -1
- package/dist/esm/index.js.map +0 -1
- package/dist/esm/types.d.ts.map +0 -1
- package/dist/esm/types.js.map +0 -1
- package/dist/tsconfig.cjs.tsbuildinfo +0 -1
- package/dist/tsconfig.esm.tsbuildinfo +0 -1
- package/dist/tsconfig.tsbuildinfo +0 -1
package/src/browser.ts
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createShroudAgentFromBrowserWallet — browser / EIP-1193 wallet entry point.
|
|
3
|
+
*
|
|
4
|
+
* Flow:
|
|
5
|
+
* 1. Ask the wallet to sign BROWSER_KEY_MESSAGE via personal_sign.
|
|
6
|
+
* 2. HKDF the signature bytes to a deterministic 32-byte master seed.
|
|
7
|
+
* 3. Delegate to createShroudAgent.
|
|
8
|
+
*
|
|
9
|
+
* The same wallet always produces the same agent identity — no localStorage,
|
|
10
|
+
* no persistence. Bumping BROWSER_KEY_MESSAGE forks the identity space.
|
|
11
|
+
*
|
|
12
|
+
* Privacy invariants:
|
|
13
|
+
* - No console.* anywhere.
|
|
14
|
+
* - The signature bytes never appear in error messages or stack frames.
|
|
15
|
+
* - The derived master seed is passed to createShroudAgent and immediately
|
|
16
|
+
* consumed by createAgentIdentity; no caller-visible reference is retained.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { Address } from 'viem';
|
|
20
|
+
import { hkdf } from '@noble/hashes/hkdf';
|
|
21
|
+
import { sha256 } from '@noble/hashes/sha256';
|
|
22
|
+
import type { FinalityLevel } from '@shroud-fi/scanning';
|
|
23
|
+
import type { ShroudFiTransport } from '@shroud-fi/transport';
|
|
24
|
+
import { createShroudAgent } from './factory.js';
|
|
25
|
+
import type { ShroudAgent } from './agent.js';
|
|
26
|
+
import {
|
|
27
|
+
BROWSER_KEY_MESSAGE,
|
|
28
|
+
BROWSER_MASTER_SEED_LENGTH,
|
|
29
|
+
HKDF_BROWSER_INFO,
|
|
30
|
+
HKDF_BROWSER_SALT,
|
|
31
|
+
} from './constants.js';
|
|
32
|
+
import {
|
|
33
|
+
BrowserWalletSignatureRejectedError,
|
|
34
|
+
MissingBrowserWalletError,
|
|
35
|
+
} from './errors.js';
|
|
36
|
+
import type { BrowserWalletAdapter } from './types.js';
|
|
37
|
+
|
|
38
|
+
export {
|
|
39
|
+
BROWSER_KEY_MESSAGE,
|
|
40
|
+
HKDF_BROWSER_SALT,
|
|
41
|
+
HKDF_BROWSER_INFO,
|
|
42
|
+
} from './constants.js';
|
|
43
|
+
|
|
44
|
+
export interface CreateShroudAgentFromBrowserWalletArgs {
|
|
45
|
+
readonly wallet: BrowserWalletAdapter;
|
|
46
|
+
readonly transport: ShroudFiTransport;
|
|
47
|
+
readonly stealthContract?: Address;
|
|
48
|
+
readonly startBlock: bigint;
|
|
49
|
+
readonly finality?: FinalityLevel;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Decode a 0x-prefixed hex string (e.g. an EIP-191 signature) into bytes.
|
|
54
|
+
* Returns null on malformed input — callers map this to a privacy-safe error
|
|
55
|
+
* rather than surfacing the raw value.
|
|
56
|
+
*/
|
|
57
|
+
function hexToBytesSafe(hex: string): Uint8Array | null {
|
|
58
|
+
if (typeof hex !== 'string') return null;
|
|
59
|
+
const stripped = hex.startsWith('0x') ? hex.slice(2) : hex;
|
|
60
|
+
if (stripped.length === 0 || stripped.length % 2 !== 0) return null;
|
|
61
|
+
const out = new Uint8Array(stripped.length / 2);
|
|
62
|
+
for (let i = 0; i < out.length; i++) {
|
|
63
|
+
const byte = parseInt(stripped.slice(i * 2, i * 2 + 2), 16);
|
|
64
|
+
if (Number.isNaN(byte)) return null;
|
|
65
|
+
out[i] = byte;
|
|
66
|
+
}
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Build a ShroudAgent from a browser wallet adapter (e.g. MetaMask).
|
|
72
|
+
*
|
|
73
|
+
* @throws MissingBrowserWalletError if the adapter is missing required fields.
|
|
74
|
+
* @throws BrowserWalletSignatureRejectedError if the signature request is rejected.
|
|
75
|
+
*/
|
|
76
|
+
export async function createShroudAgentFromBrowserWallet(
|
|
77
|
+
args: CreateShroudAgentFromBrowserWalletArgs,
|
|
78
|
+
): Promise<ShroudAgent> {
|
|
79
|
+
const wallet = args.wallet;
|
|
80
|
+
if (
|
|
81
|
+
wallet === undefined ||
|
|
82
|
+
wallet === null ||
|
|
83
|
+
typeof wallet.signMessage !== 'function' ||
|
|
84
|
+
typeof wallet.address !== 'string' ||
|
|
85
|
+
!wallet.address.startsWith('0x')
|
|
86
|
+
) {
|
|
87
|
+
throw new MissingBrowserWalletError();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
let signature: string;
|
|
91
|
+
try {
|
|
92
|
+
signature = await wallet.signMessage(BROWSER_KEY_MESSAGE);
|
|
93
|
+
} catch {
|
|
94
|
+
// Never surface the underlying rejection reason — could embed wallet
|
|
95
|
+
// metadata or user prompt text.
|
|
96
|
+
throw new BrowserWalletSignatureRejectedError();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const sigBytes = hexToBytesSafe(signature);
|
|
100
|
+
if (sigBytes === null || sigBytes.length === 0) {
|
|
101
|
+
throw new BrowserWalletSignatureRejectedError();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// HKDF-SHA256: extract + expand the signature into a deterministic 32-byte
|
|
105
|
+
// master seed. The signature is the IKM, not the salt — salt and info are
|
|
106
|
+
// constant version tags.
|
|
107
|
+
const masterSeed = hkdf(
|
|
108
|
+
sha256,
|
|
109
|
+
sigBytes,
|
|
110
|
+
HKDF_BROWSER_SALT,
|
|
111
|
+
HKDF_BROWSER_INFO,
|
|
112
|
+
BROWSER_MASTER_SEED_LENGTH,
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const config = {
|
|
116
|
+
masterSeed,
|
|
117
|
+
transport: args.transport,
|
|
118
|
+
startBlock: args.startBlock,
|
|
119
|
+
...(args.stealthContract !== undefined
|
|
120
|
+
? { stealthContract: args.stealthContract }
|
|
121
|
+
: {}),
|
|
122
|
+
...(args.finality !== undefined ? { finality: args.finality } : {}),
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
return createShroudAgent(config);
|
|
126
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent runtime constants.
|
|
3
|
+
*
|
|
4
|
+
* BROWSER_KEY_MESSAGE is the personal-sign payload the browser factory asks
|
|
5
|
+
* MetaMask (or any EIP-1193 wallet) to sign. It is HKDF'd into a deterministic
|
|
6
|
+
* 32-byte master seed so the same wallet always produces the same agent
|
|
7
|
+
* identity — no localStorage, no key persistence.
|
|
8
|
+
*
|
|
9
|
+
* Versioning the message string is load-bearing: bumping `-v1` to `-v2` will
|
|
10
|
+
* derive a different identity even for the same wallet. Treat as a one-way
|
|
11
|
+
* cryptographic input; never rename without a migration story.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export const BROWSER_KEY_MESSAGE =
|
|
15
|
+
'ShroudFi-Demo-Key-Derivation-v1';
|
|
16
|
+
|
|
17
|
+
export const HKDF_BROWSER_SALT = new TextEncoder().encode('ShroudFi-Demo-v1');
|
|
18
|
+
export const HKDF_BROWSER_INFO = new TextEncoder().encode('agent-master-seed');
|
|
19
|
+
|
|
20
|
+
export const BROWSER_MASTER_SEED_LENGTH = 32;
|
package/src/contacts.ts
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// Address-book / contact registry for @shroud-fi/agent-runtime.
|
|
2
|
+
//
|
|
3
|
+
// Pure-local, file-backed map of human-friendly labels → either a wallet
|
|
4
|
+
// address (0x…) or a stealth meta-address (st:base:…). Persisted at
|
|
5
|
+
// ~/.shroudfi/contacts.json on POSIX, %USERPROFILE%\.shroudfi\contacts.json
|
|
6
|
+
// on Windows. File mode is 0600 on platforms that honour it.
|
|
7
|
+
//
|
|
8
|
+
// Privacy rule: NO method on ContactBook puts the label or the value into an
|
|
9
|
+
// error message. Test file contacts.test.ts asserts this for every error
|
|
10
|
+
// class. Callers who want to surface the label to a UI must read it from the
|
|
11
|
+
// structured Contact object the API returns.
|
|
12
|
+
|
|
13
|
+
// Bare imports (not `node:` URL scheme) so browser bundlers like webpack can
|
|
14
|
+
// stub these to false via resolve.fallback when ContactBook isn't actually
|
|
15
|
+
// instantiated client-side. See apps/demo/next.config.mjs for the stub.
|
|
16
|
+
import * as fs from 'fs';
|
|
17
|
+
import * as path from 'path';
|
|
18
|
+
import * as os from 'os';
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
ContactAlreadyExistsError,
|
|
22
|
+
InvalidContactLabelError,
|
|
23
|
+
InvalidContactValueError,
|
|
24
|
+
UnknownContactError,
|
|
25
|
+
} from './errors.js';
|
|
26
|
+
import type { Contact, ContactKind } from './types.js';
|
|
27
|
+
|
|
28
|
+
const SCHEMA_VERSION = 1;
|
|
29
|
+
|
|
30
|
+
const WALLET_ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
|
|
31
|
+
const META_ADDRESS_RE = /^st:base:0x[0-9a-fA-F]{66,200}$/;
|
|
32
|
+
const LABEL_RE = /^[A-Za-z0-9._-]{1,64}$/;
|
|
33
|
+
|
|
34
|
+
export interface ContactBookOptions {
|
|
35
|
+
/** Override the on-disk JSON file location. Defaults to the home-dir path. */
|
|
36
|
+
filePath?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface FileShape {
|
|
40
|
+
version?: number;
|
|
41
|
+
contacts: Record<string, Omit<Contact, 'label'>>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Local contact registry. Each instance is bound to one JSON file and
|
|
46
|
+
* re-loads on every read so multiple instances pointed at the same file stay
|
|
47
|
+
* coherent within a single process.
|
|
48
|
+
*/
|
|
49
|
+
export class ContactBook {
|
|
50
|
+
private readonly filePath: string;
|
|
51
|
+
|
|
52
|
+
constructor(opts: ContactBookOptions = {}) {
|
|
53
|
+
this.filePath = opts.filePath ?? ContactBook.defaultFilePath();
|
|
54
|
+
// Touch-load on construction so schema-mismatch errors surface eagerly.
|
|
55
|
+
this.read();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
static defaultFilePath(): string {
|
|
59
|
+
return path.join(os.homedir(), '.shroudfi', 'contacts.json');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
add(label: string, value: string): Contact {
|
|
63
|
+
if (!isValidLabel(label)) {
|
|
64
|
+
throw new InvalidContactLabelError();
|
|
65
|
+
}
|
|
66
|
+
const kind = detectKind(value);
|
|
67
|
+
if (kind === undefined) {
|
|
68
|
+
throw new InvalidContactValueError();
|
|
69
|
+
}
|
|
70
|
+
const file = this.read();
|
|
71
|
+
if (file.contacts[label] !== undefined) {
|
|
72
|
+
throw new ContactAlreadyExistsError();
|
|
73
|
+
}
|
|
74
|
+
const entry: Omit<Contact, 'label'> = {
|
|
75
|
+
kind,
|
|
76
|
+
value,
|
|
77
|
+
addedAt: new Date().toISOString(),
|
|
78
|
+
};
|
|
79
|
+
file.contacts[label] = entry;
|
|
80
|
+
this.write(file);
|
|
81
|
+
return { label, ...entry };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
get(label: string): Contact | undefined {
|
|
85
|
+
if (!isValidLabel(label)) {
|
|
86
|
+
// We don't throw — get is read-side, returning undefined matches
|
|
87
|
+
// typical Map.get ergonomics. Invalid labels can't be in the book.
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
const file = this.read();
|
|
91
|
+
const entry = file.contacts[label];
|
|
92
|
+
if (entry === undefined) return undefined;
|
|
93
|
+
return { label, ...entry };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
list(): Contact[] {
|
|
97
|
+
const file = this.read();
|
|
98
|
+
return Object.entries(file.contacts)
|
|
99
|
+
.map(([label, entry]) => ({ label, ...entry }))
|
|
100
|
+
.sort((a, b) => (a.label < b.label ? -1 : a.label > b.label ? 1 : 0));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
remove(label: string): void {
|
|
104
|
+
const file = this.read();
|
|
105
|
+
if (file.contacts[label] === undefined) {
|
|
106
|
+
throw new UnknownContactError();
|
|
107
|
+
}
|
|
108
|
+
delete file.contacts[label];
|
|
109
|
+
this.write(file);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ---------------------------------------------------------------------
|
|
113
|
+
// internals
|
|
114
|
+
// ---------------------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
private read(): FileShape {
|
|
117
|
+
if (!fs.existsSync(this.filePath)) {
|
|
118
|
+
return { version: SCHEMA_VERSION, contacts: {} };
|
|
119
|
+
}
|
|
120
|
+
let raw: unknown;
|
|
121
|
+
try {
|
|
122
|
+
raw = JSON.parse(fs.readFileSync(this.filePath, 'utf8'));
|
|
123
|
+
} catch {
|
|
124
|
+
// A corrupt file is treated as a fatal-but-privacy-safe error. We do not
|
|
125
|
+
// leak the file's contents into the error message.
|
|
126
|
+
throw new InvalidContactValueError();
|
|
127
|
+
}
|
|
128
|
+
if (!isFileShape(raw)) {
|
|
129
|
+
throw new InvalidContactValueError();
|
|
130
|
+
}
|
|
131
|
+
const version = raw.version ?? SCHEMA_VERSION;
|
|
132
|
+
if (version !== SCHEMA_VERSION) {
|
|
133
|
+
throw new InvalidContactValueError();
|
|
134
|
+
}
|
|
135
|
+
return { version, contacts: raw.contacts };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private write(file: FileShape): void {
|
|
139
|
+
const dir = path.dirname(this.filePath);
|
|
140
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
141
|
+
const out: FileShape = { version: SCHEMA_VERSION, contacts: file.contacts };
|
|
142
|
+
fs.writeFileSync(this.filePath, JSON.stringify(out, null, 2) + '\n', {
|
|
143
|
+
encoding: 'utf8',
|
|
144
|
+
mode: 0o600,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function isValidLabel(label: unknown): label is string {
|
|
150
|
+
if (typeof label !== 'string') return false;
|
|
151
|
+
if (!LABEL_RE.test(label)) return false;
|
|
152
|
+
// Reject anything that looks like an on-chain identifier — eliminates the
|
|
153
|
+
// foot-gun where a half-typed address gets accepted as a label.
|
|
154
|
+
if (label.startsWith('0x')) return false;
|
|
155
|
+
if (label.startsWith('st:')) return false;
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function detectKind(value: string): ContactKind | undefined {
|
|
160
|
+
if (typeof value !== 'string') return undefined;
|
|
161
|
+
if (WALLET_ADDRESS_RE.test(value)) return 'wallet';
|
|
162
|
+
if (META_ADDRESS_RE.test(value)) return 'meta';
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function isFileShape(raw: unknown): raw is FileShape {
|
|
167
|
+
if (typeof raw !== 'object' || raw === null) return false;
|
|
168
|
+
const r = raw as Record<string, unknown>;
|
|
169
|
+
if (r.contacts === undefined || typeof r.contacts !== 'object' || r.contacts === null) {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
if (r.version !== undefined && typeof r.version !== 'number') return false;
|
|
173
|
+
return true;
|
|
174
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Privacy-safe error classes for @shroud-fi/agent-runtime.
|
|
3
|
+
*
|
|
4
|
+
* Invariants (mirrored from @shroud-fi/scanning + @shroud-fi/payments):
|
|
5
|
+
* - No 32-byte hex (key material) in any message.
|
|
6
|
+
* - No ephemeral pubkey bytes in any message.
|
|
7
|
+
* - No raw signature bytes in any message.
|
|
8
|
+
* - No transfer amounts in any message.
|
|
9
|
+
* - Messages carry short, structured reasons only.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export class AgentRuntimeError extends Error {
|
|
13
|
+
override readonly name: string = 'AgentRuntimeError';
|
|
14
|
+
constructor(message: string) {
|
|
15
|
+
super(message);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class AgentNotStartedError extends AgentRuntimeError {
|
|
20
|
+
override readonly name: string = 'AgentNotStartedError';
|
|
21
|
+
constructor() {
|
|
22
|
+
super('Agent has not been started — call start() first');
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class AgentAlreadyStartedError extends AgentRuntimeError {
|
|
27
|
+
override readonly name: string = 'AgentAlreadyStartedError';
|
|
28
|
+
constructor() {
|
|
29
|
+
super('Agent already started — call stop() before start() again');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class MissingBrowserWalletError extends AgentRuntimeError {
|
|
34
|
+
override readonly name: string = 'MissingBrowserWalletError';
|
|
35
|
+
constructor() {
|
|
36
|
+
super('Browser wallet adapter missing required signMessage or address');
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class BrowserWalletSignatureRejectedError extends AgentRuntimeError {
|
|
41
|
+
override readonly name: string = 'BrowserWalletSignatureRejectedError';
|
|
42
|
+
constructor() {
|
|
43
|
+
super('Browser wallet rejected signature request');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Thrown when `sweepViaRelayer` is called but no relayer contract has been
|
|
49
|
+
* registered for the agent's chain. Callers can pass an explicit
|
|
50
|
+
* `relayerContract` to override the lookup.
|
|
51
|
+
*/
|
|
52
|
+
export class RelayerContractNotConfiguredError extends AgentRuntimeError {
|
|
53
|
+
override readonly name: string = 'RelayerContractNotConfiguredError';
|
|
54
|
+
constructor() {
|
|
55
|
+
super('Relayer contract not configured for this chain');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* P5.1 — thrown when the caller tries to use the ETH gasless sweep path but
|
|
61
|
+
* has not provided a `selfHostEndpoint`. ETH gasless requires a self-host
|
|
62
|
+
* relayer service that builds + broadcasts the EIP-7702 type-0x04 tx; there
|
|
63
|
+
* is no third-party fallback (Gelato's 7702 surface is wallet-shaped, not
|
|
64
|
+
* relayer-shaped).
|
|
65
|
+
*/
|
|
66
|
+
export class EthRelayerEndpointRequiredError extends AgentRuntimeError {
|
|
67
|
+
override readonly name: string = 'EthRelayerEndpointRequiredError';
|
|
68
|
+
constructor() {
|
|
69
|
+
super(
|
|
70
|
+
'ETH gasless sweep requires a self-host relayer endpoint — pass relayerOptions.selfHostEndpoint',
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* P5.1 — thrown when no ShroudFiEthRelayer contract has been registered for
|
|
77
|
+
* the agent's chain and the caller did not pass `ethRelayerContract` to
|
|
78
|
+
* override the manifest lookup.
|
|
79
|
+
*/
|
|
80
|
+
export class EthRelayerContractNotConfiguredError extends AgentRuntimeError {
|
|
81
|
+
override readonly name: string = 'EthRelayerContractNotConfiguredError';
|
|
82
|
+
constructor() {
|
|
83
|
+
super('ShroudFiEthRelayer contract not configured for this chain');
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @deprecated Retained as a soft-removed export — callers updated to use
|
|
89
|
+
* EthRelayerEndpointRequiredError or the gasless ETH path directly. P5.1
|
|
90
|
+
* activated the previously-stubbed ETH-via-relayer path.
|
|
91
|
+
*/
|
|
92
|
+
export class RelayerNotAvailableForETHError extends AgentRuntimeError {
|
|
93
|
+
override readonly name: string = 'RelayerNotAvailableForETHError';
|
|
94
|
+
constructor() {
|
|
95
|
+
super('Relayer sweep does not support ETH — use direct sweep or ERC-20');
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* M3 — thrown when `register()` is called but the agent's registrant wallet
|
|
101
|
+
* already has a non-empty stealth meta-address on file in the canonical
|
|
102
|
+
* ERC-6538 registry for scheme 1. Callers should treat this as benign and
|
|
103
|
+
* skip the registration tx; `ensureRegistered` does this automatically.
|
|
104
|
+
*
|
|
105
|
+
* Privacy: the registrant wallet address is NOT placed in `.message` — it
|
|
106
|
+
* would link the agent's runtime wallet to the error log. Callers who need
|
|
107
|
+
* to display the wallet must source it from `transport.walletClient.account`
|
|
108
|
+
* themselves.
|
|
109
|
+
*/
|
|
110
|
+
export class AlreadyRegisteredError extends AgentRuntimeError {
|
|
111
|
+
override readonly name: string = 'AlreadyRegisteredError';
|
|
112
|
+
constructor() {
|
|
113
|
+
super('Agent is already registered in the ERC-6538 registry');
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* M3 — wraps an underlying error thrown while `ensureRegistered` runs inside
|
|
119
|
+
* `agent.sendToWallet` with `autoRegister: true`. Preserves the original via
|
|
120
|
+
* the standard `cause` chain so callers can introspect without parsing
|
|
121
|
+
* strings.
|
|
122
|
+
*
|
|
123
|
+
* Privacy: the inner error MUST itself comply with the runtime's no-amount /
|
|
124
|
+
* no-wallet / no-key rule. This wrapper never inlines key, signature, or
|
|
125
|
+
* amount material into its own message.
|
|
126
|
+
*/
|
|
127
|
+
export class AutoRegistrationFailedError extends AgentRuntimeError {
|
|
128
|
+
override readonly name: string = 'AutoRegistrationFailedError';
|
|
129
|
+
constructor(cause?: unknown) {
|
|
130
|
+
super('Auto-registration failed');
|
|
131
|
+
// Use the standard ES2022 `cause` slot. Avoid serializing the inner
|
|
132
|
+
// error into the message itself — the privacy invariant tests would
|
|
133
|
+
// catch a leak if the wrapped error embedded sensitive bytes.
|
|
134
|
+
if (cause !== undefined) {
|
|
135
|
+
(this as { cause?: unknown }).cause = cause;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* M3 — thrown when `register()` is invoked on a transport that has no
|
|
142
|
+
* `walletClient` (or no account on the walletClient). Registration is a state
|
|
143
|
+
* change tx that needs a signer; read-only transports cannot register.
|
|
144
|
+
*/
|
|
145
|
+
export class RegistrationRequiresWalletError extends AgentRuntimeError {
|
|
146
|
+
override readonly name: string = 'RegistrationRequiresWalletError';
|
|
147
|
+
constructor() {
|
|
148
|
+
super('Registration requires a walletClient with an account');
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* v0.1.1 — ContactBook: thrown when a label fails validation (empty,
|
|
154
|
+
* whitespace-only, control chars, looks like an address, longer than 64 chars,
|
|
155
|
+
* or contains characters outside [A-Za-z0-9._-]).
|
|
156
|
+
*
|
|
157
|
+
* Privacy: the offending label string is NOT placed in the message — it would
|
|
158
|
+
* end up in error logs / stack traces. Callers must surface the label from
|
|
159
|
+
* their own UI state.
|
|
160
|
+
*/
|
|
161
|
+
export class InvalidContactLabelError extends AgentRuntimeError {
|
|
162
|
+
override readonly name: string = 'InvalidContactLabelError';
|
|
163
|
+
constructor() {
|
|
164
|
+
super('Contact label is not valid (must match [A-Za-z0-9._-]{1,64})');
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* v0.1.1 — ContactBook: thrown when a value is neither a 20-byte wallet
|
|
170
|
+
* address nor a stealth meta-address on `base`.
|
|
171
|
+
*
|
|
172
|
+
* Privacy: the offending value is NOT placed in the message.
|
|
173
|
+
*/
|
|
174
|
+
export class InvalidContactValueError extends AgentRuntimeError {
|
|
175
|
+
override readonly name: string = 'InvalidContactValueError';
|
|
176
|
+
constructor() {
|
|
177
|
+
super('Contact value must be a wallet address (0x…) or a meta-address (st:base:0x…)');
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* v0.1.1 — ContactBook: thrown when add() is called with a label already
|
|
183
|
+
* present in the book.
|
|
184
|
+
*
|
|
185
|
+
* Privacy: the label is NOT placed in the message.
|
|
186
|
+
*/
|
|
187
|
+
export class ContactAlreadyExistsError extends AgentRuntimeError {
|
|
188
|
+
override readonly name: string = 'ContactAlreadyExistsError';
|
|
189
|
+
constructor() {
|
|
190
|
+
super('A contact with that label already exists');
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* v0.1.1 — ContactBook: thrown when remove() or a label-resolving send is
|
|
196
|
+
* called with a label that does not exist in the book.
|
|
197
|
+
*
|
|
198
|
+
* Privacy: the label is NOT placed in the message.
|
|
199
|
+
*/
|
|
200
|
+
export class UnknownContactError extends AgentRuntimeError {
|
|
201
|
+
override readonly name: string = 'UnknownContactError';
|
|
202
|
+
constructor() {
|
|
203
|
+
super('No contact with that label');
|
|
204
|
+
}
|
|
205
|
+
}
|
package/src/factory.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createShroudAgent — server / Node entry point.
|
|
3
|
+
*
|
|
4
|
+
* Validates configuration, resolves the stealth contract address from the
|
|
5
|
+
* transport's chain, builds an AgentIdentity from the optional master seed,
|
|
6
|
+
* and constructs a ShroudAgent.
|
|
7
|
+
*
|
|
8
|
+
* Privacy: no seed or key bytes ever appear in error messages. Validation
|
|
9
|
+
* errors are short structured reasons only.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { createAgentIdentity } from '@shroud-fi/core';
|
|
13
|
+
import { getShroudFiStealth } from '@shroud-fi/transport';
|
|
14
|
+
import { ShroudAgent } from './agent.js';
|
|
15
|
+
import type { ShroudAgentInternalOptions } from './agent.js';
|
|
16
|
+
import { AgentRuntimeError } from './errors.js';
|
|
17
|
+
import type { ShroudAgentConfig } from './types.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Build a fully-wired ShroudAgent from a config.
|
|
21
|
+
*
|
|
22
|
+
* @throws AgentRuntimeError on missing/invalid transport or negative startBlock.
|
|
23
|
+
*/
|
|
24
|
+
export function createShroudAgent(config: ShroudAgentConfig): ShroudAgent {
|
|
25
|
+
if (config.transport === undefined || config.transport.publicClient === undefined) {
|
|
26
|
+
throw new AgentRuntimeError('transport.publicClient is required');
|
|
27
|
+
}
|
|
28
|
+
if (config.startBlock < 0n) {
|
|
29
|
+
throw new AgentRuntimeError('startBlock must be non-negative');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const chainId = config.transport.chain.id;
|
|
33
|
+
const stealthContract =
|
|
34
|
+
config.stealthContract ?? getShroudFiStealth(chainId);
|
|
35
|
+
|
|
36
|
+
const identity = createAgentIdentity(config.masterSeed);
|
|
37
|
+
|
|
38
|
+
const opts: ShroudAgentInternalOptions = {
|
|
39
|
+
identity,
|
|
40
|
+
transport: config.transport,
|
|
41
|
+
stealthContract,
|
|
42
|
+
startBlock: config.startBlock,
|
|
43
|
+
...(config.finality !== undefined ? { finality: config.finality } : {}),
|
|
44
|
+
...(config.announcer !== undefined ? { announcer: config.announcer } : {}),
|
|
45
|
+
...(config.autoRegister !== undefined
|
|
46
|
+
? { autoRegister: config.autoRegister }
|
|
47
|
+
: {}),
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
return new ShroudAgent(opts);
|
|
51
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Public surface — @shroud-fi/agent-runtime
|
|
2
|
+
//
|
|
3
|
+
// Phase 4: ergonomic wrapper around @shroud-fi/core + @shroud-fi/payments +
|
|
4
|
+
// @shroud-fi/scanning + @shroud-fi/transport. Two construction paths:
|
|
5
|
+
// - createShroudAgent(config) — server / Node / AI agent (deterministic seed)
|
|
6
|
+
// - createShroudAgentFromBrowserWallet(args) — browser (MetaMask-derived seed)
|
|
7
|
+
//
|
|
8
|
+
// Re-exports trimmed to what callers actually need; deeper helpers stay
|
|
9
|
+
// inside the upstream packages.
|
|
10
|
+
|
|
11
|
+
// Class + factories
|
|
12
|
+
export { ShroudAgent } from './agent.js';
|
|
13
|
+
export { createShroudAgent } from './factory.js';
|
|
14
|
+
export {
|
|
15
|
+
createShroudAgentFromBrowserWallet,
|
|
16
|
+
BROWSER_KEY_MESSAGE,
|
|
17
|
+
HKDF_BROWSER_SALT,
|
|
18
|
+
HKDF_BROWSER_INFO,
|
|
19
|
+
} from './browser.js';
|
|
20
|
+
|
|
21
|
+
// Address book (v0.1.1)
|
|
22
|
+
export { ContactBook } from './contacts.js';
|
|
23
|
+
export type { ContactBookOptions } from './contacts.js';
|
|
24
|
+
|
|
25
|
+
// Types
|
|
26
|
+
export type {
|
|
27
|
+
ShroudAgentConfig,
|
|
28
|
+
ShroudAgentStartOptions,
|
|
29
|
+
BrowserWalletAdapter,
|
|
30
|
+
AgentSweepResult,
|
|
31
|
+
AgentRelayerSweepOptions,
|
|
32
|
+
AgentRelayerSweepResult,
|
|
33
|
+
AgentRelayerEthSweepOptions,
|
|
34
|
+
AgentRelayerEthSweepResult,
|
|
35
|
+
Contact,
|
|
36
|
+
ContactKind,
|
|
37
|
+
} from './types.js';
|
|
38
|
+
|
|
39
|
+
// Errors
|
|
40
|
+
export {
|
|
41
|
+
AgentRuntimeError,
|
|
42
|
+
AgentNotStartedError,
|
|
43
|
+
AgentAlreadyStartedError,
|
|
44
|
+
MissingBrowserWalletError,
|
|
45
|
+
BrowserWalletSignatureRejectedError,
|
|
46
|
+
RelayerNotAvailableForETHError,
|
|
47
|
+
RelayerContractNotConfiguredError,
|
|
48
|
+
EthRelayerEndpointRequiredError,
|
|
49
|
+
EthRelayerContractNotConfiguredError,
|
|
50
|
+
AlreadyRegisteredError,
|
|
51
|
+
AutoRegistrationFailedError,
|
|
52
|
+
RegistrationRequiresWalletError,
|
|
53
|
+
InvalidContactLabelError,
|
|
54
|
+
InvalidContactValueError,
|
|
55
|
+
ContactAlreadyExistsError,
|
|
56
|
+
UnknownContactError,
|
|
57
|
+
} from './errors.js';
|