agentlock-shared 0.2.0 → 0.3.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.
Files changed (169) hide show
  1. package/dist/__tests__/billing.test.d.ts +2 -0
  2. package/dist/__tests__/billing.test.d.ts.map +1 -0
  3. package/dist/__tests__/billing.test.js +31 -0
  4. package/dist/__tests__/billing.test.js.map +1 -0
  5. package/dist/__tests__/dns-pinning.test.d.ts +2 -0
  6. package/dist/__tests__/dns-pinning.test.d.ts.map +1 -0
  7. package/dist/__tests__/dns-pinning.test.js +33 -0
  8. package/dist/__tests__/dns-pinning.test.js.map +1 -0
  9. package/dist/__tests__/llm-classifier-cache-store.test.d.ts +2 -0
  10. package/dist/__tests__/llm-classifier-cache-store.test.d.ts.map +1 -0
  11. package/dist/__tests__/llm-classifier-cache-store.test.js +65 -0
  12. package/dist/__tests__/llm-classifier-cache-store.test.js.map +1 -0
  13. package/dist/__tests__/llm-classifier-cache.test.d.ts +2 -0
  14. package/dist/__tests__/llm-classifier-cache.test.d.ts.map +1 -0
  15. package/dist/__tests__/llm-classifier-cache.test.js +44 -0
  16. package/dist/__tests__/llm-classifier-cache.test.js.map +1 -0
  17. package/dist/__tests__/llm-classifier.test.d.ts +2 -0
  18. package/dist/__tests__/llm-classifier.test.d.ts.map +1 -0
  19. package/dist/__tests__/llm-classifier.test.js +167 -0
  20. package/dist/__tests__/llm-classifier.test.js.map +1 -0
  21. package/dist/__tests__/plans-classifier-limits.test.d.ts +2 -0
  22. package/dist/__tests__/plans-classifier-limits.test.d.ts.map +1 -0
  23. package/dist/__tests__/plans-classifier-limits.test.js +22 -0
  24. package/dist/__tests__/plans-classifier-limits.test.js.map +1 -0
  25. package/dist/__tests__/policy-category-floor.test.d.ts +2 -0
  26. package/dist/__tests__/policy-category-floor.test.d.ts.map +1 -0
  27. package/dist/__tests__/policy-category-floor.test.js +46 -0
  28. package/dist/__tests__/policy-category-floor.test.js.map +1 -0
  29. package/dist/__tests__/policy-claude-bash.test.d.ts +2 -0
  30. package/dist/__tests__/policy-claude-bash.test.d.ts.map +1 -0
  31. package/dist/__tests__/policy-claude-bash.test.js +401 -0
  32. package/dist/__tests__/policy-claude-bash.test.js.map +1 -0
  33. package/dist/__tests__/policy-llm-floor.test.d.ts +2 -0
  34. package/dist/__tests__/policy-llm-floor.test.d.ts.map +1 -0
  35. package/dist/__tests__/policy-llm-floor.test.js +107 -0
  36. package/dist/__tests__/policy-llm-floor.test.js.map +1 -0
  37. package/dist/__tests__/policy-ssh-e2e.test.d.ts +2 -0
  38. package/dist/__tests__/policy-ssh-e2e.test.d.ts.map +1 -0
  39. package/dist/__tests__/policy-ssh-e2e.test.js +89 -0
  40. package/dist/__tests__/policy-ssh-e2e.test.js.map +1 -0
  41. package/dist/__tests__/policy-ssh-sessions.test.d.ts +2 -0
  42. package/dist/__tests__/policy-ssh-sessions.test.d.ts.map +1 -0
  43. package/dist/__tests__/policy-ssh-sessions.test.js +139 -0
  44. package/dist/__tests__/policy-ssh-sessions.test.js.map +1 -0
  45. package/dist/__tests__/policy-ssh.test.d.ts +2 -0
  46. package/dist/__tests__/policy-ssh.test.d.ts.map +1 -0
  47. package/dist/__tests__/policy-ssh.test.js +180 -0
  48. package/dist/__tests__/policy-ssh.test.js.map +1 -0
  49. package/dist/__tests__/policy.test.js +400 -2
  50. package/dist/__tests__/policy.test.js.map +1 -1
  51. package/dist/__tests__/redact.test.js +76 -0
  52. package/dist/__tests__/redact.test.js.map +1 -1
  53. package/dist/__tests__/signing.test.js +89 -0
  54. package/dist/__tests__/signing.test.js.map +1 -1
  55. package/dist/__tests__/ssh-fingerprint.test.d.ts +2 -0
  56. package/dist/__tests__/ssh-fingerprint.test.d.ts.map +1 -0
  57. package/dist/__tests__/ssh-fingerprint.test.js +19 -0
  58. package/dist/__tests__/ssh-fingerprint.test.js.map +1 -0
  59. package/dist/__tests__/vpn-route.test.d.ts +2 -0
  60. package/dist/__tests__/vpn-route.test.d.ts.map +1 -0
  61. package/dist/__tests__/vpn-route.test.js +72 -0
  62. package/dist/__tests__/vpn-route.test.js.map +1 -0
  63. package/dist/__tests__/wireguard.test.d.ts +2 -0
  64. package/dist/__tests__/wireguard.test.d.ts.map +1 -0
  65. package/dist/__tests__/wireguard.test.js +114 -0
  66. package/dist/__tests__/wireguard.test.js.map +1 -0
  67. package/dist/billing.d.ts +12 -0
  68. package/dist/billing.d.ts.map +1 -0
  69. package/dist/billing.js +41 -0
  70. package/dist/billing.js.map +1 -0
  71. package/dist/crypto.d.ts +5 -0
  72. package/dist/crypto.d.ts.map +1 -1
  73. package/dist/crypto.js +80 -23
  74. package/dist/crypto.js.map +1 -1
  75. package/dist/dns-pinning.d.ts +28 -0
  76. package/dist/dns-pinning.d.ts.map +1 -0
  77. package/dist/dns-pinning.js +113 -0
  78. package/dist/dns-pinning.js.map +1 -0
  79. package/dist/index.d.ts +6 -0
  80. package/dist/index.d.ts.map +1 -1
  81. package/dist/index.js +9 -0
  82. package/dist/index.js.map +1 -1
  83. package/dist/llm-classifier-cache-store.d.ts +49 -0
  84. package/dist/llm-classifier-cache-store.d.ts.map +1 -0
  85. package/dist/llm-classifier-cache-store.js +63 -0
  86. package/dist/llm-classifier-cache-store.js.map +1 -0
  87. package/dist/llm-classifier-cache.d.ts +6 -0
  88. package/dist/llm-classifier-cache.d.ts.map +1 -0
  89. package/dist/llm-classifier-cache.js +52 -0
  90. package/dist/llm-classifier-cache.js.map +1 -0
  91. package/dist/llm-classifier.d.ts +29 -0
  92. package/dist/llm-classifier.d.ts.map +1 -0
  93. package/dist/llm-classifier.js +191 -0
  94. package/dist/llm-classifier.js.map +1 -0
  95. package/dist/observability.d.ts +36 -0
  96. package/dist/observability.d.ts.map +1 -0
  97. package/dist/observability.js +75 -0
  98. package/dist/observability.js.map +1 -0
  99. package/dist/plans.d.ts +17 -0
  100. package/dist/plans.d.ts.map +1 -1
  101. package/dist/plans.js +36 -14
  102. package/dist/plans.js.map +1 -1
  103. package/dist/policy.d.ts +173 -3
  104. package/dist/policy.d.ts.map +1 -1
  105. package/dist/policy.js +910 -42
  106. package/dist/policy.js.map +1 -1
  107. package/dist/redact.d.ts.map +1 -1
  108. package/dist/redact.js +83 -3
  109. package/dist/redact.js.map +1 -1
  110. package/dist/regex-safety.d.ts +21 -0
  111. package/dist/regex-safety.d.ts.map +1 -0
  112. package/dist/regex-safety.js +49 -0
  113. package/dist/regex-safety.js.map +1 -0
  114. package/dist/sanitize.d.ts +31 -0
  115. package/dist/sanitize.d.ts.map +1 -0
  116. package/dist/sanitize.js +54 -0
  117. package/dist/sanitize.js.map +1 -0
  118. package/dist/schemas.d.ts +202 -10
  119. package/dist/schemas.d.ts.map +1 -1
  120. package/dist/schemas.js +91 -1
  121. package/dist/schemas.js.map +1 -1
  122. package/dist/signing.d.ts +15 -0
  123. package/dist/signing.d.ts.map +1 -1
  124. package/dist/signing.js +53 -4
  125. package/dist/signing.js.map +1 -1
  126. package/dist/ssh-fingerprint.d.ts +10 -0
  127. package/dist/ssh-fingerprint.d.ts.map +1 -0
  128. package/dist/ssh-fingerprint.js +52 -0
  129. package/dist/ssh-fingerprint.js.map +1 -0
  130. package/dist/ssrf.d.ts +36 -0
  131. package/dist/ssrf.d.ts.map +1 -0
  132. package/dist/ssrf.js +140 -0
  133. package/dist/ssrf.js.map +1 -0
  134. package/dist/types.d.ts +130 -0
  135. package/dist/types.d.ts.map +1 -1
  136. package/dist/wireguard.d.ts +63 -0
  137. package/dist/wireguard.d.ts.map +1 -0
  138. package/dist/wireguard.js +226 -0
  139. package/dist/wireguard.js.map +1 -0
  140. package/package.json +42 -29
  141. package/.turbo/turbo-build.log +0 -4
  142. package/.turbo/turbo-test.log +0 -76
  143. package/dist/__tests__/content-crypto.test.d.ts +0 -2
  144. package/dist/__tests__/content-crypto.test.d.ts.map +0 -1
  145. package/dist/__tests__/content-crypto.test.js +0 -117
  146. package/dist/__tests__/content-crypto.test.js.map +0 -1
  147. package/dist/__tests__/signing.test (# Edit conflict 2026-04-01 z3etfmC #).js +0 -51
  148. package/dist/__tests__/signing.test.js (# Edit conflict 2026-04-01 4rndy9C #).map +0 -1
  149. package/dist/content-crypto.d.ts +0 -24
  150. package/dist/content-crypto.d.ts.map +0 -1
  151. package/dist/content-crypto.js +0 -58
  152. package/dist/content-crypto.js.map +0 -1
  153. package/src/__tests__/crypto.test.ts +0 -169
  154. package/src/__tests__/messaging.test.ts +0 -83
  155. package/src/__tests__/policy.test.ts +0 -222
  156. package/src/__tests__/redact.test.ts +0 -41
  157. package/src/__tests__/signing.test.ts +0 -55
  158. package/src/crypto.ts +0 -235
  159. package/src/index.ts +0 -8
  160. package/src/mcp-catalog.ts +0 -181
  161. package/src/plans.ts +0 -116
  162. package/src/policy.ts +0 -216
  163. package/src/redact.ts +0 -131
  164. package/src/schemas.ts +0 -121
  165. package/src/signing.ts +0 -120
  166. package/src/types.ts +0 -213
  167. package/test-gateway.mjs +0 -47
  168. package/tsconfig.json +0 -10
  169. package/vitest.config.ts +0 -8
package/src/policy.ts DELETED
@@ -1,216 +0,0 @@
1
- import type {
2
- PolicyRules,
3
- AgentActionRequest,
4
- PolicyEvaluationResult,
5
- RiskLevel,
6
- PolicyDecision,
7
- } from './types.js';
8
- import { redact } from './redact.js';
9
-
10
- const RISK_MAP: Record<string, RiskLevel> = {
11
- read: 'low',
12
- write: 'medium',
13
- financial: 'high',
14
- admin: 'critical',
15
- };
16
-
17
- export const DEFAULT_POLICY_RULES: PolicyRules = {
18
- defaultMode: 'require_approval',
19
- rules: [
20
- { action_type: 'read', decision: 'ALLOW' },
21
- { action_type: 'write', decision: 'REQUIRE_APPROVAL' },
22
- { action_type: 'financial', decision: 'REQUIRE_APPROVAL' },
23
- { action_type: 'admin', decision: 'BLOCK' },
24
- { tool: 'mcp.list_tools', decision: 'ALLOW' },
25
- ],
26
- http: {
27
- allowedDomains: [],
28
- allowedMethods: ['GET', 'POST', 'PUT', 'DELETE'],
29
- blockList: [],
30
- },
31
- limits: {
32
- maxActionsPerHour: 100, // Enforced in gateway via check_rate_limit RPC
33
- },
34
- };
35
-
36
- // Default HTTP rules when rules.http is not configured
37
- const DEFAULT_HTTP_RULES = {
38
- allowedDomains: [] as string[],
39
- allowedMethods: ['GET', 'POST', 'PUT', 'DELETE'],
40
- blockList: [] as string[],
41
- };
42
-
43
- export function evaluatePolicy(
44
- action: AgentActionRequest,
45
- rules: PolicyRules
46
- ): PolicyEvaluationResult {
47
- // Block unknown action types (defense-in-depth)
48
- if (!(action.action_type in RISK_MAP)) {
49
- return { decision: 'BLOCK', risk_level: 'critical', reason: `Unknown action type: ${action.action_type}` };
50
- }
51
-
52
- // SECURITY: Normalize tool name to lowercase to prevent case-sensitivity
53
- // bypasses (e.g., "Http.get" skipping HTTP domain allowlist checks).
54
- const normalizedTool = action.tool.toLowerCase();
55
-
56
- const risk_level = RISK_MAP[action.action_type] ?? 'medium';
57
-
58
- // Browser tools: browser.open always requires approval
59
- if (normalizedTool.startsWith('browser.')) {
60
- if (normalizedTool === 'browser.open') {
61
- return {
62
- decision: 'REQUIRE_APPROVAL',
63
- risk_level: 'medium',
64
- reason: 'Browser sessions always require approval to start',
65
- };
66
- }
67
- // Other browser.* actions with a valid session are handled at the gateway
68
- // level (auto-approved). If they reach the policy engine without a session,
69
- // they should be blocked.
70
- return {
71
- decision: 'BLOCK',
72
- risk_level: 'medium',
73
- reason: 'Browser actions require an active session (use browser.open first)',
74
- };
75
- }
76
-
77
- // MCP tools: list_tools handled via default rules (can be overridden by custom policies)
78
-
79
- // HTTP tool checks: always enforce domain/method restrictions
80
- const isHttpTool = normalizedTool.split('.')[0] === 'http';
81
- if (isHttpTool) {
82
- const httpRules = rules.http ?? DEFAULT_HTTP_RULES;
83
- const url = action.payload.url as string | undefined;
84
- if (!url) {
85
- // SECURITY: HTTP actions without a URL must be blocked at the policy level.
86
- // The connector would also reject it, but policy should be the first gate.
87
- return { decision: 'BLOCK', risk_level: 'critical', reason: 'HTTP actions require a URL in payload' };
88
- }
89
- if (url) {
90
- try {
91
- const domain = new URL(url).hostname.replace(/\.$/, '').toLowerCase();
92
- // Use exact match or proper subdomain match (preceded by a dot)
93
- // to prevent "not-trusted.com" from matching allowlist entry "trusted.com"
94
- // Both sides are normalized: lowercase + trailing-dot stripped.
95
- const matchesDomain = (d: string, pattern: string) => {
96
- const normalizedPattern = pattern.replace(/\.$/, '').toLowerCase();
97
- return d === normalizedPattern || d.endsWith('.' + normalizedPattern);
98
- };
99
- if (httpRules.blockList.some((b) => matchesDomain(domain, b))) {
100
- return { decision: 'BLOCK', risk_level: 'critical', reason: `Domain ${domain} is in block list` };
101
- }
102
- if (httpRules.allowedDomains.length === 0) {
103
- // No allowlist configured: safe default is REQUIRE_APPROVAL, not ALLOW.
104
- // This prevents agents from exfiltrating data to arbitrary domains.
105
- return {
106
- decision: 'REQUIRE_APPROVAL',
107
- risk_level,
108
- reason: 'HTTP allowlist not configured — approval required for all HTTP calls',
109
- };
110
- }
111
- if (!httpRules.allowedDomains.some((d) => matchesDomain(domain, d))) {
112
- return { decision: 'BLOCK', risk_level, reason: `Domain ${domain} not in allowed list` };
113
- }
114
- } catch {
115
- return { decision: 'BLOCK', risk_level: 'critical', reason: 'Invalid URL' };
116
- }
117
- }
118
- const method = (action.payload.method as string | undefined)?.toUpperCase();
119
- if (method && !httpRules.allowedMethods.includes(method)) {
120
- return { decision: 'BLOCK', risk_level, reason: `HTTP method ${method} not allowed` };
121
- }
122
- }
123
-
124
- if (
125
- rules.limits?.maxCostPerAction !== undefined &&
126
- action.cost_estimate !== undefined &&
127
- action.cost_estimate > rules.limits.maxCostPerAction
128
- ) {
129
- return {
130
- decision: 'BLOCK',
131
- risk_level: 'high',
132
- reason: `Cost estimate ${action.cost_estimate} exceeds limit ${rules.limits.maxCostPerAction}`,
133
- };
134
- }
135
-
136
- // Most specific: tool-specific rule (case-insensitive to prevent bypasses)
137
- let matched = rules.rules.find((r) => r.tool !== undefined && r.tool.toLowerCase() === normalizedTool);
138
- // Then action-type rule
139
- if (!matched) matched = rules.rules.find((r) => r.action_type === action.action_type);
140
-
141
- if (matched) {
142
- let finalDecision = matched.decision;
143
- // SECURITY: Never auto-allow admin/financial actions even via explicit rules
144
- if (finalDecision === 'ALLOW' && ['admin', 'financial'].includes(action.action_type)) {
145
- finalDecision = 'REQUIRE_APPROVAL';
146
- }
147
- return {
148
- decision: finalDecision,
149
- risk_level,
150
- reason: `Matched rule: ${matched.action_type ?? matched.tool}`,
151
- matched_rule: matched,
152
- };
153
- }
154
-
155
- const defaultDecision: PolicyDecision =
156
- rules.defaultMode === 'allow' ? 'ALLOW' : rules.defaultMode === 'block' ? 'BLOCK' : 'REQUIRE_APPROVAL';
157
-
158
- // SECURITY: Never auto-allow high-risk actions via permissive default mode
159
- if (defaultDecision === 'ALLOW' && ['admin', 'financial'].includes(action.action_type)) {
160
- return { decision: 'REQUIRE_APPROVAL', risk_level, reason: 'High-risk action types always require approval even with permissive default policy' };
161
- }
162
-
163
- return { decision: defaultDecision, risk_level, reason: 'Default policy' };
164
- }
165
-
166
- export function buildActionPreview(action: AgentActionRequest): {
167
- summary: string;
168
- target?: string;
169
- impact?: string;
170
- cost_estimate?: number;
171
- } {
172
- const normalizedTool = action.tool.toLowerCase();
173
- let summary = `${action.action_type.toUpperCase()} via ${action.tool}`;
174
- let target: string | undefined;
175
-
176
- if (normalizedTool.split('.')[0] === 'http') {
177
- const url = action.payload.url as string | undefined;
178
- const method = action.payload.method as string | undefined;
179
- if (url) {
180
- try {
181
- target = new URL(url).hostname;
182
- } catch {
183
- target = url;
184
- }
185
- summary = `${method?.toUpperCase() ?? 'HTTP'} request to ${target}`;
186
- }
187
- } else if (normalizedTool === 'browser.open') {
188
- const url = action.payload.url as string | undefined;
189
- if (url) {
190
- try {
191
- target = new URL(url).hostname;
192
- } catch {
193
- target = url;
194
- }
195
- summary = `Open browser session to ${target}`;
196
- } else {
197
- summary = 'Open browser session';
198
- }
199
- } else if (normalizedTool === 'mcp.list_tools') {
200
- const server = action.payload.server as string | undefined;
201
- target = server;
202
- summary = `List available tools on MCP server "${server ?? 'unknown'}"`;
203
- } else if (normalizedTool === 'mcp.call_tool') {
204
- const server = action.payload.server as string | undefined;
205
- const method = action.payload.method as string | undefined;
206
- target = server;
207
- summary = `Call "${method ?? 'unknown'}" on MCP server "${server ?? 'unknown'}"`;
208
- }
209
-
210
- return {
211
- summary,
212
- target,
213
- impact: action.action_type === 'write' ? 'Data will be modified' : undefined,
214
- cost_estimate: action.cost_estimate,
215
- };
216
- }
package/src/redact.ts DELETED
@@ -1,131 +0,0 @@
1
- // Exact-match field names (checked after lowercasing)
2
- const SECRET_FIELDS = new Set([
3
- 'authorization',
4
- 'api_key',
5
- 'apikey',
6
- 'api-key',
7
- 'token',
8
- 'secret',
9
- 'password',
10
- 'passwd',
11
- 'private_key',
12
- 'privatekey',
13
- 'access_token',
14
- 'refresh_token',
15
- 'client_secret',
16
- 'x-api-key',
17
- 'x-auth-token',
18
- 'credentials',
19
- 'bearer',
20
- 'session_token',
21
- 'session_key',
22
- 'cookie',
23
- 'set-cookie',
24
- 'aws_secret_access_key',
25
- 'aws_session_token',
26
- 'database_url',
27
- 'connection_string',
28
- 'private-key',
29
- 'master_key',
30
- 'encryption_key',
31
- 'signing_key',
32
- 'service_role_key',
33
- 'supabase_service_role_key',
34
- ]);
35
-
36
- // Substring patterns: if the lowercased key contains any of these, redact
37
- const SECRET_SUBSTRINGS = [
38
- 'secret',
39
- 'password',
40
- 'passwd',
41
- 'token',
42
- 'api_key',
43
- 'apikey',
44
- 'private_key',
45
- 'privatekey',
46
- 'credential',
47
- 'authorization',
48
- 'auth_key',
49
- 'master_key',
50
- 'encryption_key',
51
- 'signing_key',
52
- 'connection_string',
53
- 'database_url',
54
- 'access_key',
55
- 'auth_token',
56
- 'refresh_key',
57
- 'session_secret',
58
- 'webhook_secret',
59
- 'jwt',
60
- 'oauth',
61
- 'ssn',
62
- 'credit_card',
63
- 'routing_number',
64
- ];
65
-
66
- const REDACTED = '[REDACTED]';
67
-
68
- // Value-based patterns to detect secrets regardless of field name
69
- const SECRET_VALUE_PATTERNS = [
70
- /^(sk|pk|rk)_(live|test)_[a-zA-Z0-9]{10,}$/, // Stripe keys
71
- /^r[us]_[a-zA-Z0-9]{20,}$/, // Stripe restricted keys
72
- /^ghp_[a-zA-Z0-9]{36}$/, // GitHub PATs
73
- /^github_pat_[a-zA-Z0-9_]{20,}$/, // GitHub fine-grained PATs
74
- /^gho_[a-zA-Z0-9]{36}$/, // GitHub OAuth tokens
75
- /^AKIA[A-Z0-9]{16}$/, // AWS access key IDs
76
- /^eyJ[a-zA-Z0-9_-]{20,}\.[a-zA-Z0-9_-]{20,}/, // JWTs (eyJ prefix)
77
- /^xox[bpras]-[a-zA-Z0-9-]{10,}$/, // Slack tokens
78
- /^Bearer\s+[a-zA-Z0-9._\-]{20,}$/, // Bearer tokens
79
- /^AIza[a-zA-Z0-9_-]{35}$/, // Google API keys
80
- /^sk-[a-zA-Z0-9]{20,}$/, // OpenAI API keys
81
- /^sk-ant-[a-zA-Z0-9_-]{20,}$/, // Anthropic API keys
82
- /^SG\.[a-zA-Z0-9_-]{20,}$/, // SendGrid API keys
83
- /^SK[a-f0-9]{32}$/, // Twilio API keys
84
- /-----BEGIN\s+(RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/, // PEM private keys
85
- /^DefaultEndpointsProtocol=/, // Azure connection strings
86
- /^[A-Za-z0-9+\/]{43,}={0,2}$/, // Base64-encoded symmetric keys (32+ bytes)
87
- /^[A-Za-z0-9_-]{43,}={0,2}$/, // URL-safe base64 encoded keys
88
- /^whsec_[a-zA-Z0-9]{20,}$/, // Stripe webhook secrets
89
- /^npm_[a-zA-Z0-9]{20,}$/, // npm tokens
90
- /^vercel_[a-zA-Z0-9]{20,}$/, // Vercel tokens
91
- ];
92
-
93
- function isSecretField(key: string): boolean {
94
- const lower = key.toLowerCase();
95
- if (SECRET_FIELDS.has(lower)) return true;
96
- return SECRET_SUBSTRINGS.some((sub) => lower.includes(sub));
97
- }
98
-
99
- function isSecretValue(value: string): boolean {
100
- return SECRET_VALUE_PATTERNS.some((pattern) => pattern.test(value));
101
- }
102
-
103
- export function redact(obj: unknown, depth = 0): unknown {
104
- // SECURITY: At max depth, redact entirely rather than passing data through.
105
- // Prevents secrets in deeply nested objects from bypassing redaction.
106
- if (depth > 20) return REDACTED;
107
- if (obj === null || obj === undefined) return obj;
108
- if (typeof obj === 'string') return isSecretValue(obj) ? REDACTED : obj;
109
- if (typeof obj !== 'object') return obj;
110
- if (Array.isArray(obj)) return obj.map((item) => redact(item, depth + 1));
111
-
112
- const result: Record<string, unknown> = {};
113
- for (const [key, value] of Object.entries(obj as Record<string, unknown>)) {
114
- result[key] = isSecretField(key) ? REDACTED : redact(value, depth + 1);
115
- }
116
- return result;
117
- }
118
-
119
- export function redactHeaders(headers: Record<string, string>): Record<string, string> {
120
- const result: Record<string, string> = {};
121
- for (const [key, value] of Object.entries(headers)) {
122
- result[key] = (isSecretField(key) || isSecretValue(value)) ? REDACTED : value;
123
- }
124
- return result;
125
- }
126
-
127
- export function sanitizeActionRequest(
128
- request: Record<string, unknown>
129
- ): Record<string, unknown> {
130
- return redact(request) as Record<string, unknown>;
131
- }
package/src/schemas.ts DELETED
@@ -1,121 +0,0 @@
1
- import { z } from 'zod';
2
-
3
- /** Max payload size: 64KB when serialized */
4
- const MAX_PAYLOAD_SIZE = 65_536;
5
-
6
- /** Maximum length for webhook URLs (standard URL length limit) */
7
- const MAX_WEBHOOK_URL_LENGTH = 2048;
8
-
9
- /**
10
- * Reusable Zod schema for webhook URLs.
11
- * Enforces: max length 2048, valid URL syntax, HTTPS-only,
12
- * and rejects private/internal hostnames at parse time.
13
- */
14
- export const WebhookUrlSchema = z
15
- .string()
16
- .max(MAX_WEBHOOK_URL_LENGTH, `Webhook URL exceeds maximum length (${MAX_WEBHOOK_URL_LENGTH} characters)`)
17
- .refine(
18
- (val) => {
19
- try {
20
- const parsed = new URL(val);
21
- return parsed.protocol === 'https:';
22
- } catch {
23
- return false;
24
- }
25
- },
26
- { message: 'Webhook URL must be a valid HTTPS URL' }
27
- )
28
- .refine(
29
- (val) => {
30
- try {
31
- const parsed = new URL(val);
32
- const hostname = parsed.hostname;
33
- const privatePatterns = [
34
- /^127\./, /^10\./, /^172\.(1[6-9]|2\d|3[01])\./,
35
- /^192\.168\./, /^169\.254\./, /^0\./,
36
- /^localhost$/i, /\.local$/i, /\.internal$/i,
37
- ];
38
- return !privatePatterns.some((p) => p.test(hostname));
39
- } catch {
40
- return false;
41
- }
42
- },
43
- { message: 'Webhook URL cannot target private or internal addresses' }
44
- );
45
-
46
- export const AgentActionRequestSchema = z.object({
47
- action_type: z.enum(['read', 'write', 'financial', 'admin']),
48
- tool: z.string().min(1).max(100).regex(/^[a-zA-Z0-9._\-:]+$/, 'Tool name must be alphanumeric with dots, dashes, underscores, or colons'),
49
- payload: z.record(z.unknown()).refine(
50
- (val) => JSON.stringify(val).length <= MAX_PAYLOAD_SIZE,
51
- { message: `Payload exceeds maximum size of ${MAX_PAYLOAD_SIZE} bytes` }
52
- ),
53
- idempotency_key: z.string().max(128).optional(),
54
- cost_estimate: z.number().nonnegative().optional(),
55
- });
56
-
57
- export const RegisterAgentSchema = z.object({
58
- name: z.string().min(1).max(100),
59
- environment: z.enum(['development', 'staging', 'production']).default('production'),
60
- public_key: z.string().min(40),
61
- allowed_tools: z.array(z.string()).default([]),
62
- });
63
-
64
- const DOMAIN_RE = /^(\*\.)?([a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?\.)*[a-zA-Z]{2,}$/;
65
-
66
- export const PolicyRulesSchema = z.object({
67
- defaultMode: z.enum(['allow', 'require_approval', 'block']),
68
- rules: z.array(
69
- z.object({
70
- action_type: z.enum(['read', 'write', 'financial', 'admin']).optional(),
71
- tool: z.string().max(100).regex(/^[a-zA-Z0-9._\-:]+$/, 'Tool name must be alphanumeric with dots, dashes, underscores, or colons').optional(),
72
- domain: z.string().regex(DOMAIN_RE, 'Invalid domain format').optional(),
73
- decision: z.enum(['ALLOW', 'REQUIRE_APPROVAL', 'BLOCK']),
74
- require_two_approvals: z.boolean().optional(),
75
- allowed_approvers: z.array(z.string().uuid()).optional(),
76
- }).refine(r => r.action_type || r.tool, { message: 'Rule must specify action_type or tool' })
77
- ).max(100),
78
- http: z
79
- .object({
80
- allowedDomains: z.array(z.string().min(1).max(253).regex(DOMAIN_RE, 'Invalid domain format')),
81
- allowedMethods: z.array(z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'])),
82
- blockList: z.array(z.string().min(1).max(253).regex(DOMAIN_RE, 'Invalid domain format')),
83
- })
84
- .optional(),
85
- limits: z
86
- .object({
87
- maxCostPerAction: z.number().nonnegative().optional(),
88
- maxActionsPerHour: z.number().nonnegative().optional(),
89
- })
90
- .optional(),
91
- });
92
-
93
- export const ApproveRequestSchema = z.object({
94
- action: z.enum(['approve', 'deny']),
95
- reason: z.string().max(1000).optional(),
96
- reply_message: z.string().max(2000).optional(),
97
- /** Server-side biometric challenge token (mobile clients only) */
98
- biometric_challenge: z.string().uuid().optional(),
99
- });
100
-
101
- /** Max metadata size: 8KB when serialized (prevents storage exhaustion) */
102
- const MAX_METADATA_SIZE = 8_192;
103
-
104
- export const SendMessageSchema = z.object({
105
- content: z.string().min(1).max(4096),
106
- thread_id: z.string().uuid().optional(),
107
- expires_at: z.string().datetime().optional(),
108
- metadata: z.record(z.unknown()).refine(
109
- (val) => JSON.stringify(val).length <= MAX_METADATA_SIZE,
110
- { message: `Metadata exceeds maximum size of ${MAX_METADATA_SIZE} bytes` }
111
- ).optional(),
112
- });
113
-
114
- export const AgentSendMessageSchema = z.object({
115
- content: z.string().min(1).max(4096),
116
- thread_id: z.string().uuid(),
117
- metadata: z.record(z.unknown()).refine(
118
- (val) => JSON.stringify(val).length <= MAX_METADATA_SIZE,
119
- { message: `Metadata exceeds maximum size of ${MAX_METADATA_SIZE} bytes` }
120
- ).optional(),
121
- });
package/src/signing.ts DELETED
@@ -1,120 +0,0 @@
1
- import nacl from 'tweetnacl';
2
- import { encodeBase64, decodeBase64, decodeUTF8 } from 'tweetnacl-util';
3
-
4
- export interface SignedHeaders {
5
- 'x-agent-id': string;
6
- 'x-timestamp': string;
7
- 'x-signature': string;
8
- 'x-nonce'?: string;
9
- }
10
-
11
- export interface KeyPair {
12
- publicKey: string;
13
- privateKey: string;
14
- }
15
-
16
- export function generateKeypair(): KeyPair {
17
- const pair = nacl.sign.keyPair();
18
- return {
19
- publicKey: encodeBase64(pair.publicKey),
20
- privateKey: encodeBase64(pair.secretKey),
21
- };
22
- }
23
-
24
- /**
25
- * Recursively stable-stringify: sorts object keys at every nesting level.
26
- * This is critical for signature verification — every language SDK must produce
27
- * the exact same canonical string for the same payload.
28
- *
29
- * Bug fixed: JSON.stringify(obj, replacerArray) only serializes keys named in the
30
- * replacer at every nesting level, so nested objects would be serialized as {}.
31
- */
32
- function stableStringify(val: unknown): string | undefined {
33
- if (val === undefined) return undefined;
34
- if (val === null) return 'null';
35
- if (typeof val === 'number') {
36
- // NaN, Infinity, -Infinity serialize to null per JSON spec
37
- if (!Number.isFinite(val)) return 'null';
38
- return JSON.stringify(val);
39
- }
40
- if (typeof val === 'boolean' || typeof val === 'string') return JSON.stringify(val);
41
- if (Array.isArray(val)) {
42
- return `[${val.map((v) => stableStringify(v) ?? 'null').join(',')}]`;
43
- }
44
- if (typeof val === 'object') {
45
- const sorted = Object.keys(val as object).sort();
46
- const pairs: string[] = [];
47
- for (const k of sorted) {
48
- const v = stableStringify((val as Record<string, unknown>)[k]);
49
- if (v !== undefined) {
50
- pairs.push(`${JSON.stringify(k)}:${v}`);
51
- }
52
- }
53
- return `{${pairs.join(',')}}`;
54
- }
55
- return JSON.stringify(val);
56
- }
57
-
58
- export function canonicalStringify(obj: Record<string, unknown>): string {
59
- return stableStringify(obj) ?? '{}';
60
- }
61
-
62
- export function signRequest(
63
- body: Record<string, unknown>,
64
- agentId: string,
65
- privateKeyBase64: string
66
- ): SignedHeaders {
67
- const timestamp = Date.now().toString();
68
- const nonce = encodeBase64(nacl.randomBytes(16));
69
- const canonical = canonicalStringify(body);
70
- const message = decodeUTF8(`${canonical}:${timestamp}:${nonce}`);
71
-
72
- const privateKey = decodeBase64(privateKeyBase64);
73
- const signature = nacl.sign.detached(message, privateKey);
74
-
75
- return {
76
- 'x-agent-id': agentId,
77
- 'x-timestamp': timestamp,
78
- 'x-signature': encodeBase64(signature),
79
- 'x-nonce': nonce,
80
- };
81
- }
82
-
83
- export function verifyRequest(
84
- body: Record<string, unknown>,
85
- headers: {
86
- 'x-agent-id'?: string;
87
- 'x-timestamp'?: string;
88
- 'x-signature'?: string;
89
- 'x-nonce'?: string;
90
- },
91
- publicKeyBase64: string,
92
- maxSkewMs = 5 * 60 * 1000
93
- ): { agentId: string; nonce: string } {
94
- const agentId = headers['x-agent-id'];
95
- const timestamp = headers['x-timestamp'];
96
- const signatureB64 = headers['x-signature'];
97
- const nonce = headers['x-nonce'];
98
-
99
- if (!agentId || !timestamp || !signatureB64 || !nonce) {
100
- throw new Error('Missing required signature headers');
101
- }
102
-
103
- const ts = parseInt(timestamp, 10);
104
- const now = Date.now();
105
- if (Math.abs(now - ts) > maxSkewMs) {
106
- throw new Error(`Timestamp skew too large: ${Math.abs(now - ts)}ms`);
107
- }
108
-
109
- const canonical = canonicalStringify(body);
110
- const message = decodeUTF8(`${canonical}:${timestamp}:${nonce}`);
111
- const signature = decodeBase64(signatureB64);
112
- const publicKey = decodeBase64(publicKeyBase64);
113
-
114
- const valid = nacl.sign.detached.verify(message, signature, publicKey);
115
- if (!valid) {
116
- throw new Error('Invalid signature');
117
- }
118
-
119
- return { agentId, nonce };
120
- }