@tangle-network/agent-gateway 0.7.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -6
- package/dist/{chunk-3IKQWFKX.js → chunk-Q4YAIEZY.js} +92 -32
- package/dist/chunk-Q4YAIEZY.js.map +1 -0
- package/dist/index.d.ts +13 -9
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/middleware.d.ts +1 -1
- package/dist/middleware.js +1 -1
- package/dist/{types-CX2V06cN.d.ts → types-DEsMmS-X.d.ts} +15 -2
- package/dist/types.d.ts +1 -1
- package/package.json +1 -1
- package/src/a2a/agent-card.ts +4 -3
- package/src/a2a/handler.ts +1 -2
- package/src/dispatch.ts +37 -16
- package/src/index.ts +7 -1
- package/src/middleware.ts +6 -6
- package/src/types.ts +15 -2
- package/src/verify.ts +93 -26
- package/dist/chunk-3IKQWFKX.js.map +0 -1
package/src/dispatch.ts
CHANGED
|
@@ -21,7 +21,13 @@ import type {
|
|
|
21
21
|
GatewayConfig,
|
|
22
22
|
PaymentMethod,
|
|
23
23
|
} from './types'
|
|
24
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
defaultVerifyApiKey,
|
|
26
|
+
isApiKeyAuthEnabled,
|
|
27
|
+
isMppAuthEnabled,
|
|
28
|
+
verifyMpp,
|
|
29
|
+
verifyX402,
|
|
30
|
+
} from './verify'
|
|
25
31
|
|
|
26
32
|
/** Single bundle of long-lived gateway state shared across all handlers in one createAgentGateway call. */
|
|
27
33
|
export interface GatewayState {
|
|
@@ -68,7 +74,7 @@ export async function authenticateAndGuard(
|
|
|
68
74
|
await state.obs?.onRequestStart?.(ctx)
|
|
69
75
|
|
|
70
76
|
const agent = await config.resolveAgent(slug)
|
|
71
|
-
if (!agent) {
|
|
77
|
+
if (!agent || !agent.enabled) {
|
|
72
78
|
return c.json({ error: { message: 'Agent not found', type: 'not_found' } }, 404)
|
|
73
79
|
}
|
|
74
80
|
if (!messages?.length) {
|
|
@@ -109,11 +115,11 @@ export async function authenticateAndGuard(
|
|
|
109
115
|
}
|
|
110
116
|
consumerId = signer
|
|
111
117
|
paymentMethod = 'x402'
|
|
112
|
-
} else if (config
|
|
113
|
-
const signer = await verifyMpp(authHeader, config.mpp
|
|
118
|
+
} else if (isMppAuthEnabled(config) && authHeader.toLowerCase().startsWith('payment ')) {
|
|
119
|
+
const signer = await verifyMpp(authHeader, config.mpp!, config.x402, state.nonceStore)
|
|
114
120
|
if (!signer) {
|
|
115
|
-
const realm = config.mpp
|
|
116
|
-
const method = config.mpp
|
|
121
|
+
const realm = config.mpp!.realm
|
|
122
|
+
const method = config.mpp!.method ?? 'blueprintevm'
|
|
117
123
|
await state.obs?.onAuthFailure?.(ctx, {
|
|
118
124
|
method: 'mpp',
|
|
119
125
|
code: 'invalid_mpp_credential',
|
|
@@ -139,7 +145,18 @@ export async function authenticateAndGuard(
|
|
|
139
145
|
consumerId = signer
|
|
140
146
|
paymentMethod = 'mpp'
|
|
141
147
|
} else if (authHeader.startsWith('Bearer ')) {
|
|
142
|
-
const verify = config.verifyApiKey ?? defaultVerifyApiKey
|
|
148
|
+
const verify = config.verifyApiKey ?? (config.x402.demoMode ? defaultVerifyApiKey : null)
|
|
149
|
+
if (!verify || !isApiKeyAuthEnabled(config)) {
|
|
150
|
+
await state.obs?.onAuthFailure?.(ctx, {
|
|
151
|
+
method: 'apikey',
|
|
152
|
+
code: 'api_keys_not_configured',
|
|
153
|
+
httpStatus: 401,
|
|
154
|
+
})
|
|
155
|
+
return c.json(
|
|
156
|
+
{ error: { message: 'API key authentication is not configured', type: 'authentication_error' } },
|
|
157
|
+
{ status: 401, headers: { 'X-Request-Id': requestId } },
|
|
158
|
+
)
|
|
159
|
+
}
|
|
143
160
|
const key = await verify(authHeader)
|
|
144
161
|
if (!key) {
|
|
145
162
|
await state.obs?.onAuthFailure?.(ctx, {
|
|
@@ -179,13 +196,13 @@ export async function authenticateAndGuard(
|
|
|
179
196
|
httpStatus: 402,
|
|
180
197
|
})
|
|
181
198
|
const methods: string[] = ['x402']
|
|
182
|
-
if (config
|
|
183
|
-
methods.push('api_key')
|
|
199
|
+
if (isMppAuthEnabled(config)) methods.push('mpp')
|
|
200
|
+
if (isApiKeyAuthEnabled(config)) methods.push('api_key')
|
|
184
201
|
const headers: Record<string, string> = {
|
|
185
202
|
'X-Payment-Required': methods.join(', '),
|
|
186
203
|
'X-Request-Id': requestId,
|
|
187
204
|
}
|
|
188
|
-
if (config.mpp) {
|
|
205
|
+
if (isMppAuthEnabled(config) && config.mpp) {
|
|
189
206
|
headers['WWW-Authenticate'] =
|
|
190
207
|
`Payment realm="${config.mpp.realm}", method="${config.mpp.method ?? 'blueprintevm'}"`
|
|
191
208
|
}
|
|
@@ -201,14 +218,18 @@ export async function authenticateAndGuard(
|
|
|
201
218
|
credits_address: config.x402.creditsAddress,
|
|
202
219
|
estimated_amount_per_request: '20000',
|
|
203
220
|
},
|
|
204
|
-
...(config.mpp
|
|
221
|
+
...(isMppAuthEnabled(config) && config.mpp
|
|
205
222
|
? { mpp: { realm: config.mpp.realm, method: config.mpp.method ?? 'blueprintevm' } }
|
|
206
223
|
: {}),
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
224
|
+
...(isApiKeyAuthEnabled(config)
|
|
225
|
+
? {
|
|
226
|
+
api_key: {
|
|
227
|
+
purchase_url: config.baseUrl
|
|
228
|
+
? `${config.baseUrl}/agents/${slug}/api-keys`
|
|
229
|
+
: undefined,
|
|
230
|
+
},
|
|
231
|
+
}
|
|
232
|
+
: {}),
|
|
212
233
|
},
|
|
213
234
|
},
|
|
214
235
|
{ status: 402, headers },
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
export { createAgentGateway } from './middleware'
|
|
2
|
-
export {
|
|
2
|
+
export {
|
|
3
|
+
verifyX402,
|
|
4
|
+
verifyMpp,
|
|
5
|
+
defaultVerifyApiKey,
|
|
6
|
+
isApiKeyAuthEnabled,
|
|
7
|
+
isMppAuthEnabled,
|
|
8
|
+
} from './verify'
|
|
3
9
|
export {
|
|
4
10
|
filterConsumerMessages,
|
|
5
11
|
filterConsumerMessagesStrict,
|
package/src/middleware.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { MemoryNonceStore } from './nonce-store'
|
|
|
14
14
|
import { type GatewayObserver, type RequestContext, generateRequestId } from './observer'
|
|
15
15
|
import { MemoryRateLimitStore, type RateLimitStore } from './rate-limit'
|
|
16
16
|
import type { ChatCompletionChunk, ChatCompletionRequest, GatewayConfig } from './types'
|
|
17
|
+
import { isApiKeyAuthEnabled, isMppAuthEnabled } from './verify'
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Create a Hono router that serves the agent gateway.
|
|
@@ -51,7 +52,7 @@ export function createAgentGateway(config: GatewayConfig) {
|
|
|
51
52
|
gw.get('/:slug/chat/completions', async (c) => {
|
|
52
53
|
const slug = c.req.param('slug')
|
|
53
54
|
const agent = await config.resolveAgent(slug)
|
|
54
|
-
if (!agent) return c.json({ error: 'Agent not found or not published' }, 404)
|
|
55
|
+
if (!agent || !agent.enabled) return c.json({ error: 'Agent not found or not published' }, 404)
|
|
55
56
|
|
|
56
57
|
const paymentMethods: Array<Record<string, unknown>> = [
|
|
57
58
|
{
|
|
@@ -61,14 +62,14 @@ export function createAgentGateway(config: GatewayConfig) {
|
|
|
61
62
|
credits_contract: config.x402.creditsAddress,
|
|
62
63
|
},
|
|
63
64
|
]
|
|
64
|
-
if (config
|
|
65
|
+
if (isMppAuthEnabled(config)) {
|
|
65
66
|
paymentMethods.push({
|
|
66
67
|
type: 'mpp',
|
|
67
|
-
realm: config.mpp
|
|
68
|
-
method: config.mpp
|
|
68
|
+
realm: config.mpp!.realm,
|
|
69
|
+
method: config.mpp!.method ?? 'blueprintevm',
|
|
69
70
|
})
|
|
70
71
|
}
|
|
71
|
-
paymentMethods.push({ type: 'api_key', prefix: 'sk_agent_' })
|
|
72
|
+
if (isApiKeyAuthEnabled(config)) paymentMethods.push({ type: 'api_key', prefix: 'sk_agent_' })
|
|
72
73
|
|
|
73
74
|
return c.json({
|
|
74
75
|
slug: agent.slug,
|
|
@@ -231,4 +232,3 @@ function streamChatCompletions(
|
|
|
231
232
|
},
|
|
232
233
|
})
|
|
233
234
|
}
|
|
234
|
-
|
package/src/types.ts
CHANGED
|
@@ -93,6 +93,18 @@ export interface MppConfig {
|
|
|
93
93
|
realm: string
|
|
94
94
|
/** MPP method name (default: "blueprintevm") */
|
|
95
95
|
method?: string
|
|
96
|
+
/**
|
|
97
|
+
* Production verifier for the method-specific credential. Return the
|
|
98
|
+
* authenticated consumer id, or null when the credential is invalid.
|
|
99
|
+
* The callback receives the decoded JSON payload when one exists plus the
|
|
100
|
+
* original decoded credential so non-JSON methods can verify their own form.
|
|
101
|
+
* Omit only when x402.demoMode is explicitly enabled for local testing, or
|
|
102
|
+
* when x402.verifySigner handles an x402-compatible MPP credential.
|
|
103
|
+
*/
|
|
104
|
+
verifySigner?: (
|
|
105
|
+
payload: Record<string, unknown>,
|
|
106
|
+
context: { method: string; credential: string },
|
|
107
|
+
) => Promise<string | null>
|
|
96
108
|
}
|
|
97
109
|
|
|
98
110
|
export interface PaymentResult {
|
|
@@ -194,12 +206,13 @@ export interface GatewayConfig {
|
|
|
194
206
|
/** x402 payment configuration */
|
|
195
207
|
x402: X402Config
|
|
196
208
|
|
|
197
|
-
/** MPP (Machine Payments Protocol) configuration.
|
|
209
|
+
/** MPP (Machine Payments Protocol) configuration. It is advertised only when a production verifier or explicit demo mode is available. */
|
|
198
210
|
mpp?: MppConfig
|
|
199
211
|
|
|
200
212
|
/**
|
|
201
213
|
* Verify an API key. Return key info if valid, null if invalid.
|
|
202
|
-
*
|
|
214
|
+
* In explicit x402 demo mode, the built-in verifier accepts `sk_agent_*` keys.
|
|
215
|
+
* Production gateways must provide this callback.
|
|
203
216
|
*/
|
|
204
217
|
verifyApiKey?: (authHeader: string) => Promise<ApiKeyInfo | null>
|
|
205
218
|
|
package/src/verify.ts
CHANGED
|
@@ -1,6 +1,26 @@
|
|
|
1
|
-
import type { X402Config, MppConfig, ApiKeyInfo } from './types'
|
|
1
|
+
import type { X402Config, MppConfig, ApiKeyInfo, GatewayConfig } from './types'
|
|
2
2
|
import type { NonceStore } from './nonce-store'
|
|
3
3
|
|
|
4
|
+
/** Pure capability checks shared by discovery and every request protocol. */
|
|
5
|
+
export function isApiKeyAuthEnabled(
|
|
6
|
+
config: Pick<GatewayConfig, 'verifyApiKey' | 'x402'>,
|
|
7
|
+
): boolean {
|
|
8
|
+
return config.verifyApiKey !== undefined || config.x402.demoMode === true
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** MPP is enabled only when a real verifier or explicit demo mode exists. */
|
|
12
|
+
export function isMppAuthEnabled(
|
|
13
|
+
config: Pick<GatewayConfig, 'mpp' | 'x402'>,
|
|
14
|
+
): boolean {
|
|
15
|
+
const method = config.mpp?.method ?? 'blueprintevm'
|
|
16
|
+
return Boolean(
|
|
17
|
+
config.mpp &&
|
|
18
|
+
(config.mpp.verifySigner !== undefined ||
|
|
19
|
+
(method === 'blueprintevm' && config.x402.verifySigner !== undefined) ||
|
|
20
|
+
config.x402.demoMode === true),
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
4
24
|
/**
|
|
5
25
|
* Verify x402 SpendAuth signature (EIP-712).
|
|
6
26
|
* Returns the signer address (commitment) if valid, null otherwise.
|
|
@@ -32,14 +52,8 @@ export async function verifyX402(
|
|
|
32
52
|
// Reject zero-amount payments
|
|
33
53
|
if (amount <= 0n) return null
|
|
34
54
|
|
|
35
|
-
// Reject replayed nonces
|
|
36
55
|
const nonceKey = `${raw.commitment}:${nonce.toString()}`
|
|
37
|
-
if (nonceStore)
|
|
38
|
-
if (await nonceStore.hasSeen(nonceKey)) return null
|
|
39
|
-
// Mark seen with TTL matching the expiry window (max 1 hour)
|
|
40
|
-
const ttl = Math.min(Number(expiry) - Math.floor(Date.now() / 1000), 3600)
|
|
41
|
-
await nonceStore.markSeen(nonceKey, Math.max(ttl, 60))
|
|
42
|
-
}
|
|
56
|
+
if (nonceStore && await nonceStore.hasSeen(nonceKey)) return null
|
|
43
57
|
|
|
44
58
|
if (config.verifySigner) {
|
|
45
59
|
const verified = await config.verifySigner(raw)
|
|
@@ -48,6 +62,14 @@ export async function verifyX402(
|
|
|
48
62
|
return null
|
|
49
63
|
}
|
|
50
64
|
|
|
65
|
+
// Check and mark only after the signature is accepted. Otherwise an
|
|
66
|
+
// invalid request can burn a valid payer nonce and deny the real request.
|
|
67
|
+
if (nonceStore) {
|
|
68
|
+
// Mark seen with TTL matching the expiry window (max 1 hour)
|
|
69
|
+
const ttl = Math.min(Number(expiry) - Math.floor(Date.now() / 1000), 3600)
|
|
70
|
+
await nonceStore.markSeen(nonceKey, Math.max(ttl, 60))
|
|
71
|
+
}
|
|
72
|
+
|
|
51
73
|
return raw.commitment
|
|
52
74
|
} catch {
|
|
53
75
|
return null
|
|
@@ -57,43 +79,88 @@ export async function verifyX402(
|
|
|
57
79
|
/**
|
|
58
80
|
* Verify MPP (Machine Payments Protocol) Authorization: Payment header.
|
|
59
81
|
*
|
|
60
|
-
* MPP uses `Authorization: Payment <method> <credential>` format
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
82
|
+
* MPP uses `Authorization: Payment <method> <credential>` format. The
|
|
83
|
+
* credential is method-specific; `MppConfig.verifySigner` owns verification
|
|
84
|
+
* and returns the consumer identity. The built-in `blueprintevm` path can
|
|
85
|
+
* reuse the x402 verifier for credentials with the compatible payload shape.
|
|
64
86
|
*
|
|
65
87
|
* Returns the signer address if valid, null otherwise.
|
|
66
|
-
* In demo mode, accepts any well-formed Payment header.
|
|
88
|
+
* In demo mode, accepts any well-formed Payment header with an identity.
|
|
67
89
|
*/
|
|
68
90
|
export async function verifyMpp(
|
|
69
91
|
authHeader: string,
|
|
70
|
-
|
|
92
|
+
config: MppConfig,
|
|
71
93
|
x402Config: X402Config,
|
|
94
|
+
nonceStore?: NonceStore,
|
|
72
95
|
): Promise<string | null> {
|
|
73
96
|
// MPP format: "Payment <method> <base64url-credential>"
|
|
74
97
|
const match = authHeader.match(/^Payment\s+(\S+)\s+(\S+)$/i)
|
|
75
98
|
if (!match) return null
|
|
76
99
|
|
|
77
|
-
const [, , credentialB64] = match
|
|
100
|
+
const [, method, credentialB64] = match
|
|
101
|
+
if (config.method && method !== config.method) return null
|
|
78
102
|
|
|
79
103
|
try {
|
|
80
|
-
|
|
104
|
+
if (!/^[A-Za-z0-9_-]+$/.test(credentialB64)) return null
|
|
81
105
|
const decoded = Buffer.from(credentialB64, 'base64url').toString('utf-8')
|
|
82
|
-
|
|
106
|
+
let payload: Record<string, unknown> = {}
|
|
107
|
+
try {
|
|
108
|
+
const credential = JSON.parse(decoded) as unknown
|
|
109
|
+
if (credential && typeof credential === 'object' && !Array.isArray(credential)) {
|
|
110
|
+
const nested = (credential as Record<string, unknown>).payload
|
|
111
|
+
payload =
|
|
112
|
+
nested && typeof nested === 'object' && !Array.isArray(nested)
|
|
113
|
+
? (nested as Record<string, unknown>)
|
|
114
|
+
: (credential as Record<string, unknown>)
|
|
115
|
+
}
|
|
116
|
+
} catch {
|
|
117
|
+
// Method-specific verifiers may accept a non-JSON credential format.
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const nonceKey =
|
|
121
|
+
nonceStore && payload.nonce !== undefined
|
|
122
|
+
? `mpp:${method}:${String(payload.commitment ?? payload.from ?? 'unknown')}:${String(payload.nonce)}`
|
|
123
|
+
: null
|
|
124
|
+
if (nonceKey && await nonceStore!.hasSeen(nonceKey)) return null
|
|
83
125
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
126
|
+
let consumerId: string | null = null
|
|
127
|
+
if (config.verifySigner) {
|
|
128
|
+
consumerId = await config.verifySigner(payload, { method, credential: decoded })
|
|
129
|
+
} else if (method === 'blueprintevm' && x402Config.verifySigner && payload.commitment) {
|
|
130
|
+
const verified = await x402Config.verifySigner(payload)
|
|
131
|
+
consumerId = verified ? String(payload.commitment) : null
|
|
132
|
+
} else if (x402Config.demoMode) {
|
|
133
|
+
const identity = payload.commitment ?? payload.from
|
|
134
|
+
if (typeof identity !== 'string' || identity.length === 0) return null
|
|
135
|
+
consumerId = identity
|
|
136
|
+
} else {
|
|
137
|
+
return null
|
|
138
|
+
}
|
|
139
|
+
if (!consumerId) return null
|
|
87
140
|
|
|
88
|
-
// Validate
|
|
141
|
+
// Validate common EVM fields when present. Method-specific verifiers own
|
|
142
|
+
// the complete credential contract for non-EVM methods.
|
|
89
143
|
const operator = payload.operator ?? payload.to
|
|
90
|
-
if (operator
|
|
144
|
+
if (operator !== undefined) {
|
|
145
|
+
if (typeof operator !== 'string' || operator.toLowerCase() !== x402Config.operatorAddress.toLowerCase()) {
|
|
146
|
+
return null
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (payload.amount !== undefined && BigInt(String(payload.amount)) <= 0n) return null
|
|
150
|
+
if (payload.nonce !== undefined) BigInt(String(payload.nonce))
|
|
151
|
+
if (payload.expiry !== undefined && BigInt(String(payload.expiry)) < BigInt(Math.floor(Date.now() / 1000))) {
|
|
152
|
+
return null
|
|
153
|
+
}
|
|
91
154
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
155
|
+
if (nonceStore && payload.nonce !== undefined) {
|
|
156
|
+
const expiry = payload.expiry === undefined
|
|
157
|
+
? Math.floor(Date.now() / 1000) + 3600
|
|
158
|
+
: Number(payload.expiry)
|
|
159
|
+
const ttl = Math.min(expiry - Math.floor(Date.now() / 1000), 3600)
|
|
160
|
+
await nonceStore.markSeen(nonceKey!, Math.max(ttl, 60))
|
|
161
|
+
}
|
|
95
162
|
|
|
96
|
-
return
|
|
163
|
+
return consumerId
|
|
97
164
|
} catch {
|
|
98
165
|
return null
|
|
99
166
|
}
|