servcraft 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +29 -0
- package/.github/CODEOWNERS +18 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +46 -0
- package/.github/dependabot.yml +59 -0
- package/.github/workflows/ci.yml +188 -0
- package/.github/workflows/release.yml +195 -0
- package/AUDIT.md +602 -0
- package/README.md +1070 -1
- package/dist/cli/index.cjs +2026 -2168
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +2026 -2168
- package/dist/cli/index.js.map +1 -1
- package/dist/index.cjs +595 -616
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +114 -52
- package/dist/index.d.ts +114 -52
- package/dist/index.js +595 -616
- package/dist/index.js.map +1 -1
- package/docs/CLI-001_MULTI_DB_PLAN.md +546 -0
- package/docs/DATABASE_MULTI_ORM.md +399 -0
- package/docs/PHASE1_BREAKDOWN.md +346 -0
- package/docs/PROGRESS.md +550 -0
- package/docs/modules/ANALYTICS.md +226 -0
- package/docs/modules/API-VERSIONING.md +252 -0
- package/docs/modules/AUDIT.md +192 -0
- package/docs/modules/AUTH.md +431 -0
- package/docs/modules/CACHE.md +346 -0
- package/docs/modules/EMAIL.md +254 -0
- package/docs/modules/FEATURE-FLAG.md +291 -0
- package/docs/modules/I18N.md +294 -0
- package/docs/modules/MEDIA-PROCESSING.md +281 -0
- package/docs/modules/MFA.md +266 -0
- package/docs/modules/NOTIFICATION.md +311 -0
- package/docs/modules/OAUTH.md +237 -0
- package/docs/modules/PAYMENT.md +804 -0
- package/docs/modules/QUEUE.md +540 -0
- package/docs/modules/RATE-LIMIT.md +339 -0
- package/docs/modules/SEARCH.md +288 -0
- package/docs/modules/SECURITY.md +327 -0
- package/docs/modules/SESSION.md +382 -0
- package/docs/modules/SWAGGER.md +305 -0
- package/docs/modules/UPLOAD.md +296 -0
- package/docs/modules/USER.md +505 -0
- package/docs/modules/VALIDATION.md +294 -0
- package/docs/modules/WEBHOOK.md +270 -0
- package/docs/modules/WEBSOCKET.md +691 -0
- package/package.json +53 -38
- package/prisma/schema.prisma +395 -1
- package/src/cli/commands/add-module.ts +520 -87
- package/src/cli/commands/db.ts +3 -4
- package/src/cli/commands/docs.ts +256 -6
- package/src/cli/commands/generate.ts +12 -19
- package/src/cli/commands/init.ts +384 -214
- package/src/cli/index.ts +0 -4
- package/src/cli/templates/repository.ts +6 -1
- package/src/cli/templates/routes.ts +6 -21
- package/src/cli/utils/docs-generator.ts +6 -7
- package/src/cli/utils/env-manager.ts +717 -0
- package/src/cli/utils/field-parser.ts +16 -7
- package/src/cli/utils/interactive-prompt.ts +223 -0
- package/src/cli/utils/template-manager.ts +346 -0
- package/src/config/database.config.ts +183 -0
- package/src/config/env.ts +0 -10
- package/src/config/index.ts +0 -14
- package/src/core/server.ts +1 -1
- package/src/database/adapters/mongoose.adapter.ts +132 -0
- package/src/database/adapters/prisma.adapter.ts +118 -0
- package/src/database/connection.ts +190 -0
- package/src/database/interfaces/database.interface.ts +85 -0
- package/src/database/interfaces/index.ts +7 -0
- package/src/database/interfaces/repository.interface.ts +129 -0
- package/src/database/models/mongoose/index.ts +7 -0
- package/src/database/models/mongoose/payment.schema.ts +347 -0
- package/src/database/models/mongoose/user.schema.ts +154 -0
- package/src/database/prisma.ts +1 -4
- package/src/database/redis.ts +101 -0
- package/src/database/repositories/mongoose/index.ts +7 -0
- package/src/database/repositories/mongoose/payment.repository.ts +380 -0
- package/src/database/repositories/mongoose/user.repository.ts +255 -0
- package/src/database/seed.ts +6 -1
- package/src/index.ts +9 -20
- package/src/middleware/security.ts +2 -6
- package/src/modules/analytics/analytics.routes.ts +80 -0
- package/src/modules/analytics/analytics.service.ts +364 -0
- package/src/modules/analytics/index.ts +18 -0
- package/src/modules/analytics/types.ts +180 -0
- package/src/modules/api-versioning/index.ts +15 -0
- package/src/modules/api-versioning/types.ts +86 -0
- package/src/modules/api-versioning/versioning.middleware.ts +120 -0
- package/src/modules/api-versioning/versioning.routes.ts +54 -0
- package/src/modules/api-versioning/versioning.service.ts +189 -0
- package/src/modules/audit/audit.repository.ts +206 -0
- package/src/modules/audit/audit.service.ts +27 -59
- package/src/modules/auth/auth.controller.ts +2 -2
- package/src/modules/auth/auth.middleware.ts +3 -9
- package/src/modules/auth/auth.routes.ts +10 -107
- package/src/modules/auth/auth.service.ts +126 -23
- package/src/modules/auth/index.ts +3 -4
- package/src/modules/cache/cache.service.ts +367 -0
- package/src/modules/cache/index.ts +10 -0
- package/src/modules/cache/types.ts +44 -0
- package/src/modules/email/email.service.ts +3 -10
- package/src/modules/email/templates.ts +2 -8
- package/src/modules/feature-flag/feature-flag.repository.ts +303 -0
- package/src/modules/feature-flag/feature-flag.routes.ts +247 -0
- package/src/modules/feature-flag/feature-flag.service.ts +566 -0
- package/src/modules/feature-flag/index.ts +20 -0
- package/src/modules/feature-flag/types.ts +192 -0
- package/src/modules/i18n/i18n.middleware.ts +186 -0
- package/src/modules/i18n/i18n.routes.ts +191 -0
- package/src/modules/i18n/i18n.service.ts +456 -0
- package/src/modules/i18n/index.ts +18 -0
- package/src/modules/i18n/types.ts +118 -0
- package/src/modules/media-processing/index.ts +17 -0
- package/src/modules/media-processing/media-processing.routes.ts +111 -0
- package/src/modules/media-processing/media-processing.service.ts +245 -0
- package/src/modules/media-processing/types.ts +156 -0
- package/src/modules/mfa/index.ts +20 -0
- package/src/modules/mfa/mfa.repository.ts +206 -0
- package/src/modules/mfa/mfa.routes.ts +595 -0
- package/src/modules/mfa/mfa.service.ts +572 -0
- package/src/modules/mfa/totp.ts +150 -0
- package/src/modules/mfa/types.ts +57 -0
- package/src/modules/notification/index.ts +20 -0
- package/src/modules/notification/notification.repository.ts +356 -0
- package/src/modules/notification/notification.service.ts +483 -0
- package/src/modules/notification/types.ts +119 -0
- package/src/modules/oauth/index.ts +20 -0
- package/src/modules/oauth/oauth.repository.ts +219 -0
- package/src/modules/oauth/oauth.routes.ts +446 -0
- package/src/modules/oauth/oauth.service.ts +293 -0
- package/src/modules/oauth/providers/apple.provider.ts +250 -0
- package/src/modules/oauth/providers/facebook.provider.ts +181 -0
- package/src/modules/oauth/providers/github.provider.ts +248 -0
- package/src/modules/oauth/providers/google.provider.ts +189 -0
- package/src/modules/oauth/providers/twitter.provider.ts +214 -0
- package/src/modules/oauth/types.ts +94 -0
- package/src/modules/payment/index.ts +19 -0
- package/src/modules/payment/payment.repository.ts +733 -0
- package/src/modules/payment/payment.routes.ts +390 -0
- package/src/modules/payment/payment.service.ts +354 -0
- package/src/modules/payment/providers/mobile-money.provider.ts +274 -0
- package/src/modules/payment/providers/paypal.provider.ts +190 -0
- package/src/modules/payment/providers/stripe.provider.ts +215 -0
- package/src/modules/payment/types.ts +140 -0
- package/src/modules/queue/cron.ts +438 -0
- package/src/modules/queue/index.ts +87 -0
- package/src/modules/queue/queue.routes.ts +600 -0
- package/src/modules/queue/queue.service.ts +842 -0
- package/src/modules/queue/types.ts +222 -0
- package/src/modules/queue/workers.ts +366 -0
- package/src/modules/rate-limit/index.ts +59 -0
- package/src/modules/rate-limit/rate-limit.middleware.ts +134 -0
- package/src/modules/rate-limit/rate-limit.routes.ts +269 -0
- package/src/modules/rate-limit/rate-limit.service.ts +348 -0
- package/src/modules/rate-limit/stores/memory.store.ts +165 -0
- package/src/modules/rate-limit/stores/redis.store.ts +322 -0
- package/src/modules/rate-limit/types.ts +153 -0
- package/src/modules/search/adapters/elasticsearch.adapter.ts +326 -0
- package/src/modules/search/adapters/meilisearch.adapter.ts +261 -0
- package/src/modules/search/adapters/memory.adapter.ts +278 -0
- package/src/modules/search/index.ts +21 -0
- package/src/modules/search/search.service.ts +234 -0
- package/src/modules/search/types.ts +214 -0
- package/src/modules/security/index.ts +40 -0
- package/src/modules/security/sanitize.ts +223 -0
- package/src/modules/security/security-audit.service.ts +388 -0
- package/src/modules/security/security.middleware.ts +398 -0
- package/src/modules/session/index.ts +3 -0
- package/src/modules/session/session.repository.ts +159 -0
- package/src/modules/session/session.service.ts +340 -0
- package/src/modules/session/types.ts +38 -0
- package/src/modules/swagger/index.ts +7 -1
- package/src/modules/swagger/schema-builder.ts +16 -4
- package/src/modules/swagger/swagger.service.ts +9 -10
- package/src/modules/swagger/types.ts +0 -2
- package/src/modules/upload/index.ts +14 -0
- package/src/modules/upload/types.ts +83 -0
- package/src/modules/upload/upload.repository.ts +199 -0
- package/src/modules/upload/upload.routes.ts +311 -0
- package/src/modules/upload/upload.service.ts +448 -0
- package/src/modules/user/index.ts +3 -3
- package/src/modules/user/user.controller.ts +15 -9
- package/src/modules/user/user.repository.ts +237 -113
- package/src/modules/user/user.routes.ts +39 -164
- package/src/modules/user/user.service.ts +4 -3
- package/src/modules/validation/validator.ts +12 -17
- package/src/modules/webhook/index.ts +91 -0
- package/src/modules/webhook/retry.ts +196 -0
- package/src/modules/webhook/signature.ts +135 -0
- package/src/modules/webhook/types.ts +181 -0
- package/src/modules/webhook/webhook.repository.ts +358 -0
- package/src/modules/webhook/webhook.routes.ts +442 -0
- package/src/modules/webhook/webhook.service.ts +457 -0
- package/src/modules/websocket/features.ts +504 -0
- package/src/modules/websocket/index.ts +106 -0
- package/src/modules/websocket/middlewares.ts +298 -0
- package/src/modules/websocket/types.ts +181 -0
- package/src/modules/websocket/websocket.service.ts +692 -0
- package/src/utils/errors.ts +7 -0
- package/src/utils/pagination.ts +4 -1
- package/tests/helpers/db-check.ts +79 -0
- package/tests/integration/auth-redis.test.ts +94 -0
- package/tests/integration/cache-redis.test.ts +387 -0
- package/tests/integration/mongoose-repositories.test.ts +410 -0
- package/tests/integration/payment-prisma.test.ts +637 -0
- package/tests/integration/queue-bullmq.test.ts +417 -0
- package/tests/integration/user-prisma.test.ts +441 -0
- package/tests/integration/websocket-socketio.test.ts +552 -0
- package/tests/setup.ts +11 -9
- package/vitest.config.ts +3 -8
- package/npm-cache/_cacache/content-v2/sha512/1c/d0/03440d500a0487621aad1d6402978340698976602046db8e24fa03c01ee6c022c69b0582f969042d9442ee876ac35c038e960dd427d1e622fa24b8eb7dba +0 -0
- package/npm-cache/_cacache/content-v2/sha512/42/55/28b493ca491833e5aab0e9c3108d29ab3f36c248ca88f45d4630674fce9130959e56ae308797ac2b6328fa7f09a610b9550ed09cb971d039876d293fc69d +0 -0
- package/npm-cache/_cacache/content-v2/sha512/e0/12/f360dc9315ee5f17844a0c8c233ee6bf7c30837c4a02ea0d56c61c7f7ab21c0e958e50ed2c57c59f983c762b93056778c9009b2398ffc26def0183999b13 +0 -0
- package/npm-cache/_cacache/content-v2/sha512/ed/b0/fae1161902898f4c913c67d7f6cdf6be0665aec3b389b9c4f4f0a101ca1da59badf1b59c4e0030f5223023b8d63cfe501c46a32c20c895d4fb3f11ca2232 +0 -0
- package/npm-cache/_cacache/index-v5/58/94/c2cba79e0f16b4c10e95a87e32255741149e8222cc314a476aab67c39cc0 +0 -5
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import type {
|
|
2
|
+
import type { ZodSchema, ZodError } from 'zod';
|
|
3
3
|
import { ValidationError } from '../../utils/errors.js';
|
|
4
4
|
|
|
5
|
-
export function validateBody<T
|
|
5
|
+
export function validateBody<T>(schema: ZodSchema<T>, data: unknown): T {
|
|
6
6
|
const result = schema.safeParse(data);
|
|
7
7
|
|
|
8
8
|
if (!result.success) {
|
|
@@ -12,7 +12,7 @@ export function validateBody<T extends ZodTypeAny>(schema: T, data: unknown): z.
|
|
|
12
12
|
return result.data;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
export function validateQuery<T extends ZodTypeAny>(schema: T, data: unknown): z.infer<T> {
|
|
15
|
+
export function validateQuery<T extends z.ZodTypeAny>(schema: T, data: unknown): z.infer<T> {
|
|
16
16
|
const result = schema.safeParse(data);
|
|
17
17
|
|
|
18
18
|
if (!result.success) {
|
|
@@ -22,7 +22,7 @@ export function validateQuery<T extends ZodTypeAny>(schema: T, data: unknown): z
|
|
|
22
22
|
return result.data;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
export function validateParams<T
|
|
25
|
+
export function validateParams<T>(schema: ZodSchema<T>, data: unknown): T {
|
|
26
26
|
const result = schema.safeParse(data);
|
|
27
27
|
|
|
28
28
|
if (!result.success) {
|
|
@@ -32,7 +32,7 @@ export function validateParams<T extends ZodTypeAny>(schema: T, data: unknown):
|
|
|
32
32
|
return result.data;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
export function validate<T
|
|
35
|
+
export function validate<T>(schema: ZodSchema<T>, data: unknown): T {
|
|
36
36
|
return validateBody(schema, data);
|
|
37
37
|
}
|
|
38
38
|
|
|
@@ -83,21 +83,16 @@ export const passwordSchema = z
|
|
|
83
83
|
export const urlSchema = z.string().url('Invalid URL format');
|
|
84
84
|
|
|
85
85
|
// Phone validation (basic international format)
|
|
86
|
-
export const phoneSchema = z.string().regex(
|
|
87
|
-
/^\+?[1-9]\d{1,14}$/,
|
|
88
|
-
'Invalid phone number format'
|
|
89
|
-
);
|
|
86
|
+
export const phoneSchema = z.string().regex(/^\+?[1-9]\d{1,14}$/, 'Invalid phone number format');
|
|
90
87
|
|
|
91
88
|
// Date validation
|
|
92
89
|
export const dateSchema = z.coerce.date();
|
|
93
|
-
export const futureDateSchema = z.coerce
|
|
94
|
-
|
|
95
|
-
'Date must be in the future'
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
(date) => date < new Date(),
|
|
99
|
-
'Date must be in the past'
|
|
100
|
-
);
|
|
90
|
+
export const futureDateSchema = z.coerce
|
|
91
|
+
.date()
|
|
92
|
+
.refine((date) => date > new Date(), 'Date must be in the future');
|
|
93
|
+
export const pastDateSchema = z.coerce
|
|
94
|
+
.date()
|
|
95
|
+
.refine((date) => date < new Date(), 'Date must be in the past');
|
|
101
96
|
|
|
102
97
|
// Type exports
|
|
103
98
|
export type IdParam = z.infer<typeof idParamSchema>;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webhook Module
|
|
3
|
+
*
|
|
4
|
+
* Provides outgoing webhook functionality with:
|
|
5
|
+
* - HMAC signature verification
|
|
6
|
+
* - Automatic retry with exponential backoff
|
|
7
|
+
* - Delivery tracking and monitoring
|
|
8
|
+
* - Multiple retry strategies
|
|
9
|
+
* - Event publishing and subscription
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* import { WebhookService, createWebhookRoutes } from './modules/webhook';
|
|
14
|
+
*
|
|
15
|
+
* // Create service
|
|
16
|
+
* const webhookService = new WebhookService({
|
|
17
|
+
* maxRetries: 5,
|
|
18
|
+
* timeout: 10000,
|
|
19
|
+
* enableSignature: true
|
|
20
|
+
* });
|
|
21
|
+
*
|
|
22
|
+
* // Create endpoint
|
|
23
|
+
* const endpoint = await webhookService.createEndpoint({
|
|
24
|
+
* url: 'https://example.com/webhook',
|
|
25
|
+
* events: ['user.created', 'order.completed'],
|
|
26
|
+
* description: 'Production webhook'
|
|
27
|
+
* });
|
|
28
|
+
*
|
|
29
|
+
* // Publish event
|
|
30
|
+
* await webhookService.publishEvent('user.created', {
|
|
31
|
+
* userId: '123',
|
|
32
|
+
* email: 'user@example.com',
|
|
33
|
+
* name: 'John Doe'
|
|
34
|
+
* });
|
|
35
|
+
*
|
|
36
|
+
* // Add routes to your app
|
|
37
|
+
* app.use('/api/webhooks', authMiddleware, createWebhookRoutes(webhookService));
|
|
38
|
+
* ```
|
|
39
|
+
*
|
|
40
|
+
* ## Signature Verification
|
|
41
|
+
*
|
|
42
|
+
* Webhooks are signed with HMAC-SHA256. Recipients should verify:
|
|
43
|
+
*
|
|
44
|
+
* ```typescript
|
|
45
|
+
* import { verifyWebhookSignature } from './modules/webhook';
|
|
46
|
+
*
|
|
47
|
+
* app.post('/webhook', (req, res) => {
|
|
48
|
+
* const signature = req.headers['x-webhook-signature'];
|
|
49
|
+
* const secret = 'your-webhook-secret';
|
|
50
|
+
*
|
|
51
|
+
* if (!verifyWebhookSignature(req.body, signature, secret)) {
|
|
52
|
+
* return res.status(401).json({ error: 'Invalid signature' });
|
|
53
|
+
* }
|
|
54
|
+
*
|
|
55
|
+
* // Process webhook
|
|
56
|
+
* res.json({ received: true });
|
|
57
|
+
* });
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
|
|
61
|
+
// Types
|
|
62
|
+
export * from './types.js';
|
|
63
|
+
|
|
64
|
+
// Service
|
|
65
|
+
export { WebhookService } from './webhook.service.js';
|
|
66
|
+
|
|
67
|
+
// Routes
|
|
68
|
+
export { createWebhookRoutes } from './webhook.routes.js';
|
|
69
|
+
|
|
70
|
+
// Signature utilities
|
|
71
|
+
export {
|
|
72
|
+
generateSignature,
|
|
73
|
+
verifySignature,
|
|
74
|
+
verifyWebhookSignature,
|
|
75
|
+
parseSignatureHeader,
|
|
76
|
+
formatSignatureHeader,
|
|
77
|
+
generateWebhookSecret,
|
|
78
|
+
} from './signature.js';
|
|
79
|
+
|
|
80
|
+
// Retry strategies
|
|
81
|
+
export {
|
|
82
|
+
ExponentialBackoffStrategy,
|
|
83
|
+
LinearBackoffStrategy,
|
|
84
|
+
FixedDelayStrategy,
|
|
85
|
+
CustomDelayStrategy,
|
|
86
|
+
createDefaultRetryStrategy,
|
|
87
|
+
calculateNextRetryTime,
|
|
88
|
+
shouldRetryDelivery,
|
|
89
|
+
getExponentialBackoff,
|
|
90
|
+
parseRetryAfter,
|
|
91
|
+
} from './retry.js';
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import type { WebhookRetryStrategy } from './types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Exponential backoff retry strategy
|
|
5
|
+
*/
|
|
6
|
+
export class ExponentialBackoffStrategy implements WebhookRetryStrategy {
|
|
7
|
+
constructor(
|
|
8
|
+
private initialDelay: number = 1000, // 1 second
|
|
9
|
+
private maxDelay: number = 60000, // 1 minute
|
|
10
|
+
private multiplier: number = 2,
|
|
11
|
+
private maxRetries: number = 5
|
|
12
|
+
) {}
|
|
13
|
+
|
|
14
|
+
getNextRetryDelay(attempt: number): number {
|
|
15
|
+
if (attempt >= this.maxRetries) {
|
|
16
|
+
return -1; // No more retries
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Calculate exponential backoff: initialDelay * (multiplier ^ attempt)
|
|
20
|
+
const delay = this.initialDelay * Math.pow(this.multiplier, attempt);
|
|
21
|
+
|
|
22
|
+
// Add jitter (±25% randomization) to prevent thundering herd
|
|
23
|
+
const jitter = delay * 0.25 * (Math.random() * 2 - 1);
|
|
24
|
+
|
|
25
|
+
// Cap at maxDelay
|
|
26
|
+
return Math.min(delay + jitter, this.maxDelay);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
shouldRetry(attempt: number, error?: Error): boolean {
|
|
30
|
+
// Don't retry if max attempts reached
|
|
31
|
+
if (attempt >= this.maxRetries) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Don't retry client errors (4xx), except specific cases
|
|
36
|
+
if (error && 'statusCode' in error) {
|
|
37
|
+
const statusCode = (error as { statusCode: number }).statusCode;
|
|
38
|
+
|
|
39
|
+
// Retry on server errors (5xx) and specific client errors
|
|
40
|
+
if (statusCode >= 500) {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Retry on specific client errors
|
|
45
|
+
if (statusCode === 408 || statusCode === 429) {
|
|
46
|
+
return true; // Request Timeout or Too Many Requests
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Don't retry other client errors
|
|
50
|
+
if (statusCode >= 400 && statusCode < 500) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Linear backoff retry strategy
|
|
61
|
+
*/
|
|
62
|
+
export class LinearBackoffStrategy implements WebhookRetryStrategy {
|
|
63
|
+
constructor(
|
|
64
|
+
private delay: number = 5000, // 5 seconds
|
|
65
|
+
private maxRetries: number = 3
|
|
66
|
+
) {}
|
|
67
|
+
|
|
68
|
+
getNextRetryDelay(attempt: number): number {
|
|
69
|
+
if (attempt >= this.maxRetries) {
|
|
70
|
+
return -1;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Linear increase: delay * (attempt + 1)
|
|
74
|
+
return this.delay * (attempt + 1);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
shouldRetry(attempt: number): boolean {
|
|
78
|
+
return attempt < this.maxRetries;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Fixed delay retry strategy
|
|
84
|
+
*/
|
|
85
|
+
export class FixedDelayStrategy implements WebhookRetryStrategy {
|
|
86
|
+
constructor(
|
|
87
|
+
private delay: number = 10000, // 10 seconds
|
|
88
|
+
private maxRetries: number = 3
|
|
89
|
+
) {}
|
|
90
|
+
|
|
91
|
+
getNextRetryDelay(attempt: number): number {
|
|
92
|
+
if (attempt >= this.maxRetries) {
|
|
93
|
+
return -1;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return this.delay;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
shouldRetry(attempt: number): boolean {
|
|
100
|
+
return attempt < this.maxRetries;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Custom retry delays strategy
|
|
106
|
+
*/
|
|
107
|
+
export class CustomDelayStrategy implements WebhookRetryStrategy {
|
|
108
|
+
constructor(private delays: number[]) {}
|
|
109
|
+
|
|
110
|
+
getNextRetryDelay(attempt: number): number {
|
|
111
|
+
if (attempt >= this.delays.length) {
|
|
112
|
+
return -1;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return this.delays[attempt] ?? -1;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
shouldRetry(attempt: number): boolean {
|
|
119
|
+
return attempt < this.delays.length;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Retry utility functions
|
|
125
|
+
*/
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Calculate next retry time
|
|
129
|
+
*/
|
|
130
|
+
export function calculateNextRetryTime(
|
|
131
|
+
attempt: number,
|
|
132
|
+
strategy: WebhookRetryStrategy
|
|
133
|
+
): Date | null {
|
|
134
|
+
const delay = strategy.getNextRetryDelay(attempt);
|
|
135
|
+
|
|
136
|
+
if (delay < 0) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return new Date(Date.now() + delay);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Check if delivery should be retried
|
|
145
|
+
*/
|
|
146
|
+
export function shouldRetryDelivery(
|
|
147
|
+
attempts: number,
|
|
148
|
+
strategy: WebhookRetryStrategy,
|
|
149
|
+
error?: Error
|
|
150
|
+
): boolean {
|
|
151
|
+
return strategy.shouldRetry(attempts, error);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Get retry delay with exponential backoff
|
|
156
|
+
*/
|
|
157
|
+
export function getExponentialBackoff(
|
|
158
|
+
attempt: number,
|
|
159
|
+
initialDelay = 1000,
|
|
160
|
+
maxDelay = 60000,
|
|
161
|
+
multiplier = 2
|
|
162
|
+
): number {
|
|
163
|
+
const delay = initialDelay * Math.pow(multiplier, attempt);
|
|
164
|
+
const jitter = delay * 0.25 * (Math.random() * 2 - 1);
|
|
165
|
+
return Math.min(delay + jitter, maxDelay);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Create default retry strategy
|
|
170
|
+
*/
|
|
171
|
+
export function createDefaultRetryStrategy(): WebhookRetryStrategy {
|
|
172
|
+
return new ExponentialBackoffStrategy(1000, 60000, 2, 5);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Parse retry-after header
|
|
177
|
+
*/
|
|
178
|
+
export function parseRetryAfter(retryAfter: string | number): number {
|
|
179
|
+
if (typeof retryAfter === 'number') {
|
|
180
|
+
return retryAfter * 1000; // seconds to milliseconds
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Try parsing as number (seconds)
|
|
184
|
+
const seconds = parseInt(retryAfter, 10);
|
|
185
|
+
if (!isNaN(seconds)) {
|
|
186
|
+
return seconds * 1000;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Try parsing as HTTP date
|
|
190
|
+
const date = new Date(retryAfter);
|
|
191
|
+
if (!isNaN(date.getTime())) {
|
|
192
|
+
return Math.max(0, date.getTime() - Date.now());
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return 0;
|
|
196
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual } from 'crypto';
|
|
2
|
+
import type { WebhookSignature } from './types.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Generate HMAC signature for webhook payload
|
|
6
|
+
*/
|
|
7
|
+
export function generateSignature(
|
|
8
|
+
payload: string | Record<string, unknown>,
|
|
9
|
+
secret: string,
|
|
10
|
+
timestamp?: number
|
|
11
|
+
): WebhookSignature {
|
|
12
|
+
const ts = timestamp || Date.now();
|
|
13
|
+
const payloadString = typeof payload === 'string' ? payload : JSON.stringify(payload);
|
|
14
|
+
|
|
15
|
+
// Format: timestamp.payload
|
|
16
|
+
const signedPayload = `${ts}.${payloadString}`;
|
|
17
|
+
|
|
18
|
+
// Generate HMAC SHA256 signature
|
|
19
|
+
const signature = createHmac('sha256', secret).update(signedPayload).digest('hex');
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
signature,
|
|
23
|
+
timestamp: ts,
|
|
24
|
+
version: 'v1',
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Verify webhook signature
|
|
30
|
+
*/
|
|
31
|
+
export function verifySignature(
|
|
32
|
+
payload: string | Record<string, unknown>,
|
|
33
|
+
signature: string,
|
|
34
|
+
secret: string,
|
|
35
|
+
timestamp: number,
|
|
36
|
+
options: {
|
|
37
|
+
toleranceSeconds?: number;
|
|
38
|
+
} = {}
|
|
39
|
+
): boolean {
|
|
40
|
+
const { toleranceSeconds = 300 } = options; // 5 minutes default tolerance
|
|
41
|
+
|
|
42
|
+
// Check timestamp tolerance
|
|
43
|
+
const now = Date.now();
|
|
44
|
+
const timeDiff = Math.abs(now - timestamp);
|
|
45
|
+
|
|
46
|
+
if (timeDiff > toleranceSeconds * 1000) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Generate expected signature
|
|
51
|
+
const expected = generateSignature(payload, secret, timestamp);
|
|
52
|
+
|
|
53
|
+
// Timing-safe comparison
|
|
54
|
+
try {
|
|
55
|
+
const receivedBuffer = Buffer.from(signature, 'hex');
|
|
56
|
+
const expectedBuffer = Buffer.from(expected.signature, 'hex');
|
|
57
|
+
|
|
58
|
+
if (receivedBuffer.length !== expectedBuffer.length) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return timingSafeEqual(receivedBuffer, expectedBuffer);
|
|
63
|
+
} catch {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Parse signature header
|
|
70
|
+
* Format: "t=<timestamp>,v1=<signature>"
|
|
71
|
+
*/
|
|
72
|
+
export function parseSignatureHeader(header: string): {
|
|
73
|
+
timestamp: number;
|
|
74
|
+
signature: string;
|
|
75
|
+
} | null {
|
|
76
|
+
try {
|
|
77
|
+
const parts = header.split(',');
|
|
78
|
+
const result: { timestamp?: number; signature?: string } = {};
|
|
79
|
+
|
|
80
|
+
for (const part of parts) {
|
|
81
|
+
const [key, value] = part.split('=');
|
|
82
|
+
if (key === 't' && value) {
|
|
83
|
+
result.timestamp = parseInt(value, 10);
|
|
84
|
+
} else if (key === 'v1' && value) {
|
|
85
|
+
result.signature = value;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (result.timestamp && result.signature) {
|
|
90
|
+
return {
|
|
91
|
+
timestamp: result.timestamp,
|
|
92
|
+
signature: result.signature,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return null;
|
|
97
|
+
} catch {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Format signature for header
|
|
104
|
+
* Format: "t=<timestamp>,v1=<signature>"
|
|
105
|
+
*/
|
|
106
|
+
export function formatSignatureHeader(sig: WebhookSignature): string {
|
|
107
|
+
return `t=${sig.timestamp},v1=${sig.signature}`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Generate webhook secret
|
|
112
|
+
*/
|
|
113
|
+
export function generateWebhookSecret(): string {
|
|
114
|
+
return createHmac('sha256', Date.now().toString()).update(Math.random().toString()).digest('hex');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Verify webhook signature from headers
|
|
119
|
+
*/
|
|
120
|
+
export function verifyWebhookSignature(
|
|
121
|
+
payload: string | Record<string, unknown>,
|
|
122
|
+
signatureHeader: string,
|
|
123
|
+
secret: string,
|
|
124
|
+
options: {
|
|
125
|
+
toleranceSeconds?: number;
|
|
126
|
+
} = {}
|
|
127
|
+
): boolean {
|
|
128
|
+
const parsed = parseSignatureHeader(signatureHeader);
|
|
129
|
+
|
|
130
|
+
if (!parsed) {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return verifySignature(payload, parsed.signature, secret, parsed.timestamp, options);
|
|
135
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
export type WebhookEventType =
|
|
2
|
+
| 'user.created'
|
|
3
|
+
| 'user.updated'
|
|
4
|
+
| 'user.deleted'
|
|
5
|
+
| 'payment.succeeded'
|
|
6
|
+
| 'payment.failed'
|
|
7
|
+
| 'order.created'
|
|
8
|
+
| 'order.completed'
|
|
9
|
+
| 'order.cancelled'
|
|
10
|
+
| string; // Allow custom event types
|
|
11
|
+
|
|
12
|
+
export type WebhookStatus = 'pending' | 'delivered' | 'failed' | 'disabled';
|
|
13
|
+
|
|
14
|
+
export type WebhookDeliveryStatus = 'pending' | 'success' | 'failed' | 'retrying';
|
|
15
|
+
|
|
16
|
+
export interface WebhookEndpoint {
|
|
17
|
+
/** Unique identifier */
|
|
18
|
+
id: string;
|
|
19
|
+
/** Endpoint URL */
|
|
20
|
+
url: string;
|
|
21
|
+
/** Secret for HMAC signature */
|
|
22
|
+
secret: string;
|
|
23
|
+
/** Events to listen for */
|
|
24
|
+
events: WebhookEventType[];
|
|
25
|
+
/** Whether the endpoint is active */
|
|
26
|
+
enabled: boolean;
|
|
27
|
+
/** Optional description */
|
|
28
|
+
description?: string;
|
|
29
|
+
/** Custom headers to send */
|
|
30
|
+
headers?: Record<string, string>;
|
|
31
|
+
/** Metadata */
|
|
32
|
+
metadata?: Record<string, unknown>;
|
|
33
|
+
/** Creation date */
|
|
34
|
+
createdAt: Date;
|
|
35
|
+
/** Last update date */
|
|
36
|
+
updatedAt: Date;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface WebhookEvent {
|
|
40
|
+
/** Unique identifier */
|
|
41
|
+
id: string;
|
|
42
|
+
/** Event type */
|
|
43
|
+
type: WebhookEventType;
|
|
44
|
+
/** Event payload */
|
|
45
|
+
payload: Record<string, unknown>;
|
|
46
|
+
/** When the event occurred */
|
|
47
|
+
occurredAt: Date;
|
|
48
|
+
/** Endpoints that should receive this event */
|
|
49
|
+
endpoints?: string[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface WebhookDelivery {
|
|
53
|
+
/** Unique identifier */
|
|
54
|
+
id: string;
|
|
55
|
+
/** Webhook endpoint ID */
|
|
56
|
+
endpointId: string;
|
|
57
|
+
/** Event ID */
|
|
58
|
+
eventId: string;
|
|
59
|
+
/** Event type */
|
|
60
|
+
eventType: WebhookEventType;
|
|
61
|
+
/** Delivery status */
|
|
62
|
+
status: WebhookDeliveryStatus;
|
|
63
|
+
/** Number of attempts */
|
|
64
|
+
attempts: number;
|
|
65
|
+
/** Maximum attempts */
|
|
66
|
+
maxAttempts: number;
|
|
67
|
+
/** Next retry time */
|
|
68
|
+
nextRetryAt?: Date;
|
|
69
|
+
/** HTTP response status */
|
|
70
|
+
responseStatus?: number;
|
|
71
|
+
/** Response body */
|
|
72
|
+
responseBody?: string;
|
|
73
|
+
/** Error message if failed */
|
|
74
|
+
error?: string;
|
|
75
|
+
/** Request payload */
|
|
76
|
+
payload: Record<string, unknown>;
|
|
77
|
+
/** Creation date */
|
|
78
|
+
createdAt: Date;
|
|
79
|
+
/** Last attempt date */
|
|
80
|
+
lastAttemptAt?: Date;
|
|
81
|
+
/** Delivery completion date */
|
|
82
|
+
deliveredAt?: Date;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface WebhookConfig {
|
|
86
|
+
/** Default maximum retry attempts */
|
|
87
|
+
maxRetries?: number;
|
|
88
|
+
/** Initial retry delay in ms */
|
|
89
|
+
initialRetryDelay?: number;
|
|
90
|
+
/** Maximum retry delay in ms */
|
|
91
|
+
maxRetryDelay?: number;
|
|
92
|
+
/** Backoff multiplier */
|
|
93
|
+
backoffMultiplier?: number;
|
|
94
|
+
/** Request timeout in ms */
|
|
95
|
+
timeout?: number;
|
|
96
|
+
/** Enable signature verification */
|
|
97
|
+
enableSignature?: boolean;
|
|
98
|
+
/** Signature header name */
|
|
99
|
+
signatureHeader?: string;
|
|
100
|
+
/** Timestamp header name */
|
|
101
|
+
timestampHeader?: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface WebhookSignature {
|
|
105
|
+
/** Signature value */
|
|
106
|
+
signature: string;
|
|
107
|
+
/** Timestamp */
|
|
108
|
+
timestamp: number;
|
|
109
|
+
/** Version */
|
|
110
|
+
version: string;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface WebhookDeliveryAttempt {
|
|
114
|
+
/** Attempt number */
|
|
115
|
+
attempt: number;
|
|
116
|
+
/** HTTP status code */
|
|
117
|
+
statusCode?: number;
|
|
118
|
+
/** Response body */
|
|
119
|
+
responseBody?: string;
|
|
120
|
+
/** Error message */
|
|
121
|
+
error?: string;
|
|
122
|
+
/** Attempt timestamp */
|
|
123
|
+
timestamp: Date;
|
|
124
|
+
/** Duration in ms */
|
|
125
|
+
duration?: number;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface WebhookStats {
|
|
129
|
+
/** Total events sent */
|
|
130
|
+
totalEvents: number;
|
|
131
|
+
/** Successful deliveries */
|
|
132
|
+
successfulDeliveries: number;
|
|
133
|
+
/** Failed deliveries */
|
|
134
|
+
failedDeliveries: number;
|
|
135
|
+
/** Pending deliveries */
|
|
136
|
+
pendingDeliveries: number;
|
|
137
|
+
/** Average delivery time in ms */
|
|
138
|
+
averageDeliveryTime: number;
|
|
139
|
+
/** Success rate (0-100) */
|
|
140
|
+
successRate: number;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export interface WebhookFilter {
|
|
144
|
+
/** Filter by endpoint ID */
|
|
145
|
+
endpointId?: string;
|
|
146
|
+
/** Filter by event type */
|
|
147
|
+
eventType?: WebhookEventType;
|
|
148
|
+
/** Filter by status */
|
|
149
|
+
status?: WebhookDeliveryStatus;
|
|
150
|
+
/** Filter by date range */
|
|
151
|
+
startDate?: Date;
|
|
152
|
+
endDate?: Date;
|
|
153
|
+
/** Limit results */
|
|
154
|
+
limit?: number;
|
|
155
|
+
/** Offset for pagination */
|
|
156
|
+
offset?: number;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export interface CreateWebhookEndpointData {
|
|
160
|
+
url: string;
|
|
161
|
+
events: WebhookEventType[];
|
|
162
|
+
description?: string;
|
|
163
|
+
headers?: Record<string, string>;
|
|
164
|
+
metadata?: Record<string, unknown>;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export interface UpdateWebhookEndpointData {
|
|
168
|
+
url?: string;
|
|
169
|
+
events?: WebhookEventType[];
|
|
170
|
+
enabled?: boolean;
|
|
171
|
+
description?: string;
|
|
172
|
+
headers?: Record<string, string>;
|
|
173
|
+
metadata?: Record<string, unknown>;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export interface WebhookRetryStrategy {
|
|
177
|
+
/** Calculate next retry delay */
|
|
178
|
+
getNextRetryDelay(attempt: number): number;
|
|
179
|
+
/** Check if should retry */
|
|
180
|
+
shouldRetry(attempt: number, error?: Error): boolean;
|
|
181
|
+
}
|