@whooshbang/contracts 1.0.0-rc.13

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 (229) hide show
  1. package/CHANGELOG.md +198 -0
  2. package/LICENSE +21 -0
  3. package/README.md +120 -0
  4. package/contract-manifest.json +390 -0
  5. package/dist/base64url.d.ts +2 -0
  6. package/dist/base64url.js +51 -0
  7. package/dist/base64url.js.map +1 -0
  8. package/dist/generated/artifact-index.d.ts +205 -0
  9. package/dist/generated/artifact-index.js +396 -0
  10. package/dist/generated/artifact-index.js.map +1 -0
  11. package/dist/generated/contract-manifest.d.ts +66 -0
  12. package/dist/generated/contract-manifest.js +6 -0
  13. package/dist/generated/contract-manifest.js.map +1 -0
  14. package/dist/generated/core-scenario.d.ts +18 -0
  15. package/dist/generated/core-scenario.js +6 -0
  16. package/dist/generated/core-scenario.js.map +1 -0
  17. package/dist/generated/fixture-index.d.ts +33 -0
  18. package/dist/generated/fixture-index.js +6 -0
  19. package/dist/generated/fixture-index.js.map +1 -0
  20. package/dist/generated/interaction-machine-scenario.d.ts +263 -0
  21. package/dist/generated/interaction-machine-scenario.js +6 -0
  22. package/dist/generated/interaction-machine-scenario.js.map +1 -0
  23. package/dist/generated/interaction-v1.d.ts +152 -0
  24. package/dist/generated/interaction-v1.js +6 -0
  25. package/dist/generated/interaction-v1.js.map +1 -0
  26. package/dist/generated/machine-v1.d.ts +169 -0
  27. package/dist/generated/machine-v1.js +6 -0
  28. package/dist/generated/machine-v1.js.map +1 -0
  29. package/dist/generated/provider-capabilities-v1.d.ts +78 -0
  30. package/dist/generated/provider-capabilities-v1.js +6 -0
  31. package/dist/generated/provider-capabilities-v1.js.map +1 -0
  32. package/dist/generated/validators-standalone.d.ts +659 -0
  33. package/dist/generated/validators-standalone.js +47800 -0
  34. package/dist/generated/validators-standalone.js.map +1 -0
  35. package/dist/generated/validators.d.ts +74 -0
  36. package/dist/generated/validators.js +493 -0
  37. package/dist/generated/validators.js.map +1 -0
  38. package/dist/generated/webhook-delivery-case-v1.d.ts +91 -0
  39. package/dist/generated/webhook-delivery-case-v1.js +6 -0
  40. package/dist/generated/webhook-delivery-case-v1.js.map +1 -0
  41. package/dist/generated/webhook-signing-vector-v1.d.ts +88 -0
  42. package/dist/generated/webhook-signing-vector-v1.js +6 -0
  43. package/dist/generated/webhook-signing-vector-v1.js.map +1 -0
  44. package/dist/generated/whooshbang-event-v1.d.ts +191 -0
  45. package/dist/generated/whooshbang-event-v1.js +6 -0
  46. package/dist/generated/whooshbang-event-v1.js.map +1 -0
  47. package/dist/generated/whooshbang-v1.d.ts +1162 -0
  48. package/dist/generated/whooshbang-v1.js +6 -0
  49. package/dist/generated/whooshbang-v1.js.map +1 -0
  50. package/dist/index.d.ts +20 -0
  51. package/dist/index.js +7 -0
  52. package/dist/index.js.map +1 -0
  53. package/dist/machine-credential.d.ts +31 -0
  54. package/dist/machine-credential.js +78 -0
  55. package/dist/machine-credential.js.map +1 -0
  56. package/dist/webhook-consumer.d.ts +50 -0
  57. package/dist/webhook-consumer.js +71 -0
  58. package/dist/webhook-consumer.js.map +1 -0
  59. package/dist/webhook-delivery.d.ts +46 -0
  60. package/dist/webhook-delivery.js +126 -0
  61. package/dist/webhook-delivery.js.map +1 -0
  62. package/dist/webhook-signature.d.ts +36 -0
  63. package/dist/webhook-signature.js +255 -0
  64. package/dist/webhook-signature.js.map +1 -0
  65. package/examples/python/verify_webhook.py +283 -0
  66. package/fixtures/.gitkeep +1 -0
  67. package/fixtures/core-v1/fixture-index.json +963 -0
  68. package/fixtures/core-v1/problems/auth-invalid.json +9 -0
  69. package/fixtures/core-v1/problems/authentication-required.json +9 -0
  70. package/fixtures/core-v1/problems/cancellation-too-late.json +9 -0
  71. package/fixtures/core-v1/problems/endpoint-rotation-conflict.json +10 -0
  72. package/fixtures/core-v1/problems/environment-mismatch.json +9 -0
  73. package/fixtures/core-v1/problems/idempotency-conflict.json +9 -0
  74. package/fixtures/core-v1/problems/idempotency-key-required.json +9 -0
  75. package/fixtures/core-v1/problems/project-slug-conflict.json +16 -0
  76. package/fixtures/core-v1/problems/provider-outcome-unknown.json +9 -0
  77. package/fixtures/core-v1/problems/provider-retryable.json +10 -0
  78. package/fixtures/core-v1/problems/provider-terminal.json +9 -0
  79. package/fixtures/core-v1/problems/rate-limited.json +10 -0
  80. package/fixtures/core-v1/problems/replay-unavailable-not-terminal.json +16 -0
  81. package/fixtures/core-v1/problems/replay-unavailable-payload.json +16 -0
  82. package/fixtures/core-v1/problems/replay-unavailable-rollout.json +16 -0
  83. package/fixtures/core-v1/problems/request-invalid.json +16 -0
  84. package/fixtures/core-v1/problems/resource-expired.json +9 -0
  85. package/fixtures/core-v1/problems/resource-not-found.json +9 -0
  86. package/fixtures/core-v1/problems/scope-forbidden.json +9 -0
  87. package/fixtures/core-v1/problems/subscriber-unbound.json +9 -0
  88. package/fixtures/core-v1/requests/create-api-credential.json +9 -0
  89. package/fixtures/core-v1/requests/create-customer-endpoint.json +5 -0
  90. package/fixtures/core-v1/requests/create-message.json +15 -0
  91. package/fixtures/core-v1/requests/create-project.json +4 -0
  92. package/fixtures/core-v1/requests/create-subscription-link.json +5 -0
  93. package/fixtures/core-v1/requests/rotate-api-credential.json +3 -0
  94. package/fixtures/core-v1/requests/rotate-customer-endpoint.json +3 -0
  95. package/fixtures/core-v1/scenarios/api-credential-invalid-indistinguishable.json +14 -0
  96. package/fixtures/core-v1/scenarios/api-credential-one-time-reveal.json +21 -0
  97. package/fixtures/core-v1/scenarios/api-credential-revocation-replay.json +17 -0
  98. package/fixtures/core-v1/scenarios/api-credential-rotation-overlap.json +18 -0
  99. package/fixtures/core-v1/scenarios/cancellation-too-late.json +15 -0
  100. package/fixtures/core-v1/scenarios/cross-account-project.json +14 -0
  101. package/fixtures/core-v1/scenarios/cross-project-resource.json +17 -0
  102. package/fixtures/core-v1/scenarios/environment-mismatch.json +17 -0
  103. package/fixtures/core-v1/scenarios/idempotency-conflict.json +35 -0
  104. package/fixtures/core-v1/scenarios/idempotency-equivalent-replay.json +27 -0
  105. package/fixtures/core-v1/scenarios/malformed-bearer.json +14 -0
  106. package/fixtures/core-v1/scenarios/metadata-overrun.json +14 -0
  107. package/fixtures/core-v1/scenarios/missing-scope.json +15 -0
  108. package/fixtures/core-v1/scenarios/project-create-equivalent-replay.json +26 -0
  109. package/fixtures/core-v1/scenarios/project-slug-conflict.json +21 -0
  110. package/fixtures/core-v1/scenarios/provider-outcome-unknown.json +15 -0
  111. package/fixtures/core-v1/scenarios/subscription-activation-replay.json +18 -0
  112. package/fixtures/core-v1/scenarios/subscription-expired-replay.json +16 -0
  113. package/fixtures/core-v1/success/account-projection.json +11 -0
  114. package/fixtures/core-v1/success/api-credential-collection.json +22 -0
  115. package/fixtures/core-v1/success/api-credential-created.json +20 -0
  116. package/fixtures/core-v1/success/api-credential-revoked.json +19 -0
  117. package/fixtures/core-v1/success/api-credential-rotated.json +41 -0
  118. package/fixtures/core-v1/success/customer-endpoint-attempts.json +15 -0
  119. package/fixtures/core-v1/success/customer-endpoint-collection.json +26 -0
  120. package/fixtures/core-v1/success/customer-endpoint-create-replayed.json +16 -0
  121. package/fixtures/core-v1/success/customer-endpoint-created.json +17 -0
  122. package/fixtures/core-v1/success/customer-endpoint-rotated.json +21 -0
  123. package/fixtures/core-v1/success/message-accepted.json +30 -0
  124. package/fixtures/core-v1/success/message-cancellation-too-late.json +27 -0
  125. package/fixtures/core-v1/success/message-cancelled.json +27 -0
  126. package/fixtures/core-v1/success/message-outcome-unknown.json +33 -0
  127. package/fixtures/core-v1/success/message-provider-accepted.json +30 -0
  128. package/fixtures/core-v1/success/project-collection.json +42 -0
  129. package/fixtures/core-v1/success/project-created.json +38 -0
  130. package/fixtures/core-v1/success/subscription-link-activated.json +22 -0
  131. package/fixtures/core-v1/success/subscription-link-cancelled.json +14 -0
  132. package/fixtures/core-v1/success/subscription-link-paused.json +22 -0
  133. package/fixtures/core-v1/success/subscription-link-pending.json +14 -0
  134. package/fixtures/core-v1/success/subscription-link-revoked.json +22 -0
  135. package/fixtures/interaction-machine-v1/capabilities/providers.json +57 -0
  136. package/fixtures/interaction-machine-v1/capabilities/telegram.json +52 -0
  137. package/fixtures/interaction-machine-v1/events/confirm-answered.json +22 -0
  138. package/fixtures/interaction-machine-v1/events/input-answered.json +22 -0
  139. package/fixtures/interaction-machine-v1/events/input-redacted.json +18 -0
  140. package/fixtures/interaction-machine-v1/events/select-answered.json +22 -0
  141. package/fixtures/interaction-machine-v1/requests/ack-processed.json +5 -0
  142. package/fixtures/interaction-machine-v1/requests/ack-quarantined.json +5 -0
  143. package/fixtures/interaction-machine-v1/requests/create-machine-client.json +6 -0
  144. package/fixtures/interaction-machine-v1/requests/create-message-confirm.json +18 -0
  145. package/fixtures/interaction-machine-v1/requests/create-message-input.json +16 -0
  146. package/fixtures/interaction-machine-v1/requests/create-message-select.json +26 -0
  147. package/fixtures/interaction-machine-v1/requests/register-machine-credential.json +4 -0
  148. package/fixtures/interaction-machine-v1/scenarios/ack-conflict.json +14 -0
  149. package/fixtures/interaction-machine-v1/scenarios/ack-cross-machine.json +15 -0
  150. package/fixtures/interaction-machine-v1/scenarios/ack-out-of-order.json +15 -0
  151. package/fixtures/interaction-machine-v1/scenarios/ack-replay.json +17 -0
  152. package/fixtures/interaction-machine-v1/scenarios/ambiguous-free-text.json +15 -0
  153. package/fixtures/interaction-machine-v1/scenarios/callback-token-opacity.json +16 -0
  154. package/fixtures/interaction-machine-v1/scenarios/credential-registration-conflict.json +16 -0
  155. package/fixtures/interaction-machine-v1/scenarios/credential-registration-replay.json +15 -0
  156. package/fixtures/interaction-machine-v1/scenarios/credential-revocation-replay.json +14 -0
  157. package/fixtures/interaction-machine-v1/scenarios/credential-rotation.json +16 -0
  158. package/fixtures/interaction-machine-v1/scenarios/first-credential-activates.json +15 -0
  159. package/fixtures/interaction-machine-v1/scenarios/interaction-duplicate.json +16 -0
  160. package/fixtures/interaction-machine-v1/scenarios/interaction-expired.json +17 -0
  161. package/fixtures/interaction-machine-v1/scenarios/interaction-mismatch.json +16 -0
  162. package/fixtures/interaction-machine-v1/scenarios/interaction-retired.json +16 -0
  163. package/fixtures/interaction-machine-v1/scenarios/interaction-stale.json +16 -0
  164. package/fixtures/interaction-machine-v1/scenarios/machine-admin-denied.json +14 -0
  165. package/fixtures/interaction-machine-v1/scenarios/machine-bound-message-create.json +22 -0
  166. package/fixtures/interaction-machine-v1/scenarios/machine-cancel-denied.json +14 -0
  167. package/fixtures/interaction-machine-v1/scenarios/machine-crash-before-ack.json +16 -0
  168. package/fixtures/interaction-machine-v1/scenarios/machine-notifier-mismatch.json +14 -0
  169. package/fixtures/interaction-machine-v1/scenarios/machine-other-message-read.json +14 -0
  170. package/fixtures/interaction-machine-v1/scenarios/machine-owned-message-read.json +22 -0
  171. package/fixtures/interaction-machine-v1/scenarios/machine-subscriber-mismatch.json +14 -0
  172. package/fixtures/interaction-machine-v1/scenarios/poll-uncommitted-cursor-rejected.json +17 -0
  173. package/fixtures/interaction-machine-v1/scenarios/project-credential-machine-endpoint-denied.json +14 -0
  174. package/fixtures/interaction-machine-v1/scenarios/quarantine-integrity-rejected.json +15 -0
  175. package/fixtures/interaction-machine-v1/scenarios/quarantine-signature-rejected.json +16 -0
  176. package/fixtures/interaction-machine-v1/scenarios/quarantine-valid.json +15 -0
  177. package/fixtures/interaction-machine-v1/scenarios/revoked-machine-credential.json +15 -0
  178. package/fixtures/interaction-machine-v1/success/ack-out-of-order.json +8 -0
  179. package/fixtures/interaction-machine-v1/success/ack-processed.json +8 -0
  180. package/fixtures/interaction-machine-v1/success/ack-quarantined.json +9 -0
  181. package/fixtures/interaction-machine-v1/success/interaction-answered-confirm.json +10 -0
  182. package/fixtures/interaction-machine-v1/success/interaction-answered-input.json +10 -0
  183. package/fixtures/interaction-machine-v1/success/interaction-answered-redacted.json +8 -0
  184. package/fixtures/interaction-machine-v1/success/interaction-answered-select.json +10 -0
  185. package/fixtures/interaction-machine-v1/success/interaction-open.json +7 -0
  186. package/fixtures/interaction-machine-v1/success/machine-client-active.json +20 -0
  187. package/fixtures/interaction-machine-v1/success/machine-client-provisioning.json +19 -0
  188. package/fixtures/interaction-machine-v1/success/machine-client-revoked.json +21 -0
  189. package/fixtures/interaction-machine-v1/success/machine-credential-active.json +13 -0
  190. package/fixtures/interaction-machine-v1/success/machine-credential-revoked.json +14 -0
  191. package/fixtures/interaction-machine-v1/success/message-interaction-open.json +33 -0
  192. package/fixtures/interaction-machine-v1/success/poll-empty.json +6 -0
  193. package/fixtures/interaction-machine-v1/success/poll-events.json +51 -0
  194. package/fixtures/interaction-machine-v1/tokens/callback-token.json +1 -0
  195. package/fixtures/whooshbang-events-v1/delivery/cases.json +418 -0
  196. package/fixtures/whooshbang-events-v1/events/agent-binding-degraded.json +17 -0
  197. package/fixtures/whooshbang-events-v1/events/interaction-expired.json +16 -0
  198. package/fixtures/whooshbang-events-v1/events/interaction-received.json +23 -0
  199. package/fixtures/whooshbang-events-v1/events/message-delivered.json +16 -0
  200. package/fixtures/whooshbang-events-v1/events/message-expired.json +15 -0
  201. package/fixtures/whooshbang-events-v1/events/message-failed.json +17 -0
  202. package/fixtures/whooshbang-events-v1/events/message-provider-accepted.json +15 -0
  203. package/fixtures/whooshbang-events-v1/events/subscription-activated.json +18 -0
  204. package/fixtures/whooshbang-events-v1/events/subscription-revoked.json +18 -0
  205. package/fixtures/whooshbang-events-v1/signing/vectors.json +852 -0
  206. package/openapi/.gitkeep +1 -0
  207. package/openapi/generated/whooshbang-v1.bundle.json +5913 -0
  208. package/openapi/whooshbang-v1.yaml +2334 -0
  209. package/package.json +63 -0
  210. package/schemas/contract-manifest.schema.json +140 -0
  211. package/schemas/core-scenario.schema.json +38 -0
  212. package/schemas/events/agent-binding-degraded-v1.schema.json +8 -0
  213. package/schemas/events/interaction-expired-v1.schema.json +8 -0
  214. package/schemas/events/interaction-received-v1.schema.json +8 -0
  215. package/schemas/events/message-delivered-v1.schema.json +8 -0
  216. package/schemas/events/message-expired-v1.schema.json +8 -0
  217. package/schemas/events/message-failed-v1.schema.json +8 -0
  218. package/schemas/events/message-provider-accepted-v1.schema.json +8 -0
  219. package/schemas/events/subscription-activated-v1.schema.json +8 -0
  220. package/schemas/events/subscription-revoked-v1.schema.json +8 -0
  221. package/schemas/events/whooshbang-event-v1.schema.json +689 -0
  222. package/schemas/fixture-index.schema.json +67 -0
  223. package/schemas/interaction-machine-scenario.schema.json +700 -0
  224. package/schemas/interaction-v1.schema.json +483 -0
  225. package/schemas/machine-v1.schema.json +430 -0
  226. package/schemas/provider-capabilities-v1.schema.json +263 -0
  227. package/schemas/webhooks/webhook-delivery-case-v1.schema.json +167 -0
  228. package/schemas/webhooks/webhook-signing-vector-v1.schema.json +197 -0
  229. package/schemas/whooshbang-v1.schema.json +2173 -0
@@ -0,0 +1,255 @@
1
+ export const DEFAULT_WEBHOOK_TIMESTAMP_TOLERANCE_SECONDS = 300;
2
+ export const MAX_WEBHOOK_SIGNATURES = 2;
3
+ export const MAX_WEBHOOK_SECRETS = 2;
4
+ const runtimeGlobal = globalThis;
5
+ const eventIdentifier = /^evt_[A-Za-z0-9_-]+$/u;
6
+ const canonicalTimestamp = /^(?:0|[1-9][0-9]{0,14})$/u;
7
+ const canonicalAttempt = /^[1-8]$/u;
8
+ const signatureToken = /^([A-Za-z0-9]+),([A-Za-z0-9+/]+={0,2})$/u;
9
+ const canonicalBase64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u;
10
+ function reject(reason) {
11
+ return { reason, verified: false };
12
+ }
13
+ function collectHeaderValues(headers, wantedName) {
14
+ const values = [];
15
+ const runtimeHeaders = headers;
16
+ if (Array.isArray(runtimeHeaders)) {
17
+ for (const entry of runtimeHeaders) {
18
+ if (!Array.isArray(entry) ||
19
+ entry.length !== 2 ||
20
+ typeof entry[0] !== "string" ||
21
+ typeof entry[1] !== "string") {
22
+ return { status: "malformed" };
23
+ }
24
+ if (entry[0].toLowerCase() === wantedName) {
25
+ values.push(entry[1]);
26
+ }
27
+ }
28
+ }
29
+ else if (typeof runtimeHeaders === "object" && runtimeHeaders !== null) {
30
+ for (const [name, rawValue] of Object.entries(runtimeHeaders)) {
31
+ if (name.toLowerCase() !== wantedName || rawValue === undefined) {
32
+ continue;
33
+ }
34
+ if (typeof rawValue === "string") {
35
+ values.push(rawValue);
36
+ }
37
+ else if (Array.isArray(rawValue) &&
38
+ rawValue.every((value) => typeof value === "string")) {
39
+ values.push(...rawValue);
40
+ }
41
+ else {
42
+ return { status: "malformed" };
43
+ }
44
+ }
45
+ }
46
+ else {
47
+ return { status: "malformed" };
48
+ }
49
+ if (values.length === 0) {
50
+ return { status: "missing" };
51
+ }
52
+ if (values.length !== 1) {
53
+ return { status: "duplicate" };
54
+ }
55
+ const value = values[0];
56
+ return value === undefined
57
+ ? { status: "malformed" }
58
+ : { status: "ok", value };
59
+ }
60
+ function headerFailure(result) {
61
+ if (result.status === "missing") {
62
+ return reject("missing_header");
63
+ }
64
+ if (result.status === "duplicate") {
65
+ return reject("duplicate_header");
66
+ }
67
+ return reject("malformed_header");
68
+ }
69
+ function decodeCanonicalBase64(value) {
70
+ if (!canonicalBase64.test(value)) {
71
+ return undefined;
72
+ }
73
+ try {
74
+ const decoded = atob(value);
75
+ if (btoa(decoded) !== value) {
76
+ return undefined;
77
+ }
78
+ return Uint8Array.from(decoded, (character) => character.charCodeAt(0));
79
+ }
80
+ catch {
81
+ return undefined;
82
+ }
83
+ }
84
+ function parseSecret(value) {
85
+ if (!value.startsWith("whsec_")) {
86
+ return undefined;
87
+ }
88
+ const decoded = decodeCanonicalBase64(value.slice("whsec_".length));
89
+ return decoded !== undefined &&
90
+ decoded.byteLength >= 24 &&
91
+ decoded.byteLength <= 64
92
+ ? decoded
93
+ : undefined;
94
+ }
95
+ function parseSignatures(value) {
96
+ if (value.length === 0 ||
97
+ value.startsWith(" ") ||
98
+ value.endsWith(" ") ||
99
+ value.includes(" ")) {
100
+ return "malformed_signature";
101
+ }
102
+ const tokens = value.split(" ");
103
+ if (tokens.length > MAX_WEBHOOK_SIGNATURES) {
104
+ return "too_many_signatures";
105
+ }
106
+ const signatures = [];
107
+ const seen = new Set();
108
+ for (const token of tokens) {
109
+ const match = signatureToken.exec(token);
110
+ const version = match?.[1];
111
+ const encoded = match?.[2];
112
+ if (version === undefined || encoded === undefined) {
113
+ return "malformed_signature";
114
+ }
115
+ if (version !== "v1") {
116
+ return "unsupported_signature_version";
117
+ }
118
+ if (seen.has(token)) {
119
+ return "duplicate_signature";
120
+ }
121
+ seen.add(token);
122
+ const bytes = decodeCanonicalBase64(encoded);
123
+ if (bytes === undefined || bytes.byteLength !== 32) {
124
+ return "malformed_signature";
125
+ }
126
+ signatures.push({ bytes, encoded });
127
+ }
128
+ return signatures;
129
+ }
130
+ function signingBytes(eventId, timestamp, rawBody) {
131
+ const prefix = new TextEncoder().encode(`${eventId}.${timestamp}.`);
132
+ const result = new Uint8Array(prefix.byteLength + rawBody.byteLength);
133
+ result.set(prefix);
134
+ result.set(rawBody, prefix.byteLength);
135
+ return result;
136
+ }
137
+ function constantTimeEqual(left, right) {
138
+ let mismatch = left.byteLength ^ right.byteLength;
139
+ const length = Math.max(left.byteLength, right.byteLength);
140
+ for (let index = 0; index < length; index += 1) {
141
+ mismatch |=
142
+ (left[index % left.byteLength] ?? 0) ^
143
+ (right[index % right.byteLength] ?? 0);
144
+ }
145
+ return mismatch === 0;
146
+ }
147
+ /**
148
+ * Verifies the Standard Webhooks V1 signature over the exact request bytes.
149
+ *
150
+ * Header entries are accepted as tuples so callers can preserve and reject
151
+ * duplicate security headers before framework normalization.
152
+ */
153
+ export async function verifyWebhookSignature(options) {
154
+ if (!(options.rawBody instanceof Uint8Array)) {
155
+ return reject("invalid_body");
156
+ }
157
+ const runtimeCrypto = options.crypto ?? runtimeGlobal.crypto;
158
+ if (runtimeCrypto === undefined) {
159
+ return reject("crypto_unavailable");
160
+ }
161
+ const idHeader = collectHeaderValues(options.headers, "webhook-id");
162
+ if (idHeader.status !== "ok") {
163
+ return headerFailure(idHeader);
164
+ }
165
+ if (idHeader.value.length < 8 ||
166
+ idHeader.value.length > 128 ||
167
+ !eventIdentifier.test(idHeader.value)) {
168
+ return reject("malformed_webhook_id");
169
+ }
170
+ const timestampHeader = collectHeaderValues(options.headers, "webhook-timestamp");
171
+ if (timestampHeader.status !== "ok") {
172
+ return headerFailure(timestampHeader);
173
+ }
174
+ if (!canonicalTimestamp.test(timestampHeader.value)) {
175
+ return reject("malformed_timestamp");
176
+ }
177
+ const timestamp = Number(timestampHeader.value);
178
+ if (!Number.isSafeInteger(timestamp)) {
179
+ return reject("malformed_timestamp");
180
+ }
181
+ const signatureHeader = collectHeaderValues(options.headers, "webhook-signature");
182
+ if (signatureHeader.status !== "ok") {
183
+ return headerFailure(signatureHeader);
184
+ }
185
+ const signatures = parseSignatures(signatureHeader.value);
186
+ if (typeof signatures === "string") {
187
+ return reject(signatures);
188
+ }
189
+ const attemptHeader = collectHeaderValues(options.headers, "webhook-attempt");
190
+ if (attemptHeader.status !== "ok") {
191
+ return headerFailure(attemptHeader);
192
+ }
193
+ if (!canonicalAttempt.test(attemptHeader.value)) {
194
+ return reject("malformed_attempt");
195
+ }
196
+ const attempt = Number(attemptHeader.value);
197
+ const tolerance = options.toleranceSeconds ?? DEFAULT_WEBHOOK_TIMESTAMP_TOLERANCE_SECONDS;
198
+ if (!Number.isSafeInteger(tolerance) ||
199
+ tolerance < 1 ||
200
+ tolerance > DEFAULT_WEBHOOK_TIMESTAMP_TOLERANCE_SECONDS) {
201
+ return reject("invalid_tolerance");
202
+ }
203
+ const nowSeconds = options.nowSeconds === undefined
204
+ ? Math.floor(Date.now() / 1000)
205
+ : options.nowSeconds;
206
+ if (!Number.isSafeInteger(nowSeconds) || nowSeconds < 0) {
207
+ return reject("invalid_clock");
208
+ }
209
+ if (Math.abs(nowSeconds - timestamp) > tolerance) {
210
+ return reject("timestamp_outside_tolerance");
211
+ }
212
+ if (!Array.isArray(options.secrets) ||
213
+ !options.secrets.every((secret) => typeof secret === "string")) {
214
+ return reject("invalid_secret");
215
+ }
216
+ if (options.secrets.length > MAX_WEBHOOK_SECRETS) {
217
+ return reject("too_many_secrets");
218
+ }
219
+ if (options.secrets.length === 0) {
220
+ return reject("invalid_secret");
221
+ }
222
+ if (new Set(options.secrets).size !== options.secrets.length) {
223
+ return reject("duplicate_secret");
224
+ }
225
+ const decodedSecrets = options.secrets.map(parseSecret);
226
+ if (decodedSecrets.some((secret) => secret === undefined)) {
227
+ return reject("invalid_secret");
228
+ }
229
+ const message = signingBytes(idHeader.value, timestampHeader.value, options.rawBody);
230
+ let matched = false;
231
+ try {
232
+ for (const possibleSecret of decodedSecrets) {
233
+ if (possibleSecret === undefined) {
234
+ continue;
235
+ }
236
+ const key = await runtimeCrypto.subtle.importKey("raw", Uint8Array.from(possibleSecret), { hash: "SHA-256", name: "HMAC" }, false, ["sign"]);
237
+ const digest = new Uint8Array(await runtimeCrypto.subtle.sign("HMAC", key, Uint8Array.from(message)));
238
+ for (const signature of signatures) {
239
+ matched = constantTimeEqual(digest, signature.bytes) || matched;
240
+ }
241
+ }
242
+ }
243
+ catch {
244
+ return reject("crypto_unavailable");
245
+ }
246
+ return matched
247
+ ? {
248
+ attempt,
249
+ eventId: idHeader.value,
250
+ timestamp,
251
+ verified: true,
252
+ }
253
+ : reject("signature_mismatch");
254
+ }
255
+ //# sourceMappingURL=webhook-signature.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhook-signature.js","sourceRoot":"","sources":["../src/webhook-signature.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,283 @@
1
+ #!/usr/bin/env python3
2
+ """Verify WhooshBang Standard Webhooks fixtures with the stdlib only."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import base64
7
+ import binascii
8
+ import hashlib
9
+ import hmac
10
+ import json
11
+ import re
12
+ import sys
13
+ from pathlib import Path
14
+ from typing import Any
15
+
16
+ DEFAULT_TOLERANCE_SECONDS = 300
17
+ MAX_SIGNATURES = 2
18
+ MAX_SECRETS = 2
19
+
20
+ EVENT_IDENTIFIER = re.compile(r"^evt_[A-Za-z0-9_-]+$", re.ASCII)
21
+ CANONICAL_TIMESTAMP = re.compile(r"^(?:0|[1-9][0-9]{0,14})$", re.ASCII)
22
+ CANONICAL_ATTEMPT = re.compile(r"^[1-8]$", re.ASCII)
23
+ SIGNATURE_TOKEN = re.compile(
24
+ r"^([A-Za-z0-9]+),([A-Za-z0-9+/]+={0,2})$", re.ASCII
25
+ )
26
+ CANONICAL_BASE64 = re.compile(
27
+ r"^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$",
28
+ re.ASCII,
29
+ )
30
+
31
+
32
+ def rejected(reason: str) -> dict[str, Any]:
33
+ """Return a deliberately redacted verification failure."""
34
+
35
+ return {"verified": False, "reason": reason}
36
+
37
+
38
+ def single_header(
39
+ headers: Any, wanted_name: str
40
+ ) -> tuple[str | None, str | None]:
41
+ if not isinstance(headers, list):
42
+ return None, "malformed_header"
43
+
44
+ values: list[str] = []
45
+ for entry in headers:
46
+ if (
47
+ not isinstance(entry, (list, tuple))
48
+ or len(entry) != 2
49
+ or not isinstance(entry[0], str)
50
+ or not isinstance(entry[1], str)
51
+ ):
52
+ return None, "malformed_header"
53
+ if entry[0].lower() == wanted_name:
54
+ values.append(entry[1])
55
+
56
+ if not values:
57
+ return None, "missing_header"
58
+ if len(values) != 1:
59
+ return None, "duplicate_header"
60
+ return values[0], None
61
+
62
+
63
+ def decode_canonical_base64(value: str) -> bytes | None:
64
+ if CANONICAL_BASE64.fullmatch(value) is None:
65
+ return None
66
+ try:
67
+ decoded = base64.b64decode(value, validate=True)
68
+ except (ValueError, binascii.Error):
69
+ return None
70
+ if base64.b64encode(decoded).decode("ascii") != value:
71
+ return None
72
+ return decoded
73
+
74
+
75
+ def parse_secret(value: str) -> bytes | None:
76
+ if not value.startswith("whsec_"):
77
+ return None
78
+ decoded = decode_canonical_base64(value[len("whsec_") :])
79
+ if decoded is None or not 24 <= len(decoded) <= 64:
80
+ return None
81
+ return decoded
82
+
83
+
84
+ def parse_signatures(value: str) -> tuple[list[bytes] | None, str | None]:
85
+ if (
86
+ not value
87
+ or value.startswith(" ")
88
+ or value.endswith(" ")
89
+ or " " in value
90
+ ):
91
+ return None, "malformed_signature"
92
+
93
+ tokens = value.split(" ")
94
+ if len(tokens) > MAX_SIGNATURES:
95
+ return None, "too_many_signatures"
96
+
97
+ seen: set[str] = set()
98
+ signatures: list[bytes] = []
99
+ for token in tokens:
100
+ match = SIGNATURE_TOKEN.fullmatch(token)
101
+ if match is None:
102
+ return None, "malformed_signature"
103
+ version, encoded = match.groups()
104
+ if version != "v1":
105
+ return None, "unsupported_signature_version"
106
+ if token in seen:
107
+ return None, "duplicate_signature"
108
+ seen.add(token)
109
+ decoded = decode_canonical_base64(encoded)
110
+ if decoded is None or len(decoded) != 32:
111
+ return None, "malformed_signature"
112
+ signatures.append(decoded)
113
+ return signatures, None
114
+
115
+
116
+ def is_plain_integer(value: Any) -> bool:
117
+ return isinstance(value, int) and not isinstance(value, bool)
118
+
119
+
120
+ def verify_webhook(
121
+ *,
122
+ raw_body: Any,
123
+ headers: Any,
124
+ secrets: Any,
125
+ now_seconds: Any,
126
+ tolerance_seconds: Any = DEFAULT_TOLERANCE_SECONDS,
127
+ ) -> dict[str, Any]:
128
+ """Verify one attempt without parsing or re-serializing its JSON body."""
129
+
130
+ if not isinstance(raw_body, bytes):
131
+ return rejected("invalid_body")
132
+
133
+ event_id, error = single_header(headers, "webhook-id")
134
+ if error is not None:
135
+ return rejected(error)
136
+ if (
137
+ event_id is None
138
+ or not 8 <= len(event_id) <= 128
139
+ or EVENT_IDENTIFIER.fullmatch(event_id) is None
140
+ ):
141
+ return rejected("malformed_webhook_id")
142
+
143
+ timestamp_text, error = single_header(headers, "webhook-timestamp")
144
+ if error is not None:
145
+ return rejected(error)
146
+ if (
147
+ timestamp_text is None
148
+ or CANONICAL_TIMESTAMP.fullmatch(timestamp_text) is None
149
+ ):
150
+ return rejected("malformed_timestamp")
151
+ timestamp = int(timestamp_text)
152
+
153
+ signature_text, error = single_header(headers, "webhook-signature")
154
+ if error is not None:
155
+ return rejected(error)
156
+ if signature_text is None:
157
+ return rejected("malformed_signature")
158
+ signatures, error = parse_signatures(signature_text)
159
+ if error is not None:
160
+ return rejected(error)
161
+ if signatures is None:
162
+ return rejected("malformed_signature")
163
+
164
+ attempt_text, error = single_header(headers, "webhook-attempt")
165
+ if error is not None:
166
+ return rejected(error)
167
+ if (
168
+ attempt_text is None
169
+ or CANONICAL_ATTEMPT.fullmatch(attempt_text) is None
170
+ ):
171
+ return rejected("malformed_attempt")
172
+ attempt = int(attempt_text)
173
+
174
+ if (
175
+ not is_plain_integer(tolerance_seconds)
176
+ or not 1 <= tolerance_seconds <= DEFAULT_TOLERANCE_SECONDS
177
+ ):
178
+ return rejected("invalid_tolerance")
179
+ if not is_plain_integer(now_seconds) or now_seconds < 0:
180
+ return rejected("invalid_clock")
181
+ if abs(now_seconds - timestamp) > tolerance_seconds:
182
+ return rejected("timestamp_outside_tolerance")
183
+
184
+ if not isinstance(secrets, list) or not all(
185
+ isinstance(secret, str) for secret in secrets
186
+ ):
187
+ return rejected("invalid_secret")
188
+ if len(secrets) > MAX_SECRETS:
189
+ return rejected("too_many_secrets")
190
+ if not secrets:
191
+ return rejected("invalid_secret")
192
+ if len(set(secrets)) != len(secrets):
193
+ return rejected("duplicate_secret")
194
+ decoded_secrets = [parse_secret(secret) for secret in secrets]
195
+ if any(secret is None for secret in decoded_secrets):
196
+ return rejected("invalid_secret")
197
+
198
+ signed_bytes = (
199
+ event_id.encode("ascii")
200
+ + b"."
201
+ + timestamp_text.encode("ascii")
202
+ + b"."
203
+ + raw_body
204
+ )
205
+ matched = False
206
+ for possible_secret in decoded_secrets:
207
+ if possible_secret is None:
208
+ continue
209
+ digest = hmac.new(
210
+ possible_secret, signed_bytes, hashlib.sha256
211
+ ).digest()
212
+ for signature in signatures:
213
+ matched = hmac.compare_digest(digest, signature) or matched
214
+
215
+ if not matched:
216
+ return rejected("signature_mismatch")
217
+ return {
218
+ "verified": True,
219
+ "event_id": event_id,
220
+ "timestamp": timestamp,
221
+ "attempt": attempt,
222
+ }
223
+
224
+
225
+ def run_vectors(vector_path: Path) -> dict[str, Any]:
226
+ vector_set = json.loads(vector_path.read_text(encoding="utf-8"))
227
+ keys = vector_set["synthetic_verification_keys"]
228
+ bodies = vector_set["raw_bodies"]
229
+ cases = vector_set["cases"]
230
+
231
+ for vector in cases:
232
+ result = verify_webhook(
233
+ raw_body=bodies[vector["raw_body"]].encode("utf-8"),
234
+ headers=[
235
+ (header["name"], header["value"])
236
+ for header in vector["headers"]
237
+ ],
238
+ secrets=[keys[name] for name in vector["trusted_keys"]],
239
+ now_seconds=vector["now_seconds"],
240
+ tolerance_seconds=vector["tolerance_seconds"],
241
+ )
242
+ if result != vector["expected"]:
243
+ return {
244
+ "status": "fail",
245
+ "code": "WEBHOOK_VECTOR_MISMATCH",
246
+ "case_id": vector["id"],
247
+ }
248
+
249
+ return {
250
+ "status": "pass",
251
+ "code": "WEBHOOK_VECTORS_OK",
252
+ "cases": len(cases),
253
+ }
254
+
255
+
256
+ def main() -> int:
257
+ if len(sys.argv) != 2:
258
+ sys.stderr.write(
259
+ json.dumps(
260
+ {
261
+ "status": "fail",
262
+ "code": "WEBHOOK_VECTOR_PATH_REQUIRED",
263
+ }
264
+ )
265
+ + "\n"
266
+ )
267
+ return 2
268
+
269
+ try:
270
+ summary = run_vectors(Path(sys.argv[1]))
271
+ except Exception:
272
+ summary = {
273
+ "status": "fail",
274
+ "code": "WEBHOOK_VECTOR_RUN_FAILED",
275
+ }
276
+
277
+ stream = sys.stdout if summary["status"] == "pass" else sys.stderr
278
+ stream.write(json.dumps(summary, sort_keys=True) + "\n")
279
+ return 0 if summary["status"] == "pass" else 1
280
+
281
+
282
+ if __name__ == "__main__":
283
+ raise SystemExit(main())
@@ -0,0 +1 @@
1
+