@gravito/echo 3.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (221) hide show
  1. package/README.md +211 -0
  2. package/dist/atlas/src/DB.d.ts +301 -0
  3. package/dist/atlas/src/OrbitAtlas.d.ts +9 -0
  4. package/dist/atlas/src/config/defineConfig.d.ts +14 -0
  5. package/dist/atlas/src/config/index.d.ts +7 -0
  6. package/dist/atlas/src/config/loadConfig.d.ts +48 -0
  7. package/dist/atlas/src/connection/Connection.d.ts +108 -0
  8. package/dist/atlas/src/connection/ConnectionManager.d.ts +111 -0
  9. package/dist/atlas/src/drivers/BunSQLDriver.d.ts +32 -0
  10. package/dist/atlas/src/drivers/BunSQLPreparedStatement.d.ts +118 -0
  11. package/dist/atlas/src/drivers/MongoDBDriver.d.ts +36 -0
  12. package/dist/atlas/src/drivers/MySQLDriver.d.ts +66 -0
  13. package/dist/atlas/src/drivers/PostgresDriver.d.ts +83 -0
  14. package/dist/atlas/src/drivers/RedisDriver.d.ts +43 -0
  15. package/dist/atlas/src/drivers/SQLiteDriver.d.ts +45 -0
  16. package/dist/atlas/src/drivers/types.d.ts +260 -0
  17. package/dist/atlas/src/errors/index.d.ts +45 -0
  18. package/dist/atlas/src/grammar/Grammar.d.ts +342 -0
  19. package/dist/atlas/src/grammar/MongoGrammar.d.ts +47 -0
  20. package/dist/atlas/src/grammar/MySQLGrammar.d.ts +54 -0
  21. package/dist/atlas/src/grammar/NullGrammar.d.ts +35 -0
  22. package/dist/atlas/src/grammar/PostgresGrammar.d.ts +62 -0
  23. package/dist/atlas/src/grammar/SQLiteGrammar.d.ts +32 -0
  24. package/dist/atlas/src/index.d.ts +67 -0
  25. package/dist/atlas/src/migration/Migration.d.ts +64 -0
  26. package/dist/atlas/src/migration/MigrationRepository.d.ts +65 -0
  27. package/dist/atlas/src/migration/Migrator.d.ts +110 -0
  28. package/dist/atlas/src/migration/index.d.ts +6 -0
  29. package/dist/atlas/src/observability/AtlasMetrics.d.ts +11 -0
  30. package/dist/atlas/src/observability/AtlasObservability.d.ts +15 -0
  31. package/dist/atlas/src/observability/AtlasTracer.d.ts +12 -0
  32. package/dist/atlas/src/observability/index.d.ts +9 -0
  33. package/dist/atlas/src/orm/index.d.ts +5 -0
  34. package/dist/atlas/src/orm/model/DirtyTracker.d.ts +121 -0
  35. package/dist/atlas/src/orm/model/Model.d.ts +449 -0
  36. package/dist/atlas/src/orm/model/ModelRegistry.d.ts +20 -0
  37. package/dist/atlas/src/orm/model/concerns/HasAttributes.d.ts +136 -0
  38. package/dist/atlas/src/orm/model/concerns/HasEvents.d.ts +36 -0
  39. package/dist/atlas/src/orm/model/concerns/HasPersistence.d.ts +87 -0
  40. package/dist/atlas/src/orm/model/concerns/HasRelationships.d.ts +117 -0
  41. package/dist/atlas/src/orm/model/concerns/HasSerialization.d.ts +64 -0
  42. package/dist/atlas/src/orm/model/concerns/applyMixins.d.ts +15 -0
  43. package/dist/atlas/src/orm/model/concerns/index.d.ts +12 -0
  44. package/dist/atlas/src/orm/model/decorators.d.ts +109 -0
  45. package/dist/atlas/src/orm/model/errors.d.ts +52 -0
  46. package/dist/atlas/src/orm/model/index.d.ts +10 -0
  47. package/dist/atlas/src/orm/model/relationships.d.ts +207 -0
  48. package/dist/atlas/src/orm/model/types.d.ts +12 -0
  49. package/dist/atlas/src/orm/schema/SchemaRegistry.d.ts +123 -0
  50. package/dist/atlas/src/orm/schema/SchemaSniffer.d.ts +54 -0
  51. package/dist/atlas/src/orm/schema/index.d.ts +6 -0
  52. package/dist/atlas/src/orm/schema/types.d.ts +85 -0
  53. package/dist/atlas/src/query/Expression.d.ts +60 -0
  54. package/dist/atlas/src/query/NPlusOneDetector.d.ts +10 -0
  55. package/dist/atlas/src/query/QueryBuilder.d.ts +573 -0
  56. package/dist/atlas/src/query/clauses/GroupByClause.d.ts +51 -0
  57. package/dist/atlas/src/query/clauses/HavingClause.d.ts +70 -0
  58. package/dist/atlas/src/query/clauses/JoinClause.d.ts +87 -0
  59. package/dist/atlas/src/query/clauses/LimitClause.d.ts +82 -0
  60. package/dist/atlas/src/query/clauses/OrderByClause.d.ts +69 -0
  61. package/dist/atlas/src/query/clauses/SelectClause.d.ts +71 -0
  62. package/dist/atlas/src/query/clauses/WhereClause.d.ts +167 -0
  63. package/dist/atlas/src/query/clauses/index.d.ts +11 -0
  64. package/dist/atlas/src/schema/Blueprint.d.ts +276 -0
  65. package/dist/atlas/src/schema/ColumnDefinition.d.ts +154 -0
  66. package/dist/atlas/src/schema/ForeignKeyDefinition.d.ts +37 -0
  67. package/dist/atlas/src/schema/Schema.d.ts +131 -0
  68. package/dist/atlas/src/schema/grammars/MySQLSchemaGrammar.d.ts +23 -0
  69. package/dist/atlas/src/schema/grammars/PostgresSchemaGrammar.d.ts +26 -0
  70. package/dist/atlas/src/schema/grammars/SQLiteSchemaGrammar.d.ts +28 -0
  71. package/dist/atlas/src/schema/grammars/SchemaGrammar.d.ts +97 -0
  72. package/dist/atlas/src/schema/grammars/index.d.ts +7 -0
  73. package/dist/atlas/src/schema/index.d.ts +8 -0
  74. package/dist/atlas/src/seed/Factory.d.ts +90 -0
  75. package/dist/atlas/src/seed/Seeder.d.ts +28 -0
  76. package/dist/atlas/src/seed/SeederRunner.d.ts +74 -0
  77. package/dist/atlas/src/seed/index.d.ts +6 -0
  78. package/dist/atlas/src/types/index.d.ts +1100 -0
  79. package/dist/atlas/src/utils/levenshtein.d.ts +9 -0
  80. package/dist/core/src/Application.d.ts +215 -0
  81. package/dist/core/src/CommandKernel.d.ts +33 -0
  82. package/dist/core/src/ConfigManager.d.ts +26 -0
  83. package/dist/core/src/Container.d.ts +108 -0
  84. package/dist/core/src/ErrorHandler.d.ts +63 -0
  85. package/dist/core/src/Event.d.ts +5 -0
  86. package/dist/core/src/EventManager.d.ts +123 -0
  87. package/dist/core/src/GlobalErrorHandlers.d.ts +47 -0
  88. package/dist/core/src/GravitoServer.d.ts +28 -0
  89. package/dist/core/src/HookManager.d.ts +496 -0
  90. package/dist/core/src/Listener.d.ts +4 -0
  91. package/dist/core/src/Logger.d.ts +20 -0
  92. package/dist/core/src/PlanetCore.d.ts +289 -0
  93. package/dist/core/src/Route.d.ts +36 -0
  94. package/dist/core/src/Router.d.ts +284 -0
  95. package/dist/core/src/ServiceProvider.d.ts +156 -0
  96. package/dist/core/src/adapters/GravitoEngineAdapter.d.ts +27 -0
  97. package/dist/core/src/adapters/PhotonAdapter.d.ts +171 -0
  98. package/dist/core/src/adapters/bun/BunContext.d.ts +45 -0
  99. package/dist/core/src/adapters/bun/BunNativeAdapter.d.ts +31 -0
  100. package/dist/core/src/adapters/bun/BunRequest.d.ts +31 -0
  101. package/dist/core/src/adapters/bun/RadixNode.d.ts +19 -0
  102. package/dist/core/src/adapters/bun/RadixRouter.d.ts +31 -0
  103. package/dist/core/src/adapters/bun/types.d.ts +20 -0
  104. package/dist/core/src/adapters/photon-types.d.ts +73 -0
  105. package/dist/core/src/adapters/types.d.ts +235 -0
  106. package/dist/core/src/engine/AOTRouter.d.ts +124 -0
  107. package/dist/core/src/engine/FastContext.d.ts +100 -0
  108. package/dist/core/src/engine/Gravito.d.ts +137 -0
  109. package/dist/core/src/engine/MinimalContext.d.ts +79 -0
  110. package/dist/core/src/engine/analyzer.d.ts +27 -0
  111. package/dist/core/src/engine/constants.d.ts +23 -0
  112. package/dist/core/src/engine/index.d.ts +26 -0
  113. package/dist/core/src/engine/path.d.ts +26 -0
  114. package/dist/core/src/engine/pool.d.ts +83 -0
  115. package/dist/core/src/engine/types.d.ts +143 -0
  116. package/dist/core/src/events/CircuitBreaker.d.ts +229 -0
  117. package/dist/core/src/events/DeadLetterQueue.d.ts +145 -0
  118. package/dist/core/src/events/EventBackend.d.ts +11 -0
  119. package/dist/core/src/events/EventOptions.d.ts +109 -0
  120. package/dist/core/src/events/EventPriorityQueue.d.ts +202 -0
  121. package/dist/core/src/events/IdempotencyCache.d.ts +60 -0
  122. package/dist/core/src/events/index.d.ts +14 -0
  123. package/dist/core/src/events/observability/EventMetrics.d.ts +132 -0
  124. package/dist/core/src/events/observability/EventTracer.d.ts +68 -0
  125. package/dist/core/src/events/observability/EventTracing.d.ts +161 -0
  126. package/dist/core/src/events/observability/OTelEventMetrics.d.ts +240 -0
  127. package/dist/core/src/events/observability/ObservableHookManager.d.ts +108 -0
  128. package/dist/core/src/events/observability/index.d.ts +20 -0
  129. package/dist/core/src/events/observability/metrics-types.d.ts +16 -0
  130. package/dist/core/src/events/types.d.ts +75 -0
  131. package/dist/core/src/exceptions/AuthenticationException.d.ts +8 -0
  132. package/dist/core/src/exceptions/AuthorizationException.d.ts +8 -0
  133. package/dist/core/src/exceptions/CircularDependencyException.d.ts +9 -0
  134. package/dist/core/src/exceptions/GravitoException.d.ts +23 -0
  135. package/dist/core/src/exceptions/HttpException.d.ts +9 -0
  136. package/dist/core/src/exceptions/ModelNotFoundException.d.ts +10 -0
  137. package/dist/core/src/exceptions/ValidationException.d.ts +22 -0
  138. package/dist/core/src/exceptions/index.d.ts +7 -0
  139. package/dist/core/src/helpers/Arr.d.ts +19 -0
  140. package/dist/core/src/helpers/Str.d.ts +23 -0
  141. package/dist/core/src/helpers/data.d.ts +25 -0
  142. package/dist/core/src/helpers/errors.d.ts +34 -0
  143. package/dist/core/src/helpers/response.d.ts +41 -0
  144. package/dist/core/src/helpers.d.ts +338 -0
  145. package/dist/core/src/http/CookieJar.d.ts +51 -0
  146. package/dist/core/src/http/cookie.d.ts +29 -0
  147. package/dist/core/src/http/middleware/BodySizeLimit.d.ts +16 -0
  148. package/dist/core/src/http/middleware/Cors.d.ts +24 -0
  149. package/dist/core/src/http/middleware/Csrf.d.ts +23 -0
  150. package/dist/core/src/http/middleware/HeaderTokenGate.d.ts +28 -0
  151. package/dist/core/src/http/middleware/SecurityHeaders.d.ts +29 -0
  152. package/dist/core/src/http/middleware/ThrottleRequests.d.ts +18 -0
  153. package/dist/core/src/http/types.d.ts +355 -0
  154. package/dist/core/src/index.d.ts +76 -0
  155. package/dist/core/src/instrumentation/index.d.ts +35 -0
  156. package/dist/core/src/instrumentation/opentelemetry.d.ts +178 -0
  157. package/dist/core/src/instrumentation/types.d.ts +182 -0
  158. package/dist/core/src/reliability/DeadLetterQueueManager.d.ts +316 -0
  159. package/dist/core/src/reliability/RetryPolicy.d.ts +217 -0
  160. package/dist/core/src/reliability/index.d.ts +6 -0
  161. package/dist/core/src/router/ControllerDispatcher.d.ts +12 -0
  162. package/dist/core/src/router/RequestValidator.d.ts +20 -0
  163. package/dist/core/src/runtime.d.ts +119 -0
  164. package/dist/core/src/security/Encrypter.d.ts +33 -0
  165. package/dist/core/src/security/Hasher.d.ts +29 -0
  166. package/dist/core/src/testing/HttpTester.d.ts +39 -0
  167. package/dist/core/src/testing/TestResponse.d.ts +78 -0
  168. package/dist/core/src/testing/index.d.ts +2 -0
  169. package/dist/core/src/types/events.d.ts +94 -0
  170. package/dist/echo/src/OrbitEcho.d.ts +115 -0
  171. package/dist/echo/src/dlq/DeadLetterQueue.d.ts +94 -0
  172. package/dist/echo/src/dlq/MemoryDeadLetterQueue.d.ts +36 -0
  173. package/dist/echo/src/dlq/index.d.ts +2 -0
  174. package/dist/echo/src/index.d.ts +64 -0
  175. package/dist/echo/src/middleware/RequestBufferMiddleware.d.ts +62 -0
  176. package/dist/echo/src/middleware/index.d.ts +8 -0
  177. package/dist/echo/src/observability/index.d.ts +3 -0
  178. package/dist/echo/src/observability/logging/ConsoleEchoLogger.d.ts +37 -0
  179. package/dist/echo/src/observability/logging/EchoLogger.d.ts +38 -0
  180. package/dist/echo/src/observability/logging/index.d.ts +2 -0
  181. package/dist/echo/src/observability/metrics/MetricsProvider.d.ts +69 -0
  182. package/dist/echo/src/observability/metrics/NoopMetricsProvider.d.ts +17 -0
  183. package/dist/echo/src/observability/metrics/PrometheusMetricsProvider.d.ts +39 -0
  184. package/dist/echo/src/observability/metrics/index.d.ts +3 -0
  185. package/dist/echo/src/observability/tracing/NoopTracer.d.ts +33 -0
  186. package/dist/echo/src/observability/tracing/Tracer.d.ts +75 -0
  187. package/dist/echo/src/observability/tracing/index.d.ts +2 -0
  188. package/dist/echo/src/providers/GenericProvider.d.ts +53 -0
  189. package/dist/echo/src/providers/GitHubProvider.d.ts +35 -0
  190. package/dist/echo/src/providers/LinearProvider.d.ts +27 -0
  191. package/dist/echo/src/providers/PaddleProvider.d.ts +31 -0
  192. package/dist/echo/src/providers/ShopifyProvider.d.ts +27 -0
  193. package/dist/echo/src/providers/SlackProvider.d.ts +27 -0
  194. package/dist/echo/src/providers/StripeProvider.d.ts +38 -0
  195. package/dist/echo/src/providers/TwilioProvider.d.ts +31 -0
  196. package/dist/echo/src/providers/base/BaseProvider.d.ts +87 -0
  197. package/dist/echo/src/providers/base/HeaderUtils.d.ts +34 -0
  198. package/dist/echo/src/providers/index.d.ts +14 -0
  199. package/dist/echo/src/receive/SignatureValidator.d.ts +67 -0
  200. package/dist/echo/src/receive/WebhookReceiver.d.ts +185 -0
  201. package/dist/echo/src/receive/index.d.ts +2 -0
  202. package/dist/echo/src/replay/WebhookReplayService.d.ts +35 -0
  203. package/dist/echo/src/replay/index.d.ts +1 -0
  204. package/dist/echo/src/resilience/CircuitBreaker.d.ts +117 -0
  205. package/dist/echo/src/resilience/index.d.ts +10 -0
  206. package/dist/echo/src/rotation/KeyRotationManager.d.ts +127 -0
  207. package/dist/echo/src/rotation/index.d.ts +10 -0
  208. package/dist/echo/src/send/WebhookDispatcher.d.ts +198 -0
  209. package/dist/echo/src/send/index.d.ts +1 -0
  210. package/dist/echo/src/storage/MemoryWebhookStore.d.ts +14 -0
  211. package/dist/echo/src/storage/WebhookStore.d.ts +236 -0
  212. package/dist/echo/src/storage/index.d.ts +2 -0
  213. package/dist/echo/src/types.d.ts +756 -0
  214. package/dist/index.js +1332 -190
  215. package/dist/index.js.map +28 -10
  216. package/dist/photon/src/index.d.ts +84 -0
  217. package/dist/photon/src/middleware/binary.d.ts +31 -0
  218. package/dist/photon/src/middleware/htmx.d.ts +39 -0
  219. package/dist/photon/src/middleware/ratelimit.d.ts +157 -0
  220. package/dist/photon/src/openapi.d.ts +19 -0
  221. package/package.json +7 -5
package/dist/index.js CHANGED
@@ -1,4 +1,205 @@
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 ||= {});
2
203
  // src/receive/SignatureValidator.ts
3
204
  async function computeHmacSha256(payload, secret) {
4
205
  const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
@@ -6,12 +207,24 @@ async function computeHmacSha256(payload, secret) {
6
207
  const signature = await crypto.subtle.sign("HMAC", key, payloadBuffer);
7
208
  return Buffer.from(signature).toString("hex");
8
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
+ }
9
216
  async function computeHmacSha1(payload, secret) {
10
217
  const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(secret), { name: "HMAC", hash: "SHA-1" }, false, ["sign"]);
11
218
  const payloadBuffer = typeof payload === "string" ? new TextEncoder().encode(payload) : new Uint8Array(payload.buffer, payload.byteOffset, payload.byteLength);
12
219
  const signature = await crypto.subtle.sign("HMAC", key, payloadBuffer);
13
220
  return Buffer.from(signature).toString("hex");
14
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
+ }
15
228
  function timingSafeEqual(a, b) {
16
229
  if (a.length !== b.length) {
17
230
  return false;
@@ -46,106 +259,117 @@ function parseStripeSignature(header) {
46
259
  return { timestamp, signatures };
47
260
  }
48
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
+
49
306
  // src/providers/GenericProvider.ts
50
- class GenericProvider {
307
+ class GenericProvider extends BaseProvider {
51
308
  name = "generic";
52
309
  signatureHeader;
53
310
  timestampHeader;
54
- tolerance;
55
311
  constructor(options = {}) {
312
+ super(options);
56
313
  this.signatureHeader = options.signatureHeader ?? "x-webhook-signature";
57
314
  this.timestampHeader = options.timestampHeader ?? "x-webhook-timestamp";
58
- this.tolerance = options.tolerance ?? 300;
59
315
  }
60
316
  async verify(payload, headers, secret) {
61
317
  const signature = this.getHeader(headers, this.signatureHeader);
62
318
  if (!signature) {
63
- return {
64
- valid: false,
65
- error: `Missing signature header: ${this.signatureHeader}`
66
- };
319
+ return this.createFailure(`Missing signature header: ${this.signatureHeader}`);
67
320
  }
68
321
  const timestampStr = this.getHeader(headers, this.timestampHeader);
69
322
  if (timestampStr) {
70
323
  const timestamp = parseInt(timestampStr, 10);
71
324
  if (Number.isNaN(timestamp) || !validateTimestamp(timestamp, this.tolerance)) {
72
- return {
73
- valid: false,
74
- error: "Timestamp validation failed"
75
- };
325
+ return this.createFailure("Timestamp validation failed");
76
326
  }
77
327
  }
78
- const payloadStr = typeof payload === "string" ? payload : payload.toString("utf-8");
328
+ const payloadStr = this.payloadToString(payload);
79
329
  const expectedSignature = await computeHmacSha256(payloadStr, secret);
80
330
  if (!timingSafeEqual(signature.toLowerCase(), expectedSignature.toLowerCase())) {
81
- return {
82
- valid: false,
83
- error: "Signature verification failed"
84
- };
331
+ return this.createFailure("Signature verification failed");
85
332
  }
86
- try {
87
- const parsed = JSON.parse(payloadStr);
88
- return {
89
- valid: true,
90
- payload: parsed,
91
- eventType: parsed.type ?? parsed.event ?? parsed.eventType,
92
- webhookId: parsed.id ?? parsed.webhookId
93
- };
94
- } catch {
95
- return {
96
- valid: true,
97
- payload: payloadStr
98
- };
333
+ const parseResult = this.safeParseJson(payloadStr);
334
+ if (!parseResult.success) {
335
+ return this.createSuccess(payloadStr);
99
336
  }
100
- }
101
- getHeader(headers, name) {
102
- const value = headers[name] ?? headers[name.toLowerCase()];
103
- return Array.isArray(value) ? value[0] : value;
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
+ });
104
342
  }
105
343
  }
106
344
 
107
345
  // src/providers/GitHubProvider.ts
108
- class GitHubProvider {
346
+ class GitHubProvider extends BaseProvider {
109
347
  name = "github";
110
348
  async verify(payload, headers, secret) {
111
349
  const signature = this.getHeader(headers, "x-hub-signature-256");
112
350
  if (!signature) {
113
- return {
114
- valid: false,
115
- error: "Missing X-Hub-Signature-256 header"
116
- };
351
+ return this.createFailure("Missing X-Hub-Signature-256 header");
117
352
  }
118
353
  if (!signature.startsWith("sha256=")) {
119
- return {
120
- valid: false,
121
- error: "Invalid signature format (expected sha256=...)"
122
- };
354
+ return this.createFailure("Invalid signature format (expected sha256=...)");
123
355
  }
124
356
  const signatureValue = signature.slice(7);
125
- const payloadStr = typeof payload === "string" ? payload : payload.toString("utf-8");
357
+ const payloadStr = this.payloadToString(payload);
126
358
  const expectedSignature = await computeHmacSha256(payloadStr, secret);
127
359
  if (!timingSafeEqual(signatureValue.toLowerCase(), expectedSignature.toLowerCase())) {
128
- return {
129
- valid: false,
130
- error: "Signature verification failed"
131
- };
360
+ return this.createFailure("Signature verification failed");
132
361
  }
133
- try {
134
- const event = JSON.parse(payloadStr);
135
- const eventType = this.getHeader(headers, "x-github-event");
136
- const deliveryId = this.getHeader(headers, "x-github-delivery");
137
- return {
138
- valid: true,
139
- payload: event,
140
- eventType: eventType ?? undefined,
141
- webhookId: deliveryId ?? undefined
142
- };
143
- } catch {
144
- return {
145
- valid: false,
146
- error: "Failed to parse webhook payload"
147
- };
362
+ const parseResult = this.safeParseJson(payloadStr);
363
+ if (!parseResult.success) {
364
+ return this.createFailure("Failed to parse webhook payload");
148
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
+ });
149
373
  }
150
374
  parseEventType(payload) {
151
375
  if (typeof payload === "object" && payload !== null && "action" in payload) {
@@ -153,65 +377,180 @@ class GitHubProvider {
153
377
  }
154
378
  return;
155
379
  }
156
- getHeader(headers, name) {
157
- const value = headers[name] ?? headers[name.toLowerCase()];
158
- return Array.isArray(value) ? value[0] : value;
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
+ });
159
516
  }
160
517
  }
161
518
 
162
519
  // src/providers/StripeProvider.ts
163
- class StripeProvider {
520
+ class StripeProvider extends BaseProvider {
164
521
  name = "stripe";
165
- tolerance;
166
522
  constructor(options = {}) {
167
- this.tolerance = options.tolerance ?? 300;
523
+ super(options);
168
524
  }
169
525
  async verify(payload, headers, secret) {
170
526
  const signatureHeader = this.getHeader(headers, "stripe-signature");
171
527
  if (!signatureHeader) {
172
- return {
173
- valid: false,
174
- error: "Missing Stripe-Signature header"
175
- };
528
+ return this.createFailure("Missing Stripe-Signature header");
176
529
  }
177
530
  const parsed = parseStripeSignature(signatureHeader);
178
531
  if (!parsed) {
179
- return {
180
- valid: false,
181
- error: "Invalid Stripe-Signature header format"
182
- };
532
+ return this.createFailure("Invalid Stripe-Signature header format");
183
533
  }
184
534
  const { timestamp, signatures } = parsed;
185
535
  if (!validateTimestamp(timestamp, this.tolerance)) {
186
- return {
187
- valid: false,
188
- error: `Timestamp outside tolerance window (${this.tolerance}s)`
189
- };
536
+ return this.createFailure(`Timestamp outside tolerance window (${this.tolerance}s)`);
190
537
  }
191
- const payloadStr = typeof payload === "string" ? payload : payload.toString("utf-8");
538
+ const payloadStr = this.payloadToString(payload);
192
539
  const signedPayload = `${timestamp}.${payloadStr}`;
193
540
  const expectedSignature = await computeHmacSha256(signedPayload, secret);
194
541
  const signatureValid = signatures.some((sig) => timingSafeEqual(sig.toLowerCase(), expectedSignature.toLowerCase()));
195
542
  if (!signatureValid) {
196
- return {
197
- valid: false,
198
- error: "Signature verification failed"
199
- };
543
+ return this.createFailure("Signature verification failed");
200
544
  }
201
- try {
202
- const event = JSON.parse(payloadStr);
203
- return {
204
- valid: true,
205
- payload: event,
206
- eventType: event.type,
207
- webhookId: event.id
208
- };
209
- } catch {
210
- return {
211
- valid: false,
212
- error: "Failed to parse webhook payload"
213
- };
545
+ const parseResult = this.safeParseJson(payloadStr);
546
+ if (!parseResult.success) {
547
+ return this.createFailure("Failed to parse webhook payload");
214
548
  }
549
+ const event = parseResult.data;
550
+ return this.createSuccess(event, {
551
+ eventType: event.type,
552
+ webhookId: event.id
553
+ });
215
554
  }
216
555
  parseEventType(payload) {
217
556
  if (typeof payload === "object" && payload !== null && "type" in payload) {
@@ -219,9 +558,33 @@ class StripeProvider {
219
558
  }
220
559
  return;
221
560
  }
222
- getHeader(headers, name) {
223
- const value = headers[name] ?? headers[name.toLowerCase()];
224
- return Array.isArray(value) ? value[0] : value;
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
+ });
225
588
  }
226
589
  }
227
590
 
@@ -230,12 +593,42 @@ class WebhookReceiver {
230
593
  providers = new Map;
231
594
  handlers = new Map;
232
595
  globalHandlers = new Map;
596
+ store;
597
+ metrics = new NoopMetricsProvider;
598
+ tracer = new NoopTracer;
599
+ logger = new ConsoleEchoLogger;
600
+ keyRotationManager;
233
601
  constructor() {
234
602
  this.registerProviderType("generic", GenericProvider);
235
603
  this.registerProviderType("stripe", StripeProvider);
236
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);
237
610
  }
238
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
+ }
239
632
  registerProviderType(name, ProviderCls) {
240
633
  this.providerTypes.set(name, ProviderCls);
241
634
  return this;
@@ -250,11 +643,34 @@ class WebhookReceiver {
250
643
  this.providers.set(name, { provider, secret });
251
644
  return this;
252
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
+ }
253
664
  on(providerName, eventType, handler) {
254
665
  if (!this.handlers.has(providerName)) {
255
666
  this.handlers.set(providerName, new Map);
256
667
  }
257
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
+ }
258
674
  if (!providerHandlers.has(eventType)) {
259
675
  providerHandlers.set(eventType, []);
260
676
  }
@@ -268,48 +684,186 @@ class WebhookReceiver {
268
684
  this.globalHandlers.get(providerName)?.push(handler);
269
685
  return this;
270
686
  }
271
- async handle(providerName, body, headers) {
272
- const config = this.providers.get(providerName);
273
- if (!config) {
274
- return {
275
- valid: false,
276
- error: `Provider not registered: ${providerName}`,
277
- handled: false
278
- };
279
- }
280
- const { provider, secret } = config;
281
- const result = await provider.verify(body, headers, secret);
282
- if (!result.valid) {
283
- return { ...result, handled: false };
284
- }
285
- const event = {
286
- provider: providerName,
287
- type: result.eventType ?? "unknown",
288
- payload: result.payload,
289
- headers,
290
- rawBody: typeof body === "string" ? body : body.toString("utf-8"),
291
- receivedAt: new Date,
292
- id: result.webhookId
293
- };
294
- let handled = false;
295
- const providerHandlers = this.handlers.get(providerName);
296
- if (providerHandlers) {
297
- const eventHandlers = providerHandlers.get(event.type);
298
- if (eventHandlers) {
299
- for (const handler of eventHandlers) {
300
- await handler(event);
301
- handled = true;
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
+ };
302
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);
303
850
  }
851
+ });
852
+ }
853
+ categorizeError(error) {
854
+ if (!error) {
855
+ return "unknown";
304
856
  }
305
- const globalHandlers = this.globalHandlers.get(providerName);
306
- if (globalHandlers) {
307
- for (const handler of globalHandlers) {
308
- await handler(event);
309
- handled = true;
310
- }
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";
311
865
  }
312
- return { ...result, handled };
866
+ return "other";
313
867
  }
314
868
  async verify(providerName, body, headers) {
315
869
  const config = this.providers.get(providerName);
@@ -323,6 +877,220 @@ class WebhookReceiver {
323
877
  }
324
878
  }
325
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
+
326
1094
  // src/send/WebhookDispatcher.ts
327
1095
  var DEFAULT_RETRY_CONFIG = {
328
1096
  maxAttempts: 3,
@@ -337,13 +1105,125 @@ class WebhookDispatcher {
337
1105
  retryConfig;
338
1106
  timeout;
339
1107
  userAgent;
1108
+ dlq;
1109
+ metrics = new NoopMetricsProvider;
1110
+ tracer = new NoopTracer;
1111
+ logger;
1112
+ circuitBreakers = new Map;
1113
+ circuitBreakerConfig;
340
1114
  constructor(config) {
341
1115
  this.secret = config.secret;
342
1116
  this.retryConfig = { ...DEFAULT_RETRY_CONFIG, ...config.retry };
343
1117
  this.timeout = config.timeout ?? 30000;
344
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();
345
1180
  }
346
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) {
347
1227
  let lastResult = null;
348
1228
  for (let attempt = 1;attempt <= this.retryConfig.maxAttempts; attempt++) {
349
1229
  const result = await this.attemptDelivery(payload, attempt);
@@ -359,63 +1239,167 @@ class WebhookDispatcher {
359
1239
  continue;
360
1240
  }
361
1241
  }
362
- return result;
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");
363
1262
  }
364
1263
  return lastResult;
365
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
+ }
366
1330
  async attemptDelivery(payload, attempt) {
367
- const startTime = Date.now();
368
- const timestamp = Math.floor(Date.now() / 1000);
369
- const webhookId = payload.id ?? crypto.randomUUID();
370
- try {
371
- const body = JSON.stringify({
372
- id: webhookId,
373
- type: payload.event,
374
- timestamp,
375
- data: payload.data
376
- });
377
- const signedPayload = `${timestamp}.${body}`;
378
- const signature = await computeHmacSha256(signedPayload, this.secret);
379
- const controller = new AbortController;
380
- const timeoutId = setTimeout(() => controller.abort(), this.timeout);
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();
381
1336
  try {
382
- const response = await fetch(payload.url, {
383
- method: "POST",
384
- headers: {
385
- "Content-Type": "application/json",
386
- "User-Agent": this.userAgent,
387
- "X-Webhook-ID": webhookId,
388
- "X-Webhook-Timestamp": String(timestamp),
389
- "X-Webhook-Signature": `t=${timestamp},v1=${signature}`
390
- },
391
- body,
392
- signal: controller.signal
1337
+ const body = JSON.stringify({
1338
+ id: webhookId,
1339
+ type: payload.event,
1340
+ timestamp,
1341
+ data: payload.data
393
1342
  });
394
- clearTimeout(timeoutId);
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) {
395
1376
  const duration = Date.now() - startTime;
396
- const responseBody = await response.text();
397
1377
  return {
398
- success: response.ok,
399
- statusCode: response.status,
400
- body: responseBody,
1378
+ success: false,
401
1379
  attempt,
402
1380
  duration,
403
1381
  deliveredAt: new Date,
404
- error: response.ok ? undefined : `HTTP ${response.status}`
1382
+ error: error instanceof Error ? error.message : "Unknown error"
405
1383
  };
406
- } finally {
407
- clearTimeout(timeoutId);
408
1384
  }
409
- } catch (error) {
410
- const duration = Date.now() - startTime;
411
- return {
412
- success: false,
413
- attempt,
414
- duration,
415
- deliveredAt: new Date,
416
- error: error instanceof Error ? error.message : "Unknown error"
417
- };
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
+ }
418
1401
  }
1402
+ return await deliveryFn();
419
1403
  }
420
1404
  shouldRetry(result) {
421
1405
  if (!result.statusCode) {
@@ -427,6 +1411,18 @@ class WebhookDispatcher {
427
1411
  const delay = this.retryConfig.initialDelay * this.retryConfig.backoffMultiplier ** (attempt - 1);
428
1412
  return Math.min(delay, this.retryConfig.maxDelay);
429
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
+ }
430
1426
  sleep(ms) {
431
1427
  return new Promise((resolve) => setTimeout(resolve, ms));
432
1428
  }
@@ -434,31 +1430,73 @@ class WebhookDispatcher {
434
1430
 
435
1431
  // src/OrbitEcho.ts
436
1432
  class OrbitEcho {
437
- static config = { singleton: true };
438
1433
  receiver;
439
1434
  dispatcher;
440
1435
  echoConfig;
1436
+ keyRotationManager;
441
1437
  constructor(config = {}) {
442
1438
  this.echoConfig = config;
443
1439
  this.receiver = new WebhookReceiver;
1440
+ if (config.keyRotation?.enabled) {
1441
+ this.keyRotationManager = new KeyRotationManager(config.keyRotation);
1442
+ this.receiver.setKeyRotationManager(this.keyRotationManager);
1443
+ }
444
1444
  if (config.providers) {
445
1445
  for (const [name, providerConfig] of Object.entries(config.providers)) {
446
- this.receiver.registerProvider(name, providerConfig.secret, {
447
- type: providerConfig.name,
448
- tolerance: providerConfig.tolerance
449
- });
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
+ }
450
1458
  }
451
1459
  }
452
1460
  if (config.dispatcher) {
453
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
+ }
454
1482
  }
455
1483
  }
456
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
+ }
457
1490
  core.container.instance("echo", this);
458
1491
  core.container.instance("echo.receiver", this.receiver);
459
1492
  if (this.dispatcher) {
460
1493
  core.container.instance("echo.dispatcher", this.dispatcher);
461
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");
462
1500
  }
463
1501
  getReceiver() {
464
1502
  return this.receiver;
@@ -469,19 +1507,123 @@ class OrbitEcho {
469
1507
  getConfig() {
470
1508
  return this.echoConfig;
471
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
+ }
472
1596
  }
473
1597
  export {
474
1598
  validateTimestamp,
475
1599
  timingSafeEqual,
476
1600
  parseStripeSignature,
1601
+ createRequestBufferMiddleware,
477
1602
  computeHmacSha256,
478
1603
  computeHmacSha1,
1604
+ WebhookReplayService,
479
1605
  WebhookReceiver,
480
- WebhookDispatcher,
1606
+ TwilioProvider,
481
1607
  StripeProvider,
1608
+ SpanStatusCode,
1609
+ SlackProvider,
1610
+ ShopifyProvider,
1611
+ RequestBufferMiddleware,
1612
+ PrometheusMetricsProvider,
1613
+ PaddleProvider,
482
1614
  OrbitEcho,
1615
+ NoopTracer,
1616
+ NoopSpan,
1617
+ NoopMetricsProvider,
1618
+ MemoryDeadLetterQueue,
1619
+ LinearProvider,
1620
+ KeyRotationManager,
483
1621
  GitHubProvider,
484
- GenericProvider
1622
+ GenericProvider,
1623
+ EchoMetrics,
1624
+ ConsoleEchoLogger,
1625
+ CircuitBreaker,
1626
+ BaseProvider
485
1627
  };
486
1628
 
487
- //# debugId=B1111F963DAFCE1264756E2164756E21
1629
+ //# debugId=58FA12C7769E147864756E2164756E21