@agentcash/router 0.4.5 → 0.4.7

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/.claude/CLAUDE.md CHANGED
@@ -28,6 +28,98 @@ Protocol-agnostic route framework for Next.js App Router APIs with x402 payment,
28
28
  - `src/protocols/` — Protocol handlers (x402.ts, mpp.ts, detect.ts)
29
29
  - `src/discovery/` — Auto-generated endpoints (well-known.ts, openapi.ts)
30
30
 
31
+ ## Auth Modes
32
+
33
+ Four auth modes, mutually exclusive (except `.apiKey()` composes with `.paid()`):
34
+
35
+ ### `.paid(pricing)` — Payment required
36
+ ```typescript
37
+ .paid('0.01') // Static price
38
+ .paid((body) => calcPrice(body)) // Dynamic pricing
39
+ .paid({ field: 'tier', tiers: { basic: { price: '0.01' } } }) // Tiered
40
+ ```
41
+
42
+ ### `.siwx()` — Wallet identity required (no payment)
43
+ ```typescript
44
+ .siwx().handler(async ({ wallet }) => { /* wallet is verified */ })
45
+ ```
46
+
47
+ ### `.apiKey(resolver)` — API key / Bearer token auth
48
+ For admin routes, cron jobs, internal services. Checks `X-API-Key` header OR `Authorization: Bearer <token>`.
49
+
50
+ ```typescript
51
+ // Admin route with API key
52
+ export const GET = router
53
+ .route('admin/users')
54
+ .apiKey(async (key) => {
55
+ const admin = await db.admin.findByKey(key);
56
+ return admin ?? null; // null = 401, truthy = ctx.account
57
+ })
58
+ .handler(async ({ account }) => {
59
+ // account is whatever resolver returned
60
+ return db.user.findMany();
61
+ });
62
+
63
+ // Cron job with static secret
64
+ export const POST = router
65
+ .route('cron/cleanup')
66
+ .apiKey((key) => key === process.env.CRON_SECRET ? { cron: true } : null)
67
+ .handler(async () => { /* ... */ });
68
+ ```
69
+
70
+ **Headers accepted:** `X-API-Key: <key>` or `Authorization: Bearer <key>`
71
+
72
+ **Composing with payment:** `.apiKey()` can layer on `.paid()` — auth runs first, payment second:
73
+ ```typescript
74
+ .apiKey(resolver).paid('0.01') // Must pass API key AND pay
75
+ ```
76
+
77
+ ### `.unprotected()` — No auth
78
+ ```typescript
79
+ .unprotected().handler(async () => { /* public endpoint */ })
80
+ ```
81
+
82
+ ## Pre-Payment Validation
83
+
84
+ ### `.validate(fn)` — Async business validation before 402 challenge
85
+
86
+ For checks that need DB lookups or external APIs before showing a price. Runs after body parsing, before the 402 challenge. Requires `.body()`.
87
+
88
+ ```typescript
89
+ // Domain registration with availability check
90
+ router
91
+ .route('domain/register')
92
+ .paid(calculatePrice, { maxPrice: '10.00' })
93
+ .body(RegisterSchema) // .body() before .validate() for type inference
94
+ .validate(async (body) => {
95
+ if (await isDomainTaken(body.domain)) {
96
+ throw Object.assign(new Error('Domain already taken'), { status: 409 });
97
+ }
98
+ })
99
+ .handler(async ({ body, wallet }) => {
100
+ return registerDomain(body.domain, wallet);
101
+ });
102
+
103
+ // Rate limiting before payment
104
+ router
105
+ .route('api/expensive')
106
+ .paid('1.00')
107
+ .body(RequestSchema)
108
+ .validate(async (body) => {
109
+ const usage = await getUserUsage(body.userId);
110
+ if (usage >= DAILY_LIMIT) {
111
+ throw Object.assign(new Error('Daily limit reached'), { status: 429 });
112
+ }
113
+ })
114
+ .handler(async ({ body }) => { ... });
115
+ ```
116
+
117
+ **Pipeline order:** `body parse → validate → 402 challenge → payment → handler`
118
+
119
+ **Error handling:** Respects `.status` on thrown errors (default: 400). Use `Object.assign(new Error('msg'), { status: 409 })` for custom codes.
120
+
121
+ **Works with all auth modes:** paid, siwx, apiKey, unprotected.
122
+
31
123
  ## Critical Rules
32
124
 
33
125
  - **Error handling:** Respect `.status` on any thrown error, not just `HttpError`. The `Object.assign(new Error(), { status })` pattern is universal in Node.js.
@@ -95,6 +187,43 @@ pnpm typecheck # tsc --noEmit
95
187
  pnpm check # format + lint + typecheck + build + test
96
188
  ```
97
189
 
190
+ ## Releasing
191
+
192
+ **Release flow:** PR with version bump → merge → create GitHub Release → auto-publish to npm
193
+
194
+ ### When doing work that should be released:
195
+
196
+ 1. **Update `CHANGELOG.md`** — Add entry under new version heading with changes
197
+ 2. **Bump version in `package.json`** — Match the changelog version
198
+ 3. **Commit both** — e.g., `chore: bump to v0.6.0`
199
+ 4. **Merge PR to main**
200
+
201
+ ### To publish (human step):
202
+
203
+ 1. Go to [GitHub Releases](https://github.com/Merit-Systems/agentcash-router/releases)
204
+ 2. Click **Draft a new release**
205
+ 3. Create tag: `v0.6.0` (must match package.json version)
206
+ 4. Title: `v0.6.0`
207
+ 5. Description: Copy from CHANGELOG.md or click "Generate release notes"
208
+ 6. Click **Publish release**
209
+
210
+ The `publish.yml` workflow will:
211
+ - Run full test suite (`pnpm check`)
212
+ - Verify package.json version matches tag
213
+ - Publish to npm with `--access public`
214
+
215
+ ### Version format
216
+
217
+ - **Patch** (`0.5.1`): Bug fixes, docs, internal changes
218
+ - **Minor** (`0.6.0`): New features, non-breaking additions
219
+ - **Major** (`1.0.0`): Breaking changes (holding until API stabilizes)
220
+
221
+ ### Troubleshooting
222
+
223
+ - **Version mismatch error**: package.json version must exactly match the release tag (without `v` prefix)
224
+ - **Publish fails**: Check `NPM_TOKEN` secret is set and has write access to `@agentcash` scope
225
+ - **Tests fail**: Fix in a new PR, then re-create the release
226
+
98
227
  ## Development Record
99
228
 
100
229
  The `.claude/` directory contains design docs, decision records, and bug analyses that document the reasoning behind the router's architecture. See `.claude/INDEX.md` for a table of contents.
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/client/index.ts
31
+ var client_exports = {};
32
+ __export(client_exports, {
33
+ SIWX_ERROR_MESSAGES: () => SIWX_ERROR_MESSAGES,
34
+ fetchWithSiwx: () => fetchWithSiwx
35
+ });
36
+ module.exports = __toCommonJS(client_exports);
37
+
38
+ // src/auth/siwx.ts
39
+ var SIWX_ERROR_MESSAGES = {
40
+ siwx_missing_header: "Missing SIGN-IN-WITH-X header",
41
+ siwx_malformed: "Malformed SIWX payload",
42
+ siwx_expired: "SIWX message expired \u2014 request a new challenge",
43
+ siwx_nonce_used: "Nonce already used \u2014 request a new challenge",
44
+ siwx_invalid_signature: "Invalid signature \u2014 wallet mismatch or corrupted proof"
45
+ };
46
+
47
+ // src/client/index.ts
48
+ async function fetchWithSiwx(url, options) {
49
+ const { signer, headers, ...init } = options;
50
+ const challengeRes = await fetch(url, {
51
+ ...init,
52
+ headers
53
+ });
54
+ if (challengeRes.status !== 402) {
55
+ return challengeRes;
56
+ }
57
+ let body;
58
+ try {
59
+ body = await challengeRes.json();
60
+ } catch {
61
+ throw new Error("Expected JSON body in 402 response");
62
+ }
63
+ const siwxExtension = body.extensions?.["sign-in-with-x"];
64
+ if (!siwxExtension) {
65
+ throw new Error(
66
+ "Expected SIWX challenge in 402 response. This endpoint may require payment instead of SIWX auth."
67
+ );
68
+ }
69
+ const { createSIWxPayload, encodeSIWxHeader } = await import("@x402/extensions/sign-in-with-x");
70
+ const chainInfo = siwxExtension.supportedChains?.find((c) => c.type === "eip191") ?? {
71
+ chainId: siwxExtension.info.chainId,
72
+ type: siwxExtension.info.type
73
+ };
74
+ const completeInfo = {
75
+ ...siwxExtension.info,
76
+ chainId: chainInfo.chainId,
77
+ type: chainInfo.type,
78
+ ...chainInfo.signatureScheme ? { signatureScheme: chainInfo.signatureScheme } : {}
79
+ };
80
+ const payload = await createSIWxPayload(completeInfo, signer);
81
+ const header = encodeSIWxHeader(payload);
82
+ return fetch(url, {
83
+ ...init,
84
+ headers: {
85
+ ...headers instanceof Headers ? Object.fromEntries(headers.entries()) : headers,
86
+ "SIGN-IN-WITH-X": header
87
+ }
88
+ });
89
+ }
90
+ // Annotate the CommonJS export names for ESM import in node:
91
+ 0 && (module.exports = {
92
+ SIWX_ERROR_MESSAGES,
93
+ fetchWithSiwx
94
+ });
@@ -0,0 +1,86 @@
1
+ export { S as SIWX_ERROR_MESSAGES, a as SiwxErrorCode } from '../siwx-BMlja_nt.cjs';
2
+
3
+ /**
4
+ * @agentcash/router/client
5
+ *
6
+ * Client-side utilities for SIWX (Sign-In With X) authentication.
7
+ * Use these to authenticate with SIWX-protected endpoints.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * import { fetchWithSiwx } from '@agentcash/router/client';
12
+ *
13
+ * const response = await fetchWithSiwx('https://api.example.com/protected', {
14
+ * method: 'GET',
15
+ * signer: walletClient, // viem WalletClient or PrivateKeyAccount
16
+ * });
17
+ * ```
18
+ */
19
+
20
+ /**
21
+ * SIWX challenge structure from 402 response.
22
+ * This is what the server returns in `extensions['sign-in-with-x'].info`.
23
+ */
24
+ interface SiwxChallenge {
25
+ domain: string;
26
+ uri: string;
27
+ version: string;
28
+ chainId: string;
29
+ type: 'eip191' | 'ed25519';
30
+ nonce: string;
31
+ issuedAt: string;
32
+ expirationTime?: string;
33
+ statement?: string;
34
+ }
35
+ /**
36
+ * Fetch options with SIWX signer.
37
+ * The signer must be compatible with @x402/extensions EVMSigner interface.
38
+ */
39
+ interface FetchWithSiwxOptions extends Omit<RequestInit, 'headers'> {
40
+ /**
41
+ * Wallet signer compatible with viem's WalletClient or PrivateKeyAccount.
42
+ * Must have a `signMessage` method that accepts `{ message: string }`.
43
+ */
44
+ signer: {
45
+ signMessage: (args: {
46
+ message: string;
47
+ account?: unknown;
48
+ }) => Promise<string>;
49
+ account?: {
50
+ address: string;
51
+ };
52
+ address?: string;
53
+ };
54
+ /**
55
+ * Additional headers to include in the request.
56
+ */
57
+ headers?: HeadersInit;
58
+ }
59
+ /**
60
+ * Fetch a SIWX-protected endpoint with automatic challenge-response handling.
61
+ *
62
+ * 1. Makes initial request
63
+ * 2. If 402 with SIWX challenge, extracts challenge from response
64
+ * 3. Signs the challenge with the provided signer
65
+ * 4. Retries request with SIGN-IN-WITH-X header
66
+ *
67
+ * @example
68
+ * ```ts
69
+ * import { fetchWithSiwx } from '@agentcash/router/client';
70
+ * import { createWalletClient, custom } from 'viem';
71
+ *
72
+ * const walletClient = createWalletClient({
73
+ * transport: custom(window.ethereum),
74
+ * });
75
+ *
76
+ * const response = await fetchWithSiwx('https://api.example.com/jobs', {
77
+ * method: 'GET',
78
+ * signer: walletClient,
79
+ * });
80
+ *
81
+ * const jobs = await response.json();
82
+ * ```
83
+ */
84
+ declare function fetchWithSiwx(url: string, options: FetchWithSiwxOptions): Promise<Response>;
85
+
86
+ export { type FetchWithSiwxOptions, type SiwxChallenge, fetchWithSiwx };
@@ -0,0 +1,86 @@
1
+ export { S as SIWX_ERROR_MESSAGES, a as SiwxErrorCode } from '../siwx-BMlja_nt.js';
2
+
3
+ /**
4
+ * @agentcash/router/client
5
+ *
6
+ * Client-side utilities for SIWX (Sign-In With X) authentication.
7
+ * Use these to authenticate with SIWX-protected endpoints.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * import { fetchWithSiwx } from '@agentcash/router/client';
12
+ *
13
+ * const response = await fetchWithSiwx('https://api.example.com/protected', {
14
+ * method: 'GET',
15
+ * signer: walletClient, // viem WalletClient or PrivateKeyAccount
16
+ * });
17
+ * ```
18
+ */
19
+
20
+ /**
21
+ * SIWX challenge structure from 402 response.
22
+ * This is what the server returns in `extensions['sign-in-with-x'].info`.
23
+ */
24
+ interface SiwxChallenge {
25
+ domain: string;
26
+ uri: string;
27
+ version: string;
28
+ chainId: string;
29
+ type: 'eip191' | 'ed25519';
30
+ nonce: string;
31
+ issuedAt: string;
32
+ expirationTime?: string;
33
+ statement?: string;
34
+ }
35
+ /**
36
+ * Fetch options with SIWX signer.
37
+ * The signer must be compatible with @x402/extensions EVMSigner interface.
38
+ */
39
+ interface FetchWithSiwxOptions extends Omit<RequestInit, 'headers'> {
40
+ /**
41
+ * Wallet signer compatible with viem's WalletClient or PrivateKeyAccount.
42
+ * Must have a `signMessage` method that accepts `{ message: string }`.
43
+ */
44
+ signer: {
45
+ signMessage: (args: {
46
+ message: string;
47
+ account?: unknown;
48
+ }) => Promise<string>;
49
+ account?: {
50
+ address: string;
51
+ };
52
+ address?: string;
53
+ };
54
+ /**
55
+ * Additional headers to include in the request.
56
+ */
57
+ headers?: HeadersInit;
58
+ }
59
+ /**
60
+ * Fetch a SIWX-protected endpoint with automatic challenge-response handling.
61
+ *
62
+ * 1. Makes initial request
63
+ * 2. If 402 with SIWX challenge, extracts challenge from response
64
+ * 3. Signs the challenge with the provided signer
65
+ * 4. Retries request with SIGN-IN-WITH-X header
66
+ *
67
+ * @example
68
+ * ```ts
69
+ * import { fetchWithSiwx } from '@agentcash/router/client';
70
+ * import { createWalletClient, custom } from 'viem';
71
+ *
72
+ * const walletClient = createWalletClient({
73
+ * transport: custom(window.ethereum),
74
+ * });
75
+ *
76
+ * const response = await fetchWithSiwx('https://api.example.com/jobs', {
77
+ * method: 'GET',
78
+ * signer: walletClient,
79
+ * });
80
+ *
81
+ * const jobs = await response.json();
82
+ * ```
83
+ */
84
+ declare function fetchWithSiwx(url: string, options: FetchWithSiwxOptions): Promise<Response>;
85
+
86
+ export { type FetchWithSiwxOptions, type SiwxChallenge, fetchWithSiwx };
@@ -0,0 +1,56 @@
1
+ // src/auth/siwx.ts
2
+ var SIWX_ERROR_MESSAGES = {
3
+ siwx_missing_header: "Missing SIGN-IN-WITH-X header",
4
+ siwx_malformed: "Malformed SIWX payload",
5
+ siwx_expired: "SIWX message expired \u2014 request a new challenge",
6
+ siwx_nonce_used: "Nonce already used \u2014 request a new challenge",
7
+ siwx_invalid_signature: "Invalid signature \u2014 wallet mismatch or corrupted proof"
8
+ };
9
+
10
+ // src/client/index.ts
11
+ async function fetchWithSiwx(url, options) {
12
+ const { signer, headers, ...init } = options;
13
+ const challengeRes = await fetch(url, {
14
+ ...init,
15
+ headers
16
+ });
17
+ if (challengeRes.status !== 402) {
18
+ return challengeRes;
19
+ }
20
+ let body;
21
+ try {
22
+ body = await challengeRes.json();
23
+ } catch {
24
+ throw new Error("Expected JSON body in 402 response");
25
+ }
26
+ const siwxExtension = body.extensions?.["sign-in-with-x"];
27
+ if (!siwxExtension) {
28
+ throw new Error(
29
+ "Expected SIWX challenge in 402 response. This endpoint may require payment instead of SIWX auth."
30
+ );
31
+ }
32
+ const { createSIWxPayload, encodeSIWxHeader } = await import("@x402/extensions/sign-in-with-x");
33
+ const chainInfo = siwxExtension.supportedChains?.find((c) => c.type === "eip191") ?? {
34
+ chainId: siwxExtension.info.chainId,
35
+ type: siwxExtension.info.type
36
+ };
37
+ const completeInfo = {
38
+ ...siwxExtension.info,
39
+ chainId: chainInfo.chainId,
40
+ type: chainInfo.type,
41
+ ...chainInfo.signatureScheme ? { signatureScheme: chainInfo.signatureScheme } : {}
42
+ };
43
+ const payload = await createSIWxPayload(completeInfo, signer);
44
+ const header = encodeSIWxHeader(payload);
45
+ return fetch(url, {
46
+ ...init,
47
+ headers: {
48
+ ...headers instanceof Headers ? Object.fromEntries(headers.entries()) : headers,
49
+ "SIGN-IN-WITH-X": header
50
+ }
51
+ });
52
+ }
53
+ export {
54
+ SIWX_ERROR_MESSAGES,
55
+ fetchWithSiwx
56
+ };