@tangle-network/agent-gateway 0.6.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 +31 -6
- package/dist/chunk-Q4YAIEZY.js +1763 -0
- package/dist/chunk-Q4YAIEZY.js.map +1 -0
- package/dist/index.d.ts +13 -9
- package/dist/index.js +112 -1
- package/dist/index.js.map +1 -1
- package/dist/middleware.d.ts +2 -2
- package/dist/middleware.js +1 -1
- package/dist/types-DEsMmS-X.d.ts +875 -0
- package/dist/types.d.ts +1 -1
- package/package.json +14 -10
- package/src/a2a/agent-card.ts +55 -0
- package/src/a2a/handler.ts +797 -0
- package/src/a2a/jsonrpc.ts +65 -0
- package/src/a2a/push-notifications.ts +299 -0
- package/src/a2a/task-store-sql.ts +189 -0
- package/src/a2a/task-store.ts +53 -0
- package/src/a2a/translate.ts +77 -0
- package/src/a2a/types.ts +217 -0
- package/src/dispatch.ts +486 -0
- package/src/index.ts +58 -1
- package/src/middleware.ts +139 -294
- package/src/types.ts +76 -2
- package/src/verify.ts +93 -26
- package/dist/chunk-373QHRKV.js +0 -635
- package/dist/chunk-373QHRKV.js.map +0 -1
- package/dist/types-C_L7yXXI.d.ts +0 -362
package/src/types.ts
CHANGED
|
@@ -55,6 +55,18 @@ export interface AgentMeta {
|
|
|
55
55
|
* Only meaningful when `harness` is set; ignored otherwise.
|
|
56
56
|
*/
|
|
57
57
|
harnessModel?: string
|
|
58
|
+
/**
|
|
59
|
+
* Optional human description surfaced in the A2A Agent Card. Defaults to
|
|
60
|
+
* `"{slug} agent"` when absent.
|
|
61
|
+
*/
|
|
62
|
+
description?: string
|
|
63
|
+
/**
|
|
64
|
+
* Optional A2A skill descriptors. Each entry advertises what the agent
|
|
65
|
+
* can do so non-Tangle A2A clients can select agents by capability. When
|
|
66
|
+
* absent, the gateway synthesizes a single default `chat` skill from
|
|
67
|
+
* `slug` + `description`.
|
|
68
|
+
*/
|
|
69
|
+
skills?: import('./a2a/types').AgentSkill[]
|
|
58
70
|
}
|
|
59
71
|
|
|
60
72
|
// --- Payment ---
|
|
@@ -81,6 +93,18 @@ export interface MppConfig {
|
|
|
81
93
|
realm: string
|
|
82
94
|
/** MPP method name (default: "blueprintevm") */
|
|
83
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>
|
|
84
108
|
}
|
|
85
109
|
|
|
86
110
|
export interface PaymentResult {
|
|
@@ -141,6 +165,15 @@ export interface SandboxStreamEvent {
|
|
|
141
165
|
part?: { type?: string; text?: string }
|
|
142
166
|
delta?: string
|
|
143
167
|
finalText?: string
|
|
168
|
+
/**
|
|
169
|
+
* Optional sandbox-side signal that the agent has paused and is waiting
|
|
170
|
+
* for additional input from the caller. The A2A gateway translates this
|
|
171
|
+
* into an `input-required` task status; the caller can then submit a
|
|
172
|
+
* follow-up `message/send` with the same `taskId` to continue. Ignored
|
|
173
|
+
* by the OpenAI-compat path. Carry an optional `prompt` to surface to
|
|
174
|
+
* the caller (rendered as the input-required message body).
|
|
175
|
+
*/
|
|
176
|
+
inputRequired?: { prompt?: string }
|
|
144
177
|
}
|
|
145
178
|
}
|
|
146
179
|
|
|
@@ -173,12 +206,13 @@ export interface GatewayConfig {
|
|
|
173
206
|
/** x402 payment configuration */
|
|
174
207
|
x402: X402Config
|
|
175
208
|
|
|
176
|
-
/** 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. */
|
|
177
210
|
mpp?: MppConfig
|
|
178
211
|
|
|
179
212
|
/**
|
|
180
213
|
* Verify an API key. Return key info if valid, null if invalid.
|
|
181
|
-
*
|
|
214
|
+
* In explicit x402 demo mode, the built-in verifier accepts `sk_agent_*` keys.
|
|
215
|
+
* Production gateways must provide this callback.
|
|
182
216
|
*/
|
|
183
217
|
verifyApiKey?: (authHeader: string) => Promise<ApiKeyInfo | null>
|
|
184
218
|
|
|
@@ -218,6 +252,46 @@ export interface GatewayConfig {
|
|
|
218
252
|
* ConsoleObserver / CompositeObserver implementations.
|
|
219
253
|
*/
|
|
220
254
|
observer?: import('./observer').GatewayObserver
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* A2A protocol configuration. When set, the gateway exposes the A2A
|
|
258
|
+
* surface alongside its OpenAI-compatible endpoints:
|
|
259
|
+
* GET /:slug/.well-known/agent.json — AgentCard discovery
|
|
260
|
+
* POST /:slug — JSON-RPC 2.0 endpoint
|
|
261
|
+
* methods: message/send, message/stream, tasks/get, tasks/cancel
|
|
262
|
+
* Auth + rate-limit + injection-filter + authorization all share the
|
|
263
|
+
* same pipeline as the OpenAI-compat path. `taskStore` defaults to
|
|
264
|
+
* `InMemoryTaskStore`; swap in D1/postgres/DO for durable deployments.
|
|
265
|
+
*/
|
|
266
|
+
a2a?: {
|
|
267
|
+
/**
|
|
268
|
+
* Where tasks live. Defaults to `InMemoryTaskStore`; swap in
|
|
269
|
+
* `SqlTaskStore` (D1, postgres, sqlite, libSQL) for durability across
|
|
270
|
+
* gateway restarts.
|
|
271
|
+
*/
|
|
272
|
+
taskStore?: import('./a2a/task-store').TaskStore
|
|
273
|
+
/**
|
|
274
|
+
* Where push notification configs live. When set, the gateway advertises
|
|
275
|
+
* `capabilities.pushNotifications: true` and exposes the four
|
|
276
|
+
* `tasks/pushNotificationConfig/*` JSON-RPC methods. Defaults to
|
|
277
|
+
* undefined (push support disabled), so the agent card honestly reflects
|
|
278
|
+
* what the gateway will actually do.
|
|
279
|
+
*/
|
|
280
|
+
pushStore?: import('./a2a/push-notifications').PushNotificationStore
|
|
281
|
+
/**
|
|
282
|
+
* Shared HMAC secret used to sign webhook deliveries (`X-A2A-Signature:
|
|
283
|
+
* sha256=<hex>`). The consumer's webhook verifies the body against this
|
|
284
|
+
* secret to confirm the call originated from this gateway. Required when
|
|
285
|
+
* `pushStore` is set; without it, deliveries fire unsigned and a
|
|
286
|
+
* malicious party that knows the webhook URL can forge deliveries.
|
|
287
|
+
*/
|
|
288
|
+
webhookSecret?: string
|
|
289
|
+
/**
|
|
290
|
+
* Optional fetcher override for webhook delivery. Defaults to global
|
|
291
|
+
* `fetch`. Override for tests or to wire a queue-backed sender.
|
|
292
|
+
*/
|
|
293
|
+
pushFetcher?: typeof fetch
|
|
294
|
+
}
|
|
221
295
|
}
|
|
222
296
|
|
|
223
297
|
// --- Chat completion types (OpenAI-compatible) ---
|
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
|
}
|