@openwop/openwop 1.1.0 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/dist/client.d.ts +80 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +186 -0
- package/dist/client.js.map +1 -1
- package/dist/cost-attribution.d.ts +49 -0
- package/dist/cost-attribution.d.ts.map +1 -0
- package/dist/cost-attribution.js +65 -0
- package/dist/cost-attribution.js.map +1 -0
- package/dist/event-helpers.d.ts +95 -0
- package/dist/event-helpers.d.ts.map +1 -0
- package/dist/event-helpers.js +160 -0
- package/dist/event-helpers.js.map +1 -0
- package/dist/index.d.ts +10 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -1
- package/dist/registry-helpers.d.ts +118 -0
- package/dist/registry-helpers.d.ts.map +1 -0
- package/dist/registry-helpers.js +82 -0
- package/dist/registry-helpers.js.map +1 -0
- package/dist/types.d.ts +376 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/webhook-helpers.d.ts +73 -0
- package/dist/webhook-helpers.d.ts.map +1 -0
- package/dist/webhook-helpers.js +97 -0
- package/dist/webhook-helpers.js.map +1 -0
- package/package.json +1 -1
- package/src/client.ts +218 -0
- package/src/cost-attribution.ts +72 -0
- package/src/event-helpers.ts +238 -0
- package/src/index.ts +96 -0
- package/src/registry-helpers.ts +173 -0
- package/src/types.ts +424 -0
- package/src/webhook-helpers.ts +131 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webhook delivery-verification helpers per `spec/v1/webhooks.md`
|
|
3
|
+
* §"Signature recipe". Receivers MUST verify both the HMAC AND the
|
|
4
|
+
* timestamp freshness before accepting a delivery — verifying HMAC
|
|
5
|
+
* alone leaves the receiver open to replay attacks.
|
|
6
|
+
*
|
|
7
|
+
* The canonical signing recipe:
|
|
8
|
+
*
|
|
9
|
+
* hmac = HMAC-SHA256(secret, `${timestamp}.${rawBody}`)
|
|
10
|
+
* header `openwop-Webhook-Signature: v1=<hmac-hex>`
|
|
11
|
+
* header `openwop-Webhook-Timestamp: <unix-seconds>`
|
|
12
|
+
*
|
|
13
|
+
* Verification:
|
|
14
|
+
*
|
|
15
|
+
* 1. Parse the `v1=<hex>` value from the signature header.
|
|
16
|
+
* 2. Recompute `expected = HMAC-SHA256(secret, `${timestamp}.${rawBody}`)`.
|
|
17
|
+
* 3. Compare using **constant-time** equality (timing-safe).
|
|
18
|
+
* 4. Reject when `|now - timestamp|` exceeds the freshness window
|
|
19
|
+
* (default 5 minutes per `webhooks.md`'s recommendation).
|
|
20
|
+
*
|
|
21
|
+
* Implementation note: this helper uses `node:crypto`'s `timingSafeEqual`
|
|
22
|
+
* for the comparison. The browser-side equivalent (Web Crypto's
|
|
23
|
+
* `subtle.verify`) is not wrapped here — the SDK's runtime is Node.
|
|
24
|
+
*
|
|
25
|
+
* @module @openwop/openwop/webhook-helpers
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
29
|
+
|
|
30
|
+
/** Default freshness window per `spec/v1/webhooks.md` §"Replay attack resistance". */
|
|
31
|
+
export const DEFAULT_WEBHOOK_FRESHNESS_WINDOW_SECONDS = 300;
|
|
32
|
+
|
|
33
|
+
export interface VerifyWebhookSignatureOptions {
|
|
34
|
+
/**
|
|
35
|
+
* Maximum age in seconds for the delivery timestamp before it's
|
|
36
|
+
* treated as a replay. Default 300 (5 minutes) per the spec.
|
|
37
|
+
* Set to 0 to disable timestamp checks (NOT recommended).
|
|
38
|
+
*/
|
|
39
|
+
freshnessWindowSeconds?: number;
|
|
40
|
+
/**
|
|
41
|
+
* Override the "now" timestamp in unix seconds. Useful for testing.
|
|
42
|
+
* Default `Math.floor(Date.now() / 1000)`.
|
|
43
|
+
*/
|
|
44
|
+
nowSeconds?: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type VerifyWebhookOutcome =
|
|
48
|
+
| { valid: true }
|
|
49
|
+
| { valid: false; reason: 'signature_mismatch' | 'timestamp_expired' | 'timestamp_too_far_in_future' | 'malformed_signature_header' | 'malformed_timestamp_header' };
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Verify a webhook delivery per `spec/v1/webhooks.md` §"Signature
|
|
53
|
+
* recipe". Returns `{ valid: true }` on success; otherwise
|
|
54
|
+
* `{ valid: false, reason }` so callers can log + alert appropriately.
|
|
55
|
+
*
|
|
56
|
+
* Callers MUST pass the **raw** body bytes — JSON-parsed-then-
|
|
57
|
+
* re-serialized bodies will fail verification because the host
|
|
58
|
+
* signs the exact bytes it delivered.
|
|
59
|
+
*
|
|
60
|
+
* @param secret The pre-shared secret returned from `webhooks.register`.
|
|
61
|
+
* @param signatureHeader The value of the `openwop-Webhook-Signature` header (e.g., `"v1=abc123…"`).
|
|
62
|
+
* @param timestampHeader The value of the `openwop-Webhook-Timestamp` header (unix seconds as string).
|
|
63
|
+
* @param rawBody The exact request body bytes the host POSTed.
|
|
64
|
+
*/
|
|
65
|
+
export function verifyWebhookSignature(
|
|
66
|
+
secret: string,
|
|
67
|
+
signatureHeader: string,
|
|
68
|
+
timestampHeader: string,
|
|
69
|
+
rawBody: string | Buffer,
|
|
70
|
+
options: VerifyWebhookSignatureOptions = {},
|
|
71
|
+
): VerifyWebhookOutcome {
|
|
72
|
+
// 1. Parse the signature header.
|
|
73
|
+
if (!signatureHeader.startsWith('v1=')) {
|
|
74
|
+
return { valid: false, reason: 'malformed_signature_header' };
|
|
75
|
+
}
|
|
76
|
+
const providedHex = signatureHeader.slice(3);
|
|
77
|
+
if (!/^[0-9a-f]+$/i.test(providedHex)) {
|
|
78
|
+
return { valid: false, reason: 'malformed_signature_header' };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 2. Parse the timestamp.
|
|
82
|
+
const timestamp = Number(timestampHeader);
|
|
83
|
+
if (!Number.isInteger(timestamp) || timestamp <= 0) {
|
|
84
|
+
return { valid: false, reason: 'malformed_timestamp_header' };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 3. Freshness check.
|
|
88
|
+
const window = options.freshnessWindowSeconds ?? DEFAULT_WEBHOOK_FRESHNESS_WINDOW_SECONDS;
|
|
89
|
+
if (window > 0) {
|
|
90
|
+
const now = options.nowSeconds ?? Math.floor(Date.now() / 1000);
|
|
91
|
+
const delta = now - timestamp;
|
|
92
|
+
if (delta > window) return { valid: false, reason: 'timestamp_expired' };
|
|
93
|
+
// Allow small future skew (within the window) but reject far-future timestamps.
|
|
94
|
+
if (delta < -window) return { valid: false, reason: 'timestamp_too_far_in_future' };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 4. Recompute + constant-time compare.
|
|
98
|
+
const bodyStr = typeof rawBody === 'string' ? rawBody : rawBody.toString('utf8');
|
|
99
|
+
const signedBytes = `${timestamp}.${bodyStr}`;
|
|
100
|
+
const expectedHex = createHmac('sha256', secret).update(signedBytes, 'utf8').digest('hex');
|
|
101
|
+
|
|
102
|
+
const providedBuf = Buffer.from(providedHex, 'hex');
|
|
103
|
+
const expectedBuf = Buffer.from(expectedHex, 'hex');
|
|
104
|
+
if (providedBuf.length !== expectedBuf.length) {
|
|
105
|
+
return { valid: false, reason: 'signature_mismatch' };
|
|
106
|
+
}
|
|
107
|
+
if (!timingSafeEqual(providedBuf, expectedBuf)) {
|
|
108
|
+
return { valid: false, reason: 'signature_mismatch' };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return { valid: true };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Compute the canonical webhook signature for a payload — useful when
|
|
116
|
+
* implementing a host (forward direction) OR when generating test
|
|
117
|
+
* fixtures. Receivers verify via `verifyWebhookSignature`; this is the
|
|
118
|
+
* inverse.
|
|
119
|
+
*/
|
|
120
|
+
export function signWebhookDelivery(
|
|
121
|
+
secret: string,
|
|
122
|
+
timestamp: number,
|
|
123
|
+
rawBody: string | Buffer,
|
|
124
|
+
): { signatureHeader: string; timestampHeader: string } {
|
|
125
|
+
const bodyStr = typeof rawBody === 'string' ? rawBody : rawBody.toString('utf8');
|
|
126
|
+
const hex = createHmac('sha256', secret).update(`${timestamp}.${bodyStr}`, 'utf8').digest('hex');
|
|
127
|
+
return {
|
|
128
|
+
signatureHeader: `v1=${hex}`,
|
|
129
|
+
timestampHeader: String(timestamp),
|
|
130
|
+
};
|
|
131
|
+
}
|