@teneo-protocol/sdk 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dockerignore +14 -0
- package/.env.test.example +14 -0
- package/.eslintrc.json +26 -0
- package/.github/workflows/claude-code-review.yml +78 -0
- package/.github/workflows/claude-reviewer.yml +64 -0
- package/.github/workflows/publish-npm.yml +38 -0
- package/.github/workflows/push-to-main.yml +23 -0
- package/.node-version +1 -0
- package/.prettierrc +11 -0
- package/Dockerfile +25 -0
- package/LICENCE +661 -0
- package/README.md +709 -0
- package/dist/constants.d.ts +42 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +45 -0
- package/dist/constants.js.map +1 -0
- package/dist/core/websocket-client.d.ts +261 -0
- package/dist/core/websocket-client.d.ts.map +1 -0
- package/dist/core/websocket-client.js +875 -0
- package/dist/core/websocket-client.js.map +1 -0
- package/dist/formatters/response-formatter.d.ts +354 -0
- package/dist/formatters/response-formatter.d.ts.map +1 -0
- package/dist/formatters/response-formatter.js +575 -0
- package/dist/formatters/response-formatter.js.map +1 -0
- package/dist/handlers/message-handler-registry.d.ts +155 -0
- package/dist/handlers/message-handler-registry.d.ts.map +1 -0
- package/dist/handlers/message-handler-registry.js +216 -0
- package/dist/handlers/message-handler-registry.js.map +1 -0
- package/dist/handlers/message-handlers/agent-selected-handler.d.ts +112 -0
- package/dist/handlers/message-handlers/agent-selected-handler.d.ts.map +1 -0
- package/dist/handlers/message-handlers/agent-selected-handler.js +40 -0
- package/dist/handlers/message-handlers/agent-selected-handler.js.map +1 -0
- package/dist/handlers/message-handlers/agents-list-handler.d.ts +14 -0
- package/dist/handlers/message-handlers/agents-list-handler.d.ts.map +1 -0
- package/dist/handlers/message-handlers/agents-list-handler.js +25 -0
- package/dist/handlers/message-handlers/agents-list-handler.js.map +1 -0
- package/dist/handlers/message-handlers/auth-error-handler.d.ts +71 -0
- package/dist/handlers/message-handlers/auth-error-handler.d.ts.map +1 -0
- package/dist/handlers/message-handlers/auth-error-handler.js +30 -0
- package/dist/handlers/message-handlers/auth-error-handler.js.map +1 -0
- package/dist/handlers/message-handlers/auth-message-handler.d.ts +18 -0
- package/dist/handlers/message-handlers/auth-message-handler.d.ts.map +1 -0
- package/dist/handlers/message-handlers/auth-message-handler.js +60 -0
- package/dist/handlers/message-handlers/auth-message-handler.js.map +1 -0
- package/dist/handlers/message-handlers/auth-required-handler.d.ts +76 -0
- package/dist/handlers/message-handlers/auth-required-handler.d.ts.map +1 -0
- package/dist/handlers/message-handlers/auth-required-handler.js +23 -0
- package/dist/handlers/message-handlers/auth-required-handler.js.map +1 -0
- package/dist/handlers/message-handlers/auth-success-handler.d.ts +18 -0
- package/dist/handlers/message-handlers/auth-success-handler.d.ts.map +1 -0
- package/dist/handlers/message-handlers/auth-success-handler.js +51 -0
- package/dist/handlers/message-handlers/auth-success-handler.js.map +1 -0
- package/dist/handlers/message-handlers/base-handler.d.ts +55 -0
- package/dist/handlers/message-handlers/base-handler.d.ts.map +1 -0
- package/dist/handlers/message-handlers/base-handler.js +83 -0
- package/dist/handlers/message-handlers/base-handler.js.map +1 -0
- package/dist/handlers/message-handlers/challenge-handler.d.ts +73 -0
- package/dist/handlers/message-handlers/challenge-handler.d.ts.map +1 -0
- package/dist/handlers/message-handlers/challenge-handler.js +47 -0
- package/dist/handlers/message-handlers/challenge-handler.js.map +1 -0
- package/dist/handlers/message-handlers/error-message-handler.d.ts +76 -0
- package/dist/handlers/message-handlers/error-message-handler.d.ts.map +1 -0
- package/dist/handlers/message-handlers/error-message-handler.js +29 -0
- package/dist/handlers/message-handlers/error-message-handler.js.map +1 -0
- package/dist/handlers/message-handlers/index.d.ts +28 -0
- package/dist/handlers/message-handlers/index.d.ts.map +1 -0
- package/dist/handlers/message-handlers/index.js +100 -0
- package/dist/handlers/message-handlers/index.js.map +1 -0
- package/dist/handlers/message-handlers/list-rooms-response-handler.d.ts +122 -0
- package/dist/handlers/message-handlers/list-rooms-response-handler.d.ts.map +1 -0
- package/dist/handlers/message-handlers/list-rooms-response-handler.js +30 -0
- package/dist/handlers/message-handlers/list-rooms-response-handler.js.map +1 -0
- package/dist/handlers/message-handlers/ping-pong-handler.d.ts +104 -0
- package/dist/handlers/message-handlers/ping-pong-handler.d.ts.map +1 -0
- package/dist/handlers/message-handlers/ping-pong-handler.js +36 -0
- package/dist/handlers/message-handlers/ping-pong-handler.js.map +1 -0
- package/dist/handlers/message-handlers/regular-message-handler.d.ts +56 -0
- package/dist/handlers/message-handlers/regular-message-handler.d.ts.map +1 -0
- package/dist/handlers/message-handlers/regular-message-handler.js +59 -0
- package/dist/handlers/message-handlers/regular-message-handler.js.map +1 -0
- package/dist/handlers/message-handlers/subscribe-response-handler.d.ts +81 -0
- package/dist/handlers/message-handlers/subscribe-response-handler.d.ts.map +1 -0
- package/dist/handlers/message-handlers/subscribe-response-handler.js +48 -0
- package/dist/handlers/message-handlers/subscribe-response-handler.js.map +1 -0
- package/dist/handlers/message-handlers/task-response-handler.d.ts +14 -0
- package/dist/handlers/message-handlers/task-response-handler.d.ts.map +1 -0
- package/dist/handlers/message-handlers/task-response-handler.js +44 -0
- package/dist/handlers/message-handlers/task-response-handler.js.map +1 -0
- package/dist/handlers/message-handlers/types.d.ts +51 -0
- package/dist/handlers/message-handlers/types.d.ts.map +1 -0
- package/dist/handlers/message-handlers/types.js +7 -0
- package/dist/handlers/message-handlers/types.js.map +1 -0
- package/dist/handlers/message-handlers/unsubscribe-response-handler.d.ts +81 -0
- package/dist/handlers/message-handlers/unsubscribe-response-handler.d.ts.map +1 -0
- package/dist/handlers/message-handlers/unsubscribe-response-handler.js +48 -0
- package/dist/handlers/message-handlers/unsubscribe-response-handler.js.map +1 -0
- package/dist/handlers/webhook-handler.d.ts +202 -0
- package/dist/handlers/webhook-handler.d.ts.map +1 -0
- package/dist/handlers/webhook-handler.js +511 -0
- package/dist/handlers/webhook-handler.js.map +1 -0
- package/dist/index.d.ts +71 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +217 -0
- package/dist/index.js.map +1 -0
- package/dist/managers/agent-registry.d.ts +173 -0
- package/dist/managers/agent-registry.d.ts.map +1 -0
- package/dist/managers/agent-registry.js +310 -0
- package/dist/managers/agent-registry.js.map +1 -0
- package/dist/managers/connection-manager.d.ts +134 -0
- package/dist/managers/connection-manager.d.ts.map +1 -0
- package/dist/managers/connection-manager.js +176 -0
- package/dist/managers/connection-manager.js.map +1 -0
- package/dist/managers/index.d.ts +9 -0
- package/dist/managers/index.d.ts.map +1 -0
- package/dist/managers/index.js +16 -0
- package/dist/managers/index.js.map +1 -0
- package/dist/managers/message-router.d.ts +112 -0
- package/dist/managers/message-router.d.ts.map +1 -0
- package/dist/managers/message-router.js +260 -0
- package/dist/managers/message-router.js.map +1 -0
- package/dist/managers/room-manager.d.ts +165 -0
- package/dist/managers/room-manager.d.ts.map +1 -0
- package/dist/managers/room-manager.js +227 -0
- package/dist/managers/room-manager.js.map +1 -0
- package/dist/teneo-sdk.d.ts +703 -0
- package/dist/teneo-sdk.d.ts.map +1 -0
- package/dist/teneo-sdk.js +907 -0
- package/dist/teneo-sdk.js.map +1 -0
- package/dist/types/config.d.ts +1047 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/config.js +720 -0
- package/dist/types/config.js.map +1 -0
- package/dist/types/error-codes.d.ts +29 -0
- package/dist/types/error-codes.d.ts.map +1 -0
- package/dist/types/error-codes.js +41 -0
- package/dist/types/error-codes.js.map +1 -0
- package/dist/types/events.d.ts +616 -0
- package/dist/types/events.d.ts.map +1 -0
- package/dist/types/events.js +261 -0
- package/dist/types/events.js.map +1 -0
- package/dist/types/health.d.ts +40 -0
- package/dist/types/health.d.ts.map +1 -0
- package/dist/types/health.js +6 -0
- package/dist/types/health.js.map +1 -0
- package/dist/types/index.d.ts +10 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +123 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/messages.d.ts +3734 -0
- package/dist/types/messages.d.ts.map +1 -0
- package/dist/types/messages.js +482 -0
- package/dist/types/messages.js.map +1 -0
- package/dist/types/validation.d.ts +81 -0
- package/dist/types/validation.d.ts.map +1 -0
- package/dist/types/validation.js +115 -0
- package/dist/types/validation.js.map +1 -0
- package/dist/utils/bounded-queue.d.ts +127 -0
- package/dist/utils/bounded-queue.d.ts.map +1 -0
- package/dist/utils/bounded-queue.js +181 -0
- package/dist/utils/bounded-queue.js.map +1 -0
- package/dist/utils/circuit-breaker.d.ts +141 -0
- package/dist/utils/circuit-breaker.d.ts.map +1 -0
- package/dist/utils/circuit-breaker.js +215 -0
- package/dist/utils/circuit-breaker.js.map +1 -0
- package/dist/utils/deduplication-cache.d.ts +110 -0
- package/dist/utils/deduplication-cache.d.ts.map +1 -0
- package/dist/utils/deduplication-cache.js +177 -0
- package/dist/utils/deduplication-cache.js.map +1 -0
- package/dist/utils/event-waiter.d.ts +101 -0
- package/dist/utils/event-waiter.d.ts.map +1 -0
- package/dist/utils/event-waiter.js +118 -0
- package/dist/utils/event-waiter.js.map +1 -0
- package/dist/utils/index.d.ts +51 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +72 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/logger.d.ts +22 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +91 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/rate-limiter.d.ts +122 -0
- package/dist/utils/rate-limiter.d.ts.map +1 -0
- package/dist/utils/rate-limiter.js +190 -0
- package/dist/utils/rate-limiter.js.map +1 -0
- package/dist/utils/retry-policy.d.ts +191 -0
- package/dist/utils/retry-policy.d.ts.map +1 -0
- package/dist/utils/retry-policy.js +225 -0
- package/dist/utils/retry-policy.js.map +1 -0
- package/dist/utils/secure-private-key.d.ts +113 -0
- package/dist/utils/secure-private-key.d.ts.map +1 -0
- package/dist/utils/secure-private-key.js +188 -0
- package/dist/utils/secure-private-key.js.map +1 -0
- package/dist/utils/signature-verifier.d.ts +143 -0
- package/dist/utils/signature-verifier.d.ts.map +1 -0
- package/dist/utils/signature-verifier.js +238 -0
- package/dist/utils/signature-verifier.js.map +1 -0
- package/dist/utils/ssrf-validator.d.ts +36 -0
- package/dist/utils/ssrf-validator.d.ts.map +1 -0
- package/dist/utils/ssrf-validator.js +195 -0
- package/dist/utils/ssrf-validator.js.map +1 -0
- package/examples/.env.example +17 -0
- package/examples/basic-usage.ts +211 -0
- package/examples/production-dashboard/.env.example +153 -0
- package/examples/production-dashboard/package.json +39 -0
- package/examples/production-dashboard/public/dashboard.html +642 -0
- package/examples/production-dashboard/server.ts +753 -0
- package/examples/webhook-integration.ts +239 -0
- package/examples/x-influencer-battle-redesign.html +1065 -0
- package/examples/x-influencer-battle-server.ts +217 -0
- package/examples/x-influencer-battle.html +787 -0
- package/package.json +65 -0
- package/src/constants.ts +43 -0
- package/src/core/websocket-client.test.ts +512 -0
- package/src/core/websocket-client.ts +1056 -0
- package/src/formatters/response-formatter.test.ts +571 -0
- package/src/formatters/response-formatter.ts +677 -0
- package/src/handlers/message-handler-registry.ts +239 -0
- package/src/handlers/message-handlers/agent-selected-handler.ts +40 -0
- package/src/handlers/message-handlers/agents-list-handler.ts +26 -0
- package/src/handlers/message-handlers/auth-error-handler.ts +31 -0
- package/src/handlers/message-handlers/auth-message-handler.ts +66 -0
- package/src/handlers/message-handlers/auth-required-handler.ts +23 -0
- package/src/handlers/message-handlers/auth-success-handler.ts +57 -0
- package/src/handlers/message-handlers/base-handler.ts +101 -0
- package/src/handlers/message-handlers/challenge-handler.ts +57 -0
- package/src/handlers/message-handlers/error-message-handler.ts +27 -0
- package/src/handlers/message-handlers/index.ts +77 -0
- package/src/handlers/message-handlers/list-rooms-response-handler.ts +28 -0
- package/src/handlers/message-handlers/ping-pong-handler.ts +30 -0
- package/src/handlers/message-handlers/regular-message-handler.ts +65 -0
- package/src/handlers/message-handlers/subscribe-response-handler.ts +47 -0
- package/src/handlers/message-handlers/task-response-handler.ts +45 -0
- package/src/handlers/message-handlers/types.ts +77 -0
- package/src/handlers/message-handlers/unsubscribe-response-handler.ts +47 -0
- package/src/handlers/webhook-handler.test.ts +789 -0
- package/src/handlers/webhook-handler.ts +576 -0
- package/src/index.ts +269 -0
- package/src/managers/agent-registry.test.ts +466 -0
- package/src/managers/agent-registry.ts +347 -0
- package/src/managers/connection-manager.ts +195 -0
- package/src/managers/index.ts +9 -0
- package/src/managers/message-router.ts +349 -0
- package/src/managers/room-manager.ts +248 -0
- package/src/teneo-sdk.ts +1022 -0
- package/src/types/config.test.ts +325 -0
- package/src/types/config.ts +799 -0
- package/src/types/error-codes.ts +44 -0
- package/src/types/events.test.ts +302 -0
- package/src/types/events.ts +382 -0
- package/src/types/health.ts +46 -0
- package/src/types/index.ts +199 -0
- package/src/types/messages.test.ts +660 -0
- package/src/types/messages.ts +570 -0
- package/src/types/validation.ts +123 -0
- package/src/utils/bounded-queue.test.ts +356 -0
- package/src/utils/bounded-queue.ts +205 -0
- package/src/utils/circuit-breaker.test.ts +394 -0
- package/src/utils/circuit-breaker.ts +262 -0
- package/src/utils/deduplication-cache.test.ts +380 -0
- package/src/utils/deduplication-cache.ts +198 -0
- package/src/utils/event-waiter.test.ts +381 -0
- package/src/utils/event-waiter.ts +172 -0
- package/src/utils/index.ts +74 -0
- package/src/utils/logger.ts +87 -0
- package/src/utils/rate-limiter.test.ts +341 -0
- package/src/utils/rate-limiter.ts +211 -0
- package/src/utils/retry-policy.test.ts +558 -0
- package/src/utils/retry-policy.ts +272 -0
- package/src/utils/secure-private-key.test.ts +356 -0
- package/src/utils/secure-private-key.ts +205 -0
- package/src/utils/signature-verifier.test.ts +464 -0
- package/src/utils/signature-verifier.ts +298 -0
- package/src/utils/ssrf-validator.test.ts +372 -0
- package/src/utils/ssrf-validator.ts +224 -0
- package/tests/integration/real-server.test.ts +740 -0
- package/tests/integration/websocket.test.ts +381 -0
- package/tests/integration-setup.ts +16 -0
- package/tests/setup.ts +34 -0
- package/tsconfig.json +32 -0
- package/vitest.config.ts +42 -0
- package/vitest.integration.config.ts +23 -0
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Signature Verifier for Message Authentication
|
|
3
|
+
* Provides cryptographic verification of message signatures using Ethereum ECDSA
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { verifyMessage, hashMessage, type Address } from 'viem';
|
|
7
|
+
import { BaseMessage, MessageType } from '../types';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Options for signature verification
|
|
11
|
+
*/
|
|
12
|
+
export interface SignatureVerificationOptions {
|
|
13
|
+
/** Whitelist of trusted agent addresses (empty = allow all) */
|
|
14
|
+
trustedAddresses?: Address[];
|
|
15
|
+
|
|
16
|
+
/** Message types that require signatures */
|
|
17
|
+
requireSignaturesFor?: MessageType[];
|
|
18
|
+
|
|
19
|
+
/** Reject messages with missing signatures (vs just warn) */
|
|
20
|
+
strictMode?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Result of signature verification
|
|
25
|
+
*/
|
|
26
|
+
export interface VerificationResult {
|
|
27
|
+
/** Whether the signature is valid */
|
|
28
|
+
valid: boolean;
|
|
29
|
+
|
|
30
|
+
/** Recovered address from signature (if signature present) */
|
|
31
|
+
recoveredAddress?: Address;
|
|
32
|
+
|
|
33
|
+
/** Reason for failure (if not valid) */
|
|
34
|
+
reason?: string;
|
|
35
|
+
|
|
36
|
+
/** Whether signature was missing */
|
|
37
|
+
signatureMissing: boolean;
|
|
38
|
+
|
|
39
|
+
/** Whether address is in trusted whitelist (if whitelist configured) */
|
|
40
|
+
isTrusted?: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Signature verifier for authenticating message origins
|
|
45
|
+
* Prevents spoofing attacks by verifying Ethereum signatures on messages
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```typescript
|
|
49
|
+
* const verifier = new SignatureVerifier({
|
|
50
|
+
* trustedAddresses: ['0x123...', '0x456...'],
|
|
51
|
+
* requireSignaturesFor: ['task_response', 'agent_selected'],
|
|
52
|
+
* strictMode: true
|
|
53
|
+
* });
|
|
54
|
+
*
|
|
55
|
+
* const result = await verifier.verify(message);
|
|
56
|
+
* if (!result.valid) {
|
|
57
|
+
* console.log(`Verification failed: ${result.reason}`);
|
|
58
|
+
* }
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
export class SignatureVerifier {
|
|
62
|
+
private readonly options: Required<SignatureVerificationOptions>;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Creates a new signature verifier
|
|
66
|
+
*
|
|
67
|
+
* @param options - Verification options
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* ```typescript
|
|
71
|
+
* const verifier = new SignatureVerifier({
|
|
72
|
+
* trustedAddresses: ['0x123...'],
|
|
73
|
+
* requireSignaturesFor: ['task_response'],
|
|
74
|
+
* strictMode: false
|
|
75
|
+
* });
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
constructor(options: SignatureVerificationOptions = {}) {
|
|
79
|
+
this.options = {
|
|
80
|
+
trustedAddresses: options.trustedAddresses || [],
|
|
81
|
+
requireSignaturesFor: options.requireSignaturesFor || [],
|
|
82
|
+
strictMode: options.strictMode !== undefined ? options.strictMode : false
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Verify a message's signature
|
|
88
|
+
*
|
|
89
|
+
* @param message - The message to verify
|
|
90
|
+
* @returns Verification result with validity and recovered address
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ```typescript
|
|
94
|
+
* const result = await verifier.verify(message);
|
|
95
|
+
* if (result.valid) {
|
|
96
|
+
* console.log(`Valid signature from ${result.recoveredAddress}`);
|
|
97
|
+
* } else {
|
|
98
|
+
* console.log(`Invalid: ${result.reason}`);
|
|
99
|
+
* }
|
|
100
|
+
* ```
|
|
101
|
+
*/
|
|
102
|
+
public async verify(message: BaseMessage): Promise<VerificationResult> {
|
|
103
|
+
// Check if signature is present
|
|
104
|
+
if (!message.signature) {
|
|
105
|
+
const isRequired = this.isSignatureRequired(message.type);
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
valid: !isRequired && !this.options.strictMode,
|
|
109
|
+
signatureMissing: true,
|
|
110
|
+
reason: isRequired
|
|
111
|
+
? `Signature required for message type '${message.type}'`
|
|
112
|
+
: 'Signature missing but not required'
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Extract signable content (excludes signature and publicKey fields)
|
|
117
|
+
const signableContent = this.getSignableContent(message);
|
|
118
|
+
|
|
119
|
+
// Create canonical message hash
|
|
120
|
+
const messageHash = this.createMessageHash(signableContent);
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
// Determine which address to verify against
|
|
124
|
+
const addressToVerify = await this.getVerificationAddress(message);
|
|
125
|
+
|
|
126
|
+
if (!addressToVerify) {
|
|
127
|
+
return {
|
|
128
|
+
valid: false,
|
|
129
|
+
signatureMissing: false,
|
|
130
|
+
reason: 'No address available for verification (missing publicKey and from fields)'
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Verify signature using viem
|
|
135
|
+
const isValid = await verifyMessage({
|
|
136
|
+
address: addressToVerify,
|
|
137
|
+
message: messageHash,
|
|
138
|
+
signature: message.signature as `0x${string}`
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
if (!isValid) {
|
|
142
|
+
return {
|
|
143
|
+
valid: false,
|
|
144
|
+
recoveredAddress: addressToVerify,
|
|
145
|
+
signatureMissing: false,
|
|
146
|
+
reason: 'Signature verification failed - signature does not match message content'
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Check if address is trusted (if whitelist configured)
|
|
151
|
+
const isTrusted = this.isTrustedAddress(addressToVerify);
|
|
152
|
+
|
|
153
|
+
// If whitelist is configured and address is not trusted, reject
|
|
154
|
+
if (this.options.trustedAddresses.length > 0 && !isTrusted) {
|
|
155
|
+
return {
|
|
156
|
+
valid: false,
|
|
157
|
+
recoveredAddress: addressToVerify,
|
|
158
|
+
signatureMissing: false,
|
|
159
|
+
isTrusted: false,
|
|
160
|
+
reason: `Address ${addressToVerify} is not in trusted whitelist`
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
valid: true,
|
|
166
|
+
recoveredAddress: addressToVerify,
|
|
167
|
+
signatureMissing: false,
|
|
168
|
+
isTrusted
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
} catch (error) {
|
|
172
|
+
return {
|
|
173
|
+
valid: false,
|
|
174
|
+
signatureMissing: false,
|
|
175
|
+
reason: `Signature verification error: ${error instanceof Error ? error.message : String(error)}`
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Create a canonical hash of message content for signing
|
|
182
|
+
*
|
|
183
|
+
* @param content - The signable content object
|
|
184
|
+
* @returns Message hash string
|
|
185
|
+
*/
|
|
186
|
+
public createMessageHash(content: object): string {
|
|
187
|
+
// Create canonical JSON string (sorted keys for consistency)
|
|
188
|
+
const canonical = JSON.stringify(content, Object.keys(content).sort());
|
|
189
|
+
|
|
190
|
+
// Use viem's hashMessage for Ethereum-compatible hashing
|
|
191
|
+
return hashMessage(canonical);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Extract signable content from message
|
|
196
|
+
* Excludes signature and publicKey fields to prevent circular dependency
|
|
197
|
+
*
|
|
198
|
+
* @param message - The message to extract content from
|
|
199
|
+
* @returns Object containing signable fields
|
|
200
|
+
*/
|
|
201
|
+
public getSignableContent(message: BaseMessage): object {
|
|
202
|
+
const { signature, publicKey, id, ...signableContent } = message;
|
|
203
|
+
|
|
204
|
+
// Include only defined fields for consistent hashing
|
|
205
|
+
const filtered: Record<string, any> = {};
|
|
206
|
+
for (const [key, value] of Object.entries(signableContent)) {
|
|
207
|
+
if (value !== undefined) {
|
|
208
|
+
filtered[key] = value;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return filtered;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Check if signature is required for a message type
|
|
217
|
+
*
|
|
218
|
+
* @param messageType - The type of message
|
|
219
|
+
* @returns True if signature is required
|
|
220
|
+
*/
|
|
221
|
+
public isSignatureRequired(messageType: MessageType): boolean {
|
|
222
|
+
return this.options.requireSignaturesFor.includes(messageType);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Check if an address is in the trusted whitelist
|
|
227
|
+
*
|
|
228
|
+
* @param address - The address to check
|
|
229
|
+
* @returns True if address is trusted (or no whitelist configured)
|
|
230
|
+
*/
|
|
231
|
+
public isTrustedAddress(address: Address): boolean {
|
|
232
|
+
if (this.options.trustedAddresses.length === 0) {
|
|
233
|
+
return true; // No whitelist = trust all
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return this.options.trustedAddresses.some(
|
|
237
|
+
trusted => trusted.toLowerCase() === address.toLowerCase()
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Get the address to verify against
|
|
243
|
+
* Tries publicKey first, falls back to 'from' field
|
|
244
|
+
*
|
|
245
|
+
* @param message - The message to get address from
|
|
246
|
+
* @returns Address to verify, or undefined if none available
|
|
247
|
+
*/
|
|
248
|
+
private async getVerificationAddress(message: BaseMessage): Promise<Address | undefined> {
|
|
249
|
+
// If message includes publicKey, derive address from it
|
|
250
|
+
if (message.publicKey) {
|
|
251
|
+
// publicKey should be an Ethereum address already
|
|
252
|
+
// If it's actually a public key, we'd need to derive the address
|
|
253
|
+
// For now, assume publicKey field contains the address
|
|
254
|
+
return message.publicKey as Address;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Fall back to 'from' field if it looks like an address
|
|
258
|
+
if (message.from && message.from.startsWith('0x') && message.from.length === 42) {
|
|
259
|
+
return message.from as Address;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return undefined;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Update verification options
|
|
267
|
+
*
|
|
268
|
+
* @param options - New options (merged with existing)
|
|
269
|
+
*
|
|
270
|
+
* @example
|
|
271
|
+
* ```typescript
|
|
272
|
+
* verifier.updateOptions({
|
|
273
|
+
* trustedAddresses: ['0x789...'],
|
|
274
|
+
* strictMode: true
|
|
275
|
+
* });
|
|
276
|
+
* ```
|
|
277
|
+
*/
|
|
278
|
+
public updateOptions(options: Partial<SignatureVerificationOptions>): void {
|
|
279
|
+
if (options.trustedAddresses !== undefined) {
|
|
280
|
+
this.options.trustedAddresses = options.trustedAddresses;
|
|
281
|
+
}
|
|
282
|
+
if (options.requireSignaturesFor !== undefined) {
|
|
283
|
+
this.options.requireSignaturesFor = options.requireSignaturesFor;
|
|
284
|
+
}
|
|
285
|
+
if (options.strictMode !== undefined) {
|
|
286
|
+
this.options.strictMode = options.strictMode;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Get current verification options
|
|
292
|
+
*
|
|
293
|
+
* @returns Copy of current options
|
|
294
|
+
*/
|
|
295
|
+
public getOptions(): SignatureVerificationOptions {
|
|
296
|
+
return { ...this.options };
|
|
297
|
+
}
|
|
298
|
+
}
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for SSRF validator
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect } from "vitest";
|
|
6
|
+
import {
|
|
7
|
+
isPrivateIP,
|
|
8
|
+
isCloudMetadataEndpoint,
|
|
9
|
+
isLocalhostException,
|
|
10
|
+
validateWebhookUrl
|
|
11
|
+
} from "./ssrf-validator";
|
|
12
|
+
|
|
13
|
+
describe("SSRF Validator", () => {
|
|
14
|
+
describe("isPrivateIP", () => {
|
|
15
|
+
it("should detect RFC1918 private IPv4 ranges", () => {
|
|
16
|
+
// 10.0.0.0/8
|
|
17
|
+
expect(isPrivateIP("10.0.0.1")).toBe(true);
|
|
18
|
+
expect(isPrivateIP("10.255.255.255")).toBe(true);
|
|
19
|
+
|
|
20
|
+
// 192.168.0.0/16
|
|
21
|
+
expect(isPrivateIP("192.168.0.1")).toBe(true);
|
|
22
|
+
expect(isPrivateIP("192.168.255.255")).toBe(true);
|
|
23
|
+
|
|
24
|
+
// 172.16.0.0/12 (172.16-31)
|
|
25
|
+
expect(isPrivateIP("172.16.0.1")).toBe(true);
|
|
26
|
+
expect(isPrivateIP("172.31.255.255")).toBe(true);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("should NOT detect public IPv4 addresses as private", () => {
|
|
30
|
+
expect(isPrivateIP("8.8.8.8")).toBe(false);
|
|
31
|
+
expect(isPrivateIP("1.1.1.1")).toBe(false);
|
|
32
|
+
expect(isPrivateIP("172.15.0.1")).toBe(false); // Before 172.16
|
|
33
|
+
expect(isPrivateIP("172.32.0.1")).toBe(false); // After 172.31
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("should detect IPv4 loopback addresses", () => {
|
|
37
|
+
expect(isPrivateIP("127.0.0.1")).toBe(true);
|
|
38
|
+
expect(isPrivateIP("127.0.0.255")).toBe(true);
|
|
39
|
+
expect(isPrivateIP("127.255.255.255")).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("should detect IPv4 link-local addresses", () => {
|
|
43
|
+
expect(isPrivateIP("169.254.0.0")).toBe(true);
|
|
44
|
+
expect(isPrivateIP("169.254.169.254")).toBe(true);
|
|
45
|
+
expect(isPrivateIP("169.254.255.255")).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("should detect IPv4 multicast addresses", () => {
|
|
49
|
+
expect(isPrivateIP("224.0.0.1")).toBe(true);
|
|
50
|
+
expect(isPrivateIP("239.255.255.255")).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("should detect IPv4 broadcast address", () => {
|
|
54
|
+
expect(isPrivateIP("255.255.255.255")).toBe(true);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("should detect IPv6 loopback", () => {
|
|
58
|
+
expect(isPrivateIP("::1")).toBe(true);
|
|
59
|
+
expect(isPrivateIP("[::1]")).toBe(true);
|
|
60
|
+
expect(isPrivateIP("0:0:0:0:0:0:0:1")).toBe(true);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("should detect IPv6 unspecified address", () => {
|
|
64
|
+
expect(isPrivateIP("::")).toBe(true);
|
|
65
|
+
expect(isPrivateIP("[::]")).toBe(true);
|
|
66
|
+
expect(isPrivateIP("0:0:0:0:0:0:0:0")).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("should detect IPv6 link-local addresses", () => {
|
|
70
|
+
expect(isPrivateIP("fe80::1")).toBe(true);
|
|
71
|
+
expect(isPrivateIP("fe80:0000:0000:0000:0000:0000:0000:0001")).toBe(true);
|
|
72
|
+
expect(isPrivateIP("feb0::1")).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("should detect IPv6 unique local addresses", () => {
|
|
76
|
+
expect(isPrivateIP("fc00::1")).toBe(true);
|
|
77
|
+
expect(isPrivateIP("fd00::1")).toBe(true);
|
|
78
|
+
expect(isPrivateIP("fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")).toBe(true);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("should NOT detect public IPv6 addresses as private", () => {
|
|
82
|
+
expect(isPrivateIP("2001:4860:4860::8888")).toBe(false); // Google DNS
|
|
83
|
+
expect(isPrivateIP("2606:4700:4700::1111")).toBe(false); // Cloudflare DNS
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe("isCloudMetadataEndpoint", () => {
|
|
88
|
+
it("should detect AWS metadata endpoints", () => {
|
|
89
|
+
expect(isCloudMetadataEndpoint("169.254.169.254")).toBe(true);
|
|
90
|
+
expect(isCloudMetadataEndpoint("fd00:ec2::254")).toBe(true);
|
|
91
|
+
expect(isCloudMetadataEndpoint("instance-data")).toBe(true);
|
|
92
|
+
expect(isCloudMetadataEndpoint("instance-data.ec2.internal")).toBe(true);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("should detect Google Cloud metadata endpoints", () => {
|
|
96
|
+
expect(isCloudMetadataEndpoint("metadata.google.internal")).toBe(true);
|
|
97
|
+
expect(isCloudMetadataEndpoint("metadata.google.com")).toBe(true);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("should detect Kubernetes endpoints", () => {
|
|
101
|
+
expect(isCloudMetadataEndpoint("kubernetes.default")).toBe(true);
|
|
102
|
+
expect(isCloudMetadataEndpoint("kubernetes.default.svc")).toBe(true);
|
|
103
|
+
expect(isCloudMetadataEndpoint("kubernetes.default.svc.cluster.local")).toBe(true);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("should detect bind-all addresses", () => {
|
|
107
|
+
expect(isCloudMetadataEndpoint("0.0.0.0")).toBe(true);
|
|
108
|
+
expect(isCloudMetadataEndpoint("::")).toBe(true);
|
|
109
|
+
expect(isCloudMetadataEndpoint("[::]")).toBe(true);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("should NOT detect normal hostnames as metadata", () => {
|
|
113
|
+
expect(isCloudMetadataEndpoint("example.com")).toBe(false);
|
|
114
|
+
expect(isCloudMetadataEndpoint("api.example.com")).toBe(false);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("should be case-insensitive", () => {
|
|
118
|
+
expect(isCloudMetadataEndpoint("METADATA.GOOGLE.INTERNAL")).toBe(true);
|
|
119
|
+
expect(isCloudMetadataEndpoint("Kubernetes.Default")).toBe(true);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe("isLocalhostException", () => {
|
|
124
|
+
it("should detect localhost hostnames", () => {
|
|
125
|
+
expect(isLocalhostException("localhost")).toBe(true);
|
|
126
|
+
expect(isLocalhostException("LOCALHOST")).toBe(true);
|
|
127
|
+
expect(isLocalhostException("127.0.0.1")).toBe(true);
|
|
128
|
+
expect(isLocalhostException("::1")).toBe(true);
|
|
129
|
+
expect(isLocalhostException("[::1]")).toBe(true);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("should NOT detect bind-all addresses as localhost", () => {
|
|
133
|
+
expect(isLocalhostException("0.0.0.0")).toBe(false);
|
|
134
|
+
expect(isLocalhostException("::")).toBe(false);
|
|
135
|
+
expect(isLocalhostException("[::]")).toBe(false);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("should NOT detect non-localhost as localhost", () => {
|
|
139
|
+
expect(isLocalhostException("example.com")).toBe(false);
|
|
140
|
+
expect(isLocalhostException("127.0.0.2")).toBe(false);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe("validateWebhookUrl", () => {
|
|
145
|
+
describe("Valid URLs", () => {
|
|
146
|
+
it("should accept HTTPS public URLs", () => {
|
|
147
|
+
const urls = [
|
|
148
|
+
"https://webhook.example.com/events",
|
|
149
|
+
"https://api.example.org/webhook",
|
|
150
|
+
"https://example.com:8443/webhook"
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
urls.forEach((url) => {
|
|
154
|
+
expect(() => validateWebhookUrl(url, false)).not.toThrow();
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("should accept localhost HTTP when allowed", () => {
|
|
159
|
+
const urls = [
|
|
160
|
+
"http://localhost/webhook",
|
|
161
|
+
"http://localhost:3000/events",
|
|
162
|
+
"http://127.0.0.1/webhook",
|
|
163
|
+
"http://127.0.0.1:8080/events",
|
|
164
|
+
"http://[::1]/webhook"
|
|
165
|
+
];
|
|
166
|
+
|
|
167
|
+
urls.forEach((url) => {
|
|
168
|
+
expect(() => validateWebhookUrl(url, true)).not.toThrow();
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe("Protocol Validation", () => {
|
|
174
|
+
it("should reject non-HTTP/HTTPS protocols", () => {
|
|
175
|
+
const urls = [
|
|
176
|
+
"ftp://example.com/webhook",
|
|
177
|
+
"file:///etc/passwd",
|
|
178
|
+
"javascript:alert(1)",
|
|
179
|
+
"data:text/html,<script>alert(1)</script>"
|
|
180
|
+
];
|
|
181
|
+
|
|
182
|
+
urls.forEach((url) => {
|
|
183
|
+
expect(() => validateWebhookUrl(url, false)).toThrow(/protocol/i);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("should reject HTTP for non-localhost without allowLocalhost", () => {
|
|
188
|
+
// Public domain should require HTTPS
|
|
189
|
+
expect(() => validateWebhookUrl("http://example.com/webhook", false)).toThrow(/HTTPS/i);
|
|
190
|
+
|
|
191
|
+
// Private IPs should be blocked as private IPs (more specific error)
|
|
192
|
+
expect(() => validateWebhookUrl("http://192.168.1.1/webhook", false)).toThrow(
|
|
193
|
+
/private IP address/i
|
|
194
|
+
);
|
|
195
|
+
expect(() => validateWebhookUrl("http://10.0.0.1/webhook", false)).toThrow(
|
|
196
|
+
/private IP address/i
|
|
197
|
+
);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe("Cloud Metadata Protection", () => {
|
|
202
|
+
it("should block AWS metadata endpoints", () => {
|
|
203
|
+
const urls = [
|
|
204
|
+
"http://169.254.169.254/latest/meta-data/",
|
|
205
|
+
"https://169.254.169.254/latest/meta-data/iam/security-credentials/",
|
|
206
|
+
"http://instance-data/latest/meta-data/",
|
|
207
|
+
"http://instance-data.ec2.internal/latest/meta-data/"
|
|
208
|
+
];
|
|
209
|
+
|
|
210
|
+
urls.forEach((url) => {
|
|
211
|
+
expect(() => validateWebhookUrl(url, true)).toThrow(/cloud metadata endpoint/i);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("should block Google Cloud metadata endpoints", () => {
|
|
216
|
+
const urls = [
|
|
217
|
+
"http://metadata.google.internal/computeMetadata/v1/",
|
|
218
|
+
"http://metadata.google.com/computeMetadata/v1/"
|
|
219
|
+
];
|
|
220
|
+
|
|
221
|
+
urls.forEach((url) => {
|
|
222
|
+
expect(() => validateWebhookUrl(url, true)).toThrow(/cloud metadata endpoint/i);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("should block Kubernetes service discovery", () => {
|
|
227
|
+
// Kubernetes default endpoints are in BLOCKED_HOSTNAMES (cloud metadata)
|
|
228
|
+
const k8sDefaultUrls = [
|
|
229
|
+
"http://kubernetes.default/api",
|
|
230
|
+
"http://kubernetes.default.svc/api",
|
|
231
|
+
"http://kubernetes.default.svc.cluster.local/api"
|
|
232
|
+
];
|
|
233
|
+
|
|
234
|
+
k8sDefaultUrls.forEach((url) => {
|
|
235
|
+
expect(() => validateWebhookUrl(url, true)).toThrow(/cloud metadata endpoint/i);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// Other k8s services should be blocked by isKubernetesService check
|
|
239
|
+
expect(() => validateWebhookUrl("http://redis.default.svc.cluster.local:6379", true)).toThrow(
|
|
240
|
+
/Kubernetes service/i
|
|
241
|
+
);
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
describe("Private IP Protection", () => {
|
|
246
|
+
it("should block RFC1918 private IP ranges", () => {
|
|
247
|
+
const urls = [
|
|
248
|
+
"https://10.0.0.1/webhook",
|
|
249
|
+
"https://192.168.1.1/webhook",
|
|
250
|
+
"https://172.16.0.1/webhook",
|
|
251
|
+
"https://172.31.255.255/webhook"
|
|
252
|
+
];
|
|
253
|
+
|
|
254
|
+
urls.forEach((url) => {
|
|
255
|
+
expect(() => validateWebhookUrl(url, false)).toThrow(/private IP address/i);
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it("should block loopback addresses (except allowed localhost)", () => {
|
|
260
|
+
const urls = [
|
|
261
|
+
"https://127.0.0.2/webhook", // Not 127.0.0.1
|
|
262
|
+
"https://127.255.255.255/webhook"
|
|
263
|
+
];
|
|
264
|
+
|
|
265
|
+
urls.forEach((url) => {
|
|
266
|
+
expect(() => validateWebhookUrl(url, false)).toThrow(/private IP|localhost/i);
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("should block link-local addresses", () => {
|
|
271
|
+
const urls = ["https://169.254.1.1/webhook", "https://169.254.255.255/webhook"];
|
|
272
|
+
|
|
273
|
+
urls.forEach((url) => {
|
|
274
|
+
expect(() => validateWebhookUrl(url, false)).toThrow(/private IP|cloud metadata/i);
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it("should block IPv6 private ranges", () => {
|
|
279
|
+
const urls = [
|
|
280
|
+
"https://[fc00::1]/webhook",
|
|
281
|
+
"https://[fd00::1]/webhook",
|
|
282
|
+
"https://[fe80::1]/webhook"
|
|
283
|
+
];
|
|
284
|
+
|
|
285
|
+
urls.forEach((url) => {
|
|
286
|
+
expect(() => validateWebhookUrl(url, false)).toThrow(/private IP/i);
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
describe("Dangerous Ports Protection", () => {
|
|
292
|
+
it("should block common internal service ports", () => {
|
|
293
|
+
const dangerousPorts = [
|
|
294
|
+
{ port: 22, name: "SSH" },
|
|
295
|
+
{ port: 3306, name: "MySQL" },
|
|
296
|
+
{ port: 5432, name: "PostgreSQL" },
|
|
297
|
+
{ port: 6379, name: "Redis" },
|
|
298
|
+
{ port: 27017, name: "MongoDB" }
|
|
299
|
+
];
|
|
300
|
+
|
|
301
|
+
dangerousPorts.forEach(({ port }) => {
|
|
302
|
+
const url = `https://example.com:${port}/webhook`;
|
|
303
|
+
expect(() => validateWebhookUrl(url, false)).toThrow(/Port.*internal services/i);
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it("should allow safe ports", () => {
|
|
308
|
+
const safePorts = [80, 443, 8080, 8443, 3000, 4000, 5000];
|
|
309
|
+
|
|
310
|
+
safePorts.forEach((port) => {
|
|
311
|
+
const url = `https://example.com:${port}/webhook`;
|
|
312
|
+
expect(() => validateWebhookUrl(url, false)).not.toThrow();
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
describe("Localhost Handling", () => {
|
|
318
|
+
it("should block localhost HTTP when allowLocalhost=false", () => {
|
|
319
|
+
const urls = ["http://localhost/webhook", "http://127.0.0.1/webhook"];
|
|
320
|
+
|
|
321
|
+
urls.forEach((url) => {
|
|
322
|
+
expect(() => validateWebhookUrl(url, false)).toThrow(/localhost.*allowInsecureWebhooks/i);
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it("should block bind-all addresses even with allowLocalhost=true", () => {
|
|
327
|
+
const urls = ["http://0.0.0.0/webhook", "http://[::]/webhook"];
|
|
328
|
+
|
|
329
|
+
urls.forEach((url) => {
|
|
330
|
+
expect(() => validateWebhookUrl(url, true)).toThrow(/cloud metadata|private IP/i);
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
describe("Error Messages", () => {
|
|
336
|
+
it("should provide helpful error messages", () => {
|
|
337
|
+
try {
|
|
338
|
+
validateWebhookUrl("http://169.254.169.254/latest/meta-data/", true);
|
|
339
|
+
expect.fail("Should have thrown");
|
|
340
|
+
} catch (error) {
|
|
341
|
+
expect((error as Error).message).toContain("cloud metadata endpoint");
|
|
342
|
+
expect((error as Error).message).toContain("sensitive credentials");
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
try {
|
|
346
|
+
validateWebhookUrl("https://10.0.0.1/webhook", false);
|
|
347
|
+
expect.fail("Should have thrown");
|
|
348
|
+
} catch (error) {
|
|
349
|
+
expect((error as Error).message).toContain("private IP address");
|
|
350
|
+
expect((error as Error).message).toContain("allowInsecureWebhooks");
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
describe("Invalid URLs", () => {
|
|
356
|
+
it("should reject malformed URLs", () => {
|
|
357
|
+
// Truly malformed URLs (cannot be parsed)
|
|
358
|
+
const malformedUrls = ["not-a-url", "", "example.com"];
|
|
359
|
+
|
|
360
|
+
malformedUrls.forEach((url) => {
|
|
361
|
+
expect(() => validateWebhookUrl(url, false)).toThrow(/Invalid webhook URL/i);
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it("should reject URLs with invalid protocols", () => {
|
|
366
|
+
// Valid URLs but wrong protocol
|
|
367
|
+
expect(() => validateWebhookUrl("htp://example.com", false)).toThrow(/protocol/i);
|
|
368
|
+
expect(() => validateWebhookUrl("ftp://example.com", false)).toThrow(/protocol/i);
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
});
|