@buildersgarden/siwa 0.0.11 → 0.0.12
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 +2 -2
- package/dist/erc8128.d.ts +38 -10
- package/dist/erc8128.js +51 -19
- package/dist/express.d.ts +3 -0
- package/dist/express.js +1 -0
- package/dist/identity.d.ts +7 -7
- package/dist/identity.js +8 -8
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/keystore.d.ts +17 -57
- package/dist/keystore.js +20 -54
- package/dist/next.d.ts +3 -0
- package/dist/next.js +1 -0
- package/dist/receipt.d.ts +2 -0
- package/dist/registry.d.ts +22 -3
- package/dist/registry.js +20 -9
- package/dist/signer.d.ts +145 -0
- package/dist/signer.js +179 -0
- package/dist/siwa.d.ts +33 -8
- package/dist/siwa.js +52 -39
- package/dist/tba.d.ts +82 -0
- package/dist/tba.js +113 -0
- package/package.json +9 -1
package/dist/registry.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* npm install viem
|
|
9
9
|
*/
|
|
10
10
|
import { type PublicClient } from 'viem';
|
|
11
|
-
import {
|
|
11
|
+
import type { TransactionSigner } from './signer.js';
|
|
12
12
|
/** Service endpoint types defined in ERC-8004 */
|
|
13
13
|
export type ServiceType = 'web' | 'A2A' | 'MCP' | 'OASF' | 'ENS' | 'DID' | 'email';
|
|
14
14
|
/** Trust models defined in ERC-8004 */
|
|
@@ -74,10 +74,14 @@ export declare function getAgent(agentId: number, options: GetAgentOptions): Pro
|
|
|
74
74
|
*/
|
|
75
75
|
export declare function getReputation(agentId: number, options: GetReputationOptions): Promise<ReputationSummary>;
|
|
76
76
|
export interface RegisterAgentOptions {
|
|
77
|
+
/** The agent metadata URI (IPFS, HTTP, or data URL) */
|
|
77
78
|
agentURI: string;
|
|
79
|
+
/** The chain ID to register on */
|
|
78
80
|
chainId: number;
|
|
81
|
+
/** Optional RPC URL (defaults to chain's default endpoint) */
|
|
79
82
|
rpcUrl?: string;
|
|
80
|
-
|
|
83
|
+
/** A TransactionSigner for signing the registration transaction */
|
|
84
|
+
signer: TransactionSigner;
|
|
81
85
|
}
|
|
82
86
|
export interface RegisterAgentResult {
|
|
83
87
|
agentId: string;
|
|
@@ -88,7 +92,22 @@ export interface RegisterAgentResult {
|
|
|
88
92
|
/**
|
|
89
93
|
* Register an agent on the ERC-8004 Identity Registry in a single call.
|
|
90
94
|
*
|
|
91
|
-
* Builds, signs (via
|
|
95
|
+
* Builds, signs (via the provided signer), and broadcasts the `register(agentURI)`
|
|
92
96
|
* transaction, then waits for confirmation and parses the `Registered` event.
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* ```typescript
|
|
100
|
+
* import { registerAgent, createLocalAccountSigner } from '@buildersgarden/siwa';
|
|
101
|
+
* import { privateKeyToAccount } from 'viem/accounts';
|
|
102
|
+
*
|
|
103
|
+
* const account = privateKeyToAccount('0x...');
|
|
104
|
+
* const signer = createLocalAccountSigner(account);
|
|
105
|
+
*
|
|
106
|
+
* const result = await registerAgent({
|
|
107
|
+
* agentURI: 'ipfs://...',
|
|
108
|
+
* chainId: 84532,
|
|
109
|
+
* signer,
|
|
110
|
+
* });
|
|
111
|
+
* ```
|
|
93
112
|
*/
|
|
94
113
|
export declare function registerAgent(options: RegisterAgentOptions): Promise<RegisterAgentResult>;
|
package/dist/registry.js
CHANGED
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { zeroAddress, createPublicClient, http, encodeFunctionData, parseEventLogs, } from 'viem';
|
|
11
11
|
import { getRegistryAddress, getAgentRegistryString, RPC_ENDPOINTS } from './addresses.js';
|
|
12
|
-
import { getAddress, signTransaction } from './keystore.js';
|
|
13
12
|
// ─── ABI Fragments ──────────────────────────────────────────────────
|
|
14
13
|
const IDENTITY_REGISTRY_ABI = [
|
|
15
14
|
{
|
|
@@ -160,27 +159,39 @@ export async function getReputation(agentId, options) {
|
|
|
160
159
|
/**
|
|
161
160
|
* Register an agent on the ERC-8004 Identity Registry in a single call.
|
|
162
161
|
*
|
|
163
|
-
* Builds, signs (via
|
|
162
|
+
* Builds, signs (via the provided signer), and broadcasts the `register(agentURI)`
|
|
164
163
|
* transaction, then waits for confirmation and parses the `Registered` event.
|
|
164
|
+
*
|
|
165
|
+
* @example
|
|
166
|
+
* ```typescript
|
|
167
|
+
* import { registerAgent, createLocalAccountSigner } from '@buildersgarden/siwa';
|
|
168
|
+
* import { privateKeyToAccount } from 'viem/accounts';
|
|
169
|
+
*
|
|
170
|
+
* const account = privateKeyToAccount('0x...');
|
|
171
|
+
* const signer = createLocalAccountSigner(account);
|
|
172
|
+
*
|
|
173
|
+
* const result = await registerAgent({
|
|
174
|
+
* agentURI: 'ipfs://...',
|
|
175
|
+
* chainId: 84532,
|
|
176
|
+
* signer,
|
|
177
|
+
* });
|
|
178
|
+
* ```
|
|
165
179
|
*/
|
|
166
180
|
export async function registerAgent(options) {
|
|
167
|
-
const { agentURI, chainId,
|
|
181
|
+
const { agentURI, chainId, signer } = options;
|
|
168
182
|
const registryAddress = getRegistryAddress(chainId);
|
|
169
183
|
const rpcUrl = options.rpcUrl || RPC_ENDPOINTS[chainId];
|
|
170
184
|
if (!rpcUrl) {
|
|
171
185
|
throw new Error(`No RPC URL provided and no default endpoint for chain ${chainId}.`);
|
|
172
186
|
}
|
|
173
187
|
const publicClient = createPublicClient({ transport: http(rpcUrl) });
|
|
174
|
-
const address = await getAddress(
|
|
175
|
-
if (!address) {
|
|
176
|
-
throw new Error('Could not resolve wallet address from keyring proxy.');
|
|
177
|
-
}
|
|
188
|
+
const address = await signer.getAddress();
|
|
178
189
|
const data = encodeFunctionData({
|
|
179
190
|
abi: IDENTITY_REGISTRY_ABI,
|
|
180
191
|
functionName: 'register',
|
|
181
192
|
args: [agentURI],
|
|
182
193
|
});
|
|
183
|
-
const nonce = await publicClient.getTransactionCount({ address
|
|
194
|
+
const nonce = await publicClient.getTransactionCount({ address });
|
|
184
195
|
const feeData = await publicClient.estimateFeesPerGas();
|
|
185
196
|
const gasEstimate = await publicClient.estimateGas({
|
|
186
197
|
to: registryAddress,
|
|
@@ -198,7 +209,7 @@ export async function registerAgent(options) {
|
|
|
198
209
|
maxPriorityFeePerGas: feeData.maxPriorityFeePerGas,
|
|
199
210
|
gas,
|
|
200
211
|
};
|
|
201
|
-
const
|
|
212
|
+
const signedTx = await signer.signTransaction(txReq);
|
|
202
213
|
const txHash = await publicClient.sendRawTransaction({
|
|
203
214
|
serializedTransaction: signedTx,
|
|
204
215
|
});
|
package/dist/signer.d.ts
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* signer.ts
|
|
3
|
+
*
|
|
4
|
+
* Wallet-agnostic signing abstraction for SIWA.
|
|
5
|
+
*
|
|
6
|
+
* This module provides a `Signer` interface that abstracts signing operations,
|
|
7
|
+
* allowing developers to use any wallet provider without changing the SDK.
|
|
8
|
+
*
|
|
9
|
+
* Available signer implementations:
|
|
10
|
+
* - createKeyringProxySigner(config) — Keyring proxy server (HMAC-authenticated)
|
|
11
|
+
* - createLocalAccountSigner(account) — viem LocalAccount (privateKeyToAccount)
|
|
12
|
+
* - createWalletClientSigner(client) — viem WalletClient (Privy, MetaMask, etc.)
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* import { signSIWAMessage, createLocalAccountSigner } from '@buildersgarden/siwa';
|
|
16
|
+
* import { privateKeyToAccount } from 'viem/accounts';
|
|
17
|
+
*
|
|
18
|
+
* const account = privateKeyToAccount('0x...');
|
|
19
|
+
* const signer = createLocalAccountSigner(account);
|
|
20
|
+
* const { message, signature } = await signSIWAMessage(fields, signer);
|
|
21
|
+
*/
|
|
22
|
+
import type { Address, Hex, WalletClient } from 'viem';
|
|
23
|
+
import type { LocalAccount } from 'viem/accounts';
|
|
24
|
+
/**
|
|
25
|
+
* Signer type detected during SIWA sign-in.
|
|
26
|
+
*
|
|
27
|
+
* - `'eoa'` — Externally Owned Account (ECDSA key pair)
|
|
28
|
+
* - `'sca'` — Smart Contract Account (ERC-1271, e.g. Safe, TBA, Kernel)
|
|
29
|
+
*/
|
|
30
|
+
export type SignerType = 'eoa' | 'sca';
|
|
31
|
+
/**
|
|
32
|
+
* Core signer interface for message signing.
|
|
33
|
+
*
|
|
34
|
+
* Implement this interface to add support for new wallet providers.
|
|
35
|
+
*/
|
|
36
|
+
export interface Signer {
|
|
37
|
+
/** Get the signer's address */
|
|
38
|
+
getAddress(): Promise<Address>;
|
|
39
|
+
/** Sign a message (EIP-191 personal_sign) */
|
|
40
|
+
signMessage(message: string): Promise<Hex>;
|
|
41
|
+
/**
|
|
42
|
+
* Sign raw bytes (optional).
|
|
43
|
+
* Used by ERC-8128 for HTTP message signatures.
|
|
44
|
+
* If not implemented, signMessage will be used as fallback.
|
|
45
|
+
*/
|
|
46
|
+
signRawMessage?(rawHex: Hex): Promise<Hex>;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Extended signer with transaction signing capabilities.
|
|
50
|
+
*
|
|
51
|
+
* Required for onchain operations like agent registration.
|
|
52
|
+
*/
|
|
53
|
+
export interface TransactionSigner extends Signer {
|
|
54
|
+
/** Sign a transaction and return the serialized signed transaction */
|
|
55
|
+
signTransaction(tx: TransactionRequest): Promise<Hex>;
|
|
56
|
+
}
|
|
57
|
+
/** Transaction request compatible with viem */
|
|
58
|
+
export interface TransactionRequest {
|
|
59
|
+
to?: Address;
|
|
60
|
+
data?: Hex;
|
|
61
|
+
value?: bigint;
|
|
62
|
+
nonce?: number;
|
|
63
|
+
chainId?: number;
|
|
64
|
+
gas?: bigint;
|
|
65
|
+
maxFeePerGas?: bigint;
|
|
66
|
+
maxPriorityFeePerGas?: bigint;
|
|
67
|
+
gasPrice?: bigint;
|
|
68
|
+
type?: number | string;
|
|
69
|
+
accessList?: any[];
|
|
70
|
+
}
|
|
71
|
+
/** Configuration for the keyring proxy signer */
|
|
72
|
+
export interface KeyringProxyConfig {
|
|
73
|
+
/** URL of the keyring proxy server (or KEYRING_PROXY_URL env var) */
|
|
74
|
+
proxyUrl?: string;
|
|
75
|
+
/** HMAC shared secret (or KEYRING_PROXY_SECRET env var) */
|
|
76
|
+
proxySecret?: string;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Create a signer backed by the keyring proxy server.
|
|
80
|
+
*
|
|
81
|
+
* The private key is stored securely in the proxy server and never
|
|
82
|
+
* enters the calling process. All signing operations are performed
|
|
83
|
+
* via HMAC-authenticated HTTP requests.
|
|
84
|
+
*
|
|
85
|
+
* @param config - Proxy URL and secret (or use env vars)
|
|
86
|
+
* @returns A TransactionSigner that delegates to the keyring proxy
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* ```typescript
|
|
90
|
+
* const signer = createKeyringProxySigner({
|
|
91
|
+
* proxyUrl: 'http://localhost:3100',
|
|
92
|
+
* proxySecret: 'my-secret',
|
|
93
|
+
* });
|
|
94
|
+
* const { message, signature } = await signSIWAMessage(fields, signer);
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
97
|
+
export declare function createKeyringProxySigner(config?: KeyringProxyConfig): TransactionSigner;
|
|
98
|
+
/**
|
|
99
|
+
* Create a signer from a viem LocalAccount.
|
|
100
|
+
*
|
|
101
|
+
* Use this when you have direct access to a private key via
|
|
102
|
+
* viem's `privateKeyToAccount()` or similar.
|
|
103
|
+
*
|
|
104
|
+
* @param account - A viem LocalAccount (from privateKeyToAccount, mnemonicToAccount, etc.)
|
|
105
|
+
* @returns A TransactionSigner that signs using the local account
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* ```typescript
|
|
109
|
+
* import { privateKeyToAccount } from 'viem/accounts';
|
|
110
|
+
*
|
|
111
|
+
* const account = privateKeyToAccount('0x...');
|
|
112
|
+
* const signer = createLocalAccountSigner(account);
|
|
113
|
+
* const { message, signature } = await signSIWAMessage(fields, signer);
|
|
114
|
+
* ```
|
|
115
|
+
*/
|
|
116
|
+
export declare function createLocalAccountSigner(account: LocalAccount): TransactionSigner;
|
|
117
|
+
/**
|
|
118
|
+
* Create a signer from a viem WalletClient.
|
|
119
|
+
*
|
|
120
|
+
* Use this for browser wallets (MetaMask, etc.), embedded wallets (Privy),
|
|
121
|
+
* WalletConnect, or any wallet that provides an EIP-1193 provider.
|
|
122
|
+
*
|
|
123
|
+
* @param client - A viem WalletClient
|
|
124
|
+
* @param account - Optional specific account address to use
|
|
125
|
+
* @returns A Signer that delegates to the WalletClient
|
|
126
|
+
*
|
|
127
|
+
* @example
|
|
128
|
+
* ```typescript
|
|
129
|
+
* // With Privy embedded wallet
|
|
130
|
+
* const provider = await privyWallet.getEthereumProvider();
|
|
131
|
+
* const walletClient = createWalletClient({
|
|
132
|
+
* chain: baseSepolia,
|
|
133
|
+
* transport: custom(provider),
|
|
134
|
+
* });
|
|
135
|
+
* const signer = createWalletClientSigner(walletClient);
|
|
136
|
+
*
|
|
137
|
+
* // With browser wallet (MetaMask)
|
|
138
|
+
* const walletClient = createWalletClient({
|
|
139
|
+
* chain: mainnet,
|
|
140
|
+
* transport: custom(window.ethereum),
|
|
141
|
+
* });
|
|
142
|
+
* const signer = createWalletClientSigner(walletClient);
|
|
143
|
+
* ```
|
|
144
|
+
*/
|
|
145
|
+
export declare function createWalletClientSigner(client: WalletClient, account?: Address): Signer;
|
package/dist/signer.js
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* signer.ts
|
|
3
|
+
*
|
|
4
|
+
* Wallet-agnostic signing abstraction for SIWA.
|
|
5
|
+
*
|
|
6
|
+
* This module provides a `Signer` interface that abstracts signing operations,
|
|
7
|
+
* allowing developers to use any wallet provider without changing the SDK.
|
|
8
|
+
*
|
|
9
|
+
* Available signer implementations:
|
|
10
|
+
* - createKeyringProxySigner(config) — Keyring proxy server (HMAC-authenticated)
|
|
11
|
+
* - createLocalAccountSigner(account) — viem LocalAccount (privateKeyToAccount)
|
|
12
|
+
* - createWalletClientSigner(client) — viem WalletClient (Privy, MetaMask, etc.)
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* import { signSIWAMessage, createLocalAccountSigner } from '@buildersgarden/siwa';
|
|
16
|
+
* import { privateKeyToAccount } from 'viem/accounts';
|
|
17
|
+
*
|
|
18
|
+
* const account = privateKeyToAccount('0x...');
|
|
19
|
+
* const signer = createLocalAccountSigner(account);
|
|
20
|
+
* const { message, signature } = await signSIWAMessage(fields, signer);
|
|
21
|
+
*/
|
|
22
|
+
import { computeHmac } from './proxy-auth.js';
|
|
23
|
+
// ─── Internal: Keyring Proxy Request ─────────────────────────────────
|
|
24
|
+
async function proxyRequest(config, endpoint, body = {}) {
|
|
25
|
+
const url = config.proxyUrl || process.env.KEYRING_PROXY_URL;
|
|
26
|
+
const secret = config.proxySecret || process.env.KEYRING_PROXY_SECRET;
|
|
27
|
+
if (!url) {
|
|
28
|
+
throw new Error('Keyring proxy requires KEYRING_PROXY_URL or config.proxyUrl');
|
|
29
|
+
}
|
|
30
|
+
if (!secret) {
|
|
31
|
+
throw new Error('Keyring proxy requires KEYRING_PROXY_SECRET or config.proxySecret');
|
|
32
|
+
}
|
|
33
|
+
const bodyStr = JSON.stringify(body, (_key, value) => typeof value === 'bigint' ? '0x' + value.toString(16) : value);
|
|
34
|
+
const hmacHeaders = computeHmac(secret, 'POST', endpoint, bodyStr);
|
|
35
|
+
const res = await fetch(`${url}${endpoint}`, {
|
|
36
|
+
method: 'POST',
|
|
37
|
+
headers: {
|
|
38
|
+
'Content-Type': 'application/json',
|
|
39
|
+
...hmacHeaders,
|
|
40
|
+
},
|
|
41
|
+
body: bodyStr,
|
|
42
|
+
});
|
|
43
|
+
if (!res.ok) {
|
|
44
|
+
const text = await res.text();
|
|
45
|
+
throw new Error(`Proxy ${endpoint} failed (${res.status}): ${text}`);
|
|
46
|
+
}
|
|
47
|
+
return res.json();
|
|
48
|
+
}
|
|
49
|
+
// ─── Keyring Proxy Signer ────────────────────────────────────────────
|
|
50
|
+
/**
|
|
51
|
+
* Create a signer backed by the keyring proxy server.
|
|
52
|
+
*
|
|
53
|
+
* The private key is stored securely in the proxy server and never
|
|
54
|
+
* enters the calling process. All signing operations are performed
|
|
55
|
+
* via HMAC-authenticated HTTP requests.
|
|
56
|
+
*
|
|
57
|
+
* @param config - Proxy URL and secret (or use env vars)
|
|
58
|
+
* @returns A TransactionSigner that delegates to the keyring proxy
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```typescript
|
|
62
|
+
* const signer = createKeyringProxySigner({
|
|
63
|
+
* proxyUrl: 'http://localhost:3100',
|
|
64
|
+
* proxySecret: 'my-secret',
|
|
65
|
+
* });
|
|
66
|
+
* const { message, signature } = await signSIWAMessage(fields, signer);
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
export function createKeyringProxySigner(config = {}) {
|
|
70
|
+
return {
|
|
71
|
+
async getAddress() {
|
|
72
|
+
const data = await proxyRequest(config, '/get-address');
|
|
73
|
+
return data.address;
|
|
74
|
+
},
|
|
75
|
+
async signMessage(message) {
|
|
76
|
+
const data = await proxyRequest(config, '/sign-message', { message });
|
|
77
|
+
return data.signature;
|
|
78
|
+
},
|
|
79
|
+
async signRawMessage(rawHex) {
|
|
80
|
+
const data = await proxyRequest(config, '/sign-message', {
|
|
81
|
+
message: rawHex,
|
|
82
|
+
raw: true,
|
|
83
|
+
});
|
|
84
|
+
return data.signature;
|
|
85
|
+
},
|
|
86
|
+
async signTransaction(tx) {
|
|
87
|
+
const data = await proxyRequest(config, '/sign-transaction', { tx });
|
|
88
|
+
return data.signedTx;
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
// ─── Local Account Signer ────────────────────────────────────────────
|
|
93
|
+
/**
|
|
94
|
+
* Create a signer from a viem LocalAccount.
|
|
95
|
+
*
|
|
96
|
+
* Use this when you have direct access to a private key via
|
|
97
|
+
* viem's `privateKeyToAccount()` or similar.
|
|
98
|
+
*
|
|
99
|
+
* @param account - A viem LocalAccount (from privateKeyToAccount, mnemonicToAccount, etc.)
|
|
100
|
+
* @returns A TransactionSigner that signs using the local account
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* ```typescript
|
|
104
|
+
* import { privateKeyToAccount } from 'viem/accounts';
|
|
105
|
+
*
|
|
106
|
+
* const account = privateKeyToAccount('0x...');
|
|
107
|
+
* const signer = createLocalAccountSigner(account);
|
|
108
|
+
* const { message, signature } = await signSIWAMessage(fields, signer);
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
export function createLocalAccountSigner(account) {
|
|
112
|
+
return {
|
|
113
|
+
async getAddress() {
|
|
114
|
+
return account.address;
|
|
115
|
+
},
|
|
116
|
+
async signMessage(message) {
|
|
117
|
+
return account.signMessage({ message });
|
|
118
|
+
},
|
|
119
|
+
async signRawMessage(rawHex) {
|
|
120
|
+
return account.signMessage({ message: { raw: rawHex } });
|
|
121
|
+
},
|
|
122
|
+
async signTransaction(tx) {
|
|
123
|
+
return account.signTransaction(tx);
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
// ─── WalletClient Signer ─────────────────────────────────────────────
|
|
128
|
+
/**
|
|
129
|
+
* Create a signer from a viem WalletClient.
|
|
130
|
+
*
|
|
131
|
+
* Use this for browser wallets (MetaMask, etc.), embedded wallets (Privy),
|
|
132
|
+
* WalletConnect, or any wallet that provides an EIP-1193 provider.
|
|
133
|
+
*
|
|
134
|
+
* @param client - A viem WalletClient
|
|
135
|
+
* @param account - Optional specific account address to use
|
|
136
|
+
* @returns A Signer that delegates to the WalletClient
|
|
137
|
+
*
|
|
138
|
+
* @example
|
|
139
|
+
* ```typescript
|
|
140
|
+
* // With Privy embedded wallet
|
|
141
|
+
* const provider = await privyWallet.getEthereumProvider();
|
|
142
|
+
* const walletClient = createWalletClient({
|
|
143
|
+
* chain: baseSepolia,
|
|
144
|
+
* transport: custom(provider),
|
|
145
|
+
* });
|
|
146
|
+
* const signer = createWalletClientSigner(walletClient);
|
|
147
|
+
*
|
|
148
|
+
* // With browser wallet (MetaMask)
|
|
149
|
+
* const walletClient = createWalletClient({
|
|
150
|
+
* chain: mainnet,
|
|
151
|
+
* transport: custom(window.ethereum),
|
|
152
|
+
* });
|
|
153
|
+
* const signer = createWalletClientSigner(walletClient);
|
|
154
|
+
* ```
|
|
155
|
+
*/
|
|
156
|
+
export function createWalletClientSigner(client, account) {
|
|
157
|
+
const resolveAccount = async () => {
|
|
158
|
+
if (account)
|
|
159
|
+
return account;
|
|
160
|
+
const addresses = await client.getAddresses();
|
|
161
|
+
if (!addresses || addresses.length === 0) {
|
|
162
|
+
throw new Error('No address found in wallet');
|
|
163
|
+
}
|
|
164
|
+
return addresses[0];
|
|
165
|
+
};
|
|
166
|
+
return {
|
|
167
|
+
async getAddress() {
|
|
168
|
+
return resolveAccount();
|
|
169
|
+
},
|
|
170
|
+
async signMessage(message) {
|
|
171
|
+
const addr = await resolveAccount();
|
|
172
|
+
return client.signMessage({ account: addr, message });
|
|
173
|
+
},
|
|
174
|
+
async signRawMessage(rawHex) {
|
|
175
|
+
const addr = await resolveAccount();
|
|
176
|
+
return client.signMessage({ account: addr, message: { raw: rawHex } });
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
}
|
package/dist/siwa.d.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { type PublicClient } from 'viem';
|
|
11
11
|
import { AgentProfile, ServiceType, TrustModel } from './registry.js';
|
|
12
|
+
import type { Signer, SignerType } from './signer.js';
|
|
12
13
|
export declare enum SIWAErrorCode {
|
|
13
14
|
INVALID_SIGNATURE = "INVALID_SIGNATURE",
|
|
14
15
|
DOMAIN_MISMATCH = "DOMAIN_MISMATCH",
|
|
@@ -47,6 +48,7 @@ export interface SIWAVerificationResult {
|
|
|
47
48
|
agentRegistry: string;
|
|
48
49
|
chainId: number;
|
|
49
50
|
verified: 'offline' | 'onchain';
|
|
51
|
+
signerType?: SignerType;
|
|
50
52
|
code?: SIWAErrorCode;
|
|
51
53
|
error?: string;
|
|
52
54
|
agent?: AgentProfile;
|
|
@@ -58,6 +60,7 @@ export interface SIWAVerifyCriteria {
|
|
|
58
60
|
requiredServices?: (ServiceType | (string & {}))[];
|
|
59
61
|
mustBeActive?: boolean;
|
|
60
62
|
requiredTrust?: (TrustModel | (string & {}))[];
|
|
63
|
+
allowedSignerTypes?: SignerType[];
|
|
61
64
|
custom?: (agent: AgentProfile) => boolean | Promise<boolean>;
|
|
62
65
|
}
|
|
63
66
|
export interface SIWAResponse {
|
|
@@ -67,6 +70,7 @@ export interface SIWAResponse {
|
|
|
67
70
|
agentRegistry?: string;
|
|
68
71
|
chainId?: number;
|
|
69
72
|
verified?: 'offline' | 'onchain';
|
|
73
|
+
signerType?: SignerType;
|
|
70
74
|
code?: SIWAErrorCode;
|
|
71
75
|
error?: string;
|
|
72
76
|
action?: SIWAAction;
|
|
@@ -141,26 +145,47 @@ export declare function createSIWANonce(params: SIWANonceParams, client: PublicC
|
|
|
141
145
|
/**
|
|
142
146
|
* Fields accepted by signSIWAMessage.
|
|
143
147
|
* `address` is optional — when omitted, the address is fetched directly
|
|
144
|
-
* from the
|
|
148
|
+
* from the signer (the trusted source of truth for the agent wallet).
|
|
145
149
|
*/
|
|
146
150
|
export type SIWASignFields = Omit<SIWAMessageFields, 'address'> & {
|
|
147
151
|
address?: string;
|
|
148
152
|
};
|
|
149
153
|
/**
|
|
150
|
-
* Sign a SIWA message using the
|
|
154
|
+
* Sign a SIWA message using the provided signer.
|
|
151
155
|
*
|
|
152
|
-
* The
|
|
153
|
-
*
|
|
156
|
+
* The signer abstracts the wallet implementation, allowing you to use:
|
|
157
|
+
* - createKeyringProxySigner(config) — Keyring proxy server
|
|
158
|
+
* - createLocalAccountSigner(account) — viem LocalAccount (private key)
|
|
159
|
+
* - createWalletClientSigner(client) — viem WalletClient (Privy, MetaMask, etc.)
|
|
154
160
|
*
|
|
155
|
-
* The agent address is always resolved from the
|
|
161
|
+
* The agent address is always resolved from the signer — the single source
|
|
156
162
|
* of truth — so the caller doesn't need to supply (or risk hallucinating) it.
|
|
157
|
-
* If `fields.address` is provided it must match the
|
|
163
|
+
* If `fields.address` is provided it must match the signer's address.
|
|
158
164
|
*
|
|
159
165
|
* @param fields — SIWA message fields (domain, agentId, etc.). `address` is optional.
|
|
160
|
-
* @param
|
|
166
|
+
* @param signer — A Signer implementation (see createKeyringProxySigner, createLocalAccountSigner, createWalletClientSigner)
|
|
161
167
|
* @returns { message, signature, address } — the plaintext message, EIP-191 signature, and resolved address
|
|
168
|
+
*
|
|
169
|
+
* @example
|
|
170
|
+
* ```typescript
|
|
171
|
+
* import { signSIWAMessage, createLocalAccountSigner } from '@buildersgarden/siwa';
|
|
172
|
+
* import { privateKeyToAccount } from 'viem/accounts';
|
|
173
|
+
*
|
|
174
|
+
* const account = privateKeyToAccount('0x...');
|
|
175
|
+
* const signer = createLocalAccountSigner(account);
|
|
176
|
+
*
|
|
177
|
+
* const { message, signature, address } = await signSIWAMessage({
|
|
178
|
+
* domain: 'example.com',
|
|
179
|
+
* uri: 'https://example.com/login',
|
|
180
|
+
* agentId: 123,
|
|
181
|
+
* agentRegistry: 'eip155:84532:0x...',
|
|
182
|
+
* chainId: 84532,
|
|
183
|
+
* nonce: 'abc123',
|
|
184
|
+
* issuedAt: new Date().toISOString(),
|
|
185
|
+
* }, signer);
|
|
186
|
+
* ```
|
|
162
187
|
*/
|
|
163
|
-
export declare function signSIWAMessage(fields: SIWASignFields,
|
|
188
|
+
export declare function signSIWAMessage(fields: SIWASignFields, signer: Signer): Promise<{
|
|
164
189
|
message: string;
|
|
165
190
|
signature: string;
|
|
166
191
|
address: string;
|
package/dist/siwa.js
CHANGED
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
* Dependencies:
|
|
8
8
|
* npm install viem
|
|
9
9
|
*/
|
|
10
|
-
import { verifyMessage, hashMessage, } from 'viem';
|
|
11
10
|
import * as crypto from 'crypto';
|
|
12
11
|
import { getAgent, getReputation } from './registry.js';
|
|
13
12
|
// ─── Types ───────────────────────────────────────────────────────────
|
|
@@ -39,6 +38,7 @@ export function buildSIWAResponse(result) {
|
|
|
39
38
|
agentRegistry: result.agentRegistry || undefined,
|
|
40
39
|
chainId: result.chainId || undefined,
|
|
41
40
|
verified: result.verified,
|
|
41
|
+
...(result.signerType ? { signerType: result.signerType } : {}),
|
|
42
42
|
};
|
|
43
43
|
const skillRef = {
|
|
44
44
|
name: '@buildersgarden/siwa',
|
|
@@ -284,39 +284,55 @@ export async function createSIWANonce(params, client, options) {
|
|
|
284
284
|
return result;
|
|
285
285
|
}
|
|
286
286
|
/**
|
|
287
|
-
* Sign a SIWA message using the
|
|
287
|
+
* Sign a SIWA message using the provided signer.
|
|
288
288
|
*
|
|
289
|
-
* The
|
|
290
|
-
*
|
|
289
|
+
* The signer abstracts the wallet implementation, allowing you to use:
|
|
290
|
+
* - createKeyringProxySigner(config) — Keyring proxy server
|
|
291
|
+
* - createLocalAccountSigner(account) — viem LocalAccount (private key)
|
|
292
|
+
* - createWalletClientSigner(client) — viem WalletClient (Privy, MetaMask, etc.)
|
|
291
293
|
*
|
|
292
|
-
* The agent address is always resolved from the
|
|
294
|
+
* The agent address is always resolved from the signer — the single source
|
|
293
295
|
* of truth — so the caller doesn't need to supply (or risk hallucinating) it.
|
|
294
|
-
* If `fields.address` is provided it must match the
|
|
296
|
+
* If `fields.address` is provided it must match the signer's address.
|
|
295
297
|
*
|
|
296
298
|
* @param fields — SIWA message fields (domain, agentId, etc.). `address` is optional.
|
|
297
|
-
* @param
|
|
299
|
+
* @param signer — A Signer implementation (see createKeyringProxySigner, createLocalAccountSigner, createWalletClientSigner)
|
|
298
300
|
* @returns { message, signature, address } — the plaintext message, EIP-191 signature, and resolved address
|
|
301
|
+
*
|
|
302
|
+
* @example
|
|
303
|
+
* ```typescript
|
|
304
|
+
* import { signSIWAMessage, createLocalAccountSigner } from '@buildersgarden/siwa';
|
|
305
|
+
* import { privateKeyToAccount } from 'viem/accounts';
|
|
306
|
+
*
|
|
307
|
+
* const account = privateKeyToAccount('0x...');
|
|
308
|
+
* const signer = createLocalAccountSigner(account);
|
|
309
|
+
*
|
|
310
|
+
* const { message, signature, address } = await signSIWAMessage({
|
|
311
|
+
* domain: 'example.com',
|
|
312
|
+
* uri: 'https://example.com/login',
|
|
313
|
+
* agentId: 123,
|
|
314
|
+
* agentRegistry: 'eip155:84532:0x...',
|
|
315
|
+
* chainId: 84532,
|
|
316
|
+
* nonce: 'abc123',
|
|
317
|
+
* issuedAt: new Date().toISOString(),
|
|
318
|
+
* }, signer);
|
|
319
|
+
* ```
|
|
299
320
|
*/
|
|
300
|
-
export async function signSIWAMessage(fields,
|
|
301
|
-
//
|
|
302
|
-
const
|
|
303
|
-
// Resolve the address from the keystore — the trusted source of truth
|
|
304
|
-
const keystoreAddress = await getAddress(keystoreConfig);
|
|
305
|
-
if (!keystoreAddress) {
|
|
306
|
-
throw new Error('No wallet found in keystore. Run createWallet() first.');
|
|
307
|
-
}
|
|
321
|
+
export async function signSIWAMessage(fields, signer) {
|
|
322
|
+
// Resolve the address from the signer — the trusted source of truth
|
|
323
|
+
const signerAddress = await signer.getAddress();
|
|
308
324
|
// If the caller supplied an address, verify it matches (defensive check)
|
|
309
|
-
if (fields.address &&
|
|
310
|
-
throw new Error(`Address mismatch:
|
|
325
|
+
if (fields.address && signerAddress.toLowerCase() !== fields.address.toLowerCase()) {
|
|
326
|
+
throw new Error(`Address mismatch: signer has ${signerAddress}, message claims ${fields.address}`);
|
|
311
327
|
}
|
|
312
328
|
const resolvedFields = {
|
|
313
329
|
...fields,
|
|
314
|
-
address:
|
|
330
|
+
address: signerAddress,
|
|
315
331
|
};
|
|
316
332
|
const message = buildSIWAMessage(resolvedFields);
|
|
317
|
-
// Sign via
|
|
318
|
-
const
|
|
319
|
-
return { message, signature
|
|
333
|
+
// Sign via signer
|
|
334
|
+
const signature = await signer.signMessage(message);
|
|
335
|
+
return { message, signature, address: signerAddress };
|
|
320
336
|
}
|
|
321
337
|
/**
|
|
322
338
|
* Verify a SIWA message + signature.
|
|
@@ -342,8 +358,12 @@ export async function verifySIWA(message, signature, expectedDomain, nonceValid,
|
|
|
342
358
|
try {
|
|
343
359
|
// 1. Parse
|
|
344
360
|
const fields = parseSIWAMessage(message);
|
|
345
|
-
// 2.
|
|
346
|
-
|
|
361
|
+
// 2. Verify signature (supports both EOA and ERC-1271 smart wallets)
|
|
362
|
+
// Using client.verifyMessage handles:
|
|
363
|
+
// - EOA signatures (ECDSA recovery)
|
|
364
|
+
// - ERC-1271 smart contract wallets (Safe, Argent, etc.)
|
|
365
|
+
// - ERC-6492 pre-deployed smart wallets
|
|
366
|
+
const isValid = await client.verifyMessage({
|
|
347
367
|
address: fields.address,
|
|
348
368
|
message,
|
|
349
369
|
signature: signature,
|
|
@@ -352,6 +372,9 @@ export async function verifySIWA(message, signature, expectedDomain, nonceValid,
|
|
|
352
372
|
return fail(fields, SIWAErrorCode.INVALID_SIGNATURE, 'Invalid signature');
|
|
353
373
|
}
|
|
354
374
|
const recovered = fields.address;
|
|
375
|
+
// 2b. Detect signer type (EOA vs smart contract account)
|
|
376
|
+
const signerCode = await client.getCode({ address: fields.address });
|
|
377
|
+
const signerType = (signerCode && signerCode !== '0x') ? 'sca' : 'eoa';
|
|
355
378
|
// 3. Address match is implicit in verifyMessage (it checks against the address)
|
|
356
379
|
// 4. Domain binding
|
|
357
380
|
if (fields.domain !== expectedDomain) {
|
|
@@ -404,22 +427,7 @@ export async function verifySIWA(message, signature, expectedDomain, nonceValid,
|
|
|
404
427
|
return fail(fields, SIWAErrorCode.NOT_REGISTERED, 'Agent is not registered on the ERC-8004 Identity Registry');
|
|
405
428
|
}
|
|
406
429
|
if (owner.toLowerCase() !== recovered.toLowerCase()) {
|
|
407
|
-
|
|
408
|
-
const messageHash = hashMessage(message);
|
|
409
|
-
try {
|
|
410
|
-
const magicValue = await client.readContract({
|
|
411
|
-
address: owner,
|
|
412
|
-
abi: [{ name: 'isValidSignature', type: 'function', stateMutability: 'view', inputs: [{ name: 'hash', type: 'bytes32' }, { name: 'signature', type: 'bytes' }], outputs: [{ name: '', type: 'bytes4' }] }],
|
|
413
|
-
functionName: 'isValidSignature',
|
|
414
|
-
args: [messageHash, signature],
|
|
415
|
-
});
|
|
416
|
-
if (magicValue !== '0x1626ba7e') {
|
|
417
|
-
return fail(fields, SIWAErrorCode.NOT_OWNER, 'Signer is not the owner of this agent NFT (ERC-1271 check also failed)');
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
catch {
|
|
421
|
-
return fail(fields, SIWAErrorCode.NOT_OWNER, 'Signer is not the owner of this agent NFT');
|
|
422
|
-
}
|
|
430
|
+
return fail(fields, SIWAErrorCode.NOT_OWNER, 'Signer is not the owner of this agent NFT');
|
|
423
431
|
}
|
|
424
432
|
// 8. Base result
|
|
425
433
|
const baseResult = {
|
|
@@ -429,9 +437,14 @@ export async function verifySIWA(message, signature, expectedDomain, nonceValid,
|
|
|
429
437
|
agentRegistry: fields.agentRegistry,
|
|
430
438
|
chainId: fields.chainId,
|
|
431
439
|
verified: 'onchain',
|
|
440
|
+
signerType,
|
|
432
441
|
};
|
|
433
442
|
if (!criteria)
|
|
434
443
|
return baseResult;
|
|
444
|
+
// Signer type policy (checked before fetching metadata for early exit)
|
|
445
|
+
if (criteria.allowedSignerTypes?.length && !criteria.allowedSignerTypes.includes(signerType)) {
|
|
446
|
+
return { ...baseResult, valid: false, code: SIWAErrorCode.CUSTOM_CHECK_FAILED, error: `Signer type '${signerType}' is not in allowed types [${criteria.allowedSignerTypes.join(', ')}]` };
|
|
447
|
+
}
|
|
435
448
|
const agent = await getAgent(fields.agentId, {
|
|
436
449
|
registryAddress: registryAddress,
|
|
437
450
|
client,
|