@shroud-fi/agent-runtime 0.1.5 → 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/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
+ }
@@ -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;
@@ -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';