@tangle-network/agent-gateway 0.1.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 +37 -0
- package/dist/api-keys.d.ts +63 -0
- package/dist/api-keys.js +9 -0
- package/dist/api-keys.js.map +1 -0
- package/dist/chunk-4FULF5LW.js +55 -0
- package/dist/chunk-4FULF5LW.js.map +1 -0
- package/dist/chunk-5O75YDQP.js +89 -0
- package/dist/chunk-5O75YDQP.js.map +1 -0
- package/dist/chunk-5ZJPIIIV.js +56 -0
- package/dist/chunk-5ZJPIIIV.js.map +1 -0
- package/dist/chunk-EMGS63QE.js +398 -0
- package/dist/chunk-EMGS63QE.js.map +1 -0
- package/dist/chunk-Z22ALGHW.js +33 -0
- package/dist/chunk-Z22ALGHW.js.map +1 -0
- package/dist/index.d.ts +74 -0
- package/dist/index.js +41 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware.d.ts +19 -0
- package/dist/middleware.js +9 -0
- package/dist/middleware.js.map +1 -0
- package/dist/nonce-store.d.ts +20 -0
- package/dist/nonce-store.js +7 -0
- package/dist/nonce-store.js.map +1 -0
- package/dist/publish.d.ts +42 -0
- package/dist/publish.js +7 -0
- package/dist/publish.js.map +1 -0
- package/dist/rate-limit.d.ts +33 -0
- package/dist/rate-limit.js +9 -0
- package/dist/rate-limit.js.map +1 -0
- package/dist/types.d.ts +157 -0
- package/dist/types.js +1 -0
- package/dist/types.js.map +1 -0
- package/package.json +37 -0
- package/src/api-keys.ts +187 -0
- package/src/filter.ts +138 -0
- package/src/index.ts +49 -0
- package/src/middleware.ts +321 -0
- package/src/nonce-store.ts +43 -0
- package/src/publish.ts +116 -0
- package/src/rate-limit.ts +87 -0
- package/src/types.ts +177 -0
- package/src/verify.ts +112 -0
package/src/types.ts
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// --- Agent resolution ---
|
|
2
|
+
|
|
3
|
+
export interface AgentMeta {
|
|
4
|
+
/** Unique agent identifier (workspace ID, session ID, etc.) */
|
|
5
|
+
id: string
|
|
6
|
+
/** Owner/creator user ID */
|
|
7
|
+
ownerId: string
|
|
8
|
+
/** Public URL slug */
|
|
9
|
+
slug: string
|
|
10
|
+
/** System prompt for the agent (injected before consumer messages) */
|
|
11
|
+
systemPrompt?: string
|
|
12
|
+
/** Per-token price in USD (default: 0.00002) */
|
|
13
|
+
pricePerTokenUsd: number
|
|
14
|
+
/** Platform fee as decimal 0-1 (default: 0.20 = 20%) */
|
|
15
|
+
platformFeePercent: number
|
|
16
|
+
/** Remote operator endpoint for sovereignty mode (null = centralized) */
|
|
17
|
+
sandboxEndpoint: string | null
|
|
18
|
+
/** Sandbox ID on remote operator */
|
|
19
|
+
remoteSandboxId: string | null
|
|
20
|
+
/** PASETO bearer token for remote operator auth */
|
|
21
|
+
remoteBearerToken: string | null
|
|
22
|
+
/** Whether agent is published and accepting requests */
|
|
23
|
+
enabled: boolean
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// --- Payment ---
|
|
27
|
+
|
|
28
|
+
export type PaymentMethod = 'x402' | 'mpp' | 'apikey' | 'none'
|
|
29
|
+
|
|
30
|
+
export interface X402Config {
|
|
31
|
+
/** Ethereum operator address for SpendAuth verification */
|
|
32
|
+
operatorAddress: string
|
|
33
|
+
/** Blockchain network ID (default: 3799) */
|
|
34
|
+
chainId: number
|
|
35
|
+
/** ShieldedCredits contract address */
|
|
36
|
+
creditsAddress?: string
|
|
37
|
+
/** RPC URL for on-chain verification (optional, demo mode skips this) */
|
|
38
|
+
rpcUrl?: string
|
|
39
|
+
/** Demo mode: skip signature verification (default: false). NEVER enable in production. */
|
|
40
|
+
demoMode?: boolean
|
|
41
|
+
/** Production signer verification. Called with the raw SpendAuth payload. Return true if signature is valid. */
|
|
42
|
+
verifySigner?: (payload: Record<string, unknown>) => Promise<boolean>
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface MppConfig {
|
|
46
|
+
/** MPP realm (e.g. "agents.tangle.tools") */
|
|
47
|
+
realm: string
|
|
48
|
+
/** MPP method name (default: "blueprintevm") */
|
|
49
|
+
method?: string
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface PaymentResult {
|
|
53
|
+
method: PaymentMethod
|
|
54
|
+
consumerId: string
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface ApiKeyInfo {
|
|
58
|
+
keyId: string
|
|
59
|
+
consumerId: string
|
|
60
|
+
/** Scopes this key is authorized for (e.g. ["chat", "forms"]) */
|
|
61
|
+
scopes?: string[]
|
|
62
|
+
/** Per-key rate limit override (requests per minute). If set, overrides global rate limit. */
|
|
63
|
+
rateLimitPerMinute?: number
|
|
64
|
+
/** Per-key daily limit override. */
|
|
65
|
+
dailyLimit?: number
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// --- Usage tracking ---
|
|
69
|
+
|
|
70
|
+
export interface GatewayUsageEvent {
|
|
71
|
+
agentId: string
|
|
72
|
+
agentSlug: string
|
|
73
|
+
consumerId: string
|
|
74
|
+
paymentMethod: PaymentMethod
|
|
75
|
+
inputTokens: number
|
|
76
|
+
outputTokens: number
|
|
77
|
+
totalCostUsd: number
|
|
78
|
+
ownerEarnedUsd: number
|
|
79
|
+
platformFeeUsd: number
|
|
80
|
+
durationMs: number
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// --- Sandbox interface ---
|
|
84
|
+
|
|
85
|
+
export interface SandboxStreamEvent {
|
|
86
|
+
type?: string
|
|
87
|
+
data?: {
|
|
88
|
+
part?: { type?: string; text?: string }
|
|
89
|
+
delta?: string
|
|
90
|
+
finalText?: string
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface SandboxBox {
|
|
95
|
+
streamPrompt(message: string, opts?: { sessionId?: string; systemPrompt?: string }): AsyncIterable<SandboxStreamEvent>
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// --- Gateway config ---
|
|
99
|
+
|
|
100
|
+
export interface GatewayConfig {
|
|
101
|
+
/** Resolve agent metadata by slug. Return null if not found or not published. */
|
|
102
|
+
resolveAgent: (slug: string) => Promise<AgentMeta | null>
|
|
103
|
+
|
|
104
|
+
/** Get a sandbox instance for the agent. Called after payment is verified. */
|
|
105
|
+
getSandbox: (agent: AgentMeta) => Promise<SandboxBox>
|
|
106
|
+
|
|
107
|
+
/** Record a usage event after request completes. */
|
|
108
|
+
recordUsage: (event: GatewayUsageEvent) => Promise<void>
|
|
109
|
+
|
|
110
|
+
/** x402 payment configuration */
|
|
111
|
+
x402: X402Config
|
|
112
|
+
|
|
113
|
+
/** MPP (Machine Payments Protocol) configuration. If provided, gateway accepts Authorization: Payment headers. */
|
|
114
|
+
mpp?: MppConfig
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Verify an API key. Return key info if valid, null if invalid.
|
|
118
|
+
* Default: accepts any `sk_agent_*` key (demo mode).
|
|
119
|
+
*/
|
|
120
|
+
verifyApiKey?: (authHeader: string) => Promise<ApiKeyInfo | null>
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Settle payment after successful response.
|
|
124
|
+
* For x402: call ShieldedCredits.claimPayment()
|
|
125
|
+
* For API key: deduct from spending limit
|
|
126
|
+
* Default: no-op (demo mode).
|
|
127
|
+
*/
|
|
128
|
+
settlePayment?: (payment: PaymentResult, cost: number) => Promise<void>
|
|
129
|
+
|
|
130
|
+
/** Base URL for API key purchase links (e.g. "https://film.tangle.tools") */
|
|
131
|
+
baseUrl?: string
|
|
132
|
+
|
|
133
|
+
/** Max message length in chars (default: 8000) */
|
|
134
|
+
maxMessageLength?: number
|
|
135
|
+
|
|
136
|
+
/** Required scope for chat endpoint (default: "chat"). API keys must include this scope. */
|
|
137
|
+
requiredScope?: string
|
|
138
|
+
|
|
139
|
+
/** Block requests with detected injection patterns (default: false — log only) */
|
|
140
|
+
blockInjection?: boolean
|
|
141
|
+
|
|
142
|
+
/** Rate limiting config. Default: 60 requests per 60 seconds per consumer. */
|
|
143
|
+
rateLimit?: { limit: number; windowSeconds: number }
|
|
144
|
+
|
|
145
|
+
/** Custom rate limit store (default: in-memory). Use KV-backed for Workers. */
|
|
146
|
+
rateLimitStore?: import('./rate-limit').RateLimitStore
|
|
147
|
+
|
|
148
|
+
/** Nonce replay protection store (default: in-memory). Rejects reused x402 nonces. */
|
|
149
|
+
nonceStore?: import('./nonce-store').NonceStore
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// --- Chat completion types (OpenAI-compatible) ---
|
|
153
|
+
|
|
154
|
+
export interface ChatMessage {
|
|
155
|
+
role: 'system' | 'user' | 'assistant' | 'tool'
|
|
156
|
+
content: string
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export interface ChatCompletionRequest {
|
|
160
|
+
model?: string
|
|
161
|
+
messages: ChatMessage[]
|
|
162
|
+
stream?: boolean
|
|
163
|
+
temperature?: number
|
|
164
|
+
max_tokens?: number
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export interface ChatCompletionChunk {
|
|
168
|
+
id: string
|
|
169
|
+
object: 'chat.completion.chunk'
|
|
170
|
+
created: number
|
|
171
|
+
model: string
|
|
172
|
+
choices: Array<{
|
|
173
|
+
index: number
|
|
174
|
+
delta: { content?: string; role?: string }
|
|
175
|
+
finish_reason: string | null
|
|
176
|
+
}>
|
|
177
|
+
}
|
package/src/verify.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { X402Config, MppConfig, ApiKeyInfo } from './types'
|
|
2
|
+
import type { NonceStore } from './nonce-store'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Verify x402 SpendAuth signature (EIP-712).
|
|
6
|
+
* Returns the signer address (commitment) if valid, null otherwise.
|
|
7
|
+
*
|
|
8
|
+
* DEMO MODE (demoMode: true): accepts any well-formed header structure.
|
|
9
|
+
* PRODUCTION: requires config.verifySigner callback for on-chain verification.
|
|
10
|
+
*/
|
|
11
|
+
export async function verifyX402(
|
|
12
|
+
spendAuthHeader: string,
|
|
13
|
+
config: X402Config,
|
|
14
|
+
nonceStore?: NonceStore,
|
|
15
|
+
): Promise<string | null> {
|
|
16
|
+
try {
|
|
17
|
+
const raw = JSON.parse(spendAuthHeader)
|
|
18
|
+
if (!raw.commitment || !raw.signature || !raw.amount) return null
|
|
19
|
+
if (raw.operator?.toLowerCase() !== config.operatorAddress.toLowerCase()) return null
|
|
20
|
+
|
|
21
|
+
const amount = BigInt(raw.amount)
|
|
22
|
+
const nonce = BigInt(raw.nonce)
|
|
23
|
+
const expiry = BigInt(raw.expiry)
|
|
24
|
+
|
|
25
|
+
// Reject expired payments
|
|
26
|
+
if (expiry < BigInt(Math.floor(Date.now() / 1000))) return null
|
|
27
|
+
|
|
28
|
+
// Reject zero-amount payments
|
|
29
|
+
if (amount <= 0n) return null
|
|
30
|
+
|
|
31
|
+
// Reject replayed nonces
|
|
32
|
+
const nonceKey = `${raw.commitment}:${nonce.toString()}`
|
|
33
|
+
if (nonceStore) {
|
|
34
|
+
if (await nonceStore.hasSeen(nonceKey)) return null
|
|
35
|
+
// Mark seen with TTL matching the expiry window (max 1 hour)
|
|
36
|
+
const ttl = Math.min(Number(expiry) - Math.floor(Date.now() / 1000), 3600)
|
|
37
|
+
await nonceStore.markSeen(nonceKey, Math.max(ttl, 60))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Production: delegate to on-chain verification
|
|
41
|
+
if (config.verifySigner) {
|
|
42
|
+
const verified = await config.verifySigner(raw)
|
|
43
|
+
if (!verified) return null
|
|
44
|
+
} else if (!config.demoMode) {
|
|
45
|
+
console.warn('[agent-gateway] x402 verification running without verifySigner — set demoMode: true to suppress this warning')
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return raw.commitment
|
|
49
|
+
} catch {
|
|
50
|
+
return null
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Verify MPP (Machine Payments Protocol) Authorization: Payment header.
|
|
56
|
+
*
|
|
57
|
+
* MPP uses `Authorization: Payment <method> <credential>` format where
|
|
58
|
+
* the credential is a base64url-encoded JSON wrapping the same EIP-3009
|
|
59
|
+
* payment payload that x402 uses. This means existing x402 wallets work
|
|
60
|
+
* unchanged over the MPP wire format.
|
|
61
|
+
*
|
|
62
|
+
* Returns the signer address if valid, null otherwise.
|
|
63
|
+
* In demo mode, accepts any well-formed Payment header.
|
|
64
|
+
*/
|
|
65
|
+
export async function verifyMpp(
|
|
66
|
+
authHeader: string,
|
|
67
|
+
_config: MppConfig,
|
|
68
|
+
x402Config: X402Config,
|
|
69
|
+
): Promise<string | null> {
|
|
70
|
+
// MPP format: "Payment <method> <base64url-credential>"
|
|
71
|
+
const match = authHeader.match(/^Payment\s+(\S+)\s+(\S+)$/i)
|
|
72
|
+
if (!match) return null
|
|
73
|
+
|
|
74
|
+
const [, , credentialB64] = match
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
// Decode base64url credential → JSON with the same EIP-3009 payload
|
|
78
|
+
const decoded = Buffer.from(credentialB64, 'base64url').toString('utf-8')
|
|
79
|
+
const credential = JSON.parse(decoded)
|
|
80
|
+
|
|
81
|
+
// The credential payload wraps the same fields x402 uses
|
|
82
|
+
const payload = credential.payload ?? credential
|
|
83
|
+
if (!payload.commitment && !payload.from) return null
|
|
84
|
+
|
|
85
|
+
// Validate operator match (same as x402)
|
|
86
|
+
const operator = payload.operator ?? payload.to
|
|
87
|
+
if (operator && operator.toLowerCase() !== x402Config.operatorAddress.toLowerCase()) return null
|
|
88
|
+
|
|
89
|
+
// Validate bigint fields if present
|
|
90
|
+
if (payload.amount) BigInt(payload.amount)
|
|
91
|
+
if (payload.nonce) BigInt(payload.nonce)
|
|
92
|
+
|
|
93
|
+
return payload.commitment ?? payload.from ?? null
|
|
94
|
+
} catch {
|
|
95
|
+
return null
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Default API key verifier — accepts any `sk_agent_*` key (demo mode).
|
|
101
|
+
* Override in GatewayConfig.verifyApiKey for production.
|
|
102
|
+
*/
|
|
103
|
+
export async function defaultVerifyApiKey(
|
|
104
|
+
authHeader: string,
|
|
105
|
+
): Promise<ApiKeyInfo | null> {
|
|
106
|
+
if (!authHeader.startsWith('Bearer sk_agent_')) return null
|
|
107
|
+
const key = authHeader.slice(7)
|
|
108
|
+
return {
|
|
109
|
+
keyId: key.slice(0, 16),
|
|
110
|
+
consumerId: `apikey:${key.slice(0, 16)}`,
|
|
111
|
+
}
|
|
112
|
+
}
|