@tangle-network/agent-gateway 0.7.0 → 0.8.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/README.md +108 -6
- package/dist/chunk-GITV7CPT.js +84 -0
- package/dist/chunk-GITV7CPT.js.map +1 -0
- package/dist/chunk-J5SDVHOL.js +104 -0
- package/dist/chunk-J5SDVHOL.js.map +1 -0
- package/dist/chunk-MP6IIAIA.js +5651 -0
- package/dist/chunk-MP6IIAIA.js.map +1 -0
- package/dist/index.d.ts +76 -12
- package/dist/index.js +307 -21
- package/dist/index.js.map +1 -1
- package/dist/middleware.d.ts +7 -2
- package/dist/middleware.js +3 -2
- package/dist/nonce-store.d.ts +47 -11
- package/dist/nonce-store.js +9 -3
- package/dist/observer-types-A0RtA8uL.d.ts +95 -0
- package/dist/observer.d.ts +79 -0
- package/dist/observer.js +11 -0
- package/dist/observer.js.map +1 -0
- package/dist/{types-CX2V06cN.d.ts → types-BHISsm7D.d.ts} +423 -166
- package/dist/types.d.ts +2 -1
- package/package.json +1 -1
- package/src/a2a/agent-card.ts +4 -3
- package/src/a2a/execution-fence.ts +162 -0
- package/src/a2a/handler.ts +507 -562
- package/src/a2a/message-send-execution.ts +241 -0
- package/src/a2a/message-stream-execution.ts +392 -0
- package/src/a2a/payment-recovery.ts +431 -0
- package/src/a2a/push-config-methods.ts +158 -0
- package/src/a2a/push-notifications.ts +172 -22
- package/src/a2a/task-cancellation.ts +50 -0
- package/src/a2a/task-finalization.ts +451 -0
- package/src/a2a/task-lifecycle.ts +54 -0
- package/src/a2a/task-methods.ts +163 -0
- package/src/a2a/task-push-delivery.ts +119 -0
- package/src/a2a/task-recovery.ts +11 -0
- package/src/a2a/task-state.ts +99 -0
- package/src/a2a/task-store-sql.ts +222 -24
- package/src/a2a/task-store.ts +58 -1
- package/src/a2a/task-submission-recovery.ts +178 -0
- package/src/a2a/types.ts +1 -0
- package/src/dispatch-authorization.ts +437 -0
- package/src/dispatch-payment-recovery.ts +248 -0
- package/src/dispatch-payment.ts +425 -0
- package/src/dispatch-pricing.ts +108 -0
- package/src/dispatch-sandbox.ts +422 -0
- package/src/dispatch-settlement.ts +139 -0
- package/src/dispatch-types.ts +81 -0
- package/src/dispatch.ts +35 -462
- package/src/index.ts +64 -2
- package/src/middleware.ts +313 -32
- package/src/mpp-payment.ts +117 -0
- package/src/nonce-store.ts +122 -20
- package/src/observer-types.ts +63 -0
- package/src/observer.ts +3 -63
- package/src/payment-operations.ts +485 -0
- package/src/payment-recovery-sql.ts +108 -0
- package/src/payment-recovery-worker.ts +488 -0
- package/src/payment-recovery.ts +331 -0
- package/src/payment-types.ts +48 -0
- package/src/types.ts +153 -42
- package/src/verify.ts +265 -36
- package/dist/chunk-3IKQWFKX.js +0 -1703
- package/dist/chunk-3IKQWFKX.js.map +0 -1
- package/dist/chunk-M7ZJAK4K.js +0 -53
- package/dist/chunk-M7ZJAK4K.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# @tangle-network/agent-gateway
|
|
2
2
|
|
|
3
|
-
Hono middleware that turns any Tangle agent app into a paid API.
|
|
3
|
+
Hono middleware that turns any Tangle agent app into a paid API.
|
|
4
|
+
It exposes one shared request pipeline for API keys, x402 SpendAuth, and MPP credentials, with scope enforcement, per-key rate limits, nonce replay protection, prompt-injection detection, and publish routes for the marketplace.
|
|
4
5
|
|
|
5
6
|
## Install
|
|
6
7
|
|
|
@@ -11,20 +12,121 @@ npm install @tangle-network/agent-gateway
|
|
|
11
12
|
## Usage
|
|
12
13
|
|
|
13
14
|
```ts
|
|
14
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
createAgentGateway,
|
|
17
|
+
recoverPayments,
|
|
18
|
+
SqlPaymentRecoveryStore,
|
|
19
|
+
verifyApiKeyFromStore,
|
|
20
|
+
} from '@tangle-network/agent-gateway'
|
|
15
21
|
import { Hono } from 'hono'
|
|
16
22
|
|
|
17
23
|
const app = new Hono()
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
24
|
+
const paymentRecoveryStore = new SqlPaymentRecoveryStore(sqlAdapter)
|
|
25
|
+
await paymentRecoveryStore.migrate()
|
|
26
|
+
app.route('/v1/agents', createAgentGateway({
|
|
27
|
+
resolveAgent: loadPublishedAgent,
|
|
28
|
+
getSandbox: openAgentSandbox,
|
|
29
|
+
recordUsage: recordUsageEvent,
|
|
30
|
+
x402: {
|
|
31
|
+
operatorAddress: '0x…',
|
|
32
|
+
chainId: 3799,
|
|
33
|
+
currencyDecimals: 6,
|
|
34
|
+
verifySigner: verifySpendAuthSignature,
|
|
35
|
+
paymentProtocolVersion: 2,
|
|
36
|
+
paymentOperations,
|
|
37
|
+
authorizePayment: reserveSpendAuthorization,
|
|
38
|
+
},
|
|
39
|
+
paymentRecovery: { store: paymentRecoveryStore },
|
|
40
|
+
defaultOutputTokens: 1024,
|
|
41
|
+
maxOutputTokens: 4096,
|
|
42
|
+
verifyApiKey: (authHeader) => verifyApiKeyFromStore(authHeader, apiKeyStore),
|
|
22
43
|
}))
|
|
23
44
|
```
|
|
24
45
|
|
|
46
|
+
`x402.verifySigner` is required for production.
|
|
47
|
+
Set `x402.demoMode: true` only for local development and tests; that explicit mode also enables the built-in `sk_agent_*` demo key verifier.
|
|
48
|
+
Keep `verifySigner` free of side effects.
|
|
49
|
+
Use version 2's `authorizePayment` to reserve or claim funds after rate limits, content checks, and product authorization succeed.
|
|
50
|
+
For production version 2, set `x402.paymentProtocolVersion: 2`, provide `paymentOperations`, and return its operation from `authorizePayment`.
|
|
51
|
+
Production version 2 also requires a durable `paymentRecovery.store`.
|
|
52
|
+
Production version 1 is read-only and must not configure `authorizePayment`.
|
|
53
|
+
Production x402 version 1 also rejects the legacy `settlePayment` callback before it consumes a nonce.
|
|
54
|
+
Use version 2 whenever authorization can reserve, charge, or otherwise mutate external funds.
|
|
55
|
+
Run `recoverPayments(config)` from a private scheduled worker.
|
|
56
|
+
Every live request and worker uses a unique durable fence token.
|
|
57
|
+
A stale request or worker cannot update a row after another worker takes its lease.
|
|
58
|
+
Provider settlement, recovery, and release methods must still use the operation ID idempotently.
|
|
59
|
+
`paymentOperations.getPaymentOperation` must read the authoritative provider state by operation ID without changing it.
|
|
60
|
+
This read is required for recovery of older A2A finalization records that predate the shared payment outbox.
|
|
61
|
+
The operation store owns claim, execution start, receipt retention, partial settle, release, and expiry reclaim.
|
|
62
|
+
An executing or retained operation cannot expire into a refund.
|
|
63
|
+
A retained operation settles from its receipt when one exists.
|
|
64
|
+
If the receipt does not arrive before `receiptTimeoutMs`, recovery settles the original quoted ceiling.
|
|
65
|
+
The fallback never settles the payer's larger authorization amount.
|
|
66
|
+
Keep version 1 explicitly configured while old and new gateways coexist; shared nonce storage must reject a version 1 claim owned by a version 2 operation.
|
|
67
|
+
Before it calls the verifier, the gateway requires the signed amount to cover the complete filtered conversation plus the requested output limit.
|
|
68
|
+
The default bound includes system text, message roles, and JSON framing.
|
|
69
|
+
Set `inputTokenBound` when the provider adds harness, tool, workspace, or other hidden context.
|
|
70
|
+
The gateway rejects `max_tokens` above `maxOutputTokens` and stops the sandbox stream at the accepted limit.
|
|
71
|
+
An unpaid request receives `required_amount`, `currency_decimals`, and `max_output_tokens` in the 402 response.
|
|
72
|
+
Sandbox adapters should emit a complete `sandbox.usage` receipt.
|
|
73
|
+
Requests with a version 2 operation or generic MPP charge reject missing receipts.
|
|
74
|
+
API-key requests keep the legacy visible-token estimate path.
|
|
75
|
+
recordUsage must atomically upsert by event.requestId; recovery may retry an event after its acknowledgement is lost.
|
|
76
|
+
The default gateway still exposes A2A with an in-memory task store.
|
|
77
|
+
Older custom A2A task stores remain source-compatible at the type boundary.
|
|
78
|
+
The OpenAI surface stays available when such a store is configured, while A2A returns `503` until its owner supplies atomic methods.
|
|
79
|
+
Use an atomic task store for multi-worker production deployments.
|
|
80
|
+
|
|
81
|
+
MPP is method-specific.
|
|
82
|
+
Configure `mpp.authenticateCredential` for production MPP credentials.
|
|
83
|
+
This callback receives the decoded payload and live credential.
|
|
84
|
+
It returns `{ consumerId, paymentIdentity }` or `null`.
|
|
85
|
+
`paymentIdentity` must be a stable, non-secret processor identity.
|
|
86
|
+
Equivalent encodings of one credential must return the same payment identity.
|
|
87
|
+
It must not reserve, confirm, or consume payment.
|
|
88
|
+
The default `blueprintevm` method may reuse `x402.verifySigner` when its credential has the compatible x402 payload shape.
|
|
89
|
+
Every other method requires an `mpp.charge` lifecycle.
|
|
90
|
+
The lifecycle confirms payment after all request denials.
|
|
91
|
+
The gateway then acquires its execution fence before it returns a response or starts sandbox work.
|
|
92
|
+
`confirmPayment` must bind the provider operation to the supplied `operationId` before confirmation.
|
|
93
|
+
It must return only after it verifies final payment success.
|
|
94
|
+
`recoverPayment` must inspect that operation ID and must never create another charge.
|
|
95
|
+
An authoritative `not-found` result must fence the operation ID against a later charge.
|
|
96
|
+
`releasePayment` must perform an idempotent refund or release.
|
|
97
|
+
The live credential is passed to `confirmPayment` only on the original request.
|
|
98
|
+
The nonce and recovery stores persist only the SHA-256 digest of `paymentIdentity`.
|
|
99
|
+
`Payment-Receipt` values must contain visible ASCII only.
|
|
100
|
+
|
|
101
|
+
`NonceStore` remains source-compatible with 0.7.1 `hasSeen`/`markSeen` stores.
|
|
102
|
+
Payment requests now require its atomic `claim` method, including version 1.
|
|
103
|
+
This is a deliberate safety boundary: a check followed by a write can accept two concurrent payments.
|
|
104
|
+
`KvNonceStore` with plain Cloudflare KV is not atomic and is rejected by `createAgentGateway`.
|
|
105
|
+
Provide `KvNonceStore` an `atomicClaim` callback backed by D1, a Durable Object, or another linearizable store.
|
|
106
|
+
Payment paths fail closed unless the store also provides one atomic `claim` method.
|
|
107
|
+
The 0.7.1 `mpp.verifySigner` callback is also supported; the gateway derives a stable identity until the integration moves to `authenticateCredential`.
|
|
108
|
+
|
|
109
|
+
The same authentication, authorization, rate-limit, filtering, sandbox, settlement, and usage-recording pipeline is used by the OpenAI-compatible and A2A endpoints.
|
|
110
|
+
Wire protocol handlers only translate their request and response shapes.
|
|
111
|
+
|
|
25
112
|
## A2A protocol
|
|
26
113
|
|
|
27
114
|
The gateway speaks Google's A2A protocol alongside its OpenAI-compatible surface: discovery via `.well-known/agent.json`, JSON-RPC 2.0 dispatch for `message/send`, `message/stream`, `tasks/get`, `tasks/cancel`, `tasks/resubscribe`, and the four `tasks/pushNotificationConfig/*` methods. Long-horizon agents — durable tasks across worker restarts, webhook delivery on terminal state, `input-required` pauses with multi-turn continuation — are documented in [`docs/a2a-long-horizon.md`](./docs/a2a-long-horizon.md).
|
|
115
|
+
Production A2A task control requires `a2a.authorizeTaskAccess`; explicit demo mode is the local-test exception.
|
|
116
|
+
Custom production task stores must implement atomic `createIfAbsent`, `compareAndSet`, and `compareAndSetExecution` methods.
|
|
117
|
+
`compareAndSetExecution` must reject a renewal when the stored owner lease has expired.
|
|
118
|
+
Task stores must retain payment recovery metadata until reconciliation clears it.
|
|
119
|
+
The short-lived `gatewaySubmission` marker is not a payment recovery record and may expire with its task.
|
|
120
|
+
The bundled memory and SQL stores enforce this rule even after the normal task TTL.
|
|
121
|
+
Push destinations must use HTTPS without URL credentials.
|
|
122
|
+
Push delivery does not follow redirects.
|
|
123
|
+
Production push delivery also requires `a2a.pushUrlValidator` to reject private DNS destinations.
|
|
124
|
+
Production push delivery requires `a2a.webhookSecret` so every webhook has an HMAC signature.
|
|
125
|
+
The exported `deliverPushNotifications` function also requires a non-empty secret.
|
|
126
|
+
Use `deliverDemoPushNotifications` only for explicit local demo mode.
|
|
127
|
+
Tasks created before this release have no recorded origin and fail closed; migrate them with a verified owner binding or let them expire.
|
|
128
|
+
The payment claim keeps its submission lease until the atomic submitted-to-working transition.
|
|
129
|
+
An expired execution lease fails the working task and preserves its payment recovery markers.
|
|
28
130
|
|
|
29
131
|
## Tier
|
|
30
132
|
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// src/observer.ts
|
|
2
|
+
var ConsoleObserver = class {
|
|
3
|
+
constructor(log = (e) => console.log(JSON.stringify(e))) {
|
|
4
|
+
this.log = log;
|
|
5
|
+
}
|
|
6
|
+
log;
|
|
7
|
+
emit(level, event, ctx, rest = {}) {
|
|
8
|
+
this.log({
|
|
9
|
+
level,
|
|
10
|
+
event,
|
|
11
|
+
time: (/* @__PURE__ */ new Date()).toISOString(),
|
|
12
|
+
requestId: ctx.requestId,
|
|
13
|
+
agentSlug: ctx.agentSlug,
|
|
14
|
+
durationMs: Date.now() - ctx.startMs,
|
|
15
|
+
...rest
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
onRequestStart(ctx) {
|
|
19
|
+
this.emit("info", "gateway.request.start", ctx);
|
|
20
|
+
}
|
|
21
|
+
onPaymentVerified(ctx, info) {
|
|
22
|
+
this.emit("info", "gateway.payment.verified", ctx, info);
|
|
23
|
+
}
|
|
24
|
+
onAuthFailure(ctx, reason) {
|
|
25
|
+
this.emit("warn", "gateway.auth.failure", ctx, reason);
|
|
26
|
+
}
|
|
27
|
+
onRateLimited(ctx, info) {
|
|
28
|
+
this.emit("warn", "gateway.rate_limit", ctx, info);
|
|
29
|
+
}
|
|
30
|
+
onBodyTooLarge(ctx, contentLength) {
|
|
31
|
+
this.emit("warn", "gateway.body_too_large", ctx, { contentLength });
|
|
32
|
+
}
|
|
33
|
+
onInjectionDetected(ctx, info) {
|
|
34
|
+
this.emit("warn", "gateway.injection", ctx, info);
|
|
35
|
+
}
|
|
36
|
+
onRequestComplete(ctx, usage) {
|
|
37
|
+
this.emit("info", "gateway.request.complete", ctx, usage);
|
|
38
|
+
}
|
|
39
|
+
onStreamError(ctx, info) {
|
|
40
|
+
this.emit("error", "gateway.stream.error", ctx, info);
|
|
41
|
+
}
|
|
42
|
+
onSettlementError(ctx, info) {
|
|
43
|
+
this.emit("error", "gateway.settlement.error", ctx, info);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
var CompositeObserver = class {
|
|
47
|
+
constructor(observers) {
|
|
48
|
+
this.observers = observers;
|
|
49
|
+
}
|
|
50
|
+
observers;
|
|
51
|
+
async fanOut(event, ...args) {
|
|
52
|
+
for (const obs of this.observers) {
|
|
53
|
+
const fn = obs[event];
|
|
54
|
+
if (!fn) continue;
|
|
55
|
+
try {
|
|
56
|
+
await fn.apply(obs, args);
|
|
57
|
+
} catch (err) {
|
|
58
|
+
console.warn(`[agent-gateway] observer ${event} threw:`, err instanceof Error ? err.message : err);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
onRequestStart = (ctx) => this.fanOut("onRequestStart", ctx);
|
|
63
|
+
onPaymentVerified = (ctx, info) => this.fanOut("onPaymentVerified", ctx, info);
|
|
64
|
+
onAuthFailure = (ctx, reason) => this.fanOut("onAuthFailure", ctx, reason);
|
|
65
|
+
onRateLimited = (ctx, info) => this.fanOut("onRateLimited", ctx, info);
|
|
66
|
+
onBodyTooLarge = (ctx, contentLength) => this.fanOut("onBodyTooLarge", ctx, contentLength);
|
|
67
|
+
onInjectionDetected = (ctx, info) => this.fanOut("onInjectionDetected", ctx, info);
|
|
68
|
+
onRequestComplete = (ctx, usage) => this.fanOut("onRequestComplete", ctx, usage);
|
|
69
|
+
onStreamError = (ctx, info) => this.fanOut("onStreamError", ctx, info);
|
|
70
|
+
onSettlementError = (ctx, info) => this.fanOut("onSettlementError", ctx, info);
|
|
71
|
+
};
|
|
72
|
+
function generateRequestId() {
|
|
73
|
+
const bytes = new Uint8Array(16);
|
|
74
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
75
|
+
const hex = Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
76
|
+
return `req_${hex}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export {
|
|
80
|
+
ConsoleObserver,
|
|
81
|
+
CompositeObserver,
|
|
82
|
+
generateRequestId
|
|
83
|
+
};
|
|
84
|
+
//# sourceMappingURL=chunk-GITV7CPT.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/observer.ts"],"sourcesContent":["/**\n * Observability hook surface.\n *\n * Consumers implement GatewayObserver to wire the gateway into their existing\n * telemetry stack (Langfuse, OTEL, structured logs, Prometheus, etc.) without\n * the gateway itself depending on any of those libraries.\n *\n * Every event carries a requestId so downstream metrics can correlate the\n * payment verification, sandbox execution, and settlement for one request.\n * When no observer is configured, the gateway stays silent.\n */\n\nimport type { GatewayUsageEvent, PaymentMethod } from './payment-types'\nimport type { AuthFailureReason, GatewayObserver, RequestContext } from './observer-types'\n\nexport type { AuthFailureReason, GatewayObserver, RequestContext } from './observer-types'\n\n// ---------------------------------------------------------------------------\n// Convenience implementations\n// ---------------------------------------------------------------------------\n\n/**\n * Structured-log observer. Emits one JSON line per event on the `log` function.\n * Default sink: console.log. Production consumers usually pipe their own\n * structured logger (pino, winston, the cf Logs binding).\n *\n * Usage:\n * new ConsoleObserver(({ level, event, ...rest }) => logger.info({ event, ...rest }))\n */\nexport class ConsoleObserver implements GatewayObserver {\n constructor(\n private readonly log: (entry: Record<string, unknown>) => void = (e) => console.log(JSON.stringify(e)),\n ) {}\n\n private emit(level: 'info' | 'warn' | 'error', event: string, ctx: RequestContext, rest: Record<string, unknown> = {}) {\n this.log({\n level,\n event,\n time: new Date().toISOString(),\n requestId: ctx.requestId,\n agentSlug: ctx.agentSlug,\n durationMs: Date.now() - ctx.startMs,\n ...rest,\n })\n }\n\n onRequestStart(ctx: RequestContext) { this.emit('info', 'gateway.request.start', ctx) }\n onPaymentVerified(ctx: RequestContext, info: { method: PaymentMethod; consumerId: string; keyId?: string }) {\n this.emit('info', 'gateway.payment.verified', ctx, info)\n }\n onAuthFailure(ctx: RequestContext, reason: AuthFailureReason) {\n this.emit('warn', 'gateway.auth.failure', ctx, reason as unknown as Record<string, unknown>)\n }\n onRateLimited(ctx: RequestContext, info: { consumerId: string; retryAfterSeconds: number }) {\n this.emit('warn', 'gateway.rate_limit', ctx, info)\n }\n onBodyTooLarge(ctx: RequestContext, contentLength: number) {\n this.emit('warn', 'gateway.body_too_large', ctx, { contentLength })\n }\n onInjectionDetected(ctx: RequestContext, info: { consumerId: string; patterns: string[]; blocked: boolean }) {\n this.emit('warn', 'gateway.injection', ctx, info)\n }\n onRequestComplete(ctx: RequestContext, usage: GatewayUsageEvent) {\n this.emit('info', 'gateway.request.complete', ctx, usage as unknown as Record<string, unknown>)\n }\n onStreamError(ctx: RequestContext, info: { consumerId: string; errorMessage: string }) {\n this.emit('error', 'gateway.stream.error', ctx, info)\n }\n onSettlementError(ctx: RequestContext, info: { consumerId: string; method: PaymentMethod; errorMessage: string }) {\n this.emit('error', 'gateway.settlement.error', ctx, info)\n }\n}\n\n/**\n * Compose multiple observers into one. Errors in any individual observer\n * don't break the others (fire-and-forget telemetry).\n */\nexport class CompositeObserver implements GatewayObserver {\n constructor(private readonly observers: GatewayObserver[]) {}\n\n private async fanOut<K extends keyof GatewayObserver>(event: K, ...args: unknown[]): Promise<void> {\n for (const obs of this.observers) {\n const fn = obs[event] as ((...a: unknown[]) => void | Promise<void>) | undefined\n if (!fn) continue\n try {\n await fn.apply(obs, args)\n } catch (err) {\n console.warn(`[agent-gateway] observer ${event} threw:`, err instanceof Error ? err.message : err)\n }\n }\n }\n\n onRequestStart = (ctx: RequestContext) => this.fanOut('onRequestStart', ctx)\n onPaymentVerified = (ctx: RequestContext, info: Parameters<Required<GatewayObserver>['onPaymentVerified']>[1]) =>\n this.fanOut('onPaymentVerified', ctx, info)\n onAuthFailure = (ctx: RequestContext, reason: AuthFailureReason) =>\n this.fanOut('onAuthFailure', ctx, reason)\n onRateLimited = (ctx: RequestContext, info: Parameters<Required<GatewayObserver>['onRateLimited']>[1]) =>\n this.fanOut('onRateLimited', ctx, info)\n onBodyTooLarge = (ctx: RequestContext, contentLength: number) =>\n this.fanOut('onBodyTooLarge', ctx, contentLength)\n onInjectionDetected = (ctx: RequestContext, info: Parameters<Required<GatewayObserver>['onInjectionDetected']>[1]) =>\n this.fanOut('onInjectionDetected', ctx, info)\n onRequestComplete = (ctx: RequestContext, usage: GatewayUsageEvent) =>\n this.fanOut('onRequestComplete', ctx, usage)\n onStreamError = (ctx: RequestContext, info: Parameters<Required<GatewayObserver>['onStreamError']>[1]) =>\n this.fanOut('onStreamError', ctx, info)\n onSettlementError = (ctx: RequestContext, info: Parameters<Required<GatewayObserver>['onSettlementError']>[1]) =>\n this.fanOut('onSettlementError', ctx, info)\n}\n\n/**\n * Generate a request-id. Crypto-random 16 bytes, hex-encoded with an `req_` prefix.\n * Works in Workers, Node, and browsers — all have globalThis.crypto.\n */\nexport function generateRequestId(): string {\n const bytes = new Uint8Array(16)\n globalThis.crypto.getRandomValues(bytes)\n const hex = Array.from(bytes).map((b) => b.toString(16).padStart(2, '0')).join('')\n return `req_${hex}`\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// src/nonce-store.ts
|
|
2
|
+
function nonceTtlSeconds(expiry, nowSeconds = Math.floor(Date.now() / 1e3)) {
|
|
3
|
+
const remaining = expiry - BigInt(nowSeconds);
|
|
4
|
+
if (remaining <= 0n || remaining > BigInt(Number.MAX_SAFE_INTEGER)) return void 0;
|
|
5
|
+
return Math.max(Number(remaining), 60);
|
|
6
|
+
}
|
|
7
|
+
var MemoryNonceStore = class {
|
|
8
|
+
seen = /* @__PURE__ */ new Map();
|
|
9
|
+
lastEviction = Date.now();
|
|
10
|
+
async hasSeen(nonce) {
|
|
11
|
+
this.evictExpired();
|
|
12
|
+
const entry = this.seen.get(nonce);
|
|
13
|
+
if (!entry) return false;
|
|
14
|
+
if (entry.expiresAt < Date.now()) {
|
|
15
|
+
this.seen.delete(nonce);
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
async claim(nonce, ttlSeconds, ownerId) {
|
|
21
|
+
this.evictExpired();
|
|
22
|
+
const now = Date.now();
|
|
23
|
+
const entry = this.seen.get(nonce);
|
|
24
|
+
if (entry !== void 0 && entry.expiresAt >= now) {
|
|
25
|
+
return ownerId !== void 0 && entry.ownerId === ownerId;
|
|
26
|
+
}
|
|
27
|
+
this.seen.set(nonce, { expiresAt: now + ttlSeconds * 1e3, ownerId });
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
async markSeen(nonce, ttlSeconds) {
|
|
31
|
+
this.evictExpired();
|
|
32
|
+
this.seen.set(nonce, { expiresAt: Date.now() + ttlSeconds * 1e3 });
|
|
33
|
+
}
|
|
34
|
+
evictExpired() {
|
|
35
|
+
const now = Date.now();
|
|
36
|
+
if (now - this.lastEviction < 6e4) return;
|
|
37
|
+
this.lastEviction = now;
|
|
38
|
+
for (const [nonce, entry] of this.seen) {
|
|
39
|
+
if (entry.expiresAt < now) this.seen.delete(nonce);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
var KvNonceStore = class {
|
|
44
|
+
constructor(kv, prefix = "nonce", options = {}) {
|
|
45
|
+
this.kv = kv;
|
|
46
|
+
this.prefix = prefix;
|
|
47
|
+
this.atomicClaim = options.atomicClaim ?? (kv.putIfAbsent ? async (key, ttlSeconds, ownerId) => {
|
|
48
|
+
const value = ownerId ?? "1";
|
|
49
|
+
if (ownerId !== void 0) {
|
|
50
|
+
const existing = await kv.get(key);
|
|
51
|
+
if (existing !== null) return existing === ownerId;
|
|
52
|
+
}
|
|
53
|
+
const inserted = await kv.putIfAbsent(key, value, { expirationTtl: ttlSeconds });
|
|
54
|
+
if (inserted || ownerId === void 0) return inserted;
|
|
55
|
+
return await kv.get(key) === ownerId;
|
|
56
|
+
} : void 0);
|
|
57
|
+
}
|
|
58
|
+
kv;
|
|
59
|
+
prefix;
|
|
60
|
+
atomicClaim;
|
|
61
|
+
async hasSeen(nonce) {
|
|
62
|
+
return await this.kv.get(this.key(nonce)) !== null;
|
|
63
|
+
}
|
|
64
|
+
async markSeen(nonce, ttlSeconds) {
|
|
65
|
+
const ttl = Math.max(ttlSeconds, 60);
|
|
66
|
+
await this.kv.put(this.key(nonce), "1", { expirationTtl: ttl });
|
|
67
|
+
}
|
|
68
|
+
async claim(nonce, ttlSeconds, ownerId) {
|
|
69
|
+
if (!this.atomicClaim) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
"KvNonceStore requires an atomicClaim backed by D1, Durable Objects, or an atomic KV extension"
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
const ttl = Math.max(ttlSeconds, 60);
|
|
75
|
+
return this.atomicClaim(this.key(nonce), ttl, ownerId);
|
|
76
|
+
}
|
|
77
|
+
/** Used by gateway validation to reject plain, non-atomic KV bindings. */
|
|
78
|
+
hasAtomicClaim() {
|
|
79
|
+
return this.atomicClaim !== void 0;
|
|
80
|
+
}
|
|
81
|
+
key(nonce) {
|
|
82
|
+
return `${this.prefix}:${nonce}`;
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
async function claimStoredNonce(store, nonce, ttlSeconds, ownerId) {
|
|
86
|
+
if (typeof store.claim !== "function") {
|
|
87
|
+
throw new Error("NonceStore.claim is required for atomic payment replay protection");
|
|
88
|
+
}
|
|
89
|
+
return store.claim(nonce, ttlSeconds, ownerId);
|
|
90
|
+
}
|
|
91
|
+
function isAtomicNonceStore(store) {
|
|
92
|
+
const kvStore = store;
|
|
93
|
+
if (typeof kvStore.hasAtomicClaim === "function" && !kvStore.hasAtomicClaim()) return false;
|
|
94
|
+
return typeof store.claim === "function";
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export {
|
|
98
|
+
nonceTtlSeconds,
|
|
99
|
+
MemoryNonceStore,
|
|
100
|
+
KvNonceStore,
|
|
101
|
+
claimStoredNonce,
|
|
102
|
+
isAtomicNonceStore
|
|
103
|
+
};
|
|
104
|
+
//# sourceMappingURL=chunk-J5SDVHOL.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/nonce-store.ts"],"sourcesContent":["/**\n * Nonce replay protection for x402/MPP payments.\n * Tracks seen nonces to prevent the same payment from being used twice.\n */\n\nexport interface NonceStore {\n /** Check if nonce has been seen. This method never grants ownership. */\n hasSeen(nonce: string): Promise<boolean>\n /**\n * Atomically claim a nonce. An owner id makes a retry by the same payment\n * operation idempotent. This is optional only for the 0.7.1 check-and-mark\n * compatibility contract; durable owner claims require this method.\n */\n claim?(nonce: string, ttlSeconds: number, ownerId?: string): Promise<boolean>\n /** @deprecated Use claim() for atomic ownership in new stores. */\n markSeen?(nonce: string, ttlSeconds: number): Promise<void>\n}\n\nexport interface AtomicNonceStore extends NonceStore {\n claim(nonce: string, ttlSeconds: number, ownerId?: string): Promise<boolean>\n}\n\n/**\n * Return the seconds for which a signed nonce must remain stored.\n *\n * The signed expiry is the replay boundary. A fixed one-hour cap would allow\n * a still-valid authorization to replay after the nonce entry expires.\n */\nexport function nonceTtlSeconds(\n expiry: bigint,\n nowSeconds = Math.floor(Date.now() / 1000),\n): number | undefined {\n const remaining = expiry - BigInt(nowSeconds)\n if (remaining <= 0n || remaining > BigInt(Number.MAX_SAFE_INTEGER)) return undefined\n return Math.max(Number(remaining), 60)\n}\n\n// ---------------------------------------------------------------------------\n// In-memory implementation — single-worker, ephemeral\n// ---------------------------------------------------------------------------\n\n/** In-memory nonce store with automatic eviction. Use in tests or single-worker deploys. */\nexport class MemoryNonceStore implements NonceStore {\n private seen = new Map<string, { expiresAt: number; ownerId?: string }>()\n private lastEviction = Date.now()\n\n async hasSeen(nonce: string): Promise<boolean> {\n this.evictExpired()\n const entry = this.seen.get(nonce)\n if (!entry) return false\n if (entry.expiresAt < Date.now()) {\n this.seen.delete(nonce)\n return false\n }\n return true\n }\n\n async claim(nonce: string, ttlSeconds: number, ownerId?: string): Promise<boolean> {\n this.evictExpired()\n const now = Date.now()\n const entry = this.seen.get(nonce)\n if (entry !== undefined && entry.expiresAt >= now) {\n return ownerId !== undefined && entry.ownerId === ownerId\n }\n this.seen.set(nonce, { expiresAt: now + ttlSeconds * 1000, ownerId })\n return true\n }\n\n async markSeen(nonce: string, ttlSeconds: number): Promise<void> {\n this.evictExpired()\n this.seen.set(nonce, { expiresAt: Date.now() + ttlSeconds * 1000 })\n }\n\n private evictExpired() {\n const now = Date.now()\n // Evict at most every 60 seconds to avoid O(n) on every request\n if (now - this.lastEviction < 60_000) return\n this.lastEviction = now\n for (const [nonce, entry] of this.seen) {\n if (entry.expiresAt < now) this.seen.delete(nonce)\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Cloudflare KV implementation — multi-worker, distributed\n// ---------------------------------------------------------------------------\n\n/**\n * Minimal KVNamespace shape — matches Cloudflare Workers' @cloudflare/workers-types\n * without pulling that package as a dep. Production consumers cast their KV\n * binding to this interface at the construction site.\n */\nexport interface KVNamespace {\n get(key: string, options?: { type?: 'text' | 'json' }): Promise<string | null>\n put(key: string, value: string, options?: { expirationTtl?: number }): Promise<void>\n /** Optional linearizable create-if-absent extension. Cloudflare KV does not provide it. */\n putIfAbsent?(key: string, value: string, options?: { expirationTtl?: number }): Promise<boolean>\n delete(key: string): Promise<void>\n}\n\n/** Atomic claim supplied by D1, a Durable Object, or another linearizable store. */\nexport type AtomicKvNonceClaim = (\n key: string,\n ttlSeconds: number,\n ownerId?: string,\n) => Promise<boolean>\n\nexport interface KvNonceStoreOptions {\n /**\n * Claim the fully namespaced key atomically.\n * The callback must make same-owner retries idempotent.\n */\n atomicClaim?: AtomicKvNonceClaim\n}\n\n/**\n * KV-backed NonceStore for distributed Cloudflare Workers deployments.\n *\n * Why this exists: MemoryNonceStore works on a single worker instance, but\n * Cloudflare routes requests across multiple isolates. Without shared state,\n * an attacker could retry a replayed nonce against a different isolate and\n * have it accepted. Cloudflare KV has no conditional write, so a plain KV\n * binding is not an atomic payment store. Supply `atomicClaim` from D1,\n * Durable Objects, or another linearizable service before using this store\n * for paid requests.\n *\n * Usage:\n * const nonceStore = new KvNonceStore(env.NONCE_KV, 'x402')\n * createAgentGateway({ ...config, nonceStore })\n */\nexport class KvNonceStore implements NonceStore {\n private readonly atomicClaim?: AtomicKvNonceClaim\n\n constructor(\n private readonly kv: KVNamespace,\n /** Key prefix to namespace within a shared KV (default: \"nonce\"). */\n private readonly prefix: string = 'nonce',\n options: KvNonceStoreOptions = {},\n ) {\n this.atomicClaim = options.atomicClaim ?? (\n kv.putIfAbsent\n ? async (key, ttlSeconds, ownerId) => {\n const value = ownerId ?? '1'\n if (ownerId !== undefined) {\n const existing = await kv.get(key)\n if (existing !== null) return existing === ownerId\n }\n const inserted = await kv.putIfAbsent!(key, value, { expirationTtl: ttlSeconds })\n if (inserted || ownerId === undefined) return inserted\n return (await kv.get(key)) === ownerId\n }\n : undefined\n )\n }\n\n async hasSeen(nonce: string): Promise<boolean> {\n return (await this.kv.get(this.key(nonce))) !== null\n }\n\n async markSeen(nonce: string, ttlSeconds: number): Promise<void> {\n const ttl = Math.max(ttlSeconds, 60)\n await this.kv.put(this.key(nonce), '1', { expirationTtl: ttl })\n }\n\n async claim(nonce: string, ttlSeconds: number, ownerId?: string): Promise<boolean> {\n if (!this.atomicClaim) {\n throw new Error(\n 'KvNonceStore requires an atomicClaim backed by D1, Durable Objects, or an atomic KV extension',\n )\n }\n const ttl = Math.max(ttlSeconds, 60)\n return this.atomicClaim(this.key(nonce), ttl, ownerId)\n }\n\n /** Used by gateway validation to reject plain, non-atomic KV bindings. */\n hasAtomicClaim(): boolean {\n return this.atomicClaim !== undefined\n }\n\n private key(nonce: string): string {\n return `${this.prefix}:${nonce}`\n }\n}\n\n/** Claim through the one atomic contract used by every payment path. */\nexport async function claimStoredNonce(\n store: NonceStore,\n nonce: string,\n ttlSeconds: number,\n ownerId?: string,\n): Promise<boolean> {\n if (typeof store.claim !== 'function') {\n throw new Error('NonceStore.claim is required for atomic payment replay protection')\n }\n return store.claim(nonce, ttlSeconds, ownerId)\n}\n\n/** Durable payment paths must use a store with a single atomic claim operation. */\nexport function isAtomicNonceStore(store: NonceStore): store is AtomicNonceStore {\n const kvStore = store as NonceStore & { hasAtomicClaim?: () => boolean }\n if (typeof kvStore.hasAtomicClaim === 'function' && !kvStore.hasAtomicClaim()) return false\n return typeof store.claim === 'function'\n}\n"],"mappings":"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","names":[]}
|