@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/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
+ }