@buildersgarden/siwa 0.0.7 → 0.0.9
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 +7 -4
- package/dist/erc8128.d.ts +104 -0
- package/dist/erc8128.js +260 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/keystore.d.ts +9 -0
- package/dist/keystore.js +15 -0
- package/dist/receipt.d.ts +53 -0
- package/dist/receipt.js +80 -0
- package/package.json +10 -1
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ A Claude Code skill for registering AI agents on the [ERC-8004 (Trustless Agents
|
|
|
6
6
|
|
|
7
7
|
- **Create Wallet** — Generate an Ethereum wallet via a keyring proxy (private key never enters the agent process)
|
|
8
8
|
- **Register Agent (Sign Up)** — Mint an ERC-721 identity NFT on the ERC-8004 Identity Registry with metadata (endpoints, trust model, services)
|
|
9
|
-
- **Authenticate (Sign In)** — Prove ownership of an onchain agent identity by signing a structured SIWA message; receive a
|
|
9
|
+
- **Authenticate (Sign In)** — Prove ownership of an onchain agent identity by signing a structured SIWA message; receive a verification receipt from the relying party and use ERC-8128 per-request signatures for subsequent API calls
|
|
10
10
|
|
|
11
11
|
## Project Structure
|
|
12
12
|
|
|
@@ -18,6 +18,8 @@ src/ Core SDK modules
|
|
|
18
18
|
proxy-auth.ts HMAC-SHA256 authentication utilities
|
|
19
19
|
registry.ts Onchain agent profile & reputation lookups
|
|
20
20
|
addresses.ts Deployed contract addresses
|
|
21
|
+
receipt.ts Stateless HMAC receipt creation and verification
|
|
22
|
+
erc8128.ts ERC-8128 HTTP Message Signatures (sign/verify)
|
|
21
23
|
|
|
22
24
|
references/ Protocol documentation
|
|
23
25
|
siwa-spec.md Full SIWA specification
|
|
@@ -26,13 +28,14 @@ references/ Protocol documentation
|
|
|
26
28
|
assets/ Templates
|
|
27
29
|
IDENTITY.template.md
|
|
28
30
|
|
|
29
|
-
test/ Local test environment (Express server + CLI agent)
|
|
30
31
|
```
|
|
31
32
|
|
|
32
33
|
## Quick Start (Local Test)
|
|
33
34
|
|
|
35
|
+
The test harness lives in the `siwa-testing` package (sibling in this monorepo):
|
|
36
|
+
|
|
34
37
|
```bash
|
|
35
|
-
cd
|
|
38
|
+
cd packages/siwa-testing
|
|
36
39
|
pnpm install
|
|
37
40
|
|
|
38
41
|
# Terminal 1: Start the SIWA relying-party server
|
|
@@ -45,7 +48,7 @@ pnpm run agent:flow
|
|
|
45
48
|
pnpm run dev
|
|
46
49
|
```
|
|
47
50
|
|
|
48
|
-
See [`
|
|
51
|
+
See [`packages/siwa-testing/README.md`](../siwa-testing/README.md) for full details on the test environment.
|
|
49
52
|
|
|
50
53
|
## Security Model
|
|
51
54
|
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* erc8128.ts
|
|
3
|
+
*
|
|
4
|
+
* Full ERC-8128 HTTP Message Signatures integration for SIWA.
|
|
5
|
+
*
|
|
6
|
+
* The SDK fully abstracts `@slicekit/erc8128`. Platform developers call:
|
|
7
|
+
* - signAuthenticatedRequest() — agent-side: attach receipt + sign request
|
|
8
|
+
* - verifyAuthenticatedRequest() — server-side: verify signature + receipt + optional onchain
|
|
9
|
+
*
|
|
10
|
+
* These are the two main entry points. Everything else is internal.
|
|
11
|
+
*/
|
|
12
|
+
import type { PublicClient } from 'viem';
|
|
13
|
+
import { type EthHttpSigner } from '@slicekit/erc8128';
|
|
14
|
+
import { type KeystoreConfig } from './keystore.js';
|
|
15
|
+
export interface VerifyOptions {
|
|
16
|
+
receiptSecret: string;
|
|
17
|
+
rpcUrl?: string;
|
|
18
|
+
verifyOnchain?: boolean;
|
|
19
|
+
publicClient?: PublicClient;
|
|
20
|
+
}
|
|
21
|
+
export type AuthResult = {
|
|
22
|
+
valid: true;
|
|
23
|
+
agent: {
|
|
24
|
+
address: string;
|
|
25
|
+
agentId: number;
|
|
26
|
+
agentRegistry: string;
|
|
27
|
+
chainId: number;
|
|
28
|
+
};
|
|
29
|
+
} | {
|
|
30
|
+
valid: false;
|
|
31
|
+
error: string;
|
|
32
|
+
};
|
|
33
|
+
/** Header name for the verification receipt */
|
|
34
|
+
export declare const RECEIPT_HEADER = "X-SIWA-Receipt";
|
|
35
|
+
/**
|
|
36
|
+
* Create an ERC-8128 signer backed by the keyring proxy.
|
|
37
|
+
*
|
|
38
|
+
* The `signMessage` callback converts the RFC 9421 signature base
|
|
39
|
+
* (Uint8Array) to a hex string and delegates to the proxy via
|
|
40
|
+
* `signRawMessage`, which signs with `{ raw: true }`.
|
|
41
|
+
*/
|
|
42
|
+
export declare function createProxySigner(config: KeystoreConfig, chainId: number): Promise<EthHttpSigner>;
|
|
43
|
+
/**
|
|
44
|
+
* Attach a verification receipt to a request.
|
|
45
|
+
*
|
|
46
|
+
* Sets the `X-SIWA-Receipt` header.
|
|
47
|
+
*/
|
|
48
|
+
export declare function attachReceipt(request: Request, receipt: string): Request;
|
|
49
|
+
/**
|
|
50
|
+
* Sign an authenticated request: attach receipt + ERC-8128 signature.
|
|
51
|
+
*
|
|
52
|
+
* This is the main function platform developers use on the agent side.
|
|
53
|
+
* One call does everything:
|
|
54
|
+
* 1. Attaches the receipt header
|
|
55
|
+
* 2. Creates a proxy-backed ERC-8128 signer
|
|
56
|
+
* 3. Signs the request with HTTP Message Signatures (RFC 9421)
|
|
57
|
+
*
|
|
58
|
+
* @param request The outgoing Request object
|
|
59
|
+
* @param receipt Verification receipt from SIWA sign-in
|
|
60
|
+
* @param config Keystore config (proxy URL + secret)
|
|
61
|
+
* @param chainId Chain ID for the ERC-8128 keyid
|
|
62
|
+
* @returns A new Request with Signature, Signature-Input, Content-Digest, and X-SIWA-Receipt headers
|
|
63
|
+
*/
|
|
64
|
+
export declare function signAuthenticatedRequest(request: Request, receipt: string, config: KeystoreConfig, chainId: number): Promise<Request>;
|
|
65
|
+
/**
|
|
66
|
+
* Verify an authenticated request: ERC-8128 signature + receipt + optional onchain check.
|
|
67
|
+
*
|
|
68
|
+
* This is the main function platform developers use on the server side.
|
|
69
|
+
* One call does everything:
|
|
70
|
+
* 1. Extracts and verifies the HMAC receipt
|
|
71
|
+
* 2. Verifies the ERC-8128 HTTP signature (recovers signer address)
|
|
72
|
+
* 3. Checks that the signer address matches the receipt address
|
|
73
|
+
* 4. Optionally does an onchain ownerOf check
|
|
74
|
+
*
|
|
75
|
+
* @param request The incoming Request object (with Signature + X-SIWA-Receipt headers)
|
|
76
|
+
* @param options Verification options (receipt secret, optional onchain settings)
|
|
77
|
+
* @returns `{ valid: true, agent }` or `{ valid: false, error }`
|
|
78
|
+
*/
|
|
79
|
+
export declare function verifyAuthenticatedRequest(request: Request, options: VerifyOptions): Promise<AuthResult>;
|
|
80
|
+
/**
|
|
81
|
+
* Convert an Express request to a Fetch API Request.
|
|
82
|
+
*
|
|
83
|
+
* Needed because ERC-8128 operates on the Fetch `Request` object,
|
|
84
|
+
* but Express uses its own request type.
|
|
85
|
+
*
|
|
86
|
+
* @param req Express request object (must have `rawBody` for Content-Digest verification)
|
|
87
|
+
*/
|
|
88
|
+
export declare function expressToFetchRequest(req: {
|
|
89
|
+
method: string;
|
|
90
|
+
protocol: string;
|
|
91
|
+
get: (name: string) => string | undefined;
|
|
92
|
+
originalUrl: string;
|
|
93
|
+
headers: Record<string, string | string[] | undefined>;
|
|
94
|
+
rawBody?: string;
|
|
95
|
+
}): Request;
|
|
96
|
+
/**
|
|
97
|
+
* Normalize a Next.js/serverless Request for ERC-8128 verification.
|
|
98
|
+
*
|
|
99
|
+
* Behind reverse proxies (Vercel, Railway, Cloudflare), the request URL
|
|
100
|
+
* may reflect internal routing instead of the public origin. This helper
|
|
101
|
+
* reads X-Forwarded-Host / X-Forwarded-Proto headers and reconstructs
|
|
102
|
+
* the URL to match what the agent signed.
|
|
103
|
+
*/
|
|
104
|
+
export declare function nextjsToFetchRequest(req: Request): Request;
|
package/dist/erc8128.js
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* erc8128.ts
|
|
3
|
+
*
|
|
4
|
+
* Full ERC-8128 HTTP Message Signatures integration for SIWA.
|
|
5
|
+
*
|
|
6
|
+
* The SDK fully abstracts `@slicekit/erc8128`. Platform developers call:
|
|
7
|
+
* - signAuthenticatedRequest() — agent-side: attach receipt + sign request
|
|
8
|
+
* - verifyAuthenticatedRequest() — server-side: verify signature + receipt + optional onchain
|
|
9
|
+
*
|
|
10
|
+
* These are the two main entry points. Everything else is internal.
|
|
11
|
+
*/
|
|
12
|
+
import { signRequest, verifyRequest, } from '@slicekit/erc8128';
|
|
13
|
+
import { signRawMessage, getAddress } from './keystore.js';
|
|
14
|
+
import { verifyReceipt } from './receipt.js';
|
|
15
|
+
/** Header name for the verification receipt */
|
|
16
|
+
export const RECEIPT_HEADER = 'X-SIWA-Receipt';
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Agent-side: signer creation
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
/**
|
|
21
|
+
* Create an ERC-8128 signer backed by the keyring proxy.
|
|
22
|
+
*
|
|
23
|
+
* The `signMessage` callback converts the RFC 9421 signature base
|
|
24
|
+
* (Uint8Array) to a hex string and delegates to the proxy via
|
|
25
|
+
* `signRawMessage`, which signs with `{ raw: true }`.
|
|
26
|
+
*/
|
|
27
|
+
export async function createProxySigner(config, chainId) {
|
|
28
|
+
const address = await getAddress(config);
|
|
29
|
+
if (!address)
|
|
30
|
+
throw new Error('No wallet found in keystore');
|
|
31
|
+
return {
|
|
32
|
+
address: address,
|
|
33
|
+
chainId,
|
|
34
|
+
signMessage: async (message) => {
|
|
35
|
+
const hex = ('0x' + Array.from(message).map(b => b.toString(16).padStart(2, '0')).join(''));
|
|
36
|
+
const result = await signRawMessage(hex, config);
|
|
37
|
+
return result.signature;
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Agent-side: high-level request signing
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
/**
|
|
45
|
+
* Attach a verification receipt to a request.
|
|
46
|
+
*
|
|
47
|
+
* Sets the `X-SIWA-Receipt` header.
|
|
48
|
+
*/
|
|
49
|
+
export function attachReceipt(request, receipt) {
|
|
50
|
+
const headers = new Headers(request.headers);
|
|
51
|
+
headers.set(RECEIPT_HEADER, receipt);
|
|
52
|
+
return new Request(request, { headers });
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Sign an authenticated request: attach receipt + ERC-8128 signature.
|
|
56
|
+
*
|
|
57
|
+
* This is the main function platform developers use on the agent side.
|
|
58
|
+
* One call does everything:
|
|
59
|
+
* 1. Attaches the receipt header
|
|
60
|
+
* 2. Creates a proxy-backed ERC-8128 signer
|
|
61
|
+
* 3. Signs the request with HTTP Message Signatures (RFC 9421)
|
|
62
|
+
*
|
|
63
|
+
* @param request The outgoing Request object
|
|
64
|
+
* @param receipt Verification receipt from SIWA sign-in
|
|
65
|
+
* @param config Keystore config (proxy URL + secret)
|
|
66
|
+
* @param chainId Chain ID for the ERC-8128 keyid
|
|
67
|
+
* @returns A new Request with Signature, Signature-Input, Content-Digest, and X-SIWA-Receipt headers
|
|
68
|
+
*/
|
|
69
|
+
export async function signAuthenticatedRequest(request, receipt, config, chainId) {
|
|
70
|
+
// 1. Attach receipt header
|
|
71
|
+
const withReceipt = attachReceipt(request, receipt);
|
|
72
|
+
// 2. Create proxy-backed signer
|
|
73
|
+
const signer = await createProxySigner(config, chainId);
|
|
74
|
+
// 3. Sign with ERC-8128 (includes Content-Digest for bodies)
|
|
75
|
+
return signRequest(withReceipt, signer);
|
|
76
|
+
}
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
// Server-side: high-level request verification
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
/**
|
|
81
|
+
* In-memory nonce store for ERC-8128 replay protection.
|
|
82
|
+
*
|
|
83
|
+
* Uses a Map with TTL-based expiry. For production, replace with Redis
|
|
84
|
+
* or another persistent store via the NonceStore interface.
|
|
85
|
+
*/
|
|
86
|
+
function createMemoryNonceStore() {
|
|
87
|
+
const seen = new Map(); // key → expiry timestamp (ms)
|
|
88
|
+
return {
|
|
89
|
+
async consume(key, ttlSeconds) {
|
|
90
|
+
// Lazy cleanup of expired entries
|
|
91
|
+
const now = Date.now();
|
|
92
|
+
for (const [k, expiry] of seen) {
|
|
93
|
+
if (expiry < now)
|
|
94
|
+
seen.delete(k);
|
|
95
|
+
}
|
|
96
|
+
if (seen.has(key))
|
|
97
|
+
return false; // replay
|
|
98
|
+
seen.set(key, now + ttlSeconds * 1000);
|
|
99
|
+
return true;
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/** Singleton nonce store — shared across the server process */
|
|
104
|
+
const nonceStore = createMemoryNonceStore();
|
|
105
|
+
/**
|
|
106
|
+
* Verify an authenticated request: ERC-8128 signature + receipt + optional onchain check.
|
|
107
|
+
*
|
|
108
|
+
* This is the main function platform developers use on the server side.
|
|
109
|
+
* One call does everything:
|
|
110
|
+
* 1. Extracts and verifies the HMAC receipt
|
|
111
|
+
* 2. Verifies the ERC-8128 HTTP signature (recovers signer address)
|
|
112
|
+
* 3. Checks that the signer address matches the receipt address
|
|
113
|
+
* 4. Optionally does an onchain ownerOf check
|
|
114
|
+
*
|
|
115
|
+
* @param request The incoming Request object (with Signature + X-SIWA-Receipt headers)
|
|
116
|
+
* @param options Verification options (receipt secret, optional onchain settings)
|
|
117
|
+
* @returns `{ valid: true, agent }` or `{ valid: false, error }`
|
|
118
|
+
*/
|
|
119
|
+
export async function verifyAuthenticatedRequest(request, options) {
|
|
120
|
+
// 1. Extract and verify receipt
|
|
121
|
+
const receiptToken = request.headers.get(RECEIPT_HEADER);
|
|
122
|
+
if (!receiptToken) {
|
|
123
|
+
return { valid: false, error: 'Missing X-SIWA-Receipt header' };
|
|
124
|
+
}
|
|
125
|
+
const receipt = verifyReceipt(receiptToken, options.receiptSecret);
|
|
126
|
+
if (!receipt) {
|
|
127
|
+
return { valid: false, error: 'Invalid or expired receipt' };
|
|
128
|
+
}
|
|
129
|
+
// 2. Verify ERC-8128 signature
|
|
130
|
+
const { verifyMessage } = await import('viem');
|
|
131
|
+
const verifyResult = await verifyRequest(request, async (args) => {
|
|
132
|
+
// If a publicClient is provided, use it for ERC-1271 support
|
|
133
|
+
if (options.publicClient) {
|
|
134
|
+
return options.publicClient.verifyMessage({
|
|
135
|
+
address: args.address,
|
|
136
|
+
message: args.message,
|
|
137
|
+
signature: args.signature,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
// Fallback to pure EOA verification
|
|
141
|
+
return verifyMessage({
|
|
142
|
+
address: args.address,
|
|
143
|
+
message: args.message,
|
|
144
|
+
signature: args.signature,
|
|
145
|
+
});
|
|
146
|
+
}, nonceStore);
|
|
147
|
+
if (!verifyResult.ok) {
|
|
148
|
+
return { valid: false, error: `ERC-8128 verification failed: ${verifyResult.reason}${verifyResult.detail ? ` (${verifyResult.detail})` : ''}` };
|
|
149
|
+
}
|
|
150
|
+
// 3. Address match: signer must match receipt
|
|
151
|
+
if (verifyResult.address.toLowerCase() !== receipt.address.toLowerCase()) {
|
|
152
|
+
return { valid: false, error: 'Signer address does not match receipt address' };
|
|
153
|
+
}
|
|
154
|
+
// 4. Optional onchain check
|
|
155
|
+
if (options.verifyOnchain) {
|
|
156
|
+
const client = options.publicClient ?? (await createOnchainClient(options.rpcUrl));
|
|
157
|
+
if (!client) {
|
|
158
|
+
return { valid: false, error: 'Onchain verification requested but no RPC URL or publicClient provided' };
|
|
159
|
+
}
|
|
160
|
+
const registryParts = receipt.agentRegistry.split(':');
|
|
161
|
+
if (registryParts.length !== 3 || registryParts[0] !== 'eip155') {
|
|
162
|
+
return { valid: false, error: 'Invalid agentRegistry format in receipt' };
|
|
163
|
+
}
|
|
164
|
+
const registryAddress = registryParts[2];
|
|
165
|
+
try {
|
|
166
|
+
const owner = await client.readContract({
|
|
167
|
+
address: registryAddress,
|
|
168
|
+
abi: [{
|
|
169
|
+
name: 'ownerOf',
|
|
170
|
+
type: 'function',
|
|
171
|
+
stateMutability: 'view',
|
|
172
|
+
inputs: [{ name: 'tokenId', type: 'uint256' }],
|
|
173
|
+
outputs: [{ name: '', type: 'address' }],
|
|
174
|
+
}],
|
|
175
|
+
functionName: 'ownerOf',
|
|
176
|
+
args: [BigInt(receipt.agentId)],
|
|
177
|
+
});
|
|
178
|
+
if (owner.toLowerCase() !== receipt.address.toLowerCase()) {
|
|
179
|
+
return { valid: false, error: 'Onchain ownership check failed: signer is not the NFT owner' };
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
return { valid: false, error: 'Onchain ownership check failed: agent not registered' };
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
valid: true,
|
|
188
|
+
agent: {
|
|
189
|
+
address: receipt.address,
|
|
190
|
+
agentId: receipt.agentId,
|
|
191
|
+
agentRegistry: receipt.agentRegistry,
|
|
192
|
+
chainId: receipt.chainId,
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
// ---------------------------------------------------------------------------
|
|
197
|
+
// Utilities
|
|
198
|
+
// ---------------------------------------------------------------------------
|
|
199
|
+
/**
|
|
200
|
+
* Convert an Express request to a Fetch API Request.
|
|
201
|
+
*
|
|
202
|
+
* Needed because ERC-8128 operates on the Fetch `Request` object,
|
|
203
|
+
* but Express uses its own request type.
|
|
204
|
+
*
|
|
205
|
+
* @param req Express request object (must have `rawBody` for Content-Digest verification)
|
|
206
|
+
*/
|
|
207
|
+
export function expressToFetchRequest(req) {
|
|
208
|
+
const host = req.get('host') || 'localhost';
|
|
209
|
+
const url = `${req.protocol}://${host}${req.originalUrl}`;
|
|
210
|
+
const headers = new Headers();
|
|
211
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
212
|
+
if (value === undefined)
|
|
213
|
+
continue;
|
|
214
|
+
if (Array.isArray(value)) {
|
|
215
|
+
for (const v of value)
|
|
216
|
+
headers.append(key, v);
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
headers.set(key, value);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
const hasBody = req.method !== 'GET' && req.method !== 'HEAD';
|
|
223
|
+
return new Request(url, {
|
|
224
|
+
method: req.method,
|
|
225
|
+
headers,
|
|
226
|
+
body: hasBody ? (req.rawBody ?? null) : null,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Normalize a Next.js/serverless Request for ERC-8128 verification.
|
|
231
|
+
*
|
|
232
|
+
* Behind reverse proxies (Vercel, Railway, Cloudflare), the request URL
|
|
233
|
+
* may reflect internal routing instead of the public origin. This helper
|
|
234
|
+
* reads X-Forwarded-Host / X-Forwarded-Proto headers and reconstructs
|
|
235
|
+
* the URL to match what the agent signed.
|
|
236
|
+
*/
|
|
237
|
+
export function nextjsToFetchRequest(req) {
|
|
238
|
+
const forwardedHost = req.headers.get('x-forwarded-host') || req.headers.get('host');
|
|
239
|
+
const forwardedProto = req.headers.get('x-forwarded-proto') || 'https';
|
|
240
|
+
if (!forwardedHost)
|
|
241
|
+
return req; // no proxy headers, return as-is
|
|
242
|
+
const url = new URL(req.url);
|
|
243
|
+
const publicUrl = `${forwardedProto}://${forwardedHost}${url.pathname}${url.search}`;
|
|
244
|
+
return new Request(publicUrl, {
|
|
245
|
+
method: req.method,
|
|
246
|
+
headers: req.headers,
|
|
247
|
+
body: req.body,
|
|
248
|
+
// @ts-ignore - duplex required for streaming bodies in Node 18+
|
|
249
|
+
duplex: 'half',
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Lazily create a viem PublicClient from an RPC URL.
|
|
254
|
+
*/
|
|
255
|
+
async function createOnchainClient(rpcUrl) {
|
|
256
|
+
if (!rpcUrl)
|
|
257
|
+
return null;
|
|
258
|
+
const { createPublicClient, http } = await import('viem');
|
|
259
|
+
return createPublicClient({ transport: http(rpcUrl) });
|
|
260
|
+
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/keystore.d.ts
CHANGED
|
@@ -75,6 +75,15 @@ export declare function getAddress(config?: KeystoreConfig): Promise<string | nu
|
|
|
75
75
|
* Only the signature is returned.
|
|
76
76
|
*/
|
|
77
77
|
export declare function signMessage(message: string, config?: KeystoreConfig): Promise<SignResult>;
|
|
78
|
+
/**
|
|
79
|
+
* Sign a raw hex message via the keyring proxy.
|
|
80
|
+
*
|
|
81
|
+
* Used internally by the ERC-8128 signer — the signature base bytes are
|
|
82
|
+
* passed as a hex string and signed with `{ raw: true }` so the proxy
|
|
83
|
+
* interprets them as raw bytes (not UTF-8). Note: the proxy still applies
|
|
84
|
+
* EIP-191 personal_sign wrapping (viem `signMessage({ message: { raw } })`).
|
|
85
|
+
*/
|
|
86
|
+
export declare function signRawMessage(rawHex: string, config?: KeystoreConfig): Promise<SignResult>;
|
|
78
87
|
/**
|
|
79
88
|
* Sign a transaction via the keyring proxy.
|
|
80
89
|
* Only the signed transaction is returned.
|
package/dist/keystore.js
CHANGED
|
@@ -80,6 +80,21 @@ export async function signMessage(message, config = {}) {
|
|
|
80
80
|
const data = await proxyRequest(config, "/sign-message", { message: msg });
|
|
81
81
|
return { signature: data.signature, address: data.address };
|
|
82
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* Sign a raw hex message via the keyring proxy.
|
|
85
|
+
*
|
|
86
|
+
* Used internally by the ERC-8128 signer — the signature base bytes are
|
|
87
|
+
* passed as a hex string and signed with `{ raw: true }` so the proxy
|
|
88
|
+
* interprets them as raw bytes (not UTF-8). Note: the proxy still applies
|
|
89
|
+
* EIP-191 personal_sign wrapping (viem `signMessage({ message: { raw } })`).
|
|
90
|
+
*/
|
|
91
|
+
export async function signRawMessage(rawHex, config = {}) {
|
|
92
|
+
const data = await proxyRequest(config, "/sign-message", {
|
|
93
|
+
message: rawHex,
|
|
94
|
+
raw: true,
|
|
95
|
+
});
|
|
96
|
+
return { signature: data.signature, address: data.address };
|
|
97
|
+
}
|
|
83
98
|
/**
|
|
84
99
|
* Sign a transaction via the keyring proxy.
|
|
85
100
|
* Only the signed transaction is returned.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* receipt.ts
|
|
3
|
+
*
|
|
4
|
+
* Stateless HMAC-signed verification receipts.
|
|
5
|
+
*
|
|
6
|
+
* A receipt proves that onchain registration was checked during SIWA sign-in.
|
|
7
|
+
* It is issued after successful verification and attached to every subsequent
|
|
8
|
+
* request alongside an ERC-8128 HTTP signature.
|
|
9
|
+
*
|
|
10
|
+
* Receipt alone is useless (can't forge signatures).
|
|
11
|
+
* Signature alone is insufficient (proves key control, not registration).
|
|
12
|
+
* Together = authentication + authorization.
|
|
13
|
+
*
|
|
14
|
+
* Format: base64url(json).base64url(hmac-sha256)
|
|
15
|
+
* Same token format as nonce tokens in siwa.ts.
|
|
16
|
+
*/
|
|
17
|
+
export interface ReceiptPayload {
|
|
18
|
+
address: string;
|
|
19
|
+
agentId: number;
|
|
20
|
+
agentRegistry: string;
|
|
21
|
+
chainId: number;
|
|
22
|
+
verified: 'offline' | 'onchain';
|
|
23
|
+
iat: number;
|
|
24
|
+
exp: number;
|
|
25
|
+
}
|
|
26
|
+
export interface ReceiptOptions {
|
|
27
|
+
secret: string;
|
|
28
|
+
ttl?: number;
|
|
29
|
+
}
|
|
30
|
+
export interface ReceiptResult {
|
|
31
|
+
receipt: string;
|
|
32
|
+
expiresAt: string;
|
|
33
|
+
}
|
|
34
|
+
/** Default receipt validity: 30 minutes */
|
|
35
|
+
export declare const DEFAULT_RECEIPT_TTL: number;
|
|
36
|
+
/**
|
|
37
|
+
* Create an HMAC-signed receipt token.
|
|
38
|
+
*
|
|
39
|
+
* @param payload Agent identity fields from a successful SIWA verification
|
|
40
|
+
* @param options HMAC secret and optional TTL override
|
|
41
|
+
* @returns `{ receipt, expiresAt }` — the token and its ISO expiry
|
|
42
|
+
*/
|
|
43
|
+
export declare function createReceipt(payload: Omit<ReceiptPayload, 'iat' | 'exp'>, options: ReceiptOptions): ReceiptResult;
|
|
44
|
+
/**
|
|
45
|
+
* Verify and decode a receipt token.
|
|
46
|
+
*
|
|
47
|
+
* Uses constant-time comparison and checks expiry.
|
|
48
|
+
*
|
|
49
|
+
* @param receipt The `base64url(json).base64url(hmac)` token
|
|
50
|
+
* @param secret HMAC secret used to sign the receipt
|
|
51
|
+
* @returns Decoded payload, or `null` if invalid/expired
|
|
52
|
+
*/
|
|
53
|
+
export declare function verifyReceipt(receipt: string, secret: string): ReceiptPayload | null;
|
package/dist/receipt.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* receipt.ts
|
|
3
|
+
*
|
|
4
|
+
* Stateless HMAC-signed verification receipts.
|
|
5
|
+
*
|
|
6
|
+
* A receipt proves that onchain registration was checked during SIWA sign-in.
|
|
7
|
+
* It is issued after successful verification and attached to every subsequent
|
|
8
|
+
* request alongside an ERC-8128 HTTP signature.
|
|
9
|
+
*
|
|
10
|
+
* Receipt alone is useless (can't forge signatures).
|
|
11
|
+
* Signature alone is insufficient (proves key control, not registration).
|
|
12
|
+
* Together = authentication + authorization.
|
|
13
|
+
*
|
|
14
|
+
* Format: base64url(json).base64url(hmac-sha256)
|
|
15
|
+
* Same token format as nonce tokens in siwa.ts.
|
|
16
|
+
*/
|
|
17
|
+
import * as crypto from 'crypto';
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Constants
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
/** Default receipt validity: 30 minutes */
|
|
22
|
+
export const DEFAULT_RECEIPT_TTL = 30 * 60 * 1000;
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Create
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
/**
|
|
27
|
+
* Create an HMAC-signed receipt token.
|
|
28
|
+
*
|
|
29
|
+
* @param payload Agent identity fields from a successful SIWA verification
|
|
30
|
+
* @param options HMAC secret and optional TTL override
|
|
31
|
+
* @returns `{ receipt, expiresAt }` — the token and its ISO expiry
|
|
32
|
+
*/
|
|
33
|
+
export function createReceipt(payload, options) {
|
|
34
|
+
const now = Date.now();
|
|
35
|
+
const ttl = options.ttl ?? DEFAULT_RECEIPT_TTL;
|
|
36
|
+
const exp = now + ttl;
|
|
37
|
+
const full = {
|
|
38
|
+
...payload,
|
|
39
|
+
iat: now,
|
|
40
|
+
exp,
|
|
41
|
+
};
|
|
42
|
+
const data = Buffer.from(JSON.stringify(full)).toString('base64url');
|
|
43
|
+
const sig = crypto.createHmac('sha256', options.secret).update(data).digest('base64url');
|
|
44
|
+
return {
|
|
45
|
+
receipt: `${data}.${sig}`,
|
|
46
|
+
expiresAt: new Date(exp).toISOString(),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Verify
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
/**
|
|
53
|
+
* Verify and decode a receipt token.
|
|
54
|
+
*
|
|
55
|
+
* Uses constant-time comparison and checks expiry.
|
|
56
|
+
*
|
|
57
|
+
* @param receipt The `base64url(json).base64url(hmac)` token
|
|
58
|
+
* @param secret HMAC secret used to sign the receipt
|
|
59
|
+
* @returns Decoded payload, or `null` if invalid/expired
|
|
60
|
+
*/
|
|
61
|
+
export function verifyReceipt(receipt, secret) {
|
|
62
|
+
const dotIdx = receipt.indexOf('.');
|
|
63
|
+
if (dotIdx === -1)
|
|
64
|
+
return null;
|
|
65
|
+
const data = receipt.slice(0, dotIdx);
|
|
66
|
+
const sig = receipt.slice(dotIdx + 1);
|
|
67
|
+
if (!data || !sig)
|
|
68
|
+
return null;
|
|
69
|
+
const expected = crypto.createHmac('sha256', secret).update(data).digest('base64url');
|
|
70
|
+
// Constant-time comparison
|
|
71
|
+
if (sig.length !== expected.length)
|
|
72
|
+
return null;
|
|
73
|
+
if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected)))
|
|
74
|
+
return null;
|
|
75
|
+
// Decode and check expiry
|
|
76
|
+
const payload = JSON.parse(Buffer.from(data, 'base64url').toString());
|
|
77
|
+
if (payload.exp < Date.now())
|
|
78
|
+
return null;
|
|
79
|
+
return payload;
|
|
80
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@buildersgarden/siwa",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.9",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -30,6 +30,14 @@
|
|
|
30
30
|
"./addresses": {
|
|
31
31
|
"types": "./dist/addresses.d.ts",
|
|
32
32
|
"default": "./dist/addresses.js"
|
|
33
|
+
},
|
|
34
|
+
"./receipt": {
|
|
35
|
+
"types": "./dist/receipt.d.ts",
|
|
36
|
+
"default": "./dist/receipt.js"
|
|
37
|
+
},
|
|
38
|
+
"./erc8128": {
|
|
39
|
+
"types": "./dist/erc8128.d.ts",
|
|
40
|
+
"default": "./dist/erc8128.js"
|
|
33
41
|
}
|
|
34
42
|
},
|
|
35
43
|
"main": "./dist/index.js",
|
|
@@ -50,6 +58,7 @@
|
|
|
50
58
|
"clean": "rm -rf dist"
|
|
51
59
|
},
|
|
52
60
|
"dependencies": {
|
|
61
|
+
"@slicekit/erc8128": "^0.1.0",
|
|
53
62
|
"viem": "^2.21.0"
|
|
54
63
|
},
|
|
55
64
|
"devDependencies": {
|