@firela/billclaw-core 0.1.4 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/billclaw.d.ts +8 -0
- package/dist/billclaw.d.ts.map +1 -1
- package/dist/billclaw.js +51 -1
- package/dist/billclaw.js.map +1 -1
- package/dist/config/config-manager.d.ts +127 -0
- package/dist/config/config-manager.d.ts.map +1 -0
- package/dist/config/config-manager.js +304 -0
- package/dist/config/config-manager.js.map +1 -0
- package/dist/config/env-loader.d.ts +33 -0
- package/dist/config/env-loader.d.ts.map +1 -0
- package/dist/config/env-loader.js +115 -0
- package/dist/config/env-loader.js.map +1 -0
- package/dist/config/index.d.ts +14 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +14 -0
- package/dist/config/index.js.map +1 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/models/config.d.ts +147 -0
- package/dist/models/config.d.ts.map +1 -1
- package/dist/models/config.js +36 -0
- package/dist/models/config.js.map +1 -1
- package/dist/oauth/index.d.ts +12 -0
- package/dist/oauth/index.d.ts.map +1 -0
- package/dist/oauth/index.js +13 -0
- package/dist/oauth/index.js.map +1 -0
- package/dist/oauth/providers/gmail.d.ts +63 -0
- package/dist/oauth/providers/gmail.d.ts.map +1 -0
- package/dist/oauth/providers/gmail.js +213 -0
- package/dist/oauth/providers/gmail.js.map +1 -0
- package/dist/oauth/providers/plaid.d.ts +40 -0
- package/dist/oauth/providers/plaid.d.ts.map +1 -0
- package/dist/oauth/providers/plaid.js +90 -0
- package/dist/oauth/providers/plaid.js.map +1 -0
- package/dist/oauth/types.d.ts +102 -0
- package/dist/oauth/types.d.ts.map +1 -0
- package/dist/oauth/types.js +10 -0
- package/dist/oauth/types.js.map +1 -0
- package/dist/runtime/types.d.ts +2 -0
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/runtime/types.js.map +1 -1
- package/dist/storage/locking.d.ts +4 -0
- package/dist/storage/locking.d.ts.map +1 -1
- package/dist/storage/locking.js +4 -0
- package/dist/storage/locking.js.map +1 -1
- package/dist/test-fixtures.d.ts.map +1 -1
- package/dist/test-fixtures.js +5 -0
- package/dist/test-fixtures.js.map +1 -1
- package/dist/webhooks/deduplication.d.ts +117 -0
- package/dist/webhooks/deduplication.d.ts.map +1 -0
- package/dist/webhooks/deduplication.js +258 -0
- package/dist/webhooks/deduplication.js.map +1 -0
- package/dist/webhooks/handlers/gmail.d.ts +39 -0
- package/dist/webhooks/handlers/gmail.d.ts.map +1 -0
- package/dist/webhooks/handlers/gmail.js +56 -0
- package/dist/webhooks/handlers/gmail.js.map +1 -0
- package/dist/webhooks/handlers/gocardless.d.ts +39 -0
- package/dist/webhooks/handlers/gocardless.d.ts.map +1 -0
- package/dist/webhooks/handlers/gocardless.js +73 -0
- package/dist/webhooks/handlers/gocardless.js.map +1 -0
- package/dist/webhooks/handlers/index.d.ts +10 -0
- package/dist/webhooks/handlers/index.d.ts.map +1 -0
- package/dist/webhooks/handlers/index.js +10 -0
- package/dist/webhooks/handlers/index.js.map +1 -0
- package/dist/webhooks/handlers/plaid.d.ts +73 -0
- package/dist/webhooks/handlers/plaid.d.ts.map +1 -0
- package/dist/webhooks/handlers/plaid.js +169 -0
- package/dist/webhooks/handlers/plaid.js.map +1 -0
- package/dist/webhooks/index.d.ts +15 -0
- package/dist/webhooks/index.d.ts.map +1 -0
- package/dist/webhooks/index.js +17 -0
- package/dist/webhooks/index.js.map +1 -0
- package/dist/webhooks/processor.d.ts +76 -0
- package/dist/webhooks/processor.d.ts.map +1 -0
- package/dist/webhooks/processor.js +116 -0
- package/dist/webhooks/processor.js.map +1 -0
- package/dist/webhooks/router.d.ts +80 -0
- package/dist/webhooks/router.d.ts.map +1 -0
- package/dist/webhooks/router.js +107 -0
- package/dist/webhooks/router.js.map +1 -0
- package/dist/webhooks/security.d.ts +90 -0
- package/dist/webhooks/security.d.ts.map +1 -0
- package/dist/webhooks/security.js +138 -0
- package/dist/webhooks/security.js.map +1 -0
- package/dist/webhooks/sync-rate-limiter.d.ts +138 -0
- package/dist/webhooks/sync-rate-limiter.d.ts.map +1 -0
- package/dist/webhooks/sync-rate-limiter.js +228 -0
- package/dist/webhooks/sync-rate-limiter.js.map +1 -0
- package/dist/webhooks/types.d.ts +140 -0
- package/dist/webhooks/types.d.ts.map +1 -0
- package/dist/webhooks/types.js +18 -0
- package/dist/webhooks/types.js.map +1 -0
- package/package.json +12 -12
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webhook router registry
|
|
3
|
+
*
|
|
4
|
+
* Registry pattern for managing webhook handlers.
|
|
5
|
+
* Provides handler registration and lookup functionality.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Webhook router
|
|
9
|
+
*
|
|
10
|
+
* Registry for webhook handlers with routing functionality.
|
|
11
|
+
*/
|
|
12
|
+
export class WebhookRouter {
|
|
13
|
+
handlers = new Map();
|
|
14
|
+
logger;
|
|
15
|
+
constructor(options) {
|
|
16
|
+
this.logger = options.logger;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Register a webhook handler for a specific source
|
|
20
|
+
*
|
|
21
|
+
* @param source - Webhook source identifier
|
|
22
|
+
* @param handler - Handler implementation
|
|
23
|
+
*/
|
|
24
|
+
register(source, handler) {
|
|
25
|
+
if (this.handlers.has(source)) {
|
|
26
|
+
this.logger.warn?.(`Handler already registered for ${source}, replacing`);
|
|
27
|
+
}
|
|
28
|
+
this.handlers.set(source, handler);
|
|
29
|
+
this.logger.info?.(`Registered webhook handler for ${source}`);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Unregister a handler for a specific source
|
|
33
|
+
*
|
|
34
|
+
* @param source - Webhook source identifier
|
|
35
|
+
*/
|
|
36
|
+
unregister(source) {
|
|
37
|
+
if (this.handlers.delete(source)) {
|
|
38
|
+
this.logger.info?.(`Unregistered webhook handler for ${source}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Get handler by source
|
|
43
|
+
*
|
|
44
|
+
* @param source - Webhook source identifier
|
|
45
|
+
* @returns Handler or undefined if not found
|
|
46
|
+
*/
|
|
47
|
+
getHandler(source) {
|
|
48
|
+
return this.handlers.get(source);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Check if handler is registered for source
|
|
52
|
+
*
|
|
53
|
+
* @param source - Webhook source identifier
|
|
54
|
+
* @returns True if handler exists
|
|
55
|
+
*/
|
|
56
|
+
hasHandler(source) {
|
|
57
|
+
return this.handlers.has(source);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Get all registered sources
|
|
61
|
+
*
|
|
62
|
+
* @returns Array of registered source identifiers
|
|
63
|
+
*/
|
|
64
|
+
getSources() {
|
|
65
|
+
return Array.from(this.handlers.keys());
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Route request to appropriate handler
|
|
69
|
+
*
|
|
70
|
+
* @param request - Webhook request
|
|
71
|
+
* @returns Response from handler or error if not found
|
|
72
|
+
*/
|
|
73
|
+
async route(request) {
|
|
74
|
+
const handler = this.handlers.get(request.source);
|
|
75
|
+
if (!handler) {
|
|
76
|
+
this.logger.warn?.(`No handler registered for source: ${request.source}`);
|
|
77
|
+
return {
|
|
78
|
+
status: 400,
|
|
79
|
+
body: {
|
|
80
|
+
received: false,
|
|
81
|
+
error: `No handler registered for source: ${request.source}`,
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return handler.handle(request);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Clear all registered handlers
|
|
89
|
+
*/
|
|
90
|
+
clear() {
|
|
91
|
+
this.handlers.clear();
|
|
92
|
+
this.logger.info?.("Cleared all webhook handlers");
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Get number of registered handlers
|
|
96
|
+
*/
|
|
97
|
+
get size() {
|
|
98
|
+
return this.handlers.size;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Create a webhook router with default configuration
|
|
103
|
+
*/
|
|
104
|
+
export function createWebhookRouter(logger) {
|
|
105
|
+
return new WebhookRouter({ logger });
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=router.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"router.js","sourceRoot":"","sources":["../../src/webhooks/router.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAoBH;;;;GAIG;AACH,MAAM,OAAO,aAAa;IACP,QAAQ,GAAG,IAAI,GAAG,EAAiC,CAAA;IACnD,MAAM,CAAQ;IAE/B,YAAY,OAA6B;QACvC,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,CAAA;IAC9B,CAAC;IAED;;;;;OAKG;IACH,QAAQ,CAAC,MAAqB,EAAE,OAAuB;QACrD,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;YAC9B,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,kCAAkC,MAAM,aAAa,CAAC,CAAA;QAC3E,CAAC;QAED,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;QAClC,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,kCAAkC,MAAM,EAAE,CAAC,CAAA;IAChE,CAAC;IAED;;;;OAIG;IACH,UAAU,CAAC,MAAqB;QAC9B,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC;YACjC,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,oCAAoC,MAAM,EAAE,CAAC,CAAA;QAClE,CAAC;IACH,CAAC;IAED;;;;;OAKG;IACH,UAAU,CAAC,MAAqB;QAC9B,OAAO,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;IAClC,CAAC;IAED;;;;;OAKG;IACH,UAAU,CAAC,MAAqB;QAC9B,OAAO,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;IAClC,CAAC;IAED;;;;OAIG;IACH,UAAU;QACR,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAA;IACzC,CAAC;IAED;;;;;OAKG;IACH,KAAK,CAAC,KAAK,CAAC,OAAuB;QACjC,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAA;QAEjD,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,qCAAqC,OAAO,CAAC,MAAM,EAAE,CAAC,CAAA;YACzE,OAAO;gBACL,MAAM,EAAE,GAAG;gBACX,IAAI,EAAE;oBACJ,QAAQ,EAAE,KAAK;oBACf,KAAK,EAAE,qCAAqC,OAAO,CAAC,MAAM,EAAE;iBAC7D;aACF,CAAA;QACH,CAAC;QAED,OAAO,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;IAChC,CAAC;IAED;;OAEG;IACH,KAAK;QACH,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAA;QACrB,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,8BAA8B,CAAC,CAAA;IACpD,CAAC;IAED;;OAEG;IACH,IAAI,IAAI;QACN,OAAO,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAA;IAC3B,CAAC;CACF;AAED;;GAEG;AACH,MAAM,UAAU,mBAAmB,CAAC,MAAc;IAChD,OAAO,IAAI,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC,CAAA;AACtC,CAAC"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webhook security layer (P0)
|
|
3
|
+
*
|
|
4
|
+
* Provides replay attack protection, signature verification, and
|
|
5
|
+
* rate limiting coordination for inbound webhooks.
|
|
6
|
+
*
|
|
7
|
+
* Security features:
|
|
8
|
+
* - Replay attack protection using timestamp + nonce validation
|
|
9
|
+
* - HMAC-SHA256 signature verification with timing-safe comparison
|
|
10
|
+
* - Rate limiting coordination
|
|
11
|
+
*/
|
|
12
|
+
import type { Logger } from "../errors/errors.js";
|
|
13
|
+
import type { WebhookDeduplication } from "./deduplication.js";
|
|
14
|
+
/**
|
|
15
|
+
* Configuration for webhook security
|
|
16
|
+
*/
|
|
17
|
+
export interface WebhookSecurityConfig {
|
|
18
|
+
/**
|
|
19
|
+
* Maximum allowed timestamp age in milliseconds
|
|
20
|
+
* Default: 15 minutes
|
|
21
|
+
*/
|
|
22
|
+
maxTimestampAge?: number;
|
|
23
|
+
/**
|
|
24
|
+
* Future timestamp tolerance in milliseconds
|
|
25
|
+
* Default: 5 minutes (to handle clock skew)
|
|
26
|
+
*/
|
|
27
|
+
futureTimestampTolerance?: number;
|
|
28
|
+
/**
|
|
29
|
+
* Deduplication cache for nonce tracking
|
|
30
|
+
*/
|
|
31
|
+
deduplication: WebhookDeduplication;
|
|
32
|
+
/**
|
|
33
|
+
* Logger instance
|
|
34
|
+
*/
|
|
35
|
+
logger: Logger;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Webhook security layer
|
|
39
|
+
*
|
|
40
|
+
* Provides P0 security features for webhook processing.
|
|
41
|
+
*/
|
|
42
|
+
export declare class WebhookSecurity {
|
|
43
|
+
private readonly maxTimestampAge;
|
|
44
|
+
private readonly futureTolerance;
|
|
45
|
+
private readonly deduplication;
|
|
46
|
+
private readonly logger;
|
|
47
|
+
constructor(config: WebhookSecurityConfig);
|
|
48
|
+
/**
|
|
49
|
+
* Validate replay protection (timestamp + nonce)
|
|
50
|
+
*
|
|
51
|
+
* Checks that:
|
|
52
|
+
* 1. Timestamp is within valid range (not too old, not too far in future)
|
|
53
|
+
* 2. Nonce has not been used before
|
|
54
|
+
*
|
|
55
|
+
* @param timestamp - Webhook timestamp (Unix milliseconds)
|
|
56
|
+
* @param nonce - Unique webhook identifier
|
|
57
|
+
* @returns True if valid, false if replay detected
|
|
58
|
+
*/
|
|
59
|
+
validateReplayProtection(timestamp: number | undefined, nonce: string | undefined): Promise<boolean>;
|
|
60
|
+
/**
|
|
61
|
+
* Verify webhook signature
|
|
62
|
+
*
|
|
63
|
+
* Uses timing-safe comparison to prevent timing attacks.
|
|
64
|
+
*
|
|
65
|
+
* @param payload - Request payload (stringified JSON)
|
|
66
|
+
* @param signature - Signature to verify (format: "sha256=<hex>")
|
|
67
|
+
* @param secret - Webhook secret key
|
|
68
|
+
* @returns True if signature is valid
|
|
69
|
+
*/
|
|
70
|
+
verifySignature(payload: string, signature: string | undefined, secret: string): boolean;
|
|
71
|
+
/**
|
|
72
|
+
* Generate HMAC-SHA256 signature for testing
|
|
73
|
+
*
|
|
74
|
+
* @param payload - Payload to sign
|
|
75
|
+
* @param secret - Secret key for HMAC
|
|
76
|
+
* @returns Signature in format "sha256=<hex>"
|
|
77
|
+
*/
|
|
78
|
+
generateSignature(payload: string, secret: string): string;
|
|
79
|
+
/**
|
|
80
|
+
* Create a nonce for testing
|
|
81
|
+
*
|
|
82
|
+
* @returns Unique nonce string
|
|
83
|
+
*/
|
|
84
|
+
generateNonce(): string;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Create a webhook security instance with default configuration
|
|
88
|
+
*/
|
|
89
|
+
export declare function createWebhookSecurity(deduplication: WebhookDeduplication, logger: Logger): WebhookSecurity;
|
|
90
|
+
//# sourceMappingURL=security.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"security.d.ts","sourceRoot":"","sources":["../../src/webhooks/security.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAA;AACjD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAA;AAE9D;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAA;IAExB;;;OAGG;IACH,wBAAwB,CAAC,EAAE,MAAM,CAAA;IAEjC;;OAEG;IACH,aAAa,EAAE,oBAAoB,CAAA;IAEnC;;OAEG;IACH,MAAM,EAAE,MAAM,CAAA;CACf;AAQD;;;;GAIG;AACH,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAQ;IACxC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAQ;IACxC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAsB;IACpD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAQ;gBAEnB,MAAM,EAAE,qBAAqB;IAOzC;;;;;;;;;;OAUG;IACG,wBAAwB,CAC5B,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,KAAK,EAAE,MAAM,GAAG,SAAS,GACxB,OAAO,CAAC,OAAO,CAAC;IA4CnB;;;;;;;;;OASG;IACH,eAAe,CACb,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,MAAM,EAAE,MAAM,GACb,OAAO;IA2BV;;;;;;OAMG;IACH,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM;IAM1D;;;;OAIG;IACH,aAAa,IAAI,MAAM;CAGxB;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CACnC,aAAa,EAAE,oBAAoB,EACnC,MAAM,EAAE,MAAM,GACb,eAAe,CAKjB"}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webhook security layer (P0)
|
|
3
|
+
*
|
|
4
|
+
* Provides replay attack protection, signature verification, and
|
|
5
|
+
* rate limiting coordination for inbound webhooks.
|
|
6
|
+
*
|
|
7
|
+
* Security features:
|
|
8
|
+
* - Replay attack protection using timestamp + nonce validation
|
|
9
|
+
* - HMAC-SHA256 signature verification with timing-safe comparison
|
|
10
|
+
* - Rate limiting coordination
|
|
11
|
+
*/
|
|
12
|
+
import * as crypto from "node:crypto";
|
|
13
|
+
/**
|
|
14
|
+
* Default configuration values
|
|
15
|
+
*/
|
|
16
|
+
const DEFAULT_MAX_TIMESTAMP_AGE = 15 * 60 * 1000; // 15 minutes
|
|
17
|
+
const DEFAULT_FUTURE_TOLERANCE = 5 * 60 * 1000; // 5 minutes
|
|
18
|
+
/**
|
|
19
|
+
* Webhook security layer
|
|
20
|
+
*
|
|
21
|
+
* Provides P0 security features for webhook processing.
|
|
22
|
+
*/
|
|
23
|
+
export class WebhookSecurity {
|
|
24
|
+
maxTimestampAge;
|
|
25
|
+
futureTolerance;
|
|
26
|
+
deduplication;
|
|
27
|
+
logger;
|
|
28
|
+
constructor(config) {
|
|
29
|
+
this.maxTimestampAge = config.maxTimestampAge ?? DEFAULT_MAX_TIMESTAMP_AGE;
|
|
30
|
+
this.futureTolerance = config.futureTimestampTolerance ?? DEFAULT_FUTURE_TOLERANCE;
|
|
31
|
+
this.deduplication = config.deduplication;
|
|
32
|
+
this.logger = config.logger;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Validate replay protection (timestamp + nonce)
|
|
36
|
+
*
|
|
37
|
+
* Checks that:
|
|
38
|
+
* 1. Timestamp is within valid range (not too old, not too far in future)
|
|
39
|
+
* 2. Nonce has not been used before
|
|
40
|
+
*
|
|
41
|
+
* @param timestamp - Webhook timestamp (Unix milliseconds)
|
|
42
|
+
* @param nonce - Unique webhook identifier
|
|
43
|
+
* @returns True if valid, false if replay detected
|
|
44
|
+
*/
|
|
45
|
+
async validateReplayProtection(timestamp, nonce) {
|
|
46
|
+
const now = Date.now();
|
|
47
|
+
// Check if timestamp is provided
|
|
48
|
+
if (timestamp === undefined) {
|
|
49
|
+
this.logger.debug?.("Webhook missing timestamp");
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
// Check if nonce is provided
|
|
53
|
+
if (nonce === undefined) {
|
|
54
|
+
this.logger.debug?.("Webhook missing nonce");
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
// Check timestamp is not too old
|
|
58
|
+
if (now - timestamp > this.maxTimestampAge) {
|
|
59
|
+
this.logger.warn?.(`Webhook rejected: timestamp too old (${now - timestamp}ms ago)`);
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
// Check timestamp is not too far in future (allow clock skew)
|
|
63
|
+
if (timestamp - now > this.futureTolerance) {
|
|
64
|
+
this.logger.warn?.(`Webhook rejected: timestamp too far in future (${timestamp - now}ms)`);
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
// Check nonce has not been used before
|
|
68
|
+
const isProcessed = await this.deduplication.isProcessed(nonce);
|
|
69
|
+
if (isProcessed) {
|
|
70
|
+
this.logger.warn?.(`Webhook rejected: nonce already used (${nonce})`);
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
// Mark nonce as processed
|
|
74
|
+
await this.deduplication.markProcessed(nonce, 60_000); // 60 second TTL
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Verify webhook signature
|
|
79
|
+
*
|
|
80
|
+
* Uses timing-safe comparison to prevent timing attacks.
|
|
81
|
+
*
|
|
82
|
+
* @param payload - Request payload (stringified JSON)
|
|
83
|
+
* @param signature - Signature to verify (format: "sha256=<hex>")
|
|
84
|
+
* @param secret - Webhook secret key
|
|
85
|
+
* @returns True if signature is valid
|
|
86
|
+
*/
|
|
87
|
+
verifySignature(payload, signature, secret) {
|
|
88
|
+
if (!signature) {
|
|
89
|
+
this.logger.debug?.("Webhook missing signature");
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
// Compute expected signature
|
|
94
|
+
const expectedSignature = crypto
|
|
95
|
+
.createHmac("sha256", secret)
|
|
96
|
+
.update(payload)
|
|
97
|
+
.digest("hex");
|
|
98
|
+
// Extract provided signature (remove "sha256=" prefix if present)
|
|
99
|
+
const providedSignature = signature.replace(/^sha256=/i, "");
|
|
100
|
+
// Use timing-safe comparison to prevent timing attacks
|
|
101
|
+
return crypto.timingSafeEqual(Buffer.from(expectedSignature), Buffer.from(providedSignature));
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
this.logger.error?.("Signature verification failed:", error);
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Generate HMAC-SHA256 signature for testing
|
|
110
|
+
*
|
|
111
|
+
* @param payload - Payload to sign
|
|
112
|
+
* @param secret - Secret key for HMAC
|
|
113
|
+
* @returns Signature in format "sha256=<hex>"
|
|
114
|
+
*/
|
|
115
|
+
generateSignature(payload, secret) {
|
|
116
|
+
const hmac = crypto.createHmac("sha256", secret);
|
|
117
|
+
hmac.update(payload);
|
|
118
|
+
return `sha256=${hmac.digest("hex")}`;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Create a nonce for testing
|
|
122
|
+
*
|
|
123
|
+
* @returns Unique nonce string
|
|
124
|
+
*/
|
|
125
|
+
generateNonce() {
|
|
126
|
+
return `nonce_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Create a webhook security instance with default configuration
|
|
131
|
+
*/
|
|
132
|
+
export function createWebhookSecurity(deduplication, logger) {
|
|
133
|
+
return new WebhookSecurity({
|
|
134
|
+
deduplication,
|
|
135
|
+
logger,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
//# sourceMappingURL=security.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"security.js","sourceRoot":"","sources":["../../src/webhooks/security.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync rate limiter (P0)
|
|
3
|
+
*
|
|
4
|
+
* Prevents Plaid API bans from webhook-triggered sync floods.
|
|
5
|
+
* Implements separate rate limit buckets for manual vs webhook-triggered syncs.
|
|
6
|
+
*
|
|
7
|
+
* Design:
|
|
8
|
+
* - Separate rate limit buckets: manual vs webhook-triggered
|
|
9
|
+
* - Circuit breaker to disable webhook syncs when rate limit near
|
|
10
|
+
* - Sliding window for accurate rate limiting
|
|
11
|
+
*/
|
|
12
|
+
import type { Logger } from "../errors/errors.js";
|
|
13
|
+
/**
|
|
14
|
+
* Rate limit configuration
|
|
15
|
+
*/
|
|
16
|
+
export interface RateLimitConfig {
|
|
17
|
+
/**
|
|
18
|
+
* Maximum number of requests allowed
|
|
19
|
+
*/
|
|
20
|
+
requests: number;
|
|
21
|
+
/**
|
|
22
|
+
* Time window in milliseconds
|
|
23
|
+
*/
|
|
24
|
+
window: number;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Sync rate limiter configuration
|
|
28
|
+
*/
|
|
29
|
+
export interface SyncRateLimiterConfig {
|
|
30
|
+
/**
|
|
31
|
+
* Rate limit for manual syncs
|
|
32
|
+
*/
|
|
33
|
+
manual: RateLimitConfig;
|
|
34
|
+
/**
|
|
35
|
+
* Rate limit for webhook-triggered syncs
|
|
36
|
+
*/
|
|
37
|
+
webhook: RateLimitConfig;
|
|
38
|
+
/**
|
|
39
|
+
* Circuit breaker threshold (0-1)
|
|
40
|
+
* Disable webhook syncs when usage exceeds this ratio
|
|
41
|
+
*/
|
|
42
|
+
circuitThreshold?: number;
|
|
43
|
+
/**
|
|
44
|
+
* Logger instance
|
|
45
|
+
*/
|
|
46
|
+
logger: Logger;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Sync rate limiter
|
|
50
|
+
*
|
|
51
|
+
* Tracks sync requests and enforces rate limits separately for
|
|
52
|
+
* manual and webhook-triggered syncs.
|
|
53
|
+
*/
|
|
54
|
+
export declare class SyncRateLimiter {
|
|
55
|
+
private readonly config;
|
|
56
|
+
private readonly logger;
|
|
57
|
+
private readonly requests;
|
|
58
|
+
private circuitOpen;
|
|
59
|
+
private circuitOpenUntil;
|
|
60
|
+
constructor(config: SyncRateLimiterConfig);
|
|
61
|
+
/**
|
|
62
|
+
* Record a manual sync request
|
|
63
|
+
*
|
|
64
|
+
* @param accountId - Account ID for the sync
|
|
65
|
+
*/
|
|
66
|
+
recordManualSync(accountId: string): void;
|
|
67
|
+
/**
|
|
68
|
+
* Record a webhook-triggered sync request
|
|
69
|
+
*
|
|
70
|
+
* @param accountId - Account ID for the sync
|
|
71
|
+
*/
|
|
72
|
+
recordWebhookSync(accountId: string): void;
|
|
73
|
+
/**
|
|
74
|
+
* Check if webhook sync is allowed
|
|
75
|
+
*
|
|
76
|
+
* @param accountId - Account ID for the sync
|
|
77
|
+
* @returns True if sync is allowed
|
|
78
|
+
*/
|
|
79
|
+
isWebhookSyncAllowed(accountId: string): boolean;
|
|
80
|
+
/**
|
|
81
|
+
* Check if manual sync is allowed
|
|
82
|
+
*
|
|
83
|
+
* @param accountId - Account ID for the sync
|
|
84
|
+
* @returns True if sync is allowed
|
|
85
|
+
*/
|
|
86
|
+
isManualSyncAllowed(accountId: string): boolean;
|
|
87
|
+
/**
|
|
88
|
+
* Check if circuit breaker is open
|
|
89
|
+
*/
|
|
90
|
+
isCircuitOpen(): boolean;
|
|
91
|
+
/**
|
|
92
|
+
* Open circuit breaker
|
|
93
|
+
*
|
|
94
|
+
* Disables webhook syncs for a cooldown period.
|
|
95
|
+
*/
|
|
96
|
+
openCircuit(): void;
|
|
97
|
+
/**
|
|
98
|
+
* Close circuit breaker
|
|
99
|
+
*/
|
|
100
|
+
closeCircuit(): void;
|
|
101
|
+
/**
|
|
102
|
+
* Get rate limiter statistics
|
|
103
|
+
*/
|
|
104
|
+
getStats(): {
|
|
105
|
+
manualCount: number;
|
|
106
|
+
webhookCount: number;
|
|
107
|
+
circuitOpen: boolean;
|
|
108
|
+
usageRatio: number;
|
|
109
|
+
};
|
|
110
|
+
/**
|
|
111
|
+
* Reset rate limiter (for testing)
|
|
112
|
+
*/
|
|
113
|
+
reset(): void;
|
|
114
|
+
/**
|
|
115
|
+
* Clean up old requests
|
|
116
|
+
*/
|
|
117
|
+
cleanup(): void;
|
|
118
|
+
/**
|
|
119
|
+
* Record a request
|
|
120
|
+
*/
|
|
121
|
+
private recordRequest;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Create a sync rate limiter with default configuration
|
|
125
|
+
*/
|
|
126
|
+
export declare function createSyncRateLimiter(logger: Logger, config?: Partial<SyncRateLimiterConfig>): SyncRateLimiter;
|
|
127
|
+
/**
|
|
128
|
+
* In-memory rate limiter for testing
|
|
129
|
+
*/
|
|
130
|
+
export declare class InMemoryRateLimiter {
|
|
131
|
+
private readonly config;
|
|
132
|
+
private readonly counters;
|
|
133
|
+
constructor(config: RateLimitConfig, _logger: Logger);
|
|
134
|
+
recordRequest(identifier: string): Promise<void>;
|
|
135
|
+
isRateLimited(identifier: string): Promise<boolean>;
|
|
136
|
+
reset(): void;
|
|
137
|
+
}
|
|
138
|
+
//# sourceMappingURL=sync-rate-limiter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync-rate-limiter.d.ts","sourceRoot":"","sources":["../../src/webhooks/sync-rate-limiter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAA;AAEjD;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B;;OAEG;IACH,QAAQ,EAAE,MAAM,CAAA;IAEhB;;OAEG;IACH,MAAM,EAAE,MAAM,CAAA;CACf;AAED;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC;;OAEG;IACH,MAAM,EAAE,eAAe,CAAA;IAEvB;;OAEG;IACH,OAAO,EAAE,eAAe,CAAA;IAExB;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAA;IAEzB;;OAEG;IACH,MAAM,EAAE,MAAM,CAAA;CACf;AAyBD;;;;;GAKG;AACH,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAiD;IACxE,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAQ;IAC/B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAqB;IAC9C,OAAO,CAAC,WAAW,CAAQ;IAC3B,OAAO,CAAC,gBAAgB,CAAI;gBAEhB,MAAM,EAAE,qBAAqB;IAKzC;;;;OAIG;IACH,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAIzC;;;;OAIG;IACH,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAI1C;;;;;OAKG;IACH,oBAAoB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO;IAyChD;;;;;OAKG;IACH,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO;IAmB/C;;OAEG;IACH,aAAa,IAAI,OAAO;IAcxB;;;;OAIG;IACH,WAAW,IAAI,IAAI;IAKnB;;OAEG;IACH,YAAY,IAAI,IAAI;IAMpB;;OAEG;IACH,QAAQ,IAAI;QACV,WAAW,EAAE,MAAM,CAAA;QACnB,YAAY,EAAE,MAAM,CAAA;QACpB,WAAW,EAAE,OAAO,CAAA;QACpB,UAAU,EAAE,MAAM,CAAA;KACnB;IAsBD;;OAEG;IACH,KAAK,IAAI,IAAI;IAKb;;OAEG;IACH,OAAO,IAAI,IAAI;IAmBf;;OAEG;IACH,OAAO,CAAC,aAAa;CAWtB;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,MAAM,EACd,MAAM,CAAC,EAAE,OAAO,CAAC,qBAAqB,CAAC,GACtC,eAAe,CAQjB;AAED;;GAEG;AACH,qBAAa,mBAAmB;IAI5B,OAAO,CAAC,QAAQ,CAAC,MAAM;IAHzB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA4B;gBAGlC,MAAM,EAAE,eAAe,EACxC,OAAO,EAAE,MAAM;IAGX,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAKhD,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAKzD,KAAK,IAAI,IAAI;CAGd"}
|