@greatwait/contracts 0.1.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (241) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +77 -0
  3. package/dist/authority.d.ts +217 -0
  4. package/dist/authority.js +261 -0
  5. package/dist/callback-ownership.d.ts +58 -0
  6. package/dist/callback-ownership.js +71 -0
  7. package/dist/callback-safety.d.ts +38 -0
  8. package/dist/callback-safety.js +403 -0
  9. package/dist/compatibility.d.ts +82 -0
  10. package/dist/compatibility.js +115 -0
  11. package/dist/delivery.d.ts +401 -0
  12. package/dist/delivery.js +311 -0
  13. package/dist/endpoints.d.ts +305 -0
  14. package/dist/endpoints.js +179 -0
  15. package/dist/events.d.ts +62 -0
  16. package/dist/events.js +38 -0
  17. package/dist/first-value-journey.d.ts +305 -0
  18. package/dist/first-value-journey.js +466 -0
  19. package/dist/forecast.d.ts +443 -0
  20. package/dist/forecast.js +729 -0
  21. package/dist/identifiers.d.ts +109 -0
  22. package/dist/identifiers.js +131 -0
  23. package/dist/identity.d.ts +196 -0
  24. package/dist/identity.js +182 -0
  25. package/dist/index.d.ts +27 -0
  26. package/dist/index.js +27 -0
  27. package/dist/mcp.d.ts +51 -0
  28. package/dist/mcp.js +56 -0
  29. package/dist/oauth.d.ts +796 -0
  30. package/dist/oauth.js +708 -0
  31. package/dist/operations.d.ts +3985 -0
  32. package/dist/operations.js +1837 -0
  33. package/dist/partition.d.ts +771 -0
  34. package/dist/partition.js +206 -0
  35. package/dist/policy.d.ts +93 -0
  36. package/dist/policy.js +180 -0
  37. package/dist/problems.d.ts +158 -0
  38. package/dist/problems.js +104 -0
  39. package/dist/protocol.d.ts +308 -0
  40. package/dist/protocol.js +515 -0
  41. package/dist/scheduling-records.d.ts +395 -0
  42. package/dist/scheduling-records.js +284 -0
  43. package/dist/scheduling.d.ts +1098 -0
  44. package/dist/scheduling.js +1116 -0
  45. package/dist/secrets.d.ts +383 -0
  46. package/dist/secrets.js +272 -0
  47. package/dist/signatures.d.ts +704 -0
  48. package/dist/signatures.js +867 -0
  49. package/dist/tenancy.d.ts +178 -0
  50. package/dist/tenancy.js +145 -0
  51. package/dist/test-sink.d.ts +986 -0
  52. package/dist/test-sink.js +755 -0
  53. package/dist/time.d.ts +45 -0
  54. package/dist/time.js +128 -0
  55. package/dist/usage.d.ts +121 -0
  56. package/dist/usage.js +200 -0
  57. package/dist/webhooks.d.ts +116 -0
  58. package/dist/webhooks.js +161 -0
  59. package/dist/workspace.d.ts +92 -0
  60. package/dist/workspace.js +59 -0
  61. package/generated/v1/callback-delivery-signature.json +467 -0
  62. package/generated/v1/callback-endpoint-verification.json +59 -0
  63. package/generated/v1/callback-receiver.json +241 -0
  64. package/generated/v1/examples/attempts.list.response.json +24 -0
  65. package/generated/v1/examples/attempts.read.response.json +20 -0
  66. package/generated/v1/examples/credentials.create.request.json +5 -0
  67. package/generated/v1/examples/credentials.create.response.json +10 -0
  68. package/generated/v1/examples/credentials.list.response.json +17 -0
  69. package/generated/v1/examples/credentials.revoke.response.json +11 -0
  70. package/generated/v1/examples/credentials.rotate.request.json +3 -0
  71. package/generated/v1/examples/credentials.rotate.response.json +22 -0
  72. package/generated/v1/examples/endpoints.create.request.json +3 -0
  73. package/generated/v1/examples/endpoints.create.response.json +16 -0
  74. package/generated/v1/examples/endpoints.disable.response.json +15 -0
  75. package/generated/v1/examples/endpoints.list.response.json +21 -0
  76. package/generated/v1/examples/endpoints.read.response.json +14 -0
  77. package/generated/v1/examples/endpoints.rotateSigningKey.request.json +3 -0
  78. package/generated/v1/examples/endpoints.rotateSigningKey.response.json +23 -0
  79. package/generated/v1/examples/endpoints.update.request.json +3 -0
  80. package/generated/v1/examples/endpoints.update.response.json +13 -0
  81. package/generated/v1/examples/endpoints.verify.response.json +15 -0
  82. package/generated/v1/examples/forecast.read.response.json +28 -0
  83. package/generated/v1/examples/forecast.windows.response.json +21 -0
  84. package/generated/v1/examples/grants.list.response.json +17 -0
  85. package/generated/v1/examples/grants.revoke.response.json +16 -0
  86. package/generated/v1/examples/meta.scaffold.response.json +5 -0
  87. package/generated/v1/examples/meta.version.response.json +5 -0
  88. package/generated/v1/examples/occurrences.list.response.json +17 -0
  89. package/generated/v1/examples/occurrences.read.response.json +15 -0
  90. package/generated/v1/examples/occurrences.redeliver.response.json +16 -0
  91. package/generated/v1/examples/projects.create.request.json +3 -0
  92. package/generated/v1/examples/projects.create.response.json +5 -0
  93. package/generated/v1/examples/projects.list.response.json +9 -0
  94. package/generated/v1/examples/schedules.archive.response.json +25 -0
  95. package/generated/v1/examples/schedules.cancel.response.json +24 -0
  96. package/generated/v1/examples/schedules.create.request.json +9 -0
  97. package/generated/v1/examples/schedules.create.response.json +25 -0
  98. package/generated/v1/examples/schedules.list.response.json +23 -0
  99. package/generated/v1/examples/schedules.pause.response.json +23 -0
  100. package/generated/v1/examples/schedules.read.response.json +25 -0
  101. package/generated/v1/examples/schedules.replace.request.json +8 -0
  102. package/generated/v1/examples/schedules.replace.response.json +25 -0
  103. package/generated/v1/examples/schedules.resume.response.json +23 -0
  104. package/generated/v1/examples/sinkdeliveries.list.response.json +19 -0
  105. package/generated/v1/examples/sinkdeliveries.read.response.json +16 -0
  106. package/generated/v1/examples/testsinks.configure.request.json +3 -0
  107. package/generated/v1/examples/testsinks.configure.response.json +13 -0
  108. package/generated/v1/examples/testsinks.create.request.json +8 -0
  109. package/generated/v1/examples/testsinks.create.response.json +13 -0
  110. package/generated/v1/examples/testsinks.delete.response.json +13 -0
  111. package/generated/v1/examples/testsinks.list.response.json +20 -0
  112. package/generated/v1/examples/testsinks.read.response.json +14 -0
  113. package/generated/v1/examples/usage.read.response.json +20 -0
  114. package/generated/v1/first-value-journey.json +436 -0
  115. package/generated/v1/manifest.json +1037 -0
  116. package/generated/v1/openapi.json +23665 -0
  117. package/generated/v1/policy-matrix.json +305 -0
  118. package/generated/v1/schemas/attempts.list.query.json +16 -0
  119. package/generated/v1/schemas/attempts.list.response.json +175 -0
  120. package/generated/v1/schemas/attempts.read.response.json +168 -0
  121. package/generated/v1/schemas/credentials.create.request.json +39 -0
  122. package/generated/v1/schemas/credentials.create.response.json +88 -0
  123. package/generated/v1/schemas/credentials.list.query.json +16 -0
  124. package/generated/v1/schemas/credentials.list.response.json +124 -0
  125. package/generated/v1/schemas/credentials.revoke.response.json +84 -0
  126. package/generated/v1/schemas/credentials.rotate.request.json +11 -0
  127. package/generated/v1/schemas/credentials.rotate.response.json +171 -0
  128. package/generated/v1/schemas/endpoints.create.request.json +12 -0
  129. package/generated/v1/schemas/endpoints.create.response.json +115 -0
  130. package/generated/v1/schemas/endpoints.disable.response-headers.json +11 -0
  131. package/generated/v1/schemas/endpoints.disable.response.json +100 -0
  132. package/generated/v1/schemas/endpoints.list.query.json +16 -0
  133. package/generated/v1/schemas/endpoints.list.response.json +146 -0
  134. package/generated/v1/schemas/endpoints.read.response-headers.json +11 -0
  135. package/generated/v1/schemas/endpoints.read.response.json +100 -0
  136. package/generated/v1/schemas/endpoints.rotateSigningKey.request-headers.json +10 -0
  137. package/generated/v1/schemas/endpoints.rotateSigningKey.request.json +11 -0
  138. package/generated/v1/schemas/endpoints.rotateSigningKey.response-headers.json +11 -0
  139. package/generated/v1/schemas/endpoints.rotateSigningKey.response.json +118 -0
  140. package/generated/v1/schemas/endpoints.update.request-headers.json +10 -0
  141. package/generated/v1/schemas/endpoints.update.request.json +12 -0
  142. package/generated/v1/schemas/endpoints.update.response-headers.json +11 -0
  143. package/generated/v1/schemas/endpoints.update.response.json +100 -0
  144. package/generated/v1/schemas/endpoints.verify.request-headers.json +10 -0
  145. package/generated/v1/schemas/endpoints.verify.response-headers.json +11 -0
  146. package/generated/v1/schemas/endpoints.verify.response.json +100 -0
  147. package/generated/v1/schemas/forecast.read.query.json +26 -0
  148. package/generated/v1/schemas/forecast.read.response.json +100 -0
  149. package/generated/v1/schemas/forecast.windows.query.json +26 -0
  150. package/generated/v1/schemas/forecast.windows.response.json +94 -0
  151. package/generated/v1/schemas/grants.list.response.json +98 -0
  152. package/generated/v1/schemas/grants.revoke.response.json +95 -0
  153. package/generated/v1/schemas/meta.scaffold.response.json +36 -0
  154. package/generated/v1/schemas/meta.version.response.json +19 -0
  155. package/generated/v1/schemas/occurrences.list.query.json +16 -0
  156. package/generated/v1/schemas/occurrences.list.response.json +83 -0
  157. package/generated/v1/schemas/occurrences.read.response.json +76 -0
  158. package/generated/v1/schemas/occurrences.redeliver.request-headers.json +13 -0
  159. package/generated/v1/schemas/occurrences.redeliver.response-headers.json +10 -0
  160. package/generated/v1/schemas/occurrences.redeliver.response.json +168 -0
  161. package/generated/v1/schemas/projects.create.request.json +12 -0
  162. package/generated/v1/schemas/projects.create.response.json +25 -0
  163. package/generated/v1/schemas/projects.list.response.json +35 -0
  164. package/generated/v1/schemas/schedules.archive.request-headers.json +10 -0
  165. package/generated/v1/schemas/schedules.archive.response-headers.json +11 -0
  166. package/generated/v1/schemas/schedules.archive.response.json +189 -0
  167. package/generated/v1/schemas/schedules.cancel.request-headers.json +10 -0
  168. package/generated/v1/schemas/schedules.cancel.response-headers.json +11 -0
  169. package/generated/v1/schemas/schedules.cancel.response.json +189 -0
  170. package/generated/v1/schemas/schedules.create.request-headers.json +12 -0
  171. package/generated/v1/schemas/schedules.create.request.json +99 -0
  172. package/generated/v1/schemas/schedules.create.response-headers.json +15 -0
  173. package/generated/v1/schemas/schedules.create.response.json +189 -0
  174. package/generated/v1/schemas/schedules.list.query.json +16 -0
  175. package/generated/v1/schemas/schedules.list.response.json +170 -0
  176. package/generated/v1/schemas/schedules.pause.request-headers.json +10 -0
  177. package/generated/v1/schemas/schedules.pause.response-headers.json +11 -0
  178. package/generated/v1/schemas/schedules.pause.response.json +189 -0
  179. package/generated/v1/schemas/schedules.read.response-headers.json +11 -0
  180. package/generated/v1/schemas/schedules.read.response.json +189 -0
  181. package/generated/v1/schemas/schedules.replace.request-headers.json +10 -0
  182. package/generated/v1/schemas/schedules.replace.request.json +99 -0
  183. package/generated/v1/schemas/schedules.replace.response-headers.json +11 -0
  184. package/generated/v1/schemas/schedules.replace.response.json +189 -0
  185. package/generated/v1/schemas/schedules.resume.request-headers.json +10 -0
  186. package/generated/v1/schemas/schedules.resume.response-headers.json +11 -0
  187. package/generated/v1/schemas/schedules.resume.response.json +189 -0
  188. package/generated/v1/schemas/sinkdeliveries.list.query.json +16 -0
  189. package/generated/v1/schemas/sinkdeliveries.list.response.json +126 -0
  190. package/generated/v1/schemas/sinkdeliveries.read.response.json +98 -0
  191. package/generated/v1/schemas/testsinks.configure.request.json +66 -0
  192. package/generated/v1/schemas/testsinks.configure.response.json +115 -0
  193. package/generated/v1/schemas/testsinks.create.request.json +76 -0
  194. package/generated/v1/schemas/testsinks.create.response.json +115 -0
  195. package/generated/v1/schemas/testsinks.delete.response.json +115 -0
  196. package/generated/v1/schemas/testsinks.list.query.json +16 -0
  197. package/generated/v1/schemas/testsinks.list.response.json +161 -0
  198. package/generated/v1/schemas/testsinks.read.response.json +115 -0
  199. package/generated/v1/schemas/usage.read.query.json +15 -0
  200. package/generated/v1/schemas/usage.read.response.json +71 -0
  201. package/generated/v1/snippets/attempts.list.json +5 -0
  202. package/generated/v1/snippets/attempts.read.json +5 -0
  203. package/generated/v1/snippets/credentials.create.json +5 -0
  204. package/generated/v1/snippets/credentials.list.json +5 -0
  205. package/generated/v1/snippets/credentials.revoke.json +5 -0
  206. package/generated/v1/snippets/credentials.rotate.json +5 -0
  207. package/generated/v1/snippets/endpoints.create.json +5 -0
  208. package/generated/v1/snippets/endpoints.disable.json +5 -0
  209. package/generated/v1/snippets/endpoints.list.json +5 -0
  210. package/generated/v1/snippets/endpoints.read.json +5 -0
  211. package/generated/v1/snippets/endpoints.rotateSigningKey.json +5 -0
  212. package/generated/v1/snippets/endpoints.update.json +5 -0
  213. package/generated/v1/snippets/endpoints.verify.json +5 -0
  214. package/generated/v1/snippets/forecast.read.json +5 -0
  215. package/generated/v1/snippets/forecast.windows.json +5 -0
  216. package/generated/v1/snippets/grants.list.json +5 -0
  217. package/generated/v1/snippets/grants.revoke.json +5 -0
  218. package/generated/v1/snippets/meta.scaffold.json +5 -0
  219. package/generated/v1/snippets/meta.version.json +5 -0
  220. package/generated/v1/snippets/occurrences.list.json +5 -0
  221. package/generated/v1/snippets/occurrences.read.json +5 -0
  222. package/generated/v1/snippets/occurrences.redeliver.json +5 -0
  223. package/generated/v1/snippets/projects.create.json +5 -0
  224. package/generated/v1/snippets/projects.list.json +5 -0
  225. package/generated/v1/snippets/schedules.archive.json +5 -0
  226. package/generated/v1/snippets/schedules.cancel.json +5 -0
  227. package/generated/v1/snippets/schedules.create.json +5 -0
  228. package/generated/v1/snippets/schedules.list.json +5 -0
  229. package/generated/v1/snippets/schedules.pause.json +5 -0
  230. package/generated/v1/snippets/schedules.read.json +5 -0
  231. package/generated/v1/snippets/schedules.replace.json +5 -0
  232. package/generated/v1/snippets/schedules.resume.json +5 -0
  233. package/generated/v1/snippets/sinkdeliveries.list.json +5 -0
  234. package/generated/v1/snippets/sinkdeliveries.read.json +5 -0
  235. package/generated/v1/snippets/testsinks.configure.json +5 -0
  236. package/generated/v1/snippets/testsinks.create.json +5 -0
  237. package/generated/v1/snippets/testsinks.delete.json +5 -0
  238. package/generated/v1/snippets/testsinks.list.json +5 -0
  239. package/generated/v1/snippets/testsinks.read.json +5 -0
  240. package/generated/v1/snippets/usage.read.json +5 -0
  241. package/package.json +153 -0
@@ -0,0 +1,867 @@
1
+ import { z } from "zod";
2
+ import { CALLBACK_COMPLETION_OUTCOMES, isCallbackRefusalStatus, } from "./delivery.js";
3
+ import { attemptIdSchema, callbackSigningKeyIdSchema, deliveryIdSchema, occurrenceIdSchema, scheduleIdSchema, } from "./identifiers.js";
4
+ import { timestampSchema } from "./time.js";
5
+ /**
6
+ * Callback authenticity.
7
+ *
8
+ * The signature covers the timestamp, the delivery id, and the exact raw
9
+ * request-body bytes. A receiver verifies before parsing, in constant time.
10
+ *
11
+ * This is the most consequential break from the legacy scheduler, which signed
12
+ * `scheduleId.owner.tenant.timestampMs` and never touched the body: a signature
13
+ * that does not cover the body authenticates only the envelope, so an attacker
14
+ * who can replay a captured header set may substitute any payload. W6-03
15
+ * migrates the autoscaler onto this contract through a separate v2 endpoint so
16
+ * the old and new verifiers never weaken each other.
17
+ *
18
+ * The `Callback-*` header namespace is intentionally independent of the display
19
+ * brand. Renaming the product must not force receivers to change verification
20
+ * code.
21
+ */
22
+ export const CALLBACK_HEADERS = {
23
+ deliveryId: "Callback-Delivery-Id",
24
+ keyId: "Callback-Key-Id",
25
+ occurrenceId: "Callback-Occurrence-Id",
26
+ signature: "Callback-Signature",
27
+ timestamp: "Callback-Timestamp",
28
+ };
29
+ /** Only `v1` exists. A new scheme is added alongside, never in place of. */
30
+ export const SIGNATURE_SCHEME = "v1";
31
+ /** How far a receiver's clock may differ before a signature is stale. */
32
+ export const SIGNATURE_TOLERANCE_SECONDS = 300;
33
+ /** Version of the complete delivery-authentication protocol artifact. */
34
+ export const CALLBACK_DELIVERY_PROTOCOL_VERSION = "2026-08-23";
35
+ /** The media type sent on every occurrence delivery. */
36
+ export const CALLBACK_CONTENT_TYPE = "application/json";
37
+ export const signatureHeaderSchema = z
38
+ .string()
39
+ .regex(/^v1=[0-9a-f]{64}$/u, "must be v1= followed by a lowercase hex SHA-256 HMAC");
40
+ /**
41
+ * The exact bytes that are signed: `<timestamp>.<delivery-id>.` followed by the
42
+ * raw body, with no re-serialization anywhere in the path. Re-encoding JSON
43
+ * before signing or verifying is the classic way to break this.
44
+ *
45
+ * Both prefix fields are validated here rather than trusted, and that is a
46
+ * security property, not tidiness. The delimiter `.` also occurs inside every
47
+ * contract timestamp, so without validation the encoding is not injective:
48
+ * `("2026-07-28T15:00:00", "000Z.dlv_…", body)` produces the same bytes as
49
+ * `("2026-07-28T15:00:00.000Z", "dlv_…", body)`, and an attacker holding one
50
+ * legitimate signature could move bytes across the delimiter — shortening the
51
+ * body and lengthening the delivery-id header — and still verify.
52
+ *
53
+ * Validating makes both fields fixed-width, so the prefix is a constant 56
54
+ * bytes and the encoding is injective. A verifier that computes the MAC before
55
+ * validating its headers is the natural implementation and would be
56
+ * vulnerable; making that impossible here means no receiver has to know.
57
+ *
58
+ * **Throws** on a malformed timestamp or delivery id. That is right on the
59
+ * signing side — refusing to sign something we cannot encode unambiguously —
60
+ * but a receiver verifying attacker-controlled headers wants a boolean, not an
61
+ * exception: use `trySigningInput`, or an uncaught throw turns a
62
+ * malformed-header probe into a 500.
63
+ */
64
+ export function signingInput(timestamp, deliveryId, rawBody) {
65
+ const checkedTimestamp = timestampSchema.parse(timestamp);
66
+ const checkedDeliveryId = deliveryIdSchema.parse(deliveryId);
67
+ const prefix = `${checkedTimestamp}.${checkedDeliveryId}.`;
68
+ // Both fields are ASCII by construction, now that both have been validated, so
69
+ // UTF-8 encoding is byte-for-byte their code points and the prefix stays the
70
+ // constant 56 bytes the injectivity argument above depends on.
71
+ const prefixBytes = new TextEncoder().encode(prefix);
72
+ const input = new Uint8Array(prefixBytes.length + rawBody.length);
73
+ input.set(prefixBytes, 0);
74
+ input.set(rawBody, prefixBytes.length);
75
+ return input;
76
+ }
77
+ /**
78
+ * `signingInput` for untrusted input: returns `undefined` instead of throwing
79
+ * when the prefix fields are malformed, which is what a receiver verifying
80
+ * headers off the wire needs.
81
+ */
82
+ export function trySigningInput(timestamp, deliveryId, rawBody) {
83
+ try {
84
+ return signingInput(timestamp, deliveryId, rawBody);
85
+ }
86
+ catch (error) {
87
+ // Only a validation failure means "malformed headers". Anything else — an
88
+ // allocation failure on an oversized body, say — is a real fault and must
89
+ // not be reported to the caller as a bad signature.
90
+ if (error instanceof z.ZodError) {
91
+ return undefined;
92
+ }
93
+ throw error;
94
+ }
95
+ }
96
+ export function formatSignature(hexMac) {
97
+ return `${SIGNATURE_SCHEME}=${hexMac}`;
98
+ }
99
+ /**
100
+ * Whether a signed timestamp is close enough to now. Replay protection beyond
101
+ * this window is the receiver's, keyed on the delivery id.
102
+ */
103
+ export function isTimestampFresh(timestamp, nowMs, toleranceSeconds = SIGNATURE_TOLERANCE_SECONDS) {
104
+ // Validated with the contract schema, not `Date.parse`. A timestamp this
105
+ // service would never emit — `2026-07-28T15:00:00Z`, say — must not be
106
+ // treated as fresh just because it happens to parse.
107
+ if (!timestampSchema.safeParse(timestamp).success) {
108
+ return false;
109
+ }
110
+ return Math.abs(nowMs - Date.parse(timestamp)) <= toleranceSeconds * 1000;
111
+ }
112
+ /**
113
+ * Why this delivery exists.
114
+ *
115
+ * `automatic` is everything the system decided for itself: a first attempt, and
116
+ * every retry after an ambiguous or failed one. Nobody asked for it, it may
117
+ * duplicate work already done, and a receiver must therefore treat it as at
118
+ * most one execution of the occurrence.
119
+ *
120
+ * `redelivery` is a person pressing the button, scoped by
121
+ * `occurrences:redeliver` and named against the retained attempt they chose to
122
+ * follow. It is a *request for the work to happen again*, and a receiver that
123
+ * suppresses it as a duplicate is answering a deliberate instruction with
124
+ * silence.
125
+ *
126
+ * The two were indistinguishable on the wire until this field existed, so the
127
+ * only rule safe against the first was applied to both. That is the defect
128
+ * W3-08 exists to remove, and the reason the field is inside the signed bytes
129
+ * rather than in a header: a receiver that would re-run work on the strength of
130
+ * an unauthenticated hint has handed anyone who can reach its endpoint a
131
+ * duplicate-execution button.
132
+ */
133
+ export const callbackTriggerSchema = z.discriminatedUnion("kind", [
134
+ z.object({ kind: z.literal("automatic") }),
135
+ z.object({
136
+ kind: z.literal("redelivery"),
137
+ sourceAttemptId: attemptIdSchema,
138
+ }),
139
+ ]);
140
+ /**
141
+ * The envelope version this service emits.
142
+ *
143
+ * Dated and independent of the contract version. It moved on 2026-08-08 because
144
+ * the envelope gained `trigger`, and again on 2026-08-23 because `completion`
145
+ * stopped being optional: every delivery now carries a completion URL, a
146
+ * per-dispatch credential and an absolute deadline, whatever the schedule asked
147
+ * for. `assess` classifies both moves as `breaking` — see
148
+ * `CALLBACK_ENVELOPE_VERSION_CHANGE`.
149
+ *
150
+ * **A required field is why this is a version rather than an addition.** Under
151
+ * `2026-08-08` an absent `completion` means "this schedule did not ask for
152
+ * one"; under `2026-08-23` it means the envelope is malformed. A receiver that
153
+ * could not tell those apart would have to treat "no completion material" as
154
+ * both a normal delivery and a broken one.
155
+ */
156
+ export const CALLBACK_ENVELOPE_VERSION = "2026-08-23";
157
+ /** The first published envelope version, which this service no longer emits. */
158
+ export const CALLBACK_ENVELOPE_VERSION_2026_07_01 = "2026-07-01";
159
+ /**
160
+ * The second published version, which this service no longer emits.
161
+ *
162
+ * Still accepted inbound, and its known-answer vectors are still published:
163
+ * a receiver deployed before the sender moved keeps verifying deliveries that
164
+ * were signed under it.
165
+ */
166
+ export const CALLBACK_ENVELOPE_VERSION_2026_08_08 = "2026-08-08";
167
+ /**
168
+ * Every envelope version a receiver may be handed, oldest first.
169
+ *
170
+ * All are accepted on the way in and only the newest is sent, which is what
171
+ * makes a receiver deployable ahead of the sender. A receiver upgraded first
172
+ * keeps verifying the deliveries already in flight; one upgraded afterwards
173
+ * answers `invalid_envelope` to everything, because a version literal it has
174
+ * never heard of is exactly the case the field exists to catch.
175
+ */
176
+ export const CALLBACK_ENVELOPE_VERSIONS = [
177
+ CALLBACK_ENVELOPE_VERSION_2026_07_01,
178
+ CALLBACK_ENVELOPE_VERSION_2026_08_08,
179
+ CALLBACK_ENVELOPE_VERSION,
180
+ ];
181
+ const callbackEnvelopeShape = {
182
+ attempt: z.number().int().positive(),
183
+ /**
184
+ * The customer's payload, delivered verbatim. Deliberately any JSON value:
185
+ * the legacy scheduler accepted arrays and scalars, and narrowing to an
186
+ * object here would make existing payloads undeliverable after migration.
187
+ */
188
+ data: z.unknown(),
189
+ occurrenceId: occurrenceIdSchema,
190
+ plannedFor: timestampSchema,
191
+ scheduleId: scheduleIdSchema,
192
+ type: z.literal("schedule.occurrence"),
193
+ };
194
+ /**
195
+ * Where a `callback` receiver reports that it finished, and with what.
196
+ *
197
+ * **Present only on a delivery whose schedule asked for `completion:
198
+ * "callback"`**, which is what makes adding it to an existing envelope version
199
+ * safe rather than a break. A receiver sees this field for the first time on a
200
+ * delivery its own customer deliberately reconfigured; no receiver that has not
201
+ * opted in can be handed one. That is a different situation from `trigger`,
202
+ * which appeared on *every* delivery and moved the version to `2026-08-08` in
203
+ * consequence.
204
+ *
205
+ * The token authenticates the report and nothing else. It is minted per dispatch
206
+ * and names one attempt at one dispatch epoch, so a report replayed against a
207
+ * later attempt is refused by the same fence that refuses a late executor
208
+ * result.
209
+ */
210
+ export const callbackCompletionSchema = z.object({
211
+ /** Bearer credential for the report. Send it, do not inspect it. */
212
+ token: z.string().min(1),
213
+ /** Absolute URL to POST to when the work is done. */
214
+ url: z.url(),
215
+ });
216
+ /**
217
+ * The completion material every `2026-08-23` delivery carries.
218
+ *
219
+ * The same URL and credential, plus the one thing a receiver could not
220
+ * previously know: **when Greatwait stops waiting.** Without it a receiver
221
+ * deciding whether to accept the work has to infer the deadline from the
222
+ * schedule's configured duration, which it cannot see, and a receiver that
223
+ * reports one second late looks identical to one that never reported at all.
224
+ *
225
+ * `deadline` is the schedule's own allowance measured from this attempt, in
226
+ * the wire's one timestamp representation. It is the maximum: a receiver may
227
+ * ask for an earlier instant in its acknowledgement and may not ask for a
228
+ * later one.
229
+ */
230
+ export const callbackDeferredCompletionSchema = z.object({
231
+ /** When this attempt stops being waited for. Absolute, not a duration. */
232
+ deadline: timestampSchema,
233
+ /** Bearer credential for the report. Send it, do not inspect it. */
234
+ token: z.string().min(1),
235
+ /** Absolute URL to POST to when the work is done. */
236
+ url: z.url(),
237
+ });
238
+ /**
239
+ * The report body, whose vocabulary lives in the Zod-free `delivery` module so
240
+ * the scheduler's completion route can read it without importing Zod.
241
+ */
242
+ export const callbackCompletionReportSchema = z.object({
243
+ outcome: z.enum(CALLBACK_COMPLETION_OUTCOMES).default("succeeded"),
244
+ });
245
+ /**
246
+ * The versioned JSON envelope delivered to a customer endpoint.
247
+ *
248
+ * A union rather than one object with an optional field, and the difference is
249
+ * the whole point of a version: under `2026-07-01` a missing `trigger` means
250
+ * "this sender cannot tell you", and under `2026-08-08` it is a malformed
251
+ * envelope. Collapsing the two would leave a receiver unable to know whether
252
+ * silence about a redelivery is an answer or an absence.
253
+ */
254
+ export const callbackEnvelopeSchema = z.discriminatedUnion("version", [
255
+ z.object({
256
+ ...callbackEnvelopeShape,
257
+ /** Envelope version, dated and independent of the contract version. */
258
+ version: z.literal(CALLBACK_ENVELOPE_VERSION_2026_07_01),
259
+ }),
260
+ z.object({
261
+ ...callbackEnvelopeShape,
262
+ /**
263
+ * Where to report completion. Present only in `callback` mode, absent in
264
+ * every `response` delivery, which is every delivery this service sent
265
+ * before 2026-08-13.
266
+ */
267
+ completion: callbackCompletionSchema.optional(),
268
+ /** Why this delivery exists. Required from `2026-08-08` onward. */
269
+ trigger: callbackTriggerSchema,
270
+ version: z.literal(CALLBACK_ENVELOPE_VERSION_2026_08_08),
271
+ }),
272
+ z.object({
273
+ ...callbackEnvelopeShape,
274
+ /**
275
+ * Where to report completion, its credential, and the deadline.
276
+ *
277
+ * **Required, on every delivery.** That is the whole of this version: a
278
+ * receiver no longer has to have been configured for deferral to be
279
+ * allowed to defer, so the material cannot be conditional on a setting the
280
+ * receiver cannot see.
281
+ */
282
+ completion: callbackDeferredCompletionSchema,
283
+ /** Why this delivery exists. Required from `2026-08-08` onward. */
284
+ trigger: callbackTriggerSchema,
285
+ version: z.literal(CALLBACK_ENVELOPE_VERSION),
286
+ }),
287
+ ]);
288
+ /**
289
+ * What caused a delivery, for an envelope of any published version.
290
+ *
291
+ * A `2026-07-01` envelope carries no trigger and reads as `automatic`, which is
292
+ * the conservative answer rather than a neutral one: it keeps duplicate
293
+ * suppression in force for a sender that cannot say a redelivery was ordered.
294
+ * Every receiver-side decision goes through this function so that default lives
295
+ * in one place instead of being re-decided at each call site.
296
+ */
297
+ export function callbackEnvelopeTrigger(envelope) {
298
+ return "trigger" in envelope ? envelope.trigger : { kind: "automatic" };
299
+ }
300
+ export function callbackRun(envelope) {
301
+ return callbackEnvelopeTrigger(envelope).kind === "redelivery"
302
+ ? { kind: "redelivery", ordinal: envelope.attempt }
303
+ : { kind: "automatic" };
304
+ }
305
+ const textEncoder = new TextEncoder();
306
+ function isJsonValue(value, ancestors) {
307
+ if (value === null ||
308
+ typeof value === "string" ||
309
+ typeof value === "boolean") {
310
+ return true;
311
+ }
312
+ if (typeof value === "number") {
313
+ return Number.isFinite(value);
314
+ }
315
+ if (typeof value !== "object" || ancestors.has(value)) {
316
+ return false;
317
+ }
318
+ if (Array.isArray(value)) {
319
+ ancestors.add(value);
320
+ try {
321
+ return value.every((entry) => isJsonValue(entry, ancestors));
322
+ }
323
+ finally {
324
+ ancestors.delete(value);
325
+ }
326
+ }
327
+ const prototype = Object.getPrototypeOf(value);
328
+ if (prototype !== Object.prototype && prototype !== null) {
329
+ return false;
330
+ }
331
+ ancestors.add(value);
332
+ try {
333
+ return Object.values(value).every((entry) => isJsonValue(entry, ancestors));
334
+ }
335
+ finally {
336
+ ancestors.delete(value);
337
+ }
338
+ }
339
+ /**
340
+ * Serialize one callback envelope exactly once in the frozen wire order.
341
+ *
342
+ * The returned bytes are both the bytes HMAC signs and the bytes `fetch`
343
+ * sends. `data` is checked as JSON before stringification so values such as
344
+ * `undefined`, `NaN`, `bigint`, a `Date`, or a cycle cannot be silently omitted
345
+ * or rewritten into a body different from the caller's input.
346
+ */
347
+ export function serializeCallbackEnvelope(envelope) {
348
+ const checked = callbackEnvelopeSchema.parse(envelope);
349
+ if (!isJsonValue(checked.data, new Set())) {
350
+ throw new TypeError("callback envelope data must be a JSON value");
351
+ }
352
+ const head = {
353
+ version: checked.version,
354
+ type: checked.type,
355
+ scheduleId: checked.scheduleId,
356
+ occurrenceId: checked.occurrenceId,
357
+ plannedFor: checked.plannedFor,
358
+ attempt: checked.attempt,
359
+ };
360
+ if (!("trigger" in checked)) {
361
+ return textEncoder.encode(JSON.stringify({ ...head, data: checked.data }));
362
+ }
363
+ // Rebuilt member by member rather than spread, because these bytes are
364
+ // signed and a parsed object's key order is not a property anyone should
365
+ // have to reason about. `kind` first, `sourceAttemptId` second, and nothing
366
+ // else has a representation here.
367
+ const trigger = checked.trigger.kind === "redelivery"
368
+ ? {
369
+ kind: checked.trigger.kind,
370
+ sourceAttemptId: checked.trigger.sourceAttemptId,
371
+ }
372
+ : { kind: checked.trigger.kind };
373
+ // Before `trigger` in both versions that carry it. The wire order is frozen
374
+ // because these bytes are signed, so a field added later still has one fixed
375
+ // position rather than wherever an object literal put it — and `deadline` is
376
+ // appended inside `completion` for the same reason, so a receiver that
377
+ // already reconstructs the `2026-08-08` object only has to add a key rather
378
+ // than to re-derive an order.
379
+ const completion = checked.version === CALLBACK_ENVELOPE_VERSION
380
+ ? {
381
+ completion: {
382
+ token: checked.completion.token,
383
+ url: checked.completion.url,
384
+ deadline: checked.completion.deadline,
385
+ },
386
+ }
387
+ : checked.completion === undefined
388
+ ? {}
389
+ : {
390
+ completion: {
391
+ token: checked.completion.token,
392
+ url: checked.completion.url,
393
+ },
394
+ };
395
+ return textEncoder.encode(JSON.stringify({ ...head, ...completion, trigger, data: checked.data }));
396
+ }
397
+ /**
398
+ * The version is matched by *shape* rather than against the published list, and
399
+ * that is deliberate.
400
+ *
401
+ * This runs before the body is parsed, on bytes the MAC has already
402
+ * authenticated, and its one job is to find the two identifiers so the
403
+ * occurrence header can be compared without trusting attacker-controlled JSON.
404
+ * Whether the version is one we publish is a question the schema answers a
405
+ * moment later, and answering it *here* would collapse two different refusals
406
+ * into one: a receiver handed an envelope from a newer sender would report
407
+ * `invalid_headers`, sending an integrator to check their signing keys, when
408
+ * what actually happened is that the two ends disagree about the contract.
409
+ * Matching a dated literal keeps the prefix a fixed shape and lets
410
+ * `invalid_envelope` mean what it says.
411
+ */
412
+ const CANONICAL_ENVELOPE_ID_PREFIX = /^\{"version":"\d{4}-\d{2}-\d{2}","type":"schedule\.occurrence","scheduleId":"(sch_[0-9abcdefghjkmnpqrstvwxyz]{26})","occurrenceId":"(occ_[0-9abcdefghjkmnpqrstvwxyz]{26})",/u;
413
+ /**
414
+ * Read the two leading envelope identifiers without parsing attacker-controlled
415
+ * JSON. This lets receiver middleware reject a mutated occurrence header after
416
+ * authenticating the exact bytes but before `JSON.parse` or an application
417
+ * handler sees them.
418
+ */
419
+ export function extractCanonicalEnvelopeIds(rawBody) {
420
+ let decoded;
421
+ try {
422
+ decoded = new TextDecoder("utf-8", {
423
+ fatal: true,
424
+ ignoreBOM: false,
425
+ }).decode(rawBody);
426
+ }
427
+ catch {
428
+ return undefined;
429
+ }
430
+ const match = CANONICAL_ENVELOPE_ID_PREFIX.exec(decoded);
431
+ if (match === null)
432
+ return undefined;
433
+ const scheduleId = scheduleIdSchema.safeParse(match[1]);
434
+ const occurrenceId = occurrenceIdSchema.safeParse(match[2]);
435
+ return scheduleId.success && occurrenceId.success
436
+ ? { occurrenceId: occurrenceId.data, scheduleId: scheduleId.data }
437
+ : undefined;
438
+ }
439
+ export const callbackHeadersSchema = z.object({
440
+ [CALLBACK_HEADERS.deliveryId]: deliveryIdSchema,
441
+ [CALLBACK_HEADERS.keyId]: callbackSigningKeyIdSchema,
442
+ [CALLBACK_HEADERS.occurrenceId]: occurrenceIdSchema,
443
+ [CALLBACK_HEADERS.signature]: signatureHeaderSchema,
444
+ [CALLBACK_HEADERS.timestamp]: timestampSchema,
445
+ });
446
+ const VECTOR_PREFIX = "2026-07-28T15:00:00.000Z.dlv_0123456789abcdefghjkmnpqrs.";
447
+ const CALLBACK_DELIVERY_VECTOR_BODY = '{"version":"2026-08-23","type":"schedule.occurrence","scheduleId":"sch_0123456789abcdefghjkmnpqrs","occurrenceId":"occ_0123456789abcdefghjkmnpqrs","plannedFor":"2026-07-28T15:00:00.000Z","attempt":1,"completion":{"token":"v2.example-claim-not-a-real-token.example-mac","url":"https://greatwait-scheduler.example/callbacks/completion","deadline":"2026-07-28T15:00:05.000Z"},"trigger":{"kind":"automatic"},"data":{}}';
448
+ const CALLBACK_DELIVERY_VECTOR_INPUT = `${VECTOR_PREFIX}${CALLBACK_DELIVERY_VECTOR_BODY}`;
449
+ const CALLBACK_DELIVERY_UNICODE_VECTOR_BODY = '{"version":"2026-08-23","type":"schedule.occurrence","scheduleId":"sch_0123456789abcdefghjkmnpqrs","occurrenceId":"occ_0123456789abcdefghjkmnpqrs","plannedFor":"2026-07-28T15:00:00.000Z","attempt":1,"completion":{"token":"v2.example-claim-not-a-real-token.example-mac","url":"https://greatwait-scheduler.example/callbacks/completion","deadline":"2026-07-28T15:00:05.000Z"},"trigger":{"kind":"automatic"},"data":{"note":"café ☃"}}';
450
+ const CALLBACK_DELIVERY_UNICODE_VECTOR_INPUT = `${VECTOR_PREFIX}${CALLBACK_DELIVERY_UNICODE_VECTOR_BODY}`;
451
+ const CALLBACK_DELIVERY_REDELIVERY_VECTOR_BODY = '{"version":"2026-08-23","type":"schedule.occurrence","scheduleId":"sch_0123456789abcdefghjkmnpqrs","occurrenceId":"occ_0123456789abcdefghjkmnpqrs","plannedFor":"2026-07-28T15:00:00.000Z","attempt":2,"completion":{"token":"v2.example-claim-not-a-real-token.example-mac","url":"https://greatwait-scheduler.example/callbacks/completion","deadline":"2026-07-28T15:00:05.000Z"},"trigger":{"kind":"redelivery","sourceAttemptId":"att_0123456789abcdefghjkmnpqrs"},"data":{}}';
452
+ const CALLBACK_DELIVERY_REDELIVERY_VECTOR_INPUT = `${VECTOR_PREFIX}${CALLBACK_DELIVERY_REDELIVERY_VECTOR_BODY}`;
453
+ /**
454
+ * The `2026-08-08` body with no `completion`, byte for byte as published.
455
+ *
456
+ * Under `2026-08-08` an absent `completion` was the ordinary case and this is
457
+ * its known answer. It is the vector `testVector` used to be, **moved rather
458
+ * than recomputed**: the bytes and the MAC are the ones published on
459
+ * 2026-08-08, so a change to `signingInput` still has to break them. Deleting
460
+ * it instead would have retired the only published answer for the shape a
461
+ * receiver mid-rollout is still being handed.
462
+ */
463
+ const CALLBACK_DELIVERY_TRIGGER_VECTOR_BODY = '{"version":"2026-08-08","type":"schedule.occurrence","scheduleId":"sch_0123456789abcdefghjkmnpqrs","occurrenceId":"occ_0123456789abcdefghjkmnpqrs","plannedFor":"2026-07-28T15:00:00.000Z","attempt":1,"trigger":{"kind":"automatic"},"data":{}}';
464
+ const CALLBACK_DELIVERY_TRIGGER_VECTOR_INPUT = `${VECTOR_PREFIX}${CALLBACK_DELIVERY_TRIGGER_VECTOR_BODY}`;
465
+ const CALLBACK_DELIVERY_COMPLETION_VECTOR_BODY = '{"version":"2026-08-08","type":"schedule.occurrence","scheduleId":"sch_0123456789abcdefghjkmnpqrs","occurrenceId":"occ_0123456789abcdefghjkmnpqrs","plannedFor":"2026-07-28T15:00:00.000Z","attempt":1,"completion":{"token":"v1.example-claim-not-a-real-token.example-mac","url":"https://greatwait-scheduler.example/callbacks/completion"},"trigger":{"kind":"automatic"},"data":{}}';
466
+ const CALLBACK_DELIVERY_COMPLETION_VECTOR_INPUT = `${VECTOR_PREFIX}${CALLBACK_DELIVERY_COMPLETION_VECTOR_BODY}`;
467
+ /**
468
+ * The `2026-07-01` body, byte for byte as it was published, with the MAC it was
469
+ * published with.
470
+ *
471
+ * It is here rather than deleted because these vectors exist to make a change to
472
+ * `signingInput` fail loudly, and recomputing them is exactly the move that
473
+ * would hide one. Keeping the retired envelope's known answer *unchanged* is
474
+ * what separates "the bytes we send are new" from "the algorithm moved": if this
475
+ * MAC ever has to be recomputed, something signed differently and every
476
+ * published verifier is wrong.
477
+ */
478
+ const CALLBACK_DELIVERY_LEGACY_VECTOR_BODY = '{"version":"2026-07-01","type":"schedule.occurrence","scheduleId":"sch_0123456789abcdefghjkmnpqrs","occurrenceId":"occ_0123456789abcdefghjkmnpqrs","plannedFor":"2026-07-28T15:00:00.000Z","attempt":1,"data":{}}';
479
+ const CALLBACK_DELIVERY_LEGACY_VECTOR_INPUT = `${VECTOR_PREFIX}${CALLBACK_DELIVERY_LEGACY_VECTOR_BODY}`;
480
+ /**
481
+ * Language-neutral known-answer contract for one complete delivery request.
482
+ * The secret is synthetic and deliberately not shaped like a credential.
483
+ */
484
+ export const CALLBACK_DELIVERY_PROTOCOL = {
485
+ envelope: {
486
+ /**
487
+ * The frozen wire order, and `completion` sits inside it.
488
+ *
489
+ * **This list is what a receiver outside JavaScript implements against**,
490
+ * so a field that reached the wire without reaching this array would make
491
+ * every callback delivery fail signature verification for exactly the
492
+ * integrators who cannot read the serializer. From `2026-08-23` every
493
+ * delivery carries `completion`; under `2026-08-08` it was optional, and
494
+ * the order was fixed anyway, because a position decided at emit time is a
495
+ * position nobody else can reproduce.
496
+ */
497
+ propertyOrder: [
498
+ "version",
499
+ "type",
500
+ "scheduleId",
501
+ "occurrenceId",
502
+ "plannedFor",
503
+ "attempt",
504
+ "completion",
505
+ "trigger",
506
+ "data",
507
+ ],
508
+ /**
509
+ * The order **inside** `completion`, which is frozen for the same reason.
510
+ *
511
+ * `deadline` is appended rather than inserted, so a receiver that already
512
+ * reconstructs the `2026-08-08` object adds a key instead of re-deriving
513
+ * an order. Under `2026-08-08` the same object is the first two entries
514
+ * and nothing else.
515
+ */
516
+ completionPropertyOrder: ["token", "url", "deadline"],
517
+ /**
518
+ * The order under `2026-08-08`, whose `completion` is optional and carries
519
+ * no `deadline`. The top-level list is the same; what differs is inside.
520
+ */
521
+ propertyOrder_2026_08_08: [
522
+ "version",
523
+ "type",
524
+ "scheduleId",
525
+ "occurrenceId",
526
+ "plannedFor",
527
+ "attempt",
528
+ "completion",
529
+ "trigger",
530
+ "data",
531
+ ],
532
+ /** The order under `2026-07-01`, which carries no trigger. */
533
+ propertyOrder_2026_07_01: [
534
+ "version",
535
+ "type",
536
+ "scheduleId",
537
+ "occurrenceId",
538
+ "plannedFor",
539
+ "attempt",
540
+ "data",
541
+ ],
542
+ serialization: "compact UTF-8 JSON with the stated top-level property order",
543
+ trigger: {
544
+ automatic: "A first attempt or a retry the system chose. Nobody asked for it, it may repeat work already done, and a receiver must run the occurrence at most once.",
545
+ default: "An envelope with no trigger field is automatic. Only 2026-07-01 can omit it.",
546
+ propertyOrder: ["kind", "sourceAttemptId"],
547
+ ordinal: "attempt. It only ever increases within an occurrence and every copy of one redelivery carries the same value, so a receiver recognises a repeat by keeping the highest it has run to completion rather than by keeping them all.",
548
+ redelivery: "A person ordered this delivery under the occurrences:redeliver scope. sourceAttemptId names the retained attempt they chose to follow and is there to show them, not to decide whether the work runs — attempt is what decides that. It is a request for the work to happen again, and a receiver that suppresses it as a duplicate answers a deliberate instruction with silence.",
549
+ requirement: "run the handler again for a redelivery whose attempt is above the highest this receiver has run to completion for the occurrence, keep suppressing every automatic delivery of a completed occurrence, and suppress every redelivery at or below that high-water mark",
550
+ },
551
+ version: CALLBACK_ENVELOPE_VERSION,
552
+ /**
553
+ * Every version a receiver may be handed. A sender emits only the last;
554
+ * accepting the whole list is what lets a receiver deploy first.
555
+ */
556
+ versions: CALLBACK_ENVELOPE_VERSIONS,
557
+ },
558
+ headers: {
559
+ contentType: CALLBACK_CONTENT_TYPE,
560
+ required: [
561
+ "Content-Type",
562
+ CALLBACK_HEADERS.deliveryId,
563
+ CALLBACK_HEADERS.occurrenceId,
564
+ CALLBACK_HEADERS.timestamp,
565
+ CALLBACK_HEADERS.keyId,
566
+ CALLBACK_HEADERS.signature,
567
+ ],
568
+ },
569
+ replay: {
570
+ identity: CALLBACK_HEADERS.deliveryId,
571
+ requirement: "atomically reject a previously accepted delivery id before parsing",
572
+ },
573
+ signature: {
574
+ algorithm: "HMAC-SHA-256",
575
+ format: "v1=<64 lowercase hexadecimal characters>",
576
+ input: "ASCII <Callback-Timestamp>.<Callback-Delivery-Id>. followed by the exact raw request body bytes",
577
+ scheme: SIGNATURE_SCHEME,
578
+ timestampToleranceSeconds: SIGNATURE_TOLERANCE_SECONDS,
579
+ },
580
+ /**
581
+ * The retired `2026-07-01` envelope, kept as a known answer.
582
+ *
583
+ * A receiver is still held to verifying it, because a sender mid-rollout may
584
+ * have signed it before the version moved. Its MAC is the one published on
585
+ * 2026-07-28 and is deliberately not recomputed.
586
+ */
587
+ legacyTestVector: {
588
+ envelope: {
589
+ attempt: 1,
590
+ data: {},
591
+ occurrenceId: "occ_0123456789abcdefghjkmnpqrs",
592
+ plannedFor: "2026-07-28T15:00:00.000Z",
593
+ scheduleId: "sch_0123456789abcdefghjkmnpqrs",
594
+ type: "schedule.occurrence",
595
+ version: CALLBACK_ENVELOPE_VERSION_2026_07_01,
596
+ },
597
+ exactBodyBase64: "eyJ2ZXJzaW9uIjoiMjAyNi0wNy0wMSIsInR5cGUiOiJzY2hlZHVsZS5vY2N1cnJlbmNlIiwic2NoZWR1bGVJZCI6InNjaF8wMTIzNDU2Nzg5YWJjZGVmZ2hqa21ucHFycyIsIm9jY3VycmVuY2VJZCI6Im9jY18wMTIzNDU2Nzg5YWJjZGVmZ2hqa21ucHFycyIsInBsYW5uZWRGb3IiOiIyMDI2LTA3LTI4VDE1OjAwOjAwLjAwMFoiLCJhdHRlbXB0IjoxLCJkYXRhIjp7fX0=",
598
+ exactBodyUtf8: CALLBACK_DELIVERY_LEGACY_VECTOR_BODY,
599
+ headers: {
600
+ "Callback-Delivery-Id": "dlv_0123456789abcdefghjkmnpqrs",
601
+ "Callback-Key-Id": "csk_0123456789abcdefghjkmnpqrs",
602
+ "Callback-Occurrence-Id": "occ_0123456789abcdefghjkmnpqrs",
603
+ "Callback-Signature": "v1=11135fdfc0bc5d49ebcc9d3a0d8df1116179097cefe961c6eca281339e5cfab3",
604
+ "Callback-Timestamp": "2026-07-28T15:00:00.000Z",
605
+ "Content-Type": CALLBACK_CONTENT_TYPE,
606
+ },
607
+ secret: "greatwait-signature-vector-key-not-a-credential",
608
+ signingInputBase64: "MjAyNi0wNy0yOFQxNTowMDowMC4wMDBaLmRsdl8wMTIzNDU2Nzg5YWJjZGVmZ2hqa21ucHFycy57InZlcnNpb24iOiIyMDI2LTA3LTAxIiwidHlwZSI6InNjaGVkdWxlLm9jY3VycmVuY2UiLCJzY2hlZHVsZUlkIjoic2NoXzAxMjM0NTY3ODlhYmNkZWZnaGprbW5wcXJzIiwib2NjdXJyZW5jZUlkIjoib2NjXzAxMjM0NTY3ODlhYmNkZWZnaGprbW5wcXJzIiwicGxhbm5lZEZvciI6IjIwMjYtMDctMjhUMTU6MDA6MDAuMDAwWiIsImF0dGVtcHQiOjEsImRhdGEiOnt9fQ==",
609
+ signingInputUtf8: CALLBACK_DELIVERY_LEGACY_VECTOR_INPUT,
610
+ },
611
+ /**
612
+ * The retired `2026-08-08` envelope **without** `completion`, kept as a known
613
+ * answer.
614
+ *
615
+ * It is the vector `testVector` was until 2026-08-23, moved here rather than
616
+ * recomputed. Under `2026-08-08` an absent `completion` was the ordinary
617
+ * case, so this is the shape most deliveries signed under that version had,
618
+ * and a receiver still verifying them needs its published answer to stay put.
619
+ */
620
+ triggerTestVector: {
621
+ envelope: {
622
+ attempt: 1,
623
+ data: {},
624
+ occurrenceId: "occ_0123456789abcdefghjkmnpqrs",
625
+ plannedFor: "2026-07-28T15:00:00.000Z",
626
+ scheduleId: "sch_0123456789abcdefghjkmnpqrs",
627
+ trigger: { kind: "automatic" },
628
+ type: "schedule.occurrence",
629
+ version: CALLBACK_ENVELOPE_VERSION_2026_08_08,
630
+ },
631
+ exactBodyBase64: "eyJ2ZXJzaW9uIjoiMjAyNi0wOC0wOCIsInR5cGUiOiJzY2hlZHVsZS5vY2N1cnJlbmNlIiwic2NoZWR1bGVJZCI6InNjaF8wMTIzNDU2Nzg5YWJjZGVmZ2hqa21ucHFycyIsIm9jY3VycmVuY2VJZCI6Im9jY18wMTIzNDU2Nzg5YWJjZGVmZ2hqa21ucHFycyIsInBsYW5uZWRGb3IiOiIyMDI2LTA3LTI4VDE1OjAwOjAwLjAwMFoiLCJhdHRlbXB0IjoxLCJ0cmlnZ2VyIjp7ImtpbmQiOiJhdXRvbWF0aWMifSwiZGF0YSI6e319",
632
+ exactBodyUtf8: CALLBACK_DELIVERY_TRIGGER_VECTOR_BODY,
633
+ headers: {
634
+ "Callback-Delivery-Id": "dlv_0123456789abcdefghjkmnpqrs",
635
+ "Callback-Key-Id": "csk_0123456789abcdefghjkmnpqrs",
636
+ "Callback-Occurrence-Id": "occ_0123456789abcdefghjkmnpqrs",
637
+ "Callback-Signature": "v1=bd96c6d68a5c0521c18600d92913ce4dcf32036f2c930d0f9db9de55da9280e7",
638
+ "Callback-Timestamp": "2026-07-28T15:00:00.000Z",
639
+ "Content-Type": CALLBACK_CONTENT_TYPE,
640
+ },
641
+ secret: "greatwait-signature-vector-key-not-a-credential",
642
+ signingInputBase64: "MjAyNi0wNy0yOFQxNTowMDowMC4wMDBaLmRsdl8wMTIzNDU2Nzg5YWJjZGVmZ2hqa21ucHFycy57InZlcnNpb24iOiIyMDI2LTA4LTA4IiwidHlwZSI6InNjaGVkdWxlLm9jY3VycmVuY2UiLCJzY2hlZHVsZUlkIjoic2NoXzAxMjM0NTY3ODlhYmNkZWZnaGprbW5wcXJzIiwib2NjdXJyZW5jZUlkIjoib2NjXzAxMjM0NTY3ODlhYmNkZWZnaGprbW5wcXJzIiwicGxhbm5lZEZvciI6IjIwMjYtMDctMjhUMTU6MDA6MDAuMDAwWiIsImF0dGVtcHQiOjEsInRyaWdnZXIiOnsia2luZCI6ImF1dG9tYXRpYyJ9LCJkYXRhIjp7fX0=",
643
+ signingInputUtf8: CALLBACK_DELIVERY_TRIGGER_VECTOR_INPUT,
644
+ },
645
+ /**
646
+ * The retired `2026-08-08` envelope **with** `completion`, kept as a known
647
+ * answer.
648
+ *
649
+ * Under that version the field was optional and appeared only on a schedule
650
+ * configured for `callback` completion, and it carried no `deadline`. This is
651
+ * the published answer for that shape, byte for byte and MAC unchanged: a
652
+ * receiver mid-rollout is still handed it, and recomputing this vector is
653
+ * exactly the move that would hide a change to `signingInput`.
654
+ *
655
+ * The token is synthetic and shaped like ours without being one. A real token
656
+ * names an attempt at a dispatch epoch and expires; this is a vector.
657
+ */
658
+ completionTestVector: {
659
+ envelope: {
660
+ attempt: 1,
661
+ completion: {
662
+ token: "v1.example-claim-not-a-real-token.example-mac",
663
+ url: "https://greatwait-scheduler.example/callbacks/completion",
664
+ },
665
+ data: {},
666
+ occurrenceId: "occ_0123456789abcdefghjkmnpqrs",
667
+ plannedFor: "2026-07-28T15:00:00.000Z",
668
+ scheduleId: "sch_0123456789abcdefghjkmnpqrs",
669
+ trigger: { kind: "automatic" },
670
+ type: "schedule.occurrence",
671
+ version: CALLBACK_ENVELOPE_VERSION_2026_08_08,
672
+ },
673
+ exactBodyBase64: "eyJ2ZXJzaW9uIjoiMjAyNi0wOC0wOCIsInR5cGUiOiJzY2hlZHVsZS5vY2N1cnJlbmNlIiwic2NoZWR1bGVJZCI6InNjaF8wMTIzNDU2Nzg5YWJjZGVmZ2hqa21ucHFycyIsIm9jY3VycmVuY2VJZCI6Im9jY18wMTIzNDU2Nzg5YWJjZGVmZ2hqa21ucHFycyIsInBsYW5uZWRGb3IiOiIyMDI2LTA3LTI4VDE1OjAwOjAwLjAwMFoiLCJhdHRlbXB0IjoxLCJjb21wbGV0aW9uIjp7InRva2VuIjoidjEuZXhhbXBsZS1jbGFpbS1ub3QtYS1yZWFsLXRva2VuLmV4YW1wbGUtbWFjIiwidXJsIjoiaHR0cHM6Ly9ncmVhdHdhaXQtc2NoZWR1bGVyLmV4YW1wbGUvY2FsbGJhY2tzL2NvbXBsZXRpb24ifSwidHJpZ2dlciI6eyJraW5kIjoiYXV0b21hdGljIn0sImRhdGEiOnt9fQ==",
674
+ exactBodyUtf8: CALLBACK_DELIVERY_COMPLETION_VECTOR_BODY,
675
+ headers: {
676
+ "Callback-Delivery-Id": "dlv_0123456789abcdefghjkmnpqrs",
677
+ "Callback-Key-Id": "csk_0123456789abcdefghjkmnpqrs",
678
+ "Callback-Occurrence-Id": "occ_0123456789abcdefghjkmnpqrs",
679
+ "Callback-Signature": "v1=38c6d854051087ed408a5f6252d90375859f305aeb0bca1c3f3a5bb63135e138",
680
+ "Callback-Timestamp": "2026-07-28T15:00:00.000Z",
681
+ "Content-Type": CALLBACK_CONTENT_TYPE,
682
+ },
683
+ secret: "greatwait-signature-vector-key-not-a-credential",
684
+ signingInputBase64: "MjAyNi0wNy0yOFQxNTowMDowMC4wMDBaLmRsdl8wMTIzNDU2Nzg5YWJjZGVmZ2hqa21ucHFycy57InZlcnNpb24iOiIyMDI2LTA4LTA4IiwidHlwZSI6InNjaGVkdWxlLm9jY3VycmVuY2UiLCJzY2hlZHVsZUlkIjoic2NoXzAxMjM0NTY3ODlhYmNkZWZnaGprbW5wcXJzIiwib2NjdXJyZW5jZUlkIjoib2NjXzAxMjM0NTY3ODlhYmNkZWZnaGprbW5wcXJzIiwicGxhbm5lZEZvciI6IjIwMjYtMDctMjhUMTU6MDA6MDAuMDAwWiIsImF0dGVtcHQiOjEsImNvbXBsZXRpb24iOnsidG9rZW4iOiJ2MS5leGFtcGxlLWNsYWltLW5vdC1hLXJlYWwtdG9rZW4uZXhhbXBsZS1tYWMiLCJ1cmwiOiJodHRwczovL2dyZWF0d2FpdC1zY2hlZHVsZXIuZXhhbXBsZS9jYWxsYmFja3MvY29tcGxldGlvbiJ9LCJ0cmlnZ2VyIjp7ImtpbmQiOiJhdXRvbWF0aWMifSwiZGF0YSI6e319",
685
+ signingInputUtf8: CALLBACK_DELIVERY_COMPLETION_VECTOR_INPUT,
686
+ },
687
+ /**
688
+ * A redelivery, which is the only vector that discriminates this change.
689
+ *
690
+ * The two automatic vectors would still pass an implementation that dropped
691
+ * `trigger` on the floor and hard-coded `{"kind":"automatic"}`; this one
692
+ * cannot, because both the kind and the source attempt are inside the signed
693
+ * bytes.
694
+ */
695
+ redeliveryTestVector: {
696
+ envelope: {
697
+ attempt: 2,
698
+ completion: {
699
+ deadline: "2026-07-28T15:00:05.000Z",
700
+ token: "v2.example-claim-not-a-real-token.example-mac",
701
+ url: "https://greatwait-scheduler.example/callbacks/completion",
702
+ },
703
+ data: {},
704
+ occurrenceId: "occ_0123456789abcdefghjkmnpqrs",
705
+ plannedFor: "2026-07-28T15:00:00.000Z",
706
+ scheduleId: "sch_0123456789abcdefghjkmnpqrs",
707
+ trigger: {
708
+ kind: "redelivery",
709
+ sourceAttemptId: "att_0123456789abcdefghjkmnpqrs",
710
+ },
711
+ type: "schedule.occurrence",
712
+ version: CALLBACK_ENVELOPE_VERSION,
713
+ },
714
+ exactBodyBase64: "eyJ2ZXJzaW9uIjoiMjAyNi0wOC0yMyIsInR5cGUiOiJzY2hlZHVsZS5vY2N1cnJlbmNlIiwic2NoZWR1bGVJZCI6InNjaF8wMTIzNDU2Nzg5YWJjZGVmZ2hqa21ucHFycyIsIm9jY3VycmVuY2VJZCI6Im9jY18wMTIzNDU2Nzg5YWJjZGVmZ2hqa21ucHFycyIsInBsYW5uZWRGb3IiOiIyMDI2LTA3LTI4VDE1OjAwOjAwLjAwMFoiLCJhdHRlbXB0IjoyLCJjb21wbGV0aW9uIjp7InRva2VuIjoidjIuZXhhbXBsZS1jbGFpbS1ub3QtYS1yZWFsLXRva2VuLmV4YW1wbGUtbWFjIiwidXJsIjoiaHR0cHM6Ly9ncmVhdHdhaXQtc2NoZWR1bGVyLmV4YW1wbGUvY2FsbGJhY2tzL2NvbXBsZXRpb24iLCJkZWFkbGluZSI6IjIwMjYtMDctMjhUMTU6MDA6MDUuMDAwWiJ9LCJ0cmlnZ2VyIjp7ImtpbmQiOiJyZWRlbGl2ZXJ5Iiwic291cmNlQXR0ZW1wdElkIjoiYXR0XzAxMjM0NTY3ODlhYmNkZWZnaGprbW5wcXJzIn0sImRhdGEiOnt9fQ==",
715
+ exactBodyUtf8: CALLBACK_DELIVERY_REDELIVERY_VECTOR_BODY,
716
+ headers: {
717
+ "Callback-Delivery-Id": "dlv_0123456789abcdefghjkmnpqrs",
718
+ "Callback-Key-Id": "csk_0123456789abcdefghjkmnpqrs",
719
+ "Callback-Occurrence-Id": "occ_0123456789abcdefghjkmnpqrs",
720
+ "Callback-Signature": "v1=9d698aed6d05dcd964f00d6e6796aab238f4f50e6f5cfeb61751fd7850778a04",
721
+ "Callback-Timestamp": "2026-07-28T15:00:00.000Z",
722
+ "Content-Type": CALLBACK_CONTENT_TYPE,
723
+ },
724
+ secret: "greatwait-signature-vector-key-not-a-credential",
725
+ signingInputBase64: "MjAyNi0wNy0yOFQxNTowMDowMC4wMDBaLmRsdl8wMTIzNDU2Nzg5YWJjZGVmZ2hqa21ucHFycy57InZlcnNpb24iOiIyMDI2LTA4LTIzIiwidHlwZSI6InNjaGVkdWxlLm9jY3VycmVuY2UiLCJzY2hlZHVsZUlkIjoic2NoXzAxMjM0NTY3ODlhYmNkZWZnaGprbW5wcXJzIiwib2NjdXJyZW5jZUlkIjoib2NjXzAxMjM0NTY3ODlhYmNkZWZnaGprbW5wcXJzIiwicGxhbm5lZEZvciI6IjIwMjYtMDctMjhUMTU6MDA6MDAuMDAwWiIsImF0dGVtcHQiOjIsImNvbXBsZXRpb24iOnsidG9rZW4iOiJ2Mi5leGFtcGxlLWNsYWltLW5vdC1hLXJlYWwtdG9rZW4uZXhhbXBsZS1tYWMiLCJ1cmwiOiJodHRwczovL2dyZWF0d2FpdC1zY2hlZHVsZXIuZXhhbXBsZS9jYWxsYmFja3MvY29tcGxldGlvbiIsImRlYWRsaW5lIjoiMjAyNi0wNy0yOFQxNTowMDowNS4wMDBaIn0sInRyaWdnZXIiOnsia2luZCI6InJlZGVsaXZlcnkiLCJzb3VyY2VBdHRlbXB0SWQiOiJhdHRfMDEyMzQ1Njc4OWFiY2RlZmdoamttbnBxcnMifSwiZGF0YSI6e319",
726
+ signingInputUtf8: CALLBACK_DELIVERY_REDELIVERY_VECTOR_INPUT,
727
+ },
728
+ /**
729
+ * The ordinary current-version delivery: automatic, and carrying completion
730
+ * material because every `2026-08-23` delivery does.
731
+ */
732
+ testVector: {
733
+ envelope: {
734
+ attempt: 1,
735
+ completion: {
736
+ deadline: "2026-07-28T15:00:05.000Z",
737
+ token: "v2.example-claim-not-a-real-token.example-mac",
738
+ url: "https://greatwait-scheduler.example/callbacks/completion",
739
+ },
740
+ data: {},
741
+ occurrenceId: "occ_0123456789abcdefghjkmnpqrs",
742
+ plannedFor: "2026-07-28T15:00:00.000Z",
743
+ scheduleId: "sch_0123456789abcdefghjkmnpqrs",
744
+ trigger: { kind: "automatic" },
745
+ type: "schedule.occurrence",
746
+ version: CALLBACK_ENVELOPE_VERSION,
747
+ },
748
+ exactBodyBase64: "eyJ2ZXJzaW9uIjoiMjAyNi0wOC0yMyIsInR5cGUiOiJzY2hlZHVsZS5vY2N1cnJlbmNlIiwic2NoZWR1bGVJZCI6InNjaF8wMTIzNDU2Nzg5YWJjZGVmZ2hqa21ucHFycyIsIm9jY3VycmVuY2VJZCI6Im9jY18wMTIzNDU2Nzg5YWJjZGVmZ2hqa21ucHFycyIsInBsYW5uZWRGb3IiOiIyMDI2LTA3LTI4VDE1OjAwOjAwLjAwMFoiLCJhdHRlbXB0IjoxLCJjb21wbGV0aW9uIjp7InRva2VuIjoidjIuZXhhbXBsZS1jbGFpbS1ub3QtYS1yZWFsLXRva2VuLmV4YW1wbGUtbWFjIiwidXJsIjoiaHR0cHM6Ly9ncmVhdHdhaXQtc2NoZWR1bGVyLmV4YW1wbGUvY2FsbGJhY2tzL2NvbXBsZXRpb24iLCJkZWFkbGluZSI6IjIwMjYtMDctMjhUMTU6MDA6MDUuMDAwWiJ9LCJ0cmlnZ2VyIjp7ImtpbmQiOiJhdXRvbWF0aWMifSwiZGF0YSI6e319",
749
+ exactBodyUtf8: CALLBACK_DELIVERY_VECTOR_BODY,
750
+ headers: {
751
+ "Callback-Delivery-Id": "dlv_0123456789abcdefghjkmnpqrs",
752
+ "Callback-Key-Id": "csk_0123456789abcdefghjkmnpqrs",
753
+ "Callback-Occurrence-Id": "occ_0123456789abcdefghjkmnpqrs",
754
+ "Callback-Signature": "v1=5bf68b786fbac1afa7e3474ae5039ad27900b0185bca43a3e45185d56315a312",
755
+ "Callback-Timestamp": "2026-07-28T15:00:00.000Z",
756
+ "Content-Type": CALLBACK_CONTENT_TYPE,
757
+ },
758
+ secret: "greatwait-signature-vector-key-not-a-credential",
759
+ signingInputBase64: "MjAyNi0wNy0yOFQxNTowMDowMC4wMDBaLmRsdl8wMTIzNDU2Nzg5YWJjZGVmZ2hqa21ucHFycy57InZlcnNpb24iOiIyMDI2LTA4LTIzIiwidHlwZSI6InNjaGVkdWxlLm9jY3VycmVuY2UiLCJzY2hlZHVsZUlkIjoic2NoXzAxMjM0NTY3ODlhYmNkZWZnaGprbW5wcXJzIiwib2NjdXJyZW5jZUlkIjoib2NjXzAxMjM0NTY3ODlhYmNkZWZnaGprbW5wcXJzIiwicGxhbm5lZEZvciI6IjIwMjYtMDctMjhUMTU6MDA6MDAuMDAwWiIsImF0dGVtcHQiOjEsImNvbXBsZXRpb24iOnsidG9rZW4iOiJ2Mi5leGFtcGxlLWNsYWltLW5vdC1hLXJlYWwtdG9rZW4uZXhhbXBsZS1tYWMiLCJ1cmwiOiJodHRwczovL2dyZWF0d2FpdC1zY2hlZHVsZXIuZXhhbXBsZS9jYWxsYmFja3MvY29tcGxldGlvbiIsImRlYWRsaW5lIjoiMjAyNi0wNy0yOFQxNTowMDowNS4wMDBaIn0sInRyaWdnZXIiOnsia2luZCI6ImF1dG9tYXRpYyJ9LCJkYXRhIjp7fX0=",
760
+ signingInputUtf8: CALLBACK_DELIVERY_VECTOR_INPUT,
761
+ },
762
+ unicodeTestVector: {
763
+ envelope: {
764
+ attempt: 1,
765
+ completion: {
766
+ deadline: "2026-07-28T15:00:05.000Z",
767
+ token: "v2.example-claim-not-a-real-token.example-mac",
768
+ url: "https://greatwait-scheduler.example/callbacks/completion",
769
+ },
770
+ data: { note: "café ☃" },
771
+ occurrenceId: "occ_0123456789abcdefghjkmnpqrs",
772
+ plannedFor: "2026-07-28T15:00:00.000Z",
773
+ scheduleId: "sch_0123456789abcdefghjkmnpqrs",
774
+ trigger: { kind: "automatic" },
775
+ type: "schedule.occurrence",
776
+ version: CALLBACK_ENVELOPE_VERSION,
777
+ },
778
+ exactBodyBase64: "eyJ2ZXJzaW9uIjoiMjAyNi0wOC0yMyIsInR5cGUiOiJzY2hlZHVsZS5vY2N1cnJlbmNlIiwic2NoZWR1bGVJZCI6InNjaF8wMTIzNDU2Nzg5YWJjZGVmZ2hqa21ucHFycyIsIm9jY3VycmVuY2VJZCI6Im9jY18wMTIzNDU2Nzg5YWJjZGVmZ2hqa21ucHFycyIsInBsYW5uZWRGb3IiOiIyMDI2LTA3LTI4VDE1OjAwOjAwLjAwMFoiLCJhdHRlbXB0IjoxLCJjb21wbGV0aW9uIjp7InRva2VuIjoidjIuZXhhbXBsZS1jbGFpbS1ub3QtYS1yZWFsLXRva2VuLmV4YW1wbGUtbWFjIiwidXJsIjoiaHR0cHM6Ly9ncmVhdHdhaXQtc2NoZWR1bGVyLmV4YW1wbGUvY2FsbGJhY2tzL2NvbXBsZXRpb24iLCJkZWFkbGluZSI6IjIwMjYtMDctMjhUMTU6MDA6MDUuMDAwWiJ9LCJ0cmlnZ2VyIjp7ImtpbmQiOiJhdXRvbWF0aWMifSwiZGF0YSI6eyJub3RlIjoiY2Fmw6kg4piDIn19",
779
+ exactBodyUtf8: CALLBACK_DELIVERY_UNICODE_VECTOR_BODY,
780
+ headers: {
781
+ "Callback-Delivery-Id": "dlv_0123456789abcdefghjkmnpqrs",
782
+ "Callback-Key-Id": "csk_0123456789abcdefghjkmnpqrs",
783
+ "Callback-Occurrence-Id": "occ_0123456789abcdefghjkmnpqrs",
784
+ "Callback-Signature": "v1=18a8d1c50ea54bd0a5ec27abe80d1c98e44a7f358ecf8f06615c174075100e72",
785
+ "Callback-Timestamp": "2026-07-28T15:00:00.000Z",
786
+ "Content-Type": CALLBACK_CONTENT_TYPE,
787
+ },
788
+ secret: "greatwait-signature-vector-key-not-a-credential",
789
+ signingInputBase64: "MjAyNi0wNy0yOFQxNTowMDowMC4wMDBaLmRsdl8wMTIzNDU2Nzg5YWJjZGVmZ2hqa21ucHFycy57InZlcnNpb24iOiIyMDI2LTA4LTIzIiwidHlwZSI6InNjaGVkdWxlLm9jY3VycmVuY2UiLCJzY2hlZHVsZUlkIjoic2NoXzAxMjM0NTY3ODlhYmNkZWZnaGprbW5wcXJzIiwib2NjdXJyZW5jZUlkIjoib2NjXzAxMjM0NTY3ODlhYmNkZWZnaGprbW5wcXJzIiwicGxhbm5lZEZvciI6IjIwMjYtMDctMjhUMTU6MDA6MDAuMDAwWiIsImF0dGVtcHQiOjEsImNvbXBsZXRpb24iOnsidG9rZW4iOiJ2Mi5leGFtcGxlLWNsYWltLW5vdC1hLXJlYWwtdG9rZW4uZXhhbXBsZS1tYWMiLCJ1cmwiOiJodHRwczovL2dyZWF0d2FpdC1zY2hlZHVsZXIuZXhhbXBsZS9jYWxsYmFja3MvY29tcGxldGlvbiIsImRlYWRsaW5lIjoiMjAyNi0wNy0yOFQxNTowMDowNS4wMDBaIn0sInRyaWdnZXIiOnsia2luZCI6ImF1dG9tYXRpYyJ9LCJkYXRhIjp7Im5vdGUiOiJjYWbDqSDimIMifX0=",
790
+ signingInputUtf8: CALLBACK_DELIVERY_UNICODE_VECTOR_INPUT,
791
+ },
792
+ version: CALLBACK_DELIVERY_PROTOCOL_VERSION,
793
+ };
794
+ /**
795
+ * Every published vector, derived from the protocol rather than listed.
796
+ *
797
+ * **A hand-written list is the failure this exists to prevent.** The duplicated
798
+ * serializer in `@greatwait/delivery` is held byte-for-byte against these, and
799
+ * the one time it drifted, it drifted on the field whose vector nobody had
800
+ * added to the list yet. Reading the protocol object means a vector added later
801
+ * is driven by every suite that already iterates this, with no second edit.
802
+ *
803
+ * **Matched by shape rather than by name**, which is not fastidiousness: the
804
+ * first version of this matched keys ending in `TestVector` and silently
805
+ * dropped `testVector`, whose lower-case first letter makes that suffix test
806
+ * false. A filter that quietly returns one fewer than it should is the same
807
+ * failure as the hand-written list, arrived at differently.
808
+ */
809
+ export function callbackDeliveryTestVectors() {
810
+ return Object.entries(CALLBACK_DELIVERY_PROTOCOL)
811
+ .filter(([, value]) => typeof value === "object" &&
812
+ value !== null &&
813
+ typeof value.exactBodyUtf8 === "string")
814
+ .map(([name, vector]) => ({
815
+ ...vector,
816
+ name,
817
+ }));
818
+ }
819
+ /**
820
+ * What moving the envelope to `2026-08-08` is, in the vocabulary the
821
+ * compatibility module classifies.
822
+ *
823
+ * Recorded as data rather than asserted in prose because the verdict is an
824
+ * output: `assess(CALLBACK_ENVELOPE_VERSION_CHANGE)` returns
825
+ * `{ kind: "breaking", requiresNewContractVersion: true }`, and a fixture holds
826
+ * it there. "It only added a field, so it is additive" is the reading this
827
+ * exists to refuse — what a client parses is not the field, it is the version
828
+ * literal, and that literal *changed*.
829
+ *
830
+ * The contract version does not move with it. `CONTRACT_VERSION` versions the
831
+ * REST surface; the envelope carries its own dated version precisely so a break
832
+ * in the callback body does not force every API client to re-pin.
833
+ */
834
+ export const CALLBACK_ENVELOPE_VERSION_CHANGE = {
835
+ operation: "changed",
836
+ subject: "response-field",
837
+ };
838
+ /**
839
+ * Classifies a status code alone. Use `classifyOutcome` where a request may not
840
+ * complete.
841
+ *
842
+ * **`isCallbackRefusalStatus` is consulted rather than duplicated here.** This
843
+ * function is the published statement of what a receiver's answer means and
844
+ * `performGreatwaitAttempt` is what acts on it; before FXO-1596 the two carried
845
+ * the same status boundary as two independent literals, kept equal by hand.
846
+ * They are now one list, so a status cannot be retried by the executor while
847
+ * this function tells the customer it was terminal.
848
+ */
849
+ export function classifyStatus(status) {
850
+ if (status >= 200 && status < 300) {
851
+ return "success";
852
+ }
853
+ if (status === 408 || status === 429 || status >= 500) {
854
+ return "retryable";
855
+ }
856
+ // A refusal is retryable to a receiver author for the same reason it is to
857
+ // the executor: the cause is theirs to fix, and the delivery that follows the
858
+ // fix carries the identical bytes.
859
+ return isCallbackRefusalStatus(status) ? "retryable" : "terminal";
860
+ }
861
+ export function classifyOutcome(outcome) {
862
+ // A lost response is ambiguous, never terminal. Treating it as a customer
863
+ // refusal would drop the occurrence without ever retrying it.
864
+ return outcome.kind === "no-response"
865
+ ? "ambiguous"
866
+ : classifyStatus(outcome.status);
867
+ }