@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 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 JWT from the relying party
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 test
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 [`test/README.md`](test/README.md) for full details on the test environment.
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;
@@ -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
@@ -4,3 +4,5 @@ export * from './identity.js';
4
4
  export * from './proxy-auth.js';
5
5
  export * from './registry.js';
6
6
  export * from './addresses.js';
7
+ export * from './receipt.js';
8
+ export * from './erc8128.js';
package/dist/index.js CHANGED
@@ -4,3 +4,5 @@ export * from './identity.js';
4
4
  export * from './proxy-auth.js';
5
5
  export * from './registry.js';
6
6
  export * from './addresses.js';
7
+ export * from './receipt.js';
8
+ export * from './erc8128.js';
@@ -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;
@@ -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.7",
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": {