@astrasyncai/verification-gateway 3.9.0 → 3.10.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/dist/adapter-interface/interface.d.mts +3 -2
- package/dist/adapter-interface/interface.d.ts +3 -2
- package/dist/adapters/express.d.mts +3 -2
- package/dist/adapters/express.d.ts +3 -2
- package/dist/adapters/express.js +151 -2
- package/dist/adapters/express.js.map +1 -1
- package/dist/adapters/express.mjs +151 -2
- package/dist/adapters/express.mjs.map +1 -1
- package/dist/adapters/mcp.d.mts +2 -1
- package/dist/adapters/mcp.d.ts +2 -1
- package/dist/adapters/mcp.js +1 -1
- package/dist/adapters/mcp.js.map +1 -1
- package/dist/adapters/mcp.mjs +1 -1
- package/dist/adapters/mcp.mjs.map +1 -1
- package/dist/adapters/nextjs.d.mts +3 -2
- package/dist/adapters/nextjs.d.ts +3 -2
- package/dist/adapters/nextjs.js +151 -2
- package/dist/adapters/nextjs.js.map +1 -1
- package/dist/adapters/nextjs.mjs +151 -2
- package/dist/adapters/nextjs.mjs.map +1 -1
- package/dist/adapters/sdk.d.mts +3 -2
- package/dist/adapters/sdk.d.ts +3 -2
- package/dist/adapters/sdk.js +1 -1
- package/dist/adapters/sdk.js.map +1 -1
- package/dist/adapters/sdk.mjs +1 -1
- package/dist/adapters/sdk.mjs.map +1 -1
- package/dist/agent/index.d.mts +3 -2
- package/dist/agent/index.d.ts +3 -2
- package/dist/bin/astrasync-claude-hook.js +276 -25
- package/dist/bin/astrasync-guard.js +258 -23
- package/dist/bin/astrasync.js +282 -24
- package/dist/browser/background.js +257 -22
- package/dist/browser/background.js.map +1 -1
- package/dist/browser/background.mjs +257 -22
- package/dist/browser/background.mjs.map +1 -1
- package/dist/browser/browser-adapter.d.mts +3 -2
- package/dist/browser/browser-adapter.d.ts +3 -2
- package/dist/claude-code/claude-code-adapter.d.mts +3 -2
- package/dist/claude-code/claude-code-adapter.d.ts +3 -2
- package/dist/cli/index.d.mts +3 -2
- package/dist/cli/index.d.ts +3 -2
- package/dist/cursor/cursor-adapter.d.mts +3 -2
- package/dist/cursor/cursor-adapter.d.ts +3 -2
- package/dist/cursor/extension.d.mts +3 -2
- package/dist/cursor/extension.d.ts +3 -2
- package/dist/cursor/extension.js +257 -22
- package/dist/cursor/extension.js.map +1 -1
- package/dist/cursor/extension.mjs +257 -22
- package/dist/cursor/extension.mjs.map +1 -1
- package/dist/edge-config.d.mts +2 -1
- package/dist/edge-config.d.ts +2 -1
- package/dist/{express-sGXhDdd0.d.ts → express-ClNtDhhc.d.ts} +1 -1
- package/dist/{express-DXUyvv7t.d.mts → express-DCXa1oqj.d.mts} +1 -1
- package/dist/gateway/gateway.d.mts +29 -3
- package/dist/gateway/gateway.d.ts +29 -3
- package/dist/gateway/gateway.js +257 -22
- package/dist/gateway/gateway.js.map +1 -1
- package/dist/gateway/gateway.mjs +257 -22
- package/dist/gateway/gateway.mjs.map +1 -1
- package/dist/git-trigger/git-hooks.d.mts +3 -2
- package/dist/git-trigger/git-hooks.d.ts +3 -2
- package/dist/{index-Dalih3EB.d.mts → index-BrNKDmiR.d.mts} +1 -1
- package/dist/{index-C61VL_OW.d.ts → index-CfHurO1P.d.ts} +1 -1
- package/dist/{index-CMg-vjEK.d.ts → index-D2ztkumS.d.ts} +1 -1
- package/dist/{index-DmmdehNu.d.mts → index-Dr_XiN65.d.mts} +1 -1
- package/dist/index.d.mts +10 -9
- package/dist/index.d.ts +10 -9
- package/dist/index.js +218 -11
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +211 -11
- package/dist/index.mjs.map +1 -1
- package/dist/local-evaluator/evaluator.d.mts +3 -2
- package/dist/local-evaluator/evaluator.d.ts +3 -2
- package/dist/local-evaluator/evaluator.js +165 -7
- package/dist/local-evaluator/evaluator.js.map +1 -1
- package/dist/local-evaluator/evaluator.mjs +165 -7
- package/dist/local-evaluator/evaluator.mjs.map +1 -1
- package/dist/metadata-capture.d.mts +87 -0
- package/dist/metadata-capture.d.ts +87 -0
- package/dist/metadata-capture.js +184 -0
- package/dist/metadata-capture.js.map +1 -0
- package/dist/metadata-capture.mjs +154 -0
- package/dist/metadata-capture.mjs.map +1 -0
- package/dist/{nextjs-DwUW-Nm1.d.ts → nextjs-CEBVBsdi.d.ts} +1 -1
- package/dist/{nextjs-F_2pbPIA.d.mts → nextjs-DcQIk6Zj.d.mts} +1 -1
- package/dist/platform-signatures.d.mts +60 -10
- package/dist/platform-signatures.d.ts +60 -10
- package/dist/platform-signatures.js +37 -10
- package/dist/platform-signatures.js.map +1 -1
- package/dist/platform-signatures.mjs +35 -9
- package/dist/platform-signatures.mjs.map +1 -1
- package/dist/{sdk-Clob2-0A.d.mts → sdk-CZkbSOeO.d.mts} +1 -1
- package/dist/{sdk-CfHANs4A.d.ts → sdk-H0JCLW2y.d.ts} +1 -1
- package/dist/transport/index.d.mts +3 -2
- package/dist/transport/index.d.ts +3 -2
- package/dist/{types-ACMsV07B.d.mts → types-9tns4wyl.d.ts} +12 -0
- package/dist/{types-DmSDgPpG.d.ts → types-BV6abBnF.d.mts} +7 -1
- package/dist/{types-BtqUvK8v.d.mts → types-C23_uaY8.d.ts} +7 -1
- package/dist/{types-ACMsV07B.d.ts → types-sy3tq8lp.d.mts} +12 -0
- package/dist/ui/index.d.mts +2 -1
- package/dist/ui/index.d.ts +2 -1
- package/dist/verify.d.mts +8 -1
- package/dist/verify.d.ts +8 -1
- package/dist/verify.js +1 -1
- package/dist/verify.js.map +1 -1
- package/dist/verify.mjs +1 -1
- package/dist/verify.mjs.map +1 -1
- package/package.json +6 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/gateway/types.ts","../../src/local-evaluator/evaluator.ts"],"sourcesContent":["/**\n * AstraSync Gateway - Types for gateway modes, local evaluation, and adapter interface.\n */\n\n// ========================================================================\n// Gateway Configuration\n// ========================================================================\n\nexport type GatewayMode = 'online' | 'local' | 'hybrid';\n\n/**\n * Posture controls whether the gateway actively blocks or just monitors.\n * - active: Evaluate and enforce decisions (block/allow/review)\n * - passive: Evaluate and log but never block (telemetry-only mode)\n */\nexport type GatewayPosture = 'active' | 'passive';\n\nexport interface AstraSyncGatewayConfig {\n mode: GatewayMode;\n /** Enforcement posture: 'active' blocks actions, 'passive' logs only (default: 'active') */\n posture?: GatewayPosture;\n /** AstraSync API base URL (required for online/hybrid modes) */\n apiBaseUrl?: string;\n /** API key for authenticating with AstraSync (required for online/hybrid modes) */\n apiKey?: string;\n /** Path to local PDLSS policy YAML file (required for local/hybrid modes) */\n policyFile?: string;\n /** Inline policy object (alternative to policyFile) */\n policy?: LocalPolicy;\n /** Sync interval in seconds for hybrid mode (default: 3600) */\n syncInterval?: number;\n /** Cache verification results TTL in seconds (default: 300) */\n cacheTtl?: number;\n /** Enable debug logging */\n debug?: boolean;\n /** Enable trace logging to .astrasync/traces/ (default: false) */\n traceEnabled?: boolean;\n /** Trace log directory (default: .astrasync/traces/) */\n tracePath?: string;\n /** Default access level for unverified requests */\n defaultAccessLevel?: import('../types').AccessLevel;\n /** Minimum trust score for standard access (online/hybrid) */\n minTrustScore?: number;\n /** Minimum trust score for full access (online/hybrid) */\n minTrustScoreForFull?: number;\n /** Custom headers to send with API requests */\n customHeaders?: Record<string, string>;\n /** Counterparty URL for analytics */\n counterpartyUrl?: string;\n /** Counterparty type for analytics */\n counterpartyType?: import('../types').CounterpartyType;\n}\n\n// ========================================================================\n// PDLSS Context (Agent-side action context)\n// ========================================================================\n\nexport interface PDLSSContext {\n /** Purpose category (e.g. email.send, shell.exec, file.read) */\n purpose: string;\n /** Specific action within purpose */\n action: string;\n /** Target resource, recipient, or counterparty */\n target: string;\n /** Types of data access (read, write, delete) */\n dataAccess?: string[];\n /** Network domains/IPs being accessed */\n networkAccess?: string[];\n /** Resource type (customer, order, file, directory, process) */\n resourceType?: string;\n /** Risk factors for this action */\n riskFactors?: RiskFactor[];\n /** Transaction value (if financial) */\n transactionValue?: number;\n /** Currency for transaction */\n currency?: string;\n /** Additional metadata */\n metadata?: Record<string, unknown>;\n}\n\nexport interface RiskFactor {\n type: 'financial' | 'data_sensitivity' | 'privilege_escalation' | 'network_scope' | 'destructive';\n severity: 'low' | 'medium' | 'high' | 'critical';\n detail: string;\n}\n\n// ========================================================================\n// Verification Decision\n// ========================================================================\n\nexport interface VerificationDecision {\n recommendation: 'ALLOW' | 'DENY' | 'MANUAL_REVIEW';\n reason: string;\n trustScore?: number;\n tokenGuidance?: import('../types').TokenGuidance;\n sessionId?: string;\n /** PDLSS dimensions that were evaluated */\n evaluatedDimensions?: {\n purpose: boolean;\n scope: boolean;\n limits: boolean;\n riskThresholds: boolean;\n };\n}\n\n// ========================================================================\n// Local Policy Types (YAML format)\n// ========================================================================\n\nexport interface LocalPolicy {\n version: string;\n name: string;\n description?: string;\n purposes: LocalPurposeRule[];\n scope?: LocalScope;\n limits?: LocalLimits;\n riskThresholds?: LocalRiskThresholds;\n selfInstantiation?: LocalSelfInstantiation;\n}\n\nexport interface LocalPurposeRule {\n id: string;\n allowed: boolean;\n targets?: string[];\n blockedPatterns?: string[];\n requiresApproval?: boolean;\n}\n\nexport interface LocalScope {\n allowedDomains?: string[];\n blockedDomains?: string[];\n blockedResources?: string[];\n}\n\nexport interface LocalLimits {\n maxTransactionAmount?: number;\n maxRequestsPerHour?: number;\n currency?: string;\n}\n\nexport interface LocalRiskThresholds {\n autoAllow: { min: number; max: number };\n requireApproval: { min: number; max: number };\n autoBlock: { min: number; max: number };\n}\n\nexport interface LocalSelfInstantiation {\n /** Whether sub-agent spawning is allowed */\n allowed: boolean;\n /** Maximum depth of sub-agent chain */\n maxDepth?: number;\n}\n\n// ========================================================================\n// Risk Scoring Defaults (cherry-picked from trust-harness-core)\n// ========================================================================\n\n/** Base risk scores per action category */\nexport const BASE_RISK_SCORES: Record<string, number> = {\n 'file.read': 10,\n 'file.write': 40,\n 'file.delete': 70,\n 'shell.exec': 50,\n 'network.fetch': 60,\n 'network.request': 60,\n 'email.send': 45,\n 'email.read': 15,\n 'calendar.create': 20,\n 'calendar.modify': 30,\n 'database.query': 25,\n 'database.write': 55,\n 'payment.execute': 80,\n 'sub_agent.spawn': 65,\n 'code.execute': 45,\n};\n\n/** Shell commands that significantly increase risk score */\nexport const HIGH_RISK_COMMANDS = [\n 'rm', 'rmdir', 'dd', 'mkfs', 'chmod', 'chown',\n 'sudo', 'su', 'curl', 'wget', 'nc', 'netcat',\n 'ssh', 'scp', 'rsync', 'git push', 'npm publish',\n 'docker', 'kubectl',\n];\n\n/** File paths that indicate sensitive data access */\nexport const SENSITIVE_PATHS = [\n '.ssh', '.aws', '.gnupg', '.env', 'credentials',\n 'secrets', 'password', '.git/config', '/etc', '/var', '/root',\n 'id_rsa', '.npmrc', '.pypirc',\n];\n\n// ========================================================================\n// Trace Event Types\n// ========================================================================\n\nexport interface TraceEvent {\n id: string;\n timestamp: Date;\n type: 'evaluation' | 'decision' | 'error' | 'mode_switch';\n context?: PDLSSContext;\n decision?: VerificationDecision;\n metadata?: Record<string, unknown>;\n}\n\n// ========================================================================\n// Adapter Interface Types\n// ========================================================================\n\nexport interface AdapterConfig {\n /** The gateway instance (handles mode routing) */\n gateway: unknown; // Typed as AstraSyncGateway at usage site to avoid circular deps\n /** Platform-specific configuration */\n adapterOptions: Record<string, unknown>;\n}\n\nexport interface AgentAction {\n /** Raw action data from the platform */\n raw: unknown;\n /** Platform identifier (e.g. 'openclaw-cli', 'cursor', 'browser') */\n platform: string;\n /** Timestamp of the action */\n timestamp: Date;\n}\n\nexport interface InterceptResult {\n /** Whether the action was intercepted */\n intercepted: boolean;\n /** Extracted PDLSS context (if intercepted) */\n context?: PDLSSContext;\n /** Reason for not intercepting (if not intercepted) */\n skipReason?: string;\n}\n\n// ========================================================================\n// Sync Queue Types (Hybrid mode)\n// ========================================================================\n\nexport interface SyncQueueEntry {\n id: string;\n context: PDLSSContext;\n decision: VerificationDecision;\n timestamp: Date;\n retryCount: number;\n status: 'pending' | 'synced' | 'failed';\n}\n","/**\n * Local PDLSS Evaluator\n *\n * Evaluates agent actions against a local PDLSS policy.\n * Same logic as the cloud evaluator but runs in-process with no I/O.\n *\n * Evaluation order:\n * 1. Purpose: find matching rule, check if allowed\n * 2. Purpose: check allowed targets (if specified)\n * 3. Purpose: check blocked patterns (if specified)\n * 4. Scope: check global blocked resources/domains\n * 5. Limits: check transaction/rate limits\n * 6. Risk thresholds + approval requirements\n */\n\nimport type {\n LocalPolicy,\n PDLSSContext,\n VerificationDecision,\n LocalPurposeRule,\n} from '../gateway/types';\nimport { HIGH_RISK_COMMANDS, SENSITIVE_PATHS } from '../gateway/types';\n\nexport class LocalEvaluator {\n private policy: LocalPolicy;\n private requestCounts: Map<string, { count: number; windowStart: number }> = new Map();\n\n constructor(policy: LocalPolicy) {\n this.policy = policy;\n }\n\n /**\n * Update the policy (e.g. after hot-reload or sync).\n */\n updatePolicy(policy: LocalPolicy): void {\n this.policy = policy;\n }\n\n /**\n * Evaluate an action context against the loaded policy.\n */\n evaluate(context: PDLSSContext): VerificationDecision {\n // 1. Purpose: find matching rule\n const purposeRule = this.policy.purposes.find((p) => p.id === context.purpose);\n if (!purposeRule) {\n return { recommendation: 'DENY', reason: 'Purpose not in policy' };\n }\n if (!purposeRule.allowed) {\n return { recommendation: 'DENY', reason: 'Purpose explicitly blocked' };\n }\n\n // 2. Purpose: check allowed targets\n if (purposeRule.targets && !this.matchesAnyPattern(context.target, purposeRule.targets)) {\n return { recommendation: 'DENY', reason: 'Target not in allowed list' };\n }\n\n // 3. Purpose: check blocked patterns\n if (\n purposeRule.blockedPatterns &&\n this.matchesAnyPattern(context.target, purposeRule.blockedPatterns)\n ) {\n return { recommendation: 'DENY', reason: 'Target matches blocked pattern' };\n }\n\n // 4. Scope: check global blocked resources/domains\n const scopeBlock = this.checkScopeBlock(context);\n if (scopeBlock) {\n return { recommendation: 'DENY', reason: scopeBlock };\n }\n\n // 5. Limits: check transaction and rate limits\n const limitViolation = this.checkLimits(context);\n if (limitViolation) {\n return { recommendation: 'DENY', reason: limitViolation };\n }\n\n // 6. Self-instantiation: check sub-agent spawning rules\n if (context.purpose === 'sub_agent.spawn' && this.policy.selfInstantiation) {\n if (!this.policy.selfInstantiation.allowed) {\n return { recommendation: 'DENY', reason: 'Sub-agent spawning is not allowed' };\n }\n const depth = (context.metadata?.subAgentDepth as number) || 0;\n if (\n this.policy.selfInstantiation.maxDepth !== undefined &&\n depth >= this.policy.selfInstantiation.maxDepth\n ) {\n return {\n recommendation: 'DENY',\n reason: `Sub-agent depth ${depth} exceeds max depth ${this.policy.selfInstantiation.maxDepth}`,\n };\n }\n }\n\n // 7. Risk thresholds + approval requirements\n if (purposeRule.requiresApproval) {\n return { recommendation: 'MANUAL_REVIEW', reason: 'Purpose requires approval' };\n }\n\n const riskDecision = this.checkRiskThresholds(context);\n if (riskDecision) {\n return riskDecision;\n }\n\n return {\n recommendation: 'ALLOW',\n reason: 'All PDLSS checks passed',\n evaluatedDimensions: {\n purpose: true,\n scope: !!this.policy.scope,\n limits: !!this.policy.limits,\n riskThresholds: !!this.policy.riskThresholds,\n },\n };\n }\n\n private checkScopeBlock(context: PDLSSContext): string | null {\n const scope = this.policy.scope;\n if (!scope) return null;\n\n // Check blocked domains against target and network access\n if (scope.blockedDomains) {\n const targetDomain = this.extractDomain(context.target);\n if (this.matchesAnyPattern(targetDomain, scope.blockedDomains)) {\n return `Target blocked by scope: ${context.target}`;\n }\n if (context.networkAccess) {\n for (const domain of context.networkAccess) {\n if (this.matchesAnyPattern(this.extractDomain(domain), scope.blockedDomains)) {\n return `Domain blocked by scope: ${domain}`;\n }\n }\n }\n }\n\n // Check blocked resources against target\n if (scope.blockedResources && this.matchesAnyPattern(context.target, scope.blockedResources)) {\n return `Resource blocked by scope: ${context.target}`;\n }\n\n // Check allowed domains (if specified, target must match)\n if (scope.allowedDomains && context.networkAccess) {\n for (const domain of context.networkAccess) {\n if (!this.matchesAnyPattern(this.extractDomain(domain), scope.allowedDomains)) {\n return `Domain not in allowed list: ${domain}`;\n }\n }\n }\n\n return null;\n }\n\n private checkLimits(context: PDLSSContext): string | null {\n const limits = this.policy.limits;\n if (!limits) return null;\n\n // Transaction amount check\n if (limits.maxTransactionAmount !== undefined && context.transactionValue !== undefined) {\n if (context.transactionValue > limits.maxTransactionAmount) {\n return `Transaction value ${context.transactionValue} exceeds limit ${limits.maxTransactionAmount}`;\n }\n }\n\n // Rate limit check\n if (limits.maxRequestsPerHour !== undefined) {\n const key = context.purpose;\n const now = Date.now();\n const entry = this.requestCounts.get(key);\n const hourMs = 3600000;\n\n if (!entry || now - entry.windowStart > hourMs) {\n this.requestCounts.set(key, { count: 1, windowStart: now });\n } else {\n entry.count++;\n if (entry.count > limits.maxRequestsPerHour) {\n return `Rate limit exceeded: ${entry.count}/${limits.maxRequestsPerHour} requests per hour`;\n }\n }\n }\n\n return null;\n }\n\n private checkRiskThresholds(context: PDLSSContext): VerificationDecision | null {\n if (!this.policy.riskThresholds) return null;\n\n const riskScore = this.calculateRiskScore(context);\n const thresholds = this.policy.riskThresholds;\n\n if (riskScore >= thresholds.autoBlock.min) {\n return { recommendation: 'DENY', reason: `Risk score ${riskScore} exceeds block threshold` };\n }\n\n if (riskScore >= thresholds.requireApproval.min) {\n return {\n recommendation: 'MANUAL_REVIEW',\n reason: `Risk score ${riskScore} requires approval`,\n };\n }\n\n return null;\n }\n\n private calculateRiskScore(context: PDLSSContext): number {\n let score = 0;\n\n // Explicit risk factors take priority (highest severity wins)\n if (context.riskFactors?.length) {\n const severityScores: Record<string, number> = {\n low: 10,\n medium: 40,\n high: 70,\n critical: 90,\n };\n\n for (const factor of context.riskFactors) {\n const factorScore = severityScores[factor.severity] || 0;\n if (factorScore > score) score = factorScore;\n }\n return score;\n }\n\n // Auto-detect risk from high-risk shell commands\n if (context.purpose === 'shell.exec' && context.target) {\n const targetLower = context.target.toLowerCase();\n for (const cmd of HIGH_RISK_COMMANDS) {\n if (\n targetLower.startsWith(cmd) ||\n targetLower.includes(` ${cmd} `) ||\n targetLower.includes(` ${cmd}`)\n ) {\n score = Math.max(score, 80);\n break;\n }\n }\n }\n\n // Auto-detect risk from sensitive file paths (score 50 = review range)\n if (\n (context.purpose === 'file.read' ||\n context.purpose === 'file.write' ||\n context.purpose === 'file.delete') &&\n context.target\n ) {\n const targetLower = context.target.toLowerCase();\n for (const sensitivePath of SENSITIVE_PATHS) {\n if (targetLower.includes(sensitivePath.toLowerCase())) {\n score = Math.max(score, 50);\n break;\n }\n }\n }\n\n return score;\n }\n\n /**\n * Extract domain from a URL-like string (strips protocol and path).\n */\n private extractDomain(value: string): string {\n let domain = value;\n // Strip protocol\n const protoIndex = domain.indexOf('://');\n if (protoIndex !== -1) domain = domain.slice(protoIndex + 3);\n // Strip path\n const slashIndex = domain.indexOf('/');\n if (slashIndex !== -1) domain = domain.slice(0, slashIndex);\n // Strip port\n const colonIndex = domain.indexOf(':');\n if (colonIndex !== -1) domain = domain.slice(0, colonIndex);\n return domain;\n }\n\n /**\n * Check if a value matches any of the given glob patterns.\n */\n private matchesAnyPattern(value: string, patterns: string[]): boolean {\n return patterns.some((pattern) => this.matchGlob(value, pattern));\n }\n\n /**\n * Simple glob pattern matching.\n * Supports * (matches any characters including / and spaces) and ? (single char).\n * Uses the same approach as the backend's matchGlobPattern.\n */\n private matchGlob(value: string, pattern: string): boolean {\n // Exact match\n if (pattern === value) return true;\n\n // Audit F-A1-13: ReDoS guard. Patterns with too many `*` segments\n // produce catastrophic-backtracking regexes (e.g. `a*a*a*a*a*a*a*a*b`\n // against `aaaaaaaa` runs in exponential time). Local policies are\n // operator-controlled so attacker access requires operator-side\n // compromise, but a copy-pasted-from-internet example pattern could\n // still DoS the gateway when matched against request input.\n const starCount = (pattern.match(/\\*/g) ?? []).length;\n if (starCount > 8) {\n return false;\n }\n\n // Convert glob to regex\n const regexStr = pattern\n .replace(/[.+^${}()|[\\]\\\\]/g, '\\\\$&') // Escape special regex chars (except * and ?)\n .replace(/\\*/g, '.*') // * matches anything\n .replace(/\\?/g, '.'); // ? matches single char\n\n try {\n return new RegExp(`^${regexStr}$`, 'i').test(value);\n } catch {\n return false;\n }\n }\n}\n\n/**\n * Convenience: find a matching purpose rule.\n */\nexport function findPurposeRule(\n policy: LocalPolicy,\n purposeId: string\n): LocalPurposeRule | undefined {\n return policy.purposes.find((p) => p.id === purposeId);\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/gateway/types.ts","../../src/local-evaluator/command-matcher.ts","../../src/local-evaluator/evaluator.ts"],"sourcesContent":["/**\n * AstraSync Gateway - Types for gateway modes, local evaluation, and adapter interface.\n */\n\n// ========================================================================\n// Gateway Configuration\n// ========================================================================\n\nexport type GatewayMode = 'online' | 'local' | 'hybrid';\n\n/**\n * Posture controls whether the gateway actively blocks or just monitors.\n * - active: Evaluate and enforce decisions (block/allow/review)\n * - passive: Evaluate and log but never block (telemetry-only mode)\n */\nexport type GatewayPosture = 'active' | 'passive';\n\nexport interface AstraSyncGatewayConfig {\n mode: GatewayMode;\n /** Enforcement posture: 'active' blocks actions, 'passive' logs only (default: 'active') */\n posture?: GatewayPosture;\n /** AstraSync API base URL (required for online/hybrid modes) */\n apiBaseUrl?: string;\n /** API key for authenticating with AstraSync (required for online/hybrid modes) */\n apiKey?: string;\n /**\n * ASTRA- agent id this gateway acts as. Attached to hybrid sync entries and\n * used as the default identity for online verification when the caller\n * passes no per-call astraId.\n */\n agentId?: string;\n /** Path to local PDLSS policy YAML file (required for local/hybrid modes) */\n policyFile?: string;\n /** Inline policy object (alternative to policyFile) */\n policy?: LocalPolicy;\n /** Sync interval in seconds for hybrid mode (default: 3600) */\n syncInterval?: number;\n /** Cache verification results TTL in seconds (default: 300) */\n cacheTtl?: number;\n /** Enable debug logging */\n debug?: boolean;\n /** Enable trace logging to .astrasync/traces/ (default: false) */\n traceEnabled?: boolean;\n /** Trace log directory (default: .astrasync/traces/) */\n tracePath?: string;\n /** Default access level for unverified requests */\n defaultAccessLevel?: import('../types').AccessLevel;\n /** Minimum trust score for standard access (online/hybrid) */\n minTrustScore?: number;\n /** Minimum trust score for full access (online/hybrid) */\n minTrustScoreForFull?: number;\n /** Custom headers to send with API requests */\n customHeaders?: Record<string, string>;\n /** Counterparty URL for analytics */\n counterpartyUrl?: string;\n /** Counterparty type for analytics */\n counterpartyType?: import('../types').CounterpartyType;\n}\n\n// ========================================================================\n// PDLSS Context (Agent-side action context)\n// ========================================================================\n\nexport interface PDLSSContext {\n /** Purpose category (e.g. email.send, shell.exec, file.read) */\n purpose: string;\n /** Specific action within purpose */\n action: string;\n /** Target resource, recipient, or counterparty */\n target: string;\n /** Types of data access (read, write, delete) */\n dataAccess?: string[];\n /** Network domains/IPs being accessed */\n networkAccess?: string[];\n /** Resource type (customer, order, file, directory, process) */\n resourceType?: string;\n /** Risk factors for this action */\n riskFactors?: RiskFactor[];\n /** Transaction value (if financial) */\n transactionValue?: number;\n /** Currency for transaction */\n currency?: string;\n /** Additional metadata */\n metadata?: Record<string, unknown>;\n}\n\nexport interface RiskFactor {\n type: 'financial' | 'data_sensitivity' | 'privilege_escalation' | 'network_scope' | 'destructive';\n severity: 'low' | 'medium' | 'high' | 'critical';\n detail: string;\n}\n\n// ========================================================================\n// Verification Decision\n// ========================================================================\n\nexport interface VerificationDecision {\n recommendation: 'ALLOW' | 'DENY' | 'MANUAL_REVIEW';\n reason: string;\n trustScore?: number;\n tokenGuidance?: import('../types').TokenGuidance;\n sessionId?: string;\n /** PDLSS dimensions that were evaluated */\n evaluatedDimensions?: {\n purpose: boolean;\n scope: boolean;\n limits: boolean;\n riskThresholds: boolean;\n };\n}\n\n// ========================================================================\n// Local Policy Types (YAML format)\n// ========================================================================\n\nexport interface LocalPolicy {\n version: string;\n name: string;\n description?: string;\n purposes: LocalPurposeRule[];\n scope?: LocalScope;\n limits?: LocalLimits;\n riskThresholds?: LocalRiskThresholds;\n selfInstantiation?: LocalSelfInstantiation;\n}\n\nexport interface LocalPurposeRule {\n id: string;\n allowed: boolean;\n targets?: string[];\n blockedPatterns?: string[];\n requiresApproval?: boolean;\n}\n\nexport interface LocalScope {\n allowedDomains?: string[];\n blockedDomains?: string[];\n blockedResources?: string[];\n}\n\nexport interface LocalLimits {\n maxTransactionAmount?: number;\n maxRequestsPerHour?: number;\n currency?: string;\n}\n\nexport interface LocalRiskThresholds {\n autoAllow: { min: number; max: number };\n requireApproval: { min: number; max: number };\n autoBlock: { min: number; max: number };\n}\n\nexport interface LocalSelfInstantiation {\n /** Whether sub-agent spawning is allowed */\n allowed: boolean;\n /** Maximum depth of sub-agent chain */\n maxDepth?: number;\n}\n\n// ========================================================================\n// Risk Scoring Defaults (cherry-picked from trust-harness-core)\n// ========================================================================\n\n/** Base risk scores per action category */\nexport const BASE_RISK_SCORES: Record<string, number> = {\n 'file.read': 10,\n 'file.write': 40,\n 'file.delete': 70,\n 'shell.exec': 50,\n 'network.fetch': 60,\n 'network.request': 60,\n 'email.send': 45,\n 'email.read': 15,\n 'calendar.create': 20,\n 'calendar.modify': 30,\n 'database.query': 25,\n 'database.write': 55,\n 'payment.execute': 80,\n 'sub_agent.spawn': 65,\n 'code.execute': 45,\n};\n\n/** Shell commands that significantly increase risk score */\nexport const HIGH_RISK_COMMANDS = [\n 'rm',\n 'rmdir',\n 'dd',\n 'mkfs',\n 'chmod',\n 'chown',\n 'sudo',\n 'su',\n 'curl',\n 'wget',\n 'nc',\n 'netcat',\n 'ssh',\n 'scp',\n 'rsync',\n 'git push',\n 'npm publish',\n 'docker',\n 'kubectl',\n 'gh pr create',\n 'gh pr merge',\n];\n\n/** File paths that indicate sensitive data access */\nexport const SENSITIVE_PATHS = [\n '.ssh',\n '.aws',\n '.gnupg',\n '.env',\n 'credentials',\n 'secrets',\n 'password',\n '.git/config',\n '/etc',\n '/var',\n '/root',\n 'id_rsa',\n '.npmrc',\n '.pypirc',\n];\n\n// ========================================================================\n// Trace Event Types\n// ========================================================================\n\nexport interface TraceEvent {\n id: string;\n timestamp: Date;\n type: 'evaluation' | 'decision' | 'error' | 'mode_switch';\n context?: PDLSSContext;\n decision?: VerificationDecision;\n metadata?: Record<string, unknown>;\n}\n\n// ========================================================================\n// Adapter Interface Types\n// ========================================================================\n\nexport interface AdapterConfig {\n /** The gateway instance (handles mode routing) */\n gateway: unknown; // Typed as AstraSyncGateway at usage site to avoid circular deps\n /** Platform-specific configuration */\n adapterOptions: Record<string, unknown>;\n}\n\nexport interface AgentAction {\n /** Raw action data from the platform */\n raw: unknown;\n /** Platform identifier (e.g. 'openclaw-cli', 'cursor', 'browser') */\n platform: string;\n /** Timestamp of the action */\n timestamp: Date;\n}\n\nexport interface InterceptResult {\n /** Whether the action was intercepted */\n intercepted: boolean;\n /** Extracted PDLSS context (if intercepted) */\n context?: PDLSSContext;\n /** Reason for not intercepting (if not intercepted) */\n skipReason?: string;\n}\n\n// ========================================================================\n// Sync Queue Types (Hybrid mode)\n// ========================================================================\n\nexport interface SyncQueueEntry {\n id: string;\n context: PDLSSContext;\n decision: VerificationDecision;\n timestamp: Date;\n retryCount: number;\n status: 'pending' | 'synced' | 'failed';\n}\n","/**\n * Command-position matching for high-risk shell commands.\n *\n * Replaces the substring-based HIGH_RISK_COMMANDS check that flagged any\n * command merely *containing* a high-risk name (` su` matched \"superadmin\",\n * quoted text and heredoc bodies triggered). A high-risk entry now matches\n * only when it is the invoked command of some pipeline/list segment.\n *\n * Deliberately NOT a shell parser — pure regex/token splitting inside the\n * hook's latency budget. Known accepted misses: option arguments that survive\n * flag filtering (`git -C /tmp push`), exotic quoting/escapes.\n */\n\nimport { HIGH_RISK_COMMANDS } from '../gateway/types';\n\n/** Wrapper commands that prefix the real command; hop over them and re-check. */\nconst WRAPPERS = new Set([\n 'sudo',\n 'doas',\n 'env',\n 'nohup',\n 'time',\n 'timeout',\n 'nice',\n 'ionice',\n 'stdbuf',\n 'xargs',\n 'command',\n 'exec',\n 'builtin',\n]);\n\n/** Leading environment assignment, e.g. `FOO=bar cmd` (input is lowercased). */\nconst ASSIGN_RE = /^[a-z_][a-z0-9_]*=/;\n\nconst MAX_WRAPPER_HOPS = 4;\nconst MAX_RECURSION_DEPTH = 3;\nconst MAX_COMMAND_LENGTH = 100_000;\n\n/** Hosts considered local for the curl/wget risk downgrade (brackets pre-stripped). */\nconst LOCAL_HOST_RE = /^(localhost|127\\.0\\.0\\.1|::1|[a-z0-9.-]*\\.localhost)$/;\n\ninterface RiskList {\n singleWord: Set<string>;\n /** Sorted longest-first so `gh pr create` wins over a shorter prefix entry. */\n multiWord: string[][];\n}\n\nfunction buildRiskList(entries: readonly string[]): RiskList {\n const singleWord = new Set<string>();\n const multiWord: string[][] = [];\n for (const entry of entries) {\n const words = entry.toLowerCase().trim().split(/\\s+/);\n if (words.length === 1) singleWord.add(words[0]);\n else multiWord.push(words);\n }\n multiWord.sort((a, b) => b.length - a.length);\n return { singleWord, multiWord };\n}\n\n/** Drop heredoc bodies (`<<TAG` … `TAG`) — stdin text, not executed commands. */\nfunction stripHeredocBodies(text: string): string {\n const lines = text.split('\\n');\n const kept: string[] = [];\n let terminator: string | null = null;\n for (const line of lines) {\n if (terminator !== null) {\n if (line.trim() === terminator) terminator = null;\n continue;\n }\n kept.push(line);\n const heredoc = line.match(/<<-?\\s*['\"]?(\\w+)['\"]?/);\n if (heredoc) terminator = heredoc[1];\n }\n return kept.join('\\n');\n}\n\n/**\n * Pull out `$(…)` command substitutions (recursing into their contents via\n * `collect`) and blank their spans. No quote awareness inside the parens —\n * pragmatic depth counting only.\n */\nfunction extractDollarSubstitutions(text: string, collect: (inner: string) => void): string {\n let result = text;\n let start = result.indexOf('$(');\n let iterations = 0;\n while (start !== -1 && iterations < 50) {\n iterations++;\n let depth = 0;\n let end = result.length;\n for (let i = start + 1; i < result.length; i++) {\n if (result[i] === '(') depth++;\n else if (result[i] === ')') {\n depth--;\n if (depth === 0) {\n end = i;\n break;\n }\n }\n }\n collect(result.slice(start + 2, end));\n result = `${result.slice(0, start)} ${result.slice(Math.min(end + 1, result.length))}`;\n start = result.indexOf('$(');\n }\n return result;\n}\n\n/** Pull out backtick substitutions (odd chunks when split on `) and blank them. */\nfunction extractBacktickSubstitutions(text: string, collect: (inner: string) => void): string {\n if (!text.includes('`')) return text;\n const chunks = text.split('`');\n const kept: string[] = [];\n for (let i = 0; i < chunks.length; i++) {\n if (i % 2 === 1) collect(chunks[i]);\n else kept.push(chunks[i]);\n }\n return kept.join(' ');\n}\n\nfunction basename(token: string): string {\n const slash = token.lastIndexOf('/');\n return slash === -1 ? token : token.slice(slash + 1);\n}\n\nfunction matchSegment(segment: string, list: RiskList): string | null {\n const tokens = segment.split(/\\s+/).filter((t) => /[a-z0-9]/.test(t));\n let i = 0;\n let hops = 0;\n while (i < tokens.length) {\n // Skip `export` and leading env assignments to reach the command position.\n while (i < tokens.length && (tokens[i] === 'export' || ASSIGN_RE.test(tokens[i]))) i++;\n if (i >= tokens.length) return null;\n\n const cmd = basename(tokens[i]);\n\n // Multi-word entries match against the leading non-flag tokens.\n const significant = [cmd, ...tokens.slice(i + 1).filter((t) => !t.startsWith('-'))];\n for (const entry of list.multiWord) {\n if (entry.length <= significant.length && entry.every((w, j) => significant[j] === w)) {\n return entry.join(' ');\n }\n }\n if (list.singleWord.has(cmd)) return cmd;\n\n // High-risk check ran first, so `sudo …` matches `sudo` before hopping.\n if (WRAPPERS.has(cmd) && hops < MAX_WRAPPER_HOPS) {\n hops++;\n i++;\n while (i < tokens.length && (tokens[i].startsWith('-') || ASSIGN_RE.test(tokens[i]))) i++;\n continue;\n }\n return null;\n }\n return null;\n}\n\nfunction collectMatches(\n command: string,\n list: RiskList,\n matches: Set<string>,\n depth: number\n): void {\n if (depth > MAX_RECURSION_DEPTH || command.length > MAX_COMMAND_LENGTH) return;\n\n let text = command.toLowerCase();\n text = stripHeredocBodies(text);\n\n // Substitutions are extracted (and their contents checked) BEFORE quote\n // stripping so `\"$(curl x)\"` is still evaluated in command position.\n const recurse = (inner: string) => collectMatches(inner, list, matches, depth + 1);\n text = extractDollarSubstitutions(text, recurse);\n text = extractBacktickSubstitutions(text, recurse);\n\n // Quoted spans are data, not commands; `#` comments are noise.\n text = text.replace(/'[^']*'/g, ' ').replace(/\"[^\"]*\"/g, ' ');\n text = text.replace(/(^|\\s)#[^\\n]*/g, ' ');\n\n const segments = text.split(/\\|\\||&&|[;|&\\n]|[({]/);\n for (const segment of segments) {\n const matched = matchSegment(segment, list);\n if (matched) matches.add(matched);\n }\n}\n\n/**\n * Returns every high-risk entry invoked in command position anywhere in\n * `command` (deduplicated), or an empty array.\n */\nexport function matchHighRiskCommands(\n command: string,\n highRiskCommands: readonly string[] = HIGH_RISK_COMMANDS\n): string[] {\n if (!command) return [];\n const matches = new Set<string>();\n collectMatches(command, buildRiskList(highRiskCommands), matches, 0);\n return [...matches];\n}\n\n/**\n * Returns the first high-risk entry invoked in command position, or null.\n * Convenience wrapper over {@link matchHighRiskCommands}.\n */\nexport function matchHighRiskCommand(\n command: string,\n highRiskCommands: readonly string[] = HIGH_RISK_COMMANDS\n): string | null {\n return matchHighRiskCommands(command, highRiskCommands)[0] ?? null;\n}\n\n/**\n * True when the command's network destinations are exclusively local\n * (localhost / 127.0.0.1 / ::1 / *.localhost) AND at least one destination is\n * present. Used to skip the risk-80 bump for curl/wget dev-loop probes.\n * Unparseable or absent destinations keep the bump (fail toward review).\n */\nexport function isLocalhostOnlyNetworkCommand(command: string): boolean {\n const text = command.toLowerCase();\n // Candidate destinations: explicit URLs, host:port tokens, or bare local hosts.\n const tokens = text.split(/\\s+/).map((t) => t.replace(/^[\"'`]+|[\"'`,;)]+$/g, ''));\n let localCount = 0;\n for (const token of tokens) {\n let host: string | null = null;\n if (token.includes('://')) {\n let rest = token.slice(token.indexOf('://') + 3);\n const at = rest.indexOf('@');\n if (at !== -1) rest = rest.slice(at + 1);\n host = rest.split(/[/?#]/)[0];\n } else if (/^[a-z0-9.[\\]:-]+:\\d+(\\/.*)?$/.test(token)) {\n host = token.split('/')[0];\n } else if (/^(localhost|127\\.0\\.0\\.1|\\[?::1\\]?)(\\/.*)?$/.test(token)) {\n host = token.split('/')[0];\n }\n if (host === null) continue;\n // Strip a trailing numeric port, then IPv6 brackets ([::1]:3000 → ::1).\n const bare = host.replace(/:\\d+$/, '').replace(/^\\[|\\]$/g, '');\n if (LOCAL_HOST_RE.test(bare)) {\n localCount++;\n } else {\n return false;\n }\n }\n return localCount > 0;\n}\n","/**\n * Local PDLSS Evaluator\n *\n * Evaluates agent actions against a local PDLSS policy.\n * Same logic as the cloud evaluator but runs in-process with no I/O.\n *\n * Evaluation order:\n * 1. Purpose: find matching rule, check if allowed\n * 2. Purpose: check allowed targets (if specified)\n * 3. Purpose: check blocked patterns (if specified)\n * 4. Scope: check global blocked resources/domains\n * 5. Limits: check transaction/rate limits\n * 6. Risk thresholds + approval requirements\n */\n\nimport type {\n LocalPolicy,\n PDLSSContext,\n VerificationDecision,\n LocalPurposeRule,\n} from '../gateway/types';\nimport { SENSITIVE_PATHS } from '../gateway/types';\nimport { matchHighRiskCommands, isLocalhostOnlyNetworkCommand } from './command-matcher';\n\nexport class LocalEvaluator {\n private policy: LocalPolicy;\n private requestCounts: Map<string, { count: number; windowStart: number }> = new Map();\n\n constructor(policy: LocalPolicy) {\n this.policy = policy;\n }\n\n /**\n * Update the policy (e.g. after hot-reload or sync).\n */\n updatePolicy(policy: LocalPolicy): void {\n this.policy = policy;\n }\n\n /**\n * Evaluate an action context against the loaded policy.\n */\n evaluate(context: PDLSSContext): VerificationDecision {\n // 1. Purpose: find matching rule\n const purposeRule = this.policy.purposes.find((p) => p.id === context.purpose);\n if (!purposeRule) {\n return { recommendation: 'DENY', reason: 'Purpose not in policy' };\n }\n if (!purposeRule.allowed) {\n return { recommendation: 'DENY', reason: 'Purpose explicitly blocked' };\n }\n\n // 2. Purpose: check allowed targets\n if (purposeRule.targets && !this.matchesAnyPattern(context.target, purposeRule.targets)) {\n return { recommendation: 'DENY', reason: 'Target not in allowed list' };\n }\n\n // 3. Purpose: check blocked patterns\n if (\n purposeRule.blockedPatterns &&\n this.matchesAnyPattern(context.target, purposeRule.blockedPatterns)\n ) {\n return { recommendation: 'DENY', reason: 'Target matches blocked pattern' };\n }\n\n // 4. Scope: check global blocked resources/domains\n const scopeBlock = this.checkScopeBlock(context);\n if (scopeBlock) {\n return { recommendation: 'DENY', reason: scopeBlock };\n }\n\n // 5. Limits: check transaction and rate limits\n const limitViolation = this.checkLimits(context);\n if (limitViolation) {\n return { recommendation: 'DENY', reason: limitViolation };\n }\n\n // 6. Self-instantiation: check sub-agent spawning rules\n if (context.purpose === 'sub_agent.spawn' && this.policy.selfInstantiation) {\n if (!this.policy.selfInstantiation.allowed) {\n return { recommendation: 'DENY', reason: 'Sub-agent spawning is not allowed' };\n }\n const depth = (context.metadata?.subAgentDepth as number) || 0;\n if (\n this.policy.selfInstantiation.maxDepth !== undefined &&\n depth >= this.policy.selfInstantiation.maxDepth\n ) {\n return {\n recommendation: 'DENY',\n reason: `Sub-agent depth ${depth} exceeds max depth ${this.policy.selfInstantiation.maxDepth}`,\n };\n }\n }\n\n // 7. Risk thresholds + approval requirements\n if (purposeRule.requiresApproval) {\n return { recommendation: 'MANUAL_REVIEW', reason: 'Purpose requires approval' };\n }\n\n const riskDecision = this.checkRiskThresholds(context);\n if (riskDecision) {\n return riskDecision;\n }\n\n return {\n recommendation: 'ALLOW',\n reason: 'All PDLSS checks passed',\n evaluatedDimensions: {\n purpose: true,\n scope: !!this.policy.scope,\n limits: !!this.policy.limits,\n riskThresholds: !!this.policy.riskThresholds,\n },\n };\n }\n\n private checkScopeBlock(context: PDLSSContext): string | null {\n const scope = this.policy.scope;\n if (!scope) return null;\n\n // Check blocked domains against target and network access\n if (scope.blockedDomains) {\n const targetDomain = this.extractDomain(context.target);\n if (this.matchesAnyPattern(targetDomain, scope.blockedDomains)) {\n return `Target blocked by scope: ${context.target}`;\n }\n if (context.networkAccess) {\n for (const domain of context.networkAccess) {\n if (this.matchesAnyPattern(this.extractDomain(domain), scope.blockedDomains)) {\n return `Domain blocked by scope: ${domain}`;\n }\n }\n }\n }\n\n // Check blocked resources against target\n if (scope.blockedResources && this.matchesAnyPattern(context.target, scope.blockedResources)) {\n return `Resource blocked by scope: ${context.target}`;\n }\n\n // Check allowed domains (if specified, target must match)\n if (scope.allowedDomains && context.networkAccess) {\n for (const domain of context.networkAccess) {\n if (!this.matchesAnyPattern(this.extractDomain(domain), scope.allowedDomains)) {\n return `Domain not in allowed list: ${domain}`;\n }\n }\n }\n\n return null;\n }\n\n private checkLimits(context: PDLSSContext): string | null {\n const limits = this.policy.limits;\n if (!limits) return null;\n\n // Transaction amount check\n if (limits.maxTransactionAmount !== undefined && context.transactionValue !== undefined) {\n if (context.transactionValue > limits.maxTransactionAmount) {\n return `Transaction value ${context.transactionValue} exceeds limit ${limits.maxTransactionAmount}`;\n }\n }\n\n // Rate limit check\n if (limits.maxRequestsPerHour !== undefined) {\n const key = context.purpose;\n const now = Date.now();\n const entry = this.requestCounts.get(key);\n const hourMs = 3600000;\n\n if (!entry || now - entry.windowStart > hourMs) {\n this.requestCounts.set(key, { count: 1, windowStart: now });\n } else {\n entry.count++;\n if (entry.count > limits.maxRequestsPerHour) {\n return `Rate limit exceeded: ${entry.count}/${limits.maxRequestsPerHour} requests per hour`;\n }\n }\n }\n\n return null;\n }\n\n private checkRiskThresholds(context: PDLSSContext): VerificationDecision | null {\n if (!this.policy.riskThresholds) return null;\n\n const riskScore = this.calculateRiskScore(context);\n const thresholds = this.policy.riskThresholds;\n\n if (riskScore >= thresholds.autoBlock.min) {\n return { recommendation: 'DENY', reason: `Risk score ${riskScore} exceeds block threshold` };\n }\n\n if (riskScore >= thresholds.requireApproval.min) {\n return {\n recommendation: 'MANUAL_REVIEW',\n reason: `Risk score ${riskScore} requires approval`,\n };\n }\n\n return null;\n }\n\n private calculateRiskScore(context: PDLSSContext): number {\n let score = 0;\n\n // Explicit risk factors take priority (highest severity wins)\n if (context.riskFactors?.length) {\n const severityScores: Record<string, number> = {\n low: 10,\n medium: 40,\n high: 70,\n critical: 90,\n };\n\n for (const factor of context.riskFactors) {\n const factorScore = severityScores[factor.severity] || 0;\n if (factorScore > score) score = factorScore;\n }\n return score;\n }\n\n // Auto-detect risk from high-risk shell commands (command-position match,\n // not substring). curl/wget aimed exclusively at localhost stay below the\n // review threshold — dev-loop probes, not exfiltration surface.\n if (context.purpose === 'shell.exec' && context.target) {\n const matched = matchHighRiskCommands(context.target);\n const localProbeOnly =\n matched.length > 0 &&\n matched.every((cmd) => cmd === 'curl' || cmd === 'wget') &&\n isLocalhostOnlyNetworkCommand(context.target);\n if (matched.length > 0 && !localProbeOnly) {\n score = Math.max(score, 80);\n }\n }\n\n // Auto-detect risk from sensitive file paths (score 50 = review range)\n if (\n (context.purpose === 'file.read' ||\n context.purpose === 'file.write' ||\n context.purpose === 'file.delete') &&\n context.target\n ) {\n const targetLower = context.target.toLowerCase();\n for (const sensitivePath of SENSITIVE_PATHS) {\n if (targetLower.includes(sensitivePath.toLowerCase())) {\n score = Math.max(score, 50);\n break;\n }\n }\n }\n\n return score;\n }\n\n /**\n * Extract domain from a URL-like string (strips protocol and path).\n */\n private extractDomain(value: string): string {\n let domain = value;\n // Strip protocol\n const protoIndex = domain.indexOf('://');\n if (protoIndex !== -1) domain = domain.slice(protoIndex + 3);\n // Strip path\n const slashIndex = domain.indexOf('/');\n if (slashIndex !== -1) domain = domain.slice(0, slashIndex);\n // Strip port\n const colonIndex = domain.indexOf(':');\n if (colonIndex !== -1) domain = domain.slice(0, colonIndex);\n return domain;\n }\n\n /**\n * Check if a value matches any of the given glob patterns.\n */\n private matchesAnyPattern(value: string, patterns: string[]): boolean {\n return patterns.some((pattern) => this.matchGlob(value, pattern));\n }\n\n /**\n * Simple glob pattern matching.\n * Supports * (matches any characters including / and spaces) and ? (single char).\n * Uses the same approach as the backend's matchGlobPattern.\n */\n private matchGlob(value: string, pattern: string): boolean {\n // Exact match\n if (pattern === value) return true;\n\n // Audit F-A1-13: ReDoS guard. Patterns with too many `*` segments\n // produce catastrophic-backtracking regexes (e.g. `a*a*a*a*a*a*a*a*b`\n // against `aaaaaaaa` runs in exponential time). Local policies are\n // operator-controlled so attacker access requires operator-side\n // compromise, but a copy-pasted-from-internet example pattern could\n // still DoS the gateway when matched against request input.\n const starCount = (pattern.match(/\\*/g) ?? []).length;\n if (starCount > 8) {\n return false;\n }\n\n // Convert glob to regex\n const regexStr = pattern\n .replace(/[.+^${}()|[\\]\\\\]/g, '\\\\$&') // Escape special regex chars (except * and ?)\n .replace(/\\*/g, '.*') // * matches anything\n .replace(/\\?/g, '.'); // ? matches single char\n\n try {\n return new RegExp(`^${regexStr}$`, 'i').test(value);\n } catch {\n return false;\n }\n }\n}\n\n/**\n * Convenience: find a matching purpose rule.\n */\nexport function findPurposeRule(\n policy: LocalPolicy,\n purposeId: string\n): LocalPurposeRule | undefined {\n return policy.purposes.find((p) => p.id === purposeId);\n}\n"],"mappings":";AAuLO,IAAM,qBAAqB;AAAA,EAChC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAGO,IAAM,kBAAkB;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;;;AC/MA,IAAM,WAAW,oBAAI,IAAI;AAAA,EACvB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAGD,IAAM,YAAY;AAElB,IAAM,mBAAmB;AACzB,IAAM,sBAAsB;AAC5B,IAAM,qBAAqB;AAG3B,IAAM,gBAAgB;AAQtB,SAAS,cAAc,SAAsC;AAC3D,QAAM,aAAa,oBAAI,IAAY;AACnC,QAAM,YAAwB,CAAC;AAC/B,aAAW,SAAS,SAAS;AAC3B,UAAM,QAAQ,MAAM,YAAY,EAAE,KAAK,EAAE,MAAM,KAAK;AACpD,QAAI,MAAM,WAAW,EAAG,YAAW,IAAI,MAAM,CAAC,CAAC;AAAA,QAC1C,WAAU,KAAK,KAAK;AAAA,EAC3B;AACA,YAAU,KAAK,CAAC,GAAG,MAAM,EAAE,SAAS,EAAE,MAAM;AAC5C,SAAO,EAAE,YAAY,UAAU;AACjC;AAGA,SAAS,mBAAmB,MAAsB;AAChD,QAAM,QAAQ,KAAK,MAAM,IAAI;AAC7B,QAAM,OAAiB,CAAC;AACxB,MAAI,aAA4B;AAChC,aAAW,QAAQ,OAAO;AACxB,QAAI,eAAe,MAAM;AACvB,UAAI,KAAK,KAAK,MAAM,WAAY,cAAa;AAC7C;AAAA,IACF;AACA,SAAK,KAAK,IAAI;AACd,UAAM,UAAU,KAAK,MAAM,wBAAwB;AACnD,QAAI,QAAS,cAAa,QAAQ,CAAC;AAAA,EACrC;AACA,SAAO,KAAK,KAAK,IAAI;AACvB;AAOA,SAAS,2BAA2B,MAAc,SAA0C;AAC1F,MAAI,SAAS;AACb,MAAI,QAAQ,OAAO,QAAQ,IAAI;AAC/B,MAAI,aAAa;AACjB,SAAO,UAAU,MAAM,aAAa,IAAI;AACtC;AACA,QAAI,QAAQ;AACZ,QAAI,MAAM,OAAO;AACjB,aAAS,IAAI,QAAQ,GAAG,IAAI,OAAO,QAAQ,KAAK;AAC9C,UAAI,OAAO,CAAC,MAAM,IAAK;AAAA,eACd,OAAO,CAAC,MAAM,KAAK;AAC1B;AACA,YAAI,UAAU,GAAG;AACf,gBAAM;AACN;AAAA,QACF;AAAA,MACF;AAAA,IACF;AACA,YAAQ,OAAO,MAAM,QAAQ,GAAG,GAAG,CAAC;AACpC,aAAS,GAAG,OAAO,MAAM,GAAG,KAAK,CAAC,IAAI,OAAO,MAAM,KAAK,IAAI,MAAM,GAAG,OAAO,MAAM,CAAC,CAAC;AACpF,YAAQ,OAAO,QAAQ,IAAI;AAAA,EAC7B;AACA,SAAO;AACT;AAGA,SAAS,6BAA6B,MAAc,SAA0C;AAC5F,MAAI,CAAC,KAAK,SAAS,GAAG,EAAG,QAAO;AAChC,QAAM,SAAS,KAAK,MAAM,GAAG;AAC7B,QAAM,OAAiB,CAAC;AACxB,WAAS,IAAI,GAAG,IAAI,OAAO,QAAQ,KAAK;AACtC,QAAI,IAAI,MAAM,EAAG,SAAQ,OAAO,CAAC,CAAC;AAAA,QAC7B,MAAK,KAAK,OAAO,CAAC,CAAC;AAAA,EAC1B;AACA,SAAO,KAAK,KAAK,GAAG;AACtB;AAEA,SAAS,SAAS,OAAuB;AACvC,QAAM,QAAQ,MAAM,YAAY,GAAG;AACnC,SAAO,UAAU,KAAK,QAAQ,MAAM,MAAM,QAAQ,CAAC;AACrD;AAEA,SAAS,aAAa,SAAiB,MAA+B;AACpE,QAAM,SAAS,QAAQ,MAAM,KAAK,EAAE,OAAO,CAAC,MAAM,WAAW,KAAK,CAAC,CAAC;AACpE,MAAI,IAAI;AACR,MAAI,OAAO;AACX,SAAO,IAAI,OAAO,QAAQ;AAExB,WAAO,IAAI,OAAO,WAAW,OAAO,CAAC,MAAM,YAAY,UAAU,KAAK,OAAO,CAAC,CAAC,GAAI;AACnF,QAAI,KAAK,OAAO,OAAQ,QAAO;AAE/B,UAAM,MAAM,SAAS,OAAO,CAAC,CAAC;AAG9B,UAAM,cAAc,CAAC,KAAK,GAAG,OAAO,MAAM,IAAI,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,CAAC,CAAC;AAClF,eAAW,SAAS,KAAK,WAAW;AAClC,UAAI,MAAM,UAAU,YAAY,UAAU,MAAM,MAAM,CAAC,GAAG,MAAM,YAAY,CAAC,MAAM,CAAC,GAAG;AACrF,eAAO,MAAM,KAAK,GAAG;AAAA,MACvB;AAAA,IACF;AACA,QAAI,KAAK,WAAW,IAAI,GAAG,EAAG,QAAO;AAGrC,QAAI,SAAS,IAAI,GAAG,KAAK,OAAO,kBAAkB;AAChD;AACA;AACA,aAAO,IAAI,OAAO,WAAW,OAAO,CAAC,EAAE,WAAW,GAAG,KAAK,UAAU,KAAK,OAAO,CAAC,CAAC,GAAI;AACtF;AAAA,IACF;AACA,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAEA,SAAS,eACP,SACA,MACA,SACA,OACM;AACN,MAAI,QAAQ,uBAAuB,QAAQ,SAAS,mBAAoB;AAExE,MAAI,OAAO,QAAQ,YAAY;AAC/B,SAAO,mBAAmB,IAAI;AAI9B,QAAM,UAAU,CAAC,UAAkB,eAAe,OAAO,MAAM,SAAS,QAAQ,CAAC;AACjF,SAAO,2BAA2B,MAAM,OAAO;AAC/C,SAAO,6BAA6B,MAAM,OAAO;AAGjD,SAAO,KAAK,QAAQ,YAAY,GAAG,EAAE,QAAQ,YAAY,GAAG;AAC5D,SAAO,KAAK,QAAQ,kBAAkB,GAAG;AAEzC,QAAM,WAAW,KAAK,MAAM,sBAAsB;AAClD,aAAW,WAAW,UAAU;AAC9B,UAAM,UAAU,aAAa,SAAS,IAAI;AAC1C,QAAI,QAAS,SAAQ,IAAI,OAAO;AAAA,EAClC;AACF;AAMO,SAAS,sBACd,SACA,mBAAsC,oBAC5B;AACV,MAAI,CAAC,QAAS,QAAO,CAAC;AACtB,QAAM,UAAU,oBAAI,IAAY;AAChC,iBAAe,SAAS,cAAc,gBAAgB,GAAG,SAAS,CAAC;AACnE,SAAO,CAAC,GAAG,OAAO;AACpB;AAmBO,SAAS,8BAA8B,SAA0B;AACtE,QAAM,OAAO,QAAQ,YAAY;AAEjC,QAAM,SAAS,KAAK,MAAM,KAAK,EAAE,IAAI,CAAC,MAAM,EAAE,QAAQ,uBAAuB,EAAE,CAAC;AAChF,MAAI,aAAa;AACjB,aAAW,SAAS,QAAQ;AAC1B,QAAI,OAAsB;AAC1B,QAAI,MAAM,SAAS,KAAK,GAAG;AACzB,UAAI,OAAO,MAAM,MAAM,MAAM,QAAQ,KAAK,IAAI,CAAC;AAC/C,YAAM,KAAK,KAAK,QAAQ,GAAG;AAC3B,UAAI,OAAO,GAAI,QAAO,KAAK,MAAM,KAAK,CAAC;AACvC,aAAO,KAAK,MAAM,OAAO,EAAE,CAAC;AAAA,IAC9B,WAAW,+BAA+B,KAAK,KAAK,GAAG;AACrD,aAAO,MAAM,MAAM,GAAG,EAAE,CAAC;AAAA,IAC3B,WAAW,8CAA8C,KAAK,KAAK,GAAG;AACpE,aAAO,MAAM,MAAM,GAAG,EAAE,CAAC;AAAA,IAC3B;AACA,QAAI,SAAS,KAAM;AAEnB,UAAM,OAAO,KAAK,QAAQ,SAAS,EAAE,EAAE,QAAQ,YAAY,EAAE;AAC7D,QAAI,cAAc,KAAK,IAAI,GAAG;AAC5B;AAAA,IACF,OAAO;AACL,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO,aAAa;AACtB;;;AC1NO,IAAM,iBAAN,MAAqB;AAAA,EAI1B,YAAY,QAAqB;AAFjC,SAAQ,gBAAqE,oBAAI,IAAI;AAGnF,SAAK,SAAS;AAAA,EAChB;AAAA;AAAA;AAAA;AAAA,EAKA,aAAa,QAA2B;AACtC,SAAK,SAAS;AAAA,EAChB;AAAA;AAAA;AAAA;AAAA,EAKA,SAAS,SAA6C;AAEpD,UAAM,cAAc,KAAK,OAAO,SAAS,KAAK,CAAC,MAAM,EAAE,OAAO,QAAQ,OAAO;AAC7E,QAAI,CAAC,aAAa;AAChB,aAAO,EAAE,gBAAgB,QAAQ,QAAQ,wBAAwB;AAAA,IACnE;AACA,QAAI,CAAC,YAAY,SAAS;AACxB,aAAO,EAAE,gBAAgB,QAAQ,QAAQ,6BAA6B;AAAA,IACxE;AAGA,QAAI,YAAY,WAAW,CAAC,KAAK,kBAAkB,QAAQ,QAAQ,YAAY,OAAO,GAAG;AACvF,aAAO,EAAE,gBAAgB,QAAQ,QAAQ,6BAA6B;AAAA,IACxE;AAGA,QACE,YAAY,mBACZ,KAAK,kBAAkB,QAAQ,QAAQ,YAAY,eAAe,GAClE;AACA,aAAO,EAAE,gBAAgB,QAAQ,QAAQ,iCAAiC;AAAA,IAC5E;AAGA,UAAM,aAAa,KAAK,gBAAgB,OAAO;AAC/C,QAAI,YAAY;AACd,aAAO,EAAE,gBAAgB,QAAQ,QAAQ,WAAW;AAAA,IACtD;AAGA,UAAM,iBAAiB,KAAK,YAAY,OAAO;AAC/C,QAAI,gBAAgB;AAClB,aAAO,EAAE,gBAAgB,QAAQ,QAAQ,eAAe;AAAA,IAC1D;AAGA,QAAI,QAAQ,YAAY,qBAAqB,KAAK,OAAO,mBAAmB;AAC1E,UAAI,CAAC,KAAK,OAAO,kBAAkB,SAAS;AAC1C,eAAO,EAAE,gBAAgB,QAAQ,QAAQ,oCAAoC;AAAA,MAC/E;AACA,YAAM,QAAS,QAAQ,UAAU,iBAA4B;AAC7D,UACE,KAAK,OAAO,kBAAkB,aAAa,UAC3C,SAAS,KAAK,OAAO,kBAAkB,UACvC;AACA,eAAO;AAAA,UACL,gBAAgB;AAAA,UAChB,QAAQ,mBAAmB,KAAK,sBAAsB,KAAK,OAAO,kBAAkB,QAAQ;AAAA,QAC9F;AAAA,MACF;AAAA,IACF;AAGA,QAAI,YAAY,kBAAkB;AAChC,aAAO,EAAE,gBAAgB,iBAAiB,QAAQ,4BAA4B;AAAA,IAChF;AAEA,UAAM,eAAe,KAAK,oBAAoB,OAAO;AACrD,QAAI,cAAc;AAChB,aAAO;AAAA,IACT;AAEA,WAAO;AAAA,MACL,gBAAgB;AAAA,MAChB,QAAQ;AAAA,MACR,qBAAqB;AAAA,QACnB,SAAS;AAAA,QACT,OAAO,CAAC,CAAC,KAAK,OAAO;AAAA,QACrB,QAAQ,CAAC,CAAC,KAAK,OAAO;AAAA,QACtB,gBAAgB,CAAC,CAAC,KAAK,OAAO;AAAA,MAChC;AAAA,IACF;AAAA,EACF;AAAA,EAEQ,gBAAgB,SAAsC;AAC5D,UAAM,QAAQ,KAAK,OAAO;AAC1B,QAAI,CAAC,MAAO,QAAO;AAGnB,QAAI,MAAM,gBAAgB;AACxB,YAAM,eAAe,KAAK,cAAc,QAAQ,MAAM;AACtD,UAAI,KAAK,kBAAkB,cAAc,MAAM,cAAc,GAAG;AAC9D,eAAO,4BAA4B,QAAQ,MAAM;AAAA,MACnD;AACA,UAAI,QAAQ,eAAe;AACzB,mBAAW,UAAU,QAAQ,eAAe;AAC1C,cAAI,KAAK,kBAAkB,KAAK,cAAc,MAAM,GAAG,MAAM,cAAc,GAAG;AAC5E,mBAAO,4BAA4B,MAAM;AAAA,UAC3C;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAGA,QAAI,MAAM,oBAAoB,KAAK,kBAAkB,QAAQ,QAAQ,MAAM,gBAAgB,GAAG;AAC5F,aAAO,8BAA8B,QAAQ,MAAM;AAAA,IACrD;AAGA,QAAI,MAAM,kBAAkB,QAAQ,eAAe;AACjD,iBAAW,UAAU,QAAQ,eAAe;AAC1C,YAAI,CAAC,KAAK,kBAAkB,KAAK,cAAc,MAAM,GAAG,MAAM,cAAc,GAAG;AAC7E,iBAAO,+BAA+B,MAAM;AAAA,QAC9C;AAAA,MACF;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA,EAEQ,YAAY,SAAsC;AACxD,UAAM,SAAS,KAAK,OAAO;AAC3B,QAAI,CAAC,OAAQ,QAAO;AAGpB,QAAI,OAAO,yBAAyB,UAAa,QAAQ,qBAAqB,QAAW;AACvF,UAAI,QAAQ,mBAAmB,OAAO,sBAAsB;AAC1D,eAAO,qBAAqB,QAAQ,gBAAgB,kBAAkB,OAAO,oBAAoB;AAAA,MACnG;AAAA,IACF;AAGA,QAAI,OAAO,uBAAuB,QAAW;AAC3C,YAAM,MAAM,QAAQ;AACpB,YAAM,MAAM,KAAK,IAAI;AACrB,YAAM,QAAQ,KAAK,cAAc,IAAI,GAAG;AACxC,YAAM,SAAS;AAEf,UAAI,CAAC,SAAS,MAAM,MAAM,cAAc,QAAQ;AAC9C,aAAK,cAAc,IAAI,KAAK,EAAE,OAAO,GAAG,aAAa,IAAI,CAAC;AAAA,MAC5D,OAAO;AACL,cAAM;AACN,YAAI,MAAM,QAAQ,OAAO,oBAAoB;AAC3C,iBAAO,wBAAwB,MAAM,KAAK,IAAI,OAAO,kBAAkB;AAAA,QACzE;AAAA,MACF;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA,EAEQ,oBAAoB,SAAoD;AAC9E,QAAI,CAAC,KAAK,OAAO,eAAgB,QAAO;AAExC,UAAM,YAAY,KAAK,mBAAmB,OAAO;AACjD,UAAM,aAAa,KAAK,OAAO;AAE/B,QAAI,aAAa,WAAW,UAAU,KAAK;AACzC,aAAO,EAAE,gBAAgB,QAAQ,QAAQ,cAAc,SAAS,2BAA2B;AAAA,IAC7F;AAEA,QAAI,aAAa,WAAW,gBAAgB,KAAK;AAC/C,aAAO;AAAA,QACL,gBAAgB;AAAA,QAChB,QAAQ,cAAc,SAAS;AAAA,MACjC;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA,EAEQ,mBAAmB,SAA+B;AACxD,QAAI,QAAQ;AAGZ,QAAI,QAAQ,aAAa,QAAQ;AAC/B,YAAM,iBAAyC;AAAA,QAC7C,KAAK;AAAA,QACL,QAAQ;AAAA,QACR,MAAM;AAAA,QACN,UAAU;AAAA,MACZ;AAEA,iBAAW,UAAU,QAAQ,aAAa;AACxC,cAAM,cAAc,eAAe,OAAO,QAAQ,KAAK;AACvD,YAAI,cAAc,MAAO,SAAQ;AAAA,MACnC;AACA,aAAO;AAAA,IACT;AAKA,QAAI,QAAQ,YAAY,gBAAgB,QAAQ,QAAQ;AACtD,YAAM,UAAU,sBAAsB,QAAQ,MAAM;AACpD,YAAM,iBACJ,QAAQ,SAAS,KACjB,QAAQ,MAAM,CAAC,QAAQ,QAAQ,UAAU,QAAQ,MAAM,KACvD,8BAA8B,QAAQ,MAAM;AAC9C,UAAI,QAAQ,SAAS,KAAK,CAAC,gBAAgB;AACzC,gBAAQ,KAAK,IAAI,OAAO,EAAE;AAAA,MAC5B;AAAA,IACF;AAGA,SACG,QAAQ,YAAY,eACnB,QAAQ,YAAY,gBACpB,QAAQ,YAAY,kBACtB,QAAQ,QACR;AACA,YAAM,cAAc,QAAQ,OAAO,YAAY;AAC/C,iBAAW,iBAAiB,iBAAiB;AAC3C,YAAI,YAAY,SAAS,cAAc,YAAY,CAAC,GAAG;AACrD,kBAAQ,KAAK,IAAI,OAAO,EAAE;AAC1B;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKQ,cAAc,OAAuB;AAC3C,QAAI,SAAS;AAEb,UAAM,aAAa,OAAO,QAAQ,KAAK;AACvC,QAAI,eAAe,GAAI,UAAS,OAAO,MAAM,aAAa,CAAC;AAE3D,UAAM,aAAa,OAAO,QAAQ,GAAG;AACrC,QAAI,eAAe,GAAI,UAAS,OAAO,MAAM,GAAG,UAAU;AAE1D,UAAM,aAAa,OAAO,QAAQ,GAAG;AACrC,QAAI,eAAe,GAAI,UAAS,OAAO,MAAM,GAAG,UAAU;AAC1D,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKQ,kBAAkB,OAAe,UAA6B;AACpE,WAAO,SAAS,KAAK,CAAC,YAAY,KAAK,UAAU,OAAO,OAAO,CAAC;AAAA,EAClE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOQ,UAAU,OAAe,SAA0B;AAEzD,QAAI,YAAY,MAAO,QAAO;AAQ9B,UAAM,aAAa,QAAQ,MAAM,KAAK,KAAK,CAAC,GAAG;AAC/C,QAAI,YAAY,GAAG;AACjB,aAAO;AAAA,IACT;AAGA,UAAM,WAAW,QACd,QAAQ,qBAAqB,MAAM,EACnC,QAAQ,OAAO,IAAI,EACnB,QAAQ,OAAO,GAAG;AAErB,QAAI;AACF,aAAO,IAAI,OAAO,IAAI,QAAQ,KAAK,GAAG,EAAE,KAAK,KAAK;AAAA,IACpD,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AACF;AAKO,SAAS,gBACd,QACA,WAC8B;AAC9B,SAAO,OAAO,SAAS,KAAK,CAAC,MAAM,EAAE,OAAO,SAAS;AACvD;","names":[]}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maximal metadata capture — the shared, edge-safe sanitiser.
|
|
3
|
+
*
|
|
4
|
+
* AstraSync's thesis is total metadata capture: every header and connection
|
|
5
|
+
* signal an inbound agent presents is a signal we want to keep. This module is
|
|
6
|
+
* the ONE place that decides what is safe to store verbatim, what is a secret
|
|
7
|
+
* to drop, and what credential headers reduce to a safe *format prefix* (a
|
|
8
|
+
* platform signal, never the secret itself).
|
|
9
|
+
*
|
|
10
|
+
* It runs in three places that must agree byte-for-byte:
|
|
11
|
+
* - the edge adapter (`@astrasyncai/adapter-lambda`) — the richest capture
|
|
12
|
+
* point (full CloudFront header arrays),
|
|
13
|
+
* - the SDK adapters (express / nextjs) — for SDK-only merchants with no edge,
|
|
14
|
+
* - (indirectly) the backend, which stores whatever the above forward.
|
|
15
|
+
*
|
|
16
|
+
* Pure — no Node built-ins, no I/O — so it is safe in Lambda@Edge, the browser
|
|
17
|
+
* build, and Deno. Capture is cheap: at the edge and SDK it is local string
|
|
18
|
+
* work, and the backend stores it on a fire-and-forget event insert, so it adds
|
|
19
|
+
* ZERO verify-access decision latency. Detection (which few signals imply which
|
|
20
|
+
* vendor) is a separate, curated concern — see `platform-signatures.ts`.
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* The captured, sanitised view of an inbound agent's request metadata. Attached
|
|
24
|
+
* to verify-access / beacon payloads as `callerMetadata.observedMetadata`.
|
|
25
|
+
*/
|
|
26
|
+
interface ObservedMetadata {
|
|
27
|
+
/** Header name → value, secrets removed, verbatim otherwise. */
|
|
28
|
+
headers: Record<string, string>;
|
|
29
|
+
/**
|
|
30
|
+
* Safe key-format prefix of a credential header (e.g. `sk-ant-api03`,
|
|
31
|
+
* `sk-proj`, `AIza`) — a platform signal, NEVER the secret. Absent if no
|
|
32
|
+
* credential header was present or none matched a known format.
|
|
33
|
+
*/
|
|
34
|
+
apiKeyFormat?: string;
|
|
35
|
+
/**
|
|
36
|
+
* The subset of `headers` that are known platform-signal headers
|
|
37
|
+
* (`openai-organization`, `x-goog-user-project`, `anthropic-version`, …),
|
|
38
|
+
* pulled out for convenient detection + display. Values are the same
|
|
39
|
+
* sanitised strings as in `headers`.
|
|
40
|
+
*/
|
|
41
|
+
platformHeaders?: Record<string, string>;
|
|
42
|
+
/**
|
|
43
|
+
* Connection-layer signals the edge can see but the SDK cannot (IP, ASN,
|
|
44
|
+
* country, TLS version/cipher, HTTP version, device class). Populated by the
|
|
45
|
+
* edge adapter's `collectConnectionMetadata`; absent for SDK-only capture.
|
|
46
|
+
*/
|
|
47
|
+
connection?: Record<string, string>;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Result of sanitising a raw header map. `headers` is always present (possibly
|
|
51
|
+
* empty); `apiKeyFormat` / `platformHeaders` only when a signal was found.
|
|
52
|
+
*/
|
|
53
|
+
interface SanitizeHeadersResult {
|
|
54
|
+
headers: Record<string, string>;
|
|
55
|
+
apiKeyFormat?: string;
|
|
56
|
+
platformHeaders?: Record<string, string>;
|
|
57
|
+
}
|
|
58
|
+
/** Size caps — the only guard against jsonb bloat from a hostile POST. */
|
|
59
|
+
declare const MAX_HEADERS = 64;
|
|
60
|
+
declare const MAX_HEADER_VALUE_BYTES = 1024;
|
|
61
|
+
declare const MAX_TOTAL_BYTES: number;
|
|
62
|
+
/**
|
|
63
|
+
* Extract the safe key-format prefix from a credential header value. Strips a
|
|
64
|
+
* leading `Bearer ` / `Basic ` scheme, then returns the longest known prefix
|
|
65
|
+
* that the (lowercased) value starts with. Returns undefined for opaque values
|
|
66
|
+
* (e.g. a bare UUID token) so we never guess a format we don't recognise.
|
|
67
|
+
*/
|
|
68
|
+
declare function extractApiKeyFormat(rawValue: string): string | undefined;
|
|
69
|
+
/**
|
|
70
|
+
* Sanitise a raw header map for storage. Deny-lists genuine secrets, reduces
|
|
71
|
+
* credential headers to a safe format prefix, keeps everything else verbatim,
|
|
72
|
+
* and enforces the size caps that bound jsonb growth.
|
|
73
|
+
*
|
|
74
|
+
* Accepts any string→(string|string[]|undefined) map (Node's `req.headers`,
|
|
75
|
+
* CloudFront's flattened headers, a plain object). Multi-value headers are
|
|
76
|
+
* joined with `, ` per RFC 7230.
|
|
77
|
+
*/
|
|
78
|
+
declare function sanitizeHeaders(raw: Record<string, string | string[] | undefined> | undefined | null): SanitizeHeadersResult;
|
|
79
|
+
/**
|
|
80
|
+
* Pull the known platform-signal headers out of an ALREADY-sanitised header
|
|
81
|
+
* map. `sanitizeHeaders` already computes this as a by-product; this standalone
|
|
82
|
+
* form is exported for call-sites (edge, tests) that hold a sanitised map and
|
|
83
|
+
* want only the platform subset. Never re-run over raw (unsanitised) headers.
|
|
84
|
+
*/
|
|
85
|
+
declare function extractPlatformHeaders(sanitized: Record<string, string> | undefined | null): Record<string, string>;
|
|
86
|
+
|
|
87
|
+
export { MAX_HEADERS, MAX_HEADER_VALUE_BYTES, MAX_TOTAL_BYTES, type ObservedMetadata, type SanitizeHeadersResult, extractApiKeyFormat, extractPlatformHeaders, sanitizeHeaders };
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maximal metadata capture — the shared, edge-safe sanitiser.
|
|
3
|
+
*
|
|
4
|
+
* AstraSync's thesis is total metadata capture: every header and connection
|
|
5
|
+
* signal an inbound agent presents is a signal we want to keep. This module is
|
|
6
|
+
* the ONE place that decides what is safe to store verbatim, what is a secret
|
|
7
|
+
* to drop, and what credential headers reduce to a safe *format prefix* (a
|
|
8
|
+
* platform signal, never the secret itself).
|
|
9
|
+
*
|
|
10
|
+
* It runs in three places that must agree byte-for-byte:
|
|
11
|
+
* - the edge adapter (`@astrasyncai/adapter-lambda`) — the richest capture
|
|
12
|
+
* point (full CloudFront header arrays),
|
|
13
|
+
* - the SDK adapters (express / nextjs) — for SDK-only merchants with no edge,
|
|
14
|
+
* - (indirectly) the backend, which stores whatever the above forward.
|
|
15
|
+
*
|
|
16
|
+
* Pure — no Node built-ins, no I/O — so it is safe in Lambda@Edge, the browser
|
|
17
|
+
* build, and Deno. Capture is cheap: at the edge and SDK it is local string
|
|
18
|
+
* work, and the backend stores it on a fire-and-forget event insert, so it adds
|
|
19
|
+
* ZERO verify-access decision latency. Detection (which few signals imply which
|
|
20
|
+
* vendor) is a separate, curated concern — see `platform-signatures.ts`.
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* The captured, sanitised view of an inbound agent's request metadata. Attached
|
|
24
|
+
* to verify-access / beacon payloads as `callerMetadata.observedMetadata`.
|
|
25
|
+
*/
|
|
26
|
+
interface ObservedMetadata {
|
|
27
|
+
/** Header name → value, secrets removed, verbatim otherwise. */
|
|
28
|
+
headers: Record<string, string>;
|
|
29
|
+
/**
|
|
30
|
+
* Safe key-format prefix of a credential header (e.g. `sk-ant-api03`,
|
|
31
|
+
* `sk-proj`, `AIza`) — a platform signal, NEVER the secret. Absent if no
|
|
32
|
+
* credential header was present or none matched a known format.
|
|
33
|
+
*/
|
|
34
|
+
apiKeyFormat?: string;
|
|
35
|
+
/**
|
|
36
|
+
* The subset of `headers` that are known platform-signal headers
|
|
37
|
+
* (`openai-organization`, `x-goog-user-project`, `anthropic-version`, …),
|
|
38
|
+
* pulled out for convenient detection + display. Values are the same
|
|
39
|
+
* sanitised strings as in `headers`.
|
|
40
|
+
*/
|
|
41
|
+
platformHeaders?: Record<string, string>;
|
|
42
|
+
/**
|
|
43
|
+
* Connection-layer signals the edge can see but the SDK cannot (IP, ASN,
|
|
44
|
+
* country, TLS version/cipher, HTTP version, device class). Populated by the
|
|
45
|
+
* edge adapter's `collectConnectionMetadata`; absent for SDK-only capture.
|
|
46
|
+
*/
|
|
47
|
+
connection?: Record<string, string>;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Result of sanitising a raw header map. `headers` is always present (possibly
|
|
51
|
+
* empty); `apiKeyFormat` / `platformHeaders` only when a signal was found.
|
|
52
|
+
*/
|
|
53
|
+
interface SanitizeHeadersResult {
|
|
54
|
+
headers: Record<string, string>;
|
|
55
|
+
apiKeyFormat?: string;
|
|
56
|
+
platformHeaders?: Record<string, string>;
|
|
57
|
+
}
|
|
58
|
+
/** Size caps — the only guard against jsonb bloat from a hostile POST. */
|
|
59
|
+
declare const MAX_HEADERS = 64;
|
|
60
|
+
declare const MAX_HEADER_VALUE_BYTES = 1024;
|
|
61
|
+
declare const MAX_TOTAL_BYTES: number;
|
|
62
|
+
/**
|
|
63
|
+
* Extract the safe key-format prefix from a credential header value. Strips a
|
|
64
|
+
* leading `Bearer ` / `Basic ` scheme, then returns the longest known prefix
|
|
65
|
+
* that the (lowercased) value starts with. Returns undefined for opaque values
|
|
66
|
+
* (e.g. a bare UUID token) so we never guess a format we don't recognise.
|
|
67
|
+
*/
|
|
68
|
+
declare function extractApiKeyFormat(rawValue: string): string | undefined;
|
|
69
|
+
/**
|
|
70
|
+
* Sanitise a raw header map for storage. Deny-lists genuine secrets, reduces
|
|
71
|
+
* credential headers to a safe format prefix, keeps everything else verbatim,
|
|
72
|
+
* and enforces the size caps that bound jsonb growth.
|
|
73
|
+
*
|
|
74
|
+
* Accepts any string→(string|string[]|undefined) map (Node's `req.headers`,
|
|
75
|
+
* CloudFront's flattened headers, a plain object). Multi-value headers are
|
|
76
|
+
* joined with `, ` per RFC 7230.
|
|
77
|
+
*/
|
|
78
|
+
declare function sanitizeHeaders(raw: Record<string, string | string[] | undefined> | undefined | null): SanitizeHeadersResult;
|
|
79
|
+
/**
|
|
80
|
+
* Pull the known platform-signal headers out of an ALREADY-sanitised header
|
|
81
|
+
* map. `sanitizeHeaders` already computes this as a by-product; this standalone
|
|
82
|
+
* form is exported for call-sites (edge, tests) that hold a sanitised map and
|
|
83
|
+
* want only the platform subset. Never re-run over raw (unsanitised) headers.
|
|
84
|
+
*/
|
|
85
|
+
declare function extractPlatformHeaders(sanitized: Record<string, string> | undefined | null): Record<string, string>;
|
|
86
|
+
|
|
87
|
+
export { MAX_HEADERS, MAX_HEADER_VALUE_BYTES, MAX_TOTAL_BYTES, type ObservedMetadata, type SanitizeHeadersResult, extractApiKeyFormat, extractPlatformHeaders, sanitizeHeaders };
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/metadata-capture.ts
|
|
21
|
+
var metadata_capture_exports = {};
|
|
22
|
+
__export(metadata_capture_exports, {
|
|
23
|
+
MAX_HEADERS: () => MAX_HEADERS,
|
|
24
|
+
MAX_HEADER_VALUE_BYTES: () => MAX_HEADER_VALUE_BYTES,
|
|
25
|
+
MAX_TOTAL_BYTES: () => MAX_TOTAL_BYTES,
|
|
26
|
+
extractApiKeyFormat: () => extractApiKeyFormat,
|
|
27
|
+
extractPlatformHeaders: () => extractPlatformHeaders,
|
|
28
|
+
sanitizeHeaders: () => sanitizeHeaders
|
|
29
|
+
});
|
|
30
|
+
module.exports = __toCommonJS(metadata_capture_exports);
|
|
31
|
+
var SECRET_HEADER_NAMES = /* @__PURE__ */ new Set([
|
|
32
|
+
"cookie",
|
|
33
|
+
"set-cookie",
|
|
34
|
+
"x-csrf-token",
|
|
35
|
+
"proxy-authorization",
|
|
36
|
+
"x-astrasync-signature",
|
|
37
|
+
"x-astrasync-secret",
|
|
38
|
+
"x-hub-signature",
|
|
39
|
+
"x-hub-signature-256"
|
|
40
|
+
]);
|
|
41
|
+
var SECRET_HEADER_PREFIXES = ["x-astrasync-", "kya-", "kya_"];
|
|
42
|
+
var CREDENTIAL_HEADER_NAMES = /* @__PURE__ */ new Set([
|
|
43
|
+
"authorization",
|
|
44
|
+
"x-api-key",
|
|
45
|
+
"x-goog-api-key",
|
|
46
|
+
"api-key"
|
|
47
|
+
]);
|
|
48
|
+
function isSignatureHeader(name) {
|
|
49
|
+
return name === "signature" || name.endsWith("-signature") || name.endsWith("-signature-256");
|
|
50
|
+
}
|
|
51
|
+
var PLATFORM_HEADER_NAMES = /* @__PURE__ */ new Set([
|
|
52
|
+
"openai-organization",
|
|
53
|
+
"openai-project",
|
|
54
|
+
"openai-version",
|
|
55
|
+
"openai-processing-ms",
|
|
56
|
+
"anthropic-version",
|
|
57
|
+
"anthropic-beta",
|
|
58
|
+
"x-goog-user-project",
|
|
59
|
+
"x-goog-api-client",
|
|
60
|
+
"user-agent",
|
|
61
|
+
"x-agent-card-url",
|
|
62
|
+
"x-agent-provider"
|
|
63
|
+
]);
|
|
64
|
+
var PLATFORM_HEADER_PREFIXES = ["x-goog-", "x-stainless-", "x-anthropic-", "x-openai-"];
|
|
65
|
+
var KEY_FORMAT_PREFIXES = [
|
|
66
|
+
"sk-ant-api03",
|
|
67
|
+
"sk-ant-api",
|
|
68
|
+
"sk-ant",
|
|
69
|
+
"sk-proj",
|
|
70
|
+
"sk-svcacct",
|
|
71
|
+
"sk-or-v1",
|
|
72
|
+
"sk-or",
|
|
73
|
+
"sk-lf",
|
|
74
|
+
"sk-",
|
|
75
|
+
"gsk_",
|
|
76
|
+
"aiza",
|
|
77
|
+
"ya29",
|
|
78
|
+
"ghp_",
|
|
79
|
+
"gho_",
|
|
80
|
+
"github_pat_",
|
|
81
|
+
"xai-",
|
|
82
|
+
"pplx-",
|
|
83
|
+
"r8_",
|
|
84
|
+
"hf_",
|
|
85
|
+
"astra-",
|
|
86
|
+
"astrae-"
|
|
87
|
+
];
|
|
88
|
+
var MAX_HEADERS = 64;
|
|
89
|
+
var MAX_HEADER_VALUE_BYTES = 1024;
|
|
90
|
+
var MAX_TOTAL_BYTES = 16 * 1024;
|
|
91
|
+
function byteLength(s) {
|
|
92
|
+
let bytes = 0;
|
|
93
|
+
for (let i = 0; i < s.length; i++) {
|
|
94
|
+
const code = s.charCodeAt(i);
|
|
95
|
+
if (code < 128) bytes += 1;
|
|
96
|
+
else if (code < 2048) bytes += 2;
|
|
97
|
+
else if (code >= 55296 && code <= 56319) {
|
|
98
|
+
bytes += 4;
|
|
99
|
+
i++;
|
|
100
|
+
} else bytes += 3;
|
|
101
|
+
}
|
|
102
|
+
return bytes;
|
|
103
|
+
}
|
|
104
|
+
function extractApiKeyFormat(rawValue) {
|
|
105
|
+
if (!rawValue) return void 0;
|
|
106
|
+
let value = rawValue.trim();
|
|
107
|
+
const spaceIdx = value.indexOf(" ");
|
|
108
|
+
if (spaceIdx > 0) {
|
|
109
|
+
const scheme = value.slice(0, spaceIdx).toLowerCase();
|
|
110
|
+
if (scheme === "bearer" || scheme === "basic" || scheme === "token") {
|
|
111
|
+
value = value.slice(spaceIdx + 1).trim();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const lower = value.toLowerCase();
|
|
115
|
+
for (const prefix of KEY_FORMAT_PREFIXES) {
|
|
116
|
+
if (lower.startsWith(prefix)) {
|
|
117
|
+
return prefix;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return void 0;
|
|
121
|
+
}
|
|
122
|
+
function isSecretHeader(name) {
|
|
123
|
+
if (SECRET_HEADER_NAMES.has(name)) return true;
|
|
124
|
+
if (SECRET_HEADER_PREFIXES.some((p) => name.startsWith(p))) return true;
|
|
125
|
+
if (isSignatureHeader(name)) return true;
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
function isPlatformHeader(name) {
|
|
129
|
+
if (PLATFORM_HEADER_NAMES.has(name)) return true;
|
|
130
|
+
return PLATFORM_HEADER_PREFIXES.some((p) => name.startsWith(p));
|
|
131
|
+
}
|
|
132
|
+
function sanitizeHeaders(raw) {
|
|
133
|
+
const headers = {};
|
|
134
|
+
const platformHeaders = {};
|
|
135
|
+
let apiKeyFormat;
|
|
136
|
+
let totalBytes = 0;
|
|
137
|
+
let count = 0;
|
|
138
|
+
if (!raw) return { headers };
|
|
139
|
+
for (const rawName of Object.keys(raw)) {
|
|
140
|
+
if (count >= MAX_HEADERS) break;
|
|
141
|
+
const name = rawName.toLowerCase();
|
|
142
|
+
const rawValue = raw[rawName];
|
|
143
|
+
if (rawValue === void 0) continue;
|
|
144
|
+
const joined = Array.isArray(rawValue) ? rawValue.join(", ") : String(rawValue);
|
|
145
|
+
if (CREDENTIAL_HEADER_NAMES.has(name)) {
|
|
146
|
+
const fmt = extractApiKeyFormat(joined);
|
|
147
|
+
if (fmt && !apiKeyFormat) apiKeyFormat = fmt;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (isSecretHeader(name)) continue;
|
|
151
|
+
let value = joined;
|
|
152
|
+
if (byteLength(value) > MAX_HEADER_VALUE_BYTES) {
|
|
153
|
+
value = value.slice(0, MAX_HEADER_VALUE_BYTES);
|
|
154
|
+
}
|
|
155
|
+
const entryBytes = byteLength(name) + byteLength(value);
|
|
156
|
+
if (totalBytes + entryBytes > MAX_TOTAL_BYTES) continue;
|
|
157
|
+
totalBytes += entryBytes;
|
|
158
|
+
count++;
|
|
159
|
+
headers[name] = value;
|
|
160
|
+
if (isPlatformHeader(name)) platformHeaders[name] = value;
|
|
161
|
+
}
|
|
162
|
+
const result = { headers };
|
|
163
|
+
if (apiKeyFormat) result.apiKeyFormat = apiKeyFormat;
|
|
164
|
+
if (Object.keys(platformHeaders).length > 0) result.platformHeaders = platformHeaders;
|
|
165
|
+
return result;
|
|
166
|
+
}
|
|
167
|
+
function extractPlatformHeaders(sanitized) {
|
|
168
|
+
const out = {};
|
|
169
|
+
if (!sanitized) return out;
|
|
170
|
+
for (const name of Object.keys(sanitized)) {
|
|
171
|
+
if (isPlatformHeader(name.toLowerCase())) out[name.toLowerCase()] = sanitized[name];
|
|
172
|
+
}
|
|
173
|
+
return out;
|
|
174
|
+
}
|
|
175
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
176
|
+
0 && (module.exports = {
|
|
177
|
+
MAX_HEADERS,
|
|
178
|
+
MAX_HEADER_VALUE_BYTES,
|
|
179
|
+
MAX_TOTAL_BYTES,
|
|
180
|
+
extractApiKeyFormat,
|
|
181
|
+
extractPlatformHeaders,
|
|
182
|
+
sanitizeHeaders
|
|
183
|
+
});
|
|
184
|
+
//# sourceMappingURL=metadata-capture.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/metadata-capture.ts"],"sourcesContent":["/**\n * Maximal metadata capture — the shared, edge-safe sanitiser.\n *\n * AstraSync's thesis is total metadata capture: every header and connection\n * signal an inbound agent presents is a signal we want to keep. This module is\n * the ONE place that decides what is safe to store verbatim, what is a secret\n * to drop, and what credential headers reduce to a safe *format prefix* (a\n * platform signal, never the secret itself).\n *\n * It runs in three places that must agree byte-for-byte:\n * - the edge adapter (`@astrasyncai/adapter-lambda`) — the richest capture\n * point (full CloudFront header arrays),\n * - the SDK adapters (express / nextjs) — for SDK-only merchants with no edge,\n * - (indirectly) the backend, which stores whatever the above forward.\n *\n * Pure — no Node built-ins, no I/O — so it is safe in Lambda@Edge, the browser\n * build, and Deno. Capture is cheap: at the edge and SDK it is local string\n * work, and the backend stores it on a fire-and-forget event insert, so it adds\n * ZERO verify-access decision latency. Detection (which few signals imply which\n * vendor) is a separate, curated concern — see `platform-signatures.ts`.\n */\n\n/**\n * The captured, sanitised view of an inbound agent's request metadata. Attached\n * to verify-access / beacon payloads as `callerMetadata.observedMetadata`.\n */\nexport interface ObservedMetadata {\n /** Header name → value, secrets removed, verbatim otherwise. */\n headers: Record<string, string>;\n /**\n * Safe key-format prefix of a credential header (e.g. `sk-ant-api03`,\n * `sk-proj`, `AIza`) — a platform signal, NEVER the secret. Absent if no\n * credential header was present or none matched a known format.\n */\n apiKeyFormat?: string;\n /**\n * The subset of `headers` that are known platform-signal headers\n * (`openai-organization`, `x-goog-user-project`, `anthropic-version`, …),\n * pulled out for convenient detection + display. Values are the same\n * sanitised strings as in `headers`.\n */\n platformHeaders?: Record<string, string>;\n /**\n * Connection-layer signals the edge can see but the SDK cannot (IP, ASN,\n * country, TLS version/cipher, HTTP version, device class). Populated by the\n * edge adapter's `collectConnectionMetadata`; absent for SDK-only capture.\n */\n connection?: Record<string, string>;\n}\n\n/**\n * Result of sanitising a raw header map. `headers` is always present (possibly\n * empty); `apiKeyFormat` / `platformHeaders` only when a signal was found.\n */\nexport interface SanitizeHeadersResult {\n headers: Record<string, string>;\n apiKeyFormat?: string;\n platformHeaders?: Record<string, string>;\n}\n\n/**\n * Header names dropped entirely — genuine secrets or our own credentials that\n * describe the request's transport, not the agent. Matched case-insensitively\n * on the exact header name.\n */\nconst SECRET_HEADER_NAMES = new Set([\n 'cookie',\n 'set-cookie',\n 'x-csrf-token',\n 'proxy-authorization',\n 'x-astrasync-signature',\n 'x-astrasync-secret',\n 'x-hub-signature',\n 'x-hub-signature-256',\n]);\n\n/**\n * Header-name prefixes dropped entirely — our own credential / signing surface\n * (`x-astrasync-*`, `kya-*`) must never round-trip into stored metadata.\n */\nconst SECRET_HEADER_PREFIXES = ['x-astrasync-', 'kya-', 'kya_'];\n\n/**\n * Credential headers whose VALUE is a secret but whose FORMAT is a signal. We\n * keep the leading format prefix (up to the random tail) and drop the rest.\n */\nconst CREDENTIAL_HEADER_NAMES = new Set([\n 'authorization',\n 'x-api-key',\n 'x-goog-api-key',\n 'api-key',\n]);\n\n/**\n * Header names that carry a raw signature value (drop the value; the presence\n * is not worth a bloat risk). Matched as a suffix so `x-*-signature` variants\n * are covered without an exhaustive list.\n */\nfunction isSignatureHeader(name: string): boolean {\n return name === 'signature' || name.endsWith('-signature') || name.endsWith('-signature-256');\n}\n\n/**\n * Known platform-signal headers — pulled into `platformHeaders` for convenient\n * detection + display. Exact names plus a few prefixes (`x-goog-`,\n * `x-stainless-` — the OpenAI/Anthropic SDK client fingerprint headers).\n */\nconst PLATFORM_HEADER_NAMES = new Set([\n 'openai-organization',\n 'openai-project',\n 'openai-version',\n 'openai-processing-ms',\n 'anthropic-version',\n 'anthropic-beta',\n 'x-goog-user-project',\n 'x-goog-api-client',\n 'user-agent',\n 'x-agent-card-url',\n 'x-agent-provider',\n]);\nconst PLATFORM_HEADER_PREFIXES = ['x-goog-', 'x-stainless-', 'x-anthropic-', 'x-openai-'];\n\n/**\n * Known API-key format prefixes, longest-first so `sk-ant-api03` wins over\n * `sk-ant` and `sk-`. Each is the SAFE, non-secret leading token of a\n * credential; the random tail is never captured.\n */\nconst KEY_FORMAT_PREFIXES = [\n 'sk-ant-api03',\n 'sk-ant-api',\n 'sk-ant',\n 'sk-proj',\n 'sk-svcacct',\n 'sk-or-v1',\n 'sk-or',\n 'sk-lf',\n 'sk-',\n 'gsk_',\n 'aiza',\n 'ya29',\n 'ghp_',\n 'gho_',\n 'github_pat_',\n 'xai-',\n 'pplx-',\n 'r8_',\n 'hf_',\n 'astra-',\n 'astrae-',\n];\n\n/** Size caps — the only guard against jsonb bloat from a hostile POST. */\nexport const MAX_HEADERS = 64;\nexport const MAX_HEADER_VALUE_BYTES = 1024;\nexport const MAX_TOTAL_BYTES = 16 * 1024;\n\nfunction byteLength(s: string): number {\n // Edge-safe (no Buffer): count UTF-8 bytes without allocating a Buffer.\n let bytes = 0;\n for (let i = 0; i < s.length; i++) {\n const code = s.charCodeAt(i);\n if (code < 0x80) bytes += 1;\n else if (code < 0x800) bytes += 2;\n else if (code >= 0xd800 && code <= 0xdbff) {\n bytes += 4;\n i++; // surrogate pair\n } else bytes += 3;\n }\n return bytes;\n}\n\n/**\n * Extract the safe key-format prefix from a credential header value. Strips a\n * leading `Bearer ` / `Basic ` scheme, then returns the longest known prefix\n * that the (lowercased) value starts with. Returns undefined for opaque values\n * (e.g. a bare UUID token) so we never guess a format we don't recognise.\n */\nexport function extractApiKeyFormat(rawValue: string): string | undefined {\n if (!rawValue) return undefined;\n let value = rawValue.trim();\n const spaceIdx = value.indexOf(' ');\n if (spaceIdx > 0) {\n const scheme = value.slice(0, spaceIdx).toLowerCase();\n if (scheme === 'bearer' || scheme === 'basic' || scheme === 'token') {\n value = value.slice(spaceIdx + 1).trim();\n }\n }\n const lower = value.toLowerCase();\n for (const prefix of KEY_FORMAT_PREFIXES) {\n if (lower.startsWith(prefix)) {\n // Return the prefix as-declared in the list (canonical casing).\n return prefix;\n }\n }\n return undefined;\n}\n\nfunction isSecretHeader(name: string): boolean {\n if (SECRET_HEADER_NAMES.has(name)) return true;\n if (SECRET_HEADER_PREFIXES.some((p) => name.startsWith(p))) return true;\n if (isSignatureHeader(name)) return true;\n return false;\n}\n\nfunction isPlatformHeader(name: string): boolean {\n if (PLATFORM_HEADER_NAMES.has(name)) return true;\n return PLATFORM_HEADER_PREFIXES.some((p) => name.startsWith(p));\n}\n\n/**\n * Sanitise a raw header map for storage. Deny-lists genuine secrets, reduces\n * credential headers to a safe format prefix, keeps everything else verbatim,\n * and enforces the size caps that bound jsonb growth.\n *\n * Accepts any string→(string|string[]|undefined) map (Node's `req.headers`,\n * CloudFront's flattened headers, a plain object). Multi-value headers are\n * joined with `, ` per RFC 7230.\n */\nexport function sanitizeHeaders(\n raw: Record<string, string | string[] | undefined> | undefined | null\n): SanitizeHeadersResult {\n const headers: Record<string, string> = {};\n const platformHeaders: Record<string, string> = {};\n let apiKeyFormat: string | undefined;\n let totalBytes = 0;\n let count = 0;\n\n if (!raw) return { headers };\n\n for (const rawName of Object.keys(raw)) {\n if (count >= MAX_HEADERS) break;\n const name = rawName.toLowerCase();\n const rawValue = raw[rawName];\n if (rawValue === undefined) continue;\n const joined = Array.isArray(rawValue) ? rawValue.join(', ') : String(rawValue);\n\n // Credential headers: capture the format prefix, drop the secret.\n if (CREDENTIAL_HEADER_NAMES.has(name)) {\n const fmt = extractApiKeyFormat(joined);\n if (fmt && !apiKeyFormat) apiKeyFormat = fmt;\n continue;\n }\n\n // Genuine secrets: drop entirely.\n if (isSecretHeader(name)) continue;\n\n // Everything else describing the agent: keep, bounded.\n let value = joined;\n if (byteLength(value) > MAX_HEADER_VALUE_BYTES) {\n value = value.slice(0, MAX_HEADER_VALUE_BYTES);\n }\n const entryBytes = byteLength(name) + byteLength(value);\n if (totalBytes + entryBytes > MAX_TOTAL_BYTES) continue;\n totalBytes += entryBytes;\n count++;\n headers[name] = value;\n if (isPlatformHeader(name)) platformHeaders[name] = value;\n }\n\n const result: SanitizeHeadersResult = { headers };\n if (apiKeyFormat) result.apiKeyFormat = apiKeyFormat;\n if (Object.keys(platformHeaders).length > 0) result.platformHeaders = platformHeaders;\n return result;\n}\n\n/**\n * Pull the known platform-signal headers out of an ALREADY-sanitised header\n * map. `sanitizeHeaders` already computes this as a by-product; this standalone\n * form is exported for call-sites (edge, tests) that hold a sanitised map and\n * want only the platform subset. Never re-run over raw (unsanitised) headers.\n */\nexport function extractPlatformHeaders(\n sanitized: Record<string, string> | undefined | null\n): Record<string, string> {\n const out: Record<string, string> = {};\n if (!sanitized) return out;\n for (const name of Object.keys(sanitized)) {\n if (isPlatformHeader(name.toLowerCase())) out[name.toLowerCase()] = sanitized[name];\n }\n return out;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAiEA,IAAM,sBAAsB,oBAAI,IAAI;AAAA,EAClC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAMD,IAAM,yBAAyB,CAAC,gBAAgB,QAAQ,MAAM;AAM9D,IAAM,0BAA0B,oBAAI,IAAI;AAAA,EACtC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAOD,SAAS,kBAAkB,MAAuB;AAChD,SAAO,SAAS,eAAe,KAAK,SAAS,YAAY,KAAK,KAAK,SAAS,gBAAgB;AAC9F;AAOA,IAAM,wBAAwB,oBAAI,IAAI;AAAA,EACpC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AACD,IAAM,2BAA2B,CAAC,WAAW,gBAAgB,gBAAgB,WAAW;AAOxF,IAAM,sBAAsB;AAAA,EAC1B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAGO,IAAM,cAAc;AACpB,IAAM,yBAAyB;AAC/B,IAAM,kBAAkB,KAAK;AAEpC,SAAS,WAAW,GAAmB;AAErC,MAAI,QAAQ;AACZ,WAAS,IAAI,GAAG,IAAI,EAAE,QAAQ,KAAK;AACjC,UAAM,OAAO,EAAE,WAAW,CAAC;AAC3B,QAAI,OAAO,IAAM,UAAS;AAAA,aACjB,OAAO,KAAO,UAAS;AAAA,aACvB,QAAQ,SAAU,QAAQ,OAAQ;AACzC,eAAS;AACT;AAAA,IACF,MAAO,UAAS;AAAA,EAClB;AACA,SAAO;AACT;AAQO,SAAS,oBAAoB,UAAsC;AACxE,MAAI,CAAC,SAAU,QAAO;AACtB,MAAI,QAAQ,SAAS,KAAK;AAC1B,QAAM,WAAW,MAAM,QAAQ,GAAG;AAClC,MAAI,WAAW,GAAG;AAChB,UAAM,SAAS,MAAM,MAAM,GAAG,QAAQ,EAAE,YAAY;AACpD,QAAI,WAAW,YAAY,WAAW,WAAW,WAAW,SAAS;AACnE,cAAQ,MAAM,MAAM,WAAW,CAAC,EAAE,KAAK;AAAA,IACzC;AAAA,EACF;AACA,QAAM,QAAQ,MAAM,YAAY;AAChC,aAAW,UAAU,qBAAqB;AACxC,QAAI,MAAM,WAAW,MAAM,GAAG;AAE5B,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,eAAe,MAAuB;AAC7C,MAAI,oBAAoB,IAAI,IAAI,EAAG,QAAO;AAC1C,MAAI,uBAAuB,KAAK,CAAC,MAAM,KAAK,WAAW,CAAC,CAAC,EAAG,QAAO;AACnE,MAAI,kBAAkB,IAAI,EAAG,QAAO;AACpC,SAAO;AACT;AAEA,SAAS,iBAAiB,MAAuB;AAC/C,MAAI,sBAAsB,IAAI,IAAI,EAAG,QAAO;AAC5C,SAAO,yBAAyB,KAAK,CAAC,MAAM,KAAK,WAAW,CAAC,CAAC;AAChE;AAWO,SAAS,gBACd,KACuB;AACvB,QAAM,UAAkC,CAAC;AACzC,QAAM,kBAA0C,CAAC;AACjD,MAAI;AACJ,MAAI,aAAa;AACjB,MAAI,QAAQ;AAEZ,MAAI,CAAC,IAAK,QAAO,EAAE,QAAQ;AAE3B,aAAW,WAAW,OAAO,KAAK,GAAG,GAAG;AACtC,QAAI,SAAS,YAAa;AAC1B,UAAM,OAAO,QAAQ,YAAY;AACjC,UAAM,WAAW,IAAI,OAAO;AAC5B,QAAI,aAAa,OAAW;AAC5B,UAAM,SAAS,MAAM,QAAQ,QAAQ,IAAI,SAAS,KAAK,IAAI,IAAI,OAAO,QAAQ;AAG9E,QAAI,wBAAwB,IAAI,IAAI,GAAG;AACrC,YAAM,MAAM,oBAAoB,MAAM;AACtC,UAAI,OAAO,CAAC,aAAc,gBAAe;AACzC;AAAA,IACF;AAGA,QAAI,eAAe,IAAI,EAAG;AAG1B,QAAI,QAAQ;AACZ,QAAI,WAAW,KAAK,IAAI,wBAAwB;AAC9C,cAAQ,MAAM,MAAM,GAAG,sBAAsB;AAAA,IAC/C;AACA,UAAM,aAAa,WAAW,IAAI,IAAI,WAAW,KAAK;AACtD,QAAI,aAAa,aAAa,gBAAiB;AAC/C,kBAAc;AACd;AACA,YAAQ,IAAI,IAAI;AAChB,QAAI,iBAAiB,IAAI,EAAG,iBAAgB,IAAI,IAAI;AAAA,EACtD;AAEA,QAAM,SAAgC,EAAE,QAAQ;AAChD,MAAI,aAAc,QAAO,eAAe;AACxC,MAAI,OAAO,KAAK,eAAe,EAAE,SAAS,EAAG,QAAO,kBAAkB;AACtE,SAAO;AACT;AAQO,SAAS,uBACd,WACwB;AACxB,QAAM,MAA8B,CAAC;AACrC,MAAI,CAAC,UAAW,QAAO;AACvB,aAAW,QAAQ,OAAO,KAAK,SAAS,GAAG;AACzC,QAAI,iBAAiB,KAAK,YAAY,CAAC,EAAG,KAAI,KAAK,YAAY,CAAC,IAAI,UAAU,IAAI;AAAA,EACpF;AACA,SAAO;AACT;","names":[]}
|