@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.
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Nonce replay protection for x402/MPP payments.
3
+ * Tracks seen nonces to prevent the same payment from being used twice.
4
+ */
5
+ interface NonceStore {
6
+ /** Check if nonce has been seen. Returns true if already used (reject). */
7
+ hasSeen(nonce: string): Promise<boolean>;
8
+ /** Mark nonce as used. TTL = how long to remember it (seconds). */
9
+ markSeen(nonce: string, ttlSeconds: number): Promise<void>;
10
+ }
11
+ /** In-memory nonce store with automatic eviction */
12
+ declare class MemoryNonceStore implements NonceStore {
13
+ private seen;
14
+ private lastEviction;
15
+ hasSeen(nonce: string): Promise<boolean>;
16
+ markSeen(nonce: string, ttlSeconds: number): Promise<void>;
17
+ private evictExpired;
18
+ }
19
+
20
+ export { MemoryNonceStore, type NonceStore };
@@ -0,0 +1,7 @@
1
+ import {
2
+ MemoryNonceStore
3
+ } from "./chunk-Z22ALGHW.js";
4
+ export {
5
+ MemoryNonceStore
6
+ };
7
+ //# sourceMappingURL=nonce-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,42 @@
1
+ import * as hono_types from 'hono/types';
2
+ import { Hono } from 'hono';
3
+
4
+ interface PublishedConfig {
5
+ enabled: boolean;
6
+ slug: string;
7
+ pricePerTokenUsd: number;
8
+ platformFeePercent: number;
9
+ /** Remote operator endpoint for sovereignty mode */
10
+ sandboxEndpoint?: string | null;
11
+ remoteSandboxId?: string | null;
12
+ remoteBearerToken?: string | null;
13
+ publishedAt: string;
14
+ }
15
+ interface PublishRequest {
16
+ slug?: string;
17
+ pricePerTokenUsd?: number;
18
+ platformFeePercent?: number;
19
+ sandboxEndpoint?: string | null;
20
+ remoteSandboxId?: string | null;
21
+ remoteBearerToken?: string | null;
22
+ }
23
+ /** Each agent implements this against their workspace/session model */
24
+ interface PublishStore {
25
+ /** Get current published config for a workspace/session */
26
+ getPublishedConfig(ownerId: string, resourceId: string): Promise<PublishedConfig | null>;
27
+ /** Set published config */
28
+ setPublishedConfig(ownerId: string, resourceId: string, config: PublishedConfig): Promise<void>;
29
+ /** Clear published config (unpublish) */
30
+ clearPublishedConfig(ownerId: string, resourceId: string): Promise<void>;
31
+ /** Check the resource exists and the user owns it */
32
+ verifyOwnership(ownerId: string, resourceId: string): Promise<boolean>;
33
+ }
34
+ interface PublishRoutesConfig {
35
+ store: PublishStore;
36
+ getAuthUserId: (request: Request) => Promise<string | null>;
37
+ /** Base URL for gateway endpoint display (e.g. "https://gtm.tangle.tools") */
38
+ baseUrl?: string;
39
+ }
40
+ declare function createPublishRoutes(config: PublishRoutesConfig): Hono<hono_types.BlankEnv, hono_types.BlankSchema, "/">;
41
+
42
+ export { type PublishRequest, type PublishRoutesConfig, type PublishStore, type PublishedConfig, createPublishRoutes };
@@ -0,0 +1,7 @@
1
+ import {
2
+ createPublishRoutes
3
+ } from "./chunk-5ZJPIIIV.js";
4
+ export {
5
+ createPublishRoutes
6
+ };
7
+ //# sourceMappingURL=publish.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Sliding window rate limiter.
3
+ * In-memory by default. Override with KV-backed store for Workers.
4
+ */
5
+ interface RateLimitConfig {
6
+ /** Max requests per window (default: 60) */
7
+ limit: number;
8
+ /** Window size in seconds (default: 60) */
9
+ windowSeconds: number;
10
+ }
11
+ interface RateLimitResult {
12
+ allowed: boolean;
13
+ remaining: number;
14
+ resetAt: number;
15
+ retryAfterSeconds?: number;
16
+ }
17
+ interface RateLimitStore {
18
+ /** Get timestamps of recent requests for this key */
19
+ get(key: string): Promise<number[]>;
20
+ /** Set timestamps for this key (with TTL) */
21
+ set(key: string, timestamps: number[], ttlSeconds: number): Promise<void>;
22
+ }
23
+ /** In-memory rate limit store with periodic eviction */
24
+ declare class MemoryRateLimitStore implements RateLimitStore {
25
+ private store;
26
+ private lastEviction;
27
+ get(key: string): Promise<number[]>;
28
+ set(key: string, timestamps: number[], ttlSeconds: number): Promise<void>;
29
+ private evictExpired;
30
+ }
31
+ declare function checkRateLimit(consumerId: string, config: RateLimitConfig, store: RateLimitStore): Promise<RateLimitResult>;
32
+
33
+ export { MemoryRateLimitStore, type RateLimitConfig, type RateLimitResult, type RateLimitStore, checkRateLimit };
@@ -0,0 +1,9 @@
1
+ import {
2
+ MemoryRateLimitStore,
3
+ checkRateLimit
4
+ } from "./chunk-4FULF5LW.js";
5
+ export {
6
+ MemoryRateLimitStore,
7
+ checkRateLimit
8
+ };
9
+ //# sourceMappingURL=rate-limit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,157 @@
1
+ import { NonceStore } from './nonce-store.js';
2
+ import { RateLimitStore } from './rate-limit.js';
3
+
4
+ interface AgentMeta {
5
+ /** Unique agent identifier (workspace ID, session ID, etc.) */
6
+ id: string;
7
+ /** Owner/creator user ID */
8
+ ownerId: string;
9
+ /** Public URL slug */
10
+ slug: string;
11
+ /** System prompt for the agent (injected before consumer messages) */
12
+ systemPrompt?: string;
13
+ /** Per-token price in USD (default: 0.00002) */
14
+ pricePerTokenUsd: number;
15
+ /** Platform fee as decimal 0-1 (default: 0.20 = 20%) */
16
+ platformFeePercent: number;
17
+ /** Remote operator endpoint for sovereignty mode (null = centralized) */
18
+ sandboxEndpoint: string | null;
19
+ /** Sandbox ID on remote operator */
20
+ remoteSandboxId: string | null;
21
+ /** PASETO bearer token for remote operator auth */
22
+ remoteBearerToken: string | null;
23
+ /** Whether agent is published and accepting requests */
24
+ enabled: boolean;
25
+ }
26
+ type PaymentMethod = 'x402' | 'mpp' | 'apikey' | 'none';
27
+ interface X402Config {
28
+ /** Ethereum operator address for SpendAuth verification */
29
+ operatorAddress: string;
30
+ /** Blockchain network ID (default: 3799) */
31
+ chainId: number;
32
+ /** ShieldedCredits contract address */
33
+ creditsAddress?: string;
34
+ /** RPC URL for on-chain verification (optional, demo mode skips this) */
35
+ rpcUrl?: string;
36
+ /** Demo mode: skip signature verification (default: false). NEVER enable in production. */
37
+ demoMode?: boolean;
38
+ /** Production signer verification. Called with the raw SpendAuth payload. Return true if signature is valid. */
39
+ verifySigner?: (payload: Record<string, unknown>) => Promise<boolean>;
40
+ }
41
+ interface MppConfig {
42
+ /** MPP realm (e.g. "agents.tangle.tools") */
43
+ realm: string;
44
+ /** MPP method name (default: "blueprintevm") */
45
+ method?: string;
46
+ }
47
+ interface PaymentResult {
48
+ method: PaymentMethod;
49
+ consumerId: string;
50
+ }
51
+ interface ApiKeyInfo {
52
+ keyId: string;
53
+ consumerId: string;
54
+ /** Scopes this key is authorized for (e.g. ["chat", "forms"]) */
55
+ scopes?: string[];
56
+ /** Per-key rate limit override (requests per minute). If set, overrides global rate limit. */
57
+ rateLimitPerMinute?: number;
58
+ /** Per-key daily limit override. */
59
+ dailyLimit?: number;
60
+ }
61
+ interface GatewayUsageEvent {
62
+ agentId: string;
63
+ agentSlug: string;
64
+ consumerId: string;
65
+ paymentMethod: PaymentMethod;
66
+ inputTokens: number;
67
+ outputTokens: number;
68
+ totalCostUsd: number;
69
+ ownerEarnedUsd: number;
70
+ platformFeeUsd: number;
71
+ durationMs: number;
72
+ }
73
+ interface SandboxStreamEvent {
74
+ type?: string;
75
+ data?: {
76
+ part?: {
77
+ type?: string;
78
+ text?: string;
79
+ };
80
+ delta?: string;
81
+ finalText?: string;
82
+ };
83
+ }
84
+ interface SandboxBox {
85
+ streamPrompt(message: string, opts?: {
86
+ sessionId?: string;
87
+ systemPrompt?: string;
88
+ }): AsyncIterable<SandboxStreamEvent>;
89
+ }
90
+ interface GatewayConfig {
91
+ /** Resolve agent metadata by slug. Return null if not found or not published. */
92
+ resolveAgent: (slug: string) => Promise<AgentMeta | null>;
93
+ /** Get a sandbox instance for the agent. Called after payment is verified. */
94
+ getSandbox: (agent: AgentMeta) => Promise<SandboxBox>;
95
+ /** Record a usage event after request completes. */
96
+ recordUsage: (event: GatewayUsageEvent) => Promise<void>;
97
+ /** x402 payment configuration */
98
+ x402: X402Config;
99
+ /** MPP (Machine Payments Protocol) configuration. If provided, gateway accepts Authorization: Payment headers. */
100
+ mpp?: MppConfig;
101
+ /**
102
+ * Verify an API key. Return key info if valid, null if invalid.
103
+ * Default: accepts any `sk_agent_*` key (demo mode).
104
+ */
105
+ verifyApiKey?: (authHeader: string) => Promise<ApiKeyInfo | null>;
106
+ /**
107
+ * Settle payment after successful response.
108
+ * For x402: call ShieldedCredits.claimPayment()
109
+ * For API key: deduct from spending limit
110
+ * Default: no-op (demo mode).
111
+ */
112
+ settlePayment?: (payment: PaymentResult, cost: number) => Promise<void>;
113
+ /** Base URL for API key purchase links (e.g. "https://film.tangle.tools") */
114
+ baseUrl?: string;
115
+ /** Max message length in chars (default: 8000) */
116
+ maxMessageLength?: number;
117
+ /** Required scope for chat endpoint (default: "chat"). API keys must include this scope. */
118
+ requiredScope?: string;
119
+ /** Block requests with detected injection patterns (default: false — log only) */
120
+ blockInjection?: boolean;
121
+ /** Rate limiting config. Default: 60 requests per 60 seconds per consumer. */
122
+ rateLimit?: {
123
+ limit: number;
124
+ windowSeconds: number;
125
+ };
126
+ /** Custom rate limit store (default: in-memory). Use KV-backed for Workers. */
127
+ rateLimitStore?: RateLimitStore;
128
+ /** Nonce replay protection store (default: in-memory). Rejects reused x402 nonces. */
129
+ nonceStore?: NonceStore;
130
+ }
131
+ interface ChatMessage {
132
+ role: 'system' | 'user' | 'assistant' | 'tool';
133
+ content: string;
134
+ }
135
+ interface ChatCompletionRequest {
136
+ model?: string;
137
+ messages: ChatMessage[];
138
+ stream?: boolean;
139
+ temperature?: number;
140
+ max_tokens?: number;
141
+ }
142
+ interface ChatCompletionChunk {
143
+ id: string;
144
+ object: 'chat.completion.chunk';
145
+ created: number;
146
+ model: string;
147
+ choices: Array<{
148
+ index: number;
149
+ delta: {
150
+ content?: string;
151
+ role?: string;
152
+ };
153
+ finish_reason: string | null;
154
+ }>;
155
+ }
156
+
157
+ export type { AgentMeta, ApiKeyInfo, ChatCompletionChunk, ChatCompletionRequest, ChatMessage, GatewayConfig, GatewayUsageEvent, MppConfig, PaymentMethod, PaymentResult, SandboxBox, SandboxStreamEvent, X402Config };
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@tangle-network/agent-gateway",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "x402 + API key payment gateway for Tangle agent products. Mount on any Hono app to expose agents as paid OpenAI-compatible endpoints.",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./dist/index.d.ts",
9
+ "import": "./dist/index.js"
10
+ },
11
+ "./middleware": {
12
+ "types": "./dist/middleware.d.ts",
13
+ "import": "./dist/middleware.js"
14
+ },
15
+ "./types": {
16
+ "types": "./dist/types.d.ts",
17
+ "import": "./dist/types.js"
18
+ }
19
+ },
20
+ "files": [
21
+ "dist",
22
+ "src"
23
+ ],
24
+ "scripts": {
25
+ "build": "tsup",
26
+ "dev": "tsup --watch",
27
+ "typecheck": "tsc --noEmit"
28
+ },
29
+ "dependencies": {
30
+ "hono": "^4.6.0"
31
+ },
32
+ "devDependencies": {
33
+ "@types/node": "^25.6.0",
34
+ "tsup": "^8.0.0",
35
+ "typescript": "^5.7.0"
36
+ }
37
+ }
@@ -0,0 +1,187 @@
1
+ /**
2
+ * API key management — create, list, verify, revoke.
3
+ *
4
+ * The gateway package provides:
5
+ * - Types and interfaces (ApiKeyStore)
6
+ * - A Hono router for CRUD (createApiKeyRoutes)
7
+ * - A verifyApiKey function that checks against the store
8
+ *
9
+ * Each agent implements ApiKeyStore against their own DB.
10
+ */
11
+
12
+ import { Hono } from 'hono'
13
+
14
+ // --- Types ---
15
+
16
+ export interface ApiKey {
17
+ id: string
18
+ userId: string
19
+ name: string
20
+ keyHash: string
21
+ keyPrefix: string
22
+ scopes: string[]
23
+ rateLimit: number // requests per minute
24
+ dailyLimit: number // requests per day
25
+ spendingLimitCents: number | null // max spend in cents (null = unlimited)
26
+ spentCents: number // running total spent
27
+ lastUsedAt: Date | null
28
+ expiresAt: Date | null
29
+ createdAt: Date
30
+ }
31
+
32
+ export interface ApiKeyCreateRequest {
33
+ name: string
34
+ scopes?: string[]
35
+ rateLimit?: number
36
+ dailyLimit?: number
37
+ spendingLimitCents?: number
38
+ expiresAt?: string
39
+ }
40
+
41
+ /** Each agent implements this against their DB */
42
+ export interface ApiKeyStore {
43
+ create(userId: string, data: {
44
+ name: string
45
+ keyHash: string
46
+ keyPrefix: string
47
+ scopes: string[]
48
+ rateLimit: number
49
+ dailyLimit: number
50
+ spendingLimitCents: number | null
51
+ expiresAt: Date | null
52
+ }): Promise<ApiKey>
53
+
54
+ list(userId: string): Promise<Omit<ApiKey, 'keyHash'>[]>
55
+
56
+ findByHash(keyHash: string): Promise<ApiKey | null>
57
+
58
+ delete(userId: string, keyId: string): Promise<boolean>
59
+
60
+ recordUsage(keyId: string, costCents: number): Promise<void>
61
+ }
62
+
63
+ // --- Key generation ---
64
+
65
+ function generateRawKey(prefix: string): string {
66
+ const bytes = new Uint8Array(16)
67
+ crypto.getRandomValues(bytes)
68
+ const hex = Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('')
69
+ return `${prefix}${hex}`
70
+ }
71
+
72
+ async function hashKey(raw: string): Promise<string> {
73
+ const encoded = new TextEncoder().encode(raw)
74
+ const digest = await crypto.subtle.digest('SHA-256', encoded)
75
+ return Array.from(new Uint8Array(digest)).map(b => b.toString(16).padStart(2, '0')).join('')
76
+ }
77
+
78
+ // --- Verification ---
79
+
80
+ export async function verifyApiKeyFromStore(
81
+ authHeader: string,
82
+ store: ApiKeyStore,
83
+ prefix = 'ak_',
84
+ ): Promise<{ key: ApiKey; keyId: string; consumerId: string; scopes: string[]; rateLimitPerMinute: number; dailyLimit: number } | null> {
85
+ const bearerPrefix = `Bearer ${prefix}`
86
+ if (!authHeader.startsWith(bearerPrefix)) return null
87
+
88
+ const rawKey = authHeader.slice(7) // strip "Bearer "
89
+ const keyHash = await hashKey(rawKey)
90
+ const key = await store.findByHash(keyHash)
91
+ if (!key) return null
92
+
93
+ // Check expiry
94
+ if (key.expiresAt && key.expiresAt < new Date()) return null
95
+
96
+ // Check spending limit
97
+ if (key.spendingLimitCents !== null && key.spentCents >= key.spendingLimitCents) return null
98
+
99
+ return {
100
+ key,
101
+ consumerId: `apikey:${key.id}`,
102
+ keyId: key.id,
103
+ scopes: key.scopes,
104
+ rateLimitPerMinute: key.rateLimit,
105
+ dailyLimit: key.dailyLimit,
106
+ }
107
+ }
108
+
109
+ // --- CRUD Routes ---
110
+
111
+ export interface ApiKeyRoutesConfig {
112
+ store: ApiKeyStore
113
+ /** Get the authenticated user ID from the request. Return null if not authenticated. */
114
+ getAuthUserId: (request: Request) => Promise<string | null>
115
+ /** Key prefix (default: "ak_") */
116
+ prefix?: string
117
+ /** Valid scopes for this agent (default: ["chat"]) */
118
+ validScopes?: string[]
119
+ }
120
+
121
+ export function createApiKeyRoutes(config: ApiKeyRoutesConfig) {
122
+ const router = new Hono()
123
+ const prefix = config.prefix ?? 'ak_'
124
+ const validScopes = config.validScopes ?? ['chat']
125
+
126
+ // List keys
127
+ router.get('/', async (c) => {
128
+ const userId = await config.getAuthUserId(c.req.raw)
129
+ if (!userId) return c.json({ error: 'Unauthorized' }, 401)
130
+
131
+ const keys = await config.store.list(userId)
132
+ return c.json({ keys })
133
+ })
134
+
135
+ // Create key
136
+ router.post('/', async (c) => {
137
+ const userId = await config.getAuthUserId(c.req.raw)
138
+ if (!userId) return c.json({ error: 'Unauthorized' }, 401)
139
+
140
+ const body = await c.req.json<ApiKeyCreateRequest>()
141
+ if (!body.name?.trim()) return c.json({ error: 'name is required' }, 400)
142
+
143
+ const scopes = (body.scopes ?? ['chat']).filter(s => validScopes.includes(s))
144
+ if (scopes.length === 0) scopes.push('chat')
145
+
146
+ const rawKey = generateRawKey(prefix)
147
+ const keyHash = await hashKey(rawKey)
148
+ const keyPrefix = rawKey.slice(0, prefix.length + 8)
149
+
150
+ const created = await config.store.create(userId, {
151
+ name: body.name.trim(),
152
+ keyHash,
153
+ keyPrefix,
154
+ scopes,
155
+ rateLimit: body.rateLimit ?? 60,
156
+ dailyLimit: body.dailyLimit ?? 1000,
157
+ spendingLimitCents: body.spendingLimitCents ?? null,
158
+ expiresAt: body.expiresAt ? new Date(body.expiresAt) : null,
159
+ })
160
+
161
+ return c.json({
162
+ key: rawKey,
163
+ id: created.id,
164
+ name: created.name,
165
+ keyPrefix: created.keyPrefix,
166
+ scopes: created.scopes,
167
+ rateLimit: created.rateLimit,
168
+ dailyLimit: created.dailyLimit,
169
+ spendingLimitCents: created.spendingLimitCents,
170
+ expiresAt: created.expiresAt,
171
+ _notice: 'Store this key securely. It will not be shown again.',
172
+ }, 201)
173
+ })
174
+
175
+ // Delete key
176
+ router.delete('/:keyId', async (c) => {
177
+ const userId = await config.getAuthUserId(c.req.raw)
178
+ if (!userId) return c.json({ error: 'Unauthorized' }, 401)
179
+
180
+ const deleted = await config.store.delete(userId, c.req.param('keyId'))
181
+ if (!deleted) return c.json({ error: 'API key not found' }, 404)
182
+
183
+ return c.json({ deleted: true })
184
+ })
185
+
186
+ return router
187
+ }
package/src/filter.ts ADDED
@@ -0,0 +1,138 @@
1
+ import type { ChatMessage } from './types'
2
+
3
+ // --- Injection detection patterns ---
4
+
5
+ const INJECTION_PATTERNS = [
6
+ // Direct instruction override
7
+ /ignore\s+(all\s+)?(previous|prior|above|earlier)\s+(instructions?|prompts?|rules?|directives?)/i,
8
+ /disregard\s+(all\s+)?(previous|prior|system)/i,
9
+ /forget\s+(everything|all|your)\s+(previous|instructions?|training)/i,
10
+ // Role assumption
11
+ /you\s+are\s+now\s+(a|an|the)\s+/i,
12
+ /pretend\s+(you\s+are|to\s+be)\s+/i,
13
+ /act\s+as\s+(if\s+you\s+are|a|an|the)\s+/i,
14
+ /new\s+instructions?:/i,
15
+ /\[system\]/i,
16
+ /\[INST\]/i,
17
+ // Prompt extraction
18
+ /what\s+(is|are)\s+your\s+(system\s+)?(prompt|instructions?|rules?|directives?)/i,
19
+ /repeat\s+(your|the)\s+(system\s+)?(prompt|instructions?)/i,
20
+ /output\s+(your|the)\s+(system\s+)?(prompt|instructions?)/i,
21
+ /show\s+me\s+(your|the)\s+(system|hidden|secret)\s+(prompt|instructions?|message)/i,
22
+ // Data exfiltration
23
+ /read\s+(the\s+)?(vault|workspace|config|secret|\.env)/i,
24
+ /cat\s+\/home\/agent\/(vault|config|\.env|secrets?)/i,
25
+ /list\s+(all\s+)?(vault|workspace|secret)\s+(files?|contents?|data)/i,
26
+ ]
27
+
28
+ // Unicode normalization — collapse homoglyphs and zero-width chars
29
+ function normalizeUnicode(text: string): string {
30
+ return text
31
+ // Remove zero-width chars (ZWJ, ZWNJ, ZWS, ZWSP)
32
+ .replace(/[\u200B-\u200F\u2028-\u202F\u2060\uFEFF]/g, '')
33
+ // Normalize to NFKC (collapses homoglyphs like а→a, е→e)
34
+ .normalize('NFKC')
35
+ }
36
+
37
+ /**
38
+ * Detect prompt injection attempts.
39
+ * Returns array of matched pattern descriptions, empty if clean.
40
+ */
41
+ export function detectInjection(content: string): string[] {
42
+ const normalized = normalizeUnicode(content)
43
+ const matches: string[] = []
44
+
45
+ for (const pattern of INJECTION_PATTERNS) {
46
+ if (pattern.test(normalized)) {
47
+ matches.push(pattern.source.slice(0, 60))
48
+ }
49
+ }
50
+
51
+ // Check for base64-encoded injection attempts
52
+ const b64Matches = normalized.match(/[A-Za-z0-9+/]{40,}={0,2}/g)
53
+ if (b64Matches) {
54
+ for (const b64 of b64Matches) {
55
+ try {
56
+ const decoded = atob(b64)
57
+ if (INJECTION_PATTERNS.some(p => p.test(decoded))) {
58
+ matches.push('base64-encoded injection')
59
+ }
60
+ } catch { /* not valid b64 */ }
61
+ }
62
+ }
63
+
64
+ return matches
65
+ }
66
+
67
+ /**
68
+ * Security boundary — filter consumer messages before forwarding to agent.
69
+ *
70
+ * Defense in depth:
71
+ * 1. Strip system messages (consumers cannot set system prompt)
72
+ * 2. Normalize Unicode (collapse homoglyphs, remove zero-width chars)
73
+ * 3. Detect injection patterns (instruction override, prompt extraction, data exfil)
74
+ * 4. Redact sensitive keywords
75
+ * 5. Cap message length
76
+ *
77
+ * Returns filtered messages and any injection warnings detected.
78
+ */
79
+ export function filterConsumerMessages(
80
+ messages: ChatMessage[],
81
+ maxLength = 8000,
82
+ ): ChatMessage[] {
83
+ return messages
84
+ .filter((m) => m.role !== 'system')
85
+ .map((m) => {
86
+ const normalized = normalizeUnicode(m.content)
87
+ const redacted = normalized
88
+ .replace(/\b(vault|workspace|owner|admin|secret|\.env|config\.json)[\s/:][^\s]*/gi, '[REDACTED]')
89
+ .slice(0, maxLength)
90
+ return { role: m.role, content: redacted }
91
+ })
92
+ }
93
+
94
+ /**
95
+ * Filter consumer messages with injection detection.
96
+ * Returns { messages, injectionWarnings }.
97
+ * If injectionWarnings is non-empty, the gateway should log and optionally reject.
98
+ */
99
+ export function filterConsumerMessagesStrict(
100
+ messages: ChatMessage[],
101
+ maxLength = 8000,
102
+ ): { messages: ChatMessage[]; injectionWarnings: string[] } {
103
+ const filtered = filterConsumerMessages(messages, maxLength)
104
+ const allContent = filtered.map(m => m.content).join(' ')
105
+ const injectionWarnings = detectInjection(allContent)
106
+ return { messages: filtered, injectionWarnings }
107
+ }
108
+
109
+ /**
110
+ * Redact system prompt content from agent output.
111
+ * Prevents the agent from leaking its own instructions in responses.
112
+ *
113
+ * Strategy: if any chunk of the system prompt appears verbatim (>40 chars)
114
+ * in the output, replace it with [REDACTED].
115
+ */
116
+ export function redactSystemPromptFromOutput(
117
+ output: string,
118
+ systemPrompt: string | undefined,
119
+ ): string {
120
+ if (!systemPrompt || systemPrompt.length < 40) return output
121
+
122
+ // Split system prompt into meaningful chunks (sentences or lines)
123
+ const chunks = systemPrompt
124
+ .split(/[.\n]/)
125
+ .map(s => s.trim())
126
+ .filter(s => s.length >= 40)
127
+
128
+ let redacted = output
129
+ for (const chunk of chunks) {
130
+ // Case-insensitive substring match
131
+ const idx = redacted.toLowerCase().indexOf(chunk.toLowerCase())
132
+ if (idx >= 0) {
133
+ redacted = redacted.slice(0, idx) + '[REDACTED — system instructions]' + redacted.slice(idx + chunk.length)
134
+ }
135
+ }
136
+
137
+ return redacted
138
+ }