@gravito/echo 3.1.1 → 3.1.2
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/README.md +24 -9
- package/dist/OrbitEcho.d.ts +76 -60
- package/dist/index.d.ts +31 -16
- package/dist/index.js +1 -1596
- package/dist/index.js.map +3 -28
- package/dist/{echo/src/middleware → middleware}/RequestBufferMiddleware.d.ts +3 -3
- package/dist/providers/GenericProvider.d.ts +37 -19
- package/dist/providers/GitHubProvider.d.ts +21 -13
- package/dist/providers/StripeProvider.d.ts +20 -13
- package/dist/providers/index.d.ts +14 -4
- package/dist/receive/SignatureValidator.d.ts +33 -1
- package/dist/receive/WebhookReceiver.d.ts +139 -22
- package/dist/receive/index.d.ts +0 -1
- package/dist/send/WebhookDispatcher.d.ts +159 -16
- package/dist/send/index.d.ts +0 -1
- package/dist/types.d.ts +669 -57
- package/package.json +5 -2
- package/dist/OrbitEcho.d.ts.map +0 -1
- package/dist/atlas/src/DB.d.ts +0 -348
- package/dist/atlas/src/OrbitAtlas.d.ts +0 -9
- package/dist/atlas/src/config/defineConfig.d.ts +0 -14
- package/dist/atlas/src/config/index.d.ts +0 -7
- package/dist/atlas/src/config/loadConfig.d.ts +0 -41
- package/dist/atlas/src/connection/Connection.d.ts +0 -112
- package/dist/atlas/src/connection/ConnectionManager.d.ts +0 -180
- package/dist/atlas/src/connection/ReplicaConnectionPool.d.ts +0 -54
- package/dist/atlas/src/drivers/BunSQLDriver.d.ts +0 -32
- package/dist/atlas/src/drivers/BunSQLPreparedStatement.d.ts +0 -118
- package/dist/atlas/src/drivers/MongoDBDriver.d.ts +0 -36
- package/dist/atlas/src/drivers/MySQLDriver.d.ts +0 -79
- package/dist/atlas/src/drivers/PostgresDriver.d.ts +0 -96
- package/dist/atlas/src/drivers/RedisDriver.d.ts +0 -43
- package/dist/atlas/src/drivers/SQLiteDriver.d.ts +0 -45
- package/dist/atlas/src/drivers/types.d.ts +0 -260
- package/dist/atlas/src/errors/index.d.ts +0 -45
- package/dist/atlas/src/grammar/Grammar.d.ts +0 -342
- package/dist/atlas/src/grammar/MongoGrammar.d.ts +0 -47
- package/dist/atlas/src/grammar/MySQLGrammar.d.ts +0 -54
- package/dist/atlas/src/grammar/NullGrammar.d.ts +0 -35
- package/dist/atlas/src/grammar/PostgresGrammar.d.ts +0 -62
- package/dist/atlas/src/grammar/SQLiteGrammar.d.ts +0 -32
- package/dist/atlas/src/index.d.ts +0 -79
- package/dist/atlas/src/migration/Migration.d.ts +0 -64
- package/dist/atlas/src/migration/MigrationRepository.d.ts +0 -65
- package/dist/atlas/src/migration/Migrator.d.ts +0 -110
- package/dist/atlas/src/migration/index.d.ts +0 -6
- package/dist/atlas/src/observability/AtlasMetrics.d.ts +0 -33
- package/dist/atlas/src/observability/AtlasObservability.d.ts +0 -15
- package/dist/atlas/src/observability/AtlasTracer.d.ts +0 -12
- package/dist/atlas/src/observability/index.d.ts +0 -9
- package/dist/atlas/src/orm/Repository.d.ts +0 -247
- package/dist/atlas/src/orm/index.d.ts +0 -6
- package/dist/atlas/src/orm/model/DirtyTracker.d.ts +0 -121
- package/dist/atlas/src/orm/model/Model.d.ts +0 -458
- package/dist/atlas/src/orm/model/ModelRegistry.d.ts +0 -20
- package/dist/atlas/src/orm/model/concerns/HasAttributes.d.ts +0 -150
- package/dist/atlas/src/orm/model/concerns/HasEvents.d.ts +0 -36
- package/dist/atlas/src/orm/model/concerns/HasPersistence.d.ts +0 -92
- package/dist/atlas/src/orm/model/concerns/HasRelationships.d.ts +0 -117
- package/dist/atlas/src/orm/model/concerns/HasSerialization.d.ts +0 -64
- package/dist/atlas/src/orm/model/concerns/applyMixins.d.ts +0 -15
- package/dist/atlas/src/orm/model/concerns/index.d.ts +0 -12
- package/dist/atlas/src/orm/model/decorators.d.ts +0 -138
- package/dist/atlas/src/orm/model/errors.d.ts +0 -52
- package/dist/atlas/src/orm/model/index.d.ts +0 -10
- package/dist/atlas/src/orm/model/relationships.d.ts +0 -207
- package/dist/atlas/src/orm/model/types.d.ts +0 -12
- package/dist/atlas/src/orm/schema/SchemaRegistry.d.ts +0 -124
- package/dist/atlas/src/orm/schema/SchemaSniffer.d.ts +0 -54
- package/dist/atlas/src/orm/schema/index.d.ts +0 -6
- package/dist/atlas/src/orm/schema/types.d.ts +0 -85
- package/dist/atlas/src/pool/AdaptivePoolManager.d.ts +0 -98
- package/dist/atlas/src/pool/PoolHealthChecker.d.ts +0 -91
- package/dist/atlas/src/pool/PoolStrategy.d.ts +0 -129
- package/dist/atlas/src/pool/PoolWarmer.d.ts +0 -92
- package/dist/atlas/src/query/Expression.d.ts +0 -60
- package/dist/atlas/src/query/NPlusOneDetector.d.ts +0 -10
- package/dist/atlas/src/query/QueryBuilder.d.ts +0 -643
- package/dist/atlas/src/query/RelationshipResolver.d.ts +0 -23
- package/dist/atlas/src/query/clauses/GroupByClause.d.ts +0 -51
- package/dist/atlas/src/query/clauses/HavingClause.d.ts +0 -70
- package/dist/atlas/src/query/clauses/JoinClause.d.ts +0 -87
- package/dist/atlas/src/query/clauses/LimitClause.d.ts +0 -82
- package/dist/atlas/src/query/clauses/OrderByClause.d.ts +0 -69
- package/dist/atlas/src/query/clauses/SelectClause.d.ts +0 -71
- package/dist/atlas/src/query/clauses/WhereClause.d.ts +0 -167
- package/dist/atlas/src/query/clauses/index.d.ts +0 -11
- package/dist/atlas/src/schema/Blueprint.d.ts +0 -276
- package/dist/atlas/src/schema/ColumnDefinition.d.ts +0 -154
- package/dist/atlas/src/schema/ForeignKeyDefinition.d.ts +0 -37
- package/dist/atlas/src/schema/MigrationGenerator.d.ts +0 -45
- package/dist/atlas/src/schema/Schema.d.ts +0 -131
- package/dist/atlas/src/schema/SchemaDiff.d.ts +0 -73
- package/dist/atlas/src/schema/TypeGenerator.d.ts +0 -57
- package/dist/atlas/src/schema/TypeWriter.d.ts +0 -42
- package/dist/atlas/src/schema/grammars/MySQLSchemaGrammar.d.ts +0 -23
- package/dist/atlas/src/schema/grammars/PostgresSchemaGrammar.d.ts +0 -26
- package/dist/atlas/src/schema/grammars/SQLiteSchemaGrammar.d.ts +0 -28
- package/dist/atlas/src/schema/grammars/SchemaGrammar.d.ts +0 -97
- package/dist/atlas/src/schema/grammars/index.d.ts +0 -7
- package/dist/atlas/src/schema/index.d.ts +0 -8
- package/dist/atlas/src/seed/Factory.d.ts +0 -90
- package/dist/atlas/src/seed/Seeder.d.ts +0 -28
- package/dist/atlas/src/seed/SeederRunner.d.ts +0 -74
- package/dist/atlas/src/seed/index.d.ts +0 -6
- package/dist/atlas/src/sharding/ShardingManager.d.ts +0 -59
- package/dist/atlas/src/types/index.d.ts +0 -1182
- package/dist/atlas/src/utils/CursorEncoding.d.ts +0 -63
- package/dist/atlas/src/utils/levenshtein.d.ts +0 -9
- package/dist/core/src/Application.d.ts +0 -215
- package/dist/core/src/CommandKernel.d.ts +0 -33
- package/dist/core/src/ConfigManager.d.ts +0 -65
- package/dist/core/src/Container/RequestScopeManager.d.ts +0 -62
- package/dist/core/src/Container/RequestScopeMetrics.d.ts +0 -144
- package/dist/core/src/Container.d.ts +0 -153
- package/dist/core/src/ErrorHandler.d.ts +0 -66
- package/dist/core/src/Event.d.ts +0 -5
- package/dist/core/src/EventManager.d.ts +0 -123
- package/dist/core/src/GlobalErrorHandlers.d.ts +0 -47
- package/dist/core/src/GravitoServer.d.ts +0 -28
- package/dist/core/src/HookManager.d.ts +0 -591
- package/dist/core/src/Listener.d.ts +0 -4
- package/dist/core/src/Logger.d.ts +0 -20
- package/dist/core/src/PlanetCore.d.ts +0 -378
- package/dist/core/src/RequestContext.d.ts +0 -97
- package/dist/core/src/Route.d.ts +0 -36
- package/dist/core/src/Router.d.ts +0 -284
- package/dist/core/src/ServiceProvider.d.ts +0 -178
- package/dist/core/src/adapters/GravitoEngineAdapter.d.ts +0 -27
- package/dist/core/src/adapters/PhotonAdapter.d.ts +0 -175
- package/dist/core/src/adapters/bun/BunContext.d.ts +0 -49
- package/dist/core/src/adapters/bun/BunNativeAdapter.d.ts +0 -31
- package/dist/core/src/adapters/bun/BunRequest.d.ts +0 -31
- package/dist/core/src/adapters/bun/RadixNode.d.ts +0 -19
- package/dist/core/src/adapters/bun/RadixRouter.d.ts +0 -31
- package/dist/core/src/adapters/bun/types.d.ts +0 -20
- package/dist/core/src/adapters/photon-types.d.ts +0 -73
- package/dist/core/src/adapters/types.d.ts +0 -235
- package/dist/core/src/cli/queue-commands.d.ts +0 -6
- package/dist/core/src/engine/AOTRouter.d.ts +0 -129
- package/dist/core/src/engine/FastContext.d.ts +0 -123
- package/dist/core/src/engine/Gravito.d.ts +0 -136
- package/dist/core/src/engine/MinimalContext.d.ts +0 -100
- package/dist/core/src/engine/analyzer.d.ts +0 -27
- package/dist/core/src/engine/constants.d.ts +0 -23
- package/dist/core/src/engine/index.d.ts +0 -26
- package/dist/core/src/engine/path.d.ts +0 -26
- package/dist/core/src/engine/pool.d.ts +0 -83
- package/dist/core/src/engine/types.d.ts +0 -146
- package/dist/core/src/error-handling/RequestScopeErrorContext.d.ts +0 -126
- package/dist/core/src/events/BackpressureManager.d.ts +0 -215
- package/dist/core/src/events/CircuitBreaker.d.ts +0 -229
- package/dist/core/src/events/DeadLetterQueue.d.ts +0 -219
- package/dist/core/src/events/EventBackend.d.ts +0 -12
- package/dist/core/src/events/EventOptions.d.ts +0 -204
- package/dist/core/src/events/EventPriorityQueue.d.ts +0 -301
- package/dist/core/src/events/FlowControlStrategy.d.ts +0 -109
- package/dist/core/src/events/IdempotencyCache.d.ts +0 -60
- package/dist/core/src/events/MessageQueueBridge.d.ts +0 -184
- package/dist/core/src/events/PriorityEscalationManager.d.ts +0 -82
- package/dist/core/src/events/RetryScheduler.d.ts +0 -104
- package/dist/core/src/events/WorkerPool.d.ts +0 -98
- package/dist/core/src/events/WorkerPoolConfig.d.ts +0 -153
- package/dist/core/src/events/WorkerPoolMetrics.d.ts +0 -65
- package/dist/core/src/events/aggregation/AggregationWindow.d.ts +0 -77
- package/dist/core/src/events/aggregation/DeduplicationManager.d.ts +0 -135
- package/dist/core/src/events/aggregation/EventAggregationManager.d.ts +0 -108
- package/dist/core/src/events/aggregation/EventBatcher.d.ts +0 -99
- package/dist/core/src/events/aggregation/types.d.ts +0 -117
- package/dist/core/src/events/index.d.ts +0 -25
- package/dist/core/src/events/observability/EventMetrics.d.ts +0 -132
- package/dist/core/src/events/observability/EventTracer.d.ts +0 -68
- package/dist/core/src/events/observability/EventTracing.d.ts +0 -161
- package/dist/core/src/events/observability/OTelEventMetrics.d.ts +0 -332
- package/dist/core/src/events/observability/ObservableHookManager.d.ts +0 -108
- package/dist/core/src/events/observability/StreamWorkerMetrics.d.ts +0 -76
- package/dist/core/src/events/observability/index.d.ts +0 -24
- package/dist/core/src/events/observability/metrics-types.d.ts +0 -16
- package/dist/core/src/events/types.d.ts +0 -134
- package/dist/core/src/exceptions/AuthenticationException.d.ts +0 -8
- package/dist/core/src/exceptions/AuthorizationException.d.ts +0 -8
- package/dist/core/src/exceptions/CircularDependencyException.d.ts +0 -9
- package/dist/core/src/exceptions/GravitoException.d.ts +0 -23
- package/dist/core/src/exceptions/HttpException.d.ts +0 -9
- package/dist/core/src/exceptions/ModelNotFoundException.d.ts +0 -10
- package/dist/core/src/exceptions/ValidationException.d.ts +0 -22
- package/dist/core/src/exceptions/index.d.ts +0 -7
- package/dist/core/src/health/HealthProvider.d.ts +0 -67
- package/dist/core/src/helpers/Arr.d.ts +0 -19
- package/dist/core/src/helpers/Str.d.ts +0 -23
- package/dist/core/src/helpers/data.d.ts +0 -25
- package/dist/core/src/helpers/errors.d.ts +0 -34
- package/dist/core/src/helpers/response.d.ts +0 -41
- package/dist/core/src/helpers.d.ts +0 -338
- package/dist/core/src/http/CookieJar.d.ts +0 -51
- package/dist/core/src/http/cookie.d.ts +0 -29
- package/dist/core/src/http/middleware/BodySizeLimit.d.ts +0 -16
- package/dist/core/src/http/middleware/Cors.d.ts +0 -24
- package/dist/core/src/http/middleware/Csrf.d.ts +0 -23
- package/dist/core/src/http/middleware/HeaderTokenGate.d.ts +0 -28
- package/dist/core/src/http/middleware/SecurityHeaders.d.ts +0 -29
- package/dist/core/src/http/middleware/ThrottleRequests.d.ts +0 -18
- package/dist/core/src/http/types.d.ts +0 -374
- package/dist/core/src/index.d.ts +0 -88
- package/dist/core/src/instrumentation/index.d.ts +0 -35
- package/dist/core/src/instrumentation/opentelemetry.d.ts +0 -178
- package/dist/core/src/instrumentation/types.d.ts +0 -182
- package/dist/core/src/observability/Metrics.d.ts +0 -244
- package/dist/core/src/observability/QueueDashboard.d.ts +0 -136
- package/dist/core/src/reliability/DeadLetterQueueManager.d.ts +0 -350
- package/dist/core/src/reliability/RetryPolicy.d.ts +0 -217
- package/dist/core/src/reliability/index.d.ts +0 -6
- package/dist/core/src/router/ControllerDispatcher.d.ts +0 -12
- package/dist/core/src/router/RequestValidator.d.ts +0 -20
- package/dist/core/src/runtime.d.ts +0 -119
- package/dist/core/src/security/Encrypter.d.ts +0 -33
- package/dist/core/src/security/Hasher.d.ts +0 -29
- package/dist/core/src/testing/HttpTester.d.ts +0 -39
- package/dist/core/src/testing/TestResponse.d.ts +0 -78
- package/dist/core/src/testing/index.d.ts +0 -2
- package/dist/core/src/types/events.d.ts +0 -94
- package/dist/echo/src/OrbitEcho.d.ts +0 -115
- package/dist/echo/src/index.d.ts +0 -64
- package/dist/echo/src/providers/GenericProvider.d.ts +0 -53
- package/dist/echo/src/providers/GitHubProvider.d.ts +0 -35
- package/dist/echo/src/providers/StripeProvider.d.ts +0 -38
- package/dist/echo/src/providers/index.d.ts +0 -14
- package/dist/echo/src/receive/SignatureValidator.d.ts +0 -67
- package/dist/echo/src/receive/WebhookReceiver.d.ts +0 -185
- package/dist/echo/src/receive/index.d.ts +0 -2
- package/dist/echo/src/send/WebhookDispatcher.d.ts +0 -198
- package/dist/echo/src/send/index.d.ts +0 -1
- package/dist/echo/src/types.d.ts +0 -756
- package/dist/index.d.ts.map +0 -1
- package/dist/monitor/src/MonitorOrbit.d.ts +0 -43
- package/dist/monitor/src/config.d.ts +0 -106
- package/dist/monitor/src/health/HealthController.d.ts +0 -28
- package/dist/monitor/src/health/HealthRegistry.d.ts +0 -80
- package/dist/monitor/src/health/index.d.ts +0 -36
- package/dist/monitor/src/index.d.ts +0 -13
- package/dist/monitor/src/metrics/MetricsController.d.ts +0 -22
- package/dist/monitor/src/metrics/MetricsRegistry.d.ts +0 -136
- package/dist/monitor/src/metrics/index.d.ts +0 -11
- package/dist/monitor/src/tracing/TracingManager.d.ts +0 -97
- package/dist/monitor/src/tracing/index.d.ts +0 -10
- package/dist/photon/src/index.d.ts +0 -89
- package/dist/photon/src/middleware/binary.d.ts +0 -31
- package/dist/photon/src/middleware/htmx.d.ts +0 -39
- package/dist/photon/src/middleware/ratelimit-redis.d.ts +0 -50
- package/dist/photon/src/middleware/ratelimit.d.ts +0 -161
- package/dist/photon/src/openapi.d.ts +0 -19
- package/dist/providers/GenericProvider.d.ts.map +0 -1
- package/dist/providers/GitHubProvider.d.ts.map +0 -1
- package/dist/providers/StripeProvider.d.ts.map +0 -1
- package/dist/providers/index.d.ts.map +0 -1
- package/dist/receive/SignatureValidator.d.ts.map +0 -1
- package/dist/receive/WebhookReceiver.d.ts.map +0 -1
- package/dist/receive/index.d.ts.map +0 -1
- package/dist/send/WebhookDispatcher.d.ts.map +0 -1
- package/dist/send/index.d.ts.map +0 -1
- package/dist/types.d.ts.map +0 -1
- /package/dist/{echo/src/dlq → dlq}/DeadLetterQueue.d.ts +0 -0
- /package/dist/{echo/src/dlq → dlq}/MemoryDeadLetterQueue.d.ts +0 -0
- /package/dist/{echo/src/dlq → dlq}/index.d.ts +0 -0
- /package/dist/{echo/src/middleware → middleware}/index.d.ts +0 -0
- /package/dist/{echo/src/observability → observability}/index.d.ts +0 -0
- /package/dist/{echo/src/observability → observability}/logging/ConsoleEchoLogger.d.ts +0 -0
- /package/dist/{echo/src/observability → observability}/logging/EchoLogger.d.ts +0 -0
- /package/dist/{echo/src/observability → observability}/logging/index.d.ts +0 -0
- /package/dist/{echo/src/observability → observability}/metrics/MetricsProvider.d.ts +0 -0
- /package/dist/{echo/src/observability → observability}/metrics/NoopMetricsProvider.d.ts +0 -0
- /package/dist/{echo/src/observability → observability}/metrics/PrometheusMetricsProvider.d.ts +0 -0
- /package/dist/{echo/src/observability → observability}/metrics/index.d.ts +0 -0
- /package/dist/{echo/src/observability → observability}/tracing/NoopTracer.d.ts +0 -0
- /package/dist/{echo/src/observability → observability}/tracing/Tracer.d.ts +0 -0
- /package/dist/{echo/src/observability → observability}/tracing/index.d.ts +0 -0
- /package/dist/{echo/src/providers → providers}/LinearProvider.d.ts +0 -0
- /package/dist/{echo/src/providers → providers}/PaddleProvider.d.ts +0 -0
- /package/dist/{echo/src/providers → providers}/ShopifyProvider.d.ts +0 -0
- /package/dist/{echo/src/providers → providers}/SlackProvider.d.ts +0 -0
- /package/dist/{echo/src/providers → providers}/TwilioProvider.d.ts +0 -0
- /package/dist/{echo/src/providers → providers}/base/BaseProvider.d.ts +0 -0
- /package/dist/{echo/src/providers → providers}/base/HeaderUtils.d.ts +0 -0
- /package/dist/{echo/src/replay → replay}/WebhookReplayService.d.ts +0 -0
- /package/dist/{echo/src/replay → replay}/index.d.ts +0 -0
- /package/dist/{echo/src/resilience → resilience}/CircuitBreaker.d.ts +0 -0
- /package/dist/{echo/src/resilience → resilience}/index.d.ts +0 -0
- /package/dist/{echo/src/rotation → rotation}/KeyRotationManager.d.ts +0 -0
- /package/dist/{echo/src/rotation → rotation}/index.d.ts +0 -0
- /package/dist/{echo/src/storage → storage}/MemoryWebhookStore.d.ts +0 -0
- /package/dist/{echo/src/storage → storage}/WebhookStore.d.ts +0 -0
- /package/dist/{echo/src/storage → storage}/index.d.ts +0 -0
package/dist/index.js
CHANGED
|
@@ -1,1599 +1,4 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
// src/dlq/MemoryDeadLetterQueue.ts
|
|
3
|
-
class MemoryDeadLetterQueue {
|
|
4
|
-
queue = new Map;
|
|
5
|
-
async enqueue(event) {
|
|
6
|
-
const id = event.id ?? crypto.randomUUID();
|
|
7
|
-
this.queue.set(id, { ...event, id });
|
|
8
|
-
return id;
|
|
9
|
-
}
|
|
10
|
-
async peek(limit = 10) {
|
|
11
|
-
return Array.from(this.queue.values()).sort((a, b) => a.failedAt.getTime() - b.failedAt.getTime()).slice(0, limit);
|
|
12
|
-
}
|
|
13
|
-
async dequeue(id) {
|
|
14
|
-
this.queue.delete(id);
|
|
15
|
-
}
|
|
16
|
-
async size() {
|
|
17
|
-
return this.queue.size;
|
|
18
|
-
}
|
|
19
|
-
async clear() {
|
|
20
|
-
this.queue.clear();
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
// src/middleware/RequestBufferMiddleware.ts
|
|
24
|
-
var DEFAULT_CONFIG = {
|
|
25
|
-
enabled: true,
|
|
26
|
-
maxBodySize: 10 * 1024 * 1024,
|
|
27
|
-
skipContentTypes: ["multipart/form-data", "application/octet-stream"]
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
class RequestBufferMiddleware {
|
|
31
|
-
config;
|
|
32
|
-
constructor(config = {}) {
|
|
33
|
-
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
34
|
-
}
|
|
35
|
-
handler() {
|
|
36
|
-
return async (c, next) => {
|
|
37
|
-
if (!this.config.enabled) {
|
|
38
|
-
return await next();
|
|
39
|
-
}
|
|
40
|
-
const contentType = c.req.header("content-type") ?? "";
|
|
41
|
-
if (this.config.skipContentTypes.some((type) => contentType.includes(type))) {
|
|
42
|
-
return await next();
|
|
43
|
-
}
|
|
44
|
-
const rawBody = await this.readRawBody(c);
|
|
45
|
-
const bodySize = typeof rawBody === "string" ? Buffer.byteLength(rawBody, "utf-8") : rawBody.length;
|
|
46
|
-
if (bodySize > this.config.maxBodySize) {
|
|
47
|
-
return c.json({ error: "Request body too large" }, 413);
|
|
48
|
-
}
|
|
49
|
-
const buffered = {
|
|
50
|
-
rawBody,
|
|
51
|
-
headers: c.req.header(),
|
|
52
|
-
bufferedAt: new Date
|
|
53
|
-
};
|
|
54
|
-
c.set("bufferedRequest", buffered);
|
|
55
|
-
return await next();
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
async readRawBody(c) {
|
|
59
|
-
const existing = c.get("bufferedRequest");
|
|
60
|
-
if (existing) {
|
|
61
|
-
return existing.rawBody;
|
|
62
|
-
}
|
|
63
|
-
const body = await c.req.text();
|
|
64
|
-
return body;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
function createRequestBufferMiddleware(config) {
|
|
68
|
-
return new RequestBufferMiddleware(config).handler();
|
|
69
|
-
}
|
|
70
|
-
// src/observability/logging/ConsoleEchoLogger.ts
|
|
71
|
-
class ConsoleEchoLogger {
|
|
72
|
-
formatContext(base, extra) {
|
|
73
|
-
return {
|
|
74
|
-
module: "echo",
|
|
75
|
-
timestamp: new Date().toISOString(),
|
|
76
|
-
...base,
|
|
77
|
-
...extra
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
debug(message, context) {
|
|
81
|
-
console.debug(JSON.stringify({
|
|
82
|
-
level: "debug",
|
|
83
|
-
message,
|
|
84
|
-
...this.formatContext({}, context)
|
|
85
|
-
}));
|
|
86
|
-
}
|
|
87
|
-
info(message, context) {
|
|
88
|
-
console.info(JSON.stringify({
|
|
89
|
-
level: "info",
|
|
90
|
-
message,
|
|
91
|
-
...this.formatContext({}, context)
|
|
92
|
-
}));
|
|
93
|
-
}
|
|
94
|
-
warn(message, context) {
|
|
95
|
-
console.warn(JSON.stringify({
|
|
96
|
-
level: "warn",
|
|
97
|
-
message,
|
|
98
|
-
...this.formatContext({}, context)
|
|
99
|
-
}));
|
|
100
|
-
}
|
|
101
|
-
error(message, context) {
|
|
102
|
-
console.error(JSON.stringify({
|
|
103
|
-
level: "error",
|
|
104
|
-
message,
|
|
105
|
-
...this.formatContext({}, context)
|
|
106
|
-
}));
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
// src/observability/metrics/MetricsProvider.ts
|
|
110
|
-
var EchoMetrics = {
|
|
111
|
-
INCOMING_TOTAL: "echo_incoming_webhooks_total",
|
|
112
|
-
INCOMING_DURATION: "echo_incoming_duration_seconds",
|
|
113
|
-
INCOMING_VERIFICATION_FAILURES: "echo_incoming_verification_failures_total",
|
|
114
|
-
OUTGOING_TOTAL: "echo_outgoing_webhooks_total",
|
|
115
|
-
OUTGOING_DURATION: "echo_outgoing_duration_seconds",
|
|
116
|
-
OUTGOING_RETRIES: "echo_outgoing_retries_total",
|
|
117
|
-
OUTGOING_FAILURES: "echo_outgoing_failures_total",
|
|
118
|
-
DLQ_SIZE: "echo_dlq_size",
|
|
119
|
-
DLQ_ENQUEUED: "echo_dlq_enqueued_total",
|
|
120
|
-
DLQ_PROCESSED: "echo_dlq_processed_total"
|
|
121
|
-
};
|
|
122
|
-
// src/observability/metrics/NoopMetricsProvider.ts
|
|
123
|
-
class NoopMetricsProvider {
|
|
124
|
-
increment() {}
|
|
125
|
-
histogram() {}
|
|
126
|
-
gauge() {}
|
|
127
|
-
}
|
|
128
|
-
// src/observability/metrics/PrometheusMetricsProvider.ts
|
|
129
|
-
class PrometheusMetricsProvider {
|
|
130
|
-
counters = new Map;
|
|
131
|
-
histograms = new Map;
|
|
132
|
-
gauges = new Map;
|
|
133
|
-
increment(name, labels = {}) {
|
|
134
|
-
const key = this.buildKey(name, labels);
|
|
135
|
-
const counterMap = this.counters.get(name) ?? new Map;
|
|
136
|
-
counterMap.set(key, (counterMap.get(key) ?? 0) + 1);
|
|
137
|
-
this.counters.set(name, counterMap);
|
|
138
|
-
}
|
|
139
|
-
histogram(name, value, labels = {}) {
|
|
140
|
-
const key = this.buildKey(name, labels);
|
|
141
|
-
const values = this.histograms.get(key) ?? [];
|
|
142
|
-
values.push(value);
|
|
143
|
-
this.histograms.set(key, values);
|
|
144
|
-
}
|
|
145
|
-
gauge(name, value, labels = {}) {
|
|
146
|
-
const key = this.buildKey(name, labels);
|
|
147
|
-
this.gauges.set(key, value);
|
|
148
|
-
}
|
|
149
|
-
export() {
|
|
150
|
-
const lines = [];
|
|
151
|
-
for (const [name, counterMap] of this.counters) {
|
|
152
|
-
lines.push(`# TYPE ${name} counter`);
|
|
153
|
-
for (const [key, value] of counterMap) {
|
|
154
|
-
lines.push(`${key} ${value}`);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
for (const [key, value] of this.gauges) {
|
|
158
|
-
lines.push(`${key} ${value}`);
|
|
159
|
-
}
|
|
160
|
-
return lines.join(`
|
|
161
|
-
`);
|
|
162
|
-
}
|
|
163
|
-
buildKey(name, labels) {
|
|
164
|
-
if (Object.keys(labels).length === 0) {
|
|
165
|
-
return name;
|
|
166
|
-
}
|
|
167
|
-
const labelStr = Object.entries(labels).map(([k, v]) => `${k}="${v}"`).join(",");
|
|
168
|
-
return `${name}{${labelStr}}`;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
// src/observability/tracing/NoopTracer.ts
|
|
172
|
-
class NoopSpan {
|
|
173
|
-
setAttribute() {
|
|
174
|
-
return this;
|
|
175
|
-
}
|
|
176
|
-
setAttributes() {
|
|
177
|
-
return this;
|
|
178
|
-
}
|
|
179
|
-
addEvent() {
|
|
180
|
-
return this;
|
|
181
|
-
}
|
|
182
|
-
setStatus() {
|
|
183
|
-
return this;
|
|
184
|
-
}
|
|
185
|
-
end() {}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
class NoopTracer {
|
|
189
|
-
startSpan() {
|
|
190
|
-
return new NoopSpan;
|
|
191
|
-
}
|
|
192
|
-
async withSpan(_name, fn) {
|
|
193
|
-
return fn(new NoopSpan);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
// src/observability/tracing/Tracer.ts
|
|
197
|
-
var SpanStatusCode;
|
|
198
|
-
((SpanStatusCode2) => {
|
|
199
|
-
SpanStatusCode2[SpanStatusCode2["UNSET"] = 0] = "UNSET";
|
|
200
|
-
SpanStatusCode2[SpanStatusCode2["OK"] = 1] = "OK";
|
|
201
|
-
SpanStatusCode2[SpanStatusCode2["ERROR"] = 2] = "ERROR";
|
|
202
|
-
})(SpanStatusCode ||= {});
|
|
203
|
-
// src/receive/SignatureValidator.ts
|
|
204
|
-
async function computeHmacSha256(payload, secret) {
|
|
205
|
-
const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
206
|
-
const payloadBuffer = typeof payload === "string" ? new TextEncoder().encode(payload) : new Uint8Array(payload.buffer, payload.byteOffset, payload.byteLength);
|
|
207
|
-
const signature = await crypto.subtle.sign("HMAC", key, payloadBuffer);
|
|
208
|
-
return Buffer.from(signature).toString("hex");
|
|
209
|
-
}
|
|
210
|
-
async function computeHmacSha256Base64(payload, secret) {
|
|
211
|
-
const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
212
|
-
const payloadBuffer = typeof payload === "string" ? new TextEncoder().encode(payload) : new Uint8Array(payload.buffer, payload.byteOffset, payload.byteLength);
|
|
213
|
-
const signature = await crypto.subtle.sign("HMAC", key, payloadBuffer);
|
|
214
|
-
return Buffer.from(signature).toString("base64");
|
|
215
|
-
}
|
|
216
|
-
async function computeHmacSha1(payload, secret) {
|
|
217
|
-
const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(secret), { name: "HMAC", hash: "SHA-1" }, false, ["sign"]);
|
|
218
|
-
const payloadBuffer = typeof payload === "string" ? new TextEncoder().encode(payload) : new Uint8Array(payload.buffer, payload.byteOffset, payload.byteLength);
|
|
219
|
-
const signature = await crypto.subtle.sign("HMAC", key, payloadBuffer);
|
|
220
|
-
return Buffer.from(signature).toString("hex");
|
|
221
|
-
}
|
|
222
|
-
async function computeHmacSha1Base64(payload, secret) {
|
|
223
|
-
const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(secret), { name: "HMAC", hash: "SHA-1" }, false, ["sign"]);
|
|
224
|
-
const payloadBuffer = typeof payload === "string" ? new TextEncoder().encode(payload) : new Uint8Array(payload.buffer, payload.byteOffset, payload.byteLength);
|
|
225
|
-
const signature = await crypto.subtle.sign("HMAC", key, payloadBuffer);
|
|
226
|
-
return Buffer.from(signature).toString("base64");
|
|
227
|
-
}
|
|
228
|
-
function timingSafeEqual(a, b) {
|
|
229
|
-
if (a.length !== b.length) {
|
|
230
|
-
return false;
|
|
231
|
-
}
|
|
232
|
-
const aBytes = new TextEncoder().encode(a);
|
|
233
|
-
const bBytes = new TextEncoder().encode(b);
|
|
234
|
-
let result = 0;
|
|
235
|
-
for (let i = 0;i < aBytes.length; i++) {
|
|
236
|
-
result |= (aBytes[i] ?? 0) ^ (bBytes[i] ?? 0);
|
|
237
|
-
}
|
|
238
|
-
return result === 0;
|
|
239
|
-
}
|
|
240
|
-
function validateTimestamp(timestamp, tolerance = 300) {
|
|
241
|
-
const now = Math.floor(Date.now() / 1000);
|
|
242
|
-
return Math.abs(now - timestamp) <= tolerance;
|
|
243
|
-
}
|
|
244
|
-
function parseStripeSignature(header) {
|
|
245
|
-
const parts = header.split(",");
|
|
246
|
-
let timestamp;
|
|
247
|
-
const signatures = [];
|
|
248
|
-
for (const part of parts) {
|
|
249
|
-
const [key, value] = part.split("=");
|
|
250
|
-
if (key === "t" && value !== undefined) {
|
|
251
|
-
timestamp = parseInt(value, 10);
|
|
252
|
-
} else if (key === "v1" && value !== undefined) {
|
|
253
|
-
signatures.push(value);
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
if (timestamp === undefined || signatures.length === 0) {
|
|
257
|
-
return null;
|
|
258
|
-
}
|
|
259
|
-
return { timestamp, signatures };
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// src/providers/base/HeaderUtils.ts
|
|
263
|
-
function getHeader(headers, name) {
|
|
264
|
-
const value = headers[name] ?? headers[name.toLowerCase()];
|
|
265
|
-
return Array.isArray(value) ? value[0] : value;
|
|
266
|
-
}
|
|
267
|
-
function hasHeader(headers, name) {
|
|
268
|
-
return getHeader(headers, name) !== undefined;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
// src/providers/base/BaseProvider.ts
|
|
272
|
-
class BaseProvider {
|
|
273
|
-
tolerance;
|
|
274
|
-
constructor(options = {}) {
|
|
275
|
-
this.tolerance = options.tolerance ?? 300;
|
|
276
|
-
}
|
|
277
|
-
getHeader(headers, name) {
|
|
278
|
-
return getHeader(headers, name);
|
|
279
|
-
}
|
|
280
|
-
hasHeader(headers, name) {
|
|
281
|
-
return hasHeader(headers, name);
|
|
282
|
-
}
|
|
283
|
-
createFailure(error) {
|
|
284
|
-
return { valid: false, error };
|
|
285
|
-
}
|
|
286
|
-
createSuccess(payload, options = {}) {
|
|
287
|
-
return {
|
|
288
|
-
valid: true,
|
|
289
|
-
payload,
|
|
290
|
-
eventType: options.eventType,
|
|
291
|
-
webhookId: options.webhookId
|
|
292
|
-
};
|
|
293
|
-
}
|
|
294
|
-
payloadToString(payload) {
|
|
295
|
-
return typeof payload === "string" ? payload : payload.toString("utf-8");
|
|
296
|
-
}
|
|
297
|
-
safeParseJson(str) {
|
|
298
|
-
try {
|
|
299
|
-
return { success: true, data: JSON.parse(str) };
|
|
300
|
-
} catch {
|
|
301
|
-
return { success: false, error: "Failed to parse webhook payload" };
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
// src/providers/GenericProvider.ts
|
|
307
|
-
class GenericProvider extends BaseProvider {
|
|
308
|
-
name = "generic";
|
|
309
|
-
signatureHeader;
|
|
310
|
-
timestampHeader;
|
|
311
|
-
constructor(options = {}) {
|
|
312
|
-
super(options);
|
|
313
|
-
this.signatureHeader = options.signatureHeader ?? "x-webhook-signature";
|
|
314
|
-
this.timestampHeader = options.timestampHeader ?? "x-webhook-timestamp";
|
|
315
|
-
}
|
|
316
|
-
async verify(payload, headers, secret) {
|
|
317
|
-
const signature = this.getHeader(headers, this.signatureHeader);
|
|
318
|
-
if (!signature) {
|
|
319
|
-
return this.createFailure(`Missing signature header: ${this.signatureHeader}`);
|
|
320
|
-
}
|
|
321
|
-
const timestampStr = this.getHeader(headers, this.timestampHeader);
|
|
322
|
-
if (timestampStr) {
|
|
323
|
-
const timestamp = parseInt(timestampStr, 10);
|
|
324
|
-
if (Number.isNaN(timestamp) || !validateTimestamp(timestamp, this.tolerance)) {
|
|
325
|
-
return this.createFailure("Timestamp validation failed");
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
const payloadStr = this.payloadToString(payload);
|
|
329
|
-
const expectedSignature = await computeHmacSha256(payloadStr, secret);
|
|
330
|
-
if (!timingSafeEqual(signature.toLowerCase(), expectedSignature.toLowerCase())) {
|
|
331
|
-
return this.createFailure("Signature verification failed");
|
|
332
|
-
}
|
|
333
|
-
const parseResult = this.safeParseJson(payloadStr);
|
|
334
|
-
if (!parseResult.success) {
|
|
335
|
-
return this.createSuccess(payloadStr);
|
|
336
|
-
}
|
|
337
|
-
const parsed = parseResult.data;
|
|
338
|
-
return this.createSuccess(parsed, {
|
|
339
|
-
eventType: parsed.type ?? parsed.event ?? parsed.eventType,
|
|
340
|
-
webhookId: parsed.id ?? parsed.webhookId
|
|
341
|
-
});
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
// src/providers/GitHubProvider.ts
|
|
346
|
-
class GitHubProvider extends BaseProvider {
|
|
347
|
-
name = "github";
|
|
348
|
-
async verify(payload, headers, secret) {
|
|
349
|
-
const signature = this.getHeader(headers, "x-hub-signature-256");
|
|
350
|
-
if (!signature) {
|
|
351
|
-
return this.createFailure("Missing X-Hub-Signature-256 header");
|
|
352
|
-
}
|
|
353
|
-
if (!signature.startsWith("sha256=")) {
|
|
354
|
-
return this.createFailure("Invalid signature format (expected sha256=...)");
|
|
355
|
-
}
|
|
356
|
-
const signatureValue = signature.slice(7);
|
|
357
|
-
const payloadStr = this.payloadToString(payload);
|
|
358
|
-
const expectedSignature = await computeHmacSha256(payloadStr, secret);
|
|
359
|
-
if (!timingSafeEqual(signatureValue.toLowerCase(), expectedSignature.toLowerCase())) {
|
|
360
|
-
return this.createFailure("Signature verification failed");
|
|
361
|
-
}
|
|
362
|
-
const parseResult = this.safeParseJson(payloadStr);
|
|
363
|
-
if (!parseResult.success) {
|
|
364
|
-
return this.createFailure("Failed to parse webhook payload");
|
|
365
|
-
}
|
|
366
|
-
const event = parseResult.data;
|
|
367
|
-
const eventType = this.getHeader(headers, "x-github-event");
|
|
368
|
-
const deliveryId = this.getHeader(headers, "x-github-delivery");
|
|
369
|
-
return this.createSuccess(event, {
|
|
370
|
-
eventType: eventType ?? undefined,
|
|
371
|
-
webhookId: deliveryId ?? undefined
|
|
372
|
-
});
|
|
373
|
-
}
|
|
374
|
-
parseEventType(payload) {
|
|
375
|
-
if (typeof payload === "object" && payload !== null && "action" in payload) {
|
|
376
|
-
return payload.action;
|
|
377
|
-
}
|
|
378
|
-
return;
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
// src/providers/LinearProvider.ts
|
|
383
|
-
class LinearProvider extends BaseProvider {
|
|
384
|
-
name = "linear";
|
|
385
|
-
async verify(payload, headers, secret) {
|
|
386
|
-
const signature = this.getHeader(headers, "linear-signature");
|
|
387
|
-
if (!signature) {
|
|
388
|
-
return this.createFailure("Missing Linear-Signature header");
|
|
389
|
-
}
|
|
390
|
-
const payloadStr = this.payloadToString(payload);
|
|
391
|
-
const expectedSignature = await computeHmacSha256(payloadStr, secret);
|
|
392
|
-
if (!timingSafeEqual(signature, expectedSignature)) {
|
|
393
|
-
return this.createFailure("Signature verification failed");
|
|
394
|
-
}
|
|
395
|
-
const parseResult = this.safeParseJson(payloadStr);
|
|
396
|
-
if (!parseResult.success) {
|
|
397
|
-
return this.createFailure(parseResult.error);
|
|
398
|
-
}
|
|
399
|
-
const data = parseResult.data;
|
|
400
|
-
return this.createSuccess(data, {
|
|
401
|
-
eventType: data.type ?? data.action,
|
|
402
|
-
webhookId: this.getHeader(headers, "linear-delivery")
|
|
403
|
-
});
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
// src/providers/PaddleProvider.ts
|
|
408
|
-
class PaddleProvider extends BaseProvider {
|
|
409
|
-
name = "paddle";
|
|
410
|
-
async verify(payload, headers, secret) {
|
|
411
|
-
const signatureHeader = this.getHeader(headers, "paddle-signature");
|
|
412
|
-
if (!signatureHeader) {
|
|
413
|
-
return this.createFailure("Missing Paddle-Signature header");
|
|
414
|
-
}
|
|
415
|
-
const parsed = this.parsePaddleSignature(signatureHeader);
|
|
416
|
-
if (!parsed) {
|
|
417
|
-
return this.createFailure("Invalid Paddle-Signature format");
|
|
418
|
-
}
|
|
419
|
-
const { timestamp, signature } = parsed;
|
|
420
|
-
if (!validateTimestamp(timestamp, this.tolerance)) {
|
|
421
|
-
return this.createFailure(`Timestamp outside tolerance window (${this.tolerance}s)`);
|
|
422
|
-
}
|
|
423
|
-
const payloadStr = this.payloadToString(payload);
|
|
424
|
-
const signedPayload = `${timestamp}:${payloadStr}`;
|
|
425
|
-
const expectedSignature = await computeHmacSha256(signedPayload, secret);
|
|
426
|
-
if (!timingSafeEqual(signature, expectedSignature)) {
|
|
427
|
-
return this.createFailure("Signature verification failed");
|
|
428
|
-
}
|
|
429
|
-
const parseResult = this.safeParseJson(payloadStr);
|
|
430
|
-
if (!parseResult.success) {
|
|
431
|
-
return this.createFailure(parseResult.error);
|
|
432
|
-
}
|
|
433
|
-
const data = parseResult.data;
|
|
434
|
-
return this.createSuccess(data, {
|
|
435
|
-
eventType: data.event_type,
|
|
436
|
-
webhookId: data.event_id
|
|
437
|
-
});
|
|
438
|
-
}
|
|
439
|
-
parsePaddleSignature(header) {
|
|
440
|
-
const parts = header.split(";");
|
|
441
|
-
let timestamp;
|
|
442
|
-
let signature;
|
|
443
|
-
for (const part of parts) {
|
|
444
|
-
const [key, value] = part.split("=");
|
|
445
|
-
if (key === "ts" && value) {
|
|
446
|
-
timestamp = parseInt(value, 10);
|
|
447
|
-
} else if (key === "h1" && value) {
|
|
448
|
-
signature = value;
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
if (timestamp === undefined || !signature) {
|
|
452
|
-
return null;
|
|
453
|
-
}
|
|
454
|
-
return { timestamp, signature };
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
// src/providers/ShopifyProvider.ts
|
|
459
|
-
class ShopifyProvider extends BaseProvider {
|
|
460
|
-
name = "shopify";
|
|
461
|
-
async verify(payload, headers, secret) {
|
|
462
|
-
const signature = this.getHeader(headers, "x-shopify-hmac-sha256");
|
|
463
|
-
if (!signature) {
|
|
464
|
-
return this.createFailure("Missing X-Shopify-Hmac-Sha256 header");
|
|
465
|
-
}
|
|
466
|
-
const payloadStr = this.payloadToString(payload);
|
|
467
|
-
const expectedSignature = await computeHmacSha256Base64(payloadStr, secret);
|
|
468
|
-
if (!timingSafeEqual(signature, expectedSignature)) {
|
|
469
|
-
return this.createFailure("Signature verification failed");
|
|
470
|
-
}
|
|
471
|
-
const parseResult = this.safeParseJson(payloadStr);
|
|
472
|
-
if (!parseResult.success) {
|
|
473
|
-
return this.createFailure(parseResult.error);
|
|
474
|
-
}
|
|
475
|
-
return this.createSuccess(parseResult.data, {
|
|
476
|
-
eventType: this.getHeader(headers, "x-shopify-topic"),
|
|
477
|
-
webhookId: this.getHeader(headers, "x-shopify-webhook-id")
|
|
478
|
-
});
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
// src/providers/SlackProvider.ts
|
|
483
|
-
class SlackProvider extends BaseProvider {
|
|
484
|
-
name = "slack";
|
|
485
|
-
async verify(payload, headers, secret) {
|
|
486
|
-
const signature = this.getHeader(headers, "x-slack-signature");
|
|
487
|
-
const timestampStr = this.getHeader(headers, "x-slack-request-timestamp");
|
|
488
|
-
if (!signature) {
|
|
489
|
-
return this.createFailure("Missing X-Slack-Signature header");
|
|
490
|
-
}
|
|
491
|
-
if (!timestampStr) {
|
|
492
|
-
return this.createFailure("Missing X-Slack-Request-Timestamp header");
|
|
493
|
-
}
|
|
494
|
-
if (!signature.startsWith("v0=")) {
|
|
495
|
-
return this.createFailure("Invalid signature format (expected v0=...)");
|
|
496
|
-
}
|
|
497
|
-
const timestamp = parseInt(timestampStr, 10);
|
|
498
|
-
if (!validateTimestamp(timestamp, this.tolerance)) {
|
|
499
|
-
return this.createFailure(`Timestamp outside tolerance window (${this.tolerance}s)`);
|
|
500
|
-
}
|
|
501
|
-
const payloadStr = this.payloadToString(payload);
|
|
502
|
-
const sigBasestring = `v0:${timestamp}:${payloadStr}`;
|
|
503
|
-
const expectedSignature = await computeHmacSha256(sigBasestring, secret);
|
|
504
|
-
if (!timingSafeEqual(signature.slice(3), expectedSignature)) {
|
|
505
|
-
return this.createFailure("Signature verification failed");
|
|
506
|
-
}
|
|
507
|
-
const parseResult = this.safeParseJson(payloadStr);
|
|
508
|
-
if (!parseResult.success) {
|
|
509
|
-
return this.createFailure(parseResult.error);
|
|
510
|
-
}
|
|
511
|
-
const data = parseResult.data;
|
|
512
|
-
return this.createSuccess(data, {
|
|
513
|
-
eventType: data.type,
|
|
514
|
-
webhookId: data.event_id
|
|
515
|
-
});
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
// src/providers/StripeProvider.ts
|
|
520
|
-
class StripeProvider extends BaseProvider {
|
|
521
|
-
name = "stripe";
|
|
522
|
-
constructor(options = {}) {
|
|
523
|
-
super(options);
|
|
524
|
-
}
|
|
525
|
-
async verify(payload, headers, secret) {
|
|
526
|
-
const signatureHeader = this.getHeader(headers, "stripe-signature");
|
|
527
|
-
if (!signatureHeader) {
|
|
528
|
-
return this.createFailure("Missing Stripe-Signature header");
|
|
529
|
-
}
|
|
530
|
-
const parsed = parseStripeSignature(signatureHeader);
|
|
531
|
-
if (!parsed) {
|
|
532
|
-
return this.createFailure("Invalid Stripe-Signature header format");
|
|
533
|
-
}
|
|
534
|
-
const { timestamp, signatures } = parsed;
|
|
535
|
-
if (!validateTimestamp(timestamp, this.tolerance)) {
|
|
536
|
-
return this.createFailure(`Timestamp outside tolerance window (${this.tolerance}s)`);
|
|
537
|
-
}
|
|
538
|
-
const payloadStr = this.payloadToString(payload);
|
|
539
|
-
const signedPayload = `${timestamp}.${payloadStr}`;
|
|
540
|
-
const expectedSignature = await computeHmacSha256(signedPayload, secret);
|
|
541
|
-
const signatureValid = signatures.some((sig) => timingSafeEqual(sig.toLowerCase(), expectedSignature.toLowerCase()));
|
|
542
|
-
if (!signatureValid) {
|
|
543
|
-
return this.createFailure("Signature verification failed");
|
|
544
|
-
}
|
|
545
|
-
const parseResult = this.safeParseJson(payloadStr);
|
|
546
|
-
if (!parseResult.success) {
|
|
547
|
-
return this.createFailure("Failed to parse webhook payload");
|
|
548
|
-
}
|
|
549
|
-
const event = parseResult.data;
|
|
550
|
-
return this.createSuccess(event, {
|
|
551
|
-
eventType: event.type,
|
|
552
|
-
webhookId: event.id
|
|
553
|
-
});
|
|
554
|
-
}
|
|
555
|
-
parseEventType(payload) {
|
|
556
|
-
if (typeof payload === "object" && payload !== null && "type" in payload) {
|
|
557
|
-
return payload.type;
|
|
558
|
-
}
|
|
559
|
-
return;
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
// src/providers/TwilioProvider.ts
|
|
564
|
-
class TwilioProvider extends BaseProvider {
|
|
565
|
-
name = "twilio";
|
|
566
|
-
baseUrl;
|
|
567
|
-
constructor(options = {}) {
|
|
568
|
-
super(options);
|
|
569
|
-
this.baseUrl = options.baseUrl;
|
|
570
|
-
}
|
|
571
|
-
async verify(payload, headers, secret) {
|
|
572
|
-
const signature = this.getHeader(headers, "x-twilio-signature");
|
|
573
|
-
if (!signature) {
|
|
574
|
-
return this.createFailure("Missing X-Twilio-Signature header");
|
|
575
|
-
}
|
|
576
|
-
const url = this.baseUrl ?? "";
|
|
577
|
-
const payloadStr = this.payloadToString(payload);
|
|
578
|
-
const params = new URLSearchParams(payloadStr);
|
|
579
|
-
const sortedParams = Array.from(params.entries()).sort(([a], [b]) => a.localeCompare(b)).map(([key, value]) => `${key}${value}`).join("");
|
|
580
|
-
const signaturePayload = url + sortedParams;
|
|
581
|
-
const expectedSignature = await computeHmacSha1Base64(signaturePayload, secret);
|
|
582
|
-
if (!timingSafeEqual(signature, expectedSignature)) {
|
|
583
|
-
return this.createFailure("Signature verification failed");
|
|
584
|
-
}
|
|
585
|
-
return this.createSuccess(Object.fromEntries(params), {
|
|
586
|
-
eventType: params.get("EventType") ?? undefined
|
|
587
|
-
});
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
// src/receive/WebhookReceiver.ts
|
|
592
|
-
class WebhookReceiver {
|
|
593
|
-
providers = new Map;
|
|
594
|
-
handlers = new Map;
|
|
595
|
-
globalHandlers = new Map;
|
|
596
|
-
store;
|
|
597
|
-
metrics = new NoopMetricsProvider;
|
|
598
|
-
tracer = new NoopTracer;
|
|
599
|
-
logger = new ConsoleEchoLogger;
|
|
600
|
-
keyRotationManager;
|
|
601
|
-
constructor() {
|
|
602
|
-
this.registerProviderType("generic", GenericProvider);
|
|
603
|
-
this.registerProviderType("stripe", StripeProvider);
|
|
604
|
-
this.registerProviderType("github", GitHubProvider);
|
|
605
|
-
this.registerProviderType("shopify", ShopifyProvider);
|
|
606
|
-
this.registerProviderType("twilio", TwilioProvider);
|
|
607
|
-
this.registerProviderType("slack", SlackProvider);
|
|
608
|
-
this.registerProviderType("paddle", PaddleProvider);
|
|
609
|
-
this.registerProviderType("linear", LinearProvider);
|
|
610
|
-
}
|
|
611
|
-
providerTypes = new Map;
|
|
612
|
-
setStore(store) {
|
|
613
|
-
this.store = store;
|
|
614
|
-
return this;
|
|
615
|
-
}
|
|
616
|
-
setMetrics(metrics) {
|
|
617
|
-
this.metrics = metrics;
|
|
618
|
-
return this;
|
|
619
|
-
}
|
|
620
|
-
setTracer(tracer) {
|
|
621
|
-
this.tracer = tracer;
|
|
622
|
-
return this;
|
|
623
|
-
}
|
|
624
|
-
setLogger(logger) {
|
|
625
|
-
this.logger = logger;
|
|
626
|
-
return this;
|
|
627
|
-
}
|
|
628
|
-
setKeyRotationManager(manager) {
|
|
629
|
-
this.keyRotationManager = manager;
|
|
630
|
-
return this;
|
|
631
|
-
}
|
|
632
|
-
registerProviderType(name, ProviderCls) {
|
|
633
|
-
this.providerTypes.set(name, ProviderCls);
|
|
634
|
-
return this;
|
|
635
|
-
}
|
|
636
|
-
registerProvider(name, secret, options) {
|
|
637
|
-
const type = options?.type ?? name;
|
|
638
|
-
const ProviderClass = this.providerTypes.get(type);
|
|
639
|
-
if (!ProviderClass) {
|
|
640
|
-
throw new Error(`Unknown provider type: ${type}`);
|
|
641
|
-
}
|
|
642
|
-
const provider = new ProviderClass({ tolerance: options?.tolerance });
|
|
643
|
-
this.providers.set(name, { provider, secret });
|
|
644
|
-
return this;
|
|
645
|
-
}
|
|
646
|
-
registerProviderWithRotation(name, keys, options) {
|
|
647
|
-
if (!this.keyRotationManager) {
|
|
648
|
-
throw new Error("KeyRotationManager must be set before using key rotation");
|
|
649
|
-
}
|
|
650
|
-
const type = options?.type ?? name;
|
|
651
|
-
const ProviderClass = this.providerTypes.get(type);
|
|
652
|
-
if (!ProviderClass) {
|
|
653
|
-
throw new Error(`Unknown provider type: ${type}`);
|
|
654
|
-
}
|
|
655
|
-
this.keyRotationManager.registerKeys(name, keys);
|
|
656
|
-
const primaryKey = this.keyRotationManager.getPrimaryKey(name);
|
|
657
|
-
if (!primaryKey) {
|
|
658
|
-
throw new Error(`No primary key found for provider ${name}`);
|
|
659
|
-
}
|
|
660
|
-
const provider = new ProviderClass({ tolerance: options?.tolerance });
|
|
661
|
-
this.providers.set(name, { provider, secret: primaryKey.key });
|
|
662
|
-
return this;
|
|
663
|
-
}
|
|
664
|
-
on(providerName, eventType, handler) {
|
|
665
|
-
if (!this.handlers.has(providerName)) {
|
|
666
|
-
this.handlers.set(providerName, new Map);
|
|
667
|
-
}
|
|
668
|
-
const providerHandlers = this.handlers.get(providerName);
|
|
669
|
-
if (!providerHandlers) {
|
|
670
|
-
const newHandlers = new Map;
|
|
671
|
-
this.handlers.set(providerName, newHandlers);
|
|
672
|
-
return this.on(providerName, eventType, handler);
|
|
673
|
-
}
|
|
674
|
-
if (!providerHandlers.has(eventType)) {
|
|
675
|
-
providerHandlers.set(eventType, []);
|
|
676
|
-
}
|
|
677
|
-
providerHandlers.get(eventType)?.push(handler);
|
|
678
|
-
return this;
|
|
679
|
-
}
|
|
680
|
-
onAll(providerName, handler) {
|
|
681
|
-
if (!this.globalHandlers.has(providerName)) {
|
|
682
|
-
this.globalHandlers.set(providerName, []);
|
|
683
|
-
}
|
|
684
|
-
this.globalHandlers.get(providerName)?.push(handler);
|
|
685
|
-
return this;
|
|
686
|
-
}
|
|
687
|
-
async handle(providerName, body, headers, context) {
|
|
688
|
-
const buffered = context?.get("bufferedRequest");
|
|
689
|
-
const actualBody = buffered?.rawBody ?? body;
|
|
690
|
-
const actualHeaders = buffered?.headers ?? headers;
|
|
691
|
-
return this.tracer.withSpan("echo.receive_webhook", async (span) => {
|
|
692
|
-
const startTime = performance.now();
|
|
693
|
-
const labels = { provider: providerName };
|
|
694
|
-
span.setAttributes({
|
|
695
|
-
"echo.provider": providerName,
|
|
696
|
-
"echo.direction": "incoming",
|
|
697
|
-
"echo.buffered": buffered !== undefined
|
|
698
|
-
});
|
|
699
|
-
this.logger.debug("Webhook received", {
|
|
700
|
-
component: "receiver",
|
|
701
|
-
provider: providerName,
|
|
702
|
-
buffered: buffered !== undefined
|
|
703
|
-
});
|
|
704
|
-
try {
|
|
705
|
-
const config = this.providers.get(providerName);
|
|
706
|
-
if (!config) {
|
|
707
|
-
const error = `Provider not registered: ${providerName}`;
|
|
708
|
-
this.logger.warn("Webhook provider not registered", {
|
|
709
|
-
component: "receiver",
|
|
710
|
-
provider: providerName
|
|
711
|
-
});
|
|
712
|
-
span.setStatus({ code: 2 /* ERROR */, message: error });
|
|
713
|
-
return {
|
|
714
|
-
valid: false,
|
|
715
|
-
error,
|
|
716
|
-
handled: false
|
|
717
|
-
};
|
|
718
|
-
}
|
|
719
|
-
const { provider, secret } = config;
|
|
720
|
-
span.addEvent("verification_start");
|
|
721
|
-
let result = {
|
|
722
|
-
valid: false,
|
|
723
|
-
error: "No verification performed"
|
|
724
|
-
};
|
|
725
|
-
if (this.keyRotationManager?.hasProvider(providerName)) {
|
|
726
|
-
const activeKeys = this.keyRotationManager.getActiveKeys(providerName);
|
|
727
|
-
let verified = false;
|
|
728
|
-
for (const keyEntry of activeKeys) {
|
|
729
|
-
const attemptResult = await provider.verify(actualBody, actualHeaders, keyEntry.key);
|
|
730
|
-
if (attemptResult.valid) {
|
|
731
|
-
result = attemptResult;
|
|
732
|
-
verified = true;
|
|
733
|
-
if (!keyEntry.isPrimary) {
|
|
734
|
-
this.logger.info("Webhook verified with non-primary key", {
|
|
735
|
-
component: "receiver",
|
|
736
|
-
provider: providerName,
|
|
737
|
-
keyVersion: keyEntry.version
|
|
738
|
-
});
|
|
739
|
-
}
|
|
740
|
-
break;
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
if (!verified) {
|
|
744
|
-
result = {
|
|
745
|
-
valid: false,
|
|
746
|
-
error: "Signature verification failed with all active keys"
|
|
747
|
-
};
|
|
748
|
-
}
|
|
749
|
-
} else {
|
|
750
|
-
result = await provider.verify(actualBody, actualHeaders, secret);
|
|
751
|
-
}
|
|
752
|
-
if (!result.valid) {
|
|
753
|
-
span.setStatus({ code: 2 /* ERROR */, message: result.error });
|
|
754
|
-
span.setAttribute("echo.error", result.error ?? "unknown");
|
|
755
|
-
this.metrics.increment(EchoMetrics.INCOMING_VERIFICATION_FAILURES, {
|
|
756
|
-
provider: providerName,
|
|
757
|
-
error_type: this.categorizeError(result.error)
|
|
758
|
-
});
|
|
759
|
-
this.logger.warn("Webhook verification failed", {
|
|
760
|
-
component: "receiver",
|
|
761
|
-
provider: providerName,
|
|
762
|
-
error: result.error
|
|
763
|
-
});
|
|
764
|
-
return { ...result, handled: false };
|
|
765
|
-
}
|
|
766
|
-
span.addEvent("verification_success");
|
|
767
|
-
span.setAttributes({
|
|
768
|
-
"echo.event_type": result.eventType ?? "unknown",
|
|
769
|
-
"echo.webhook_id": result.webhookId ?? ""
|
|
770
|
-
});
|
|
771
|
-
this.logger.info("Webhook verified successfully", {
|
|
772
|
-
component: "receiver",
|
|
773
|
-
provider: providerName,
|
|
774
|
-
eventType: result.eventType,
|
|
775
|
-
webhookId: result.webhookId
|
|
776
|
-
});
|
|
777
|
-
labels.event_type = result.eventType;
|
|
778
|
-
labels.status = "success";
|
|
779
|
-
let eventId;
|
|
780
|
-
if (this.store) {
|
|
781
|
-
eventId = await this.store.saveIncomingEvent({
|
|
782
|
-
provider: providerName,
|
|
783
|
-
eventType: result.eventType ?? "unknown",
|
|
784
|
-
payload: result.payload,
|
|
785
|
-
headers: Object.fromEntries(Object.entries(actualHeaders).map(([k, v]) => [k, Array.isArray(v) ? v[0] : v])),
|
|
786
|
-
rawBody: typeof actualBody === "string" ? actualBody : actualBody.toString("utf-8"),
|
|
787
|
-
receivedAt: new Date,
|
|
788
|
-
status: "pending"
|
|
789
|
-
});
|
|
790
|
-
}
|
|
791
|
-
const event = {
|
|
792
|
-
provider: providerName,
|
|
793
|
-
type: result.eventType ?? "unknown",
|
|
794
|
-
payload: result.payload,
|
|
795
|
-
headers: actualHeaders,
|
|
796
|
-
rawBody: typeof actualBody === "string" ? actualBody : actualBody.toString("utf-8"),
|
|
797
|
-
receivedAt: new Date,
|
|
798
|
-
id: result.webhookId
|
|
799
|
-
};
|
|
800
|
-
try {
|
|
801
|
-
let handled = false;
|
|
802
|
-
let handlerCount = 0;
|
|
803
|
-
span.addEvent("handlers_start");
|
|
804
|
-
const providerHandlers = this.handlers.get(providerName);
|
|
805
|
-
if (providerHandlers) {
|
|
806
|
-
const eventHandlers = providerHandlers.get(event.type);
|
|
807
|
-
if (eventHandlers) {
|
|
808
|
-
for (const handler of eventHandlers) {
|
|
809
|
-
await handler(event);
|
|
810
|
-
handled = true;
|
|
811
|
-
handlerCount++;
|
|
812
|
-
}
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
const globalHandlers = this.globalHandlers.get(providerName);
|
|
816
|
-
if (globalHandlers) {
|
|
817
|
-
for (const handler of globalHandlers) {
|
|
818
|
-
await handler(event);
|
|
819
|
-
handled = true;
|
|
820
|
-
handlerCount++;
|
|
821
|
-
}
|
|
822
|
-
}
|
|
823
|
-
span.addEvent("handlers_complete", { handler_count: handlerCount });
|
|
824
|
-
if (this.store && eventId) {
|
|
825
|
-
await this.store.markProcessed(eventId);
|
|
826
|
-
}
|
|
827
|
-
this.logger.debug("Webhook processing complete", {
|
|
828
|
-
component: "receiver",
|
|
829
|
-
provider: providerName,
|
|
830
|
-
eventType: result.eventType,
|
|
831
|
-
handlersInvoked: handlerCount
|
|
832
|
-
});
|
|
833
|
-
span.setStatus({ code: 1 /* OK */ });
|
|
834
|
-
return { ...result, handled, eventId };
|
|
835
|
-
} catch (error) {
|
|
836
|
-
if (this.store && eventId) {
|
|
837
|
-
await this.store.markFailed(eventId, String(error));
|
|
838
|
-
}
|
|
839
|
-
throw error;
|
|
840
|
-
}
|
|
841
|
-
} catch (error) {
|
|
842
|
-
labels.status = "failure";
|
|
843
|
-
labels.error_type = error instanceof Error ? error.name : "unknown";
|
|
844
|
-
span.setStatus({ code: 2 /* ERROR */, message: String(error) });
|
|
845
|
-
throw error;
|
|
846
|
-
} finally {
|
|
847
|
-
const duration = (performance.now() - startTime) / 1000;
|
|
848
|
-
this.metrics.increment(EchoMetrics.INCOMING_TOTAL, labels);
|
|
849
|
-
this.metrics.histogram(EchoMetrics.INCOMING_DURATION, duration, labels);
|
|
850
|
-
}
|
|
851
|
-
});
|
|
852
|
-
}
|
|
853
|
-
categorizeError(error) {
|
|
854
|
-
if (!error) {
|
|
855
|
-
return "unknown";
|
|
856
|
-
}
|
|
857
|
-
if (error.includes("Missing")) {
|
|
858
|
-
return "missing_header";
|
|
859
|
-
}
|
|
860
|
-
if (error.includes("Signature")) {
|
|
861
|
-
return "signature_invalid";
|
|
862
|
-
}
|
|
863
|
-
if (error.includes("Timestamp")) {
|
|
864
|
-
return "timestamp_invalid";
|
|
865
|
-
}
|
|
866
|
-
return "other";
|
|
867
|
-
}
|
|
868
|
-
async verify(providerName, body, headers) {
|
|
869
|
-
const config = this.providers.get(providerName);
|
|
870
|
-
if (!config) {
|
|
871
|
-
return {
|
|
872
|
-
valid: false,
|
|
873
|
-
error: `Provider not registered: ${providerName}`
|
|
874
|
-
};
|
|
875
|
-
}
|
|
876
|
-
return config.provider.verify(body, headers, config.secret);
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
// src/rotation/KeyRotationManager.ts
|
|
881
|
-
var DEFAULT_CONFIG2 = {
|
|
882
|
-
enabled: false,
|
|
883
|
-
autoCleanup: true,
|
|
884
|
-
gracePeriod: 24 * 60 * 60 * 1000,
|
|
885
|
-
keyProvider: async () => [],
|
|
886
|
-
onRotate: () => {}
|
|
887
|
-
};
|
|
888
|
-
|
|
889
|
-
class KeyRotationManager {
|
|
890
|
-
providerKeys = new Map;
|
|
891
|
-
cleanupTimer;
|
|
892
|
-
config;
|
|
893
|
-
constructor(config = {}) {
|
|
894
|
-
this.config = { ...DEFAULT_CONFIG2, ...config };
|
|
895
|
-
if (this.config.enabled && this.config.autoCleanup) {
|
|
896
|
-
this.startAutoCleanup();
|
|
897
|
-
}
|
|
898
|
-
}
|
|
899
|
-
registerKeys(providerName, keys) {
|
|
900
|
-
const primaryKeys = keys.filter((k) => k.isPrimary);
|
|
901
|
-
if (primaryKeys.length !== 1) {
|
|
902
|
-
throw new Error(`Provider ${providerName} must have exactly one primary key`);
|
|
903
|
-
}
|
|
904
|
-
const sorted = [...keys].sort((a, b) => b.activeFrom.getTime() - a.activeFrom.getTime());
|
|
905
|
-
this.providerKeys.set(providerName, sorted);
|
|
906
|
-
}
|
|
907
|
-
getActiveKeys(providerName) {
|
|
908
|
-
const keys = this.providerKeys.get(providerName) ?? [];
|
|
909
|
-
const now = new Date;
|
|
910
|
-
return keys.filter((key) => {
|
|
911
|
-
const isActive = key.activeFrom <= now;
|
|
912
|
-
const notExpired = !key.expiresAt || key.expiresAt > now;
|
|
913
|
-
return isActive && notExpired;
|
|
914
|
-
});
|
|
915
|
-
}
|
|
916
|
-
getPrimaryKey(providerName) {
|
|
917
|
-
const keys = this.getActiveKeys(providerName);
|
|
918
|
-
return keys.find((k) => k.isPrimary) ?? null;
|
|
919
|
-
}
|
|
920
|
-
async rotatePrimaryKey(providerName, newKey) {
|
|
921
|
-
const existingKeys = this.providerKeys.get(providerName) ?? [];
|
|
922
|
-
const updatedKeys = existingKeys.map((key) => ({
|
|
923
|
-
...key,
|
|
924
|
-
isPrimary: false,
|
|
925
|
-
expiresAt: key.isPrimary ? new Date(Date.now() + this.config.gracePeriod) : key.expiresAt
|
|
926
|
-
}));
|
|
927
|
-
const primaryKey = {
|
|
928
|
-
...newKey,
|
|
929
|
-
isPrimary: true
|
|
930
|
-
};
|
|
931
|
-
updatedKeys.unshift(primaryKey);
|
|
932
|
-
this.providerKeys.set(providerName, updatedKeys);
|
|
933
|
-
this.config.onRotate(providerName, primaryKey);
|
|
934
|
-
}
|
|
935
|
-
cleanupExpiredKeys() {
|
|
936
|
-
const now = new Date;
|
|
937
|
-
let cleaned = 0;
|
|
938
|
-
for (const [providerName, keys] of this.providerKeys.entries()) {
|
|
939
|
-
const active = keys.filter((key) => !key.expiresAt || key.expiresAt > now);
|
|
940
|
-
if (active.length !== keys.length) {
|
|
941
|
-
cleaned += keys.length - active.length;
|
|
942
|
-
this.providerKeys.set(providerName, active);
|
|
943
|
-
}
|
|
944
|
-
}
|
|
945
|
-
return cleaned;
|
|
946
|
-
}
|
|
947
|
-
startAutoCleanup() {
|
|
948
|
-
this.cleanupTimer = setInterval(() => {
|
|
949
|
-
const cleaned = this.cleanupExpiredKeys();
|
|
950
|
-
if (cleaned > 0) {
|
|
951
|
-
console.log(`[KeyRotationManager] Cleaned up ${cleaned} expired keys`);
|
|
952
|
-
}
|
|
953
|
-
}, 60 * 60 * 1000);
|
|
954
|
-
}
|
|
955
|
-
destroy() {
|
|
956
|
-
if (this.cleanupTimer) {
|
|
957
|
-
clearInterval(this.cleanupTimer);
|
|
958
|
-
}
|
|
959
|
-
}
|
|
960
|
-
getAllProviderKeys() {
|
|
961
|
-
return new Map(this.providerKeys);
|
|
962
|
-
}
|
|
963
|
-
hasProvider(providerName) {
|
|
964
|
-
return this.providerKeys.has(providerName);
|
|
965
|
-
}
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
// src/resilience/CircuitBreaker.ts
|
|
969
|
-
var DEFAULT_CONFIG3 = {
|
|
970
|
-
enabled: true,
|
|
971
|
-
failureThreshold: 5,
|
|
972
|
-
successThreshold: 2,
|
|
973
|
-
windowSize: 60000,
|
|
974
|
-
openTimeout: 30000,
|
|
975
|
-
onOpen: () => {},
|
|
976
|
-
onHalfOpen: () => {},
|
|
977
|
-
onClose: () => {}
|
|
978
|
-
};
|
|
979
|
-
|
|
980
|
-
class CircuitBreaker {
|
|
981
|
-
name;
|
|
982
|
-
state = "CLOSED";
|
|
983
|
-
failures = 0;
|
|
984
|
-
successes = 0;
|
|
985
|
-
lastFailureAt;
|
|
986
|
-
lastSuccessAt;
|
|
987
|
-
openedAt;
|
|
988
|
-
halfOpenAttempts = 0;
|
|
989
|
-
config;
|
|
990
|
-
constructor(name, config = {}) {
|
|
991
|
-
this.name = name;
|
|
992
|
-
this.config = { ...DEFAULT_CONFIG3, ...config };
|
|
993
|
-
}
|
|
994
|
-
async execute(fn) {
|
|
995
|
-
if (!this.config.enabled) {
|
|
996
|
-
return await fn();
|
|
997
|
-
}
|
|
998
|
-
this.checkStateTransition();
|
|
999
|
-
if (this.state === "OPEN") {
|
|
1000
|
-
throw new Error(`Circuit breaker is OPEN for ${this.name}`);
|
|
1001
|
-
}
|
|
1002
|
-
try {
|
|
1003
|
-
const result = await fn();
|
|
1004
|
-
this.onSuccess();
|
|
1005
|
-
return result;
|
|
1006
|
-
} catch (error) {
|
|
1007
|
-
this.onFailure();
|
|
1008
|
-
throw error;
|
|
1009
|
-
}
|
|
1010
|
-
}
|
|
1011
|
-
onSuccess() {
|
|
1012
|
-
this.lastSuccessAt = new Date;
|
|
1013
|
-
this.successes++;
|
|
1014
|
-
if (this.state === "HALF_OPEN") {
|
|
1015
|
-
this.halfOpenAttempts++;
|
|
1016
|
-
if (this.halfOpenAttempts >= this.config.successThreshold) {
|
|
1017
|
-
this.transitionTo("CLOSED");
|
|
1018
|
-
this.reset();
|
|
1019
|
-
}
|
|
1020
|
-
} else if (this.state === "CLOSED") {
|
|
1021
|
-
this.failures = 0;
|
|
1022
|
-
}
|
|
1023
|
-
}
|
|
1024
|
-
onFailure() {
|
|
1025
|
-
this.lastFailureAt = new Date;
|
|
1026
|
-
this.failures++;
|
|
1027
|
-
if (this.state === "HALF_OPEN") {
|
|
1028
|
-
this.transitionTo("OPEN");
|
|
1029
|
-
this.openedAt = new Date;
|
|
1030
|
-
} else if (this.state === "CLOSED") {
|
|
1031
|
-
if (this.failures >= this.config.failureThreshold) {
|
|
1032
|
-
this.transitionTo("OPEN");
|
|
1033
|
-
this.openedAt = new Date;
|
|
1034
|
-
}
|
|
1035
|
-
}
|
|
1036
|
-
}
|
|
1037
|
-
checkStateTransition() {
|
|
1038
|
-
if (this.state === "OPEN" && this.openedAt) {
|
|
1039
|
-
const elapsed = Date.now() - this.openedAt.getTime();
|
|
1040
|
-
if (elapsed >= this.config.openTimeout) {
|
|
1041
|
-
this.transitionTo("HALF_OPEN");
|
|
1042
|
-
this.halfOpenAttempts = 0;
|
|
1043
|
-
}
|
|
1044
|
-
}
|
|
1045
|
-
if (this.lastFailureAt) {
|
|
1046
|
-
const elapsed = Date.now() - this.lastFailureAt.getTime();
|
|
1047
|
-
if (elapsed >= this.config.windowSize) {
|
|
1048
|
-
this.failures = 0;
|
|
1049
|
-
}
|
|
1050
|
-
}
|
|
1051
|
-
}
|
|
1052
|
-
transitionTo(newState) {
|
|
1053
|
-
this.state = newState;
|
|
1054
|
-
switch (newState) {
|
|
1055
|
-
case "OPEN":
|
|
1056
|
-
this.config.onOpen(this.name);
|
|
1057
|
-
break;
|
|
1058
|
-
case "HALF_OPEN":
|
|
1059
|
-
this.config.onHalfOpen(this.name);
|
|
1060
|
-
break;
|
|
1061
|
-
case "CLOSED":
|
|
1062
|
-
this.config.onClose(this.name);
|
|
1063
|
-
break;
|
|
1064
|
-
}
|
|
1065
|
-
}
|
|
1066
|
-
reset() {
|
|
1067
|
-
this.failures = 0;
|
|
1068
|
-
this.successes = 0;
|
|
1069
|
-
this.halfOpenAttempts = 0;
|
|
1070
|
-
this.openedAt = undefined;
|
|
1071
|
-
}
|
|
1072
|
-
getMetrics() {
|
|
1073
|
-
return {
|
|
1074
|
-
state: this.state,
|
|
1075
|
-
failures: this.failures,
|
|
1076
|
-
successes: this.successes,
|
|
1077
|
-
lastFailureAt: this.lastFailureAt,
|
|
1078
|
-
lastSuccessAt: this.lastSuccessAt,
|
|
1079
|
-
openedAt: this.openedAt
|
|
1080
|
-
};
|
|
1081
|
-
}
|
|
1082
|
-
manualReset() {
|
|
1083
|
-
this.transitionTo("CLOSED");
|
|
1084
|
-
this.reset();
|
|
1085
|
-
}
|
|
1086
|
-
getState() {
|
|
1087
|
-
return this.state;
|
|
1088
|
-
}
|
|
1089
|
-
getName() {
|
|
1090
|
-
return this.name;
|
|
1091
|
-
}
|
|
1092
|
-
}
|
|
1093
|
-
|
|
1094
|
-
// src/send/WebhookDispatcher.ts
|
|
1095
|
-
var DEFAULT_RETRY_CONFIG = {
|
|
1096
|
-
maxAttempts: 3,
|
|
1097
|
-
initialDelay: 1000,
|
|
1098
|
-
backoffMultiplier: 2,
|
|
1099
|
-
maxDelay: 300000,
|
|
1100
|
-
retryableStatuses: [408, 429, 500, 502, 503, 504]
|
|
1101
|
-
};
|
|
1102
|
-
|
|
1103
|
-
class WebhookDispatcher {
|
|
1104
|
-
secret;
|
|
1105
|
-
retryConfig;
|
|
1106
|
-
timeout;
|
|
1107
|
-
userAgent;
|
|
1108
|
-
dlq;
|
|
1109
|
-
metrics = new NoopMetricsProvider;
|
|
1110
|
-
tracer = new NoopTracer;
|
|
1111
|
-
logger;
|
|
1112
|
-
circuitBreakers = new Map;
|
|
1113
|
-
circuitBreakerConfig;
|
|
1114
|
-
constructor(config) {
|
|
1115
|
-
this.secret = config.secret;
|
|
1116
|
-
this.retryConfig = { ...DEFAULT_RETRY_CONFIG, ...config.retry };
|
|
1117
|
-
this.timeout = config.timeout ?? 30000;
|
|
1118
|
-
this.userAgent = config.userAgent ?? "Gravito-Echo/1.0";
|
|
1119
|
-
this.circuitBreakerConfig = config.circuitBreaker;
|
|
1120
|
-
}
|
|
1121
|
-
setDeadLetterQueue(dlq) {
|
|
1122
|
-
this.dlq = dlq;
|
|
1123
|
-
return this;
|
|
1124
|
-
}
|
|
1125
|
-
setMetrics(metrics) {
|
|
1126
|
-
this.metrics = metrics;
|
|
1127
|
-
return this;
|
|
1128
|
-
}
|
|
1129
|
-
setTracer(tracer) {
|
|
1130
|
-
this.tracer = tracer;
|
|
1131
|
-
return this;
|
|
1132
|
-
}
|
|
1133
|
-
setLogger(logger) {
|
|
1134
|
-
this.logger = logger;
|
|
1135
|
-
return this;
|
|
1136
|
-
}
|
|
1137
|
-
getCircuitBreaker(url) {
|
|
1138
|
-
if (!this.circuitBreakerConfig?.enabled) {
|
|
1139
|
-
return;
|
|
1140
|
-
}
|
|
1141
|
-
const host = new URL(url).host;
|
|
1142
|
-
if (!this.circuitBreakers.has(host)) {
|
|
1143
|
-
const breaker = new CircuitBreaker(host, {
|
|
1144
|
-
...this.circuitBreakerConfig,
|
|
1145
|
-
onOpen: (name) => {
|
|
1146
|
-
this.logger?.warn(`Circuit breaker OPEN for ${name}`, {
|
|
1147
|
-
component: "dispatcher",
|
|
1148
|
-
host: name
|
|
1149
|
-
});
|
|
1150
|
-
this.circuitBreakerConfig?.onOpen?.(name);
|
|
1151
|
-
},
|
|
1152
|
-
onHalfOpen: (name) => {
|
|
1153
|
-
this.logger?.info(`Circuit breaker HALF_OPEN for ${name}`, {
|
|
1154
|
-
component: "dispatcher",
|
|
1155
|
-
host: name
|
|
1156
|
-
});
|
|
1157
|
-
this.circuitBreakerConfig?.onHalfOpen?.(name);
|
|
1158
|
-
},
|
|
1159
|
-
onClose: (name) => {
|
|
1160
|
-
this.logger?.info(`Circuit breaker CLOSED for ${name}`, {
|
|
1161
|
-
component: "dispatcher",
|
|
1162
|
-
host: name
|
|
1163
|
-
});
|
|
1164
|
-
this.circuitBreakerConfig?.onClose?.(name);
|
|
1165
|
-
}
|
|
1166
|
-
});
|
|
1167
|
-
this.circuitBreakers.set(host, breaker);
|
|
1168
|
-
}
|
|
1169
|
-
return this.circuitBreakers.get(host);
|
|
1170
|
-
}
|
|
1171
|
-
getCircuitBreakerMetrics(url) {
|
|
1172
|
-
const host = new URL(url).host;
|
|
1173
|
-
const breaker = this.circuitBreakers.get(host);
|
|
1174
|
-
return breaker ? breaker.getMetrics() : null;
|
|
1175
|
-
}
|
|
1176
|
-
resetCircuitBreaker(url) {
|
|
1177
|
-
const host = new URL(url).host;
|
|
1178
|
-
const breaker = this.circuitBreakers.get(host);
|
|
1179
|
-
breaker?.manualReset();
|
|
1180
|
-
}
|
|
1181
|
-
async dispatch(payload) {
|
|
1182
|
-
return this.tracer.withSpan("echo.dispatch_webhook", async (span) => {
|
|
1183
|
-
const startTime = performance.now();
|
|
1184
|
-
const labels = { event_type: payload.event };
|
|
1185
|
-
span.setAttributes({
|
|
1186
|
-
"echo.direction": "outgoing",
|
|
1187
|
-
"echo.event": payload.event,
|
|
1188
|
-
"echo.url": payload.url,
|
|
1189
|
-
"http.method": "POST",
|
|
1190
|
-
"http.url": payload.url
|
|
1191
|
-
});
|
|
1192
|
-
const result = await this.dispatchInternal(payload);
|
|
1193
|
-
const duration = (performance.now() - startTime) / 1000;
|
|
1194
|
-
labels.status = result.success ? "success" : "failure";
|
|
1195
|
-
labels.status_code = result.statusCode?.toString();
|
|
1196
|
-
this.metrics.increment(EchoMetrics.OUTGOING_TOTAL, labels);
|
|
1197
|
-
this.metrics.histogram(EchoMetrics.OUTGOING_DURATION, duration, labels);
|
|
1198
|
-
if (result.attempt > 1) {
|
|
1199
|
-
this.metrics.increment(EchoMetrics.OUTGOING_RETRIES, {
|
|
1200
|
-
event_type: payload.event
|
|
1201
|
-
});
|
|
1202
|
-
}
|
|
1203
|
-
span.setAttributes({
|
|
1204
|
-
"echo.success": result.success,
|
|
1205
|
-
"echo.attempt": result.attempt,
|
|
1206
|
-
"echo.duration_ms": result.duration
|
|
1207
|
-
});
|
|
1208
|
-
if (result.statusCode) {
|
|
1209
|
-
span.setAttribute("http.status_code", result.statusCode);
|
|
1210
|
-
}
|
|
1211
|
-
if (result.success) {
|
|
1212
|
-
span.setStatus({ code: 1 /* OK */ });
|
|
1213
|
-
} else {
|
|
1214
|
-
span.setStatus({
|
|
1215
|
-
code: 2 /* ERROR */,
|
|
1216
|
-
message: result.error
|
|
1217
|
-
});
|
|
1218
|
-
this.metrics.increment(EchoMetrics.OUTGOING_FAILURES, {
|
|
1219
|
-
event_type: payload.event,
|
|
1220
|
-
error_type: this.categorizeError(result)
|
|
1221
|
-
});
|
|
1222
|
-
}
|
|
1223
|
-
return result;
|
|
1224
|
-
});
|
|
1225
|
-
}
|
|
1226
|
-
async dispatchInternal(payload) {
|
|
1227
|
-
let lastResult = null;
|
|
1228
|
-
for (let attempt = 1;attempt <= this.retryConfig.maxAttempts; attempt++) {
|
|
1229
|
-
const result = await this.attemptDelivery(payload, attempt);
|
|
1230
|
-
lastResult = result;
|
|
1231
|
-
if (result.success) {
|
|
1232
|
-
return result;
|
|
1233
|
-
}
|
|
1234
|
-
if (attempt < this.retryConfig.maxAttempts) {
|
|
1235
|
-
const shouldRetry = this.shouldRetry(result);
|
|
1236
|
-
if (shouldRetry) {
|
|
1237
|
-
const delay = this.calculateDelay(attempt);
|
|
1238
|
-
await this.sleep(delay);
|
|
1239
|
-
continue;
|
|
1240
|
-
}
|
|
1241
|
-
}
|
|
1242
|
-
break;
|
|
1243
|
-
}
|
|
1244
|
-
if (this.dlq && lastResult && !lastResult.success) {
|
|
1245
|
-
await this.dlq.enqueue({
|
|
1246
|
-
type: "outgoing",
|
|
1247
|
-
originalEvent: {
|
|
1248
|
-
url: payload.url,
|
|
1249
|
-
event: payload.event,
|
|
1250
|
-
payload: payload.data,
|
|
1251
|
-
createdAt: new Date,
|
|
1252
|
-
status: "failed",
|
|
1253
|
-
attempts: []
|
|
1254
|
-
},
|
|
1255
|
-
failureReason: lastResult.error ?? "Unknown error",
|
|
1256
|
-
failedAt: new Date,
|
|
1257
|
-
retryCount: lastResult.attempt
|
|
1258
|
-
});
|
|
1259
|
-
}
|
|
1260
|
-
if (!lastResult) {
|
|
1261
|
-
throw new Error("No delivery attempts were made");
|
|
1262
|
-
}
|
|
1263
|
-
return lastResult;
|
|
1264
|
-
}
|
|
1265
|
-
async dispatchBatch(payloads, options = {}) {
|
|
1266
|
-
const concurrency = options.concurrency ?? 5;
|
|
1267
|
-
const stopOnFirstFailure = options.stopOnFirstFailure ?? false;
|
|
1268
|
-
const results = [];
|
|
1269
|
-
let succeeded = 0;
|
|
1270
|
-
let failed = 0;
|
|
1271
|
-
let stopped = false;
|
|
1272
|
-
for (let i = 0;i < payloads.length && !stopped; i += concurrency) {
|
|
1273
|
-
const chunk = payloads.slice(i, i + concurrency);
|
|
1274
|
-
const chunkResults = await Promise.all(chunk.map(async (payload) => {
|
|
1275
|
-
if (stopped) {
|
|
1276
|
-
return {
|
|
1277
|
-
payload,
|
|
1278
|
-
result: {
|
|
1279
|
-
success: false,
|
|
1280
|
-
attempt: 0,
|
|
1281
|
-
duration: 0,
|
|
1282
|
-
deliveredAt: new Date,
|
|
1283
|
-
error: "Batch dispatch stopped"
|
|
1284
|
-
}
|
|
1285
|
-
};
|
|
1286
|
-
}
|
|
1287
|
-
const result = await this.dispatch(payload);
|
|
1288
|
-
if (result.success) {
|
|
1289
|
-
succeeded++;
|
|
1290
|
-
} else {
|
|
1291
|
-
failed++;
|
|
1292
|
-
if (stopOnFirstFailure) {
|
|
1293
|
-
stopped = true;
|
|
1294
|
-
}
|
|
1295
|
-
}
|
|
1296
|
-
return { payload, result };
|
|
1297
|
-
}));
|
|
1298
|
-
results.push(...chunkResults);
|
|
1299
|
-
}
|
|
1300
|
-
return {
|
|
1301
|
-
total: payloads.length,
|
|
1302
|
-
succeeded,
|
|
1303
|
-
failed,
|
|
1304
|
-
results
|
|
1305
|
-
};
|
|
1306
|
-
}
|
|
1307
|
-
async retryFromDlq(id) {
|
|
1308
|
-
if (!this.dlq) {
|
|
1309
|
-
return null;
|
|
1310
|
-
}
|
|
1311
|
-
const events = await this.dlq.peek(100);
|
|
1312
|
-
const event = events.find((e) => e.id === id);
|
|
1313
|
-
if (!event || event.type !== "outgoing") {
|
|
1314
|
-
return null;
|
|
1315
|
-
}
|
|
1316
|
-
const outgoing = event.originalEvent;
|
|
1317
|
-
const result = await this.dispatch({
|
|
1318
|
-
url: outgoing.url,
|
|
1319
|
-
event: outgoing.event,
|
|
1320
|
-
data: outgoing.payload
|
|
1321
|
-
});
|
|
1322
|
-
if (result.success) {
|
|
1323
|
-
await this.dlq.dequeue(id);
|
|
1324
|
-
} else {
|
|
1325
|
-
event.retryCount++;
|
|
1326
|
-
event.lastRetryAt = new Date;
|
|
1327
|
-
}
|
|
1328
|
-
return result;
|
|
1329
|
-
}
|
|
1330
|
-
async attemptDelivery(payload, attempt) {
|
|
1331
|
-
const breaker = this.getCircuitBreaker(payload.url);
|
|
1332
|
-
const deliveryFn = async () => {
|
|
1333
|
-
const startTime = Date.now();
|
|
1334
|
-
const timestamp = Math.floor(Date.now() / 1000);
|
|
1335
|
-
const webhookId = payload.id ?? crypto.randomUUID();
|
|
1336
|
-
try {
|
|
1337
|
-
const body = JSON.stringify({
|
|
1338
|
-
id: webhookId,
|
|
1339
|
-
type: payload.event,
|
|
1340
|
-
timestamp,
|
|
1341
|
-
data: payload.data
|
|
1342
|
-
});
|
|
1343
|
-
const signedPayload = `${timestamp}.${body}`;
|
|
1344
|
-
const signature = await computeHmacSha256(signedPayload, this.secret);
|
|
1345
|
-
const controller = new AbortController;
|
|
1346
|
-
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
1347
|
-
try {
|
|
1348
|
-
const response = await fetch(payload.url, {
|
|
1349
|
-
method: "POST",
|
|
1350
|
-
headers: {
|
|
1351
|
-
"Content-Type": "application/json",
|
|
1352
|
-
"User-Agent": this.userAgent,
|
|
1353
|
-
"X-Webhook-ID": webhookId,
|
|
1354
|
-
"X-Webhook-Timestamp": String(timestamp),
|
|
1355
|
-
"X-Webhook-Signature": `t=${timestamp},v1=${signature}`
|
|
1356
|
-
},
|
|
1357
|
-
body,
|
|
1358
|
-
signal: controller.signal
|
|
1359
|
-
});
|
|
1360
|
-
clearTimeout(timeoutId);
|
|
1361
|
-
const duration = Date.now() - startTime;
|
|
1362
|
-
const responseBody = await response.text();
|
|
1363
|
-
return {
|
|
1364
|
-
success: response.ok,
|
|
1365
|
-
statusCode: response.status,
|
|
1366
|
-
body: responseBody,
|
|
1367
|
-
attempt,
|
|
1368
|
-
duration,
|
|
1369
|
-
deliveredAt: new Date,
|
|
1370
|
-
error: response.ok ? undefined : `HTTP ${response.status}`
|
|
1371
|
-
};
|
|
1372
|
-
} finally {
|
|
1373
|
-
clearTimeout(timeoutId);
|
|
1374
|
-
}
|
|
1375
|
-
} catch (error) {
|
|
1376
|
-
const duration = Date.now() - startTime;
|
|
1377
|
-
return {
|
|
1378
|
-
success: false,
|
|
1379
|
-
attempt,
|
|
1380
|
-
duration,
|
|
1381
|
-
deliveredAt: new Date,
|
|
1382
|
-
error: error instanceof Error ? error.message : "Unknown error"
|
|
1383
|
-
};
|
|
1384
|
-
}
|
|
1385
|
-
};
|
|
1386
|
-
if (breaker) {
|
|
1387
|
-
try {
|
|
1388
|
-
return await breaker.execute(deliveryFn);
|
|
1389
|
-
} catch (error) {
|
|
1390
|
-
if (error instanceof Error && error.message.includes("Circuit breaker is OPEN")) {
|
|
1391
|
-
return {
|
|
1392
|
-
success: false,
|
|
1393
|
-
attempt,
|
|
1394
|
-
duration: 0,
|
|
1395
|
-
deliveredAt: new Date,
|
|
1396
|
-
error: error.message
|
|
1397
|
-
};
|
|
1398
|
-
}
|
|
1399
|
-
throw error;
|
|
1400
|
-
}
|
|
1401
|
-
}
|
|
1402
|
-
return await deliveryFn();
|
|
1403
|
-
}
|
|
1404
|
-
shouldRetry(result) {
|
|
1405
|
-
if (!result.statusCode) {
|
|
1406
|
-
return true;
|
|
1407
|
-
}
|
|
1408
|
-
return this.retryConfig.retryableStatuses.includes(result.statusCode);
|
|
1409
|
-
}
|
|
1410
|
-
calculateDelay(attempt) {
|
|
1411
|
-
const delay = this.retryConfig.initialDelay * this.retryConfig.backoffMultiplier ** (attempt - 1);
|
|
1412
|
-
return Math.min(delay, this.retryConfig.maxDelay);
|
|
1413
|
-
}
|
|
1414
|
-
categorizeError(result) {
|
|
1415
|
-
if (!result.statusCode) {
|
|
1416
|
-
return "network_error";
|
|
1417
|
-
}
|
|
1418
|
-
if (result.statusCode >= 500) {
|
|
1419
|
-
return "server_error";
|
|
1420
|
-
}
|
|
1421
|
-
if (result.statusCode >= 400) {
|
|
1422
|
-
return "client_error";
|
|
1423
|
-
}
|
|
1424
|
-
return "other";
|
|
1425
|
-
}
|
|
1426
|
-
sleep(ms) {
|
|
1427
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1428
|
-
}
|
|
1429
|
-
}
|
|
1430
|
-
|
|
1431
|
-
// src/OrbitEcho.ts
|
|
1432
|
-
class OrbitEcho {
|
|
1433
|
-
receiver;
|
|
1434
|
-
dispatcher;
|
|
1435
|
-
echoConfig;
|
|
1436
|
-
keyRotationManager;
|
|
1437
|
-
constructor(config = {}) {
|
|
1438
|
-
this.echoConfig = config;
|
|
1439
|
-
this.receiver = new WebhookReceiver;
|
|
1440
|
-
if (config.keyRotation?.enabled) {
|
|
1441
|
-
this.keyRotationManager = new KeyRotationManager(config.keyRotation);
|
|
1442
|
-
this.receiver.setKeyRotationManager(this.keyRotationManager);
|
|
1443
|
-
}
|
|
1444
|
-
if (config.providers) {
|
|
1445
|
-
for (const [name, providerConfig] of Object.entries(config.providers)) {
|
|
1446
|
-
const rotationConfig = providerConfig;
|
|
1447
|
-
if (rotationConfig.keys && rotationConfig.keys.length > 0 && this.keyRotationManager) {
|
|
1448
|
-
this.receiver.registerProviderWithRotation(name, rotationConfig.keys, {
|
|
1449
|
-
type: providerConfig.name,
|
|
1450
|
-
tolerance: providerConfig.tolerance
|
|
1451
|
-
});
|
|
1452
|
-
} else {
|
|
1453
|
-
this.receiver.registerProvider(name, providerConfig.secret, {
|
|
1454
|
-
type: providerConfig.name,
|
|
1455
|
-
tolerance: providerConfig.tolerance
|
|
1456
|
-
});
|
|
1457
|
-
}
|
|
1458
|
-
}
|
|
1459
|
-
}
|
|
1460
|
-
if (config.dispatcher) {
|
|
1461
|
-
this.dispatcher = new WebhookDispatcher(config.dispatcher);
|
|
1462
|
-
if (config.deadLetterQueue) {
|
|
1463
|
-
this.dispatcher.setDeadLetterQueue(config.deadLetterQueue);
|
|
1464
|
-
}
|
|
1465
|
-
}
|
|
1466
|
-
if (config.store) {
|
|
1467
|
-
this.receiver.setStore(config.store);
|
|
1468
|
-
}
|
|
1469
|
-
if (config.observability) {
|
|
1470
|
-
const { metrics, tracer, logger } = config.observability;
|
|
1471
|
-
if (metrics) {
|
|
1472
|
-
this.receiver.setMetrics(metrics);
|
|
1473
|
-
this.dispatcher?.setMetrics(metrics);
|
|
1474
|
-
}
|
|
1475
|
-
if (tracer) {
|
|
1476
|
-
this.receiver.setTracer(tracer);
|
|
1477
|
-
this.dispatcher?.setTracer(tracer);
|
|
1478
|
-
}
|
|
1479
|
-
if (logger) {
|
|
1480
|
-
this.receiver.setLogger(logger);
|
|
1481
|
-
}
|
|
1482
|
-
}
|
|
1483
|
-
}
|
|
1484
|
-
install(core) {
|
|
1485
|
-
if (this.echoConfig.requestBuffer?.enabled !== false) {
|
|
1486
|
-
const bufferMiddleware = createRequestBufferMiddleware(this.echoConfig.requestBuffer);
|
|
1487
|
-
core.adapter.use("*", bufferMiddleware);
|
|
1488
|
-
core.logger.info("[OrbitEcho] Request buffer middleware installed");
|
|
1489
|
-
}
|
|
1490
|
-
core.container.instance("echo", this);
|
|
1491
|
-
core.container.instance("echo.receiver", this.receiver);
|
|
1492
|
-
if (this.dispatcher) {
|
|
1493
|
-
core.container.instance("echo.dispatcher", this.dispatcher);
|
|
1494
|
-
}
|
|
1495
|
-
core.adapter.use("*", async (c, next) => {
|
|
1496
|
-
c.set("echo", this);
|
|
1497
|
-
return await next();
|
|
1498
|
-
});
|
|
1499
|
-
core.logger.info("[OrbitEcho] Webhook receiver and dispatcher registered");
|
|
1500
|
-
}
|
|
1501
|
-
getReceiver() {
|
|
1502
|
-
return this.receiver;
|
|
1503
|
-
}
|
|
1504
|
-
getDispatcher() {
|
|
1505
|
-
return this.dispatcher;
|
|
1506
|
-
}
|
|
1507
|
-
getConfig() {
|
|
1508
|
-
return this.echoConfig;
|
|
1509
|
-
}
|
|
1510
|
-
getKeyRotationManager() {
|
|
1511
|
-
return this.keyRotationManager;
|
|
1512
|
-
}
|
|
1513
|
-
async rotateProviderKey(providerName, newKey) {
|
|
1514
|
-
if (!this.keyRotationManager) {
|
|
1515
|
-
throw new Error("Key rotation is not enabled");
|
|
1516
|
-
}
|
|
1517
|
-
await this.keyRotationManager.rotatePrimaryKey(providerName, newKey);
|
|
1518
|
-
}
|
|
1519
|
-
}
|
|
1520
|
-
// src/replay/WebhookReplayService.ts
|
|
1521
|
-
class WebhookReplayService {
|
|
1522
|
-
store;
|
|
1523
|
-
dispatcher;
|
|
1524
|
-
constructor(store, dispatcher) {
|
|
1525
|
-
this.store = store;
|
|
1526
|
-
this.dispatcher = dispatcher;
|
|
1527
|
-
}
|
|
1528
|
-
async replay(options) {
|
|
1529
|
-
const events = await this.store.queryEvents({
|
|
1530
|
-
direction: "outgoing",
|
|
1531
|
-
provider: options.provider,
|
|
1532
|
-
eventType: options.eventType,
|
|
1533
|
-
from: options.timeRange?.from,
|
|
1534
|
-
to: options.timeRange?.to
|
|
1535
|
-
});
|
|
1536
|
-
const targetEvents = options.eventIds ? events.filter((e) => options.eventIds?.includes(e.id ?? "")) : events;
|
|
1537
|
-
const result = {
|
|
1538
|
-
total: targetEvents.length,
|
|
1539
|
-
replayed: 0,
|
|
1540
|
-
skipped: 0,
|
|
1541
|
-
failed: 0,
|
|
1542
|
-
events: []
|
|
1543
|
-
};
|
|
1544
|
-
for (const event of targetEvents) {
|
|
1545
|
-
if (event.direction !== "outgoing") {
|
|
1546
|
-
result.skipped++;
|
|
1547
|
-
result.events.push({
|
|
1548
|
-
eventId: event.id ?? "unknown",
|
|
1549
|
-
status: "skipped",
|
|
1550
|
-
error: "Not an outgoing event"
|
|
1551
|
-
});
|
|
1552
|
-
continue;
|
|
1553
|
-
}
|
|
1554
|
-
const outgoing = event;
|
|
1555
|
-
if (options.dryRun) {
|
|
1556
|
-
result.replayed++;
|
|
1557
|
-
result.events.push({
|
|
1558
|
-
eventId: event.id ?? "unknown",
|
|
1559
|
-
status: "replayed"
|
|
1560
|
-
});
|
|
1561
|
-
continue;
|
|
1562
|
-
}
|
|
1563
|
-
try {
|
|
1564
|
-
const dispatchResult = await this.dispatcher.dispatch({
|
|
1565
|
-
url: options.targetUrl ?? outgoing.url,
|
|
1566
|
-
event: outgoing.event,
|
|
1567
|
-
data: outgoing.payload
|
|
1568
|
-
});
|
|
1569
|
-
if (dispatchResult.success) {
|
|
1570
|
-
result.replayed++;
|
|
1571
|
-
result.events.push({
|
|
1572
|
-
eventId: event.id ?? "unknown",
|
|
1573
|
-
status: "replayed",
|
|
1574
|
-
result: dispatchResult
|
|
1575
|
-
});
|
|
1576
|
-
} else {
|
|
1577
|
-
result.failed++;
|
|
1578
|
-
result.events.push({
|
|
1579
|
-
eventId: event.id ?? "unknown",
|
|
1580
|
-
status: "failed",
|
|
1581
|
-
result: dispatchResult,
|
|
1582
|
-
error: dispatchResult.error
|
|
1583
|
-
});
|
|
1584
|
-
}
|
|
1585
|
-
} catch (error) {
|
|
1586
|
-
result.failed++;
|
|
1587
|
-
result.events.push({
|
|
1588
|
-
eventId: event.id ?? "unknown",
|
|
1589
|
-
status: "failed",
|
|
1590
|
-
error: String(error)
|
|
1591
|
-
});
|
|
1592
|
-
}
|
|
1593
|
-
}
|
|
1594
|
-
return result;
|
|
1595
|
-
}
|
|
1596
|
-
}
|
|
1597
2
|
export {
|
|
1598
3
|
validateTimestamp,
|
|
1599
4
|
timingSafeEqual,
|
|
@@ -1626,4 +31,4 @@ export {
|
|
|
1626
31
|
BaseProvider
|
|
1627
32
|
};
|
|
1628
33
|
|
|
1629
|
-
//# debugId=
|
|
34
|
+
//# debugId=AEF7E6966FAD4D5C64756E2164756E21
|