@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,852 @@
1
+ {
2
+ "schema": "whooshbang.webhook-signing-vectors.v1",
3
+ "synthetic_verification_keys": {
4
+ "current": "whsec_AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=",
5
+ "previous": "whsec_ICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9Pj8=",
6
+ "wrong": "whsec_QEFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaW1xdXl8=",
7
+ "short_invalid": "whsec_AA=="
8
+ },
9
+ "raw_bodies": {
10
+ "original": "{\"schema\":\"whooshbang.event.v1\",\"id\":\"evt_interaction_received_synthetic\",\"type\":\"interaction.received\",\"created_at\":\"2026-07-25T18:11:00Z\",\"account_id\":\"acct_synthetic_customer_events\",\"project_id\":\"proj_synthetic_customer_events\",\"environment\":\"test\",\"data\":{\"interaction_id\":\"interaction_synthetic_confirm\",\"message_id\":\"message_synthetic_interaction\",\"binding_id\":\"binding_synthetic_telegram\",\"channel\":\"telegram\",\"correlation_id\":\"correlation_synthetic_interaction\",\"interaction_type\":\"confirm\",\"response\":{\"type\":\"confirm\",\"value\":true},\"occurred_at\":\"2026-07-25T18:10:30Z\",\"expires_at\":\"2026-07-25T18:30:00Z\"}}",
11
+ "body_mutation": "{ \"schema\":\"whooshbang.event.v1\",\"id\":\"evt_interaction_received_synthetic\",\"type\":\"interaction.received\",\"created_at\":\"2026-07-25T18:11:00Z\",\"account_id\":\"acct_synthetic_customer_events\",\"project_id\":\"proj_synthetic_customer_events\",\"environment\":\"test\",\"data\":{\"interaction_id\":\"interaction_synthetic_confirm\",\"message_id\":\"message_synthetic_interaction\",\"binding_id\":\"binding_synthetic_telegram\",\"channel\":\"telegram\",\"correlation_id\":\"correlation_synthetic_interaction\",\"interaction_type\":\"confirm\",\"response\":{\"type\":\"confirm\",\"value\":true},\"occurred_at\":\"2026-07-25T18:10:30Z\",\"expires_at\":\"2026-07-25T18:30:00Z\"}}"
12
+ },
13
+ "cases": [
14
+ {
15
+ "id": "valid-current",
16
+ "description": "One current V1 signature verifies over the exact raw bytes.",
17
+ "raw_body": "original",
18
+ "headers": [
19
+ {
20
+ "name": "webhook-id",
21
+ "value": "evt_interaction_received_synthetic"
22
+ },
23
+ {
24
+ "name": "webhook-timestamp",
25
+ "value": "1785000000"
26
+ },
27
+ {
28
+ "name": "webhook-signature",
29
+ "value": "v1,HNNY1ubZYyZMemV4NJVBoIbHqzklUZWyS6LqI5SlVrc="
30
+ },
31
+ {
32
+ "name": "webhook-attempt",
33
+ "value": "1"
34
+ }
35
+ ],
36
+ "trusted_keys": ["current"],
37
+ "now_seconds": 1785000000,
38
+ "tolerance_seconds": 300,
39
+ "expected": {
40
+ "verified": true,
41
+ "event_id": "evt_interaction_received_synthetic",
42
+ "timestamp": 1785000000,
43
+ "attempt": 1
44
+ }
45
+ },
46
+ {
47
+ "id": "valid-rotation-overlap",
48
+ "description": "Current and previous V1 signatures share the standard space-delimited header.",
49
+ "raw_body": "original",
50
+ "headers": [
51
+ {
52
+ "name": "webhook-id",
53
+ "value": "evt_interaction_received_synthetic"
54
+ },
55
+ {
56
+ "name": "webhook-timestamp",
57
+ "value": "1785000000"
58
+ },
59
+ {
60
+ "name": "webhook-signature",
61
+ "value": "v1,HNNY1ubZYyZMemV4NJVBoIbHqzklUZWyS6LqI5SlVrc= v1,0lrBBZXvwC5Odcq5hOqVUq9cNJ1mnGSLuuVw5arw87s="
62
+ },
63
+ {
64
+ "name": "webhook-attempt",
65
+ "value": "1"
66
+ }
67
+ ],
68
+ "trusted_keys": ["current", "previous"],
69
+ "now_seconds": 1785000000,
70
+ "tolerance_seconds": 300,
71
+ "expected": {
72
+ "verified": true,
73
+ "event_id": "evt_interaction_received_synthetic",
74
+ "timestamp": 1785000000,
75
+ "attempt": 1
76
+ }
77
+ },
78
+ {
79
+ "id": "valid-rotation-previous",
80
+ "description": "The immediately previous key remains trusted during bounded rotation overlap.",
81
+ "raw_body": "original",
82
+ "headers": [
83
+ {
84
+ "name": "webhook-id",
85
+ "value": "evt_interaction_received_synthetic"
86
+ },
87
+ {
88
+ "name": "webhook-timestamp",
89
+ "value": "1785000000"
90
+ },
91
+ {
92
+ "name": "webhook-signature",
93
+ "value": "v1,0lrBBZXvwC5Odcq5hOqVUq9cNJ1mnGSLuuVw5arw87s="
94
+ },
95
+ {
96
+ "name": "webhook-attempt",
97
+ "value": "1"
98
+ }
99
+ ],
100
+ "trusted_keys": ["current", "previous"],
101
+ "now_seconds": 1785000000,
102
+ "tolerance_seconds": 300,
103
+ "expected": {
104
+ "verified": true,
105
+ "event_id": "evt_interaction_received_synthetic",
106
+ "timestamp": 1785000000,
107
+ "attempt": 1
108
+ }
109
+ },
110
+ {
111
+ "id": "valid-rotation-current-only",
112
+ "description": "A receiver that has only the current key accepts the current member of an overlap header.",
113
+ "raw_body": "original",
114
+ "headers": [
115
+ {
116
+ "name": "webhook-id",
117
+ "value": "evt_interaction_received_synthetic"
118
+ },
119
+ {
120
+ "name": "webhook-timestamp",
121
+ "value": "1785000000"
122
+ },
123
+ {
124
+ "name": "webhook-signature",
125
+ "value": "v1,HNNY1ubZYyZMemV4NJVBoIbHqzklUZWyS6LqI5SlVrc= v1,0lrBBZXvwC5Odcq5hOqVUq9cNJ1mnGSLuuVw5arw87s="
126
+ },
127
+ {
128
+ "name": "webhook-attempt",
129
+ "value": "1"
130
+ }
131
+ ],
132
+ "trusted_keys": ["current"],
133
+ "now_seconds": 1785000000,
134
+ "tolerance_seconds": 300,
135
+ "expected": {
136
+ "verified": true,
137
+ "event_id": "evt_interaction_received_synthetic",
138
+ "timestamp": 1785000000,
139
+ "attempt": 1
140
+ }
141
+ },
142
+ {
143
+ "id": "wrong-secret",
144
+ "raw_body": "original",
145
+ "headers": [
146
+ {
147
+ "name": "webhook-id",
148
+ "value": "evt_interaction_received_synthetic"
149
+ },
150
+ {
151
+ "name": "webhook-timestamp",
152
+ "value": "1785000000"
153
+ },
154
+ {
155
+ "name": "webhook-signature",
156
+ "value": "v1,HNNY1ubZYyZMemV4NJVBoIbHqzklUZWyS6LqI5SlVrc="
157
+ },
158
+ {
159
+ "name": "webhook-attempt",
160
+ "value": "1"
161
+ }
162
+ ],
163
+ "trusted_keys": ["wrong"],
164
+ "now_seconds": 1785000000,
165
+ "tolerance_seconds": 300,
166
+ "expected": {
167
+ "verified": false,
168
+ "reason": "signature_mismatch"
169
+ }
170
+ },
171
+ {
172
+ "id": "body-mutation",
173
+ "description": "JSON that parses identically but has different raw bytes does not verify.",
174
+ "raw_body": "body_mutation",
175
+ "headers": [
176
+ {
177
+ "name": "webhook-id",
178
+ "value": "evt_interaction_received_synthetic"
179
+ },
180
+ {
181
+ "name": "webhook-timestamp",
182
+ "value": "1785000000"
183
+ },
184
+ {
185
+ "name": "webhook-signature",
186
+ "value": "v1,HNNY1ubZYyZMemV4NJVBoIbHqzklUZWyS6LqI5SlVrc="
187
+ },
188
+ {
189
+ "name": "webhook-attempt",
190
+ "value": "1"
191
+ }
192
+ ],
193
+ "trusted_keys": ["current"],
194
+ "now_seconds": 1785000000,
195
+ "tolerance_seconds": 300,
196
+ "expected": {
197
+ "verified": false,
198
+ "reason": "signature_mismatch"
199
+ }
200
+ },
201
+ {
202
+ "id": "id-mutation",
203
+ "raw_body": "original",
204
+ "headers": [
205
+ {
206
+ "name": "webhook-id",
207
+ "value": "evt_interaction_received_mutated"
208
+ },
209
+ {
210
+ "name": "webhook-timestamp",
211
+ "value": "1785000000"
212
+ },
213
+ {
214
+ "name": "webhook-signature",
215
+ "value": "v1,HNNY1ubZYyZMemV4NJVBoIbHqzklUZWyS6LqI5SlVrc="
216
+ },
217
+ {
218
+ "name": "webhook-attempt",
219
+ "value": "1"
220
+ }
221
+ ],
222
+ "trusted_keys": ["current"],
223
+ "now_seconds": 1785000000,
224
+ "tolerance_seconds": 300,
225
+ "expected": {
226
+ "verified": false,
227
+ "reason": "signature_mismatch"
228
+ }
229
+ },
230
+ {
231
+ "id": "timestamp-mutation",
232
+ "raw_body": "original",
233
+ "headers": [
234
+ {
235
+ "name": "webhook-id",
236
+ "value": "evt_interaction_received_synthetic"
237
+ },
238
+ {
239
+ "name": "webhook-timestamp",
240
+ "value": "1785000001"
241
+ },
242
+ {
243
+ "name": "webhook-signature",
244
+ "value": "v1,HNNY1ubZYyZMemV4NJVBoIbHqzklUZWyS6LqI5SlVrc="
245
+ },
246
+ {
247
+ "name": "webhook-attempt",
248
+ "value": "1"
249
+ }
250
+ ],
251
+ "trusted_keys": ["current"],
252
+ "now_seconds": 1785000001,
253
+ "tolerance_seconds": 300,
254
+ "expected": {
255
+ "verified": false,
256
+ "reason": "signature_mismatch"
257
+ }
258
+ },
259
+ {
260
+ "id": "timestamp-outside-tolerance",
261
+ "description": "A cryptographically valid old attempt is rejected by the replay window.",
262
+ "raw_body": "original",
263
+ "headers": [
264
+ {
265
+ "name": "webhook-id",
266
+ "value": "evt_interaction_received_synthetic"
267
+ },
268
+ {
269
+ "name": "webhook-timestamp",
270
+ "value": "1784999600"
271
+ },
272
+ {
273
+ "name": "webhook-signature",
274
+ "value": "v1,xGLKp4XxdmxIk3OE8P0/n95rAyyyKkvcQnZooclcE5c="
275
+ },
276
+ {
277
+ "name": "webhook-attempt",
278
+ "value": "1"
279
+ }
280
+ ],
281
+ "trusted_keys": ["current"],
282
+ "now_seconds": 1785000000,
283
+ "tolerance_seconds": 300,
284
+ "expected": {
285
+ "verified": false,
286
+ "reason": "timestamp_outside_tolerance"
287
+ }
288
+ },
289
+ {
290
+ "id": "missing-signature-header",
291
+ "raw_body": "original",
292
+ "headers": [
293
+ {
294
+ "name": "webhook-id",
295
+ "value": "evt_interaction_received_synthetic"
296
+ },
297
+ {
298
+ "name": "webhook-timestamp",
299
+ "value": "1785000000"
300
+ },
301
+ {
302
+ "name": "webhook-attempt",
303
+ "value": "1"
304
+ }
305
+ ],
306
+ "trusted_keys": ["current"],
307
+ "now_seconds": 1785000000,
308
+ "tolerance_seconds": 300,
309
+ "expected": {
310
+ "verified": false,
311
+ "reason": "missing_header"
312
+ }
313
+ },
314
+ {
315
+ "id": "duplicate-id-header",
316
+ "raw_body": "original",
317
+ "headers": [
318
+ {
319
+ "name": "webhook-id",
320
+ "value": "evt_interaction_received_synthetic"
321
+ },
322
+ {
323
+ "name": "Webhook-Id",
324
+ "value": "evt_interaction_received_synthetic"
325
+ },
326
+ {
327
+ "name": "webhook-timestamp",
328
+ "value": "1785000000"
329
+ },
330
+ {
331
+ "name": "webhook-signature",
332
+ "value": "v1,HNNY1ubZYyZMemV4NJVBoIbHqzklUZWyS6LqI5SlVrc="
333
+ },
334
+ {
335
+ "name": "webhook-attempt",
336
+ "value": "1"
337
+ }
338
+ ],
339
+ "trusted_keys": ["current"],
340
+ "now_seconds": 1785000000,
341
+ "tolerance_seconds": 300,
342
+ "expected": {
343
+ "verified": false,
344
+ "reason": "duplicate_header"
345
+ }
346
+ },
347
+ {
348
+ "id": "duplicate-signature-header",
349
+ "raw_body": "original",
350
+ "headers": [
351
+ {
352
+ "name": "webhook-id",
353
+ "value": "evt_interaction_received_synthetic"
354
+ },
355
+ {
356
+ "name": "webhook-timestamp",
357
+ "value": "1785000000"
358
+ },
359
+ {
360
+ "name": "webhook-signature",
361
+ "value": "v1,HNNY1ubZYyZMemV4NJVBoIbHqzklUZWyS6LqI5SlVrc="
362
+ },
363
+ {
364
+ "name": "webhook-signature",
365
+ "value": "v1,HNNY1ubZYyZMemV4NJVBoIbHqzklUZWyS6LqI5SlVrc="
366
+ },
367
+ {
368
+ "name": "webhook-attempt",
369
+ "value": "1"
370
+ }
371
+ ],
372
+ "trusted_keys": ["current"],
373
+ "now_seconds": 1785000000,
374
+ "tolerance_seconds": 300,
375
+ "expected": {
376
+ "verified": false,
377
+ "reason": "duplicate_header"
378
+ }
379
+ },
380
+ {
381
+ "id": "malformed-signature",
382
+ "raw_body": "original",
383
+ "headers": [
384
+ {
385
+ "name": "webhook-id",
386
+ "value": "evt_interaction_received_synthetic"
387
+ },
388
+ {
389
+ "name": "webhook-timestamp",
390
+ "value": "1785000000"
391
+ },
392
+ {
393
+ "name": "webhook-signature",
394
+ "value": "v1,not*base64"
395
+ },
396
+ {
397
+ "name": "webhook-attempt",
398
+ "value": "1"
399
+ }
400
+ ],
401
+ "trusted_keys": ["current"],
402
+ "now_seconds": 1785000000,
403
+ "tolerance_seconds": 300,
404
+ "expected": {
405
+ "verified": false,
406
+ "reason": "malformed_signature"
407
+ }
408
+ },
409
+ {
410
+ "id": "unknown-signature-version",
411
+ "raw_body": "original",
412
+ "headers": [
413
+ {
414
+ "name": "webhook-id",
415
+ "value": "evt_interaction_received_synthetic"
416
+ },
417
+ {
418
+ "name": "webhook-timestamp",
419
+ "value": "1785000000"
420
+ },
421
+ {
422
+ "name": "webhook-signature",
423
+ "value": "v2,HNNY1ubZYyZMemV4NJVBoIbHqzklUZWyS6LqI5SlVrc="
424
+ },
425
+ {
426
+ "name": "webhook-attempt",
427
+ "value": "1"
428
+ }
429
+ ],
430
+ "trusted_keys": ["current"],
431
+ "now_seconds": 1785000000,
432
+ "tolerance_seconds": 300,
433
+ "expected": {
434
+ "verified": false,
435
+ "reason": "unsupported_signature_version"
436
+ }
437
+ },
438
+ {
439
+ "id": "malformed-multi-signature-spacing",
440
+ "raw_body": "original",
441
+ "headers": [
442
+ {
443
+ "name": "webhook-id",
444
+ "value": "evt_interaction_received_synthetic"
445
+ },
446
+ {
447
+ "name": "webhook-timestamp",
448
+ "value": "1785000000"
449
+ },
450
+ {
451
+ "name": "webhook-signature",
452
+ "value": "v1,HNNY1ubZYyZMemV4NJVBoIbHqzklUZWyS6LqI5SlVrc= v1,0lrBBZXvwC5Odcq5hOqVUq9cNJ1mnGSLuuVw5arw87s="
453
+ },
454
+ {
455
+ "name": "webhook-attempt",
456
+ "value": "1"
457
+ }
458
+ ],
459
+ "trusted_keys": ["current", "previous"],
460
+ "now_seconds": 1785000000,
461
+ "tolerance_seconds": 300,
462
+ "expected": {
463
+ "verified": false,
464
+ "reason": "malformed_signature"
465
+ }
466
+ },
467
+ {
468
+ "id": "too-many-signatures",
469
+ "raw_body": "original",
470
+ "headers": [
471
+ {
472
+ "name": "webhook-id",
473
+ "value": "evt_interaction_received_synthetic"
474
+ },
475
+ {
476
+ "name": "webhook-timestamp",
477
+ "value": "1785000000"
478
+ },
479
+ {
480
+ "name": "webhook-signature",
481
+ "value": "v1,HNNY1ubZYyZMemV4NJVBoIbHqzklUZWyS6LqI5SlVrc= v1,0lrBBZXvwC5Odcq5hOqVUq9cNJ1mnGSLuuVw5arw87s= v1,HNNY1ubZYyZMemV4NJVBoIbHqzklUZWyS6LqI5SlVrc="
482
+ },
483
+ {
484
+ "name": "webhook-attempt",
485
+ "value": "1"
486
+ }
487
+ ],
488
+ "trusted_keys": ["current", "previous"],
489
+ "now_seconds": 1785000000,
490
+ "tolerance_seconds": 300,
491
+ "expected": {
492
+ "verified": false,
493
+ "reason": "too_many_signatures"
494
+ }
495
+ },
496
+ {
497
+ "id": "duplicate-signature-token",
498
+ "raw_body": "original",
499
+ "headers": [
500
+ {
501
+ "name": "webhook-id",
502
+ "value": "evt_interaction_received_synthetic"
503
+ },
504
+ {
505
+ "name": "webhook-timestamp",
506
+ "value": "1785000000"
507
+ },
508
+ {
509
+ "name": "webhook-signature",
510
+ "value": "v1,HNNY1ubZYyZMemV4NJVBoIbHqzklUZWyS6LqI5SlVrc= v1,HNNY1ubZYyZMemV4NJVBoIbHqzklUZWyS6LqI5SlVrc="
511
+ },
512
+ {
513
+ "name": "webhook-attempt",
514
+ "value": "1"
515
+ }
516
+ ],
517
+ "trusted_keys": ["current"],
518
+ "now_seconds": 1785000000,
519
+ "tolerance_seconds": 300,
520
+ "expected": {
521
+ "verified": false,
522
+ "reason": "duplicate_signature"
523
+ }
524
+ },
525
+ {
526
+ "id": "delimiter-injected-id",
527
+ "raw_body": "original",
528
+ "headers": [
529
+ {
530
+ "name": "webhook-id",
531
+ "value": "evt.interaction.injected"
532
+ },
533
+ {
534
+ "name": "webhook-timestamp",
535
+ "value": "1785000000"
536
+ },
537
+ {
538
+ "name": "webhook-signature",
539
+ "value": "v1,/qusIweevhAX5gR0nzLT4T41CwXLF3Qc+xGqaPTnY9k="
540
+ },
541
+ {
542
+ "name": "webhook-attempt",
543
+ "value": "1"
544
+ }
545
+ ],
546
+ "trusted_keys": ["current"],
547
+ "now_seconds": 1785000000,
548
+ "tolerance_seconds": 300,
549
+ "expected": {
550
+ "verified": false,
551
+ "reason": "malformed_webhook_id"
552
+ }
553
+ },
554
+ {
555
+ "id": "delimiter-injected-timestamp",
556
+ "raw_body": "original",
557
+ "headers": [
558
+ {
559
+ "name": "webhook-id",
560
+ "value": "evt_interaction_received_synthetic"
561
+ },
562
+ {
563
+ "name": "webhook-timestamp",
564
+ "value": "1785000000.0"
565
+ },
566
+ {
567
+ "name": "webhook-signature",
568
+ "value": "v1,cQ3H2qUQEZSXQiz5Sei6XtdrLgQcYGMkHOXgKIzMZxs="
569
+ },
570
+ {
571
+ "name": "webhook-attempt",
572
+ "value": "1"
573
+ }
574
+ ],
575
+ "trusted_keys": ["current"],
576
+ "now_seconds": 1785000000,
577
+ "tolerance_seconds": 300,
578
+ "expected": {
579
+ "verified": false,
580
+ "reason": "malformed_timestamp"
581
+ }
582
+ },
583
+ {
584
+ "id": "noncanonical-timestamp",
585
+ "raw_body": "original",
586
+ "headers": [
587
+ {
588
+ "name": "webhook-id",
589
+ "value": "evt_interaction_received_synthetic"
590
+ },
591
+ {
592
+ "name": "webhook-timestamp",
593
+ "value": "01785000000"
594
+ },
595
+ {
596
+ "name": "webhook-signature",
597
+ "value": "v1,HNNY1ubZYyZMemV4NJVBoIbHqzklUZWyS6LqI5SlVrc="
598
+ },
599
+ {
600
+ "name": "webhook-attempt",
601
+ "value": "1"
602
+ }
603
+ ],
604
+ "trusted_keys": ["current"],
605
+ "now_seconds": 1785000000,
606
+ "tolerance_seconds": 300,
607
+ "expected": {
608
+ "verified": false,
609
+ "reason": "malformed_timestamp"
610
+ }
611
+ },
612
+ {
613
+ "id": "attempt-outside-contract",
614
+ "raw_body": "original",
615
+ "headers": [
616
+ {
617
+ "name": "webhook-id",
618
+ "value": "evt_interaction_received_synthetic"
619
+ },
620
+ {
621
+ "name": "webhook-timestamp",
622
+ "value": "1785000000"
623
+ },
624
+ {
625
+ "name": "webhook-signature",
626
+ "value": "v1,HNNY1ubZYyZMemV4NJVBoIbHqzklUZWyS6LqI5SlVrc="
627
+ },
628
+ {
629
+ "name": "webhook-attempt",
630
+ "value": "9"
631
+ }
632
+ ],
633
+ "trusted_keys": ["current"],
634
+ "now_seconds": 1785000000,
635
+ "tolerance_seconds": 300,
636
+ "expected": {
637
+ "verified": false,
638
+ "reason": "malformed_attempt"
639
+ }
640
+ },
641
+ {
642
+ "id": "attempt-is-not-signed",
643
+ "description": "The diagnostic attempt may change without changing an otherwise valid signature.",
644
+ "raw_body": "original",
645
+ "headers": [
646
+ {
647
+ "name": "webhook-id",
648
+ "value": "evt_interaction_received_synthetic"
649
+ },
650
+ {
651
+ "name": "webhook-timestamp",
652
+ "value": "1785000000"
653
+ },
654
+ {
655
+ "name": "webhook-signature",
656
+ "value": "v1,HNNY1ubZYyZMemV4NJVBoIbHqzklUZWyS6LqI5SlVrc="
657
+ },
658
+ {
659
+ "name": "webhook-attempt",
660
+ "value": "8"
661
+ }
662
+ ],
663
+ "trusted_keys": ["current"],
664
+ "now_seconds": 1785000000,
665
+ "tolerance_seconds": 300,
666
+ "expected": {
667
+ "verified": true,
668
+ "event_id": "evt_interaction_received_synthetic",
669
+ "timestamp": 1785000000,
670
+ "attempt": 8
671
+ }
672
+ },
673
+ {
674
+ "id": "valid-duplicate-event-retry",
675
+ "description": "A retry keeps the exact event ID and body while timestamp, signature, and attempt change.",
676
+ "raw_body": "original",
677
+ "headers": [
678
+ {
679
+ "name": "webhook-id",
680
+ "value": "evt_interaction_received_synthetic"
681
+ },
682
+ {
683
+ "name": "webhook-timestamp",
684
+ "value": "1785000060"
685
+ },
686
+ {
687
+ "name": "webhook-signature",
688
+ "value": "v1,wGTQbelJe/UNKysG3OBoDibjwIf4DoAVRps8DTIvh4s="
689
+ },
690
+ {
691
+ "name": "webhook-attempt",
692
+ "value": "2"
693
+ }
694
+ ],
695
+ "trusted_keys": ["current"],
696
+ "now_seconds": 1785000060,
697
+ "tolerance_seconds": 300,
698
+ "duplicate_of": "valid-current",
699
+ "expected": {
700
+ "verified": true,
701
+ "event_id": "evt_interaction_received_synthetic",
702
+ "timestamp": 1785000060,
703
+ "attempt": 2
704
+ }
705
+ },
706
+ {
707
+ "id": "invalid-short-secret",
708
+ "raw_body": "original",
709
+ "headers": [
710
+ {
711
+ "name": "webhook-id",
712
+ "value": "evt_interaction_received_synthetic"
713
+ },
714
+ {
715
+ "name": "webhook-timestamp",
716
+ "value": "1785000000"
717
+ },
718
+ {
719
+ "name": "webhook-signature",
720
+ "value": "v1,HNNY1ubZYyZMemV4NJVBoIbHqzklUZWyS6LqI5SlVrc="
721
+ },
722
+ {
723
+ "name": "webhook-attempt",
724
+ "value": "1"
725
+ }
726
+ ],
727
+ "trusted_keys": ["short_invalid"],
728
+ "now_seconds": 1785000000,
729
+ "tolerance_seconds": 300,
730
+ "expected": {
731
+ "verified": false,
732
+ "reason": "invalid_secret"
733
+ }
734
+ },
735
+ {
736
+ "id": "too-many-trusted-secrets",
737
+ "raw_body": "original",
738
+ "headers": [
739
+ {
740
+ "name": "webhook-id",
741
+ "value": "evt_interaction_received_synthetic"
742
+ },
743
+ {
744
+ "name": "webhook-timestamp",
745
+ "value": "1785000000"
746
+ },
747
+ {
748
+ "name": "webhook-signature",
749
+ "value": "v1,HNNY1ubZYyZMemV4NJVBoIbHqzklUZWyS6LqI5SlVrc="
750
+ },
751
+ {
752
+ "name": "webhook-attempt",
753
+ "value": "1"
754
+ }
755
+ ],
756
+ "trusted_keys": ["current", "previous", "wrong"],
757
+ "now_seconds": 1785000000,
758
+ "tolerance_seconds": 300,
759
+ "expected": {
760
+ "verified": false,
761
+ "reason": "too_many_secrets"
762
+ }
763
+ },
764
+ {
765
+ "id": "duplicate-trusted-secret",
766
+ "raw_body": "original",
767
+ "headers": [
768
+ {
769
+ "name": "webhook-id",
770
+ "value": "evt_interaction_received_synthetic"
771
+ },
772
+ {
773
+ "name": "webhook-timestamp",
774
+ "value": "1785000000"
775
+ },
776
+ {
777
+ "name": "webhook-signature",
778
+ "value": "v1,HNNY1ubZYyZMemV4NJVBoIbHqzklUZWyS6LqI5SlVrc="
779
+ },
780
+ {
781
+ "name": "webhook-attempt",
782
+ "value": "1"
783
+ }
784
+ ],
785
+ "trusted_keys": ["current", "current"],
786
+ "now_seconds": 1785000000,
787
+ "tolerance_seconds": 300,
788
+ "expected": {
789
+ "verified": false,
790
+ "reason": "duplicate_secret"
791
+ }
792
+ },
793
+ {
794
+ "id": "missing-attempt-header",
795
+ "raw_body": "original",
796
+ "headers": [
797
+ {
798
+ "name": "webhook-id",
799
+ "value": "evt_interaction_received_synthetic"
800
+ },
801
+ {
802
+ "name": "webhook-timestamp",
803
+ "value": "1785000000"
804
+ },
805
+ {
806
+ "name": "webhook-signature",
807
+ "value": "v1,HNNY1ubZYyZMemV4NJVBoIbHqzklUZWyS6LqI5SlVrc="
808
+ }
809
+ ],
810
+ "trusted_keys": ["current"],
811
+ "now_seconds": 1785000000,
812
+ "tolerance_seconds": 300,
813
+ "expected": {
814
+ "verified": false,
815
+ "reason": "missing_header"
816
+ }
817
+ },
818
+ {
819
+ "id": "duplicate-attempt-header",
820
+ "raw_body": "original",
821
+ "headers": [
822
+ {
823
+ "name": "webhook-id",
824
+ "value": "evt_interaction_received_synthetic"
825
+ },
826
+ {
827
+ "name": "webhook-timestamp",
828
+ "value": "1785000000"
829
+ },
830
+ {
831
+ "name": "webhook-signature",
832
+ "value": "v1,HNNY1ubZYyZMemV4NJVBoIbHqzklUZWyS6LqI5SlVrc="
833
+ },
834
+ {
835
+ "name": "webhook-attempt",
836
+ "value": "1"
837
+ },
838
+ {
839
+ "name": "Webhook-Attempt",
840
+ "value": "1"
841
+ }
842
+ ],
843
+ "trusted_keys": ["current"],
844
+ "now_seconds": 1785000000,
845
+ "tolerance_seconds": 300,
846
+ "expected": {
847
+ "verified": false,
848
+ "reason": "duplicate_header"
849
+ }
850
+ }
851
+ ]
852
+ }