@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 +129 -0
- package/dist/client/index.cjs +94 -0
- package/dist/client/index.d.cts +86 -0
- package/dist/client/index.d.ts +86 -0
- package/dist/client/index.js +56 -0
- package/dist/index.cjs +245 -38
- package/dist/index.d.cts +75 -1
- package/dist/index.d.ts +75 -1
- package/dist/index.js +242 -38
- package/dist/siwx-BMlja_nt.d.cts +9 -0
- package/dist/siwx-BMlja_nt.d.ts +9 -0
- package/package.json +1 -1
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
|
+
};
|