@greatwait/contracts 0.1.0-alpha.3 → 0.1.0-alpha.5

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 (345) hide show
  1. package/README.md +12 -10
  2. package/dist/authority.d.ts +33 -27
  3. package/dist/authority.js +12 -2
  4. package/dist/billing.d.ts +1231 -0
  5. package/dist/billing.js +833 -0
  6. package/dist/callback-safety.d.ts +2 -2
  7. package/dist/callback-safety.js +15 -5
  8. package/dist/compatibility.d.ts +3 -3
  9. package/dist/compatibility.js +1 -1
  10. package/dist/configuration-bounds.d.ts +13 -0
  11. package/dist/configuration-bounds.js +13 -0
  12. package/dist/data-export.d.ts +67 -0
  13. package/dist/data-export.js +182 -0
  14. package/dist/effective-limits.d.ts +40 -0
  15. package/dist/effective-limits.js +42 -0
  16. package/dist/endpoints.d.ts +186 -245
  17. package/dist/endpoints.js +82 -86
  18. package/dist/events.d.ts +17 -17
  19. package/dist/events.js +1 -1
  20. package/dist/first-value-journey.d.ts +50 -56
  21. package/dist/first-value-journey.js +17 -20
  22. package/dist/forecast.d.ts +182 -92
  23. package/dist/forecast.js +88 -25
  24. package/dist/identifiers.d.ts +20 -20
  25. package/dist/identifiers.js +1 -1
  26. package/dist/identity.d.ts +41 -37
  27. package/dist/identity.js +5 -4
  28. package/dist/index.d.ts +12 -6
  29. package/dist/index.js +12 -6
  30. package/dist/mcp.d.ts +21 -19
  31. package/dist/mcp.js +2 -2
  32. package/dist/naming.d.ts +45 -0
  33. package/dist/naming.js +159 -0
  34. package/dist/oauth.d.ts +186 -172
  35. package/dist/oauth.js +1 -1
  36. package/dist/operation-definitions.d.ts +9 -0
  37. package/dist/operation-definitions.js +7 -0
  38. package/dist/operational-config.d.ts +101 -0
  39. package/dist/operational-config.js +341 -0
  40. package/dist/operations.d.ts +4742 -2170
  41. package/dist/operations.js +3064 -932
  42. package/dist/organization-lifecycle.d.ts +34 -0
  43. package/dist/organization-lifecycle.js +122 -0
  44. package/dist/partition.d.ts +43 -9
  45. package/dist/partition.js +3 -3
  46. package/dist/policy.d.ts +4 -4
  47. package/dist/policy.js +9 -5
  48. package/dist/problems.d.ts +57 -28
  49. package/dist/problems.js +55 -3
  50. package/dist/protocol.d.ts +209 -80
  51. package/dist/protocol.js +110 -37
  52. package/dist/scheduling-records.d.ts +349 -285
  53. package/dist/scheduling-records.js +1 -1
  54. package/dist/scheduling.d.ts +707 -361
  55. package/dist/scheduling.js +443 -61
  56. package/dist/secrets.d.ts +119 -105
  57. package/dist/secrets.js +1 -1
  58. package/dist/signatures.d.ts +74 -74
  59. package/dist/signatures.js +1 -1
  60. package/dist/tenancy.d.ts +142 -68
  61. package/dist/tenancy.js +70 -17
  62. package/dist/test-sink.d.ts +127 -127
  63. package/dist/test-sink.js +1 -1
  64. package/dist/time.d.ts +4 -4
  65. package/dist/time.js +1 -1
  66. package/dist/timing.d.ts +65 -0
  67. package/dist/timing.js +82 -0
  68. package/dist/usage.d.ts +103 -47
  69. package/dist/usage.js +183 -32
  70. package/dist/workspace.d.ts +34 -34
  71. package/dist/workspace.js +1 -1
  72. package/dist/zod-values.d.ts +3 -0
  73. package/dist/zod-values.js +2 -0
  74. package/dist/zod.d.ts +2 -0
  75. package/dist/zod.js +2 -0
  76. package/generated/v1/examples/attempts.list.response.json +6 -0
  77. package/generated/v1/examples/attempts.read.response.json +6 -0
  78. package/generated/v1/examples/billing.autotopup.request.json +5 -0
  79. package/generated/v1/examples/billing.autotopup.response.json +120 -0
  80. package/generated/v1/examples/billing.autotopuppreview.request.json +19 -0
  81. package/generated/v1/examples/billing.autotopuppreview.response.json +45 -0
  82. package/generated/v1/examples/billing.budgets.request.json +13 -0
  83. package/generated/v1/examples/billing.budgets.response.json +22 -0
  84. package/generated/v1/examples/billing.ceiling.request.json +8 -0
  85. package/generated/v1/examples/billing.ceiling.response.json +120 -0
  86. package/generated/v1/examples/billing.change.request.json +5 -0
  87. package/generated/v1/examples/billing.change.response.json +12 -0
  88. package/generated/v1/examples/billing.funding.response.json +120 -0
  89. package/generated/v1/examples/billing.fundingconfirm.response.json +120 -0
  90. package/generated/v1/examples/billing.fundingquote.request.json +7 -0
  91. package/generated/v1/examples/billing.fundingquote.response.json +13 -0
  92. package/generated/v1/examples/billing.invoicehistory.response.json +6 -0
  93. package/generated/v1/examples/billing.invoices.response.json +15 -0
  94. package/generated/v1/examples/billing.ledger.response.json +23 -0
  95. package/generated/v1/examples/billing.notices.response.json +41 -0
  96. package/generated/v1/examples/billing.notificationsettings.response.json +17 -0
  97. package/generated/v1/examples/billing.notificationsettingsupdate.request.json +9 -0
  98. package/generated/v1/examples/billing.notificationsettingsupdate.response.json +17 -0
  99. package/generated/v1/examples/billing.paymenthistory.response.json +6 -0
  100. package/generated/v1/examples/billing.paymentmethod.response.json +16 -0
  101. package/generated/v1/examples/billing.paymentmethodconfirm.request.json +3 -0
  102. package/generated/v1/examples/billing.paymentmethodconfirm.response.json +16 -0
  103. package/generated/v1/examples/billing.paymentmethodremove.request.json +3 -0
  104. package/generated/v1/examples/billing.paymentmethodremove.response.json +4 -0
  105. package/generated/v1/examples/billing.paymentmethodsetup.request.json +1 -0
  106. package/generated/v1/examples/billing.paymentmethodsetup.response.json +3 -0
  107. package/generated/v1/examples/billing.portal.response.json +3 -0
  108. package/generated/v1/examples/billing.projects.response.json +12 -0
  109. package/generated/v1/examples/billing.purchase.request.json +5 -0
  110. package/generated/v1/examples/billing.purchase.response.json +120 -0
  111. package/generated/v1/examples/billing.quote.request.json +4 -0
  112. package/generated/v1/examples/billing.quote.response.json +19 -0
  113. package/generated/v1/examples/billing.reconcile.response.json +73 -0
  114. package/generated/v1/examples/billing.refundrequest.request.json +10 -0
  115. package/generated/v1/examples/billing.refundrequest.response.json +11 -0
  116. package/generated/v1/examples/billing.refundresolution.request.json +4 -0
  117. package/generated/v1/examples/billing.refundresolution.response.json +120 -0
  118. package/generated/v1/examples/billing.summary.response.json +73 -0
  119. package/generated/v1/examples/endpoints.acknowledgeSigningKeyRotation.response.json +18 -0
  120. package/generated/v1/examples/endpoints.create.response.json +1 -1
  121. package/generated/v1/examples/endpoints.disable.response.json +0 -1
  122. package/generated/v1/examples/{endpoints.verify.response.json → endpoints.enable.response.json} +4 -5
  123. package/generated/v1/examples/endpoints.list.response.json +1 -2
  124. package/generated/v1/examples/endpoints.read.response.json +2 -2
  125. package/generated/v1/examples/endpoints.revealSigningSecret.response.json +7 -0
  126. package/generated/v1/examples/endpoints.rotateSigningKey.response.json +1 -2
  127. package/generated/v1/examples/endpoints.update.response.json +1 -1
  128. package/generated/v1/examples/executions.list.response.json +20 -0
  129. package/generated/v1/examples/exports.cancel.response.json +11 -0
  130. package/generated/v1/examples/exports.create.request.json +1 -0
  131. package/generated/v1/examples/exports.create.response.json +8 -0
  132. package/generated/v1/examples/exports.part.response.json +5 -0
  133. package/generated/v1/examples/exports.read.response.json +8 -0
  134. package/generated/v1/examples/limits.read.response.json +16 -0
  135. package/generated/v1/examples/occurrences.read.response.json +4 -0
  136. package/generated/v1/examples/occurrences.redeliver.response.json +2 -0
  137. package/generated/v1/examples/organizations.close.request.json +5 -0
  138. package/generated/v1/examples/organizations.close.response.json +10 -0
  139. package/generated/v1/examples/organizations.lifecycle.response.json +10 -0
  140. package/generated/v1/examples/organizations.restore.request.json +3 -0
  141. package/generated/v1/examples/organizations.restore.response.json +10 -0
  142. package/generated/v1/examples/projects.archive.request.json +3 -0
  143. package/generated/v1/examples/projects.archive.response.json +14 -0
  144. package/generated/v1/examples/projects.create.response.json +10 -1
  145. package/generated/v1/examples/projects.delete.response.json +9 -0
  146. package/generated/v1/examples/projects.list.response.json +12 -2
  147. package/generated/v1/examples/projects.rename.request.json +3 -0
  148. package/generated/v1/examples/projects.rename.response.json +14 -0
  149. package/generated/v1/examples/schedules.archive.response.json +3 -0
  150. package/generated/v1/examples/schedules.cancel.response.json +3 -0
  151. package/generated/v1/examples/schedules.create.request.json +4 -0
  152. package/generated/v1/examples/schedules.create.response.json +3 -0
  153. package/generated/v1/examples/schedules.list.response.json +3 -0
  154. package/generated/v1/examples/schedules.pause.response.json +3 -0
  155. package/generated/v1/examples/schedules.read.response.json +6 -0
  156. package/generated/v1/examples/schedules.replace.request.json +4 -0
  157. package/generated/v1/examples/schedules.replace.response.json +3 -0
  158. package/generated/v1/examples/schedules.resume.response.json +3 -0
  159. package/generated/v1/examples/timing.summary.response.json +35 -0
  160. package/generated/v1/examples/usage.read.response.json +1 -0
  161. package/generated/v1/first-value-journey.json +43 -18
  162. package/generated/v1/manifest.json +1287 -42
  163. package/generated/v1/openapi.json +36033 -5982
  164. package/generated/v1/policy-matrix.json +341 -47
  165. package/generated/v1/schemas/attempts.list.response.json +39 -1
  166. package/generated/v1/schemas/attempts.read.response.json +39 -1
  167. package/generated/v1/schemas/billing.autotopup.request-headers.json +13 -0
  168. package/generated/v1/schemas/billing.autotopup.request.json +22 -0
  169. package/generated/v1/schemas/billing.autotopup.response-headers.json +10 -0
  170. package/generated/v1/schemas/billing.autotopup.response.json +552 -0
  171. package/generated/v1/schemas/billing.autotopuppreview.request.json +77 -0
  172. package/generated/v1/schemas/billing.autotopuppreview.response.json +277 -0
  173. package/generated/v1/schemas/billing.budgets.request-headers.json +13 -0
  174. package/generated/v1/schemas/billing.budgets.request.json +66 -0
  175. package/generated/v1/schemas/billing.budgets.response-headers.json +10 -0
  176. package/generated/v1/schemas/billing.budgets.response.json +92 -0
  177. package/generated/v1/schemas/billing.ceiling.request-headers.json +13 -0
  178. package/generated/v1/schemas/billing.ceiling.request.json +40 -0
  179. package/generated/v1/schemas/billing.ceiling.response-headers.json +10 -0
  180. package/generated/v1/schemas/billing.ceiling.response.json +552 -0
  181. package/generated/v1/schemas/billing.change.request-headers.json +13 -0
  182. package/generated/v1/schemas/billing.change.request.json +22 -0
  183. package/generated/v1/schemas/billing.change.response.json +119 -0
  184. package/generated/v1/schemas/billing.funding.response.json +552 -0
  185. package/generated/v1/schemas/billing.fundingconfirm.response.json +552 -0
  186. package/generated/v1/schemas/billing.fundingquote.request.json +28 -0
  187. package/generated/v1/schemas/billing.fundingquote.response.json +61 -0
  188. package/generated/v1/schemas/billing.invoicehistory.query.json +16 -0
  189. package/generated/v1/schemas/billing.invoicehistory.response.json +88 -0
  190. package/generated/v1/schemas/billing.invoices.response.json +51 -0
  191. package/generated/v1/schemas/billing.ledger.query.json +16 -0
  192. package/generated/v1/schemas/billing.ledger.response.json +130 -0
  193. package/generated/v1/schemas/billing.notices.query.json +16 -0
  194. package/generated/v1/schemas/billing.notices.response.json +278 -0
  195. package/generated/v1/schemas/billing.notificationsettings.response.json +70 -0
  196. package/generated/v1/schemas/billing.notificationsettingsupdate.request-headers.json +13 -0
  197. package/generated/v1/schemas/billing.notificationsettingsupdate.request.json +43 -0
  198. package/generated/v1/schemas/billing.notificationsettingsupdate.response-headers.json +10 -0
  199. package/generated/v1/schemas/billing.notificationsettingsupdate.response.json +70 -0
  200. package/generated/v1/schemas/billing.paymenthistory.query.json +16 -0
  201. package/generated/v1/schemas/billing.paymenthistory.response.json +182 -0
  202. package/generated/v1/schemas/billing.paymentmethod.response.json +137 -0
  203. package/generated/v1/schemas/billing.paymentmethodconfirm.request.json +14 -0
  204. package/generated/v1/schemas/billing.paymentmethodconfirm.response.json +137 -0
  205. package/generated/v1/schemas/billing.paymentmethodremove.request.json +12 -0
  206. package/generated/v1/schemas/billing.paymentmethodremove.response.json +137 -0
  207. package/generated/v1/schemas/billing.paymentmethodsetup.request.json +12 -0
  208. package/generated/v1/schemas/billing.paymentmethodsetup.response.json +11 -0
  209. package/generated/v1/schemas/billing.portal.response.json +11 -0
  210. package/generated/v1/schemas/billing.projects.response.json +119 -0
  211. package/generated/v1/schemas/billing.purchase.request-headers.json +13 -0
  212. package/generated/v1/schemas/billing.purchase.request.json +26 -0
  213. package/generated/v1/schemas/billing.purchase.response-headers.json +10 -0
  214. package/generated/v1/schemas/billing.purchase.response.json +552 -0
  215. package/generated/v1/schemas/billing.quote.request.json +20 -0
  216. package/generated/v1/schemas/billing.quote.response.json +86 -0
  217. package/generated/v1/schemas/billing.reconcile.response.json +468 -0
  218. package/generated/v1/schemas/billing.refundrequest.request-headers.json +13 -0
  219. package/generated/v1/schemas/billing.refundrequest.request.json +42 -0
  220. package/generated/v1/schemas/billing.refundrequest.response-headers.json +10 -0
  221. package/generated/v1/schemas/billing.refundrequest.response.json +45 -0
  222. package/generated/v1/schemas/billing.refundresolution.request-headers.json +13 -0
  223. package/generated/v1/schemas/billing.refundresolution.request.json +61 -0
  224. package/generated/v1/schemas/billing.refundresolution.response-headers.json +10 -0
  225. package/generated/v1/schemas/billing.refundresolution.response.json +552 -0
  226. package/generated/v1/schemas/billing.summary.response.json +468 -0
  227. package/generated/v1/schemas/context.read.response.json +2 -0
  228. package/generated/v1/schemas/credentials.connection.response.json +2 -0
  229. package/generated/v1/schemas/credentials.create.request.json +2 -0
  230. package/generated/v1/schemas/credentials.create.response.json +2 -0
  231. package/generated/v1/schemas/credentials.list.response.json +2 -0
  232. package/generated/v1/schemas/credentials.revoke.response.json +2 -0
  233. package/generated/v1/schemas/credentials.rotate.response.json +4 -0
  234. package/generated/v1/schemas/{endpoints.verify.request-headers.json → endpoints.acknowledgeSigningKeyRotation.request-headers.json} +2 -1
  235. package/generated/v1/schemas/{endpoints.verify.response.json → endpoints.acknowledgeSigningKeyRotation.response.json} +8 -21
  236. package/generated/v1/schemas/endpoints.create.request.json +5 -0
  237. package/generated/v1/schemas/endpoints.create.response.json +8 -21
  238. package/generated/v1/schemas/endpoints.disable.response.json +8 -21
  239. package/generated/v1/schemas/endpoints.enable.response-headers.json +11 -0
  240. package/generated/v1/schemas/endpoints.enable.response.json +87 -0
  241. package/generated/v1/schemas/endpoints.list.response.json +8 -21
  242. package/generated/v1/schemas/endpoints.read.response.json +8 -21
  243. package/generated/v1/schemas/endpoints.revealSigningSecret.response.json +43 -0
  244. package/generated/v1/schemas/endpoints.rotateSigningKey.response.json +8 -21
  245. package/generated/v1/schemas/endpoints.update.request.json +5 -0
  246. package/generated/v1/schemas/endpoints.update.response.json +8 -21
  247. package/generated/v1/schemas/executions.list.query.json +46 -0
  248. package/generated/v1/schemas/executions.list.response.json +116 -0
  249. package/generated/v1/schemas/exports.cancel.response.json +48 -0
  250. package/generated/v1/schemas/exports.create.request.json +6 -0
  251. package/generated/v1/schemas/exports.create.response.json +38 -0
  252. package/generated/v1/schemas/exports.part.query.json +11 -0
  253. package/generated/v1/schemas/exports.part.response.json +22 -0
  254. package/generated/v1/schemas/exports.read.response.json +38 -0
  255. package/generated/v1/schemas/forecast.read.request-headers.json +10 -0
  256. package/generated/v1/schemas/forecast.read.response.json +148 -0
  257. package/generated/v1/schemas/grants.list.response.json +2 -0
  258. package/generated/v1/schemas/grants.revoke.response.json +2 -0
  259. package/generated/v1/schemas/limits.read.response.json +79 -0
  260. package/generated/v1/schemas/occurrences.list.query.json +5 -0
  261. package/generated/v1/schemas/occurrences.list.response.json +23 -0
  262. package/generated/v1/schemas/occurrences.read.response.json +23 -0
  263. package/generated/v1/schemas/occurrences.redeliver.response.json +39 -1
  264. package/generated/v1/schemas/organizations.close.request.json +20 -0
  265. package/generated/v1/schemas/organizations.close.response.json +68 -0
  266. package/generated/v1/schemas/organizations.lifecycle.response.json +68 -0
  267. package/generated/v1/schemas/organizations.restore.request.json +13 -0
  268. package/generated/v1/schemas/organizations.restore.response.json +68 -0
  269. package/generated/v1/schemas/projects.archive.request-headers.json +11 -0
  270. package/generated/v1/schemas/projects.archive.request.json +10 -0
  271. package/generated/v1/schemas/projects.archive.response-headers.json +11 -0
  272. package/generated/v1/schemas/projects.archive.response.json +79 -0
  273. package/generated/v1/schemas/projects.create.response.json +55 -1
  274. package/generated/v1/schemas/projects.delete.request-headers.json +11 -0
  275. package/generated/v1/schemas/projects.delete.response.json +37 -0
  276. package/generated/v1/schemas/projects.list.response.json +61 -2
  277. package/generated/v1/schemas/projects.rename.request-headers.json +11 -0
  278. package/generated/v1/schemas/projects.rename.request.json +12 -0
  279. package/generated/v1/schemas/projects.rename.response-headers.json +11 -0
  280. package/generated/v1/schemas/projects.rename.response.json +79 -0
  281. package/generated/v1/schemas/schedules.archive.response.json +90 -2
  282. package/generated/v1/schemas/schedules.cancel.response.json +90 -2
  283. package/generated/v1/schemas/schedules.create.request.json +24 -3
  284. package/generated/v1/schemas/schedules.create.response.json +90 -2
  285. package/generated/v1/schemas/schedules.list.query.json +13 -0
  286. package/generated/v1/schemas/schedules.list.response.json +67 -2
  287. package/generated/v1/schemas/schedules.pause.response.json +90 -2
  288. package/generated/v1/schemas/schedules.read.response.json +90 -2
  289. package/generated/v1/schemas/schedules.replace.request.json +24 -3
  290. package/generated/v1/schemas/schedules.replace.response.json +90 -2
  291. package/generated/v1/schemas/schedules.resume.response.json +90 -2
  292. package/generated/v1/schemas/timing.summary.response.json +191 -0
  293. package/generated/v1/schemas/usage.read.query.json +7 -0
  294. package/generated/v1/schemas/usage.read.response.json +36 -1
  295. package/generated/v1/snippets/billing.autotopup.json +6 -0
  296. package/generated/v1/snippets/billing.autotopuppreview.json +6 -0
  297. package/generated/v1/snippets/billing.budgets.json +6 -0
  298. package/generated/v1/snippets/billing.ceiling.json +6 -0
  299. package/generated/v1/snippets/billing.change.json +6 -0
  300. package/generated/v1/snippets/billing.funding.json +6 -0
  301. package/generated/v1/snippets/billing.fundingconfirm.json +6 -0
  302. package/generated/v1/snippets/billing.fundingquote.json +6 -0
  303. package/generated/v1/snippets/billing.invoicehistory.json +6 -0
  304. package/generated/v1/snippets/billing.invoices.json +6 -0
  305. package/generated/v1/snippets/billing.ledger.json +6 -0
  306. package/generated/v1/snippets/billing.notices.json +6 -0
  307. package/generated/v1/snippets/billing.notificationsettings.json +6 -0
  308. package/generated/v1/snippets/billing.notificationsettingsupdate.json +6 -0
  309. package/generated/v1/snippets/billing.paymenthistory.json +6 -0
  310. package/generated/v1/snippets/billing.paymentmethod.json +6 -0
  311. package/generated/v1/snippets/billing.paymentmethodconfirm.json +6 -0
  312. package/generated/v1/snippets/billing.paymentmethodremove.json +6 -0
  313. package/generated/v1/snippets/billing.paymentmethodsetup.json +6 -0
  314. package/generated/v1/snippets/billing.portal.json +6 -0
  315. package/generated/v1/snippets/billing.projects.json +6 -0
  316. package/generated/v1/snippets/billing.purchase.json +6 -0
  317. package/generated/v1/snippets/billing.quote.json +6 -0
  318. package/generated/v1/snippets/billing.reconcile.json +6 -0
  319. package/generated/v1/snippets/billing.refundrequest.json +6 -0
  320. package/generated/v1/snippets/billing.refundresolution.json +6 -0
  321. package/generated/v1/snippets/billing.summary.json +6 -0
  322. package/generated/v1/snippets/endpoints.acknowledgeSigningKeyRotation.json +6 -0
  323. package/generated/v1/snippets/{endpoints.verify.json → endpoints.enable.json} +3 -3
  324. package/generated/v1/snippets/endpoints.revealSigningSecret.json +6 -0
  325. package/generated/v1/snippets/executions.list.json +6 -0
  326. package/generated/v1/snippets/exports.cancel.json +6 -0
  327. package/generated/v1/snippets/exports.create.json +6 -0
  328. package/generated/v1/snippets/exports.part.json +6 -0
  329. package/generated/v1/snippets/exports.read.json +6 -0
  330. package/generated/v1/snippets/forecast.windows.json +1 -1
  331. package/generated/v1/snippets/limits.read.json +6 -0
  332. package/generated/v1/snippets/organizations.close.json +6 -0
  333. package/generated/v1/snippets/organizations.lifecycle.json +6 -0
  334. package/generated/v1/snippets/organizations.restore.json +6 -0
  335. package/generated/v1/snippets/projects.archive.json +6 -0
  336. package/generated/v1/snippets/projects.delete.json +6 -0
  337. package/generated/v1/snippets/projects.rename.json +6 -0
  338. package/generated/v1/snippets/schedules.create.json +3 -3
  339. package/generated/v1/snippets/schedules.replace.json +3 -3
  340. package/generated/v1/snippets/timing.summary.json +6 -0
  341. package/package.json +7 -7
  342. package/dist/callback-ownership.d.ts +0 -58
  343. package/dist/callback-ownership.js +0 -71
  344. package/generated/v1/callback-endpoint-verification.json +0 -59
  345. /package/generated/v1/schemas/{endpoints.verify.response-headers.json → endpoints.acknowledgeSigningKeyRotation.response-headers.json} +0 -0
package/dist/usage.d.ts CHANGED
@@ -1,28 +1,65 @@
1
- import { z } from "zod";
1
+ import { z } from "./zod.js";
2
2
  import type { FieldError } from "./problems.js";
3
3
  /**
4
- * What a customer is charged for, and nothing about what it cost to deliver.
4
+ * A legacy operational aggregate of terminal occurrences.
5
5
  *
6
- * **One terminal occurrence is one billable unit, however many attempts it
7
- * took.** That is the commercial rule the whole runtime is built around — a
8
- * retry is our cost, not the customer's — and this surface exists so it is
9
- * something a customer can see rather than something they are told. A schedule
10
- * that failed four times and succeeded on the fifth appears here once; the
11
- * five attempts are on the occurrence's own attempt history, which is the page
12
- * that answers "what did it cost us".
6
+ * The published field names predate PAYG and remain for client compatibility.
7
+ * They are no longer the invoice meter: PAYG charges each logical delivery
8
+ * attempt, including retries and redeliveries, through the exact billing ledger.
9
+ * A schedule that failed four times and succeeded on the fifth appears here as
10
+ * one terminal occurrence and in `billing.ledger` as five distinct attempts.
13
11
  *
14
12
  * Days are UTC and half-open, aligned to midnight at both ends. A tenant-local
15
13
  * boundary would make every bucket a different width for a different customer
16
14
  * and could not be one index range; the capacity decision states the budget in
17
15
  * UTC days for the same reason.
18
16
  *
19
- * A day with no billable occurrence is **absent** rather than zero. The window
17
+ * A day with no terminal occurrence is **absent** rather than zero. The window
20
18
  * says which days the answer covers, so an absent day is unambiguous, and a
21
19
  * quiet month is a short document rather than thirty-one zeroes.
22
20
  */
23
21
  export declare const USAGE_BOUNDS: {
22
+ /**
23
+ * The bucket a caller gets without asking: one UTC day.
24
+ *
25
+ * Unchanged from the only width this surface had, so a caller that never
26
+ * heard of `bucketSeconds` sees exactly what it saw before.
27
+ */
28
+ readonly defaultBucketSeconds: 86400;
24
29
  /** Days a window covers when the caller names neither end. */
25
30
  readonly defaultWindowDays: 31;
31
+ /**
32
+ * Buckets one answer may carry.
33
+ *
34
+ * A year of hours is 8,784, which is not a chart and not a document anybody
35
+ * reads — it is a client asking for the widest window at the finest width and
36
+ * getting a megabyte back. The ceiling is a refusal rather than a truncation,
37
+ * for the reason the forecast gives: an answer that is quietly short is one a
38
+ * caller will act on.
39
+ *
40
+ * Matched to `FORECAST_BOUNDS.maxBuckets`, because past and future now answer
41
+ * the same question in the same shape and a client charting both should not
42
+ * discover that one of them stops earlier.
43
+ */
44
+ readonly maxBuckets: 1500;
45
+ /**
46
+ * The widest bucket, which is also the default: a day.
47
+ *
48
+ * Wider than a day is a question about months, and months are not a whole
49
+ * number of days — a "monthly" bucket would have to pick a calendar and this
50
+ * surface is deliberately in UTC days for every tenant.
51
+ */
52
+ readonly maxBucketSeconds: 86400;
53
+ /**
54
+ * The finest bucket, which is an hour.
55
+ *
56
+ * Finer than an hour is not refused for cost — the read is the same indexed
57
+ * range scan at any width, and the row count depends on the window rather
58
+ * than the bucket — but because nothing asks for it. The dashboard's finest
59
+ * chart is hourly, and a width nothing consumes is a width nobody has
60
+ * checked the arithmetic of.
61
+ */
62
+ readonly minBucketSeconds: 3600;
26
63
  /**
27
64
  * The widest window this contract will describe.
28
65
  *
@@ -32,19 +69,20 @@ export declare const USAGE_BOUNDS: {
32
69
  */
33
70
  readonly maxWindowDays: 366;
34
71
  };
35
- /** How a billable occurrence ended. The four terminal occurrence states. */
72
+ /** How a terminal occurrence ended. The four retained terminal states. */
36
73
  export declare const BILLABLE_TERMINAL_STATES: readonly ["cancelled", "skipped_stale", "succeeded", "terminal_failed"];
37
- export declare const billableTerminalStateSchema: z.ZodEnum<{
74
+ export declare const billableTerminalStateSchema: import("zod").ZodEnum<{
38
75
  succeeded: "succeeded";
39
76
  cancelled: "cancelled";
40
77
  terminal_failed: "terminal_failed";
41
78
  skipped_stale: "skipped_stale";
42
79
  }>;
43
80
  export type BillableTerminalStateName = z.infer<typeof billableTerminalStateSchema>;
44
- export declare const usageRequestSchema: z.ZodObject<{
45
- from: z.ZodOptional<z.ZodString>;
46
- to: z.ZodOptional<z.ZodString>;
47
- }, z.core.$strict>;
81
+ export declare const usageRequestSchema: import("zod").ZodObject<{
82
+ bucketSeconds: import("zod").ZodDefault<import("zod").ZodNumber>;
83
+ from: import("zod").ZodOptional<import("zod").ZodString>;
84
+ to: import("zod").ZodOptional<import("zod").ZodString>;
85
+ }, import("zod/v4/core").$strict>;
48
86
  export type UsageRequest = z.infer<typeof usageRequestSchema>;
49
87
  export type UsageRequestParse = {
50
88
  readonly errors: readonly FieldError[];
@@ -65,43 +103,51 @@ export type UsageRequestParse = {
65
103
  export declare function parseUsageRequest(query: URLSearchParams): UsageRequestParse;
66
104
  /** What a caller is told when a usage window is refused rather than served. */
67
105
  export declare const USAGE_WINDOW_REASONS: Readonly<Record<UsageWindowRefusal, string>>;
68
- export declare const usageDaySchema: z.ZodObject<{
69
- billableOccurrences: z.ZodNumber;
70
- byTerminalState: z.ZodRecord<z.ZodEnum<{
106
+ export declare const usageDaySchema: import("zod").ZodObject<{
107
+ billableOccurrences: import("zod").ZodNumber;
108
+ byTerminalState: import("zod").ZodRecord<import("zod").ZodEnum<{
71
109
  succeeded: "succeeded";
72
110
  cancelled: "cancelled";
73
111
  terminal_failed: "terminal_failed";
74
112
  skipped_stale: "skipped_stale";
75
- }> & z.core.$partial, z.ZodNumber>;
76
- start: z.ZodString;
77
- }, z.core.$strip>;
113
+ }> & import("zod/v4/core").$partial, import("zod").ZodNumber>;
114
+ start: import("zod").ZodString;
115
+ }, import("zod/v4/core").$strip>;
78
116
  export type UsageDayView = z.infer<typeof usageDaySchema>;
79
- export declare const usageSchema: z.ZodObject<{
80
- days: z.ZodArray<z.ZodObject<{
81
- billableOccurrences: z.ZodNumber;
82
- byTerminalState: z.ZodRecord<z.ZodEnum<{
117
+ export declare const usageSchema: import("zod").ZodObject<{
118
+ bucketSeconds: import("zod").ZodNumber;
119
+ days: import("zod").ZodArray<import("zod").ZodObject<{
120
+ billableOccurrences: import("zod").ZodNumber;
121
+ byTerminalState: import("zod").ZodRecord<import("zod").ZodEnum<{
83
122
  succeeded: "succeeded";
84
123
  cancelled: "cancelled";
85
124
  terminal_failed: "terminal_failed";
86
125
  skipped_stale: "skipped_stale";
87
- }> & z.core.$partial, z.ZodNumber>;
88
- start: z.ZodString;
89
- }, z.core.$strip>>;
90
- freshness: z.ZodObject<{
91
- kind: z.ZodLiteral<"authoritative">;
92
- }, z.core.$strip>;
93
- totalBillableOccurrences: z.ZodNumber;
94
- window: z.ZodObject<{
95
- from: z.ZodString;
96
- to: z.ZodString;
97
- }, z.core.$strip>;
98
- }, z.core.$strip>;
126
+ }> & import("zod/v4/core").$partial, import("zod").ZodNumber>;
127
+ start: import("zod").ZodString;
128
+ }, import("zod/v4/core").$strip>>;
129
+ freshness: import("zod").ZodObject<{
130
+ kind: import("zod").ZodLiteral<"authoritative">;
131
+ }, import("zod/v4/core").$strip>;
132
+ plan: import("zod").ZodOptional<import("zod").ZodObject<{
133
+ attemptsToday: import("zod").ZodNumber;
134
+ dailyAttemptCeiling: import("zod").ZodNumber;
135
+ resetsAt: import("zod").ZodString;
136
+ since: import("zod").ZodString;
137
+ }, import("zod/v4/core").$strip>>;
138
+ totalBillableOccurrences: import("zod").ZodNumber;
139
+ window: import("zod").ZodObject<{
140
+ from: import("zod").ZodString;
141
+ to: import("zod").ZodString;
142
+ }, import("zod/v4/core").$strip>;
143
+ }, import("zod/v4/core").$strip>;
99
144
  export type UsageView = z.infer<typeof usageSchema>;
100
- export type UsageWindowRefusal = "window-inverted" | "window-too-wide" | "window-unreadable";
145
+ export type UsageWindowRefusal = "too-many-buckets" | "window-inverted" | "window-too-wide" | "window-unreadable";
101
146
  export type ResolvedUsageWindow = {
102
147
  readonly ok: false;
103
148
  readonly refusal: UsageWindowRefusal;
104
149
  } | {
150
+ readonly bucketSeconds: number;
105
151
  readonly fromMs: number;
106
152
  readonly ok: true;
107
153
  readonly toMs: number;
@@ -109,13 +155,23 @@ export type ResolvedUsageWindow = {
109
155
  /** UTC midnight of the day an instant falls in. */
110
156
  export declare function utcMidnightMs(instantMs: number): number;
111
157
  /**
112
- * Resolve the window a usage request names, aligned to UTC days.
158
+ * Resolve the window a usage request names, aligned to whole buckets.
159
+ *
160
+ * **Both ends are snapped outward, and that is not tidiness.** A window
161
+ * beginning at nine in the morning would put a partial bucket at the front of
162
+ * the list with no way for a reader to tell it from a quiet one — a bar that is
163
+ * short because it is twenty-three minutes long rather than because nothing
164
+ * happened. Snapping outward means the answer always covers whole buckets and
165
+ * never covers less than what was asked for.
113
166
  *
114
- * **Both ends are snapped to midnight, and that is not tidiness.** The answer
115
- * is bucketed by UTC day; a window beginning at nine in the morning would put a
116
- * partial day at the front of the list with no way for a reader to tell it from
117
- * a quiet one. Snapping outward — the start of `from`'s day, the start of the
118
- * day after `to`'s — means the window always covers whole days and never covers
119
- * less than what was asked for.
167
+ * **To the bucket rather than to the day, since FXO-2281**, which is the same
168
+ * argument one width down. It is the same behaviour at the default width: a
169
+ * day-wide bucket aligned to the epoch starts at UTC midnight, so a caller who
170
+ * names no width gets exactly the window this always served. An hourly caller
171
+ * asking for "the last twenty-four hours" would otherwise have both ends pushed
172
+ * to midnight and be handed up to forty-eight buckets, half of them for a day
173
+ * they did not ask about.
120
174
  */
121
- export declare function resolveUsageWindow(request: UsageRequest, nowMs: number): ResolvedUsageWindow;
175
+ export declare function resolveUsageWindow(request: Omit<UsageRequest, "bucketSeconds"> & {
176
+ readonly bucketSeconds?: number;
177
+ }, nowMs: number): ResolvedUsageWindow;
package/dist/usage.js CHANGED
@@ -1,28 +1,66 @@
1
- import { z } from "zod";
1
+ import { z } from "./zod.js";
2
+ import { MAX_USAGE_WINDOW_DAYS } from "./configuration-bounds.js";
2
3
  import { timestampSchema } from "./time.js";
3
4
  /**
4
- * What a customer is charged for, and nothing about what it cost to deliver.
5
+ * A legacy operational aggregate of terminal occurrences.
5
6
  *
6
- * **One terminal occurrence is one billable unit, however many attempts it
7
- * took.** That is the commercial rule the whole runtime is built around — a
8
- * retry is our cost, not the customer's — and this surface exists so it is
9
- * something a customer can see rather than something they are told. A schedule
10
- * that failed four times and succeeded on the fifth appears here once; the
11
- * five attempts are on the occurrence's own attempt history, which is the page
12
- * that answers "what did it cost us".
7
+ * The published field names predate PAYG and remain for client compatibility.
8
+ * They are no longer the invoice meter: PAYG charges each logical delivery
9
+ * attempt, including retries and redeliveries, through the exact billing ledger.
10
+ * A schedule that failed four times and succeeded on the fifth appears here as
11
+ * one terminal occurrence and in `billing.ledger` as five distinct attempts.
13
12
  *
14
13
  * Days are UTC and half-open, aligned to midnight at both ends. A tenant-local
15
14
  * boundary would make every bucket a different width for a different customer
16
15
  * and could not be one index range; the capacity decision states the budget in
17
16
  * UTC days for the same reason.
18
17
  *
19
- * A day with no billable occurrence is **absent** rather than zero. The window
18
+ * A day with no terminal occurrence is **absent** rather than zero. The window
20
19
  * says which days the answer covers, so an absent day is unambiguous, and a
21
20
  * quiet month is a short document rather than thirty-one zeroes.
22
21
  */
23
22
  export const USAGE_BOUNDS = {
23
+ /**
24
+ * The bucket a caller gets without asking: one UTC day.
25
+ *
26
+ * Unchanged from the only width this surface had, so a caller that never
27
+ * heard of `bucketSeconds` sees exactly what it saw before.
28
+ */
29
+ defaultBucketSeconds: 86_400,
24
30
  /** Days a window covers when the caller names neither end. */
25
31
  defaultWindowDays: 31,
32
+ /**
33
+ * Buckets one answer may carry.
34
+ *
35
+ * A year of hours is 8,784, which is not a chart and not a document anybody
36
+ * reads — it is a client asking for the widest window at the finest width and
37
+ * getting a megabyte back. The ceiling is a refusal rather than a truncation,
38
+ * for the reason the forecast gives: an answer that is quietly short is one a
39
+ * caller will act on.
40
+ *
41
+ * Matched to `FORECAST_BOUNDS.maxBuckets`, because past and future now answer
42
+ * the same question in the same shape and a client charting both should not
43
+ * discover that one of them stops earlier.
44
+ */
45
+ maxBuckets: 1_500,
46
+ /**
47
+ * The widest bucket, which is also the default: a day.
48
+ *
49
+ * Wider than a day is a question about months, and months are not a whole
50
+ * number of days — a "monthly" bucket would have to pick a calendar and this
51
+ * surface is deliberately in UTC days for every tenant.
52
+ */
53
+ maxBucketSeconds: 86_400,
54
+ /**
55
+ * The finest bucket, which is an hour.
56
+ *
57
+ * Finer than an hour is not refused for cost — the read is the same indexed
58
+ * range scan at any width, and the row count depends on the window rather
59
+ * than the bucket — but because nothing asks for it. The dashboard's finest
60
+ * chart is hourly, and a width nothing consumes is a width nobody has
61
+ * checked the arithmetic of.
62
+ */
63
+ minBucketSeconds: 3_600,
26
64
  /**
27
65
  * The widest window this contract will describe.
28
66
  *
@@ -30,9 +68,9 @@ export const USAGE_BOUNDS = {
30
68
  * never older than that. It is a contract ceiling rather than a plan one: a
31
69
  * plan may bring it closer and may never push it out.
32
70
  */
33
- maxWindowDays: 366,
71
+ maxWindowDays: MAX_USAGE_WINDOW_DAYS,
34
72
  };
35
- /** How a billable occurrence ended. The four terminal occurrence states. */
73
+ /** How a terminal occurrence ended. The four retained terminal states. */
36
74
  export const BILLABLE_TERMINAL_STATES = [
37
75
  "cancelled",
38
76
  "skipped_stale",
@@ -42,13 +80,42 @@ export const BILLABLE_TERMINAL_STATES = [
42
80
  export const billableTerminalStateSchema = z.enum(BILLABLE_TERMINAL_STATES);
43
81
  export const usageRequestSchema = z
44
82
  .object({
83
+ /**
84
+ * Bucket width in whole seconds, aligned to the epoch.
85
+ *
86
+ * **Mirrors `/v1/forecast`'s parameter, deliberately** — past and future
87
+ * answer the same question about the same book, and a client charting both
88
+ * should not have to learn two spellings of "by the hour". Seconds as a
89
+ * number rather than a duration string, which is this product's published
90
+ * convention.
91
+ *
92
+ * A whole number of the finest bucket, so re-aggregating is exact: an
93
+ * answer whose buckets straddled would be a number nobody can reconcile
94
+ * against a bill.
95
+ */
96
+ bucketSeconds: z
97
+ .number()
98
+ .int()
99
+ .min(USAGE_BOUNDS.minBucketSeconds)
100
+ .max(USAGE_BOUNDS.maxBucketSeconds)
101
+ .multipleOf(USAGE_BOUNDS.minBucketSeconds)
102
+ .default(USAGE_BOUNDS.defaultBucketSeconds),
45
103
  /** Inclusive start. Defaults to `defaultWindowDays` before the end. */
46
104
  from: timestampSchema.optional(),
47
105
  /** Exclusive end. Defaults to the start of tomorrow, UTC. */
48
106
  to: timestampSchema.optional(),
49
107
  })
50
108
  .strict();
51
- const USAGE_PARAMETERS = ["from", "to"];
109
+ /** `[name, numeric]` — the coercion each parameter needs off a URL. */
110
+ const USAGE_PARAMETERS = [
111
+ ["bucketSeconds", true],
112
+ ["from", false],
113
+ ["to", false],
114
+ ];
115
+ /** Query values arrive as strings; only a whole number may become one. */
116
+ function asNumberIfInteger(value) {
117
+ return /^-?\d+$/u.test(value) ? Number(value) : value;
118
+ }
52
119
  /**
53
120
  * Read a usage request off a URL.
54
121
  *
@@ -61,7 +128,7 @@ const USAGE_PARAMETERS = ["from", "to"];
61
128
  export function parseUsageRequest(query) {
62
129
  const errors = [];
63
130
  const raw = {};
64
- for (const field of USAGE_PARAMETERS) {
131
+ for (const [field, numeric] of USAGE_PARAMETERS) {
65
132
  const values = query.getAll(field);
66
133
  if (values.length > 1) {
67
134
  errors.push({
@@ -73,7 +140,7 @@ export function parseUsageRequest(query) {
73
140
  const value = values[0];
74
141
  if (value === undefined)
75
142
  continue;
76
- raw[field] = value;
143
+ raw[field] = numeric ? asNumberIfInteger(value) : value;
77
144
  }
78
145
  if (errors.length > 0)
79
146
  return { errors, ok: false };
@@ -90,6 +157,7 @@ export function parseUsageRequest(query) {
90
157
  }
91
158
  /** What a caller is told when a usage window is refused rather than served. */
92
159
  export const USAGE_WINDOW_REASONS = {
160
+ "too-many-buckets": `must not ask for more than ${String(USAGE_BOUNDS.maxBuckets)} buckets; widen bucketSeconds or narrow the window`,
93
161
  "window-inverted": "must end after it begins",
94
162
  "window-too-wide": `must cover at most ${String(USAGE_BOUNDS.maxWindowDays)} days`,
95
163
  "window-unreadable": "must be an RFC 3339 instant",
@@ -97,7 +165,7 @@ export const USAGE_WINDOW_REASONS = {
97
165
  export const usageDaySchema = z.object({
98
166
  billableOccurrences: z.number().int().positive(),
99
167
  /**
100
- * How the day's billable occurrences ended.
168
+ * How the day's terminal occurrences ended.
101
169
  *
102
170
  * Sums to `billableOccurrences`, and a state with no occurrences is absent
103
171
  * rather than zero — the same rule the day list follows, one level down.
@@ -108,7 +176,22 @@ export const usageDaySchema = z.object({
108
176
  });
109
177
  export const usageSchema = z
110
178
  .object({
111
- /** Ascending by `start`. Days with no billable occurrence are absent. */
179
+ /**
180
+ * The width each entry in `days` covers, in seconds.
181
+ *
182
+ * Echoed so a client knows what it got rather than what it asked for, the
183
+ * way the forecast echoes its own. It is `86400` for every caller that does
184
+ * not ask, which is what this surface always served.
185
+ */
186
+ bucketSeconds: z.number().int().positive(),
187
+ /**
188
+ * Ascending by `start`. Buckets with no terminal occurrence are absent.
189
+ *
190
+ * **Still named `days`, at a width that may not be one.** Renaming it would
191
+ * break every caller reading the field to serve callers who asked for an
192
+ * hourly width — which is nobody until they opt in. `bucketSeconds` above
193
+ * says what a row spans.
194
+ */
112
195
  days: z.array(usageDaySchema),
113
196
  /**
114
197
  * Authoritative, because this is the committed model the settlement wrote.
@@ -119,6 +202,28 @@ export const usageSchema = z
119
202
  * summary.
120
203
  */
121
204
  freshness: z.object({ kind: z.literal("authoritative") }),
205
+ /**
206
+ * Legacy daily-allowance projection, retained for installed clients and
207
+ * mixed-version responses. W0-15 retires the daily quota; current authority
208
+ * readers omit this optional object rather than querying an obsolete count.
209
+ * Older servers can still explain parked work to compatible dashboards.
210
+ */
211
+ plan: z
212
+ .object({
213
+ /** Attempts this organization has made since `since`, capped at one past the ceiling. */
214
+ attemptsToday: z.number().int().nonnegative(),
215
+ dailyAttemptCeiling: z.number().int().positive(),
216
+ /**
217
+ * When the allowance refills — the next UTC day boundary.
218
+ *
219
+ * The same instant the runtime defers to, so a page saying "runs
220
+ * resume at" and a row's own wake cannot disagree.
221
+ */
222
+ resetsAt: timestampSchema,
223
+ /** The start of the day being counted, so a reader can check the window. */
224
+ since: timestampSchema,
225
+ })
226
+ .optional(),
122
227
  totalBillableOccurrences: z.number().int().nonnegative(),
123
228
  window: z.object({ from: timestampSchema, to: timestampSchema }),
124
229
  })
@@ -138,7 +243,7 @@ export const usageSchema = z
138
243
  path: ["totalBillableOccurrences"],
139
244
  })
140
245
  .refine((usage) => usage.days.every((day) => Object.values(day.byTerminalState).reduce((sum, count) => sum + count, 0) === day.billableOccurrences), {
141
- message: "byTerminalState must account for every billable occurrence",
246
+ message: "byTerminalState must account for every terminal occurrence",
142
247
  path: ["days"],
143
248
  })
144
249
  .refine((usage) => usage.days.every((day) => {
@@ -166,35 +271,81 @@ export function utcMidnightMs(instantMs) {
166
271
  return Date.UTC(at.getUTCFullYear(), at.getUTCMonth(), at.getUTCDate());
167
272
  }
168
273
  /**
169
- * Resolve the window a usage request names, aligned to UTC days.
274
+ * Resolve the window a usage request names, aligned to whole buckets.
275
+ *
276
+ * **Both ends are snapped outward, and that is not tidiness.** A window
277
+ * beginning at nine in the morning would put a partial bucket at the front of
278
+ * the list with no way for a reader to tell it from a quiet one — a bar that is
279
+ * short because it is twenty-three minutes long rather than because nothing
280
+ * happened. Snapping outward means the answer always covers whole buckets and
281
+ * never covers less than what was asked for.
170
282
  *
171
- * **Both ends are snapped to midnight, and that is not tidiness.** The answer
172
- * is bucketed by UTC day; a window beginning at nine in the morning would put a
173
- * partial day at the front of the list with no way for a reader to tell it from
174
- * a quiet one. Snapping outward — the start of `from`'s day, the start of the
175
- * day after `to`'s — means the window always covers whole days and never covers
176
- * less than what was asked for.
283
+ * **To the bucket rather than to the day, since FXO-2281**, which is the same
284
+ * argument one width down. It is the same behaviour at the default width: a
285
+ * day-wide bucket aligned to the epoch starts at UTC midnight, so a caller who
286
+ * names no width gets exactly the window this always served. An hourly caller
287
+ * asking for "the last twenty-four hours" would otherwise have both ends pushed
288
+ * to midnight and be handed up to forty-eight buckets, half of them for a day
289
+ * they did not ask about.
177
290
  */
178
- export function resolveUsageWindow(request, nowMs) {
179
- const defaultToMs = utcMidnightMs(nowMs) + DAY_MS;
291
+ export function resolveUsageWindow(
292
+ // **The request as a caller may hand it over, not only as the schema hands
293
+ // it back.** `bucketSeconds` carries a `.default()`, which makes it required
294
+ // on the parsed type; a caller resolving a window from a literal `{ from, to }`
295
+ // — which every case here and the contract mock do — is asking the same
296
+ // question and gets the same default from the `??` below.
297
+ request, nowMs) {
298
+ const bucketSeconds = request.bucketSeconds ?? USAGE_BOUNDS.defaultBucketSeconds;
299
+ const bucketMs = bucketSeconds * 1_000;
300
+ /** The start of the bucket an instant falls in, aligned to the epoch. */
301
+ const floorToBucket = (instantMs) => Math.floor(instantMs / bucketMs) * bucketMs;
302
+ /**
303
+ * The end of the bucket an instant falls in — and the instant itself when it
304
+ * is already a boundary.
305
+ *
306
+ * **The `+ bucketMs` here used to be unconditional**, which handed every
307
+ * caller asking a tidy question one bucket more than it asked for. A
308
+ * dashboard requesting seven whole days ending at midnight was served eight,
309
+ * and printed the served window under a heading that said seven. Windows
310
+ * here are half-open, so an aligned `to` is already exact; only an instant
311
+ * *inside* a bucket has anything to round up to.
312
+ *
313
+ * `defaultToMs` reads the same way and wants to: with `to` omitted the
314
+ * answer should reach the bucket in progress, and when `now` happens to land
315
+ * exactly on a boundary the bucket in progress is empty and belongs outside
316
+ * the window rather than on the end of it as a zero.
317
+ */
318
+ const ceilToBucket = (instantMs) => {
319
+ const floored = floorToBucket(instantMs);
320
+ return floored === instantMs ? instantMs : floored + bucketMs;
321
+ };
322
+ const defaultToMs = ceilToBucket(nowMs);
180
323
  const requestedToMs = request.to === undefined ? defaultToMs : Date.parse(request.to);
181
324
  if (!Number.isFinite(requestedToMs)) {
182
325
  return { ok: false, refusal: "window-unreadable" };
183
326
  }
184
- const toMs = request.to === undefined
185
- ? defaultToMs
186
- : utcMidnightMs(requestedToMs) + DAY_MS;
327
+ const toMs = request.to === undefined ? defaultToMs : ceilToBucket(requestedToMs);
187
328
  const requestedFromMs = request.from === undefined
188
329
  ? toMs - USAGE_BOUNDS.defaultWindowDays * DAY_MS
189
330
  : Date.parse(request.from);
190
331
  if (!Number.isFinite(requestedFromMs)) {
191
332
  return { ok: false, refusal: "window-unreadable" };
192
333
  }
193
- const fromMs = utcMidnightMs(requestedFromMs);
334
+ const fromMs = floorToBucket(requestedFromMs);
194
335
  if (fromMs >= toMs)
195
336
  return { ok: false, refusal: "window-inverted" };
196
337
  if (toMs - fromMs > USAGE_BOUNDS.maxWindowDays * DAY_MS) {
197
338
  return { ok: false, refusal: "window-too-wide" };
198
339
  }
199
- return { fromMs, ok: true, toMs };
340
+ // **The bucket count, not the read.** The read is one indexed range scan over
341
+ // `occurrence_usage` at any width — `terminal_state` is an INCLUDE column on
342
+ // the index it uses, so the whole answer comes from the index and the rows it
343
+ // touches depend on the window rather than on how they are grouped. What
344
+ // grows with a finer bucket is the *answer*, and a year of hours is 8,784
345
+ // rows nobody charts. Refused rather than truncated: a quietly short answer
346
+ // is one a caller acts on.
347
+ if ((toMs - fromMs) / bucketMs > USAGE_BOUNDS.maxBuckets) {
348
+ return { ok: false, refusal: "too-many-buckets" };
349
+ }
350
+ return { bucketSeconds, fromMs, ok: true, toMs };
200
351
  }