@shipooor/walletauth 0.1.0

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 walletauth
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 ADDED
@@ -0,0 +1,300 @@
1
+ # @shipooor/walletauth
2
+
3
+ Your wallet is your API key. Agent-native auth for APIs.
4
+
5
+ ## What
6
+
7
+ Lightweight, framework-agnostic auth library that replaces API keys with wallet signatures.
8
+ Zero config for agents. Full control for API owners.
9
+
10
+ ```
11
+ Agent has wallet → requests challenge → signs nonce → gets JWT → calls API
12
+ No registration. No API keys. No rotation.
13
+ ```
14
+
15
+ ## Why
16
+
17
+ | Problem | Wallet Auth |
18
+ |---|---|
19
+ | API keys are manual (generate, copy, rotate) | Wallet = identity, automatic |
20
+ | One key = all agents (no granularity) | Each agent = own wallet = own identity |
21
+ | Keys leak, get stolen, expire | Private key never leaves the agent |
22
+ | Auth0/OAuth2 designed for humans | Agent-native, no human in the loop |
23
+
24
+ ## How it works
25
+
26
+ ```
27
+ ┌─────────────────────────────────┐
28
+ │ AI Agent (any framework) │
29
+ │ Has a wallet / keypair │
30
+ └──────────┬──────────────────────┘
31
+ │ 1. POST /auth/challenge { address }
32
+ │ 2. Server returns { nonce, challenge, expiresAt }
33
+ │ 3. Agent signs nonce with private key
34
+ │ 4. POST /auth/verify { address, signature, challenge }
35
+ │ 5. Server verifies HMAC + wallet signature → JWT
36
+
37
+ ┌──────────────────────────────────┐
38
+ │ Your API + @shipooor/walletauth │
39
+ │ │
40
+ │ ├─ Stateless challenge/verify │
41
+ │ ├─ HMAC-signed challenges │
42
+ │ ├─ Signature verification │
43
+ │ └─ JWT issuance & validation │
44
+ └───────────────────────────────────┘
45
+ ```
46
+
47
+ ### Stateless by design
48
+
49
+ Challenges are HMAC-signed — the server verifies its own signature on return. No nonce storage, no database, no Redis. Truly stateless.
50
+
51
+ ## Install
52
+
53
+ ```bash
54
+ npm install @shipooor/walletauth
55
+ ```
56
+
57
+ ## Core API
58
+
59
+ Pure functions. No framework dependency. Use with Express, NestJS, Fastify, Hono, or anything else.
60
+
61
+ ```typescript
62
+ import {
63
+ createChallenge,
64
+ verifySignature,
65
+ issueToken,
66
+ validateToken,
67
+ verifiers,
68
+ } from '@shipooor/walletauth';
69
+ ```
70
+
71
+ | Function | Description |
72
+ |---|---|
73
+ | `createChallenge(address, secret)` | Generate a stateless HMAC-signed challenge |
74
+ | `verifySignature(address, signature, challenge, secret, verifier)` | Verify HMAC + wallet signature. **async** |
75
+ | `issueToken(address, secret, options?)` | Issue a JWT for the verified address. **async** |
76
+ | `validateToken(token, secret)` | Validate JWT, return `{ address }` or `null`. **async** |
77
+ | `verifiers.evm` | EVM signature verifier (secp256k1) |
78
+ | `verifiers.ed25519` | Ed25519 verifier (Solana, raw keys) |
79
+
80
+ > `verifySignature`, `issueToken`, and `validateToken` return Promises — always `await` them.
81
+
82
+ ## Built-in verifiers
83
+
84
+ All chains supported in one lightweight package (~7KB ESM). No ethers.js, no heavy deps.
85
+
86
+ | Chain | Verifier | Crypto | Dep |
87
+ |---|---|---|---|
88
+ | EVM (Ethereum, Arbitrum, Base, etc.) | `verifiers.evm` | secp256k1 + keccak256 | `@noble/curves` + `@noble/hashes` |
89
+ | Solana | `verifiers.ed25519` | ed25519 | Node.js built-in `crypto` |
90
+ | Raw ed25519 keypair | `verifiers.ed25519` | ed25519 | Node.js built-in `crypto` |
91
+ | Custom | `(addr, msg, sig) => boolean \| Promise<boolean>` | Any | Bring your own |
92
+
93
+ Multiple verifiers supported — pass an array for multi-chain APIs:
94
+
95
+ ```typescript
96
+ verifySignature(address, signature, challenge, secret, [verifiers.evm, verifiers.ed25519])
97
+ ```
98
+
99
+ Each verifier is tried in order. First `true` wins. Cryptographically safe — a secp256k1 signature can't accidentally pass ed25519 verification.
100
+
101
+ ## Usage: Express
102
+
103
+ ```typescript
104
+ import express from 'express';
105
+ import { createChallenge, verifySignature, issueToken, validateToken, verifiers } from '@shipooor/walletauth';
106
+
107
+ const app = express();
108
+ app.use(express.json());
109
+
110
+ const SECRET = process.env.WALLETAUTH_SECRET; // Used for both HMAC challenges and JWT signing
111
+
112
+ // Step 1: Agent requests a challenge
113
+ app.post('/auth/challenge', (req, res) => {
114
+ const challenge = createChallenge(req.body.address, SECRET);
115
+ res.json(challenge); // { nonce, challenge, expiresAt }
116
+ });
117
+
118
+ // Step 2: Agent signs nonce and sends back
119
+ app.post('/auth/verify', async (req, res) => {
120
+ const { address, signature, challenge } = req.body;
121
+ const valid = await verifySignature(address, signature, challenge, SECRET, verifiers.evm);
122
+ if (!valid) return res.status(401).json({ error: 'Invalid signature' });
123
+
124
+ const token = await issueToken(address, SECRET);
125
+ res.json({ token });
126
+ });
127
+
128
+ // Middleware: protect routes
129
+ async function authMiddleware(req, res, next) {
130
+ const token = req.headers.authorization?.split(' ')[1];
131
+ const payload = await validateToken(token, SECRET);
132
+ if (!payload) return res.status(401).json({ error: 'Invalid token' });
133
+ req.wallet = payload.address;
134
+ next();
135
+ }
136
+
137
+ // Protected route
138
+ app.get('/api/data', authMiddleware, (req, res) => {
139
+ res.json({ wallet: req.wallet, data: '...' });
140
+ });
141
+ ```
142
+
143
+ ## Usage: NestJS
144
+
145
+ ```typescript
146
+ import { Injectable, CanActivate, ExecutionContext, createParamDecorator } from '@nestjs/common';
147
+ import { validateToken } from '@shipooor/walletauth';
148
+
149
+ @Injectable()
150
+ export class WalletAuthGuard implements CanActivate {
151
+ async canActivate(context: ExecutionContext): Promise<boolean> {
152
+ const request = context.switchToHttp().getRequest();
153
+ const token = request.headers.authorization?.split(' ')[1];
154
+ const payload = await validateToken(token, process.env.WALLETAUTH_SECRET);
155
+ if (!payload) return false;
156
+ request.wallet = payload.address;
157
+ return true;
158
+ }
159
+ }
160
+
161
+ export const WalletAddress = createParamDecorator(
162
+ (_data: unknown, ctx: ExecutionContext) => ctx.switchToHttp().getRequest().wallet,
163
+ );
164
+
165
+ // Usage in controller:
166
+ // @UseGuards(WalletAuthGuard)
167
+ // @Get('data')
168
+ // getData(@WalletAddress() wallet: string) { ... }
169
+ ```
170
+
171
+ ## Usage: Client (any wallet)
172
+
173
+ ```typescript
174
+ // EVM wallet (MetaMask, WDK, Coinbase CDP, etc.)
175
+ const res = await fetch('/auth/challenge', {
176
+ method: 'POST',
177
+ headers: { 'Content-Type': 'application/json' },
178
+ body: JSON.stringify({ address: wallet.address }),
179
+ }).then(r => r.json());
180
+
181
+ const signature = await wallet.signMessage(res.nonce);
182
+
183
+ const { token } = await fetch('/auth/verify', {
184
+ method: 'POST',
185
+ headers: { 'Content-Type': 'application/json' },
186
+ body: JSON.stringify({ address: wallet.address, signature, challenge: res.challenge }),
187
+ }).then(r => r.json());
188
+
189
+ // Use token for all subsequent requests
190
+ fetch('/api/data', { headers: { Authorization: `Bearer ${token}` } });
191
+ ```
192
+
193
+ ```typescript
194
+ // Solana wallet (Phantom, etc.)
195
+ import bs58 from 'bs58'; // already available via @solana/web3.js
196
+ const sigBytes = await phantom.signMessage(new TextEncoder().encode(res.nonce));
197
+ const signature = bs58.encode(sigBytes);
198
+ ```
199
+
200
+ ```typescript
201
+ // Raw ed25519 keypair (no blockchain needed)
202
+ import { sign } from 'crypto';
203
+ const sigBytes = sign(null, Buffer.from(res.nonce), privateKey);
204
+ const signature = sigBytes.toString('hex');
205
+ ```
206
+
207
+ ## Wire format
208
+
209
+ ### Challenge response (server → client)
210
+
211
+ ```json
212
+ {
213
+ "nonce": "a1b2c3d4e5f6...",
214
+ "challenge": "BASE64_HMAC_SIGNED_BLOB",
215
+ "expiresAt": 1710500000000
216
+ }
217
+ ```
218
+
219
+ - `nonce` — the message the client must sign with their wallet
220
+ - `challenge` — opaque HMAC-signed blob (client stores and sends back as-is)
221
+ - `expiresAt` — expiration timestamp (informational for the client)
222
+
223
+ ### Verify request (client → server)
224
+
225
+ ```json
226
+ {
227
+ "address": "0x1234...",
228
+ "signature": "0xabcd...",
229
+ "challenge": "BASE64_HMAC_SIGNED_BLOB"
230
+ }
231
+ ```
232
+
233
+ The client never needs to send the nonce separately — it's embedded in the challenge blob.
234
+
235
+ ## Framework adapters (planned)
236
+
237
+ Core is framework-agnostic. Optional adapter packages may be published if needed:
238
+
239
+ | Package | Status |
240
+ |---|---|
241
+ | `@shipooor/walletauth` | Core library (pure functions + verifiers) |
242
+ | `@shipooor/walletauth-express` | Planned — Express middleware wrapper |
243
+ | `@shipooor/walletauth-nestjs` | Planned — Guard + decorator |
244
+ | `@shipooor/walletauth-fastify` | Planned — Fastify plugin |
245
+
246
+ ## Size
247
+
248
+ | | @shipooor/walletauth | ethers.js (for verifyMessage) |
249
+ |---|---|---|
250
+ | Library size | ~7KB ESM | 500KB+ |
251
+ | Runtime deps | 3 (`@noble/curves`, `@noble/hashes`, `jose`) | Everything bundled |
252
+ | EVM verify | `@noble/curves` + `@noble/hashes` | Full ethers bundle |
253
+ | ed25519 verify | Node.js built-in `crypto` | Not included |
254
+
255
+ Same underlying crypto (`@noble/*`), minimal surface area.
256
+
257
+ ## Security notes
258
+
259
+ - **HTTPS required**: Always deploy behind HTTPS. Challenges, signatures, and JWTs are sent in plaintext over HTTP — an attacker on the network can intercept them.
260
+ - **Single secret (v1)**: One secret for both HMAC challenges and JWT signing. Both use HMAC-SHA256, which is a PRF — safe for key reuse. The payloads are structurally different (challenge JSON vs JWT claims), so there's no confusion risk. Separate secrets can be supported in a future version.
261
+ - **One secret per service**: If you run multiple APIs, use a different `WALLETAUTH_SECRET` for each. JWTs signed by one service are valid on any service that shares the same secret.
262
+ - **Secret rotation**: Changing `WALLETAUTH_SECRET` instantly invalidates all existing JWTs and pending challenges. Plan rotation during low-traffic windows. For graceful rotation, validate against both old and new secrets during a transition period.
263
+ - **Challenge expiration**: Default 5 minutes. Configurable via options.
264
+ - **Replay window**: Within the challenge TTL, a captured `{ address, signature, challenge }` request can be replayed to obtain a JWT. HTTPS prevents interception. For strict one-time use, implement nonce tracking at the application level.
265
+ - **JWT revocation**: Stateless JWTs cannot be revoked before expiry. If an agent is compromised, you must either rotate the secret (invalidates all tokens) or maintain a blocklist at the application level. Use short JWT expiry (`1h` default) to limit exposure.
266
+ - **No rate limiting built-in**: Rate limiting is the API owner's responsibility. The challenge endpoint is unauthenticated — protect it with your framework's middleware (express-rate-limit, @nestjs/throttler, etc.).
267
+
268
+ ## Generating a secret
269
+
270
+ ```bash
271
+ openssl rand -base64 32
272
+ ```
273
+
274
+ Minimum 16 characters. Store in environment variables, never in code.
275
+
276
+ ```bash
277
+ export WALLETAUTH_SECRET="your-generated-secret-here"
278
+ ```
279
+
280
+ ## Debugging
281
+
282
+ All verification functions return `false` or `null` on failure without revealing the reason. This is intentional — error details could leak information to attackers.
283
+
284
+ Common issues when auth fails:
285
+
286
+ | Symptom | Check |
287
+ |---|---|
288
+ | `verifySignature` returns `false` | Is the challenge expired? (default 5 min TTL) |
289
+ | `verifySignature` returns `false` | Is the correct verifier used? (evm vs ed25519) |
290
+ | `verifySignature` returns `false` | Does the address match between challenge and verify? |
291
+ | `verifySignature` returns `false` | Is the client signing the `nonce` string, not the `challenge` blob? |
292
+ | `validateToken` returns `null` | Is the JWT expired? |
293
+ | `validateToken` returns `null` | Is the same secret used for issuing and validating? |
294
+ | `assertSecret` throws | Secret must be at least 16 characters |
295
+
296
+ ## Related
297
+
298
+ - [SIWE (EIP-4361)](https://docs.siwe.xyz/) — session-based, human-facing login
299
+ - [ERC-8128](https://eips.ethereum.org/EIPS/eip-8128) — per-request HTTP signing (draft)
300
+ - [x402](https://www.x402.org/) — payment auth protocol (complementary)
package/dist/index.cjs ADDED
@@ -0,0 +1,267 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ createChallenge: () => createChallenge,
24
+ issueToken: () => issueToken,
25
+ validateToken: () => validateToken,
26
+ verifiers: () => verifiers,
27
+ verifySignature: () => verifySignature
28
+ });
29
+ module.exports = __toCommonJS(index_exports);
30
+
31
+ // src/challenge.ts
32
+ var import_node_crypto = require("crypto");
33
+
34
+ // src/secret.ts
35
+ var MIN_SECRET_LENGTH = 16;
36
+ function assertSecret(secret) {
37
+ if (!secret || secret.length < MIN_SECRET_LENGTH) {
38
+ throw new Error(
39
+ `WALLETAUTH_SECRET must be at least ${MIN_SECRET_LENGTH} characters`
40
+ );
41
+ }
42
+ }
43
+
44
+ // src/challenge.ts
45
+ var DEFAULT_EXPIRES_IN = 5 * 60 * 1e3;
46
+ function sign(payload, secret) {
47
+ const data = JSON.stringify(payload);
48
+ const mac = (0, import_node_crypto.createHmac)("sha256", secret).update(data).digest("base64url");
49
+ const encoded = Buffer.from(data).toString("base64url");
50
+ return `${encoded}.${mac}`;
51
+ }
52
+ function parseAndVerifyChallenge(challenge, secret) {
53
+ const dotIndex = challenge.indexOf(".");
54
+ if (dotIndex === -1) return null;
55
+ const encoded = challenge.slice(0, dotIndex);
56
+ const mac = challenge.slice(dotIndex + 1);
57
+ let data;
58
+ try {
59
+ data = Buffer.from(encoded, "base64url").toString();
60
+ } catch {
61
+ return null;
62
+ }
63
+ const expectedMac = (0, import_node_crypto.createHmac)("sha256", secret).update(data).digest("base64url");
64
+ const a = Buffer.from(mac);
65
+ const b = Buffer.from(expectedMac);
66
+ if (a.length !== b.length) return null;
67
+ if (!(0, import_node_crypto.timingSafeEqual)(a, b)) return null;
68
+ try {
69
+ const payload = JSON.parse(data);
70
+ if (Date.now() > payload.exp) return null;
71
+ return payload;
72
+ } catch {
73
+ return null;
74
+ }
75
+ }
76
+ function createChallenge(address, secret, options) {
77
+ assertSecret(secret);
78
+ const expiresIn = options?.expiresIn ?? DEFAULT_EXPIRES_IN;
79
+ if (!Number.isFinite(expiresIn)) {
80
+ throw new Error("expiresIn must be a finite number");
81
+ }
82
+ const nonce = (0, import_node_crypto.randomBytes)(32).toString("hex");
83
+ const exp = Date.now() + expiresIn;
84
+ const payload = { address, nonce, exp };
85
+ const challenge = sign(payload, secret);
86
+ return { nonce, challenge, expiresAt: exp };
87
+ }
88
+
89
+ // src/verify.ts
90
+ async function verifySignature(address, signature, challenge, secret, verifier) {
91
+ try {
92
+ const payload = parseAndVerifyChallenge(challenge, secret);
93
+ if (!payload) return false;
94
+ if (payload.address.toLowerCase() !== address.toLowerCase()) return false;
95
+ const verifierList = Array.isArray(verifier) ? verifier : [verifier];
96
+ for (const v of verifierList) {
97
+ try {
98
+ const result = await v(address, payload.nonce, signature);
99
+ if (result) return true;
100
+ } catch {
101
+ }
102
+ }
103
+ return false;
104
+ } catch {
105
+ return false;
106
+ }
107
+ }
108
+
109
+ // src/token.ts
110
+ var import_jose = require("jose");
111
+ var DEFAULT_EXPIRES_IN2 = "1h";
112
+ async function issueToken(address, secret, options) {
113
+ assertSecret(secret);
114
+ const secretKey = new TextEncoder().encode(secret);
115
+ const expiresIn = options?.expiresIn ?? DEFAULT_EXPIRES_IN2;
116
+ return new import_jose.SignJWT({ address }).setProtectedHeader({ alg: "HS256" }).setIssuedAt().setExpirationTime(expiresIn).sign(secretKey);
117
+ }
118
+ async function validateToken(token, secret) {
119
+ try {
120
+ const secretKey = new TextEncoder().encode(secret);
121
+ const { payload } = await (0, import_jose.jwtVerify)(token, secretKey, { algorithms: ["HS256"] });
122
+ if (typeof payload.address !== "string") return null;
123
+ if (typeof payload.iat !== "number") return null;
124
+ if (typeof payload.exp !== "number") return null;
125
+ return {
126
+ address: payload.address,
127
+ iat: payload.iat,
128
+ exp: payload.exp
129
+ };
130
+ } catch {
131
+ return null;
132
+ }
133
+ }
134
+
135
+ // src/verifiers/evm.ts
136
+ var import_secp256k1 = require("@noble/curves/secp256k1");
137
+ var import_sha3 = require("@noble/hashes/sha3");
138
+ function hashPersonalMessage(message) {
139
+ const messageBytes = new TextEncoder().encode(message);
140
+ const prefix = `Ethereum Signed Message:
141
+ ${messageBytes.length}`;
142
+ const prefixBytes = new TextEncoder().encode(prefix);
143
+ const combined = new Uint8Array(prefixBytes.length + messageBytes.length);
144
+ combined.set(prefixBytes);
145
+ combined.set(messageBytes, prefixBytes.length);
146
+ return (0, import_sha3.keccak_256)(combined);
147
+ }
148
+ function publicKeyToAddress(publicKey) {
149
+ const hash = (0, import_sha3.keccak_256)(publicKey.slice(1));
150
+ const addressBytes = hash.slice(-20);
151
+ return "0x" + Buffer.from(addressBytes).toString("hex");
152
+ }
153
+ var evm = (address, message, signature) => {
154
+ try {
155
+ const hash = hashPersonalMessage(message);
156
+ const sig = signature.startsWith("0x") ? signature.slice(2) : signature;
157
+ if (sig.length !== 130) return false;
158
+ const r = BigInt("0x" + sig.slice(0, 64));
159
+ const s = BigInt("0x" + sig.slice(64, 128));
160
+ const v = parseInt(sig.slice(128, 130), 16);
161
+ const recovery = v >= 27 ? v - 27 : v;
162
+ if (recovery !== 0 && recovery !== 1) return false;
163
+ const sigObj = new import_secp256k1.secp256k1.Signature(r, s, recovery);
164
+ const recovered = sigObj.recoverPublicKey(hash);
165
+ const recoveredAddress = publicKeyToAddress(recovered.toRawBytes(false));
166
+ return recoveredAddress.toLowerCase() === address.toLowerCase();
167
+ } catch {
168
+ return false;
169
+ }
170
+ };
171
+
172
+ // src/verifiers/ed25519.ts
173
+ var import_node_crypto2 = require("crypto");
174
+ var ED25519_DER_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
175
+ var BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
176
+ function decodeBase58(input) {
177
+ const bytes = [];
178
+ for (const char of input) {
179
+ const idx = BASE58_ALPHABET.indexOf(char);
180
+ if (idx === -1) throw new Error("Invalid base58 character");
181
+ let carry = idx;
182
+ for (let j = 0; j < bytes.length; j++) {
183
+ carry += bytes[j] * 58;
184
+ bytes[j] = carry & 255;
185
+ carry >>= 8;
186
+ }
187
+ while (carry > 0) {
188
+ bytes.push(carry & 255);
189
+ carry >>= 8;
190
+ }
191
+ }
192
+ bytes.reverse();
193
+ let leadingZeros = 0;
194
+ for (const char of input) {
195
+ if (char !== "1") break;
196
+ leadingZeros++;
197
+ }
198
+ const result = new Uint8Array(leadingZeros + bytes.length);
199
+ result.set(bytes, leadingZeros);
200
+ return Buffer.from(result);
201
+ }
202
+ function parsePublicKey(address) {
203
+ try {
204
+ if (/^[0-9a-fA-F]{64}$/.test(address)) {
205
+ return Buffer.from(address, "hex");
206
+ }
207
+ if (/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(address)) {
208
+ const decoded = decodeBase58(address);
209
+ if (decoded.length === 32) return decoded;
210
+ }
211
+ return null;
212
+ } catch {
213
+ return null;
214
+ }
215
+ }
216
+ var ed25519 = (address, message, signature) => {
217
+ try {
218
+ const publicKeyRaw = parsePublicKey(address);
219
+ if (!publicKeyRaw || publicKeyRaw.length !== 32) return false;
220
+ let sigBuffer;
221
+ if (/^[0-9a-fA-F]{128}$/.test(signature)) {
222
+ sigBuffer = Buffer.from(signature, "hex");
223
+ } else if (/^[1-9A-HJ-NP-Za-km-z]{1,90}$/.test(signature)) {
224
+ const decoded = decodeBase58(signature);
225
+ if (decoded.length === 64) {
226
+ sigBuffer = decoded;
227
+ } else {
228
+ sigBuffer = Buffer.from(signature, "base64");
229
+ }
230
+ } else {
231
+ sigBuffer = Buffer.from(signature, "base64");
232
+ }
233
+ if (sigBuffer.length !== 64) return false;
234
+ const derKey = Buffer.concat([ED25519_DER_PREFIX, publicKeyRaw]);
235
+ const keyObject = (0, import_node_crypto2.createPublicKey)({
236
+ key: derKey,
237
+ format: "der",
238
+ type: "spki"
239
+ });
240
+ return (0, import_node_crypto2.verify)(null, Buffer.from(message), keyObject, sigBuffer);
241
+ } catch {
242
+ return false;
243
+ }
244
+ };
245
+
246
+ // src/verifiers/index.ts
247
+ var verifiers = {
248
+ /**
249
+ * EVM signature verifier (Ethereum, Arbitrum, Base, Polygon, etc.).
250
+ * Verifies `personal_sign` (EIP-191) signatures using secp256k1 ecrecover.
251
+ */
252
+ evm,
253
+ /**
254
+ * Ed25519 signature verifier (Solana, raw ed25519 keypairs).
255
+ * Accepts addresses as base58 or hex. Signatures as hex, base58, or base64.
256
+ */
257
+ ed25519
258
+ };
259
+ // Annotate the CommonJS export names for ESM import in node:
260
+ 0 && (module.exports = {
261
+ createChallenge,
262
+ issueToken,
263
+ validateToken,
264
+ verifiers,
265
+ verifySignature
266
+ });
267
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/challenge.ts","../src/secret.ts","../src/verify.ts","../src/token.ts","../src/verifiers/evm.ts","../src/verifiers/ed25519.ts","../src/verifiers/index.ts"],"sourcesContent":["export { createChallenge } from './challenge.js';\nexport { verifySignature } from './verify.js';\nexport { issueToken, validateToken } from './token.js';\nexport { verifiers } from './verifiers/index.js';\nexport type {\n Verifier,\n ChallengeResult,\n ChallengeOptions,\n TokenOptions,\n TokenPayload,\n} from './types.js';\n","import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto';\nimport type { ChallengeOptions, ChallengeResult } from './types.js';\nimport { assertSecret } from './secret.js';\n\nconst DEFAULT_EXPIRES_IN = 5 * 60 * 1000; // 5 minutes\n\ninterface ChallengePayload {\n address: string;\n nonce: string;\n exp: number;\n}\n\nfunction sign(payload: ChallengePayload, secret: string): string {\n const data = JSON.stringify(payload);\n const mac = createHmac('sha256', secret).update(data).digest('base64url');\n const encoded = Buffer.from(data).toString('base64url');\n return `${encoded}.${mac}`;\n}\n\nexport function parseAndVerifyChallenge(\n challenge: string,\n secret: string,\n): ChallengePayload | null {\n const dotIndex = challenge.indexOf('.');\n if (dotIndex === -1) return null;\n\n const encoded = challenge.slice(0, dotIndex);\n const mac = challenge.slice(dotIndex + 1);\n\n let data: string;\n try {\n data = Buffer.from(encoded, 'base64url').toString();\n } catch {\n return null;\n }\n\n const expectedMac = createHmac('sha256', secret).update(data).digest('base64url');\n\n // Constant-time comparison to prevent timing attacks\n const a = Buffer.from(mac);\n const b = Buffer.from(expectedMac);\n if (a.length !== b.length) return null;\n if (!timingSafeEqual(a, b)) return null;\n\n try {\n const payload: ChallengePayload = JSON.parse(data);\n if (Date.now() > payload.exp) return null;\n return payload;\n } catch {\n return null;\n }\n}\n\n/**\n * Generate a stateless HMAC-signed challenge for wallet authentication.\n *\n * @param address - Wallet address (EVM `0x...` or Solana base58)\n * @param secret - Server secret (min 16 chars). Use `WALLETAUTH_SECRET` env var.\n * @param options - Optional. `expiresIn`: challenge TTL in ms (default 5 min).\n * @returns `{ nonce, challenge, expiresAt }` — send all three to the client.\n * The client signs `nonce` and sends back `{ address, signature, challenge }`.\n * @throws If secret is too short or expiresIn is not a finite number.\n */\nexport function createChallenge(\n address: string,\n secret: string,\n options?: ChallengeOptions,\n): ChallengeResult {\n assertSecret(secret);\n const expiresIn = options?.expiresIn ?? DEFAULT_EXPIRES_IN;\n if (!Number.isFinite(expiresIn)) {\n throw new Error('expiresIn must be a finite number');\n }\n const nonce = randomBytes(32).toString('hex');\n const exp = Date.now() + expiresIn;\n\n const payload: ChallengePayload = { address, nonce, exp };\n const challenge = sign(payload, secret);\n\n return { nonce, challenge, expiresAt: exp };\n}\n","const MIN_SECRET_LENGTH = 16;\n\nexport function assertSecret(secret: string): void {\n if (!secret || secret.length < MIN_SECRET_LENGTH) {\n throw new Error(\n `WALLETAUTH_SECRET must be at least ${MIN_SECRET_LENGTH} characters`,\n );\n }\n}\n","import { parseAndVerifyChallenge } from './challenge.js';\nimport type { Verifier } from './types.js';\n\n/**\n * Verify a wallet signature against an HMAC-signed challenge.\n *\n * Checks that the challenge is authentic (HMAC), not expired, and that the\n * wallet signature is valid for the nonce embedded in the challenge.\n *\n * @param address - Wallet address claiming ownership.\n * @param signature - Wallet signature over the nonce.\n * @param challenge - Opaque challenge blob from {@link createChallenge}.\n * @param secret - Same server secret used in `createChallenge`.\n * @param verifier - A {@link Verifier} or array of verifiers (tried in order, first `true` wins).\n * @returns `true` if the signature is valid, `false` otherwise. Never throws.\n */\nexport async function verifySignature(\n address: string,\n signature: string,\n challenge: string,\n secret: string,\n verifier: Verifier | Verifier[],\n): Promise<boolean> {\n try {\n // Verify HMAC challenge\n const payload = parseAndVerifyChallenge(challenge, secret);\n if (!payload) return false;\n\n // Verify address matches\n if (payload.address.toLowerCase() !== address.toLowerCase()) return false;\n\n // Try verifier(s)\n const verifierList = Array.isArray(verifier) ? verifier : [verifier];\n for (const v of verifierList) {\n try {\n const result = await v(address, payload.nonce, signature);\n if (result) return true;\n } catch {\n // Verifier threw — try next\n }\n }\n\n return false;\n } catch {\n return false;\n }\n}\n","import { SignJWT, jwtVerify } from 'jose';\nimport type { TokenOptions, TokenPayload } from './types.js';\nimport { assertSecret } from './secret.js';\n\nconst DEFAULT_EXPIRES_IN = '1h';\n\n/**\n * Issue a JWT for an authenticated wallet address.\n *\n * Call this after {@link verifySignature} returns `true`.\n *\n * @param address - Verified wallet address to embed in the token.\n * @param secret - Server secret (min 16 chars). Same as used for challenges.\n * @param options - Optional. `expiresIn`: JWT lifetime string (default `'1h'`). Examples: `'30m'`, `'2h'`, `'7d'`.\n * @returns Signed JWT string. Send to the client as a Bearer token.\n * @throws If secret is too short.\n */\nexport async function issueToken(\n address: string,\n secret: string,\n options?: TokenOptions,\n): Promise<string> {\n assertSecret(secret);\n const secretKey = new TextEncoder().encode(secret);\n const expiresIn = options?.expiresIn ?? DEFAULT_EXPIRES_IN;\n\n return new SignJWT({ address })\n .setProtectedHeader({ alg: 'HS256' })\n .setIssuedAt()\n .setExpirationTime(expiresIn)\n .sign(secretKey);\n}\n\n/**\n * Validate a JWT and extract the wallet address.\n *\n * Use this in auth middleware to protect routes.\n *\n * @param token - JWT string from the `Authorization: Bearer <token>` header.\n * @param secret - Same server secret used in `issueToken`.\n * @returns `{ address, iat, exp }` if valid, `null` otherwise. Never throws.\n */\nexport async function validateToken(\n token: string,\n secret: string,\n): Promise<TokenPayload | null> {\n try {\n const secretKey = new TextEncoder().encode(secret);\n const { payload } = await jwtVerify(token, secretKey, { algorithms: ['HS256'] });\n\n if (typeof payload.address !== 'string') return null;\n if (typeof payload.iat !== 'number') return null;\n if (typeof payload.exp !== 'number') return null;\n\n return {\n address: payload.address,\n iat: payload.iat,\n exp: payload.exp,\n };\n } catch {\n return null;\n }\n}\n","import { secp256k1 } from '@noble/curves/secp256k1';\nimport { keccak_256 } from '@noble/hashes/sha3';\nimport type { Verifier } from '../types.js';\n\n// EVM personal_sign prefix: \"\\x19Ethereum Signed Message:\\n\" + message length\nfunction hashPersonalMessage(message: string): Uint8Array {\n const messageBytes = new TextEncoder().encode(message);\n const prefix = `\\x19Ethereum Signed Message:\\n${messageBytes.length}`;\n const prefixBytes = new TextEncoder().encode(prefix);\n const combined = new Uint8Array(prefixBytes.length + messageBytes.length);\n combined.set(prefixBytes);\n combined.set(messageBytes, prefixBytes.length);\n return keccak_256(combined);\n}\n\nfunction publicKeyToAddress(publicKey: Uint8Array): string {\n // Remove the 0x04 prefix (uncompressed key marker), hash, take last 20 bytes\n const hash = keccak_256(publicKey.slice(1));\n const addressBytes = hash.slice(-20);\n return '0x' + Buffer.from(addressBytes).toString('hex');\n}\n\n/**\n * EVM signature verifier (Ethereum, Arbitrum, Base, Polygon, etc.).\n *\n * Verifies `personal_sign` (EIP-191) signatures using secp256k1 ecrecover.\n * Accepts signatures with or without `0x` prefix. Address comparison is case-insensitive.\n *\n * @param address - EVM address (`0x...`, 42 chars).\n * @param message - The nonce string that was signed.\n * @param signature - Hex signature: `0x` + r(32B) + s(32B) + v(1B) = 132 chars.\n */\nexport const evm: Verifier = (address, message, signature) => {\n try {\n const hash = hashPersonalMessage(message);\n\n // Parse signature: 0x + r(32 bytes) + s(32 bytes) + v(1 byte) = 0x + 130 hex chars\n const sig = signature.startsWith('0x') ? signature.slice(2) : signature;\n if (sig.length !== 130) return false;\n\n const r = BigInt('0x' + sig.slice(0, 64));\n const s = BigInt('0x' + sig.slice(64, 128));\n const v = parseInt(sig.slice(128, 130), 16);\n\n // v is 27 or 28 (legacy) or 0 or 1 (raw recovery id)\n const recovery = v >= 27 ? v - 27 : v;\n if (recovery !== 0 && recovery !== 1) return false;\n\n const sigObj = new secp256k1.Signature(r, s, recovery);\n const recovered = sigObj.recoverPublicKey(hash);\n const recoveredAddress = publicKeyToAddress(recovered.toRawBytes(false));\n\n return recoveredAddress.toLowerCase() === address.toLowerCase();\n } catch {\n return false;\n }\n};\n","import { createPublicKey, verify } from 'node:crypto';\nimport type { Verifier } from '../types.js';\n\n// Fixed DER prefix for Ed25519 SPKI public keys (12 bytes)\nconst ED25519_DER_PREFIX = Buffer.from('302a300506032b6570032100', 'hex');\n\nconst BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';\n\nfunction decodeBase58(input: string): Buffer {\n const bytes: number[] = [];\n for (const char of input) {\n const idx = BASE58_ALPHABET.indexOf(char);\n if (idx === -1) throw new Error('Invalid base58 character');\n let carry = idx;\n for (let j = 0; j < bytes.length; j++) {\n carry += bytes[j] * 58;\n bytes[j] = carry & 0xff;\n carry >>= 8;\n }\n while (carry > 0) {\n bytes.push(carry & 0xff);\n carry >>= 8;\n }\n }\n bytes.reverse();\n // Leading '1's in base58 = leading zero bytes\n let leadingZeros = 0;\n for (const char of input) {\n if (char !== '1') break;\n leadingZeros++;\n }\n const result = new Uint8Array(leadingZeros + bytes.length);\n // First leadingZeros bytes are already 0, copy computed bytes after\n result.set(bytes, leadingZeros);\n return Buffer.from(result);\n}\n\nfunction parsePublicKey(address: string): Buffer | null {\n try {\n // Hex-encoded 32-byte key (64 hex chars)\n if (/^[0-9a-fA-F]{64}$/.test(address)) {\n return Buffer.from(address, 'hex');\n }\n // Base58-encoded (Solana addresses: 32-44 chars from base58 alphabet)\n if (/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(address)) {\n const decoded = decodeBase58(address);\n if (decoded.length === 32) return decoded;\n }\n return null;\n } catch {\n return null;\n }\n}\n\n/**\n * Ed25519 signature verifier (Solana, raw ed25519 keypairs).\n *\n * Accepts addresses as base58 (Solana, 32-44 chars) or hex (64 chars).\n * Accepts signatures as hex (128 chars), base58 (Solana convention), or base64.\n * Uses Node.js built-in `crypto` — zero external dependencies.\n *\n * @param address - Public key as base58 (Solana) or hex string.\n * @param message - The nonce string that was signed.\n * @param signature - 64-byte signature in hex, base58, or base64 encoding.\n */\nexport const ed25519: Verifier = (address, message, signature) => {\n try {\n const publicKeyRaw = parsePublicKey(address);\n if (!publicKeyRaw || publicKeyRaw.length !== 32) return false;\n\n // Signature: hex (128 chars), base58 (Solana convention), or base64\n let sigBuffer: Buffer;\n if (/^[0-9a-fA-F]{128}$/.test(signature)) {\n sigBuffer = Buffer.from(signature, 'hex');\n } else if (/^[1-9A-HJ-NP-Za-km-z]{1,90}$/.test(signature)) {\n // Could be base58 — decode and validate length, fall back to base64\n const decoded = decodeBase58(signature);\n if (decoded.length === 64) {\n sigBuffer = decoded;\n } else {\n sigBuffer = Buffer.from(signature, 'base64');\n }\n } else {\n sigBuffer = Buffer.from(signature, 'base64');\n }\n if (sigBuffer.length !== 64) return false;\n\n // Wrap raw public key in DER/SPKI format for Node.js crypto\n const derKey = Buffer.concat([ED25519_DER_PREFIX, publicKeyRaw]);\n const keyObject = createPublicKey({\n key: derKey,\n format: 'der',\n type: 'spki',\n });\n\n return verify(null, Buffer.from(message), keyObject, sigBuffer);\n } catch {\n return false;\n }\n};\n","import { evm } from './evm.js';\nimport { ed25519 } from './ed25519.js';\n\nexport const verifiers = {\n /**\n * EVM signature verifier (Ethereum, Arbitrum, Base, Polygon, etc.).\n * Verifies `personal_sign` (EIP-191) signatures using secp256k1 ecrecover.\n */\n evm,\n /**\n * Ed25519 signature verifier (Solana, raw ed25519 keypairs).\n * Accepts addresses as base58 or hex. Signatures as hex, base58, or base64.\n */\n ed25519,\n};\n"],"mappings":"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","names":["DEFAULT_EXPIRES_IN","import_node_crypto"]}
@@ -0,0 +1,82 @@
1
+ type Verifier = (address: string, message: string, signature: string) => boolean | Promise<boolean>;
2
+ interface ChallengeResult {
3
+ nonce: string;
4
+ challenge: string;
5
+ expiresAt: number;
6
+ }
7
+ interface ChallengeOptions {
8
+ expiresIn?: number;
9
+ }
10
+ interface TokenOptions {
11
+ expiresIn?: string;
12
+ }
13
+ interface TokenPayload {
14
+ address: string;
15
+ iat: number;
16
+ exp: number;
17
+ }
18
+
19
+ /**
20
+ * Generate a stateless HMAC-signed challenge for wallet authentication.
21
+ *
22
+ * @param address - Wallet address (EVM `0x...` or Solana base58)
23
+ * @param secret - Server secret (min 16 chars). Use `WALLETAUTH_SECRET` env var.
24
+ * @param options - Optional. `expiresIn`: challenge TTL in ms (default 5 min).
25
+ * @returns `{ nonce, challenge, expiresAt }` — send all three to the client.
26
+ * The client signs `nonce` and sends back `{ address, signature, challenge }`.
27
+ * @throws If secret is too short or expiresIn is not a finite number.
28
+ */
29
+ declare function createChallenge(address: string, secret: string, options?: ChallengeOptions): ChallengeResult;
30
+
31
+ /**
32
+ * Verify a wallet signature against an HMAC-signed challenge.
33
+ *
34
+ * Checks that the challenge is authentic (HMAC), not expired, and that the
35
+ * wallet signature is valid for the nonce embedded in the challenge.
36
+ *
37
+ * @param address - Wallet address claiming ownership.
38
+ * @param signature - Wallet signature over the nonce.
39
+ * @param challenge - Opaque challenge blob from {@link createChallenge}.
40
+ * @param secret - Same server secret used in `createChallenge`.
41
+ * @param verifier - A {@link Verifier} or array of verifiers (tried in order, first `true` wins).
42
+ * @returns `true` if the signature is valid, `false` otherwise. Never throws.
43
+ */
44
+ declare function verifySignature(address: string, signature: string, challenge: string, secret: string, verifier: Verifier | Verifier[]): Promise<boolean>;
45
+
46
+ /**
47
+ * Issue a JWT for an authenticated wallet address.
48
+ *
49
+ * Call this after {@link verifySignature} returns `true`.
50
+ *
51
+ * @param address - Verified wallet address to embed in the token.
52
+ * @param secret - Server secret (min 16 chars). Same as used for challenges.
53
+ * @param options - Optional. `expiresIn`: JWT lifetime string (default `'1h'`). Examples: `'30m'`, `'2h'`, `'7d'`.
54
+ * @returns Signed JWT string. Send to the client as a Bearer token.
55
+ * @throws If secret is too short.
56
+ */
57
+ declare function issueToken(address: string, secret: string, options?: TokenOptions): Promise<string>;
58
+ /**
59
+ * Validate a JWT and extract the wallet address.
60
+ *
61
+ * Use this in auth middleware to protect routes.
62
+ *
63
+ * @param token - JWT string from the `Authorization: Bearer <token>` header.
64
+ * @param secret - Same server secret used in `issueToken`.
65
+ * @returns `{ address, iat, exp }` if valid, `null` otherwise. Never throws.
66
+ */
67
+ declare function validateToken(token: string, secret: string): Promise<TokenPayload | null>;
68
+
69
+ declare const verifiers: {
70
+ /**
71
+ * EVM signature verifier (Ethereum, Arbitrum, Base, Polygon, etc.).
72
+ * Verifies `personal_sign` (EIP-191) signatures using secp256k1 ecrecover.
73
+ */
74
+ evm: Verifier;
75
+ /**
76
+ * Ed25519 signature verifier (Solana, raw ed25519 keypairs).
77
+ * Accepts addresses as base58 or hex. Signatures as hex, base58, or base64.
78
+ */
79
+ ed25519: Verifier;
80
+ };
81
+
82
+ export { type ChallengeOptions, type ChallengeResult, type TokenOptions, type TokenPayload, type Verifier, createChallenge, issueToken, validateToken, verifiers, verifySignature };
@@ -0,0 +1,82 @@
1
+ type Verifier = (address: string, message: string, signature: string) => boolean | Promise<boolean>;
2
+ interface ChallengeResult {
3
+ nonce: string;
4
+ challenge: string;
5
+ expiresAt: number;
6
+ }
7
+ interface ChallengeOptions {
8
+ expiresIn?: number;
9
+ }
10
+ interface TokenOptions {
11
+ expiresIn?: string;
12
+ }
13
+ interface TokenPayload {
14
+ address: string;
15
+ iat: number;
16
+ exp: number;
17
+ }
18
+
19
+ /**
20
+ * Generate a stateless HMAC-signed challenge for wallet authentication.
21
+ *
22
+ * @param address - Wallet address (EVM `0x...` or Solana base58)
23
+ * @param secret - Server secret (min 16 chars). Use `WALLETAUTH_SECRET` env var.
24
+ * @param options - Optional. `expiresIn`: challenge TTL in ms (default 5 min).
25
+ * @returns `{ nonce, challenge, expiresAt }` — send all three to the client.
26
+ * The client signs `nonce` and sends back `{ address, signature, challenge }`.
27
+ * @throws If secret is too short or expiresIn is not a finite number.
28
+ */
29
+ declare function createChallenge(address: string, secret: string, options?: ChallengeOptions): ChallengeResult;
30
+
31
+ /**
32
+ * Verify a wallet signature against an HMAC-signed challenge.
33
+ *
34
+ * Checks that the challenge is authentic (HMAC), not expired, and that the
35
+ * wallet signature is valid for the nonce embedded in the challenge.
36
+ *
37
+ * @param address - Wallet address claiming ownership.
38
+ * @param signature - Wallet signature over the nonce.
39
+ * @param challenge - Opaque challenge blob from {@link createChallenge}.
40
+ * @param secret - Same server secret used in `createChallenge`.
41
+ * @param verifier - A {@link Verifier} or array of verifiers (tried in order, first `true` wins).
42
+ * @returns `true` if the signature is valid, `false` otherwise. Never throws.
43
+ */
44
+ declare function verifySignature(address: string, signature: string, challenge: string, secret: string, verifier: Verifier | Verifier[]): Promise<boolean>;
45
+
46
+ /**
47
+ * Issue a JWT for an authenticated wallet address.
48
+ *
49
+ * Call this after {@link verifySignature} returns `true`.
50
+ *
51
+ * @param address - Verified wallet address to embed in the token.
52
+ * @param secret - Server secret (min 16 chars). Same as used for challenges.
53
+ * @param options - Optional. `expiresIn`: JWT lifetime string (default `'1h'`). Examples: `'30m'`, `'2h'`, `'7d'`.
54
+ * @returns Signed JWT string. Send to the client as a Bearer token.
55
+ * @throws If secret is too short.
56
+ */
57
+ declare function issueToken(address: string, secret: string, options?: TokenOptions): Promise<string>;
58
+ /**
59
+ * Validate a JWT and extract the wallet address.
60
+ *
61
+ * Use this in auth middleware to protect routes.
62
+ *
63
+ * @param token - JWT string from the `Authorization: Bearer <token>` header.
64
+ * @param secret - Same server secret used in `issueToken`.
65
+ * @returns `{ address, iat, exp }` if valid, `null` otherwise. Never throws.
66
+ */
67
+ declare function validateToken(token: string, secret: string): Promise<TokenPayload | null>;
68
+
69
+ declare const verifiers: {
70
+ /**
71
+ * EVM signature verifier (Ethereum, Arbitrum, Base, Polygon, etc.).
72
+ * Verifies `personal_sign` (EIP-191) signatures using secp256k1 ecrecover.
73
+ */
74
+ evm: Verifier;
75
+ /**
76
+ * Ed25519 signature verifier (Solana, raw ed25519 keypairs).
77
+ * Accepts addresses as base58 or hex. Signatures as hex, base58, or base64.
78
+ */
79
+ ed25519: Verifier;
80
+ };
81
+
82
+ export { type ChallengeOptions, type ChallengeResult, type TokenOptions, type TokenPayload, type Verifier, createChallenge, issueToken, validateToken, verifiers, verifySignature };
package/dist/index.js ADDED
@@ -0,0 +1,236 @@
1
+ // src/challenge.ts
2
+ import { createHmac, randomBytes, timingSafeEqual } from "crypto";
3
+
4
+ // src/secret.ts
5
+ var MIN_SECRET_LENGTH = 16;
6
+ function assertSecret(secret) {
7
+ if (!secret || secret.length < MIN_SECRET_LENGTH) {
8
+ throw new Error(
9
+ `WALLETAUTH_SECRET must be at least ${MIN_SECRET_LENGTH} characters`
10
+ );
11
+ }
12
+ }
13
+
14
+ // src/challenge.ts
15
+ var DEFAULT_EXPIRES_IN = 5 * 60 * 1e3;
16
+ function sign(payload, secret) {
17
+ const data = JSON.stringify(payload);
18
+ const mac = createHmac("sha256", secret).update(data).digest("base64url");
19
+ const encoded = Buffer.from(data).toString("base64url");
20
+ return `${encoded}.${mac}`;
21
+ }
22
+ function parseAndVerifyChallenge(challenge, secret) {
23
+ const dotIndex = challenge.indexOf(".");
24
+ if (dotIndex === -1) return null;
25
+ const encoded = challenge.slice(0, dotIndex);
26
+ const mac = challenge.slice(dotIndex + 1);
27
+ let data;
28
+ try {
29
+ data = Buffer.from(encoded, "base64url").toString();
30
+ } catch {
31
+ return null;
32
+ }
33
+ const expectedMac = createHmac("sha256", secret).update(data).digest("base64url");
34
+ const a = Buffer.from(mac);
35
+ const b = Buffer.from(expectedMac);
36
+ if (a.length !== b.length) return null;
37
+ if (!timingSafeEqual(a, b)) return null;
38
+ try {
39
+ const payload = JSON.parse(data);
40
+ if (Date.now() > payload.exp) return null;
41
+ return payload;
42
+ } catch {
43
+ return null;
44
+ }
45
+ }
46
+ function createChallenge(address, secret, options) {
47
+ assertSecret(secret);
48
+ const expiresIn = options?.expiresIn ?? DEFAULT_EXPIRES_IN;
49
+ if (!Number.isFinite(expiresIn)) {
50
+ throw new Error("expiresIn must be a finite number");
51
+ }
52
+ const nonce = randomBytes(32).toString("hex");
53
+ const exp = Date.now() + expiresIn;
54
+ const payload = { address, nonce, exp };
55
+ const challenge = sign(payload, secret);
56
+ return { nonce, challenge, expiresAt: exp };
57
+ }
58
+
59
+ // src/verify.ts
60
+ async function verifySignature(address, signature, challenge, secret, verifier) {
61
+ try {
62
+ const payload = parseAndVerifyChallenge(challenge, secret);
63
+ if (!payload) return false;
64
+ if (payload.address.toLowerCase() !== address.toLowerCase()) return false;
65
+ const verifierList = Array.isArray(verifier) ? verifier : [verifier];
66
+ for (const v of verifierList) {
67
+ try {
68
+ const result = await v(address, payload.nonce, signature);
69
+ if (result) return true;
70
+ } catch {
71
+ }
72
+ }
73
+ return false;
74
+ } catch {
75
+ return false;
76
+ }
77
+ }
78
+
79
+ // src/token.ts
80
+ import { SignJWT, jwtVerify } from "jose";
81
+ var DEFAULT_EXPIRES_IN2 = "1h";
82
+ async function issueToken(address, secret, options) {
83
+ assertSecret(secret);
84
+ const secretKey = new TextEncoder().encode(secret);
85
+ const expiresIn = options?.expiresIn ?? DEFAULT_EXPIRES_IN2;
86
+ return new SignJWT({ address }).setProtectedHeader({ alg: "HS256" }).setIssuedAt().setExpirationTime(expiresIn).sign(secretKey);
87
+ }
88
+ async function validateToken(token, secret) {
89
+ try {
90
+ const secretKey = new TextEncoder().encode(secret);
91
+ const { payload } = await jwtVerify(token, secretKey, { algorithms: ["HS256"] });
92
+ if (typeof payload.address !== "string") return null;
93
+ if (typeof payload.iat !== "number") return null;
94
+ if (typeof payload.exp !== "number") return null;
95
+ return {
96
+ address: payload.address,
97
+ iat: payload.iat,
98
+ exp: payload.exp
99
+ };
100
+ } catch {
101
+ return null;
102
+ }
103
+ }
104
+
105
+ // src/verifiers/evm.ts
106
+ import { secp256k1 } from "@noble/curves/secp256k1";
107
+ import { keccak_256 } from "@noble/hashes/sha3";
108
+ function hashPersonalMessage(message) {
109
+ const messageBytes = new TextEncoder().encode(message);
110
+ const prefix = `Ethereum Signed Message:
111
+ ${messageBytes.length}`;
112
+ const prefixBytes = new TextEncoder().encode(prefix);
113
+ const combined = new Uint8Array(prefixBytes.length + messageBytes.length);
114
+ combined.set(prefixBytes);
115
+ combined.set(messageBytes, prefixBytes.length);
116
+ return keccak_256(combined);
117
+ }
118
+ function publicKeyToAddress(publicKey) {
119
+ const hash = keccak_256(publicKey.slice(1));
120
+ const addressBytes = hash.slice(-20);
121
+ return "0x" + Buffer.from(addressBytes).toString("hex");
122
+ }
123
+ var evm = (address, message, signature) => {
124
+ try {
125
+ const hash = hashPersonalMessage(message);
126
+ const sig = signature.startsWith("0x") ? signature.slice(2) : signature;
127
+ if (sig.length !== 130) return false;
128
+ const r = BigInt("0x" + sig.slice(0, 64));
129
+ const s = BigInt("0x" + sig.slice(64, 128));
130
+ const v = parseInt(sig.slice(128, 130), 16);
131
+ const recovery = v >= 27 ? v - 27 : v;
132
+ if (recovery !== 0 && recovery !== 1) return false;
133
+ const sigObj = new secp256k1.Signature(r, s, recovery);
134
+ const recovered = sigObj.recoverPublicKey(hash);
135
+ const recoveredAddress = publicKeyToAddress(recovered.toRawBytes(false));
136
+ return recoveredAddress.toLowerCase() === address.toLowerCase();
137
+ } catch {
138
+ return false;
139
+ }
140
+ };
141
+
142
+ // src/verifiers/ed25519.ts
143
+ import { createPublicKey, verify } from "crypto";
144
+ var ED25519_DER_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
145
+ var BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
146
+ function decodeBase58(input) {
147
+ const bytes = [];
148
+ for (const char of input) {
149
+ const idx = BASE58_ALPHABET.indexOf(char);
150
+ if (idx === -1) throw new Error("Invalid base58 character");
151
+ let carry = idx;
152
+ for (let j = 0; j < bytes.length; j++) {
153
+ carry += bytes[j] * 58;
154
+ bytes[j] = carry & 255;
155
+ carry >>= 8;
156
+ }
157
+ while (carry > 0) {
158
+ bytes.push(carry & 255);
159
+ carry >>= 8;
160
+ }
161
+ }
162
+ bytes.reverse();
163
+ let leadingZeros = 0;
164
+ for (const char of input) {
165
+ if (char !== "1") break;
166
+ leadingZeros++;
167
+ }
168
+ const result = new Uint8Array(leadingZeros + bytes.length);
169
+ result.set(bytes, leadingZeros);
170
+ return Buffer.from(result);
171
+ }
172
+ function parsePublicKey(address) {
173
+ try {
174
+ if (/^[0-9a-fA-F]{64}$/.test(address)) {
175
+ return Buffer.from(address, "hex");
176
+ }
177
+ if (/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(address)) {
178
+ const decoded = decodeBase58(address);
179
+ if (decoded.length === 32) return decoded;
180
+ }
181
+ return null;
182
+ } catch {
183
+ return null;
184
+ }
185
+ }
186
+ var ed25519 = (address, message, signature) => {
187
+ try {
188
+ const publicKeyRaw = parsePublicKey(address);
189
+ if (!publicKeyRaw || publicKeyRaw.length !== 32) return false;
190
+ let sigBuffer;
191
+ if (/^[0-9a-fA-F]{128}$/.test(signature)) {
192
+ sigBuffer = Buffer.from(signature, "hex");
193
+ } else if (/^[1-9A-HJ-NP-Za-km-z]{1,90}$/.test(signature)) {
194
+ const decoded = decodeBase58(signature);
195
+ if (decoded.length === 64) {
196
+ sigBuffer = decoded;
197
+ } else {
198
+ sigBuffer = Buffer.from(signature, "base64");
199
+ }
200
+ } else {
201
+ sigBuffer = Buffer.from(signature, "base64");
202
+ }
203
+ if (sigBuffer.length !== 64) return false;
204
+ const derKey = Buffer.concat([ED25519_DER_PREFIX, publicKeyRaw]);
205
+ const keyObject = createPublicKey({
206
+ key: derKey,
207
+ format: "der",
208
+ type: "spki"
209
+ });
210
+ return verify(null, Buffer.from(message), keyObject, sigBuffer);
211
+ } catch {
212
+ return false;
213
+ }
214
+ };
215
+
216
+ // src/verifiers/index.ts
217
+ var verifiers = {
218
+ /**
219
+ * EVM signature verifier (Ethereum, Arbitrum, Base, Polygon, etc.).
220
+ * Verifies `personal_sign` (EIP-191) signatures using secp256k1 ecrecover.
221
+ */
222
+ evm,
223
+ /**
224
+ * Ed25519 signature verifier (Solana, raw ed25519 keypairs).
225
+ * Accepts addresses as base58 or hex. Signatures as hex, base58, or base64.
226
+ */
227
+ ed25519
228
+ };
229
+ export {
230
+ createChallenge,
231
+ issueToken,
232
+ validateToken,
233
+ verifiers,
234
+ verifySignature
235
+ };
236
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/challenge.ts","../src/secret.ts","../src/verify.ts","../src/token.ts","../src/verifiers/evm.ts","../src/verifiers/ed25519.ts","../src/verifiers/index.ts"],"sourcesContent":["import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto';\nimport type { ChallengeOptions, ChallengeResult } from './types.js';\nimport { assertSecret } from './secret.js';\n\nconst DEFAULT_EXPIRES_IN = 5 * 60 * 1000; // 5 minutes\n\ninterface ChallengePayload {\n address: string;\n nonce: string;\n exp: number;\n}\n\nfunction sign(payload: ChallengePayload, secret: string): string {\n const data = JSON.stringify(payload);\n const mac = createHmac('sha256', secret).update(data).digest('base64url');\n const encoded = Buffer.from(data).toString('base64url');\n return `${encoded}.${mac}`;\n}\n\nexport function parseAndVerifyChallenge(\n challenge: string,\n secret: string,\n): ChallengePayload | null {\n const dotIndex = challenge.indexOf('.');\n if (dotIndex === -1) return null;\n\n const encoded = challenge.slice(0, dotIndex);\n const mac = challenge.slice(dotIndex + 1);\n\n let data: string;\n try {\n data = Buffer.from(encoded, 'base64url').toString();\n } catch {\n return null;\n }\n\n const expectedMac = createHmac('sha256', secret).update(data).digest('base64url');\n\n // Constant-time comparison to prevent timing attacks\n const a = Buffer.from(mac);\n const b = Buffer.from(expectedMac);\n if (a.length !== b.length) return null;\n if (!timingSafeEqual(a, b)) return null;\n\n try {\n const payload: ChallengePayload = JSON.parse(data);\n if (Date.now() > payload.exp) return null;\n return payload;\n } catch {\n return null;\n }\n}\n\n/**\n * Generate a stateless HMAC-signed challenge for wallet authentication.\n *\n * @param address - Wallet address (EVM `0x...` or Solana base58)\n * @param secret - Server secret (min 16 chars). Use `WALLETAUTH_SECRET` env var.\n * @param options - Optional. `expiresIn`: challenge TTL in ms (default 5 min).\n * @returns `{ nonce, challenge, expiresAt }` — send all three to the client.\n * The client signs `nonce` and sends back `{ address, signature, challenge }`.\n * @throws If secret is too short or expiresIn is not a finite number.\n */\nexport function createChallenge(\n address: string,\n secret: string,\n options?: ChallengeOptions,\n): ChallengeResult {\n assertSecret(secret);\n const expiresIn = options?.expiresIn ?? DEFAULT_EXPIRES_IN;\n if (!Number.isFinite(expiresIn)) {\n throw new Error('expiresIn must be a finite number');\n }\n const nonce = randomBytes(32).toString('hex');\n const exp = Date.now() + expiresIn;\n\n const payload: ChallengePayload = { address, nonce, exp };\n const challenge = sign(payload, secret);\n\n return { nonce, challenge, expiresAt: exp };\n}\n","const MIN_SECRET_LENGTH = 16;\n\nexport function assertSecret(secret: string): void {\n if (!secret || secret.length < MIN_SECRET_LENGTH) {\n throw new Error(\n `WALLETAUTH_SECRET must be at least ${MIN_SECRET_LENGTH} characters`,\n );\n }\n}\n","import { parseAndVerifyChallenge } from './challenge.js';\nimport type { Verifier } from './types.js';\n\n/**\n * Verify a wallet signature against an HMAC-signed challenge.\n *\n * Checks that the challenge is authentic (HMAC), not expired, and that the\n * wallet signature is valid for the nonce embedded in the challenge.\n *\n * @param address - Wallet address claiming ownership.\n * @param signature - Wallet signature over the nonce.\n * @param challenge - Opaque challenge blob from {@link createChallenge}.\n * @param secret - Same server secret used in `createChallenge`.\n * @param verifier - A {@link Verifier} or array of verifiers (tried in order, first `true` wins).\n * @returns `true` if the signature is valid, `false` otherwise. Never throws.\n */\nexport async function verifySignature(\n address: string,\n signature: string,\n challenge: string,\n secret: string,\n verifier: Verifier | Verifier[],\n): Promise<boolean> {\n try {\n // Verify HMAC challenge\n const payload = parseAndVerifyChallenge(challenge, secret);\n if (!payload) return false;\n\n // Verify address matches\n if (payload.address.toLowerCase() !== address.toLowerCase()) return false;\n\n // Try verifier(s)\n const verifierList = Array.isArray(verifier) ? verifier : [verifier];\n for (const v of verifierList) {\n try {\n const result = await v(address, payload.nonce, signature);\n if (result) return true;\n } catch {\n // Verifier threw — try next\n }\n }\n\n return false;\n } catch {\n return false;\n }\n}\n","import { SignJWT, jwtVerify } from 'jose';\nimport type { TokenOptions, TokenPayload } from './types.js';\nimport { assertSecret } from './secret.js';\n\nconst DEFAULT_EXPIRES_IN = '1h';\n\n/**\n * Issue a JWT for an authenticated wallet address.\n *\n * Call this after {@link verifySignature} returns `true`.\n *\n * @param address - Verified wallet address to embed in the token.\n * @param secret - Server secret (min 16 chars). Same as used for challenges.\n * @param options - Optional. `expiresIn`: JWT lifetime string (default `'1h'`). Examples: `'30m'`, `'2h'`, `'7d'`.\n * @returns Signed JWT string. Send to the client as a Bearer token.\n * @throws If secret is too short.\n */\nexport async function issueToken(\n address: string,\n secret: string,\n options?: TokenOptions,\n): Promise<string> {\n assertSecret(secret);\n const secretKey = new TextEncoder().encode(secret);\n const expiresIn = options?.expiresIn ?? DEFAULT_EXPIRES_IN;\n\n return new SignJWT({ address })\n .setProtectedHeader({ alg: 'HS256' })\n .setIssuedAt()\n .setExpirationTime(expiresIn)\n .sign(secretKey);\n}\n\n/**\n * Validate a JWT and extract the wallet address.\n *\n * Use this in auth middleware to protect routes.\n *\n * @param token - JWT string from the `Authorization: Bearer <token>` header.\n * @param secret - Same server secret used in `issueToken`.\n * @returns `{ address, iat, exp }` if valid, `null` otherwise. Never throws.\n */\nexport async function validateToken(\n token: string,\n secret: string,\n): Promise<TokenPayload | null> {\n try {\n const secretKey = new TextEncoder().encode(secret);\n const { payload } = await jwtVerify(token, secretKey, { algorithms: ['HS256'] });\n\n if (typeof payload.address !== 'string') return null;\n if (typeof payload.iat !== 'number') return null;\n if (typeof payload.exp !== 'number') return null;\n\n return {\n address: payload.address,\n iat: payload.iat,\n exp: payload.exp,\n };\n } catch {\n return null;\n }\n}\n","import { secp256k1 } from '@noble/curves/secp256k1';\nimport { keccak_256 } from '@noble/hashes/sha3';\nimport type { Verifier } from '../types.js';\n\n// EVM personal_sign prefix: \"\\x19Ethereum Signed Message:\\n\" + message length\nfunction hashPersonalMessage(message: string): Uint8Array {\n const messageBytes = new TextEncoder().encode(message);\n const prefix = `\\x19Ethereum Signed Message:\\n${messageBytes.length}`;\n const prefixBytes = new TextEncoder().encode(prefix);\n const combined = new Uint8Array(prefixBytes.length + messageBytes.length);\n combined.set(prefixBytes);\n combined.set(messageBytes, prefixBytes.length);\n return keccak_256(combined);\n}\n\nfunction publicKeyToAddress(publicKey: Uint8Array): string {\n // Remove the 0x04 prefix (uncompressed key marker), hash, take last 20 bytes\n const hash = keccak_256(publicKey.slice(1));\n const addressBytes = hash.slice(-20);\n return '0x' + Buffer.from(addressBytes).toString('hex');\n}\n\n/**\n * EVM signature verifier (Ethereum, Arbitrum, Base, Polygon, etc.).\n *\n * Verifies `personal_sign` (EIP-191) signatures using secp256k1 ecrecover.\n * Accepts signatures with or without `0x` prefix. Address comparison is case-insensitive.\n *\n * @param address - EVM address (`0x...`, 42 chars).\n * @param message - The nonce string that was signed.\n * @param signature - Hex signature: `0x` + r(32B) + s(32B) + v(1B) = 132 chars.\n */\nexport const evm: Verifier = (address, message, signature) => {\n try {\n const hash = hashPersonalMessage(message);\n\n // Parse signature: 0x + r(32 bytes) + s(32 bytes) + v(1 byte) = 0x + 130 hex chars\n const sig = signature.startsWith('0x') ? signature.slice(2) : signature;\n if (sig.length !== 130) return false;\n\n const r = BigInt('0x' + sig.slice(0, 64));\n const s = BigInt('0x' + sig.slice(64, 128));\n const v = parseInt(sig.slice(128, 130), 16);\n\n // v is 27 or 28 (legacy) or 0 or 1 (raw recovery id)\n const recovery = v >= 27 ? v - 27 : v;\n if (recovery !== 0 && recovery !== 1) return false;\n\n const sigObj = new secp256k1.Signature(r, s, recovery);\n const recovered = sigObj.recoverPublicKey(hash);\n const recoveredAddress = publicKeyToAddress(recovered.toRawBytes(false));\n\n return recoveredAddress.toLowerCase() === address.toLowerCase();\n } catch {\n return false;\n }\n};\n","import { createPublicKey, verify } from 'node:crypto';\nimport type { Verifier } from '../types.js';\n\n// Fixed DER prefix for Ed25519 SPKI public keys (12 bytes)\nconst ED25519_DER_PREFIX = Buffer.from('302a300506032b6570032100', 'hex');\n\nconst BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';\n\nfunction decodeBase58(input: string): Buffer {\n const bytes: number[] = [];\n for (const char of input) {\n const idx = BASE58_ALPHABET.indexOf(char);\n if (idx === -1) throw new Error('Invalid base58 character');\n let carry = idx;\n for (let j = 0; j < bytes.length; j++) {\n carry += bytes[j] * 58;\n bytes[j] = carry & 0xff;\n carry >>= 8;\n }\n while (carry > 0) {\n bytes.push(carry & 0xff);\n carry >>= 8;\n }\n }\n bytes.reverse();\n // Leading '1's in base58 = leading zero bytes\n let leadingZeros = 0;\n for (const char of input) {\n if (char !== '1') break;\n leadingZeros++;\n }\n const result = new Uint8Array(leadingZeros + bytes.length);\n // First leadingZeros bytes are already 0, copy computed bytes after\n result.set(bytes, leadingZeros);\n return Buffer.from(result);\n}\n\nfunction parsePublicKey(address: string): Buffer | null {\n try {\n // Hex-encoded 32-byte key (64 hex chars)\n if (/^[0-9a-fA-F]{64}$/.test(address)) {\n return Buffer.from(address, 'hex');\n }\n // Base58-encoded (Solana addresses: 32-44 chars from base58 alphabet)\n if (/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(address)) {\n const decoded = decodeBase58(address);\n if (decoded.length === 32) return decoded;\n }\n return null;\n } catch {\n return null;\n }\n}\n\n/**\n * Ed25519 signature verifier (Solana, raw ed25519 keypairs).\n *\n * Accepts addresses as base58 (Solana, 32-44 chars) or hex (64 chars).\n * Accepts signatures as hex (128 chars), base58 (Solana convention), or base64.\n * Uses Node.js built-in `crypto` — zero external dependencies.\n *\n * @param address - Public key as base58 (Solana) or hex string.\n * @param message - The nonce string that was signed.\n * @param signature - 64-byte signature in hex, base58, or base64 encoding.\n */\nexport const ed25519: Verifier = (address, message, signature) => {\n try {\n const publicKeyRaw = parsePublicKey(address);\n if (!publicKeyRaw || publicKeyRaw.length !== 32) return false;\n\n // Signature: hex (128 chars), base58 (Solana convention), or base64\n let sigBuffer: Buffer;\n if (/^[0-9a-fA-F]{128}$/.test(signature)) {\n sigBuffer = Buffer.from(signature, 'hex');\n } else if (/^[1-9A-HJ-NP-Za-km-z]{1,90}$/.test(signature)) {\n // Could be base58 — decode and validate length, fall back to base64\n const decoded = decodeBase58(signature);\n if (decoded.length === 64) {\n sigBuffer = decoded;\n } else {\n sigBuffer = Buffer.from(signature, 'base64');\n }\n } else {\n sigBuffer = Buffer.from(signature, 'base64');\n }\n if (sigBuffer.length !== 64) return false;\n\n // Wrap raw public key in DER/SPKI format for Node.js crypto\n const derKey = Buffer.concat([ED25519_DER_PREFIX, publicKeyRaw]);\n const keyObject = createPublicKey({\n key: derKey,\n format: 'der',\n type: 'spki',\n });\n\n return verify(null, Buffer.from(message), keyObject, sigBuffer);\n } catch {\n return false;\n }\n};\n","import { evm } from './evm.js';\nimport { ed25519 } from './ed25519.js';\n\nexport const verifiers = {\n /**\n * EVM signature verifier (Ethereum, Arbitrum, Base, Polygon, etc.).\n * Verifies `personal_sign` (EIP-191) signatures using secp256k1 ecrecover.\n */\n evm,\n /**\n * Ed25519 signature verifier (Solana, raw ed25519 keypairs).\n * Accepts addresses as base58 or hex. Signatures as hex, base58, or base64.\n */\n ed25519,\n};\n"],"mappings":"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","names":["DEFAULT_EXPIRES_IN"]}
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "@shipooor/walletauth",
3
+ "version": "0.1.0",
4
+ "description": "Your wallet is your API key. Agent-native auth for APIs.",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
+ }
15
+ },
16
+ "sideEffects": false,
17
+ "files": [
18
+ "dist"
19
+ ],
20
+ "scripts": {
21
+ "build": "tsup",
22
+ "test": "vitest run",
23
+ "test:watch": "vitest",
24
+ "prepublishOnly": "npm run build"
25
+ },
26
+ "keywords": [
27
+ "wallet",
28
+ "auth",
29
+ "authentication",
30
+ "signature",
31
+ "jwt",
32
+ "agent",
33
+ "ai-agent",
34
+ "web3",
35
+ "evm",
36
+ "solana",
37
+ "ed25519",
38
+ "secp256k1"
39
+ ],
40
+ "author": "walletauth",
41
+ "license": "MIT",
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "git+https://github.com/shipooor/walletauth.git"
45
+ },
46
+ "homepage": "https://walletauth.shipooor.xyz",
47
+ "bugs": {
48
+ "url": "https://github.com/shipooor/walletauth/issues"
49
+ },
50
+ "publishConfig": {
51
+ "access": "public"
52
+ },
53
+ "engines": {
54
+ "node": ">=18"
55
+ },
56
+ "dependencies": {
57
+ "@noble/curves": "^1.8.0",
58
+ "@noble/hashes": "^1.7.0",
59
+ "jose": "^6.0.0"
60
+ },
61
+ "devDependencies": {
62
+ "@types/node": "^25.5.0",
63
+ "tsup": "^8.0.0",
64
+ "typescript": "^5.7.0",
65
+ "vitest": "^3.0.0"
66
+ }
67
+ }