@azurioh/discord-kernel 0.1.0

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 (373) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +77 -0
  3. package/dist/authz/authorizer.d.ts +45 -0
  4. package/dist/authz/authorizer.d.ts.map +1 -0
  5. package/dist/authz/authorizer.js +31 -0
  6. package/dist/authz/authorizer.js.map +1 -0
  7. package/dist/authz/require-level-guard.d.ts +23 -0
  8. package/dist/authz/require-level-guard.d.ts.map +1 -0
  9. package/dist/authz/require-level-guard.js +37 -0
  10. package/dist/authz/require-level-guard.js.map +1 -0
  11. package/dist/clock.d.ts +13 -0
  12. package/dist/clock.d.ts.map +1 -0
  13. package/dist/clock.js +13 -0
  14. package/dist/clock.js.map +1 -0
  15. package/dist/concurrency/keyed-queue.d.ts +18 -0
  16. package/dist/concurrency/keyed-queue.d.ts.map +1 -0
  17. package/dist/concurrency/keyed-queue.js +37 -0
  18. package/dist/concurrency/keyed-queue.js.map +1 -0
  19. package/dist/concurrency/run-with-concurrency.d.ts +15 -0
  20. package/dist/concurrency/run-with-concurrency.d.ts.map +1 -0
  21. package/dist/concurrency/run-with-concurrency.js +40 -0
  22. package/dist/concurrency/run-with-concurrency.js.map +1 -0
  23. package/dist/config/env.d.ts +17 -0
  24. package/dist/config/env.d.ts.map +1 -0
  25. package/dist/config/env.js +49 -0
  26. package/dist/config/env.js.map +1 -0
  27. package/dist/config/errors.d.ts +14 -0
  28. package/dist/config/errors.d.ts.map +1 -0
  29. package/dist/config/errors.js +25 -0
  30. package/dist/config/errors.js.map +1 -0
  31. package/dist/datetime.d.ts +34 -0
  32. package/dist/datetime.d.ts.map +1 -0
  33. package/dist/datetime.js +188 -0
  34. package/dist/datetime.js.map +1 -0
  35. package/dist/discord/api-errors.d.ts +20 -0
  36. package/dist/discord/api-errors.d.ts.map +1 -0
  37. package/dist/discord/api-errors.js +90 -0
  38. package/dist/discord/api-errors.js.map +1 -0
  39. package/dist/discord/channel-exporter.d.ts +42 -0
  40. package/dist/discord/channel-exporter.d.ts.map +1 -0
  41. package/dist/discord/channel-exporter.js +3 -0
  42. package/dist/discord/channel-exporter.js.map +1 -0
  43. package/dist/discord/command/autocomplete-dispatcher.d.ts +26 -0
  44. package/dist/discord/command/autocomplete-dispatcher.d.ts.map +1 -0
  45. package/dist/discord/command/autocomplete-dispatcher.js +41 -0
  46. package/dist/discord/command/autocomplete-dispatcher.js.map +1 -0
  47. package/dist/discord/command/autocomplete.d.ts +18 -0
  48. package/dist/discord/command/autocomplete.d.ts.map +1 -0
  49. package/dist/discord/command/autocomplete.js +65 -0
  50. package/dist/discord/command/autocomplete.js.map +1 -0
  51. package/dist/discord/command/context.d.ts +33 -0
  52. package/dist/discord/command/context.d.ts.map +1 -0
  53. package/dist/discord/command/context.js +48 -0
  54. package/dist/discord/command/context.js.map +1 -0
  55. package/dist/discord/command/cooldown-guard.d.ts +25 -0
  56. package/dist/discord/command/cooldown-guard.d.ts.map +1 -0
  57. package/dist/discord/command/cooldown-guard.js +60 -0
  58. package/dist/discord/command/cooldown-guard.js.map +1 -0
  59. package/dist/discord/command/create-command.d.ts +100 -0
  60. package/dist/discord/command/create-command.d.ts.map +1 -0
  61. package/dist/discord/command/create-command.js +245 -0
  62. package/dist/discord/command/create-command.js.map +1 -0
  63. package/dist/discord/command/create-context-menu-command.d.ts +51 -0
  64. package/dist/discord/command/create-context-menu-command.d.ts.map +1 -0
  65. package/dist/discord/command/create-context-menu-command.js +128 -0
  66. package/dist/discord/command/create-context-menu-command.js.map +1 -0
  67. package/dist/discord/command/errors.d.ts +19 -0
  68. package/dist/discord/command/errors.d.ts.map +1 -0
  69. package/dist/discord/command/errors.js +27 -0
  70. package/dist/discord/command/errors.js.map +1 -0
  71. package/dist/discord/command/guard.d.ts +30 -0
  72. package/dist/discord/command/guard.d.ts.map +1 -0
  73. package/dist/discord/command/guard.js +34 -0
  74. package/dist/discord/command/guard.js.map +1 -0
  75. package/dist/discord/command/localization.d.ts +9 -0
  76. package/dist/discord/command/localization.d.ts.map +1 -0
  77. package/dist/discord/command/localization.js +13 -0
  78. package/dist/discord/command/localization.js.map +1 -0
  79. package/dist/discord/command/options.d.ts +113 -0
  80. package/dist/discord/command/options.d.ts.map +1 -0
  81. package/dist/discord/command/options.js +509 -0
  82. package/dist/discord/command/options.js.map +1 -0
  83. package/dist/discord/command/permission-guard.d.ts +21 -0
  84. package/dist/discord/command/permission-guard.d.ts.map +1 -0
  85. package/dist/discord/command/permission-guard.js +69 -0
  86. package/dist/discord/command/permission-guard.js.map +1 -0
  87. package/dist/discord/command/router.d.ts +48 -0
  88. package/dist/discord/command/router.d.ts.map +1 -0
  89. package/dist/discord/command/router.js +118 -0
  90. package/dist/discord/command/router.js.map +1 -0
  91. package/dist/discord/command/types.d.ts +51 -0
  92. package/dist/discord/command/types.d.ts.map +1 -0
  93. package/dist/discord/command/types.js +3 -0
  94. package/dist/discord/command/types.js.map +1 -0
  95. package/dist/discord/components/component-router.d.ts +116 -0
  96. package/dist/discord/components/component-router.d.ts.map +1 -0
  97. package/dist/discord/components/component-router.js +123 -0
  98. package/dist/discord/components/component-router.js.map +1 -0
  99. package/dist/discord/components/interactive-message/index.d.ts +3 -0
  100. package/dist/discord/components/interactive-message/index.d.ts.map +1 -0
  101. package/dist/discord/components/interactive-message/index.js +11 -0
  102. package/dist/discord/components/interactive-message/index.js.map +1 -0
  103. package/dist/discord/components/interactive-message/interactive-message-collector.d.ts +195 -0
  104. package/dist/discord/components/interactive-message/interactive-message-collector.d.ts.map +1 -0
  105. package/dist/discord/components/interactive-message/interactive-message-collector.js +151 -0
  106. package/dist/discord/components/interactive-message/interactive-message-collector.js.map +1 -0
  107. package/dist/discord/components/interactive-message/mount-interactive-message.d.ts +30 -0
  108. package/dist/discord/components/interactive-message/mount-interactive-message.d.ts.map +1 -0
  109. package/dist/discord/components/interactive-message/mount-interactive-message.js +44 -0
  110. package/dist/discord/components/interactive-message/mount-interactive-message.js.map +1 -0
  111. package/dist/discord/components/paginator/index.d.ts +5 -0
  112. package/dist/discord/components/paginator/index.d.ts.map +1 -0
  113. package/dist/discord/components/paginator/index.js +15 -0
  114. package/dist/discord/components/paginator/index.js.map +1 -0
  115. package/dist/discord/components/paginator/mount-paginator.d.ts +25 -0
  116. package/dist/discord/components/paginator/mount-paginator.d.ts.map +1 -0
  117. package/dist/discord/components/paginator/mount-paginator.js +41 -0
  118. package/dist/discord/components/paginator/mount-paginator.js.map +1 -0
  119. package/dist/discord/components/paginator/page-state.d.ts +21 -0
  120. package/dist/discord/components/paginator/page-state.d.ts.map +1 -0
  121. package/dist/discord/components/paginator/page-state.js +44 -0
  122. package/dist/discord/components/paginator/page-state.js.map +1 -0
  123. package/dist/discord/components/paginator/paginator-buttons.d.ts +17 -0
  124. package/dist/discord/components/paginator/paginator-buttons.d.ts.map +1 -0
  125. package/dist/discord/components/paginator/paginator-buttons.js +45 -0
  126. package/dist/discord/components/paginator/paginator-buttons.js.map +1 -0
  127. package/dist/discord/components/paginator/paginator-view.d.ts +26 -0
  128. package/dist/discord/components/paginator/paginator-view.d.ts.map +1 -0
  129. package/dist/discord/components/paginator/paginator-view.js +28 -0
  130. package/dist/discord/components/paginator/paginator-view.js.map +1 -0
  131. package/dist/discord/components/persistent-paginator.d.ts +47 -0
  132. package/dist/discord/components/persistent-paginator.d.ts.map +1 -0
  133. package/dist/discord/components/persistent-paginator.js +137 -0
  134. package/dist/discord/components/persistent-paginator.js.map +1 -0
  135. package/dist/discord/components/settings-editor/index.d.ts +7 -0
  136. package/dist/discord/components/settings-editor/index.d.ts.map +1 -0
  137. package/dist/discord/components/settings-editor/index.js +22 -0
  138. package/dist/discord/components/settings-editor/index.js.map +1 -0
  139. package/dist/discord/components/settings-editor/mount-settings-editor.d.ts +140 -0
  140. package/dist/discord/components/settings-editor/mount-settings-editor.d.ts.map +1 -0
  141. package/dist/discord/components/settings-editor/mount-settings-editor.js +344 -0
  142. package/dist/discord/components/settings-editor/mount-settings-editor.js.map +1 -0
  143. package/dist/discord/components/settings-editor/navigation.d.ts +49 -0
  144. package/dist/discord/components/settings-editor/navigation.d.ts.map +1 -0
  145. package/dist/discord/components/settings-editor/navigation.js +39 -0
  146. package/dist/discord/components/settings-editor/navigation.js.map +1 -0
  147. package/dist/discord/components/settings-editor/settings-editor-card.view.d.ts +56 -0
  148. package/dist/discord/components/settings-editor/settings-editor-card.view.d.ts.map +1 -0
  149. package/dist/discord/components/settings-editor/settings-editor-card.view.js +284 -0
  150. package/dist/discord/components/settings-editor/settings-editor-card.view.js.map +1 -0
  151. package/dist/discord/components/settings-editor/settings-editor-field-modal.d.ts +69 -0
  152. package/dist/discord/components/settings-editor/settings-editor-field-modal.d.ts.map +1 -0
  153. package/dist/discord/components/settings-editor/settings-editor-field-modal.js +353 -0
  154. package/dist/discord/components/settings-editor/settings-editor-field-modal.js.map +1 -0
  155. package/dist/discord/components/settings-editor/settings-editor-fields.d.ts +216 -0
  156. package/dist/discord/components/settings-editor/settings-editor-fields.d.ts.map +1 -0
  157. package/dist/discord/components/settings-editor/settings-editor-fields.js +66 -0
  158. package/dist/discord/components/settings-editor/settings-editor-fields.js.map +1 -0
  159. package/dist/discord/components/settings-editor/settings-editor.view.d.ts +303 -0
  160. package/dist/discord/components/settings-editor/settings-editor.view.d.ts.map +1 -0
  161. package/dist/discord/components/settings-editor/settings-editor.view.js +316 -0
  162. package/dist/discord/components/settings-editor/settings-editor.view.js.map +1 -0
  163. package/dist/discord/events/create-event.d.ts +5 -0
  164. package/dist/discord/events/create-event.d.ts.map +1 -0
  165. package/dist/discord/events/create-event.js +8 -0
  166. package/dist/discord/events/create-event.js.map +1 -0
  167. package/dist/discord/events/event-router.d.ts +16 -0
  168. package/dist/discord/events/event-router.d.ts.map +1 -0
  169. package/dist/discord/events/event-router.js +51 -0
  170. package/dist/discord/events/event-router.js.map +1 -0
  171. package/dist/discord/events/types.d.ts +16 -0
  172. package/dist/discord/events/types.d.ts.map +1 -0
  173. package/dist/discord/events/types.js +3 -0
  174. package/dist/discord/events/types.js.map +1 -0
  175. package/dist/discord/i18n.d.ts +58 -0
  176. package/dist/discord/i18n.d.ts.map +1 -0
  177. package/dist/discord/i18n.js +173 -0
  178. package/dist/discord/i18n.js.map +1 -0
  179. package/dist/discord/interaction/button.d.ts +39 -0
  180. package/dist/discord/interaction/button.d.ts.map +1 -0
  181. package/dist/discord/interaction/button.js +36 -0
  182. package/dist/discord/interaction/button.js.map +1 -0
  183. package/dist/discord/interaction/interaction-router.d.ts +21 -0
  184. package/dist/discord/interaction/interaction-router.d.ts.map +1 -0
  185. package/dist/discord/interaction/interaction-router.js +24 -0
  186. package/dist/discord/interaction/interaction-router.js.map +1 -0
  187. package/dist/discord/interaction/modal.d.ts +171 -0
  188. package/dist/discord/interaction/modal.d.ts.map +1 -0
  189. package/dist/discord/interaction/modal.js +264 -0
  190. package/dist/discord/interaction/modal.js.map +1 -0
  191. package/dist/discord/interaction/select-menu.d.ts +104 -0
  192. package/dist/discord/interaction/select-menu.d.ts.map +1 -0
  193. package/dist/discord/interaction/select-menu.js +126 -0
  194. package/dist/discord/interaction/select-menu.js.map +1 -0
  195. package/dist/discord/permissions.d.ts +32 -0
  196. package/dist/discord/permissions.d.ts.map +1 -0
  197. package/dist/discord/permissions.js +40 -0
  198. package/dist/discord/permissions.js.map +1 -0
  199. package/dist/discord/placeholder-registry-errors.d.ts +10 -0
  200. package/dist/discord/placeholder-registry-errors.d.ts.map +1 -0
  201. package/dist/discord/placeholder-registry-errors.js +18 -0
  202. package/dist/discord/placeholder-registry-errors.js.map +1 -0
  203. package/dist/discord/placeholder-registry.d.ts +53 -0
  204. package/dist/discord/placeholder-registry.d.ts.map +1 -0
  205. package/dist/discord/placeholder-registry.js +52 -0
  206. package/dist/discord/placeholder-registry.js.map +1 -0
  207. package/dist/discord/presenter.d.ts +26 -0
  208. package/dist/discord/presenter.d.ts.map +1 -0
  209. package/dist/discord/presenter.js +3 -0
  210. package/dist/discord/presenter.js.map +1 -0
  211. package/dist/discord/ui/card.d.ts +114 -0
  212. package/dist/discord/ui/card.d.ts.map +1 -0
  213. package/dist/discord/ui/card.js +162 -0
  214. package/dist/discord/ui/card.js.map +1 -0
  215. package/dist/discord/ui/color-input-errors.d.ts +12 -0
  216. package/dist/discord/ui/color-input-errors.d.ts.map +1 -0
  217. package/dist/discord/ui/color-input-errors.js +21 -0
  218. package/dist/discord/ui/color-input-errors.js.map +1 -0
  219. package/dist/discord/ui/color-input.d.ts +37 -0
  220. package/dist/discord/ui/color-input.d.ts.map +1 -0
  221. package/dist/discord/ui/color-input.js +166 -0
  222. package/dist/discord/ui/color-input.js.map +1 -0
  223. package/dist/discord/ui/colors.d.ts +44 -0
  224. package/dist/discord/ui/colors.d.ts.map +1 -0
  225. package/dist/discord/ui/colors.js +45 -0
  226. package/dist/discord/ui/colors.js.map +1 -0
  227. package/dist/discord/ui/embed.d.ts +16 -0
  228. package/dist/discord/ui/embed.d.ts.map +1 -0
  229. package/dist/discord/ui/embed.js +53 -0
  230. package/dist/discord/ui/embed.js.map +1 -0
  231. package/dist/discord/ui/message-attachments.d.ts +40 -0
  232. package/dist/discord/ui/message-attachments.d.ts.map +1 -0
  233. package/dist/discord/ui/message-attachments.js +27 -0
  234. package/dist/discord/ui/message-attachments.js.map +1 -0
  235. package/dist/errors/business-error.d.ts +41 -0
  236. package/dist/errors/business-error.d.ts.map +1 -0
  237. package/dist/errors/business-error.js +39 -0
  238. package/dist/errors/business-error.js.map +1 -0
  239. package/dist/errors/describe-error.d.ts +29 -0
  240. package/dist/errors/describe-error.d.ts.map +1 -0
  241. package/dist/errors/describe-error.js +49 -0
  242. package/dist/errors/describe-error.js.map +1 -0
  243. package/dist/errors/incident-ref.d.ts +7 -0
  244. package/dist/errors/incident-ref.d.ts.map +1 -0
  245. package/dist/errors/incident-ref.js +13 -0
  246. package/dist/errors/incident-ref.js.map +1 -0
  247. package/dist/errors/index.d.ts +4 -0
  248. package/dist/errors/index.d.ts.map +1 -0
  249. package/dist/errors/index.js +15 -0
  250. package/dist/errors/index.js.map +1 -0
  251. package/dist/i18n/business-message.d.ts +14 -0
  252. package/dist/i18n/business-message.d.ts.map +1 -0
  253. package/dist/i18n/business-message.js +19 -0
  254. package/dist/i18n/business-message.js.map +1 -0
  255. package/dist/i18n/catalog.d.ts +27 -0
  256. package/dist/i18n/catalog.d.ts.map +1 -0
  257. package/dist/i18n/catalog.js +25 -0
  258. package/dist/i18n/catalog.js.map +1 -0
  259. package/dist/i18n/errors.d.ts +10 -0
  260. package/dist/i18n/errors.d.ts.map +1 -0
  261. package/dist/i18n/errors.js +18 -0
  262. package/dist/i18n/errors.js.map +1 -0
  263. package/dist/i18n/index.d.ts +6 -0
  264. package/dist/i18n/index.d.ts.map +1 -0
  265. package/dist/i18n/index.js +16 -0
  266. package/dist/i18n/index.js.map +1 -0
  267. package/dist/i18n/locale.d.ts +22 -0
  268. package/dist/i18n/locale.d.ts.map +1 -0
  269. package/dist/i18n/locale.js +40 -0
  270. package/dist/i18n/locale.js.map +1 -0
  271. package/dist/i18n/translator.d.ts +35 -0
  272. package/dist/i18n/translator.d.ts.map +1 -0
  273. package/dist/i18n/translator.js +40 -0
  274. package/dist/i18n/translator.js.map +1 -0
  275. package/dist/logger.d.ts +28 -0
  276. package/dist/logger.d.ts.map +1 -0
  277. package/dist/logger.js +3 -0
  278. package/dist/logger.js.map +1 -0
  279. package/dist/module/module.d.ts +42 -0
  280. package/dist/module/module.d.ts.map +1 -0
  281. package/dist/module/module.js +3 -0
  282. package/dist/module/module.js.map +1 -0
  283. package/dist/persistence/database.d.ts +24 -0
  284. package/dist/persistence/database.d.ts.map +1 -0
  285. package/dist/persistence/database.js +3 -0
  286. package/dist/persistence/database.js.map +1 -0
  287. package/dist/resilience/retry.d.ts +36 -0
  288. package/dist/resilience/retry.d.ts.map +1 -0
  289. package/dist/resilience/retry.js +78 -0
  290. package/dist/resilience/retry.js.map +1 -0
  291. package/dist/scheduler/errors.d.ts +25 -0
  292. package/dist/scheduler/errors.d.ts.map +1 -0
  293. package/dist/scheduler/errors.js +45 -0
  294. package/dist/scheduler/errors.js.map +1 -0
  295. package/dist/scheduler/scheduler.d.ts +68 -0
  296. package/dist/scheduler/scheduler.d.ts.map +1 -0
  297. package/dist/scheduler/scheduler.js +162 -0
  298. package/dist/scheduler/scheduler.js.map +1 -0
  299. package/package.json +89 -0
  300. package/src/authz/authorizer.ts +76 -0
  301. package/src/authz/require-level-guard.ts +42 -0
  302. package/src/clock.ts +18 -0
  303. package/src/concurrency/keyed-queue.ts +50 -0
  304. package/src/concurrency/run-with-concurrency.ts +44 -0
  305. package/src/config/env.ts +58 -0
  306. package/src/config/errors.ts +21 -0
  307. package/src/datetime.ts +221 -0
  308. package/src/discord/api-errors.ts +91 -0
  309. package/src/discord/channel-exporter.ts +48 -0
  310. package/src/discord/command/autocomplete-dispatcher.ts +54 -0
  311. package/src/discord/command/autocomplete.ts +76 -0
  312. package/src/discord/command/context.ts +85 -0
  313. package/src/discord/command/cooldown-guard.ts +81 -0
  314. package/src/discord/command/create-command.ts +431 -0
  315. package/src/discord/command/create-context-menu-command.ts +227 -0
  316. package/src/discord/command/errors.ts +22 -0
  317. package/src/discord/command/guard.ts +54 -0
  318. package/src/discord/command/localization.ts +11 -0
  319. package/src/discord/command/options.ts +847 -0
  320. package/src/discord/command/permission-guard.ts +74 -0
  321. package/src/discord/command/router.ts +142 -0
  322. package/src/discord/command/types.ts +52 -0
  323. package/src/discord/components/component-router.ts +213 -0
  324. package/src/discord/components/interactive-message/index.ts +18 -0
  325. package/src/discord/components/interactive-message/interactive-message-collector.ts +374 -0
  326. package/src/discord/components/interactive-message/mount-interactive-message.ts +71 -0
  327. package/src/discord/components/paginator/index.ts +17 -0
  328. package/src/discord/components/paginator/mount-paginator.ts +69 -0
  329. package/src/discord/components/paginator/page-state.ts +45 -0
  330. package/src/discord/components/paginator/paginator-buttons.ts +60 -0
  331. package/src/discord/components/paginator/paginator-view.ts +61 -0
  332. package/src/discord/components/persistent-paginator.ts +197 -0
  333. package/src/discord/components/settings-editor/index.ts +41 -0
  334. package/src/discord/components/settings-editor/mount-settings-editor.ts +646 -0
  335. package/src/discord/components/settings-editor/navigation.ts +67 -0
  336. package/src/discord/components/settings-editor/settings-editor-card.view.ts +344 -0
  337. package/src/discord/components/settings-editor/settings-editor-field-modal.ts +538 -0
  338. package/src/discord/components/settings-editor/settings-editor-fields.ts +290 -0
  339. package/src/discord/components/settings-editor/settings-editor.view.ts +654 -0
  340. package/src/discord/events/create-event.ts +7 -0
  341. package/src/discord/events/event-router.ts +53 -0
  342. package/src/discord/events/types.ts +17 -0
  343. package/src/discord/i18n.ts +173 -0
  344. package/src/discord/interaction/button.ts +75 -0
  345. package/src/discord/interaction/interaction-router.ts +28 -0
  346. package/src/discord/interaction/modal.ts +505 -0
  347. package/src/discord/interaction/select-menu.ts +246 -0
  348. package/src/discord/permissions.ts +57 -0
  349. package/src/discord/placeholder-registry-errors.ts +11 -0
  350. package/src/discord/placeholder-registry.ts +77 -0
  351. package/src/discord/presenter.ts +26 -0
  352. package/src/discord/ui/card.ts +303 -0
  353. package/src/discord/ui/color-input-errors.ts +17 -0
  354. package/src/discord/ui/color-input.ts +179 -0
  355. package/src/discord/ui/colors.ts +58 -0
  356. package/src/discord/ui/embed.ts +64 -0
  357. package/src/discord/ui/message-attachments.ts +57 -0
  358. package/src/errors/business-error.ts +49 -0
  359. package/src/errors/describe-error.ts +73 -0
  360. package/src/errors/incident-ref.ts +10 -0
  361. package/src/errors/index.ts +12 -0
  362. package/src/i18n/business-message.ts +23 -0
  363. package/src/i18n/catalog.ts +41 -0
  364. package/src/i18n/errors.ts +11 -0
  365. package/src/i18n/index.ts +11 -0
  366. package/src/i18n/locale.ts +41 -0
  367. package/src/i18n/translator.ts +78 -0
  368. package/src/logger.ts +28 -0
  369. package/src/module/module.ts +42 -0
  370. package/src/persistence/database.ts +23 -0
  371. package/src/resilience/retry.ts +102 -0
  372. package/src/scheduler/errors.ts +40 -0
  373. package/src/scheduler/scheduler.ts +248 -0
package/package.json ADDED
@@ -0,0 +1,89 @@
1
+ {
2
+ "name": "@azurioh/discord-kernel",
3
+ "version": "0.1.0",
4
+ "description": "Framework kernel for Discord bots: command DSL, guards, interaction routing, composed components, i18n, ports (Presenter, Logger, DatabaseConnection, Authorizer, Scheduler).",
5
+ "keywords": [
6
+ "discord",
7
+ "discord.js",
8
+ "discord-bot",
9
+ "framework",
10
+ "clean-architecture",
11
+ "hexagonal",
12
+ "typescript",
13
+ "i18n"
14
+ ],
15
+ "license": "MIT",
16
+ "author": "Alan Cunin (https://github.com/Azurioh)",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/Azurioh/discord-kernel.git"
20
+ },
21
+ "bugs": {
22
+ "url": "https://github.com/Azurioh/discord-kernel/issues"
23
+ },
24
+ "homepage": "https://github.com/Azurioh/discord-kernel#readme",
25
+ "engines": {
26
+ "node": ">=22.12.0"
27
+ },
28
+ "publishConfig": {
29
+ "access": "public"
30
+ },
31
+ "exports": {
32
+ "./package.json": "./package.json",
33
+ "./errors": {
34
+ "types": "./dist/errors/index.d.ts",
35
+ "default": "./dist/errors/index.js"
36
+ },
37
+ "./i18n": {
38
+ "types": "./dist/i18n/index.d.ts",
39
+ "default": "./dist/i18n/index.js"
40
+ },
41
+ "./discord/components/interactive-message": {
42
+ "types": "./dist/discord/components/interactive-message/index.d.ts",
43
+ "default": "./dist/discord/components/interactive-message/index.js"
44
+ },
45
+ "./discord/components/paginator": {
46
+ "types": "./dist/discord/components/paginator/index.d.ts",
47
+ "default": "./dist/discord/components/paginator/index.js"
48
+ },
49
+ "./discord/components/settings-editor": {
50
+ "types": "./dist/discord/components/settings-editor/index.d.ts",
51
+ "default": "./dist/discord/components/settings-editor/index.js"
52
+ },
53
+ "./*": {
54
+ "types": "./dist/*.d.ts",
55
+ "default": "./dist/*.js"
56
+ }
57
+ },
58
+ "files": [
59
+ "dist",
60
+ "src"
61
+ ],
62
+ "scripts": {
63
+ "build": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json",
64
+ "typecheck": "tsc -p tsconfig.json",
65
+ "lint": "biome ci",
66
+ "format": "biome check --write",
67
+ "knip": "knip",
68
+ "test": "vitest run",
69
+ "test:watch": "vitest",
70
+ "prepublishOnly": "npm run build"
71
+ },
72
+ "dependencies": {
73
+ "node-cron": "4.6.0"
74
+ },
75
+ "peerDependencies": {
76
+ "discord.js": "^14.27.0"
77
+ },
78
+ "devDependencies": {
79
+ "@biomejs/biome": "2.5.7",
80
+ "@types/node": "24.13.3",
81
+ "discord.js": "14.27.0",
82
+ "knip": "^6.15.0",
83
+ "tsc-alias": "^1.8.16",
84
+ "typescript": "^5.9.3",
85
+ "vite-tsconfig-paths": "^6.1.1",
86
+ "vitest": "^4.1.8"
87
+ },
88
+ "packageManager": "pnpm@11.20.0"
89
+ }
@@ -0,0 +1,76 @@
1
+ /** Permission tiers, lowest to highest. `editor` implies `viewer`. */
2
+ export type PermissionLevel = "viewer" | "editor";
3
+
4
+ /** Numeric rank for comparing levels (`editor` ≥ `viewer`). */
5
+ export const LEVEL_RANK: Record<PermissionLevel, number> = {
6
+ viewer: 1,
7
+ editor: 2,
8
+ };
9
+
10
+ /** A single grant: either a role or a user, at a given level. */
11
+ export interface PermissionGrant {
12
+ readonly targetType: "role" | "user";
13
+ readonly targetId: string;
14
+ readonly level: PermissionLevel;
15
+ }
16
+
17
+ /** The full set of grants, split by target kind for cheap resolution. */
18
+ export interface PermissionGrants {
19
+ readonly roles: Record<string, PermissionLevel>;
20
+ readonly users: Record<string, PermissionLevel>;
21
+ }
22
+
23
+ /**
24
+ * Cross-cutting authorization port for feature-level permission tiers (as
25
+ * opposed to Discord's own per-command `defaultMemberPermissions`/guild
26
+ * permissions, which gate *visibility* and native capabilities). A module
27
+ * grants a role or a user a level; a guard checks whether a member meets the
28
+ * level required for an action.
29
+ *
30
+ * Deliberately has no notion of "admin": a bot-wide administrator bypass is a
31
+ * Discord permission concern, composed at the guard level with
32
+ * `anyOf(createPermissionGuard([Administrator]), requireLevel(...))` — keeping
33
+ * this port focused on the one thing it owns, feature-level grants.
34
+ */
35
+ export interface Authorizer {
36
+ /** Whether the user (directly, or via one of their roles) meets `required`. */
37
+ meetsLevel(
38
+ userId: string,
39
+ roleIds: readonly string[],
40
+ required: PermissionLevel,
41
+ ): Promise<boolean>;
42
+ grantRole(roleId: string, level: PermissionLevel): Promise<void>;
43
+ grantUser(userId: string, level: PermissionLevel): Promise<void>;
44
+ /** Returns whether a grant existed to remove. */
45
+ revokeRole(roleId: string): Promise<boolean>;
46
+ /** Returns whether a grant existed to remove. */
47
+ revokeUser(userId: string): Promise<boolean>;
48
+ listGrants(): Promise<PermissionGrants>;
49
+ }
50
+
51
+ /**
52
+ * Shared resolution used by every adapter: a direct user grant wins over role
53
+ * grants; otherwise the highest matching role level applies. Returns whether
54
+ * the resolved level meets `required`.
55
+ */
56
+ export function meetsResolvedLevel(
57
+ grants: PermissionGrants,
58
+ userId: string,
59
+ roleIds: readonly string[],
60
+ required: PermissionLevel,
61
+ ): boolean {
62
+ const userLevel = grants.users[userId];
63
+ let resolved: PermissionLevel | null = userLevel ?? null;
64
+ if (!resolved) {
65
+ for (const roleId of roleIds) {
66
+ const roleLevel = grants.roles[roleId];
67
+ if (roleLevel && (!resolved || LEVEL_RANK[roleLevel] > LEVEL_RANK[resolved])) {
68
+ resolved = roleLevel;
69
+ }
70
+ }
71
+ }
72
+ if (!resolved) {
73
+ return false;
74
+ }
75
+ return LEVEL_RANK[resolved] >= LEVEL_RANK[required];
76
+ }
@@ -0,0 +1,42 @@
1
+ import type { BaseInteraction, CommandInteraction } from "discord.js";
2
+ import type { Authorizer, PermissionLevel } from "@/authz/authorizer";
3
+ import type { Guard } from "@/discord/command/guard";
4
+ import type { LocalizedText } from "@/i18n";
5
+
6
+ /**
7
+ * Role IDs held by the interacting member, resolved without a network call:
8
+ * `interaction.member.roles` is either the cached `GuildMemberRoleManager` (a
9
+ * cached-guild interaction) or a plain array of IDs (the raw API shape for an
10
+ * uncached one) — both are handled so the guard never has to fetch the member.
11
+ *
12
+ * Typed on the base `Interaction`, not on the command flavour a guard takes: a
13
+ * control clicked inside a message carries the member exactly the same way, and
14
+ * a rule that has to hold on both paths must be able to ask both.
15
+ */
16
+ export function memberRoleIds(interaction: BaseInteraction): string[] {
17
+ const member = interaction.member;
18
+ if (!member || typeof member !== "object" || !("roles" in member)) {
19
+ return [];
20
+ }
21
+ const roles = member.roles;
22
+ return Array.isArray(roles) ? roles : Array.from(roles.cache.keys());
23
+ }
24
+
25
+ /**
26
+ * Deny unless the member meets `required` on the injected {@link Authorizer}
27
+ * (directly, or via one of their roles). Compose with a Discord-permission
28
+ * guard through `anyOf(...)` when a bot-wide administrator bypass is wanted —
29
+ * this guard alone knows nothing about Discord permissions.
30
+ */
31
+ export function requireLevel(
32
+ authz: Authorizer,
33
+ required: PermissionLevel,
34
+ denyMessage: LocalizedText,
35
+ ): Guard {
36
+ return {
37
+ async check(interaction: CommandInteraction) {
38
+ const ok = await authz.meetsLevel(interaction.user.id, memberRoleIds(interaction), required);
39
+ return ok ? { ok: true } : { ok: false, message: denyMessage };
40
+ },
41
+ };
42
+ }
package/src/clock.ts ADDED
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Abstraction over the current time. Injecting a clock (instead of calling
3
+ * `new Date()` directly) lets domain/application code be tested deterministically
4
+ * and keeps those layers free of ambient I/O.
5
+ */
6
+ export interface Clock {
7
+ now(): Date;
8
+ }
9
+
10
+ /** The real clock, backed by the system time. */
11
+ export const systemClock: Clock = {
12
+ now: () => new Date(),
13
+ };
14
+
15
+ /** A fixed clock for tests. */
16
+ export function fixedClock(instant: Date): Clock {
17
+ return { now: () => new Date(instant) };
18
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Serialises asynchronous work per key while leaving distinct keys concurrent.
3
+ */
4
+ export interface KeyedQueue {
5
+ /** Run `task` after every task already queued for `key` has settled. */
6
+ run<T>(key: string, task: () => Promise<T>): Promise<T>;
7
+ /** Number of keys with work in flight — exposed so tests can assert no leak. */
8
+ readonly size: number;
9
+ }
10
+
11
+ /**
12
+ * Gateway handlers fire concurrently for the same entity (two rapid events on one
13
+ * channel, a member joining twice, an event replay after a resume), so
14
+ * read-modify-write logic keyed on that entity interleaves and corrupts state.
15
+ * Chaining per key restores ordering without a global lock, which would
16
+ * needlessly serialise unrelated guilds.
17
+ */
18
+ export function createKeyedQueue(): KeyedQueue {
19
+ const chains = new Map<string, Promise<unknown>>();
20
+
21
+ return {
22
+ get size() {
23
+ return chains.size;
24
+ },
25
+ run<T>(key: string, task: () => Promise<T>): Promise<T> {
26
+ const previous = chains.get(key) ?? Promise.resolve();
27
+ // The chain continues on the *settled* tail: a rejected task must not
28
+ // poison the queue for the tasks behind it, while `result` keeps the
29
+ // rejection observable by this caller alone.
30
+ const settledTail = previous.then(
31
+ () => undefined,
32
+ () => undefined,
33
+ );
34
+ const result = settledTail.then(task);
35
+ const tail = result.then(
36
+ () => undefined,
37
+ () => undefined,
38
+ );
39
+ chains.set(key, tail);
40
+ // Only the task that is still the tail may drop the entry, otherwise a
41
+ // later enqueue would be forgotten and the Map would grow unbounded.
42
+ void tail.then(() => {
43
+ if (chains.get(key) === tail) {
44
+ chains.delete(key);
45
+ }
46
+ });
47
+ return result;
48
+ },
49
+ };
50
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Runs every task in `items` through `worker`, never letting more than `limit`
3
+ * run at once, and resolves once all have settled. Results are returned in the
4
+ * same order as `items`, regardless of completion order.
5
+ *
6
+ * Exists so callers that must fan out many independent async calls (e.g.
7
+ * fetching a reaction's user list per distinct emoji across thousands of
8
+ * messages) can bound how many are in flight without going fully sequential —
9
+ * without pulling in an external dependency for what is, in essence, a small
10
+ * counter and a queue (the same shape of problem `keyed-queue.ts` already
11
+ * solves for *serialised* per-key work, just bounding *parallel* work here
12
+ * instead).
13
+ */
14
+ export async function runWithConcurrency<T, R>(
15
+ items: readonly T[],
16
+ limit: number,
17
+ worker: (item: T, index: number) => Promise<R>,
18
+ ): Promise<R[]> {
19
+ if (items.length === 0) {
20
+ return [];
21
+ }
22
+
23
+ // `noUncheckedIndexedAccess` makes every `results[i]` a possible `undefined`
24
+ // at the type level; asserting it away here is correct because every index
25
+ // is written exactly once below, before this array is ever returned.
26
+ const results = new Array<R>(items.length) as R[];
27
+ let nextIndex = 0;
28
+
29
+ async function runNext(): Promise<void> {
30
+ const currentIndex = nextIndex;
31
+ nextIndex += 1;
32
+ if (currentIndex >= items.length) {
33
+ return;
34
+ }
35
+ const item = items[currentIndex] as T;
36
+ results[currentIndex] = await worker(item, currentIndex);
37
+ await runNext();
38
+ }
39
+
40
+ const workerCount = Math.max(1, Math.min(limit, items.length));
41
+ await Promise.all(Array.from({ length: workerCount }, runNext));
42
+
43
+ return results;
44
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Generic, dependency-free helpers for reading and validating environment
3
+ * variables. The concrete application config (which keys exist, their defaults)
4
+ * is assembled by the composition root in `bootstrap/config.ts` — the core only
5
+ * provides the primitives so any bot can declare its own shape.
6
+ */
7
+
8
+ import { InvalidEnvError, MissingEnvError } from "@/config/errors";
9
+
10
+ type Env = Record<string, string | undefined>;
11
+
12
+ /** Read a required variable, throwing {@link MissingEnvError} when absent. */
13
+ export function requireEnv(key: string, env: Env = process.env): string {
14
+ const value = env[key];
15
+ if (value === undefined || value.trim() === "") {
16
+ throw new MissingEnvError(key);
17
+ }
18
+ return value;
19
+ }
20
+
21
+ /** Read an optional variable, returning `fallback` (or `undefined`) when absent. */
22
+ export function optionalEnv(
23
+ key: string,
24
+ fallback?: string,
25
+ env: Env = process.env,
26
+ ): string | undefined {
27
+ const value = env[key];
28
+ if (value === undefined || value.trim() === "") {
29
+ return fallback;
30
+ }
31
+ return value;
32
+ }
33
+
34
+ /** Read a boolean variable (`true`/`1`/`yes` are truthy). */
35
+ export function boolEnv(key: string, fallback: boolean, env: Env = process.env): boolean {
36
+ const value = env[key]?.trim().toLowerCase();
37
+ if (value === undefined || value === "") {
38
+ return fallback;
39
+ }
40
+ return value === "true" || value === "1" || value === "yes";
41
+ }
42
+
43
+ /** Read a variable constrained to a fixed set of values. */
44
+ export function enumEnv<const T extends readonly string[]>(
45
+ key: string,
46
+ allowed: T,
47
+ fallback: T[number],
48
+ env: Env = process.env,
49
+ ): T[number] {
50
+ const value = env[key]?.trim();
51
+ if (value === undefined || value === "") {
52
+ return fallback;
53
+ }
54
+ if (!allowed.includes(value)) {
55
+ throw new InvalidEnvError(key, `one of ${allowed.join(", ")}`);
56
+ }
57
+ return value as T[number];
58
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Failures raised while reading environment variables. They live apart from the
3
+ * reading logic so a consumer can catch or re-map them without pulling in the
4
+ * `env` helpers, and so the helpers file stays focused on behaviour.
5
+ */
6
+
7
+ /** Raised when a required environment variable is missing or empty. */
8
+ export class MissingEnvError extends Error {
9
+ constructor(key: string) {
10
+ super(`Missing required environment variable: ${key}`);
11
+ this.name = "MissingEnvError";
12
+ }
13
+ }
14
+
15
+ /** Raised when an environment variable holds an invalid value. */
16
+ export class InvalidEnvError extends Error {
17
+ constructor(key: string, expected: string) {
18
+ super(`Invalid environment variable ${key}: expected ${expected}`);
19
+ this.name = "InvalidEnvError";
20
+ }
21
+ }
@@ -0,0 +1,221 @@
1
+ /**
2
+ * Wall-clock datetime input grammar: `YYYY-MM-DD` with an optional
3
+ * ` HH:MM` / `THH:MM` time (seconds optional). Matches the format documented to
4
+ * users in the slash-command options.
5
+ */
6
+ const WALL_CLOCK_PATTERN = /^(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2})(?::(\d{2}))?)?$/;
7
+
8
+ interface WallClock {
9
+ year: number;
10
+ month: number;
11
+ day: number;
12
+ hour: number;
13
+ minute: number;
14
+ second: number;
15
+ }
16
+
17
+ function parseWallClock(input: string): WallClock | null {
18
+ const match = WALL_CLOCK_PATTERN.exec(input.trim());
19
+ if (!match) {
20
+ return null;
21
+ }
22
+ const [, year, month, day, hour, minute, second] = match;
23
+ const wallClock: WallClock = {
24
+ year: Number(year),
25
+ month: Number(month),
26
+ day: Number(day),
27
+ hour: hour ? Number(hour) : 0,
28
+ minute: minute ? Number(minute) : 0,
29
+ second: second ? Number(second) : 0,
30
+ };
31
+ return isRealCalendarInstant(wallClock) ? wallClock : null;
32
+ }
33
+
34
+ /**
35
+ * Reject impossible values the regex alone allows (month 13, day 32, hour 25)
36
+ * by round-tripping through `Date.UTC` and checking nothing overflowed.
37
+ */
38
+ function isRealCalendarInstant(wallClock: WallClock): boolean {
39
+ const date = new Date(
40
+ Date.UTC(
41
+ wallClock.year,
42
+ wallClock.month - 1,
43
+ wallClock.day,
44
+ wallClock.hour,
45
+ wallClock.minute,
46
+ wallClock.second,
47
+ ),
48
+ );
49
+ return (
50
+ date.getUTCFullYear() === wallClock.year &&
51
+ date.getUTCMonth() === wallClock.month - 1 &&
52
+ date.getUTCDate() === wallClock.day &&
53
+ date.getUTCHours() === wallClock.hour &&
54
+ date.getUTCMinutes() === wallClock.minute &&
55
+ date.getUTCSeconds() === wallClock.second
56
+ );
57
+ }
58
+
59
+ /**
60
+ * Offset, in milliseconds, between the given instant and how that instant reads
61
+ * as a wall clock in `timeZone` (positive when the zone is ahead of UTC).
62
+ */
63
+ function timeZoneOffsetMs(timestamp: number, timeZone: string): number {
64
+ const formatter = new Intl.DateTimeFormat("en-US", {
65
+ timeZone,
66
+ hourCycle: "h23",
67
+ year: "numeric",
68
+ month: "2-digit",
69
+ day: "2-digit",
70
+ hour: "2-digit",
71
+ minute: "2-digit",
72
+ second: "2-digit",
73
+ });
74
+ const parts = new Map<string, number>();
75
+ for (const { type, value } of formatter.formatToParts(new Date(timestamp))) {
76
+ if (type !== "literal") {
77
+ parts.set(type, Number(value));
78
+ }
79
+ }
80
+ const read = (key: string): number => {
81
+ const value = parts.get(key);
82
+ if (value === undefined) {
83
+ throw new RangeError(`Missing "${key}" in formatted parts for time zone "${timeZone}"`);
84
+ }
85
+ return value;
86
+ };
87
+ const asUtc = Date.UTC(
88
+ read("year"),
89
+ read("month") - 1,
90
+ read("day"),
91
+ read("hour"),
92
+ read("minute"),
93
+ read("second"),
94
+ );
95
+ return asUtc - timestamp;
96
+ }
97
+
98
+ /**
99
+ * Interpret a wall-clock datetime string as local time in `timeZone` and return
100
+ * the matching UTC instant. Discord renders stored timestamps in each viewer's
101
+ * own timezone, so an event entered as "18:00" guild-local must be stored as the
102
+ * equivalent UTC instant — otherwise it drifts by the guild's UTC offset.
103
+ * @throws RangeError When the input does not match the wall-clock grammar.
104
+ */
105
+ export function zonedWallClockToUtc(input: string, timeZone: string): Date {
106
+ const wallClock = parseWallClock(input);
107
+ if (!wallClock) {
108
+ throw new RangeError(`Invalid wall-clock datetime: "${input}"`);
109
+ }
110
+ const utcGuess = Date.UTC(
111
+ wallClock.year,
112
+ wallClock.month - 1,
113
+ wallClock.day,
114
+ wallClock.hour,
115
+ wallClock.minute,
116
+ wallClock.second,
117
+ );
118
+ // Two passes: the offset read at `utcGuess` can be wrong across a DST
119
+ // transition (the guessed instant may land on the far side of the boundary
120
+ // from the target). Re-reading the offset at the first candidate pins the
121
+ // correct one for every time except the ~1h/year that either does not exist
122
+ // (spring-forward gap) or is ambiguous (fall-back overlap), which resolve
123
+ // deterministically to one side.
124
+ const firstOffset = timeZoneOffsetMs(utcGuess, timeZone);
125
+ const secondOffset = timeZoneOffsetMs(utcGuess - firstOffset, timeZone);
126
+ return new Date(utcGuess - secondOffset);
127
+ }
128
+
129
+ /** Whether `input` is a valid wall-clock datetime string. */
130
+ export function isValidWallClock(input: string): boolean {
131
+ return parseWallClock(input) !== null;
132
+ }
133
+
134
+ /** `YYYY-MM-DD` of a date-only value, read in UTC (matches `Date`-only options). */
135
+ export function dayString(date: Date): string {
136
+ return date.toISOString().slice(0, 10);
137
+ }
138
+
139
+ /** Start of `date`'s calendar day (00:00:00) anchored to `timeZone`. */
140
+ export function startOfGuildDay(date: Date, timeZone: string): Date {
141
+ return zonedWallClockToUtc(`${dayString(date)} 00:00:00`, timeZone);
142
+ }
143
+
144
+ /** End of `date`'s calendar day (23:59:59) anchored to `timeZone`. */
145
+ export function endOfGuildDay(date: Date, timeZone: string): Date {
146
+ return zonedWallClockToUtc(`${dayString(date)} 23:59:59`, timeZone);
147
+ }
148
+
149
+ /**
150
+ * Monday 00:00 (guild-local) of the week following `reference`'s week. Used to
151
+ * anchor "next week"/"week after next" ranges to the guild's own calendar
152
+ * rather than to UTC day boundaries, which can disagree with the guild's local
153
+ * week near midnight around the timezone offset.
154
+ */
155
+ export function startOfNextGuildWeek(reference: Date, timeZone: string): Date {
156
+ // Both the weekday *and* the calendar date must be read in `timeZone`, not
157
+ // UTC: near midnight, the guild-local day can already differ from the UTC
158
+ // day, and mixing the two would misplace the coming Monday by a day.
159
+ const formatter = new Intl.DateTimeFormat("en-US", {
160
+ timeZone,
161
+ weekday: "short",
162
+ year: "numeric",
163
+ month: "2-digit",
164
+ day: "2-digit",
165
+ });
166
+ const weekdayIndex: Record<string, number> = {
167
+ Sun: 0,
168
+ Mon: 1,
169
+ Tue: 2,
170
+ Wed: 3,
171
+ Thu: 4,
172
+ Fri: 5,
173
+ Sat: 6,
174
+ };
175
+ const parts = new Map<string, string>();
176
+ for (const { type, value } of formatter.formatToParts(reference)) {
177
+ if (type !== "literal") {
178
+ parts.set(type, value);
179
+ }
180
+ }
181
+ const currentDay = weekdayIndex[parts.get("weekday") ?? ""] ?? 0;
182
+ // ISO week starts on Monday: ahead-days is 0 only when already Monday and we
183
+ // still want *next* week's Monday, hence the `=== 1 ? 7` special-case.
184
+ const daysUntilNextMonday = currentDay === 1 ? 7 : (8 - currentDay) % 7;
185
+ // Advance the *calendar date* (a pure Y-M-D operation, DST-agnostic) before
186
+ // re-anchoring to the timezone — advancing the UTC instant instead would
187
+ // drift by an hour across a DST transition inside the date range.
188
+ const year = Number(parts.get("year"));
189
+ const month = Number(parts.get("month"));
190
+ const day = Number(parts.get("day"));
191
+ const nextMondayUtcCalendarDay = new Date(Date.UTC(year, month - 1, day + daysUntilNextMonday));
192
+ return startOfGuildDay(nextMondayUtcCalendarDay, timeZone);
193
+ }
194
+ /**
195
+ * Add `days` guild-local calendar days to `instant`, re-anchoring to
196
+ * `timeZone`'s midnight. `instant` is assumed to already be a guild-local
197
+ * midnight (e.g. the result of {@link startOfGuildDay} or
198
+ * {@link startOfNextGuildWeek}); adding a fixed millisecond offset instead
199
+ * would drift by the DST delta whenever the interval crosses a transition —
200
+ * this reads the calendar date in `timeZone` first, so the result is always
201
+ * that timezone's actual midnight `days` later.
202
+ */
203
+ export function addGuildDays(instant: Date, days: number, timeZone: string): Date {
204
+ const formatter = new Intl.DateTimeFormat("en-US", {
205
+ timeZone,
206
+ year: "numeric",
207
+ month: "2-digit",
208
+ day: "2-digit",
209
+ });
210
+ const parts = new Map<string, string>();
211
+ for (const { type, value } of formatter.formatToParts(instant)) {
212
+ if (type !== "literal") {
213
+ parts.set(type, value);
214
+ }
215
+ }
216
+ const year = Number(parts.get("year"));
217
+ const month = Number(parts.get("month"));
218
+ const day = Number(parts.get("day"));
219
+ const shiftedUtcCalendarDay = new Date(Date.UTC(year, month - 1, day + days));
220
+ return startOfGuildDay(shiftedUtcCalendarDay, timeZone);
221
+ }