@azurioh/discord-kernel 0.1.0 → 1.0.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 (529) hide show
  1. package/README.md +318 -1
  2. package/dist/authz/require-level-guard.d.ts +1 -1
  3. package/dist/authz/require-level-guard.d.ts.map +1 -1
  4. package/dist/color.d.ts +42 -0
  5. package/dist/color.d.ts.map +1 -0
  6. package/dist/color.js +130 -0
  7. package/dist/color.js.map +1 -0
  8. package/dist/discord/command/all-required.d.ts +8 -0
  9. package/dist/discord/command/all-required.d.ts.map +1 -0
  10. package/dist/discord/command/all-required.js +12 -0
  11. package/dist/discord/command/all-required.js.map +1 -0
  12. package/dist/discord/command/autocomplete-dispatcher.d.ts.map +1 -1
  13. package/dist/discord/command/autocomplete-dispatcher.js +2 -1
  14. package/dist/discord/command/autocomplete-dispatcher.js.map +1 -1
  15. package/dist/discord/command/autocomplete-limits.d.ts +3 -0
  16. package/dist/discord/command/autocomplete-limits.d.ts.map +1 -0
  17. package/dist/discord/command/autocomplete-limits.js +6 -0
  18. package/dist/discord/command/autocomplete-limits.js.map +1 -0
  19. package/dist/discord/command/autocomplete.d.ts +6 -1
  20. package/dist/discord/command/autocomplete.d.ts.map +1 -1
  21. package/dist/discord/command/autocomplete.js +6 -6
  22. package/dist/discord/command/autocomplete.js.map +1 -1
  23. package/dist/discord/command/command-responders.d.ts +26 -0
  24. package/dist/discord/command/command-responders.d.ts.map +1 -0
  25. package/dist/discord/command/command-responders.js +16 -0
  26. package/dist/discord/command/command-responders.js.map +1 -0
  27. package/dist/discord/command/context.d.ts +8 -17
  28. package/dist/discord/command/context.d.ts.map +1 -1
  29. package/dist/discord/command/context.js +8 -37
  30. package/dist/discord/command/context.js.map +1 -1
  31. package/dist/discord/command/cooldown-guard.d.ts +1 -1
  32. package/dist/discord/command/cooldown-guard.d.ts.map +1 -1
  33. package/dist/discord/command/create-command.d.ts +1 -8
  34. package/dist/discord/command/create-command.d.ts.map +1 -1
  35. package/dist/discord/command/create-command.js +17 -65
  36. package/dist/discord/command/create-command.js.map +1 -1
  37. package/dist/discord/command/create-context-menu-command.d.ts +3 -9
  38. package/dist/discord/command/create-context-menu-command.d.ts.map +1 -1
  39. package/dist/discord/command/create-context-menu-command.js +14 -65
  40. package/dist/discord/command/create-context-menu-command.js.map +1 -1
  41. package/dist/discord/command/errors.d.ts +1 -1
  42. package/dist/discord/command/errors.d.ts.map +1 -1
  43. package/dist/discord/command/errors.js +2 -2
  44. package/dist/discord/command/errors.js.map +1 -1
  45. package/dist/discord/command/guard.d.ts +15 -8
  46. package/dist/discord/command/guard.d.ts.map +1 -1
  47. package/dist/discord/command/guard.js +4 -4
  48. package/dist/discord/command/guard.js.map +1 -1
  49. package/dist/discord/command/options.d.ts +1 -10
  50. package/dist/discord/command/options.d.ts.map +1 -1
  51. package/dist/discord/command/options.js +127 -320
  52. package/dist/discord/command/options.js.map +1 -1
  53. package/dist/discord/command/passes-guard.d.ts +14 -0
  54. package/dist/discord/command/passes-guard.d.ts.map +1 -0
  55. package/dist/discord/command/passes-guard.js +27 -0
  56. package/dist/discord/command/passes-guard.js.map +1 -0
  57. package/dist/discord/command/permission-guard.d.ts +1 -1
  58. package/dist/discord/command/permission-guard.d.ts.map +1 -1
  59. package/dist/discord/command/render-command-failure.d.ts +23 -0
  60. package/dist/discord/command/render-command-failure.d.ts.map +1 -0
  61. package/dist/discord/command/render-command-failure.js +40 -0
  62. package/dist/discord/command/render-command-failure.js.map +1 -0
  63. package/dist/discord/command/route-key.d.ts +8 -0
  64. package/dist/discord/command/route-key.d.ts.map +1 -0
  65. package/dist/discord/command/route-key.js +13 -0
  66. package/dist/discord/command/route-key.js.map +1 -0
  67. package/dist/discord/command/router.d.ts +32 -7
  68. package/dist/discord/command/router.d.ts.map +1 -1
  69. package/dist/discord/command/router.js +48 -10
  70. package/dist/discord/command/router.js.map +1 -1
  71. package/dist/discord/command/send-embed.d.ts +14 -0
  72. package/dist/discord/command/send-embed.d.ts.map +1 -0
  73. package/dist/discord/command/send-embed.js +37 -0
  74. package/dist/discord/command/send-embed.js.map +1 -0
  75. package/dist/discord/command/types.d.ts +7 -1
  76. package/dist/discord/command/types.d.ts.map +1 -1
  77. package/dist/discord/components/component-access.d.ts +12 -0
  78. package/dist/discord/components/component-access.d.ts.map +1 -0
  79. package/dist/discord/components/component-access.js +21 -0
  80. package/dist/discord/components/component-access.js.map +1 -0
  81. package/dist/discord/components/component-router.d.ts +37 -15
  82. package/dist/discord/components/component-router.d.ts.map +1 -1
  83. package/dist/discord/components/component-router.js +44 -32
  84. package/dist/discord/components/component-router.js.map +1 -1
  85. package/dist/discord/components/interactive-message/index.d.ts +3 -1
  86. package/dist/discord/components/interactive-message/index.d.ts.map +1 -1
  87. package/dist/discord/components/interactive-message/index.js +5 -3
  88. package/dist/discord/components/interactive-message/index.js.map +1 -1
  89. package/dist/discord/components/interactive-message/interactive-message-collector.d.ts +3 -28
  90. package/dist/discord/components/interactive-message/interactive-message-collector.d.ts.map +1 -1
  91. package/dist/discord/components/interactive-message/interactive-message-collector.js +6 -53
  92. package/dist/discord/components/interactive-message/interactive-message-collector.js.map +1 -1
  93. package/dist/discord/components/interactive-message/message-edit-options.d.ts +18 -0
  94. package/dist/discord/components/interactive-message/message-edit-options.d.ts.map +1 -0
  95. package/dist/discord/components/interactive-message/message-edit-options.js +40 -0
  96. package/dist/discord/components/interactive-message/message-edit-options.js.map +1 -0
  97. package/dist/discord/components/interactive-message/mount-interactive-message.d.ts +2 -1
  98. package/dist/discord/components/interactive-message/mount-interactive-message.d.ts.map +1 -1
  99. package/dist/discord/components/interactive-message/mount-interactive-message.js +4 -2
  100. package/dist/discord/components/interactive-message/mount-interactive-message.js.map +1 -1
  101. package/dist/discord/components/interactive-message/state-store.d.ts +12 -0
  102. package/dist/discord/components/interactive-message/state-store.d.ts.map +1 -0
  103. package/dist/discord/components/interactive-message/state-store.js +14 -0
  104. package/dist/discord/components/interactive-message/state-store.js.map +1 -0
  105. package/dist/discord/components/paginator/mount-paginator.js +7 -5
  106. package/dist/discord/components/paginator/mount-paginator.js.map +1 -1
  107. package/dist/discord/components/paginator/paginator-view.d.ts +1 -1
  108. package/dist/discord/components/paginator/paginator-view.d.ts.map +1 -1
  109. package/dist/discord/components/persistent-paginator.d.ts +1 -1
  110. package/dist/discord/components/persistent-paginator.d.ts.map +1 -1
  111. package/dist/discord/components/persistent-paginator.js +6 -6
  112. package/dist/discord/components/persistent-paginator.js.map +1 -1
  113. package/dist/discord/components/settings-editor/from-declaration-controls.d.ts +67 -0
  114. package/dist/discord/components/settings-editor/from-declaration-controls.d.ts.map +1 -0
  115. package/dist/discord/components/settings-editor/from-declaration-controls.js +245 -0
  116. package/dist/discord/components/settings-editor/from-declaration-controls.js.map +1 -0
  117. package/dist/discord/components/settings-editor/from-declaration-display.d.ts +23 -0
  118. package/dist/discord/components/settings-editor/from-declaration-display.d.ts.map +1 -0
  119. package/dist/discord/components/settings-editor/from-declaration-display.js +114 -0
  120. package/dist/discord/components/settings-editor/from-declaration-display.js.map +1 -0
  121. package/dist/discord/components/settings-editor/from-declaration-layout.d.ts +33 -0
  122. package/dist/discord/components/settings-editor/from-declaration-layout.d.ts.map +1 -0
  123. package/dist/discord/components/settings-editor/from-declaration-layout.js +178 -0
  124. package/dist/discord/components/settings-editor/from-declaration-layout.js.map +1 -0
  125. package/dist/discord/components/settings-editor/from-declaration-search.d.ts +27 -0
  126. package/dist/discord/components/settings-editor/from-declaration-search.d.ts.map +1 -0
  127. package/dist/discord/components/settings-editor/from-declaration-search.js +58 -0
  128. package/dist/discord/components/settings-editor/from-declaration-search.js.map +1 -0
  129. package/dist/discord/components/settings-editor/from-declaration-stored.d.ts +21 -0
  130. package/dist/discord/components/settings-editor/from-declaration-stored.d.ts.map +1 -0
  131. package/dist/discord/components/settings-editor/from-declaration-stored.js +37 -0
  132. package/dist/discord/components/settings-editor/from-declaration-stored.js.map +1 -0
  133. package/dist/discord/components/settings-editor/from-declaration-subject.d.ts +32 -0
  134. package/dist/discord/components/settings-editor/from-declaration-subject.d.ts.map +1 -0
  135. package/dist/discord/components/settings-editor/from-declaration-subject.js +65 -0
  136. package/dist/discord/components/settings-editor/from-declaration-subject.js.map +1 -0
  137. package/dist/discord/components/settings-editor/from-declaration-values.d.ts +57 -0
  138. package/dist/discord/components/settings-editor/from-declaration-values.d.ts.map +1 -0
  139. package/dist/discord/components/settings-editor/from-declaration-values.js +187 -0
  140. package/dist/discord/components/settings-editor/from-declaration-values.js.map +1 -0
  141. package/dist/discord/components/settings-editor/from-declaration.d.ts +39 -0
  142. package/dist/discord/components/settings-editor/from-declaration.d.ts.map +1 -0
  143. package/dist/discord/components/settings-editor/from-declaration.js +132 -0
  144. package/dist/discord/components/settings-editor/from-declaration.js.map +1 -0
  145. package/dist/discord/components/settings-editor/index.d.ts +3 -1
  146. package/dist/discord/components/settings-editor/index.d.ts.map +1 -1
  147. package/dist/discord/components/settings-editor/index.js +6 -3
  148. package/dist/discord/components/settings-editor/index.js.map +1 -1
  149. package/dist/discord/components/settings-editor/mount-settings-editor.d.ts +16 -2
  150. package/dist/discord/components/settings-editor/mount-settings-editor.d.ts.map +1 -1
  151. package/dist/discord/components/settings-editor/mount-settings-editor.js +14 -10
  152. package/dist/discord/components/settings-editor/mount-settings-editor.js.map +1 -1
  153. package/dist/discord/components/settings-editor/navigation.d.ts +20 -0
  154. package/dist/discord/components/settings-editor/navigation.d.ts.map +1 -1
  155. package/dist/discord/components/settings-editor/navigation.js +22 -0
  156. package/dist/discord/components/settings-editor/navigation.js.map +1 -1
  157. package/dist/discord/components/settings-editor/settings-editor-card.view.d.ts +4 -3
  158. package/dist/discord/components/settings-editor/settings-editor-card.view.d.ts.map +1 -1
  159. package/dist/discord/components/settings-editor/settings-editor-card.view.js +60 -34
  160. package/dist/discord/components/settings-editor/settings-editor-card.view.js.map +1 -1
  161. package/dist/discord/components/settings-editor/settings-editor-field-modal.d.ts +2 -1
  162. package/dist/discord/components/settings-editor/settings-editor-field-modal.d.ts.map +1 -1
  163. package/dist/discord/components/settings-editor/settings-editor-field-modal.js +48 -72
  164. package/dist/discord/components/settings-editor/settings-editor-field-modal.js.map +1 -1
  165. package/dist/discord/components/settings-editor/settings-editor-field-values.d.ts +6 -0
  166. package/dist/discord/components/settings-editor/settings-editor-field-values.d.ts.map +1 -0
  167. package/dist/discord/components/settings-editor/settings-editor-field-values.js +13 -0
  168. package/dist/discord/components/settings-editor/settings-editor-field-values.js.map +1 -0
  169. package/dist/discord/components/settings-editor/settings-editor-fields.d.ts +6 -5
  170. package/dist/discord/components/settings-editor/settings-editor-fields.d.ts.map +1 -1
  171. package/dist/discord/components/settings-editor/settings-editor-fields.js +0 -10
  172. package/dist/discord/components/settings-editor/settings-editor-fields.js.map +1 -1
  173. package/dist/discord/components/settings-editor/settings-editor.view.d.ts +3 -2
  174. package/dist/discord/components/settings-editor/settings-editor.view.d.ts.map +1 -1
  175. package/dist/discord/components/settings-editor/settings-editor.view.js +4 -21
  176. package/dist/discord/components/settings-editor/settings-editor.view.js.map +1 -1
  177. package/dist/discord/events/event-guild-id.d.ts +10 -0
  178. package/dist/discord/events/event-guild-id.d.ts.map +1 -0
  179. package/dist/discord/events/event-guild-id.js +37 -0
  180. package/dist/discord/events/event-guild-id.js.map +1 -0
  181. package/dist/discord/events/event-router.d.ts +17 -3
  182. package/dist/discord/events/event-router.d.ts.map +1 -1
  183. package/dist/discord/events/event-router.js +33 -9
  184. package/dist/discord/events/event-router.js.map +1 -1
  185. package/dist/discord/i18n.d.ts +17 -2
  186. package/dist/discord/i18n.d.ts.map +1 -1
  187. package/dist/discord/i18n.js +43 -6
  188. package/dist/discord/i18n.js.map +1 -1
  189. package/dist/discord/interaction/interaction-locale.d.ts +14 -0
  190. package/dist/discord/interaction/interaction-locale.d.ts.map +1 -0
  191. package/dist/discord/interaction/interaction-locale.js +12 -0
  192. package/dist/discord/interaction/interaction-locale.js.map +1 -0
  193. package/dist/discord/interaction/locale-resolver.d.ts +27 -0
  194. package/dist/discord/interaction/locale-resolver.d.ts.map +1 -0
  195. package/dist/discord/interaction/locale-resolver.js +3 -0
  196. package/dist/discord/interaction/locale-resolver.js.map +1 -0
  197. package/dist/discord/interaction/modal.d.ts.map +1 -1
  198. package/dist/discord/interaction/modal.js +5 -40
  199. package/dist/discord/interaction/modal.js.map +1 -1
  200. package/dist/discord/interaction/reply-locale.d.ts +18 -0
  201. package/dist/discord/interaction/reply-locale.d.ts.map +1 -0
  202. package/dist/discord/interaction/reply-locale.js +25 -0
  203. package/dist/discord/interaction/reply-locale.js.map +1 -0
  204. package/dist/discord/interaction/select-bounds.d.ts +19 -0
  205. package/dist/discord/interaction/select-bounds.d.ts.map +1 -0
  206. package/dist/discord/interaction/select-bounds.js +17 -0
  207. package/dist/discord/interaction/select-bounds.js.map +1 -0
  208. package/dist/discord/interaction/select-menu.d.ts.map +1 -1
  209. package/dist/discord/interaction/select-menu.js +2 -9
  210. package/dist/discord/interaction/select-menu.js.map +1 -1
  211. package/dist/discord/placeholder-registry.d.ts +1 -1
  212. package/dist/discord/placeholder-registry.d.ts.map +1 -1
  213. package/dist/discord/presenter.d.ts +1 -1
  214. package/dist/discord/presenter.d.ts.map +1 -1
  215. package/dist/discord/settings/discord-guild-directory.d.ts +28 -0
  216. package/dist/discord/settings/discord-guild-directory.d.ts.map +1 -0
  217. package/dist/discord/settings/discord-guild-directory.js +171 -0
  218. package/dist/discord/settings/discord-guild-directory.js.map +1 -0
  219. package/dist/discord/settings/guild-locale-resolver.d.ts +25 -0
  220. package/dist/discord/settings/guild-locale-resolver.d.ts.map +1 -0
  221. package/dist/discord/settings/guild-locale-resolver.js +33 -0
  222. package/dist/discord/settings/guild-locale-resolver.js.map +1 -0
  223. package/dist/discord/settings/is-module-disabled.d.ts +16 -0
  224. package/dist/discord/settings/is-module-disabled.d.ts.map +1 -0
  225. package/dist/discord/settings/is-module-disabled.js +20 -0
  226. package/dist/discord/settings/is-module-disabled.js.map +1 -0
  227. package/dist/discord/settings/module-disabled-embed.d.ts +13 -0
  228. package/dist/discord/settings/module-disabled-embed.d.ts.map +1 -0
  229. package/dist/discord/settings/module-disabled-embed.js +16 -0
  230. package/dist/discord/settings/module-disabled-embed.js.map +1 -0
  231. package/dist/discord/settings/require-configured.d.ts +26 -0
  232. package/dist/discord/settings/require-configured.d.ts.map +1 -0
  233. package/dist/discord/settings/require-configured.js +62 -0
  234. package/dist/discord/settings/require-configured.js.map +1 -0
  235. package/dist/discord/ui/color-aliases.d.ts +31 -0
  236. package/dist/discord/ui/color-aliases.d.ts.map +1 -0
  237. package/dist/discord/ui/color-aliases.js +66 -0
  238. package/dist/discord/ui/color-aliases.js.map +1 -0
  239. package/dist/discord/ui/color-input.d.ts +5 -32
  240. package/dist/discord/ui/color-input.d.ts.map +1 -1
  241. package/dist/discord/ui/color-input.js +8 -157
  242. package/dist/discord/ui/color-input.js.map +1 -1
  243. package/dist/discord/ui/colors.d.ts +0 -2
  244. package/dist/discord/ui/colors.d.ts.map +1 -1
  245. package/dist/discord/ui/colors.js +4 -5
  246. package/dist/discord/ui/colors.js.map +1 -1
  247. package/dist/discord/ui/embed.d.ts.map +1 -1
  248. package/dist/discord/ui/embed.js +3 -8
  249. package/dist/discord/ui/embed.js.map +1 -1
  250. package/dist/discord/ui/truncate-text.d.ts +11 -0
  251. package/dist/discord/ui/truncate-text.d.ts.map +1 -0
  252. package/dist/discord/ui/truncate-text.js +19 -0
  253. package/dist/discord/ui/truncate-text.js.map +1 -0
  254. package/dist/errors/error-message.d.ts +9 -0
  255. package/dist/errors/error-message.d.ts.map +1 -0
  256. package/dist/errors/error-message.js +14 -0
  257. package/dist/errors/error-message.js.map +1 -0
  258. package/dist/i18n/business-message.d.ts +1 -1
  259. package/dist/i18n/business-message.d.ts.map +1 -1
  260. package/dist/i18n/catalog.d.ts +9 -0
  261. package/dist/i18n/catalog.d.ts.map +1 -1
  262. package/dist/i18n/catalog.js +4 -0
  263. package/dist/i18n/catalog.js.map +1 -1
  264. package/dist/i18n/locale.d.ts +2 -0
  265. package/dist/i18n/locale.d.ts.map +1 -1
  266. package/dist/i18n/locale.js +3 -1
  267. package/dist/i18n/locale.js.map +1 -1
  268. package/dist/i18n/translator.d.ts +2 -0
  269. package/dist/i18n/translator.d.ts.map +1 -1
  270. package/dist/i18n/translator.js.map +1 -1
  271. package/dist/module/module.d.ts +15 -1
  272. package/dist/module/module.d.ts.map +1 -1
  273. package/dist/resilience/retry.js +2 -2
  274. package/dist/resilience/retry.js.map +1 -1
  275. package/dist/scheduler/scheduler.d.ts.map +1 -1
  276. package/dist/scheduler/scheduler.js +2 -4
  277. package/dist/scheduler/scheduler.js.map +1 -1
  278. package/dist/settings/cache.d.ts +29 -0
  279. package/dist/settings/cache.d.ts.map +1 -0
  280. package/dist/settings/cache.js +47 -0
  281. package/dist/settings/cache.js.map +1 -0
  282. package/dist/settings/choice.d.ts +10 -0
  283. package/dist/settings/choice.d.ts.map +1 -0
  284. package/dist/settings/choice.js +3 -0
  285. package/dist/settings/choice.js.map +1 -0
  286. package/dist/settings/decode-stored.d.ts +20 -0
  287. package/dist/settings/decode-stored.d.ts.map +1 -0
  288. package/dist/settings/decode-stored.js +35 -0
  289. package/dist/settings/decode-stored.js.map +1 -0
  290. package/dist/settings/define-settings.d.ts +52 -0
  291. package/dist/settings/define-settings.d.ts.map +1 -0
  292. package/dist/settings/define-settings.js +141 -0
  293. package/dist/settings/define-settings.js.map +1 -0
  294. package/dist/settings/describe-field.d.ts +19 -0
  295. package/dist/settings/describe-field.d.ts.map +1 -0
  296. package/dist/settings/describe-field.js +131 -0
  297. package/dist/settings/describe-field.js.map +1 -0
  298. package/dist/settings/describe.d.ts +27 -0
  299. package/dist/settings/describe.d.ts.map +1 -0
  300. package/dist/settings/describe.js +62 -0
  301. package/dist/settings/describe.js.map +1 -0
  302. package/dist/settings/duration.d.ts +16 -0
  303. package/dist/settings/duration.d.ts.map +1 -0
  304. package/dist/settings/duration.js +56 -0
  305. package/dist/settings/duration.js.map +1 -0
  306. package/dist/settings/field-order.d.ts +37 -0
  307. package/dist/settings/field-order.d.ts.map +1 -0
  308. package/dist/settings/field-order.js +100 -0
  309. package/dist/settings/field-order.js.map +1 -0
  310. package/dist/settings/field-search.d.ts +10 -0
  311. package/dist/settings/field-search.d.ts.map +1 -0
  312. package/dist/settings/field-search.js +18 -0
  313. package/dist/settings/field-search.js.map +1 -0
  314. package/dist/settings/fields/builders.d.ts +41 -0
  315. package/dist/settings/fields/builders.d.ts.map +1 -0
  316. package/dist/settings/fields/builders.js +83 -0
  317. package/dist/settings/fields/builders.js.map +1 -0
  318. package/dist/settings/fields/field.d.ts +199 -0
  319. package/dist/settings/fields/field.d.ts.map +1 -0
  320. package/dist/settings/fields/field.js +33 -0
  321. package/dist/settings/fields/field.js.map +1 -0
  322. package/dist/settings/fields/zod-schema.d.ts +67 -0
  323. package/dist/settings/fields/zod-schema.d.ts.map +1 -0
  324. package/dist/settings/fields/zod-schema.js +281 -0
  325. package/dist/settings/fields/zod-schema.js.map +1 -0
  326. package/dist/settings/in-memory/in-memory-guild-directory.d.ts +24 -0
  327. package/dist/settings/in-memory/in-memory-guild-directory.d.ts.map +1 -0
  328. package/dist/settings/in-memory/in-memory-guild-directory.js +42 -0
  329. package/dist/settings/in-memory/in-memory-guild-directory.js.map +1 -0
  330. package/dist/settings/in-memory/in-memory-settings-store.d.ts +8 -0
  331. package/dist/settings/in-memory/in-memory-settings-store.d.ts.map +1 -0
  332. package/dist/settings/in-memory/in-memory-settings-store.js +43 -0
  333. package/dist/settings/in-memory/in-memory-settings-store.js.map +1 -0
  334. package/dist/settings/in-memory/in-process-notifier.d.ts +9 -0
  335. package/dist/settings/in-memory/in-process-notifier.d.ts.map +1 -0
  336. package/dist/settings/in-memory/in-process-notifier.js +32 -0
  337. package/dist/settings/in-memory/in-process-notifier.js.map +1 -0
  338. package/dist/settings/index.d.ts +22 -0
  339. package/dist/settings/index.d.ts.map +1 -0
  340. package/dist/settings/index.js +31 -0
  341. package/dist/settings/index.js.map +1 -0
  342. package/dist/settings/is-plain-object.d.ts +6 -0
  343. package/dist/settings/is-plain-object.d.ts.map +1 -0
  344. package/dist/settings/is-plain-object.js +15 -0
  345. package/dist/settings/is-plain-object.js.map +1 -0
  346. package/dist/settings/messages.d.ts +61 -0
  347. package/dist/settings/messages.d.ts.map +1 -0
  348. package/dist/settings/messages.js +155 -0
  349. package/dist/settings/messages.js.map +1 -0
  350. package/dist/settings/migrate.d.ts +35 -0
  351. package/dist/settings/migrate.d.ts.map +1 -0
  352. package/dist/settings/migrate.js +42 -0
  353. package/dist/settings/migrate.js.map +1 -0
  354. package/dist/settings/ports/guild-directory.d.ts +61 -0
  355. package/dist/settings/ports/guild-directory.d.ts.map +1 -0
  356. package/dist/settings/ports/guild-directory.js +3 -0
  357. package/dist/settings/ports/guild-directory.js.map +1 -0
  358. package/dist/settings/ports/settings-changed-notifier.d.ts +20 -0
  359. package/dist/settings/ports/settings-changed-notifier.d.ts.map +1 -0
  360. package/dist/settings/ports/settings-changed-notifier.js +3 -0
  361. package/dist/settings/ports/settings-changed-notifier.js.map +1 -0
  362. package/dist/settings/ports/settings-store.d.ts +76 -0
  363. package/dist/settings/ports/settings-store.d.ts.map +1 -0
  364. package/dist/settings/ports/settings-store.js +3 -0
  365. package/dist/settings/ports/settings-store.js.map +1 -0
  366. package/dist/settings/registry.d.ts +31 -0
  367. package/dist/settings/registry.d.ts.map +1 -0
  368. package/dist/settings/registry.js +102 -0
  369. package/dist/settings/registry.js.map +1 -0
  370. package/dist/settings/settings-declaration-error.d.ts +13 -0
  371. package/dist/settings/settings-declaration-error.d.ts.map +1 -0
  372. package/dist/settings/settings-declaration-error.js +22 -0
  373. package/dist/settings/settings-declaration-error.js.map +1 -0
  374. package/dist/settings/settings-service.d.ts +148 -0
  375. package/dist/settings/settings-service.d.ts.map +1 -0
  376. package/dist/settings/settings-service.js +279 -0
  377. package/dist/settings/settings-service.js.map +1 -0
  378. package/dist/settings/settings-validation-error.d.ts +23 -0
  379. package/dist/settings/settings-validation-error.d.ts.map +1 -0
  380. package/dist/settings/settings-validation-error.js +17 -0
  381. package/dist/settings/settings-validation-error.js.map +1 -0
  382. package/dist/settings/status.d.ts +16 -0
  383. package/dist/settings/status.d.ts.map +1 -0
  384. package/dist/settings/status.js +18 -0
  385. package/dist/settings/status.js.map +1 -0
  386. package/dist/settings/suggest.d.ts +77 -0
  387. package/dist/settings/suggest.d.ts.map +1 -0
  388. package/dist/settings/suggest.js +192 -0
  389. package/dist/settings/suggest.js.map +1 -0
  390. package/dist/settings/system/kernel-settings.d.ts +43 -0
  391. package/dist/settings/system/kernel-settings.d.ts.map +1 -0
  392. package/dist/settings/system/kernel-settings.js +53 -0
  393. package/dist/settings/system/kernel-settings.js.map +1 -0
  394. package/dist/settings/system/module-gate.d.ts +31 -0
  395. package/dist/settings/system/module-gate.d.ts.map +1 -0
  396. package/dist/settings/system/module-gate.js +34 -0
  397. package/dist/settings/system/module-gate.js.map +1 -0
  398. package/dist/settings/testing/index.d.ts +2 -0
  399. package/dist/settings/testing/index.d.ts.map +1 -0
  400. package/dist/settings/testing/index.js +6 -0
  401. package/dist/settings/testing/index.js.map +1 -0
  402. package/dist/settings/testing/settings-store-contract.d.ts +29 -0
  403. package/dist/settings/testing/settings-store-contract.d.ts.map +1 -0
  404. package/dist/settings/testing/settings-store-contract.js +192 -0
  405. package/dist/settings/testing/settings-store-contract.js.map +1 -0
  406. package/dist/settings/timed-search.d.ts +26 -0
  407. package/dist/settings/timed-search.d.ts.map +1 -0
  408. package/dist/settings/timed-search.js +48 -0
  409. package/dist/settings/timed-search.js.map +1 -0
  410. package/dist/settings/types.d.ts +37 -0
  411. package/dist/settings/types.d.ts.map +1 -0
  412. package/dist/settings/types.js +3 -0
  413. package/dist/settings/types.js.map +1 -0
  414. package/dist/settings/validate.d.ts +55 -0
  415. package/dist/settings/validate.d.ts.map +1 -0
  416. package/dist/settings/validate.js +196 -0
  417. package/dist/settings/validate.js.map +1 -0
  418. package/package.json +463 -88
  419. package/src/authz/require-level-guard.ts +1 -1
  420. package/src/color.ts +134 -0
  421. package/src/discord/command/all-required.ts +12 -0
  422. package/src/discord/command/autocomplete-dispatcher.ts +2 -1
  423. package/src/discord/command/autocomplete-limits.ts +2 -0
  424. package/src/discord/command/autocomplete.ts +8 -7
  425. package/src/discord/command/command-responders.ts +39 -0
  426. package/src/discord/command/context.ts +14 -54
  427. package/src/discord/command/cooldown-guard.ts +1 -1
  428. package/src/discord/command/create-command.ts +19 -94
  429. package/src/discord/command/create-context-menu-command.ts +19 -97
  430. package/src/discord/command/errors.ts +1 -1
  431. package/src/discord/command/guard.ts +24 -12
  432. package/src/discord/command/options.ts +202 -412
  433. package/src/discord/command/passes-guard.ts +37 -0
  434. package/src/discord/command/permission-guard.ts +1 -1
  435. package/src/discord/command/render-command-failure.ts +71 -0
  436. package/src/discord/command/route-key.ts +9 -0
  437. package/src/discord/command/router.ts +73 -12
  438. package/src/discord/command/send-embed.ts +42 -0
  439. package/src/discord/command/types.ts +7 -1
  440. package/src/discord/components/component-access.ts +20 -0
  441. package/src/discord/components/component-router.ts +73 -36
  442. package/src/discord/components/interactive-message/index.ts +5 -3
  443. package/src/discord/components/interactive-message/interactive-message-collector.ts +4 -68
  444. package/src/discord/components/interactive-message/message-edit-options.ts +47 -0
  445. package/src/discord/components/interactive-message/mount-interactive-message.ts +5 -3
  446. package/src/discord/components/interactive-message/state-store.ts +20 -0
  447. package/src/discord/components/paginator/mount-paginator.ts +3 -3
  448. package/src/discord/components/paginator/paginator-view.ts +1 -1
  449. package/src/discord/components/persistent-paginator.ts +8 -9
  450. package/src/discord/components/settings-editor/from-declaration-controls.ts +371 -0
  451. package/src/discord/components/settings-editor/from-declaration-display.ts +155 -0
  452. package/src/discord/components/settings-editor/from-declaration-layout.ts +247 -0
  453. package/src/discord/components/settings-editor/from-declaration-search.ts +77 -0
  454. package/src/discord/components/settings-editor/from-declaration-stored.ts +39 -0
  455. package/src/discord/components/settings-editor/from-declaration-subject.ts +110 -0
  456. package/src/discord/components/settings-editor/from-declaration-values.ts +240 -0
  457. package/src/discord/components/settings-editor/from-declaration.ts +218 -0
  458. package/src/discord/components/settings-editor/index.ts +5 -2
  459. package/src/discord/components/settings-editor/mount-settings-editor.ts +28 -10
  460. package/src/discord/components/settings-editor/navigation.ts +41 -0
  461. package/src/discord/components/settings-editor/settings-editor-card.view.ts +79 -36
  462. package/src/discord/components/settings-editor/settings-editor-field-modal.ts +76 -68
  463. package/src/discord/components/settings-editor/settings-editor-field-values.ts +11 -0
  464. package/src/discord/components/settings-editor/settings-editor-fields.ts +6 -11
  465. package/src/discord/components/settings-editor/settings-editor.view.ts +11 -24
  466. package/src/discord/events/event-guild-id.ts +35 -0
  467. package/src/discord/events/event-router.ts +46 -13
  468. package/src/discord/i18n.ts +47 -8
  469. package/src/discord/interaction/interaction-locale.ts +19 -0
  470. package/src/discord/interaction/locale-resolver.ts +28 -0
  471. package/src/discord/interaction/modal.ts +5 -40
  472. package/src/discord/interaction/reply-locale.ts +35 -0
  473. package/src/discord/interaction/select-bounds.ts +33 -0
  474. package/src/discord/interaction/select-menu.ts +2 -9
  475. package/src/discord/placeholder-registry.ts +1 -1
  476. package/src/discord/presenter.ts +1 -1
  477. package/src/discord/settings/discord-guild-directory.ts +214 -0
  478. package/src/discord/settings/guild-locale-resolver.ts +38 -0
  479. package/src/discord/settings/is-module-disabled.ts +22 -0
  480. package/src/discord/settings/module-disabled-embed.ts +19 -0
  481. package/src/discord/settings/require-configured.ts +68 -0
  482. package/src/discord/ui/color-aliases.ts +70 -0
  483. package/src/discord/ui/color-input.ts +8 -173
  484. package/src/discord/ui/colors.ts +2 -3
  485. package/src/discord/ui/embed.ts +3 -9
  486. package/src/discord/ui/truncate-text.ts +15 -0
  487. package/src/errors/error-message.ts +10 -0
  488. package/src/i18n/business-message.ts +1 -1
  489. package/src/i18n/catalog.ts +14 -0
  490. package/src/i18n/locale.ts +3 -0
  491. package/src/i18n/translator.ts +3 -0
  492. package/src/module/module.ts +15 -1
  493. package/src/resilience/retry.ts +1 -1
  494. package/src/scheduler/scheduler.ts +2 -5
  495. package/src/settings/cache.ts +70 -0
  496. package/src/settings/choice.ts +9 -0
  497. package/src/settings/decode-stored.ts +40 -0
  498. package/src/settings/define-settings.ts +214 -0
  499. package/src/settings/describe-field.ts +155 -0
  500. package/src/settings/describe.ts +90 -0
  501. package/src/settings/duration.ts +54 -0
  502. package/src/settings/field-order.ts +128 -0
  503. package/src/settings/field-search.ts +16 -0
  504. package/src/settings/fields/builders.ts +161 -0
  505. package/src/settings/fields/field.ts +205 -0
  506. package/src/settings/fields/zod-schema.ts +300 -0
  507. package/src/settings/in-memory/in-memory-guild-directory.ts +69 -0
  508. package/src/settings/in-memory/in-memory-settings-store.ts +46 -0
  509. package/src/settings/in-memory/in-process-notifier.ts +40 -0
  510. package/src/settings/index.ts +72 -0
  511. package/src/settings/is-plain-object.ts +11 -0
  512. package/src/settings/messages.ts +161 -0
  513. package/src/settings/migrate.ts +58 -0
  514. package/src/settings/ports/guild-directory.ts +48 -0
  515. package/src/settings/ports/settings-changed-notifier.ts +20 -0
  516. package/src/settings/ports/settings-store.ts +74 -0
  517. package/src/settings/registry.ts +137 -0
  518. package/src/settings/settings-declaration-error.ts +16 -0
  519. package/src/settings/settings-service.ts +535 -0
  520. package/src/settings/settings-validation-error.ts +41 -0
  521. package/src/settings/status.ts +25 -0
  522. package/src/settings/suggest.ts +267 -0
  523. package/src/settings/system/kernel-settings.ts +74 -0
  524. package/src/settings/system/module-gate.ts +49 -0
  525. package/src/settings/testing/index.ts +7 -0
  526. package/src/settings/testing/settings-store-contract.ts +278 -0
  527. package/src/settings/timed-search.ts +56 -0
  528. package/src/settings/types.ts +42 -0
  529. package/src/settings/validate.ts +298 -0
@@ -0,0 +1,267 @@
1
+ import { MAX_AUTOCOMPLETE_CHOICES } from "@/discord/command/autocomplete-limits";
2
+ import { resolveLocale } from "@/i18n/locale";
3
+ import type { Translator } from "@/i18n/translator";
4
+ import type { Logger } from "@/logger";
5
+ import type { Choice } from "@/settings/choice";
6
+ import {
7
+ type DynamicSuggestions,
8
+ type FieldChoice,
9
+ type FieldSpec,
10
+ type SuggestionContext,
11
+ type Suggestions,
12
+ unhandledFieldKind,
13
+ } from "@/settings/fields/field";
14
+ import type { GuildDirectory } from "@/settings/ports/guild-directory";
15
+ import { timedSearch } from "@/settings/timed-search";
16
+
17
+ /**
18
+ * The ports suggestions read: the guild's directory, the translator of static
19
+ * labels, and where a slow or failed search is logged.
20
+ */
21
+ export interface SuggestionPorts {
22
+ readonly guilds: GuildDirectory;
23
+ readonly translator: Translator;
24
+ readonly logger: Logger;
25
+ }
26
+
27
+ /** What one field's suggestions are computed with: the ports, the field, and who asks. */
28
+ export interface SuggestionScope extends SuggestionPorts {
29
+ readonly moduleId: string;
30
+ /** The field's key, named in logs. */
31
+ readonly key: string;
32
+ readonly ctx: SuggestionContext;
33
+ }
34
+
35
+ /** A search the kernel runs on the module's behalf, whatever the value type it suggests. */
36
+ type AnyDynamicSuggestions = DynamicSuggestions<unknown>;
37
+
38
+ /**
39
+ * The suggestions of one field for `query` (FR-021, FR-025): a channel, role
40
+ * or member field searches the guild; an enum and static suggestions offer
41
+ * their choices translated to the requester's locale, those whose name or
42
+ * value contains the query (case-insensitive); a dynamic search gets the
43
+ * query and the context as is. A list suggests for its item. At most
44
+ * {@link MAX_AUTOCOMPLETE_CHOICES} results, the first ones kept; a search
45
+ * slower than the budget or failing yields none (logged). Never throws.
46
+ *
47
+ * @param params.scope - who asks and the ports the search reads.
48
+ * @param params.spec - the field's kind and constraints.
49
+ * @param params.query - what was typed so far; empty for a first page.
50
+ * @returns the suggestions, labels already translated.
51
+ */
52
+ export async function suggestChoices(params: {
53
+ scope: SuggestionScope;
54
+ spec: FieldSpec;
55
+ query: string;
56
+ }): Promise<readonly Choice[]> {
57
+ const choices = await allChoices(params);
58
+ return choices.slice(0, MAX_AUTOCOMPLETE_CHOICES);
59
+ }
60
+
61
+ async function allChoices(params: {
62
+ scope: SuggestionScope;
63
+ spec: FieldSpec;
64
+ query: string;
65
+ }): Promise<readonly Choice[]> {
66
+ const { scope, spec, query } = params;
67
+ const { guilds, ctx } = scope;
68
+ switch (spec.kind) {
69
+ case "channel":
70
+ return timed({
71
+ scope,
72
+ fallback: [],
73
+ search: async () => named(await guilds.searchChannels(ctx.guildId, query, spec.types)),
74
+ });
75
+ case "role":
76
+ return timed({
77
+ scope,
78
+ fallback: [],
79
+ search: async () => named(await guilds.searchRoles(ctx.guildId, query)),
80
+ });
81
+ case "user":
82
+ return timed({
83
+ scope,
84
+ fallback: [],
85
+ search: async () => named(await guilds.searchMembers(ctx.guildId, query)),
86
+ });
87
+ case "enum":
88
+ return matching({ choices: translated({ scope, choices: spec.choices }), query });
89
+ case "integer":
90
+ case "text":
91
+ return declaredChoices({ scope, suggest: spec.suggest, query });
92
+ case "list":
93
+ return allChoices({ scope, spec: spec.item.spec, query });
94
+ case "color":
95
+ case "duration":
96
+ case "number":
97
+ case "boolean":
98
+ case "secret":
99
+ case "toggles":
100
+ return [];
101
+ default:
102
+ return unhandledFieldKind(spec);
103
+ }
104
+ }
105
+
106
+ /** The suggestions a module declared on an integer or text field, if any. */
107
+ async function declaredChoices(params: {
108
+ scope: SuggestionScope;
109
+ suggest: Suggestions<string> | Suggestions<number> | undefined;
110
+ query: string;
111
+ }): Promise<readonly Choice[]> {
112
+ const { scope, suggest, query } = params;
113
+ if (suggest === undefined) {
114
+ return [];
115
+ }
116
+ if ("choices" in suggest) {
117
+ return matching({ choices: translated({ scope, choices: suggest.choices }), query });
118
+ }
119
+ const search: AnyDynamicSuggestions = suggest;
120
+ return timed({ scope, fallback: [], search: () => search.resolve(query, scope.ctx) });
121
+ }
122
+
123
+ /**
124
+ * The readable label of one stored value of a field, in the requester's
125
+ * locale: a channel, role or member's name on the guild, a static choice's
126
+ * translated label, or what a dynamic search says (see {@link dynamicLabel}).
127
+ * A list reads `value` as one of its items. Never throws.
128
+ *
129
+ * @param params.scope - who asks and the ports the label reads.
130
+ * @param params.spec - the field's kind and constraints.
131
+ * @param params.value - the stored value.
132
+ * @returns the label, or `undefined` when none is known.
133
+ */
134
+ export async function suggestionLabel(params: {
135
+ scope: SuggestionScope;
136
+ spec: FieldSpec;
137
+ value: unknown;
138
+ }): Promise<string | undefined> {
139
+ const { scope, spec, value } = params;
140
+ const { guilds, ctx } = scope;
141
+ switch (spec.kind) {
142
+ case "channel":
143
+ return entityName({ scope, find: (id) => guilds.channel(ctx.guildId, id), value });
144
+ case "role":
145
+ return entityName({ scope, find: (id) => guilds.role(ctx.guildId, id), value });
146
+ case "user":
147
+ return entityName({ scope, find: (id) => guilds.member(ctx.guildId, id), value });
148
+ case "enum":
149
+ return staticLabel({ scope, choices: spec.choices, value });
150
+ case "integer":
151
+ case "text":
152
+ if (spec.suggest === undefined) {
153
+ return undefined;
154
+ }
155
+ return "choices" in spec.suggest
156
+ ? staticLabel({ scope, choices: spec.suggest.choices, value })
157
+ : dynamicLabel({ scope, suggest: spec.suggest, value });
158
+ case "list":
159
+ return suggestionLabel({ scope, spec: spec.item.spec, value });
160
+ case "color":
161
+ case "duration":
162
+ case "number":
163
+ case "boolean":
164
+ case "secret":
165
+ case "toggles":
166
+ return undefined;
167
+ default:
168
+ return unhandledFieldKind(spec);
169
+ }
170
+ }
171
+
172
+ /**
173
+ * What a dynamic search calls `value` (FR-022): its own `label` when it
174
+ * declares one, otherwise the name of the result of `resolve(String(value))`
175
+ * whose value is exactly `value`. `undefined` when the search does not know
176
+ * the value, or runs past the budget or fails (logged). A strict field
177
+ * accepts exactly the values this labels (FR-023).
178
+ *
179
+ * @param params.scope - who asks, and where a slow or failed search is logged.
180
+ * @param params.suggest - the field's search.
181
+ * @param params.value - the value to label.
182
+ * @returns the label, or `undefined`.
183
+ */
184
+ export function dynamicLabel(params: {
185
+ scope: SuggestionScope;
186
+ suggest: AnyDynamicSuggestions;
187
+ value: unknown;
188
+ }): Promise<string | undefined> {
189
+ const { scope, suggest, value } = params;
190
+ const { label } = suggest;
191
+ if (label !== undefined) {
192
+ return timed({ scope, fallback: undefined, search: () => label(value, scope.ctx) });
193
+ }
194
+ return timed({
195
+ scope,
196
+ fallback: undefined,
197
+ search: async () => {
198
+ const results = await suggest.resolve(String(value), scope.ctx);
199
+ return results.find((choice) => choice.value === value)?.name;
200
+ },
201
+ });
202
+ }
203
+
204
+ /** A search within the suggestion budget, logged under the scope's module, field and guild. */
205
+ function timed<T>(params: {
206
+ scope: SuggestionScope;
207
+ fallback: T;
208
+ search: () => Promise<T>;
209
+ }): Promise<T> {
210
+ const { scope, fallback, search } = params;
211
+ return timedSearch({
212
+ search,
213
+ fallback,
214
+ logger: scope.logger,
215
+ at: { moduleId: scope.moduleId, field: scope.key, guildId: scope.ctx.guildId },
216
+ });
217
+ }
218
+
219
+ /** The name of the guild entity a stored id names, if the guild still has it. */
220
+ function entityName(params: {
221
+ scope: SuggestionScope;
222
+ find: (id: string) => Promise<{ readonly name: string } | null>;
223
+ value: unknown;
224
+ }): Promise<string | undefined> {
225
+ const { scope, find, value } = params;
226
+ if (typeof value !== "string") {
227
+ return Promise.resolve(undefined);
228
+ }
229
+ return timed({ scope, fallback: undefined, search: async () => (await find(value))?.name });
230
+ }
231
+
232
+ /** Guild entities as choices: their name shown, their id stored. */
233
+ function named(entities: readonly { readonly id: string; readonly name: string }[]): Choice[] {
234
+ return entities.map((entity) => ({ name: entity.name, value: entity.id }));
235
+ }
236
+
237
+ /** Static choices with their labels translated to the requester's locale. */
238
+ function translated(params: {
239
+ scope: SuggestionScope;
240
+ choices: readonly FieldChoice<string | number>[];
241
+ }): Choice[] {
242
+ const { scope, choices } = params;
243
+ const locale = resolveLocale([scope.ctx.locale], scope.translator.defaultLocale);
244
+ return choices.map((choice) => ({
245
+ name: scope.translator.translate(locale, choice.label),
246
+ value: choice.value,
247
+ }));
248
+ }
249
+
250
+ /** The choices whose name or value contains `query`, case-insensitive; all of them for an empty query. */
251
+ function matching(params: { choices: readonly Choice[]; query: string }): readonly Choice[] {
252
+ const needle = params.query.trim().toLowerCase();
253
+ return params.choices.filter(
254
+ (choice) =>
255
+ choice.name.toLowerCase().includes(needle) ||
256
+ String(choice.value).toLowerCase().includes(needle),
257
+ );
258
+ }
259
+
260
+ /** The translated label of the static choice storing `value`. */
261
+ function staticLabel(params: {
262
+ scope: SuggestionScope;
263
+ choices: readonly FieldChoice<string | number>[];
264
+ value: unknown;
265
+ }): string | undefined {
266
+ return translated(params).find((choice) => choice.value === params.value)?.name;
267
+ }
@@ -0,0 +1,74 @@
1
+ import { type Locale, SUPPORTED_LOCALES } from "@/i18n/locale";
2
+ import { defineSettings } from "@/settings/define-settings";
3
+ import { field } from "@/settings/fields/builders";
4
+ import { KERNEL_SETTINGS_MESSAGES } from "@/settings/messages";
5
+
6
+ /** Id of the kernel's own settings declaration. */
7
+ export const KERNEL_SETTINGS_ID = "kernel";
8
+
9
+ /**
10
+ * What the kernel declaration needs to know of a module. A `BotModule`
11
+ * satisfies it as is, so the composition root passes its modules unchanged.
12
+ */
13
+ export interface ModuleEnablement {
14
+ readonly name: string;
15
+ /**
16
+ * Catalog key of the module's name as administrators read it, shown on its
17
+ * toggle. Without one the toggle shows the raw module name, and the translator
18
+ * logs it as an unknown key: give every toggleable module a label.
19
+ */
20
+ readonly label?: string;
21
+ /** Whether the module starts enabled in a guild that never toggled it. Defaults to `true`. */
22
+ readonly defaultEnabled?: boolean;
23
+ }
24
+
25
+ const LOCALE_LABELS: Readonly<Record<Locale, string>> = {
26
+ en: KERNEL_SETTINGS_MESSAGES.localeEn,
27
+ fr: KERNEL_SETTINGS_MESSAGES.localeFr,
28
+ };
29
+
30
+ /**
31
+ * The kernel's own settings, declared like any module's (FR-039): which
32
+ * modules are enabled on the guild, and the bot's language there.
33
+ *
34
+ * A factory rather than a constant because the module names are only known at
35
+ * composition: `createSettingsRegistry` calls it with the modules it receives.
36
+ *
37
+ * @param modules - the registered modules, in registration order.
38
+ * @returns the declaration of id {@link KERNEL_SETTINGS_ID}.
39
+ * @throws SettingsDeclarationError when two modules share a name.
40
+ */
41
+ export function kernelSettings(modules: readonly ModuleEnablement[]) {
42
+ return defineSettings({
43
+ id: KERNEL_SETTINGS_ID,
44
+ version: 1,
45
+ labels: {
46
+ title: KERNEL_SETTINGS_MESSAGES.title,
47
+ description: KERNEL_SETTINGS_MESSAGES.description,
48
+ },
49
+ fields: {
50
+ modules: field.toggles<string>({
51
+ label: KERNEL_SETTINGS_MESSAGES.modules,
52
+ description: KERNEL_SETTINGS_MESSAGES.modulesDescription,
53
+ keys: modules.map(({ name }) => name),
54
+ keyLabels: Object.fromEntries(
55
+ modules.flatMap(({ name, label }) => (label === undefined ? [] : [[name, label]])),
56
+ ),
57
+ default: Object.fromEntries(
58
+ modules.map(({ name, defaultEnabled }) => [name, defaultEnabled ?? true]),
59
+ ),
60
+ }),
61
+ locale: field.enum({
62
+ label: KERNEL_SETTINGS_MESSAGES.locale,
63
+ description: KERNEL_SETTINGS_MESSAGES.localeDescription,
64
+ choices: SUPPORTED_LOCALES.map((locale) => ({
65
+ value: locale,
66
+ label: LOCALE_LABELS[locale],
67
+ })),
68
+ }),
69
+ },
70
+ });
71
+ }
72
+
73
+ /** The kernel's settings declaration, as built by {@link kernelSettings}. */
74
+ export type KernelSettings = ReturnType<typeof kernelSettings>;
@@ -0,0 +1,49 @@
1
+ import { errorMessage } from "@/errors/error-message";
2
+ import type { Logger } from "@/logger";
3
+ import type { SettingsRegistry } from "@/settings/registry";
4
+ import type { SettingsService } from "@/settings/settings-service";
5
+
6
+ /**
7
+ * Whether a module runs on a guild. The command, component and event routers
8
+ * check it before running a module's handler there; scheduled jobs call it
9
+ * themselves (FR-036).
10
+ */
11
+ export interface ModuleGate {
12
+ isEnabled(moduleName: string, guildId: string): Promise<boolean>;
13
+ }
14
+
15
+ /**
16
+ * Create the gate over the guild's kernel settings (`registry.kernel`), read
17
+ * through the service's cache, so checking it on every interaction does not
18
+ * hit the store.
19
+ *
20
+ * A module the kernel declaration does not know is never gated. When the
21
+ * settings cannot be read, the module runs and the error is logged: a store
22
+ * outage must not switch every module off.
23
+ *
24
+ * @param deps.service - reads the guild's kernel settings.
25
+ * @param deps.registry - holds the kernel declaration built from the modules.
26
+ * @param deps.logger - where a failed read is reported.
27
+ * @returns the gate shared by the routers and the scheduled jobs.
28
+ */
29
+ export function createModuleGate(deps: {
30
+ service: SettingsService;
31
+ registry: SettingsRegistry;
32
+ logger: Logger;
33
+ }): ModuleGate {
34
+ const { service, registry, logger } = deps;
35
+ return {
36
+ async isEnabled(moduleName: string, guildId: string): Promise<boolean> {
37
+ try {
38
+ const { modules } = await service.get(registry.kernel, guildId);
39
+ return modules[moduleName] ?? true;
40
+ } catch (error) {
41
+ logger.error(
42
+ { module: moduleName, guildId, err: errorMessage(error) },
43
+ "Could not read whether the module is enabled; letting it run",
44
+ );
45
+ return true;
46
+ }
47
+ },
48
+ };
49
+ }
@@ -0,0 +1,7 @@
1
+ export {
2
+ type ContractAssertion,
3
+ type DescribeFn,
4
+ type ExpectFn,
5
+ type ItFn,
6
+ runSettingsStoreContract,
7
+ } from "@/settings/testing/settings-store-contract";
@@ -0,0 +1,278 @@
1
+ import { ConflictError } from "@/errors/business-error";
2
+ import type { SettingsStore, StoredSettings } from "@/settings/ports/settings-store";
3
+
4
+ /** Minimal shape of a test runner's `describe`, so this suite imports no framework. */
5
+ export type DescribeFn = (name: string, body: () => void) => void;
6
+
7
+ /** Minimal shape of a test runner's `it`. */
8
+ export type ItFn = (name: string, body: () => Promise<void>) => void;
9
+
10
+ /** The assertions this suite relies on; Vitest's and Jest's `expect` both satisfy it. */
11
+ export interface ContractAssertion {
12
+ toBe(expected: unknown): void;
13
+ toEqual(expected: unknown): void;
14
+ toBeNull(): void;
15
+ toBeInstanceOf(expected: abstract new (...args: never[]) => unknown): void;
16
+ }
17
+
18
+ /** Minimal shape of a test runner's `expect`. */
19
+ export type ExpectFn = (actual: unknown) => ContractAssertion;
20
+
21
+ const GUILD_A = "100000000000000001";
22
+ const GUILD_B = "100000000000000002";
23
+ const MODULE_A = "tickets";
24
+ const MODULE_B = "welcome";
25
+
26
+ function record(overrides: Partial<StoredSettings> = {}): StoredSettings {
27
+ return {
28
+ guildId: GUILD_A,
29
+ moduleId: MODULE_A,
30
+ version: 1,
31
+ revision: 1,
32
+ values: { maxOpen: 2 },
33
+ updatedAt: "2026-01-01T00:00:00.000Z",
34
+ updatedBy: "200000000000000001",
35
+ ...overrides,
36
+ };
37
+ }
38
+
39
+ async function captureError(action: () => Promise<unknown>): Promise<unknown> {
40
+ try {
41
+ await action();
42
+ } catch (error) {
43
+ return error;
44
+ }
45
+ return undefined;
46
+ }
47
+
48
+ /** The caller's view of a record it may scribble on, to prove the store kept its own copy. */
49
+ function mutableValues(stored: StoredSettings | null): Record<string, unknown> {
50
+ return (stored?.values ?? {}) as Record<string, unknown>;
51
+ }
52
+
53
+ /**
54
+ * Behaviour every {@link SettingsStore} implementation must have: one case per
55
+ * guarantee stated on the port. Run it from the adapter's own test file with
56
+ * the test runner's `describe`, `it` and `expect`:
57
+ * `runSettingsStoreContract(createMyStore, { describe, it, expect })`.
58
+ *
59
+ * @param createStore - returns a store with no record in it; called once per case.
60
+ * @param runner - the test runner's functions, so this suite imports no framework.
61
+ */
62
+ export function runSettingsStoreContract(
63
+ createStore: () => SettingsStore | Promise<SettingsStore>,
64
+ runner: { describe: DescribeFn; it: ItFn; expect: ExpectFn },
65
+ ): void {
66
+ const { describe, it, expect } = runner;
67
+
68
+ /**
69
+ * Deleting the default record must leave `other` — the same record moved to
70
+ * another guild or another module — exactly as it was written.
71
+ */
72
+ async function expectDeleteLeavesAlone(other: StoredSettings): Promise<void> {
73
+ const store = await createStore();
74
+ await store.write(record(), { expectedRevision: null });
75
+ await store.write(other, { expectedRevision: null });
76
+
77
+ await store.delete(GUILD_A, MODULE_A);
78
+
79
+ expect(await store.read(GUILD_A, MODULE_A)).toBeNull();
80
+ expect(await store.read(other.guildId, other.moduleId)).toEqual(other);
81
+ }
82
+
83
+ describe("SettingsStore contract", () => {
84
+ it("reads null when no record exists", async () => {
85
+ const store = await createStore();
86
+
87
+ expect(await store.read(GUILD_A, MODULE_A)).toBeNull();
88
+ });
89
+
90
+ it("creates a record when the expected revision is null", async () => {
91
+ const store = await createStore();
92
+ const created = record();
93
+
94
+ await store.write(created, { expectedRevision: null });
95
+
96
+ expect(await store.read(GUILD_A, MODULE_A)).toEqual(created);
97
+ });
98
+
99
+ it("rejects a create when a record already exists", async () => {
100
+ const store = await createStore();
101
+ await store.write(record(), { expectedRevision: null });
102
+
103
+ const error = await captureError(() =>
104
+ store.write(record({ values: { maxOpen: 3 } }), { expectedRevision: null }),
105
+ );
106
+
107
+ expect(error).toBeInstanceOf(ConflictError);
108
+ expect(await store.read(GUILD_A, MODULE_A)).toEqual(record());
109
+ });
110
+
111
+ it("rejects an update when no record exists", async () => {
112
+ const store = await createStore();
113
+
114
+ const error = await captureError(() =>
115
+ store.write(record({ revision: 2 }), { expectedRevision: 1 }),
116
+ );
117
+
118
+ expect(error).toBeInstanceOf(ConflictError);
119
+ expect(await store.read(GUILD_A, MODULE_A)).toBeNull();
120
+ });
121
+
122
+ it("rejects a write with a stale revision and keeps the stored record", async () => {
123
+ const store = await createStore();
124
+ await store.write(record(), { expectedRevision: null });
125
+ const second = record({ revision: 2, values: { maxOpen: 3 } });
126
+ await store.write(second, { expectedRevision: 1 });
127
+
128
+ const error = await captureError(() =>
129
+ store.write(record({ revision: 2, values: { maxOpen: 4 } }), { expectedRevision: 1 }),
130
+ );
131
+
132
+ expect(error).toBeInstanceOf(ConflictError);
133
+ expect(await store.read(GUILD_A, MODULE_A)).toEqual(second);
134
+ });
135
+
136
+ it("increments the revision by one on each write", async () => {
137
+ const store = await createStore();
138
+ await store.write(record(), { expectedRevision: null });
139
+ await store.write(record({ revision: 2 }), { expectedRevision: 1 });
140
+ await store.write(record({ revision: 3 }), { expectedRevision: 2 });
141
+
142
+ const stored = await store.read(GUILD_A, MODULE_A);
143
+
144
+ expect(stored?.revision).toBe(3);
145
+ });
146
+
147
+ it("deletes a record", async () => {
148
+ const store = await createStore();
149
+ await store.write(record(), { expectedRevision: null });
150
+
151
+ await store.delete(GUILD_A, MODULE_A);
152
+
153
+ expect(await store.read(GUILD_A, MODULE_A)).toBeNull();
154
+ });
155
+
156
+ it("deletes a missing record without error", async () => {
157
+ const store = await createStore();
158
+
159
+ const error = await captureError(() => store.delete(GUILD_A, MODULE_A));
160
+
161
+ expect(error).toBe(undefined);
162
+ expect(await store.read(GUILD_A, MODULE_A)).toBeNull();
163
+ });
164
+
165
+ it("accepts a create again after a delete", async () => {
166
+ const store = await createStore();
167
+ await store.write(record(), { expectedRevision: null });
168
+ await store.delete(GUILD_A, MODULE_A);
169
+
170
+ await store.write(record(), { expectedRevision: null });
171
+
172
+ expect(await store.read(GUILD_A, MODULE_A)).toEqual(record());
173
+ });
174
+
175
+ it("keeps guilds isolated", async () => {
176
+ await expectDeleteLeavesAlone(record({ guildId: GUILD_B, values: { maxOpen: 5 } }));
177
+ });
178
+
179
+ it("keeps modules isolated", async () => {
180
+ await expectDeleteLeavesAlone(record({ moduleId: MODULE_B, values: { greeting: "hi" } }));
181
+ });
182
+
183
+ it("round-trips every JSON value shape", async () => {
184
+ const store = await createStore();
185
+ const created = record({
186
+ values: {
187
+ text: "Salut « à vous » 👋",
188
+ count: -1.5,
189
+ enabled: false,
190
+ cleared: null,
191
+ roles: ["300000000000000001", "300000000000000002"],
192
+ nested: { toggles: { a: true }, list: [] },
193
+ },
194
+ });
195
+
196
+ await store.write(created, { expectedRevision: null });
197
+
198
+ expect(await store.read(GUILD_A, MODULE_A)).toEqual(created);
199
+ });
200
+
201
+ it("keeps an absent updatedBy absent", async () => {
202
+ const store = await createStore();
203
+ const { updatedBy: _omitted, ...anonymous } = record();
204
+
205
+ await store.write(anonymous, { expectedRevision: null });
206
+
207
+ expect(await store.read(GUILD_A, MODULE_A)).toEqual(anonymous);
208
+ });
209
+
210
+ it("keeps a written record independent of the caller's object", async () => {
211
+ const store = await createStore();
212
+ const written = record();
213
+ await store.write(written, { expectedRevision: null });
214
+
215
+ mutableValues(written).maxOpen = 99;
216
+
217
+ expect(await store.read(GUILD_A, MODULE_A)).toEqual(record());
218
+ });
219
+
220
+ it("returns a record the caller can change without changing the store", async () => {
221
+ const store = await createStore();
222
+ await store.write(record(), { expectedRevision: null });
223
+
224
+ mutableValues(await store.read(GUILD_A, MODULE_A)).maxOpen = 99;
225
+
226
+ expect(await store.read(GUILD_A, MODULE_A)).toEqual(record());
227
+ });
228
+
229
+ it("lets exactly one of two concurrent creates win", async () => {
230
+ const store = await createStore();
231
+ const first = record({ values: { maxOpen: 3 } });
232
+ const second = record({ values: { maxOpen: 4 } });
233
+
234
+ const outcomes = await Promise.allSettled([
235
+ store.write(first, { expectedRevision: null }),
236
+ store.write(second, { expectedRevision: null }),
237
+ ]);
238
+
239
+ await expectOneWinner(store, outcomes, [first, second]);
240
+ });
241
+
242
+ it("lets exactly one of two concurrent updates of the same revision win", async () => {
243
+ const store = await createStore();
244
+ await store.write(record(), { expectedRevision: null });
245
+ const first = record({ revision: 2, values: { maxOpen: 3 } });
246
+ const second = record({ revision: 2, values: { maxOpen: 4 } });
247
+
248
+ const outcomes = await Promise.allSettled([
249
+ store.write(first, { expectedRevision: 1 }),
250
+ store.write(second, { expectedRevision: 1 }),
251
+ ]);
252
+
253
+ await expectOneWinner(store, outcomes, [first, second]);
254
+ });
255
+ });
256
+
257
+ /**
258
+ * Of two racing writes, exactly one lands, the other fails with a
259
+ * `ConflictError`, and the stored record is the winner's.
260
+ */
261
+ async function expectOneWinner(
262
+ store: SettingsStore,
263
+ outcomes: readonly PromiseSettledResult<void>[],
264
+ written: readonly [StoredSettings, StoredSettings],
265
+ ): Promise<void> {
266
+ const winners = outcomes.flatMap((outcome, index) =>
267
+ outcome.status === "fulfilled" ? [written[index]] : [],
268
+ );
269
+ const losers = outcomes.flatMap((outcome) =>
270
+ outcome.status === "rejected" ? [outcome.reason] : [],
271
+ );
272
+
273
+ expect(winners.length).toBe(1);
274
+ expect(losers.length).toBe(1);
275
+ expect(losers[0]).toBeInstanceOf(ConflictError);
276
+ expect(await store.read(GUILD_A, MODULE_A)).toEqual(winners[0]);
277
+ }
278
+ }