@buildersgarden/siwa 0.0.1 → 0.0.3
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/LICENSE +21 -0
- package/README.md +1 -1
- package/dist/addresses.d.ts +20 -0
- package/dist/addresses.js +82 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/keystore.d.ts +23 -10
- package/dist/keystore.js +185 -65
- package/dist/proxy-auth.d.ts +2 -2
- package/dist/proxy-auth.js +4 -4
- package/dist/registry.d.ts +6 -6
- package/dist/registry.js +69 -17
- package/dist/siwa.d.ts +4 -4
- package/dist/siwa.js +34 -19
- package/package.json +43 -14
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Builders Garden SRL
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -64,7 +64,7 @@ See [`references/security-model.md`](references/security-model.md) for the full
|
|
|
64
64
|
## Tech Stack
|
|
65
65
|
|
|
66
66
|
- **TypeScript** (ES modules, strict mode)
|
|
67
|
-
- **
|
|
67
|
+
- **viem** — wallet management and contract interaction
|
|
68
68
|
- **pnpm** — package manager
|
|
69
69
|
|
|
70
70
|
## References
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* addresses.ts
|
|
3
|
+
*
|
|
4
|
+
* Canonical ERC-8004 contract addresses, chain metadata, and helpers.
|
|
5
|
+
* Published as part of the SDK so consumers don't need to hard-code addresses.
|
|
6
|
+
*/
|
|
7
|
+
export declare const REGISTRY_ADDRESSES: Record<number, string>;
|
|
8
|
+
export declare const REPUTATION_ADDRESSES: Record<number, string>;
|
|
9
|
+
export declare const RPC_ENDPOINTS: Record<number, string>;
|
|
10
|
+
export declare const CHAIN_NAMES: Record<number, string>;
|
|
11
|
+
export declare const FAUCETS: Record<number, string>;
|
|
12
|
+
export declare const BLOCK_EXPLORERS: Record<number, string>;
|
|
13
|
+
/**
|
|
14
|
+
* Get the identity registry address for a chain, or throw if unsupported.
|
|
15
|
+
*/
|
|
16
|
+
export declare function getRegistryAddress(chainId: number): string;
|
|
17
|
+
/**
|
|
18
|
+
* Get the agent registry string in `eip155:{chainId}:{address}` format.
|
|
19
|
+
*/
|
|
20
|
+
export declare function getAgentRegistryString(chainId: number): string;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* addresses.ts
|
|
3
|
+
*
|
|
4
|
+
* Canonical ERC-8004 contract addresses, chain metadata, and helpers.
|
|
5
|
+
* Published as part of the SDK so consumers don't need to hard-code addresses.
|
|
6
|
+
*/
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Identity Registries
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
export const REGISTRY_ADDRESSES = {
|
|
11
|
+
8453: '0x8004A169FB4a3325136EB29fA0ceB6D2e539a432', // Base
|
|
12
|
+
84532: '0x8004A818BFB912233c491871b3d84c89A494BD9e', // Base Sepolia
|
|
13
|
+
11155111: '0x8004a6090Cd10A7288092483047B097295Fb8847', // ETH Sepolia
|
|
14
|
+
59141: '0x8004aa7C931bCE1233973a0C6A667f73F66282e7', // Linea Sepolia
|
|
15
|
+
80002: '0x8004ad19E14B9e0654f73353e8a0B600D46C2898', // Polygon Amoy
|
|
16
|
+
};
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Reputation Registries
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
export const REPUTATION_ADDRESSES = {
|
|
21
|
+
8453: '0x8004BAa17C55a88189AE136b182e5fdA19dE9b63', // Base
|
|
22
|
+
84532: '0x8004B663056A597Dffe9eCcC1965A193B7388713', // Base Sepolia
|
|
23
|
+
};
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// RPC Endpoints (public, rate-limited)
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
export const RPC_ENDPOINTS = {
|
|
28
|
+
8453: 'https://mainnet.base.org',
|
|
29
|
+
84532: 'https://sepolia.base.org',
|
|
30
|
+
11155111: 'https://rpc.sepolia.org',
|
|
31
|
+
59141: 'https://rpc.sepolia.linea.build',
|
|
32
|
+
80002: 'https://rpc-amoy.polygon.technology',
|
|
33
|
+
};
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Chain Names
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
export const CHAIN_NAMES = {
|
|
38
|
+
8453: 'Base',
|
|
39
|
+
84532: 'Base Sepolia',
|
|
40
|
+
11155111: 'Ethereum Sepolia',
|
|
41
|
+
59141: 'Linea Sepolia',
|
|
42
|
+
80002: 'Polygon Amoy',
|
|
43
|
+
};
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// Faucets (testnets only)
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
export const FAUCETS = {
|
|
48
|
+
84532: 'https://www.alchemy.com/faucets/base-sepolia',
|
|
49
|
+
11155111: 'https://www.alchemy.com/faucets/ethereum-sepolia',
|
|
50
|
+
59141: 'https://faucets.chain.link/linea-sepolia',
|
|
51
|
+
80002: 'https://faucet.polygon.technology/',
|
|
52
|
+
};
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// Block Explorers
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
export const BLOCK_EXPLORERS = {
|
|
57
|
+
8453: 'https://basescan.org',
|
|
58
|
+
84532: 'https://sepolia.basescan.org',
|
|
59
|
+
11155111: 'https://sepolia.etherscan.io',
|
|
60
|
+
59141: 'https://sepolia.lineascan.build',
|
|
61
|
+
80002: 'https://amoy.polygonscan.com',
|
|
62
|
+
};
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// Helpers
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
/**
|
|
67
|
+
* Get the identity registry address for a chain, or throw if unsupported.
|
|
68
|
+
*/
|
|
69
|
+
export function getRegistryAddress(chainId) {
|
|
70
|
+
const addr = REGISTRY_ADDRESSES[chainId];
|
|
71
|
+
if (!addr) {
|
|
72
|
+
const supported = Object.keys(REGISTRY_ADDRESSES).join(', ');
|
|
73
|
+
throw new Error(`No identity registry for chainId ${chainId}. Supported: ${supported}`);
|
|
74
|
+
}
|
|
75
|
+
return addr;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Get the agent registry string in `eip155:{chainId}:{address}` format.
|
|
79
|
+
*/
|
|
80
|
+
export function getAgentRegistryString(chainId) {
|
|
81
|
+
return `eip155:${chainId}:${getRegistryAddress(chainId)}`;
|
|
82
|
+
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/keystore.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Three backends, in order of preference:
|
|
7
7
|
* 0. Keyring Proxy (via HMAC-authenticated HTTP) — key never enters agent process
|
|
8
|
-
* 1. Ethereum V3 Encrypted JSON Keystore (via
|
|
8
|
+
* 1. Ethereum V3 Encrypted JSON Keystore (via @noble/ciphers) — password-encrypted file on disk
|
|
9
9
|
* 2. Environment variable fallback (AGENT_PRIVATE_KEY) — least secure, for CI/testing only
|
|
10
10
|
*
|
|
11
11
|
* The private key NEVER leaves this module as a return value.
|
|
@@ -18,15 +18,15 @@
|
|
|
18
18
|
* - getAddress() → returns the public address
|
|
19
19
|
* - hasWallet() → returns boolean
|
|
20
20
|
*
|
|
21
|
-
* EIP-7702 Support
|
|
22
|
-
* Wallets are standard EOAs created via
|
|
21
|
+
* EIP-7702 Support:
|
|
22
|
+
* Wallets are standard EOAs created via viem's generatePrivateKey().
|
|
23
23
|
* EIP-7702 allows these EOAs to temporarily delegate to smart contract
|
|
24
24
|
* implementations via authorization lists in type 4 transactions.
|
|
25
25
|
* Use signAuthorization() to sign delegation authorizations without
|
|
26
26
|
* exposing the private key.
|
|
27
27
|
*
|
|
28
28
|
* Dependencies:
|
|
29
|
-
* npm install
|
|
29
|
+
* npm install viem
|
|
30
30
|
*
|
|
31
31
|
* Configuration (via env vars or passed options):
|
|
32
32
|
* KEYSTORE_BACKEND — "encrypted-file" | "env" | "proxy" (auto-detected if omitted)
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
* KEYSTORE_PATH — Path to encrypted keystore file (default: ./agent-keystore.json)
|
|
35
35
|
* AGENT_PRIVATE_KEY — Fallback for env backend only
|
|
36
36
|
*/
|
|
37
|
-
import {
|
|
37
|
+
import { type WalletClient, type Account, type Chain, type Transport } from 'viem';
|
|
38
38
|
export type KeystoreBackend = 'encrypted-file' | 'env' | 'proxy';
|
|
39
39
|
export interface KeystoreConfig {
|
|
40
40
|
backend?: KeystoreBackend;
|
|
@@ -65,6 +65,20 @@ export interface SignedAuthorization {
|
|
|
65
65
|
r: string;
|
|
66
66
|
s: string;
|
|
67
67
|
}
|
|
68
|
+
export interface TransactionLike {
|
|
69
|
+
to?: string;
|
|
70
|
+
data?: string;
|
|
71
|
+
value?: bigint;
|
|
72
|
+
nonce?: number;
|
|
73
|
+
chainId?: number;
|
|
74
|
+
type?: number;
|
|
75
|
+
maxFeePerGas?: bigint | null;
|
|
76
|
+
maxPriorityFeePerGas?: bigint | null;
|
|
77
|
+
gasLimit?: bigint;
|
|
78
|
+
gas?: bigint;
|
|
79
|
+
gasPrice?: bigint | null;
|
|
80
|
+
accessList?: any[];
|
|
81
|
+
}
|
|
68
82
|
export declare function detectBackend(): Promise<KeystoreBackend>;
|
|
69
83
|
/**
|
|
70
84
|
* Create a new random wallet and store it securely.
|
|
@@ -96,13 +110,12 @@ export declare function signMessage(message: string, config?: KeystoreConfig): P
|
|
|
96
110
|
* The private key is loaded, used, and immediately discarded.
|
|
97
111
|
* Only the signed transaction is returned.
|
|
98
112
|
*/
|
|
99
|
-
export declare function signTransaction(tx:
|
|
113
|
+
export declare function signTransaction(tx: TransactionLike, config?: KeystoreConfig): Promise<{
|
|
100
114
|
signedTx: string;
|
|
101
115
|
address: string;
|
|
102
116
|
}>;
|
|
103
117
|
/**
|
|
104
118
|
* Sign an EIP-7702 authorization for delegating the EOA to a contract.
|
|
105
|
-
* Requires ethers >= 6.14.3.
|
|
106
119
|
*
|
|
107
120
|
* This allows the agent's EOA to temporarily act as a smart contract
|
|
108
121
|
* during a type 4 transaction. The private key is loaded, used, and
|
|
@@ -113,12 +126,12 @@ export declare function signTransaction(tx: ethers.TransactionRequest, config?:
|
|
|
113
126
|
*/
|
|
114
127
|
export declare function signAuthorization(auth: AuthorizationRequest, config?: KeystoreConfig): Promise<SignedAuthorization>;
|
|
115
128
|
/**
|
|
116
|
-
* Get a
|
|
117
|
-
* NOTE: This
|
|
129
|
+
* Get a wallet client for contract interactions.
|
|
130
|
+
* NOTE: This creates a client with the private key in memory.
|
|
118
131
|
* Use only within a narrow scope and discard immediately.
|
|
119
132
|
* Prefer signMessage() / signTransaction() when possible.
|
|
120
133
|
*/
|
|
121
|
-
export declare function
|
|
134
|
+
export declare function getWalletClient(rpcUrl: string, config?: KeystoreConfig): Promise<WalletClient<Transport, Chain | undefined, Account>>;
|
|
122
135
|
/**
|
|
123
136
|
* Delete the stored wallet from the active backend.
|
|
124
137
|
* DESTRUCTIVE — the identity is lost if no backup exists.
|
package/dist/keystore.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Three backends, in order of preference:
|
|
7
7
|
* 0. Keyring Proxy (via HMAC-authenticated HTTP) — key never enters agent process
|
|
8
|
-
* 1. Ethereum V3 Encrypted JSON Keystore (via
|
|
8
|
+
* 1. Ethereum V3 Encrypted JSON Keystore (via @noble/ciphers) — password-encrypted file on disk
|
|
9
9
|
* 2. Environment variable fallback (AGENT_PRIVATE_KEY) — least secure, for CI/testing only
|
|
10
10
|
*
|
|
11
11
|
* The private key NEVER leaves this module as a return value.
|
|
@@ -18,15 +18,15 @@
|
|
|
18
18
|
* - getAddress() → returns the public address
|
|
19
19
|
* - hasWallet() → returns boolean
|
|
20
20
|
*
|
|
21
|
-
* EIP-7702 Support
|
|
22
|
-
* Wallets are standard EOAs created via
|
|
21
|
+
* EIP-7702 Support:
|
|
22
|
+
* Wallets are standard EOAs created via viem's generatePrivateKey().
|
|
23
23
|
* EIP-7702 allows these EOAs to temporarily delegate to smart contract
|
|
24
24
|
* implementations via authorization lists in type 4 transactions.
|
|
25
25
|
* Use signAuthorization() to sign delegation authorizations without
|
|
26
26
|
* exposing the private key.
|
|
27
27
|
*
|
|
28
28
|
* Dependencies:
|
|
29
|
-
* npm install
|
|
29
|
+
* npm install viem
|
|
30
30
|
*
|
|
31
31
|
* Configuration (via env vars or passed options):
|
|
32
32
|
* KEYSTORE_BACKEND — "encrypted-file" | "env" | "proxy" (auto-detected if omitted)
|
|
@@ -34,14 +34,98 @@
|
|
|
34
34
|
* KEYSTORE_PATH — Path to encrypted keystore file (default: ./agent-keystore.json)
|
|
35
35
|
* AGENT_PRIVATE_KEY — Fallback for env backend only
|
|
36
36
|
*/
|
|
37
|
-
import {
|
|
37
|
+
import { createWalletClient, http, keccak256, toBytes, toHex, concat, } from 'viem';
|
|
38
|
+
import { privateKeyToAccount, generatePrivateKey } from 'viem/accounts';
|
|
39
|
+
import { hashAuthorization } from 'viem/experimental';
|
|
40
|
+
import { scrypt } from '@noble/hashes/scrypt';
|
|
41
|
+
import { randomBytes } from '@noble/hashes/utils';
|
|
42
|
+
import { ctr } from '@noble/ciphers/aes';
|
|
38
43
|
import * as fs from 'fs';
|
|
39
44
|
import * as crypto from 'crypto';
|
|
45
|
+
import * as os from 'os';
|
|
40
46
|
import { computeHmac } from './proxy-auth.js';
|
|
41
47
|
// ---------------------------------------------------------------------------
|
|
42
48
|
// Constants
|
|
43
49
|
// ---------------------------------------------------------------------------
|
|
44
50
|
const DEFAULT_KEYSTORE_PATH = './agent-keystore.json';
|
|
51
|
+
function generateUUID() {
|
|
52
|
+
const bytes = randomBytes(16);
|
|
53
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x40; // version 4
|
|
54
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80; // variant
|
|
55
|
+
const hex = toHex(bytes).slice(2);
|
|
56
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
|
|
57
|
+
}
|
|
58
|
+
async function encryptKeystore(privateKey, password) {
|
|
59
|
+
const privateKeyBytes = toBytes(privateKey);
|
|
60
|
+
const salt = randomBytes(32);
|
|
61
|
+
const iv = randomBytes(16);
|
|
62
|
+
// Scrypt parameters (standard for V3 keystores)
|
|
63
|
+
const n = 262144; // 2^18
|
|
64
|
+
const r = 8;
|
|
65
|
+
const p = 1;
|
|
66
|
+
const dklen = 32;
|
|
67
|
+
// Derive key using scrypt
|
|
68
|
+
const derivedKey = scrypt(new TextEncoder().encode(password), salt, { N: n, r, p, dkLen: dklen });
|
|
69
|
+
// Encrypt private key with AES-128-CTR
|
|
70
|
+
const encryptionKey = derivedKey.slice(0, 16);
|
|
71
|
+
const cipher = ctr(encryptionKey, iv);
|
|
72
|
+
const ciphertext = cipher.encrypt(privateKeyBytes);
|
|
73
|
+
// Calculate MAC: keccak256(derivedKey[16:32] + ciphertext)
|
|
74
|
+
const macData = concat([toHex(derivedKey.slice(16, 32)), toHex(ciphertext)]);
|
|
75
|
+
const mac = keccak256(macData);
|
|
76
|
+
// Get address from private key
|
|
77
|
+
const account = privateKeyToAccount(privateKey);
|
|
78
|
+
const keystore = {
|
|
79
|
+
version: 3,
|
|
80
|
+
id: generateUUID(),
|
|
81
|
+
address: account.address.toLowerCase().slice(2),
|
|
82
|
+
crypto: {
|
|
83
|
+
ciphertext: toHex(ciphertext).slice(2),
|
|
84
|
+
cipherparams: { iv: toHex(iv).slice(2) },
|
|
85
|
+
cipher: 'aes-128-ctr',
|
|
86
|
+
kdf: 'scrypt',
|
|
87
|
+
kdfparams: {
|
|
88
|
+
dklen,
|
|
89
|
+
salt: toHex(salt).slice(2),
|
|
90
|
+
n,
|
|
91
|
+
r,
|
|
92
|
+
p,
|
|
93
|
+
},
|
|
94
|
+
mac: mac.slice(2),
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
return JSON.stringify(keystore);
|
|
98
|
+
}
|
|
99
|
+
async function decryptKeystore(json, password) {
|
|
100
|
+
const keystore = JSON.parse(json);
|
|
101
|
+
if (keystore.version !== 3) {
|
|
102
|
+
throw new Error(`Unsupported keystore version: ${keystore.version}`);
|
|
103
|
+
}
|
|
104
|
+
const { crypto: cryptoData } = keystore;
|
|
105
|
+
if (cryptoData.kdf !== 'scrypt') {
|
|
106
|
+
throw new Error(`Unsupported KDF: ${cryptoData.kdf}`);
|
|
107
|
+
}
|
|
108
|
+
if (cryptoData.cipher !== 'aes-128-ctr') {
|
|
109
|
+
throw new Error(`Unsupported cipher: ${cryptoData.cipher}`);
|
|
110
|
+
}
|
|
111
|
+
const { kdfparams } = cryptoData;
|
|
112
|
+
const salt = toBytes(`0x${kdfparams.salt}`);
|
|
113
|
+
const iv = toBytes(`0x${cryptoData.cipherparams.iv}`);
|
|
114
|
+
const ciphertext = toBytes(`0x${cryptoData.ciphertext}`);
|
|
115
|
+
// Derive key using scrypt
|
|
116
|
+
const derivedKey = scrypt(new TextEncoder().encode(password), salt, { N: kdfparams.n, r: kdfparams.r, p: kdfparams.p, dkLen: kdfparams.dklen });
|
|
117
|
+
// Verify MAC
|
|
118
|
+
const macData = concat([toHex(derivedKey.slice(16, 32)), toHex(ciphertext)]);
|
|
119
|
+
const calculatedMac = keccak256(macData).slice(2);
|
|
120
|
+
if (calculatedMac.toLowerCase() !== cryptoData.mac.toLowerCase()) {
|
|
121
|
+
throw new Error('Invalid password or corrupted keystore');
|
|
122
|
+
}
|
|
123
|
+
// Decrypt private key with AES-128-CTR
|
|
124
|
+
const encryptionKey = derivedKey.slice(0, 16);
|
|
125
|
+
const cipher = ctr(encryptionKey, iv);
|
|
126
|
+
const privateKeyBytes = cipher.decrypt(ciphertext);
|
|
127
|
+
return toHex(privateKeyBytes);
|
|
128
|
+
}
|
|
45
129
|
// ---------------------------------------------------------------------------
|
|
46
130
|
// Proxy backend — HMAC-authenticated HTTP to a keyring proxy server
|
|
47
131
|
// ---------------------------------------------------------------------------
|
|
@@ -86,20 +170,17 @@ export async function detectBackend() {
|
|
|
86
170
|
return 'encrypted-file';
|
|
87
171
|
}
|
|
88
172
|
// ---------------------------------------------------------------------------
|
|
89
|
-
// Encrypted V3 JSON Keystore backend
|
|
173
|
+
// Encrypted V3 JSON Keystore backend
|
|
90
174
|
// ---------------------------------------------------------------------------
|
|
91
|
-
async function encryptedFileStore(privateKey,
|
|
92
|
-
const
|
|
93
|
-
// ethers v6: encryptKeystoreJsonSync or encryptKeystoreJson
|
|
94
|
-
const json = await ethers.encryptKeystoreJson(account, password);
|
|
175
|
+
async function encryptedFileStore(privateKey, password, filePath) {
|
|
176
|
+
const json = await encryptKeystore(privateKey, password);
|
|
95
177
|
fs.writeFileSync(filePath, json, { mode: 0o600 }); // Owner-only read/write
|
|
96
178
|
}
|
|
97
179
|
async function encryptedFileLoad(password, filePath) {
|
|
98
180
|
if (!fs.existsSync(filePath))
|
|
99
181
|
return null;
|
|
100
182
|
const json = fs.readFileSync(filePath, 'utf-8');
|
|
101
|
-
|
|
102
|
-
return wallet.privateKey;
|
|
183
|
+
return decryptKeystore(json, password);
|
|
103
184
|
}
|
|
104
185
|
function encryptedFileExists(filePath) {
|
|
105
186
|
return fs.existsSync(filePath);
|
|
@@ -116,8 +197,8 @@ function deriveMachinePassword() {
|
|
|
116
197
|
const factors = [
|
|
117
198
|
process.env.USER || process.env.USERNAME || 'agent',
|
|
118
199
|
process.env.HOME || process.env.USERPROFILE || '/tmp',
|
|
119
|
-
|
|
120
|
-
|
|
200
|
+
os.hostname(),
|
|
201
|
+
os.platform(),
|
|
121
202
|
];
|
|
122
203
|
return crypto
|
|
123
204
|
.createHash('sha256')
|
|
@@ -138,13 +219,13 @@ export async function createWallet(config = {}) {
|
|
|
138
219
|
const data = await proxyRequest(config, '/create-wallet');
|
|
139
220
|
return { address: data.address, backend, keystorePath: undefined };
|
|
140
221
|
}
|
|
141
|
-
const
|
|
142
|
-
const
|
|
143
|
-
const address =
|
|
222
|
+
const privateKey = generatePrivateKey();
|
|
223
|
+
const account = privateKeyToAccount(privateKey);
|
|
224
|
+
const address = account.address;
|
|
144
225
|
switch (backend) {
|
|
145
226
|
case 'encrypted-file': {
|
|
146
227
|
const password = config.password || process.env.KEYSTORE_PASSWORD || deriveMachinePassword();
|
|
147
|
-
await encryptedFileStore(privateKey,
|
|
228
|
+
await encryptedFileStore(privateKey, password, keystorePath);
|
|
148
229
|
break;
|
|
149
230
|
}
|
|
150
231
|
case 'env':
|
|
@@ -173,12 +254,13 @@ export async function importWallet(privateKey, config = {}) {
|
|
|
173
254
|
throw new Error('importWallet() is not supported via proxy. Import the wallet on the proxy server directly.');
|
|
174
255
|
}
|
|
175
256
|
const keystorePath = config.keystorePath || process.env.KEYSTORE_PATH || DEFAULT_KEYSTORE_PATH;
|
|
176
|
-
const
|
|
177
|
-
const
|
|
257
|
+
const hexKey = (privateKey.startsWith('0x') ? privateKey : `0x${privateKey}`);
|
|
258
|
+
const account = privateKeyToAccount(hexKey);
|
|
259
|
+
const address = account.address;
|
|
178
260
|
switch (backend) {
|
|
179
261
|
case 'encrypted-file': {
|
|
180
262
|
const password = config.password || process.env.KEYSTORE_PASSWORD || deriveMachinePassword();
|
|
181
|
-
await encryptedFileStore(
|
|
263
|
+
await encryptedFileStore(hexKey, password, keystorePath);
|
|
182
264
|
break;
|
|
183
265
|
}
|
|
184
266
|
case 'env':
|
|
@@ -217,12 +299,11 @@ export async function getAddress(config = {}) {
|
|
|
217
299
|
const data = await proxyRequest(config, '/get-address');
|
|
218
300
|
return data.address;
|
|
219
301
|
}
|
|
220
|
-
const
|
|
221
|
-
if (!
|
|
302
|
+
const privateKey = await _loadPrivateKeyInternal(config);
|
|
303
|
+
if (!privateKey)
|
|
222
304
|
return null;
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
return address;
|
|
305
|
+
const account = privateKeyToAccount(privateKey);
|
|
306
|
+
return account.address;
|
|
226
307
|
}
|
|
227
308
|
/**
|
|
228
309
|
* Sign a message (EIP-191 personal_sign).
|
|
@@ -235,13 +316,12 @@ export async function signMessage(message, config = {}) {
|
|
|
235
316
|
const data = await proxyRequest(config, '/sign-message', { message });
|
|
236
317
|
return { signature: data.signature, address: data.address };
|
|
237
318
|
}
|
|
238
|
-
const
|
|
239
|
-
if (!
|
|
319
|
+
const privateKey = await _loadPrivateKeyInternal(config);
|
|
320
|
+
if (!privateKey)
|
|
240
321
|
throw new Error('No wallet found. Run createWallet() first.');
|
|
241
|
-
const
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
return { signature, address };
|
|
322
|
+
const account = privateKeyToAccount(privateKey);
|
|
323
|
+
const signature = await account.signMessage({ message });
|
|
324
|
+
return { signature, address: account.address };
|
|
245
325
|
}
|
|
246
326
|
/**
|
|
247
327
|
* Sign a transaction.
|
|
@@ -254,16 +334,37 @@ export async function signTransaction(tx, config = {}) {
|
|
|
254
334
|
const data = await proxyRequest(config, '/sign-transaction', { tx: tx });
|
|
255
335
|
return { signedTx: data.signedTx, address: data.address };
|
|
256
336
|
}
|
|
257
|
-
const
|
|
258
|
-
if (!
|
|
337
|
+
const privateKey = await _loadPrivateKeyInternal(config);
|
|
338
|
+
if (!privateKey)
|
|
259
339
|
throw new Error('No wallet found. Run createWallet() first.');
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
340
|
+
const account = privateKeyToAccount(privateKey);
|
|
341
|
+
// Build transaction request for viem
|
|
342
|
+
const viemTx = {
|
|
343
|
+
to: tx.to,
|
|
344
|
+
data: tx.data,
|
|
345
|
+
value: tx.value,
|
|
346
|
+
nonce: tx.nonce,
|
|
347
|
+
chainId: tx.chainId,
|
|
348
|
+
gas: tx.gasLimit ?? tx.gas,
|
|
349
|
+
};
|
|
350
|
+
// Handle EIP-1559 vs legacy transactions
|
|
351
|
+
if (tx.type === 2 || tx.maxFeePerGas !== undefined) {
|
|
352
|
+
viemTx.type = 'eip1559';
|
|
353
|
+
viemTx.maxFeePerGas = tx.maxFeePerGas;
|
|
354
|
+
viemTx.maxPriorityFeePerGas = tx.maxPriorityFeePerGas;
|
|
355
|
+
}
|
|
356
|
+
else if (tx.gasPrice !== undefined) {
|
|
357
|
+
viemTx.type = 'legacy';
|
|
358
|
+
viemTx.gasPrice = tx.gasPrice;
|
|
359
|
+
}
|
|
360
|
+
if (tx.accessList) {
|
|
361
|
+
viemTx.accessList = tx.accessList;
|
|
362
|
+
}
|
|
363
|
+
const signedTx = await account.signTransaction(viemTx);
|
|
364
|
+
return { signedTx, address: account.address };
|
|
263
365
|
}
|
|
264
366
|
/**
|
|
265
367
|
* Sign an EIP-7702 authorization for delegating the EOA to a contract.
|
|
266
|
-
* Requires ethers >= 6.14.3.
|
|
267
368
|
*
|
|
268
369
|
* This allows the agent's EOA to temporarily act as a smart contract
|
|
269
370
|
* during a type 4 transaction. The private key is loaded, used, and
|
|
@@ -278,37 +379,54 @@ export async function signAuthorization(auth, config = {}) {
|
|
|
278
379
|
const data = await proxyRequest(config, '/sign-authorization', { auth });
|
|
279
380
|
return data;
|
|
280
381
|
}
|
|
281
|
-
const
|
|
282
|
-
if (!
|
|
382
|
+
const privateKey = await _loadPrivateKeyInternal(config);
|
|
383
|
+
if (!privateKey)
|
|
283
384
|
throw new Error('No wallet found. Run createWallet() first.');
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
385
|
+
const account = privateKeyToAccount(privateKey);
|
|
386
|
+
// EIP-7702 authorization signing using viem experimental
|
|
387
|
+
const chainId = auth.chainId ?? 1;
|
|
388
|
+
const nonce = auth.nonce ?? 0;
|
|
389
|
+
// Hash the authorization struct according to EIP-7702
|
|
390
|
+
const authHash = hashAuthorization({
|
|
391
|
+
contractAddress: auth.address,
|
|
392
|
+
chainId,
|
|
393
|
+
nonce,
|
|
293
394
|
});
|
|
294
|
-
//
|
|
295
|
-
|
|
395
|
+
// Sign the authorization hash
|
|
396
|
+
const signature = await account.sign({ hash: authHash });
|
|
397
|
+
// Parse signature into r, s, yParity
|
|
398
|
+
const r = signature.slice(0, 66);
|
|
399
|
+
const s = `0x${signature.slice(66, 130)}`;
|
|
400
|
+
const v = parseInt(signature.slice(130, 132), 16);
|
|
401
|
+
const yParity = v - 27; // Convert v to yParity (0 or 1)
|
|
402
|
+
return {
|
|
403
|
+
address: auth.address,
|
|
404
|
+
nonce,
|
|
405
|
+
chainId,
|
|
406
|
+
yParity,
|
|
407
|
+
r,
|
|
408
|
+
s,
|
|
409
|
+
};
|
|
296
410
|
}
|
|
297
411
|
/**
|
|
298
|
-
* Get a
|
|
299
|
-
* NOTE: This
|
|
412
|
+
* Get a wallet client for contract interactions.
|
|
413
|
+
* NOTE: This creates a client with the private key in memory.
|
|
300
414
|
* Use only within a narrow scope and discard immediately.
|
|
301
415
|
* Prefer signMessage() / signTransaction() when possible.
|
|
302
416
|
*/
|
|
303
|
-
export async function
|
|
417
|
+
export async function getWalletClient(rpcUrl, config = {}) {
|
|
304
418
|
const backend = config.backend || await detectBackend();
|
|
305
419
|
if (backend === 'proxy') {
|
|
306
|
-
throw new Error('
|
|
420
|
+
throw new Error('getWalletClient() is not supported via proxy. The private key cannot be serialized over HTTP. Use signMessage() or signTransaction() instead.');
|
|
307
421
|
}
|
|
308
|
-
const
|
|
309
|
-
if (!
|
|
422
|
+
const privateKey = await _loadPrivateKeyInternal(config);
|
|
423
|
+
if (!privateKey)
|
|
310
424
|
throw new Error('No wallet found. Run createWallet() first.');
|
|
311
|
-
|
|
425
|
+
const account = privateKeyToAccount(privateKey);
|
|
426
|
+
return createWalletClient({
|
|
427
|
+
account,
|
|
428
|
+
transport: http(rpcUrl),
|
|
429
|
+
});
|
|
312
430
|
}
|
|
313
431
|
/**
|
|
314
432
|
* Delete the stored wallet from the active backend.
|
|
@@ -333,9 +451,9 @@ export async function deleteWallet(config = {}) {
|
|
|
333
451
|
}
|
|
334
452
|
}
|
|
335
453
|
// ---------------------------------------------------------------------------
|
|
336
|
-
// Internal — loads the
|
|
454
|
+
// Internal — loads the private key. NEVER exposed publicly.
|
|
337
455
|
// ---------------------------------------------------------------------------
|
|
338
|
-
async function
|
|
456
|
+
async function _loadPrivateKeyInternal(config = {}) {
|
|
339
457
|
const backend = config.backend || await detectBackend();
|
|
340
458
|
const keystorePath = config.keystorePath || process.env.KEYSTORE_PATH || DEFAULT_KEYSTORE_PATH;
|
|
341
459
|
let privateKey = null;
|
|
@@ -345,11 +463,13 @@ async function _loadWalletInternal(config = {}) {
|
|
|
345
463
|
privateKey = await encryptedFileLoad(password, keystorePath);
|
|
346
464
|
break;
|
|
347
465
|
}
|
|
348
|
-
case 'env':
|
|
349
|
-
|
|
466
|
+
case 'env': {
|
|
467
|
+
const envKey = process.env.AGENT_PRIVATE_KEY || null;
|
|
468
|
+
if (envKey) {
|
|
469
|
+
privateKey = (envKey.startsWith('0x') ? envKey : `0x${envKey}`);
|
|
470
|
+
}
|
|
350
471
|
break;
|
|
472
|
+
}
|
|
351
473
|
}
|
|
352
|
-
|
|
353
|
-
return null;
|
|
354
|
-
return new ethers.Wallet(privateKey);
|
|
474
|
+
return privateKey;
|
|
355
475
|
}
|
package/dist/proxy-auth.d.ts
CHANGED
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
* - Constant-time comparison via crypto.timingSafeEqual
|
|
11
11
|
*/
|
|
12
12
|
export interface HmacHeaders {
|
|
13
|
-
'X-
|
|
14
|
-
'X-
|
|
13
|
+
'X-Keyring-Timestamp': string;
|
|
14
|
+
'X-Keyring-Signature': string;
|
|
15
15
|
}
|
|
16
16
|
/**
|
|
17
17
|
* Compute HMAC-SHA256 headers for an outgoing request.
|
package/dist/proxy-auth.js
CHANGED
|
@@ -22,8 +22,8 @@ export function computeHmac(secret, method, path, body) {
|
|
|
22
22
|
.update(payload)
|
|
23
23
|
.digest('hex');
|
|
24
24
|
return {
|
|
25
|
-
'X-
|
|
26
|
-
'X-
|
|
25
|
+
'X-Keyring-Timestamp': timestamp,
|
|
26
|
+
'X-Keyring-Signature': signature,
|
|
27
27
|
};
|
|
28
28
|
}
|
|
29
29
|
/**
|
|
@@ -46,8 +46,8 @@ export function verifyHmac(secret, method, path, body, timestamp, signature) {
|
|
|
46
46
|
.update(payload)
|
|
47
47
|
.digest('hex');
|
|
48
48
|
// Constant-time comparison
|
|
49
|
-
const sigBuf = Buffer.from(signature, 'utf-8');
|
|
50
|
-
const expBuf = Buffer.from(expected, 'utf-8');
|
|
49
|
+
const sigBuf = new Uint8Array(Buffer.from(signature, 'utf-8'));
|
|
50
|
+
const expBuf = new Uint8Array(Buffer.from(expected, 'utf-8'));
|
|
51
51
|
if (sigBuf.length !== expBuf.length) {
|
|
52
52
|
return { valid: false, error: 'Signature mismatch' };
|
|
53
53
|
}
|
package/dist/registry.d.ts
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
* Provides functions to read agent profiles and reputation from on-chain registries.
|
|
6
6
|
*
|
|
7
7
|
* Dependencies:
|
|
8
|
-
* npm install
|
|
8
|
+
* npm install viem
|
|
9
9
|
*/
|
|
10
|
-
import {
|
|
10
|
+
import { type PublicClient } from 'viem';
|
|
11
11
|
/** Service endpoint types defined in ERC-8004 */
|
|
12
12
|
export type ServiceType = 'web' | 'A2A' | 'MCP' | 'OASF' | 'ENS' | 'DID' | 'email';
|
|
13
13
|
/** Trust models defined in ERC-8004 */
|
|
@@ -42,7 +42,7 @@ export interface AgentProfile {
|
|
|
42
42
|
}
|
|
43
43
|
export interface GetAgentOptions {
|
|
44
44
|
registryAddress: string;
|
|
45
|
-
|
|
45
|
+
client: PublicClient;
|
|
46
46
|
fetchMetadata?: boolean;
|
|
47
47
|
}
|
|
48
48
|
export interface ReputationSummary {
|
|
@@ -53,7 +53,7 @@ export interface ReputationSummary {
|
|
|
53
53
|
}
|
|
54
54
|
export interface GetReputationOptions {
|
|
55
55
|
reputationRegistryAddress: string;
|
|
56
|
-
|
|
56
|
+
client: PublicClient;
|
|
57
57
|
clients?: string[];
|
|
58
58
|
tag1?: ReputationTag | (string & {});
|
|
59
59
|
tag2?: string;
|
|
@@ -62,13 +62,13 @@ export interface GetReputationOptions {
|
|
|
62
62
|
* Read an agent from the Identity Registry and parse its profile.
|
|
63
63
|
*
|
|
64
64
|
* @param agentId The on-chain agent token ID
|
|
65
|
-
* @param options Registry address,
|
|
65
|
+
* @param options Registry address, client, and optional fetchMetadata flag
|
|
66
66
|
*/
|
|
67
67
|
export declare function getAgent(agentId: number, options: GetAgentOptions): Promise<AgentProfile>;
|
|
68
68
|
/**
|
|
69
69
|
* Read an agent's reputation summary from the Reputation Registry.
|
|
70
70
|
*
|
|
71
71
|
* @param agentId The on-chain agent token ID
|
|
72
|
-
* @param options Reputation registry address,
|
|
72
|
+
* @param options Reputation registry address, client, and optional filters
|
|
73
73
|
*/
|
|
74
74
|
export declare function getReputation(agentId: number, options: GetReputationOptions): Promise<ReputationSummary>;
|
package/dist/registry.js
CHANGED
|
@@ -5,17 +5,50 @@
|
|
|
5
5
|
* Provides functions to read agent profiles and reputation from on-chain registries.
|
|
6
6
|
*
|
|
7
7
|
* Dependencies:
|
|
8
|
-
* npm install
|
|
8
|
+
* npm install viem
|
|
9
9
|
*/
|
|
10
|
-
import {
|
|
10
|
+
import { zeroAddress, } from 'viem';
|
|
11
11
|
// ─── ABI Fragments ──────────────────────────────────────────────────
|
|
12
12
|
const IDENTITY_REGISTRY_ABI = [
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
{
|
|
14
|
+
name: 'ownerOf',
|
|
15
|
+
type: 'function',
|
|
16
|
+
stateMutability: 'view',
|
|
17
|
+
inputs: [{ name: 'tokenId', type: 'uint256' }],
|
|
18
|
+
outputs: [{ name: '', type: 'address' }],
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: 'tokenURI',
|
|
22
|
+
type: 'function',
|
|
23
|
+
stateMutability: 'view',
|
|
24
|
+
inputs: [{ name: 'tokenId', type: 'uint256' }],
|
|
25
|
+
outputs: [{ name: '', type: 'string' }],
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: 'getAgentWallet',
|
|
29
|
+
type: 'function',
|
|
30
|
+
stateMutability: 'view',
|
|
31
|
+
inputs: [{ name: 'agentId', type: 'uint256' }],
|
|
32
|
+
outputs: [{ name: '', type: 'address' }],
|
|
33
|
+
},
|
|
16
34
|
];
|
|
17
35
|
const REPUTATION_REGISTRY_ABI = [
|
|
18
|
-
|
|
36
|
+
{
|
|
37
|
+
name: 'getSummary',
|
|
38
|
+
type: 'function',
|
|
39
|
+
stateMutability: 'view',
|
|
40
|
+
inputs: [
|
|
41
|
+
{ name: 'agentId', type: 'uint256' },
|
|
42
|
+
{ name: 'clients', type: 'address[]' },
|
|
43
|
+
{ name: 'tag1', type: 'string' },
|
|
44
|
+
{ name: 'tag2', type: 'string' },
|
|
45
|
+
],
|
|
46
|
+
outputs: [
|
|
47
|
+
{ name: 'count', type: 'uint64' },
|
|
48
|
+
{ name: 'summaryValue', type: 'int128' },
|
|
49
|
+
{ name: 'valueDecimals', type: 'uint8' },
|
|
50
|
+
],
|
|
51
|
+
},
|
|
19
52
|
];
|
|
20
53
|
// ─── Internal Helpers ───────────────────────────────────────────────
|
|
21
54
|
const IPFS_GATEWAY = 'https://gateway.pinata.cloud/ipfs/';
|
|
@@ -49,17 +82,31 @@ async function resolveURI(uri) {
|
|
|
49
82
|
* Read an agent from the Identity Registry and parse its profile.
|
|
50
83
|
*
|
|
51
84
|
* @param agentId The on-chain agent token ID
|
|
52
|
-
* @param options Registry address,
|
|
85
|
+
* @param options Registry address, client, and optional fetchMetadata flag
|
|
53
86
|
*/
|
|
54
87
|
export async function getAgent(agentId, options) {
|
|
55
|
-
const { registryAddress,
|
|
56
|
-
const registry = new ethers.Contract(registryAddress, IDENTITY_REGISTRY_ABI, provider);
|
|
88
|
+
const { registryAddress, client, fetchMetadata = true } = options;
|
|
57
89
|
const [owner, uri, walletAddr] = await Promise.all([
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
90
|
+
client.readContract({
|
|
91
|
+
address: registryAddress,
|
|
92
|
+
abi: IDENTITY_REGISTRY_ABI,
|
|
93
|
+
functionName: 'ownerOf',
|
|
94
|
+
args: [BigInt(agentId)],
|
|
95
|
+
}),
|
|
96
|
+
client.readContract({
|
|
97
|
+
address: registryAddress,
|
|
98
|
+
abi: IDENTITY_REGISTRY_ABI,
|
|
99
|
+
functionName: 'tokenURI',
|
|
100
|
+
args: [BigInt(agentId)],
|
|
101
|
+
}),
|
|
102
|
+
client.readContract({
|
|
103
|
+
address: registryAddress,
|
|
104
|
+
abi: IDENTITY_REGISTRY_ABI,
|
|
105
|
+
functionName: 'getAgentWallet',
|
|
106
|
+
args: [BigInt(agentId)],
|
|
107
|
+
}),
|
|
61
108
|
]);
|
|
62
|
-
const agentWallet = walletAddr ===
|
|
109
|
+
const agentWallet = walletAddr === zeroAddress ? null : walletAddr;
|
|
63
110
|
let metadata = null;
|
|
64
111
|
if (fetchMetadata) {
|
|
65
112
|
try {
|
|
@@ -76,12 +123,17 @@ export async function getAgent(agentId, options) {
|
|
|
76
123
|
* Read an agent's reputation summary from the Reputation Registry.
|
|
77
124
|
*
|
|
78
125
|
* @param agentId The on-chain agent token ID
|
|
79
|
-
* @param options Reputation registry address,
|
|
126
|
+
* @param options Reputation registry address, client, and optional filters
|
|
80
127
|
*/
|
|
81
128
|
export async function getReputation(agentId, options) {
|
|
82
|
-
const { reputationRegistryAddress,
|
|
83
|
-
const
|
|
84
|
-
|
|
129
|
+
const { reputationRegistryAddress, client, clients = [], tag1 = '', tag2 = '', } = options;
|
|
130
|
+
const result = await client.readContract({
|
|
131
|
+
address: reputationRegistryAddress,
|
|
132
|
+
abi: REPUTATION_REGISTRY_ABI,
|
|
133
|
+
functionName: 'getSummary',
|
|
134
|
+
args: [BigInt(agentId), clients, tag1, tag2],
|
|
135
|
+
});
|
|
136
|
+
const [count, summaryValue, valueDecimals] = result;
|
|
85
137
|
const decimals = Number(valueDecimals);
|
|
86
138
|
const rawValue = BigInt(summaryValue);
|
|
87
139
|
const score = Number(rawValue) / 10 ** decimals;
|
package/dist/siwa.d.ts
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
* Provides message building, signing (agent-side), and verification (server-side).
|
|
6
6
|
*
|
|
7
7
|
* Dependencies:
|
|
8
|
-
* npm install
|
|
8
|
+
* npm install viem
|
|
9
9
|
*/
|
|
10
|
-
import {
|
|
10
|
+
import { type PublicClient } from 'viem';
|
|
11
11
|
import { AgentProfile, ServiceType, TrustModel } from './registry.js';
|
|
12
12
|
export interface SIWAMessageFields {
|
|
13
13
|
domain: string;
|
|
@@ -93,7 +93,7 @@ export declare function signSIWAMessageUnsafe(privateKey: string, fields: SIWAMe
|
|
|
93
93
|
* @param signature EIP-191 signature hex string
|
|
94
94
|
* @param expectedDomain The server's domain (for domain binding)
|
|
95
95
|
* @param nonceValid Callback that returns true if the nonce is valid and unconsumed
|
|
96
|
-
* @param
|
|
96
|
+
* @param client viem PublicClient for onchain verification
|
|
97
97
|
* @param criteria Optional criteria to validate agent profile/reputation after ownership check
|
|
98
98
|
*/
|
|
99
|
-
export declare function verifySIWA(message: string, signature: string, expectedDomain: string, nonceValid: (nonce: string) => boolean | Promise<boolean>,
|
|
99
|
+
export declare function verifySIWA(message: string, signature: string, expectedDomain: string, nonceValid: (nonce: string) => boolean | Promise<boolean>, client: PublicClient, criteria?: SIWAVerifyCriteria): Promise<SIWAVerificationResult>;
|
package/dist/siwa.js
CHANGED
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
* Provides message building, signing (agent-side), and verification (server-side).
|
|
6
6
|
*
|
|
7
7
|
* Dependencies:
|
|
8
|
-
* npm install
|
|
8
|
+
* npm install viem
|
|
9
9
|
*/
|
|
10
|
-
import {
|
|
10
|
+
import { verifyMessage, hashMessage, } from 'viem';
|
|
11
|
+
import { privateKeyToAccount } from 'viem/accounts';
|
|
11
12
|
import * as crypto from 'crypto';
|
|
12
13
|
import { getAgent, getReputation } from './registry.js';
|
|
13
14
|
// ─── Message Construction ────────────────────────────────────────────
|
|
@@ -136,12 +137,13 @@ export async function signSIWAMessage(fields, keystoreConfig) {
|
|
|
136
137
|
* Kept only for server-side testing or environments without keystore.
|
|
137
138
|
*/
|
|
138
139
|
export async function signSIWAMessageUnsafe(privateKey, fields) {
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
140
|
+
const hexKey = (privateKey.startsWith('0x') ? privateKey : `0x${privateKey}`);
|
|
141
|
+
const account = privateKeyToAccount(hexKey);
|
|
142
|
+
if (account.address.toLowerCase() !== fields.address.toLowerCase()) {
|
|
143
|
+
throw new Error(`Address mismatch: wallet is ${account.address}, message says ${fields.address}`);
|
|
142
144
|
}
|
|
143
145
|
const message = buildSIWAMessage(fields);
|
|
144
|
-
const signature = await
|
|
146
|
+
const signature = await account.signMessage({ message });
|
|
145
147
|
return { message, signature };
|
|
146
148
|
}
|
|
147
149
|
// ─── Server-Side Verification ────────────────────────────────────────
|
|
@@ -161,19 +163,24 @@ export async function signSIWAMessageUnsafe(privateKey, fields) {
|
|
|
161
163
|
* @param signature EIP-191 signature hex string
|
|
162
164
|
* @param expectedDomain The server's domain (for domain binding)
|
|
163
165
|
* @param nonceValid Callback that returns true if the nonce is valid and unconsumed
|
|
164
|
-
* @param
|
|
166
|
+
* @param client viem PublicClient for onchain verification
|
|
165
167
|
* @param criteria Optional criteria to validate agent profile/reputation after ownership check
|
|
166
168
|
*/
|
|
167
|
-
export async function verifySIWA(message, signature, expectedDomain, nonceValid,
|
|
169
|
+
export async function verifySIWA(message, signature, expectedDomain, nonceValid, client, criteria) {
|
|
168
170
|
try {
|
|
169
171
|
// 1. Parse
|
|
170
172
|
const fields = parseSIWAMessage(message);
|
|
171
173
|
// 2. Recover signer
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
174
|
+
const isValid = await verifyMessage({
|
|
175
|
+
address: fields.address,
|
|
176
|
+
message,
|
|
177
|
+
signature: signature,
|
|
178
|
+
});
|
|
179
|
+
if (!isValid) {
|
|
180
|
+
return { valid: false, address: fields.address, agentId: fields.agentId, agentRegistry: fields.agentRegistry, chainId: fields.chainId, error: 'Invalid signature' };
|
|
176
181
|
}
|
|
182
|
+
const recovered = fields.address;
|
|
183
|
+
// 3. Address match is implicit in verifyMessage (it checks against the address)
|
|
177
184
|
// 4. Domain binding
|
|
178
185
|
if (fields.domain !== expectedDomain) {
|
|
179
186
|
return { valid: false, address: recovered, agentId: fields.agentId, agentRegistry: fields.agentRegistry, chainId: fields.chainId, error: `Domain mismatch: expected ${expectedDomain}, got ${fields.domain}` };
|
|
@@ -197,16 +204,24 @@ export async function verifySIWA(message, signature, expectedDomain, nonceValid,
|
|
|
197
204
|
return { valid: false, address: recovered, agentId: fields.agentId, agentRegistry: fields.agentRegistry, chainId: fields.chainId, error: 'Invalid agentRegistry format' };
|
|
198
205
|
}
|
|
199
206
|
const registryAddress = registryParts[2];
|
|
200
|
-
const
|
|
201
|
-
|
|
207
|
+
const owner = await client.readContract({
|
|
208
|
+
address: registryAddress,
|
|
209
|
+
abi: [{ name: 'ownerOf', type: 'function', stateMutability: 'view', inputs: [{ name: 'tokenId', type: 'uint256' }], outputs: [{ name: '', type: 'address' }] }],
|
|
210
|
+
functionName: 'ownerOf',
|
|
211
|
+
args: [BigInt(fields.agentId)],
|
|
212
|
+
});
|
|
202
213
|
if (owner.toLowerCase() !== recovered.toLowerCase()) {
|
|
203
214
|
// 7b. ERC-1271 fallback for smart contract wallets / EIP-7702 delegated accounts.
|
|
204
215
|
// If ecrecover doesn't match the NFT owner, the owner may be a contract
|
|
205
216
|
// that validates signatures via isValidSignature (ERC-1271).
|
|
206
|
-
const messageHash =
|
|
217
|
+
const messageHash = hashMessage(message);
|
|
207
218
|
try {
|
|
208
|
-
const
|
|
209
|
-
|
|
219
|
+
const magicValue = await client.readContract({
|
|
220
|
+
address: owner,
|
|
221
|
+
abi: [{ name: 'isValidSignature', type: 'function', stateMutability: 'view', inputs: [{ name: 'hash', type: 'bytes32' }, { name: 'signature', type: 'bytes' }], outputs: [{ name: '', type: 'bytes4' }] }],
|
|
222
|
+
functionName: 'isValidSignature',
|
|
223
|
+
args: [messageHash, signature],
|
|
224
|
+
});
|
|
210
225
|
// ERC-1271 magic value: 0x1626ba7e
|
|
211
226
|
if (magicValue !== '0x1626ba7e') {
|
|
212
227
|
return { valid: false, address: recovered, agentId: fields.agentId, agentRegistry: fields.agentRegistry, chainId: fields.chainId, error: 'Signer is not the owner of this agent NFT (ERC-1271 check also failed)' };
|
|
@@ -230,7 +245,7 @@ export async function verifySIWA(message, signature, expectedDomain, nonceValid,
|
|
|
230
245
|
return baseResult;
|
|
231
246
|
const agent = await getAgent(fields.agentId, {
|
|
232
247
|
registryAddress: registryAddress,
|
|
233
|
-
|
|
248
|
+
client,
|
|
234
249
|
fetchMetadata: true,
|
|
235
250
|
});
|
|
236
251
|
baseResult.agent = agent;
|
|
@@ -261,7 +276,7 @@ export async function verifySIWA(message, signature, expectedDomain, nonceValid,
|
|
|
261
276
|
}
|
|
262
277
|
const rep = await getReputation(fields.agentId, {
|
|
263
278
|
reputationRegistryAddress: criteria.reputationRegistryAddress,
|
|
264
|
-
|
|
279
|
+
client,
|
|
265
280
|
});
|
|
266
281
|
if (criteria.minFeedbackCount !== undefined && rep.count < criteria.minFeedbackCount) {
|
|
267
282
|
return { ...baseResult, valid: false, error: `Agent feedback count ${rep.count} below minimum ${criteria.minFeedbackCount}` };
|
package/package.json
CHANGED
|
@@ -1,27 +1,56 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@buildersgarden/siwa",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
|
-
".": {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
"./
|
|
11
|
-
|
|
6
|
+
".": {
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"default": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"./keystore": {
|
|
11
|
+
"types": "./dist/keystore.d.ts",
|
|
12
|
+
"default": "./dist/keystore.js"
|
|
13
|
+
},
|
|
14
|
+
"./siwa": {
|
|
15
|
+
"types": "./dist/siwa.d.ts",
|
|
16
|
+
"default": "./dist/siwa.js"
|
|
17
|
+
},
|
|
18
|
+
"./memory": {
|
|
19
|
+
"types": "./dist/memory.d.ts",
|
|
20
|
+
"default": "./dist/memory.js"
|
|
21
|
+
},
|
|
22
|
+
"./proxy-auth": {
|
|
23
|
+
"types": "./dist/proxy-auth.d.ts",
|
|
24
|
+
"default": "./dist/proxy-auth.js"
|
|
25
|
+
},
|
|
26
|
+
"./registry": {
|
|
27
|
+
"types": "./dist/registry.d.ts",
|
|
28
|
+
"default": "./dist/registry.js"
|
|
29
|
+
},
|
|
30
|
+
"./addresses": {
|
|
31
|
+
"types": "./dist/addresses.d.ts",
|
|
32
|
+
"default": "./dist/addresses.js"
|
|
33
|
+
}
|
|
12
34
|
},
|
|
13
35
|
"main": "./dist/index.js",
|
|
14
36
|
"types": "./dist/index.d.ts",
|
|
15
|
-
"files": [
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
"
|
|
37
|
+
"files": [
|
|
38
|
+
"dist"
|
|
39
|
+
],
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public"
|
|
20
42
|
},
|
|
21
43
|
"dependencies": {
|
|
22
|
-
"
|
|
44
|
+
"@noble/ciphers": "^0.5.0",
|
|
45
|
+
"@noble/hashes": "^1.4.0",
|
|
46
|
+
"viem": "^2.21.0"
|
|
23
47
|
},
|
|
24
48
|
"devDependencies": {
|
|
49
|
+
"@types/node": "^25.2.1",
|
|
25
50
|
"typescript": "^5.5.0"
|
|
51
|
+
},
|
|
52
|
+
"scripts": {
|
|
53
|
+
"build": "tsc",
|
|
54
|
+
"clean": "rm -rf dist"
|
|
26
55
|
}
|
|
27
|
-
}
|
|
56
|
+
}
|