@erinjs/core 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 (389) hide show
  1. package/core/.changeset/README.md +8 -0
  2. package/core/.changeset/community-bootstrap-release.md +17 -0
  3. package/core/.changeset/config.json +11 -0
  4. package/core/.changeset/no-changelog.js +16 -0
  5. package/core/.changeset/pre.json +17 -0
  6. package/core/.editorconfig +13 -0
  7. package/core/.gitattributes +2 -0
  8. package/core/.github/CODE_OF_CONDUCT.md +23 -0
  9. package/core/.github/FUNDING.yml +7 -0
  10. package/core/.github/ISSUE_TEMPLATE/bug_report.md +31 -0
  11. package/core/.github/ISSUE_TEMPLATE/feature_request.md +23 -0
  12. package/core/.github/PULL_REQUEST_TEMPLATE.md +16 -0
  13. package/core/.github/dependabot.yml +16 -0
  14. package/core/.github/workflows/autoapp.yml +16 -0
  15. package/core/.github/workflows/ci.yml +187 -0
  16. package/core/.github/workflows/codeql.yml +30 -0
  17. package/core/.github/workflows/deploy-docs.yml +54 -0
  18. package/core/.github/workflows/publish.yml +43 -0
  19. package/core/.lintstagedrc.json +4 -0
  20. package/core/.nvmrc +1 -0
  21. package/core/.prettierignore +8 -0
  22. package/core/.prettierrc +11 -0
  23. package/core/CONTRIBUTING.md +70 -0
  24. package/core/LICENSE +203 -0
  25. package/core/README.md +61 -0
  26. package/core/SECURITY.md +21 -0
  27. package/core/apps/docs/index.html +28 -0
  28. package/core/apps/docs/middleware.ts +21 -0
  29. package/core/apps/docs/package.json +33 -0
  30. package/core/apps/docs/public/@flux.png +0 -0
  31. package/core/apps/docs/public/docs/latest/guides.json +1420 -0
  32. package/core/apps/docs/public/docs/latest/main.json +14981 -0
  33. package/core/apps/docs/public/docs/latest/rag-index.json +1 -0
  34. package/core/apps/docs/public/docs/v1.0.5/guides.json +226 -0
  35. package/core/apps/docs/public/docs/v1.0.5/main.json +7920 -0
  36. package/core/apps/docs/public/docs/v1.0.6/guides.json +226 -0
  37. package/core/apps/docs/public/docs/v1.0.6/main.json +7920 -0
  38. package/core/apps/docs/public/docs/v1.0.7/guides.json +259 -0
  39. package/core/apps/docs/public/docs/v1.0.7/main.json +8652 -0
  40. package/core/apps/docs/public/docs/v1.0.8/guides.json +313 -0
  41. package/core/apps/docs/public/docs/v1.0.8/main.json +9618 -0
  42. package/core/apps/docs/public/docs/v1.0.9/guides.json +319 -0
  43. package/core/apps/docs/public/docs/v1.0.9/main.json +10694 -0
  44. package/core/apps/docs/public/docs/v1.1.0/guides.json +589 -0
  45. package/core/apps/docs/public/docs/v1.1.0/main.json +12576 -0
  46. package/core/apps/docs/public/docs/v1.1.2/guides.json +650 -0
  47. package/core/apps/docs/public/docs/v1.1.2/main.json +13239 -0
  48. package/core/apps/docs/public/docs/v1.1.3/guides.json +650 -0
  49. package/core/apps/docs/public/docs/v1.1.3/main.json +13239 -0
  50. package/core/apps/docs/public/docs/v1.1.4/guides.json +708 -0
  51. package/core/apps/docs/public/docs/v1.1.4/main.json +13231 -0
  52. package/core/apps/docs/public/docs/v1.1.5/guides.json +1035 -0
  53. package/core/apps/docs/public/docs/v1.1.5/main.json +13838 -0
  54. package/core/apps/docs/public/docs/v1.1.6/guides.json +1041 -0
  55. package/core/apps/docs/public/docs/v1.1.6/main.json +14313 -0
  56. package/core/apps/docs/public/docs/v1.1.8/guides.json +1047 -0
  57. package/core/apps/docs/public/docs/v1.1.8/main.json +14421 -0
  58. package/core/apps/docs/public/docs/v1.1.9/guides.json +1047 -0
  59. package/core/apps/docs/public/docs/v1.1.9/main.json +14421 -0
  60. package/core/apps/docs/public/docs/v1.2.0/guides.json +1212 -0
  61. package/core/apps/docs/public/docs/v1.2.0/main.json +14663 -0
  62. package/core/apps/docs/public/docs/v1.2.1/guides.json +1293 -0
  63. package/core/apps/docs/public/docs/v1.2.1/main.json +14828 -0
  64. package/core/apps/docs/public/docs/v1.2.2/guides.json +1293 -0
  65. package/core/apps/docs/public/docs/v1.2.2/main.json +15025 -0
  66. package/core/apps/docs/public/docs/v1.2.3/guides.json +1420 -0
  67. package/core/apps/docs/public/docs/v1.2.3/main.json +14954 -0
  68. package/core/apps/docs/public/docs/v1.2.4/guides.json +1420 -0
  69. package/core/apps/docs/public/docs/v1.2.4/main.json +14981 -0
  70. package/core/apps/docs/public/docs/versions.json +24 -0
  71. package/core/apps/docs/public/flux.png +0 -0
  72. package/core/apps/docs/public/locales/en.json +50 -0
  73. package/core/apps/docs/public/locales/guides-en.json +512 -0
  74. package/core/apps/docs/public/robots.txt +4 -0
  75. package/core/apps/docs/public/sitemap.xml +33 -0
  76. package/core/apps/docs/src/App.vue +538 -0
  77. package/core/apps/docs/src/components/ApiCategorySection.vue +42 -0
  78. package/core/apps/docs/src/components/ApiDiscordCompat.vue +65 -0
  79. package/core/apps/docs/src/components/ApiEndpointCard.vue +313 -0
  80. package/core/apps/docs/src/components/ApiSchemaBlock.vue +131 -0
  81. package/core/apps/docs/src/components/CodeBlock.vue +177 -0
  82. package/core/apps/docs/src/components/CommunityCallout.vue +90 -0
  83. package/core/apps/docs/src/components/ConstructorSection.vue +82 -0
  84. package/core/apps/docs/src/components/DocDescription.vue +40 -0
  85. package/core/apps/docs/src/components/FluxerLogo.vue +3 -0
  86. package/core/apps/docs/src/components/Footer.vue +106 -0
  87. package/core/apps/docs/src/components/GuideCodeBlock.vue +102 -0
  88. package/core/apps/docs/src/components/GuideDiscordCompat.vue +77 -0
  89. package/core/apps/docs/src/components/GuideDiscordCompatCallout.vue +83 -0
  90. package/core/apps/docs/src/components/GuideTable.vue +77 -0
  91. package/core/apps/docs/src/components/GuideTip.vue +38 -0
  92. package/core/apps/docs/src/components/MethodsSection.vue +195 -0
  93. package/core/apps/docs/src/components/ParamsTable.vue +70 -0
  94. package/core/apps/docs/src/components/PropertiesSection.vue +143 -0
  95. package/core/apps/docs/src/components/SearchBar.vue +76 -0
  96. package/core/apps/docs/src/components/SearchModal.vue +361 -0
  97. package/core/apps/docs/src/components/SidebarNav.vue +225 -0
  98. package/core/apps/docs/src/components/SponsorBanner.vue +153 -0
  99. package/core/apps/docs/src/components/TypeSignature.vue +187 -0
  100. package/core/apps/docs/src/components/VersionPicker.vue +191 -0
  101. package/core/apps/docs/src/composables/useSearchIndex.ts +144 -0
  102. package/core/apps/docs/src/composables/useVersionedPath.ts +20 -0
  103. package/core/apps/docs/src/data/apiEndpoints.ts +1073 -0
  104. package/core/apps/docs/src/data/changelog.ts +717 -0
  105. package/core/apps/docs/src/data/guides.ts +2362 -0
  106. package/core/apps/docs/src/env.d.ts +7 -0
  107. package/core/apps/docs/src/locales/guides-en.json +512 -0
  108. package/core/apps/docs/src/main.ts +27 -0
  109. package/core/apps/docs/src/pages/ApiReferenceLayout.vue +175 -0
  110. package/core/apps/docs/src/pages/ApiReferencePage.vue +128 -0
  111. package/core/apps/docs/src/pages/Changelog.vue +288 -0
  112. package/core/apps/docs/src/pages/ClassPage.vue +319 -0
  113. package/core/apps/docs/src/pages/ClassesList.vue +100 -0
  114. package/core/apps/docs/src/pages/DocsLayout.vue +127 -0
  115. package/core/apps/docs/src/pages/GuidePage.vue +279 -0
  116. package/core/apps/docs/src/pages/GuidesIndex.vue +166 -0
  117. package/core/apps/docs/src/pages/GuidesLayout.vue +245 -0
  118. package/core/apps/docs/src/pages/Home.vue +125 -0
  119. package/core/apps/docs/src/pages/NotFound.vue +57 -0
  120. package/core/apps/docs/src/pages/TypedefPage.vue +230 -0
  121. package/core/apps/docs/src/pages/TypedefsList.vue +168 -0
  122. package/core/apps/docs/src/pages/VersionLayout.vue +15 -0
  123. package/core/apps/docs/src/router.ts +73 -0
  124. package/core/apps/docs/src/stores/docs.ts +54 -0
  125. package/core/apps/docs/src/stores/guides.ts +53 -0
  126. package/core/apps/docs/src/stores/version.ts +67 -0
  127. package/core/apps/docs/src/styles/main.css +278 -0
  128. package/core/apps/docs/src/styles/prism.css +95 -0
  129. package/core/apps/docs/src/types/doc-schema.ts +112 -0
  130. package/core/apps/docs/tsconfig.json +17 -0
  131. package/core/apps/docs/tsconfig.node.json +10 -0
  132. package/core/apps/docs/vite.config.d.ts +2 -0
  133. package/core/apps/docs/vite.config.js +26 -0
  134. package/core/apps/docs/vite.config.ts +28 -0
  135. package/core/apps/docs-vitepress/.vitepress/config.ts +141 -0
  136. package/core/apps/docs-vitepress/api-data/latest/main.json +15035 -0
  137. package/core/apps/docs-vitepress/api-data/v1.2.4/main.json +15035 -0
  138. package/core/apps/docs-vitepress/api-data/versions.json +6 -0
  139. package/core/apps/docs-vitepress/index.md +15 -0
  140. package/core/apps/docs-vitepress/package-lock.json +2924 -0
  141. package/core/apps/docs-vitepress/package.json +20 -0
  142. package/core/apps/docs-vitepress/public/CNAME +1 -0
  143. package/core/apps/docs-vitepress/scripts/generate-api.ts +243 -0
  144. package/core/apps/docs-vitepress/scripts/migrate-guides.ts +129 -0
  145. package/core/apps/docs-vitepress/tsconfig.json +11 -0
  146. package/core/apps/docs-vitepress/v/latest/guides/attachments-by-url.md +57 -0
  147. package/core/apps/docs-vitepress/v/latest/guides/attachments.md +62 -0
  148. package/core/apps/docs-vitepress/v/latest/guides/basic-bot.md +49 -0
  149. package/core/apps/docs-vitepress/v/latest/guides/channels.md +180 -0
  150. package/core/apps/docs-vitepress/v/latest/guides/deprecated-apis.md +58 -0
  151. package/core/apps/docs-vitepress/v/latest/guides/discord-js-compatibility.md +42 -0
  152. package/core/apps/docs-vitepress/v/latest/guides/editing-embeds.md +65 -0
  153. package/core/apps/docs-vitepress/v/latest/guides/embed-media.md +87 -0
  154. package/core/apps/docs-vitepress/v/latest/guides/embeds.md +166 -0
  155. package/core/apps/docs-vitepress/v/latest/guides/emojis.md +77 -0
  156. package/core/apps/docs-vitepress/v/latest/guides/events.md +202 -0
  157. package/core/apps/docs-vitepress/v/latest/guides/gifs.md +47 -0
  158. package/core/apps/docs-vitepress/v/latest/guides/installation.md +10 -0
  159. package/core/apps/docs-vitepress/v/latest/guides/moderation.md +89 -0
  160. package/core/apps/docs-vitepress/v/latest/guides/permissions.md +130 -0
  161. package/core/apps/docs-vitepress/v/latest/guides/prefix-commands.md +41 -0
  162. package/core/apps/docs-vitepress/v/latest/guides/profile-urls.md +58 -0
  163. package/core/apps/docs-vitepress/v/latest/guides/reactions.md +69 -0
  164. package/core/apps/docs-vitepress/v/latest/guides/roles.md +130 -0
  165. package/core/apps/docs-vitepress/v/latest/guides/sending-without-reply.md +172 -0
  166. package/core/apps/docs-vitepress/v/latest/guides/voice.md +109 -0
  167. package/core/apps/docs-vitepress/v/latest/guides/wait-for-guilds.md +37 -0
  168. package/core/apps/docs-vitepress/v/latest/guides/webhook-attachments-embeds.md +73 -0
  169. package/core/apps/docs-vitepress/v/latest/guides/webhooks.md +131 -0
  170. package/core/eslint.config.js +80 -0
  171. package/core/examples/.env.example +22 -0
  172. package/core/examples/README.md +68 -0
  173. package/core/examples/first-steps-bot.js +118 -0
  174. package/core/examples/minimal-bot.js +17 -0
  175. package/core/examples/moderation-bot.js +209 -0
  176. package/core/examples/package.json +14 -0
  177. package/core/examples/ping-bot.js +1146 -0
  178. package/core/examples/reaction-bot.js +70 -0
  179. package/core/examples/reaction-roles-bot.js +140 -0
  180. package/core/examples/webhook-bot.js +239 -0
  181. package/core/flux.png +0 -0
  182. package/core/package.json +78 -0
  183. package/core/packages/builders/package.json +51 -0
  184. package/core/packages/builders/src/index.ts +13 -0
  185. package/core/packages/builders/src/messages/AttachmentBuilder.test.ts +79 -0
  186. package/core/packages/builders/src/messages/AttachmentBuilder.ts +69 -0
  187. package/core/packages/builders/src/messages/EmbedBuilder.test.ts +266 -0
  188. package/core/packages/builders/src/messages/EmbedBuilder.ts +239 -0
  189. package/core/packages/builders/src/messages/MessagePayload.test.ts +118 -0
  190. package/core/packages/builders/src/messages/MessagePayload.ts +122 -0
  191. package/core/packages/builders/tsconfig.json +9 -0
  192. package/core/packages/builders/tsup.config.ts +9 -0
  193. package/core/packages/builders/vitest.config.ts +9 -0
  194. package/core/packages/collection/package.json +47 -0
  195. package/core/packages/collection/src/Collection.test.ts +232 -0
  196. package/core/packages/collection/src/Collection.ts +196 -0
  197. package/core/packages/collection/src/index.ts +1 -0
  198. package/core/packages/collection/tsconfig.json +9 -0
  199. package/core/packages/collection/tsup.config.ts +9 -0
  200. package/core/packages/collection/vitest.config.ts +9 -0
  201. package/core/packages/docgen/package.json +26 -0
  202. package/core/packages/docgen/src/extract.ts +262 -0
  203. package/core/packages/docgen/src/formatType.ts +24 -0
  204. package/core/packages/docgen/src/index.ts +103 -0
  205. package/core/packages/docgen/src/schema.ts +100 -0
  206. package/core/packages/docgen/src/visitor.ts +147 -0
  207. package/core/packages/docgen/tsconfig.json +9 -0
  208. package/core/packages/docgen/tsup.config.ts +9 -0
  209. package/core/packages/fluxer-core/README.md +26 -0
  210. package/core/packages/fluxer-core/package.json +60 -0
  211. package/core/packages/fluxer-core/src/client/ChannelManager.ts +143 -0
  212. package/core/packages/fluxer-core/src/client/Client.gateway.test.ts +84 -0
  213. package/core/packages/fluxer-core/src/client/Client.resolveEmoji.test.ts +45 -0
  214. package/core/packages/fluxer-core/src/client/Client.ts +558 -0
  215. package/core/packages/fluxer-core/src/client/ClientUser.ts +40 -0
  216. package/core/packages/fluxer-core/src/client/EventHandlerRegistry.ts +469 -0
  217. package/core/packages/fluxer-core/src/client/GuildManager.ts +79 -0
  218. package/core/packages/fluxer-core/src/client/GuildMemberManager.ts +91 -0
  219. package/core/packages/fluxer-core/src/client/MessageManager.ts +58 -0
  220. package/core/packages/fluxer-core/src/client/UsersManager.ts +122 -0
  221. package/core/packages/fluxer-core/src/errors/ErrorCodes.test.ts +19 -0
  222. package/core/packages/fluxer-core/src/errors/ErrorCodes.ts +12 -0
  223. package/core/packages/fluxer-core/src/errors/FluxerError.test.ts +32 -0
  224. package/core/packages/fluxer-core/src/errors/FluxerError.ts +15 -0
  225. package/core/packages/fluxer-core/src/index.ts +85 -0
  226. package/core/packages/fluxer-core/src/structures/Base.ts +7 -0
  227. package/core/packages/fluxer-core/src/structures/Channel.ts +508 -0
  228. package/core/packages/fluxer-core/src/structures/Guild.test.ts +189 -0
  229. package/core/packages/fluxer-core/src/structures/Guild.ts +734 -0
  230. package/core/packages/fluxer-core/src/structures/GuildBan.ts +35 -0
  231. package/core/packages/fluxer-core/src/structures/GuildEmoji.ts +57 -0
  232. package/core/packages/fluxer-core/src/structures/GuildMember.test.ts +203 -0
  233. package/core/packages/fluxer-core/src/structures/GuildMember.ts +213 -0
  234. package/core/packages/fluxer-core/src/structures/GuildMemberRoleManager.ts +121 -0
  235. package/core/packages/fluxer-core/src/structures/GuildSticker.ts +56 -0
  236. package/core/packages/fluxer-core/src/structures/Invite.test.ts +103 -0
  237. package/core/packages/fluxer-core/src/structures/Invite.ts +121 -0
  238. package/core/packages/fluxer-core/src/structures/Message.test.ts +109 -0
  239. package/core/packages/fluxer-core/src/structures/Message.ts +397 -0
  240. package/core/packages/fluxer-core/src/structures/MessageReaction.ts +72 -0
  241. package/core/packages/fluxer-core/src/structures/PartialMessage.ts +12 -0
  242. package/core/packages/fluxer-core/src/structures/Role.test.ts +77 -0
  243. package/core/packages/fluxer-core/src/structures/Role.ts +112 -0
  244. package/core/packages/fluxer-core/src/structures/User.test.ts +110 -0
  245. package/core/packages/fluxer-core/src/structures/User.ts +109 -0
  246. package/core/packages/fluxer-core/src/structures/Webhook.test.ts +109 -0
  247. package/core/packages/fluxer-core/src/structures/Webhook.ts +258 -0
  248. package/core/packages/fluxer-core/src/util/Constants.test.ts +16 -0
  249. package/core/packages/fluxer-core/src/util/Constants.ts +7 -0
  250. package/core/packages/fluxer-core/src/util/Events.ts +46 -0
  251. package/core/packages/fluxer-core/src/util/MessageCollector.ts +87 -0
  252. package/core/packages/fluxer-core/src/util/Options.ts +33 -0
  253. package/core/packages/fluxer-core/src/util/ReactionCollector.ts +116 -0
  254. package/core/packages/fluxer-core/src/util/cdn.test.ts +108 -0
  255. package/core/packages/fluxer-core/src/util/cdn.ts +130 -0
  256. package/core/packages/fluxer-core/src/util/guildUtils.ts +33 -0
  257. package/core/packages/fluxer-core/src/util/messageUtils.test.ts +74 -0
  258. package/core/packages/fluxer-core/src/util/messageUtils.ts +119 -0
  259. package/core/packages/fluxer-core/src/util/permissions.test.ts +95 -0
  260. package/core/packages/fluxer-core/src/util/permissions.ts +43 -0
  261. package/core/packages/fluxer-core/tsconfig.json +9 -0
  262. package/core/packages/fluxer-core/tsup.config.ts +9 -0
  263. package/core/packages/fluxer-core/vitest.config.ts +9 -0
  264. package/core/packages/rest/package.json +52 -0
  265. package/core/packages/rest/src/REST.test.ts +64 -0
  266. package/core/packages/rest/src/REST.ts +90 -0
  267. package/core/packages/rest/src/RateLimitManager.test.ts +71 -0
  268. package/core/packages/rest/src/RateLimitManager.ts +60 -0
  269. package/core/packages/rest/src/RequestManager.test.ts +87 -0
  270. package/core/packages/rest/src/RequestManager.ts +172 -0
  271. package/core/packages/rest/src/errors/FluxerAPIError.test.ts +57 -0
  272. package/core/packages/rest/src/errors/FluxerAPIError.ts +21 -0
  273. package/core/packages/rest/src/errors/HTTPError.test.ts +55 -0
  274. package/core/packages/rest/src/errors/HTTPError.ts +25 -0
  275. package/core/packages/rest/src/errors/RateLimitError.test.ts +41 -0
  276. package/core/packages/rest/src/errors/RateLimitError.ts +15 -0
  277. package/core/packages/rest/src/errors/index.ts +3 -0
  278. package/core/packages/rest/src/index.ts +6 -0
  279. package/core/packages/rest/src/utils/constants.test.ts +31 -0
  280. package/core/packages/rest/src/utils/constants.ts +5 -0
  281. package/core/packages/rest/src/utils/files.test.ts +37 -0
  282. package/core/packages/rest/src/utils/files.ts +75 -0
  283. package/core/packages/rest/tsconfig.json +9 -0
  284. package/core/packages/rest/tsup.config.ts +9 -0
  285. package/core/packages/rest/vitest.config.ts +9 -0
  286. package/core/packages/types/package.json +46 -0
  287. package/core/packages/types/src/api/ban.ts +8 -0
  288. package/core/packages/types/src/api/channel.ts +65 -0
  289. package/core/packages/types/src/api/embed.ts +82 -0
  290. package/core/packages/types/src/api/emoji.ts +12 -0
  291. package/core/packages/types/src/api/errors.ts +68 -0
  292. package/core/packages/types/src/api/gateway.ts +14 -0
  293. package/core/packages/types/src/api/guild.ts +123 -0
  294. package/core/packages/types/src/api/index.ts +15 -0
  295. package/core/packages/types/src/api/instance.ts +32 -0
  296. package/core/packages/types/src/api/interaction.ts +26 -0
  297. package/core/packages/types/src/api/invite.ts +28 -0
  298. package/core/packages/types/src/api/message.ts +140 -0
  299. package/core/packages/types/src/api/role.ts +41 -0
  300. package/core/packages/types/src/api/sticker.ts +14 -0
  301. package/core/packages/types/src/api/user.ts +79 -0
  302. package/core/packages/types/src/api/webhook.ts +41 -0
  303. package/core/packages/types/src/common/index.ts +1 -0
  304. package/core/packages/types/src/common/snowflake.test.ts +9 -0
  305. package/core/packages/types/src/common/snowflake.ts +8 -0
  306. package/core/packages/types/src/gateway/events.ts +189 -0
  307. package/core/packages/types/src/gateway/index.ts +3 -0
  308. package/core/packages/types/src/gateway/opcodes.ts +17 -0
  309. package/core/packages/types/src/gateway/payloads.ts +481 -0
  310. package/core/packages/types/src/index.ts +4 -0
  311. package/core/packages/types/src/rest/index.ts +1 -0
  312. package/core/packages/types/src/rest/routes.test.ts +169 -0
  313. package/core/packages/types/src/rest/routes.ts +109 -0
  314. package/core/packages/types/tsconfig.json +9 -0
  315. package/core/packages/types/tsup.config.ts +9 -0
  316. package/core/packages/types/vitest.config.ts +9 -0
  317. package/core/packages/util/package.json +51 -0
  318. package/core/packages/util/src/BitField.test.ts +96 -0
  319. package/core/packages/util/src/BitField.ts +105 -0
  320. package/core/packages/util/src/MessageFlagsBitField.test.ts +42 -0
  321. package/core/packages/util/src/MessageFlagsBitField.ts +20 -0
  322. package/core/packages/util/src/PermissionsBitField.test.ts +79 -0
  323. package/core/packages/util/src/PermissionsBitField.ts +97 -0
  324. package/core/packages/util/src/SnowflakeUtil.test.ts +69 -0
  325. package/core/packages/util/src/SnowflakeUtil.ts +65 -0
  326. package/core/packages/util/src/UserFlagsBitField.test.ts +39 -0
  327. package/core/packages/util/src/UserFlagsBitField.ts +48 -0
  328. package/core/packages/util/src/deprecation.test.ts +44 -0
  329. package/core/packages/util/src/deprecation.ts +28 -0
  330. package/core/packages/util/src/emojiShortcodes.generated.ts +5 -0
  331. package/core/packages/util/src/emojiShortcodes.test.ts +41 -0
  332. package/core/packages/util/src/emojiShortcodes.ts +22 -0
  333. package/core/packages/util/src/formatters.test.ts +65 -0
  334. package/core/packages/util/src/formatters.ts +35 -0
  335. package/core/packages/util/src/index.ts +34 -0
  336. package/core/packages/util/src/resolvers.test.ts +198 -0
  337. package/core/packages/util/src/resolvers.ts +127 -0
  338. package/core/packages/util/src/tenorUtils.test.ts +75 -0
  339. package/core/packages/util/src/tenorUtils.ts +86 -0
  340. package/core/packages/util/tsconfig.json +9 -0
  341. package/core/packages/util/tsup.config.ts +9 -0
  342. package/core/packages/util/vitest.config.ts +9 -0
  343. package/core/packages/voice/README.md +42 -0
  344. package/core/packages/voice/package.json +67 -0
  345. package/core/packages/voice/src/LiveKitRtcConnection.receive.test.ts +24 -0
  346. package/core/packages/voice/src/LiveKitRtcConnection.ts +1767 -0
  347. package/core/packages/voice/src/VoiceConnection.ts +413 -0
  348. package/core/packages/voice/src/VoiceManager.receive.test.ts +61 -0
  349. package/core/packages/voice/src/VoiceManager.test.ts +44 -0
  350. package/core/packages/voice/src/VoiceManager.ts +503 -0
  351. package/core/packages/voice/src/exports.test.ts +38 -0
  352. package/core/packages/voice/src/index.ts +51 -0
  353. package/core/packages/voice/src/livekit.test.ts +48 -0
  354. package/core/packages/voice/src/livekit.ts +33 -0
  355. package/core/packages/voice/src/mp4box.d.ts +32 -0
  356. package/core/packages/voice/src/opusUtils.test.ts +29 -0
  357. package/core/packages/voice/src/opusUtils.ts +86 -0
  358. package/core/packages/voice/src/streamPreviewPlaceholder.test.ts +16 -0
  359. package/core/packages/voice/src/streamPreviewPlaceholder.ts +8 -0
  360. package/core/packages/voice/src/ws.d.ts +1 -0
  361. package/core/packages/voice/tsconfig.json +5 -0
  362. package/core/packages/voice/tsup.config.ts +10 -0
  363. package/core/packages/voice/vitest.config.ts +9 -0
  364. package/core/packages/ws/package.json +52 -0
  365. package/core/packages/ws/src/WebSocketManager.ts +130 -0
  366. package/core/packages/ws/src/WebSocketShard.ts +296 -0
  367. package/core/packages/ws/src/index.ts +12 -0
  368. package/core/packages/ws/src/utils/constants.test.ts +46 -0
  369. package/core/packages/ws/src/utils/constants.ts +22 -0
  370. package/core/packages/ws/src/utils/getWebSocket.ts +55 -0
  371. package/core/packages/ws/src/ws.d.ts +10 -0
  372. package/core/packages/ws/tsconfig.json +9 -0
  373. package/core/packages/ws/tsup.config.ts +9 -0
  374. package/core/pnpm-lock.yaml +7033 -0
  375. package/core/pnpm-workspace.yaml +4 -0
  376. package/core/scripts/generate-ai-rag.ts +240 -0
  377. package/core/scripts/generate-docs.ts +143 -0
  378. package/core/scripts/generate-emoji-shortcodes.ts +58 -0
  379. package/core/scripts/generate-types.ts +6 -0
  380. package/core/scripts/publish-ordered.js +63 -0
  381. package/core/scripts/test-cjs-require.mjs +43 -0
  382. package/core/scripts/test-esm-imports.mjs +42 -0
  383. package/core/scripts/test-package-exports.mjs +98 -0
  384. package/core/scripts/test-smoke.mjs +103 -0
  385. package/core/tsconfig.json +18 -0
  386. package/core/turbo.json +30 -0
  387. package/core/vitest.config.ts +17 -0
  388. package/core/wrangler.jsonc +9 -0
  389. package/package.json +26 -0
@@ -0,0 +1,119 @@
1
+ import { APIEmbed } from '@erinjs/types';
2
+ import { EmbedBuilder } from '@erinjs/builders';
3
+
4
+ /** Resolved file data (after URL fetch). Used internally by REST layer. */
5
+ export interface ResolvedMessageFile {
6
+ name: string;
7
+ data: Blob | ArrayBuffer | Uint8Array | Buffer;
8
+ filename?: string;
9
+ }
10
+
11
+ /** File data for message attachment uploads. Use `data` for buffers or `url` to fetch from a URL. */
12
+ export type MessageFileData =
13
+ | {
14
+ name: string;
15
+ data: Blob | ArrayBuffer | Uint8Array | Buffer;
16
+ filename?: string;
17
+ }
18
+ | {
19
+ name: string;
20
+ url: string;
21
+ filename?: string;
22
+ };
23
+
24
+ const FILE_FETCH_TIMEOUT_MS = 30_000;
25
+
26
+ /** Resolve files: fetch URLs to buffers, pass through data as-is. */
27
+ export async function resolveMessageFiles(
28
+ files: MessageFileData[],
29
+ ): Promise<ResolvedMessageFile[]> {
30
+ const result: ResolvedMessageFile[] = [];
31
+ for (let i = 0; i < files.length; i++) {
32
+ const f = files[i]!;
33
+ const filename = f.filename ?? f.name;
34
+ if ('url' in f && f.url) {
35
+ if (!URL.canParse(f.url)) {
36
+ throw new Error(`Invalid file URL at index ${i}: ${f.url}`);
37
+ }
38
+ const res = await fetch(f.url, {
39
+ signal: AbortSignal.timeout(FILE_FETCH_TIMEOUT_MS),
40
+ });
41
+ if (!res.ok) {
42
+ throw new Error(`Failed to fetch file from ${f.url}: ${res.status} ${res.statusText}`);
43
+ }
44
+ const data = await res.arrayBuffer();
45
+ result.push({ name: f.name, data, filename });
46
+ } else if ('data' in f && f.data != null) {
47
+ result.push({ name: f.name, data: f.data, filename });
48
+ } else {
49
+ throw new Error(`File at index ${i} must have either "data" or "url"`);
50
+ }
51
+ }
52
+ return result;
53
+ }
54
+
55
+ /** Attachment metadata for file uploads (id matches FormData index). */
56
+ export interface MessageAttachmentMeta {
57
+ id: number;
58
+ filename: string;
59
+ title?: string | null;
60
+ description?: string | null;
61
+ /** MessageAttachmentFlags: IS_SPOILER (8), CONTAINS_EXPLICIT_MEDIA (16), IS_ANIMATED (32) */
62
+ flags?: number;
63
+ }
64
+
65
+ /** Options for sending a message (content, embeds, files). Used by Message.send, Channel.send, ChannelManager.send.
66
+ * EmbedBuilder instances are auto-converted to API format—no need to call .toJSON().
67
+ */
68
+ export type MessageSendOptions = {
69
+ content?: string;
70
+ /** EmbedBuilder instances are auto-converted; raw APIEmbed also supported. */
71
+ embeds?: (APIEmbed | EmbedBuilder)[];
72
+ /** File attachments. When present, request uses multipart/form-data. */
73
+ files?: MessageFileData[];
74
+ /** Attachment metadata for files (id = index). Use when files are provided. */
75
+ attachments?: MessageAttachmentMeta[];
76
+ /** Message flags (e.g. MessageFlags.SuppressNotifications for reply without ping). */
77
+ flags?: number;
78
+ };
79
+
80
+ /** API-ready body from MessageSendOptions or text content (serializes EmbedBuilder, includes attachments when files present). */
81
+ export interface SendBodyResult {
82
+ content?: string;
83
+ embeds?: APIEmbed[];
84
+ attachments?: Array<{
85
+ id: number;
86
+ filename: string;
87
+ title?: string | null;
88
+ description?: string | null;
89
+ flags?: number;
90
+ }>;
91
+ /** Message flags (e.g. SuppressNotifications for reply without ping). */
92
+ flags?: number;
93
+ }
94
+
95
+ /** Build API-ready body from MessageSendOptions or text content (serializes EmbedBuilder to APIEmbed). */
96
+ export function buildSendBody(options: string | MessageSendOptions): SendBodyResult {
97
+ const body = typeof options === 'string' ? { content: options } : options;
98
+ const result: SendBodyResult = {};
99
+ if (body.content !== undefined) result.content = body.content;
100
+ if (body.embeds?.length) {
101
+ result.embeds = body.embeds.map((e) => (e instanceof EmbedBuilder ? e.toJSON() : e));
102
+ }
103
+ if (body.files?.length && body.attachments) {
104
+ result.attachments = body.attachments.map((a) => ({
105
+ id: a.id,
106
+ filename: a.filename,
107
+ ...(a.title != null && { title: a.title }),
108
+ ...(a.description != null && { description: a.description }),
109
+ ...(a.flags != null && { flags: a.flags }),
110
+ }));
111
+ } else if (body.files?.length) {
112
+ result.attachments = body.files.map((f, i) => ({
113
+ id: i,
114
+ filename: f.filename ?? f.name,
115
+ }));
116
+ }
117
+ if (body.flags !== undefined) result.flags = body.flags;
118
+ return result;
119
+ }
@@ -0,0 +1,95 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { computePermissions, hasPermission } from './permissions.js';
3
+ import { OverwriteType } from '@erinjs/types';
4
+ import { PermissionFlags } from '@erinjs/util';
5
+
6
+ describe('hasPermission', () => {
7
+ it('returns true when Administrator is set (grants all permissions)', () => {
8
+ expect(hasPermission(PermissionFlags.Administrator, PermissionFlags.Administrator)).toBe(true);
9
+ expect(hasPermission(PermissionFlags.Administrator, PermissionFlags.SendMessages)).toBe(true);
10
+ });
11
+
12
+ it('returns true when specific permission is set', () => {
13
+ expect(hasPermission(PermissionFlags.SendMessages, PermissionFlags.SendMessages)).toBe(true);
14
+ });
15
+
16
+ it('returns false when permission not set', () => {
17
+ expect(hasPermission(0n, PermissionFlags.SendMessages)).toBe(false);
18
+ expect(hasPermission(0n, PermissionFlags.Administrator)).toBe(false);
19
+ });
20
+
21
+ it('returns false when Administrator not set but other permission is', () => {
22
+ expect(hasPermission(PermissionFlags.BanMembers, PermissionFlags.Administrator)).toBe(false);
23
+ });
24
+ });
25
+
26
+ describe('computePermissions', () => {
27
+ it('returns base permissions when no overwrites', () => {
28
+ const base = PermissionFlags.SendMessages | PermissionFlags.ViewChannel;
29
+ const perms = computePermissions(base, [], [], 'user1', false);
30
+ expect(hasPermission(perms, PermissionFlags.SendMessages)).toBe(true);
31
+ expect(hasPermission(perms, PermissionFlags.ViewChannel)).toBe(true);
32
+ expect(hasPermission(perms, PermissionFlags.BanMembers)).toBe(false);
33
+ });
34
+
35
+ it('returns full permissions when owner (including high-bit Fluxer perms)', () => {
36
+ const perms = computePermissions(0n, [], [], 'user1', true);
37
+ expect(hasPermission(perms, PermissionFlags.SendMessages)).toBe(true);
38
+ expect(hasPermission(perms, PermissionFlags.BanMembers)).toBe(true);
39
+ expect(hasPermission(perms, BigInt(PermissionFlags.ManageRoles))).toBe(true);
40
+ expect(hasPermission(perms, BigInt(PermissionFlags.PinMessages))).toBe(true);
41
+ expect(hasPermission(perms, BigInt(PermissionFlags.BypassSlowmode))).toBe(true);
42
+ });
43
+
44
+ it('applies role deny overwrite', () => {
45
+ const base = PermissionFlags.SendMessages;
46
+ const overwrites = [{ id: 'role1', type: OverwriteType.Role, allow: '0', deny: '2048' }];
47
+ const perms = computePermissions(base, overwrites, ['role1'], 'user1', false);
48
+ expect(hasPermission(perms, PermissionFlags.SendMessages)).toBe(false);
49
+ });
50
+
51
+ it('applies role allow overwrite', () => {
52
+ const base = 0n;
53
+ const overwrites = [{ id: 'role1', type: OverwriteType.Role, allow: '2048', deny: '0' }];
54
+ const perms = computePermissions(base, overwrites, ['role1'], 'user1', false);
55
+ expect(hasPermission(perms, PermissionFlags.SendMessages)).toBe(true);
56
+ });
57
+
58
+ it('applies member overwrite', () => {
59
+ const base = 0n;
60
+ const overwrites = [{ id: 'user1', type: OverwriteType.Member, allow: '2048', deny: '0' }];
61
+ const perms = computePermissions(base, overwrites, [], 'user1', false);
62
+ expect(hasPermission(perms, PermissionFlags.SendMessages)).toBe(true);
63
+ });
64
+
65
+ it('member overwrite can deny base permission', () => {
66
+ const base = PermissionFlags.SendMessages;
67
+ const overwrites = [{ id: 'user1', type: OverwriteType.Member, allow: '0', deny: '2048' }];
68
+ const perms = computePermissions(base, overwrites, [], 'user1', false);
69
+ expect(hasPermission(perms, PermissionFlags.SendMessages)).toBe(false);
70
+ });
71
+
72
+ it('ignores overwrites that do not apply to member', () => {
73
+ const base = PermissionFlags.SendMessages;
74
+ const overwrites = [{ id: 'role99', type: OverwriteType.Role, allow: '0', deny: '2048' }];
75
+ const perms = computePermissions(base, overwrites, ['role1'], 'user1', false);
76
+ expect(hasPermission(perms, PermissionFlags.SendMessages)).toBe(true);
77
+ });
78
+
79
+ it('applies both role and member overwrites (member overwrite wins for same permission)', () => {
80
+ const base = 0n;
81
+ const overwrites = [
82
+ { id: 'role1', type: OverwriteType.Role, allow: '2048', deny: '0' },
83
+ { id: 'user1', type: OverwriteType.Member, allow: '0', deny: '2048' },
84
+ ];
85
+ const perms = computePermissions(base, overwrites, ['role1'], 'user1', false);
86
+ // Member deny overwrites role allow
87
+ expect(hasPermission(perms, PermissionFlags.SendMessages)).toBe(false);
88
+ });
89
+
90
+ it('empty overwrites returns base unchanged', () => {
91
+ const base = 2048n;
92
+ const perms = computePermissions(base, [], [], 'user1', false);
93
+ expect(perms).toBe(base);
94
+ });
95
+ });
@@ -0,0 +1,43 @@
1
+ import { OverwriteType, type APIChannelOverwrite } from '@erinjs/types';
2
+ import { ALL_PERMISSIONS_BIGINT, PermissionFlags } from '@erinjs/util';
3
+
4
+ /**
5
+ * Compute the effective permission bitfield for a member in a channel.
6
+ * Applies role permissions and channel overwrites.
7
+ * Guild owner always receives all permissions (matches Fluxer API behavior).
8
+ * @param basePermissions - Combined permissions from all member roles (guild base)
9
+ * @param overwrites - Channel permission overwrites
10
+ * @param memberRoles - Role IDs the member has
11
+ * @param memberId - Member user ID (for member-specific overwrites)
12
+ * @param isOwner - Whether the member is the guild owner (gets all permissions)
13
+ * @returns Effective permission bitfield as bigint
14
+ */
15
+ export function computePermissions(
16
+ basePermissions: bigint,
17
+ overwrites: APIChannelOverwrite[],
18
+ memberRoles: string[],
19
+ memberId: string,
20
+ isOwner: boolean,
21
+ ): bigint {
22
+ if (isOwner) return ALL_PERMISSIONS_BIGINT;
23
+ let perms = basePermissions;
24
+ for (const overwrite of overwrites ?? []) {
25
+ const applies =
26
+ (overwrite.type === OverwriteType.Role && memberRoles.includes(overwrite.id)) ||
27
+ (overwrite.type === OverwriteType.Member && overwrite.id === memberId);
28
+ if (!applies) continue;
29
+ const allow = BigInt(overwrite.allow || '0');
30
+ const deny = BigInt(overwrite.deny || '0');
31
+ perms = (perms & ~deny) | allow;
32
+ }
33
+ return (perms & PermissionFlags.Administrator) !== 0n ? ALL_PERMISSIONS_BIGINT : perms;
34
+ }
35
+
36
+ /**
37
+ * Check if a permission bitfield has a specific permission.
38
+ * Administrator (bit 3) implies all permissions per Fluxer/Discord convention.
39
+ */
40
+ export function hasPermission(bitfield: bigint, permission: bigint): boolean {
41
+ if ((bitfield & PermissionFlags.Administrator) !== 0n) return true;
42
+ return (bitfield & permission) !== 0n;
43
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": "./src",
6
+ "noEmit": false
7
+ },
8
+ "include": ["src/**/*"]
9
+ }
@@ -0,0 +1,9 @@
1
+ import { defineConfig } from 'tsup';
2
+
3
+ export default defineConfig({
4
+ entry: ['src/index.ts'],
5
+ format: ['cjs', 'esm'],
6
+ dts: true,
7
+ splitting: false,
8
+ clean: true,
9
+ });
@@ -0,0 +1,9 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globals: true,
6
+ environment: 'node',
7
+ include: ['src/**/*.test.ts'],
8
+ },
9
+ });
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@erinjs/rest",
3
+ "publishConfig": {
4
+ "access": "public"
5
+ },
6
+ "version": "1.2.4",
7
+ "description": "REST client for the Fluxer API",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/blstmo-abandoned-us-for-the-milk/core.git",
11
+ "directory": "packages/rest"
12
+ },
13
+ "bugs": "https://github.com/blstmo-abandoned-us-for-the-milk/core/issues",
14
+ "homepage": "https://github.com/blstmo-abandoned-us-for-the-milk/core#readme",
15
+ "keywords": [
16
+ "fluxer",
17
+ "api",
18
+ "rest",
19
+ "sdk",
20
+ "bot"
21
+ ],
22
+ "license": "Apache-2.0",
23
+ "main": "./dist/index.js",
24
+ "module": "./dist/index.mjs",
25
+ "types": "./dist/index.d.ts",
26
+ "exports": {
27
+ ".": {
28
+ "types": "./dist/index.d.ts",
29
+ "import": "./dist/index.mjs",
30
+ "require": "./dist/index.js"
31
+ }
32
+ },
33
+ "files": [
34
+ "dist"
35
+ ],
36
+ "scripts": {
37
+ "build": "tsup",
38
+ "clean": "rm -rf dist",
39
+ "lint": "eslint src --max-warnings 0 --config ../../eslint.config.js",
40
+ "lint:fix": "eslint src --fix --config ../../eslint.config.js",
41
+ "test": "vitest run --passWithNoTests",
42
+ "test:coverage": "vitest run --coverage --passWithNoTests"
43
+ },
44
+ "dependencies": {
45
+ "@erinjs/types": "workspace:*"
46
+ },
47
+ "devDependencies": {
48
+ "@types/node": "^20.0.0",
49
+ "tsup": "^8.3.0",
50
+ "typescript": "^5.6.0"
51
+ }
52
+ }
@@ -0,0 +1,64 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { REST } from './REST.js';
3
+ import { Routes } from '@erinjs/types';
4
+
5
+ describe('REST', () => {
6
+ let fetchMock: ReturnType<typeof vi.fn>;
7
+
8
+ beforeEach(() => {
9
+ fetchMock = vi.fn();
10
+ vi.stubGlobal('fetch', fetchMock);
11
+ });
12
+
13
+ afterEach(() => {
14
+ vi.unstubAllGlobals();
15
+ });
16
+
17
+ it('constructor and setToken', () => {
18
+ const rest = new REST();
19
+ expect(rest.token).toBeNull();
20
+ rest.setToken('abc123');
21
+ expect(rest.token).toBe('abc123');
22
+ rest.setToken(null);
23
+ expect(rest.token).toBeNull();
24
+ });
25
+
26
+ it('setToken returns this for chaining', () => {
27
+ const rest = new REST();
28
+ expect(rest.setToken('x')).toBe(rest);
29
+ });
30
+
31
+ it('get delegates to requestManager', async () => {
32
+ const rest = new REST();
33
+ fetchMock.mockResolvedValueOnce({
34
+ ok: true,
35
+ status: 200,
36
+ text: () => Promise.resolve('{"id":"1"}'),
37
+ headers: new Headers(),
38
+ });
39
+ const result = await rest.get('/channels/1');
40
+ expect(result).toEqual({ id: '1' });
41
+ });
42
+
43
+ it('post sends body', async () => {
44
+ const rest = new REST();
45
+ fetchMock.mockResolvedValueOnce({
46
+ ok: true,
47
+ status: 200,
48
+ text: () => Promise.resolve('{}'),
49
+ headers: new Headers(),
50
+ });
51
+ await rest.post('/channels', { body: { name: 'test' } });
52
+ expect(fetchMock).toHaveBeenCalledWith(
53
+ expect.any(String),
54
+ expect.objectContaining({
55
+ method: 'POST',
56
+ body: '{"name":"test"}',
57
+ }),
58
+ );
59
+ });
60
+
61
+ it('Routes is exposed', () => {
62
+ expect(REST.Routes).toBe(Routes);
63
+ });
64
+ });
@@ -0,0 +1,90 @@
1
+ import { EventEmitter } from 'events';
2
+ import { RequestManager } from './RequestManager.js';
3
+ import { Routes } from '@erinjs/types';
4
+
5
+ /** Options for the REST client. */
6
+ export interface RESTOptions {
7
+ api?: string;
8
+ version?: string;
9
+ authPrefix?: 'Bot' | 'Bearer';
10
+ timeout?: number;
11
+ retries?: number;
12
+ userAgent?: string;
13
+ }
14
+
15
+ /** HTTP client for the Fluxer API. Used by Client internally. */
16
+ export class REST extends EventEmitter {
17
+ private readonly requestManager: RequestManager;
18
+ private _token: string | null = null;
19
+
20
+ constructor(options: RESTOptions = {}) {
21
+ super();
22
+ this.requestManager = new RequestManager({
23
+ api: options.api ?? 'https://api.fluxer.app',
24
+ version: options.version ?? '1',
25
+ authPrefix: options.authPrefix ?? 'Bot',
26
+ timeout: options.timeout ?? 15000,
27
+ retries: options.retries ?? 3,
28
+ userAgent: options.userAgent ?? 'erin.js',
29
+ });
30
+ }
31
+
32
+ /** Set the bot token for authenticated requests. */
33
+ setToken(token: string | null): this {
34
+ this._token = token;
35
+ this.requestManager.setToken(token);
36
+ return this;
37
+ }
38
+
39
+ /** Current bot token, or null. */
40
+ get token(): string | null {
41
+ return this._token;
42
+ }
43
+
44
+ /** Send a GET request. */
45
+ async get<T>(route: string, options?: { auth?: boolean }): Promise<T> {
46
+ return this.requestManager.request<T>('GET', route, { auth: options?.auth });
47
+ }
48
+
49
+ /** Send a POST request. */
50
+ async post<T>(
51
+ route: string,
52
+ options?: {
53
+ body?: unknown;
54
+ auth?: boolean;
55
+ files?: Array<{ name: string; data: Blob | ArrayBuffer | Uint8Array; filename?: string }>;
56
+ },
57
+ ): Promise<T> {
58
+ return this.requestManager.request<T>('POST', route, {
59
+ body: options?.body,
60
+ auth: options?.auth,
61
+ files: options?.files,
62
+ });
63
+ }
64
+
65
+ /** Send a PATCH request. */
66
+ async patch<T>(route: string, options?: { body?: unknown; auth?: boolean }): Promise<T> {
67
+ return this.requestManager.request<T>('PATCH', route, {
68
+ body: options?.body,
69
+ auth: options?.auth,
70
+ });
71
+ }
72
+
73
+ /** Send a PUT request. */
74
+ async put<T>(route: string, options?: { body?: unknown; auth?: boolean }): Promise<T> {
75
+ return this.requestManager.request<T>('PUT', route, {
76
+ body: options?.body,
77
+ auth: options?.auth,
78
+ });
79
+ }
80
+
81
+ /** Send a DELETE request. */
82
+ async delete<T>(route: string, options?: { auth?: boolean }): Promise<T> {
83
+ return this.requestManager.request<T>('DELETE', route, { auth: options?.auth });
84
+ }
85
+
86
+ /** Route helpers (from @erinjs/types) for building paths. */
87
+ static get Routes(): typeof Routes {
88
+ return Routes;
89
+ }
90
+ }
@@ -0,0 +1,71 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { RateLimitManager } from './RateLimitManager.js';
3
+
4
+ describe('RateLimitManager', () => {
5
+ let manager: RateLimitManager;
6
+
7
+ beforeEach(() => {
8
+ manager = new RateLimitManager();
9
+ });
10
+
11
+ it('getBucket returns undefined for unknown route', () => {
12
+ expect(manager.getBucket('unknown')).toBeUndefined();
13
+ });
14
+
15
+ it('setBucket and getBucket', () => {
16
+ manager.setBucket('/channels/:id', 5, 3, Date.now() + 60000);
17
+ const bucket = manager.getBucket('/channels/:id');
18
+ expect(bucket).toEqual({ limit: 5, remaining: 3, resetAt: expect.any(Number) });
19
+ });
20
+
21
+ it('getWaitTime returns 0 when no limit', () => {
22
+ expect(manager.getWaitTime('any')).toBe(0);
23
+ });
24
+
25
+ it('getWaitTime returns wait when bucket exhausted', () => {
26
+ const resetAt = Date.now() + 5000;
27
+ manager.setBucket('route', 5, 0, resetAt);
28
+ const wait = manager.getWaitTime('route');
29
+ expect(wait).toBeGreaterThan(0);
30
+ expect(wait).toBeLessThanOrEqual(5000);
31
+ });
32
+
33
+ it('getWaitTime returns 0 when resetAt in past', () => {
34
+ manager.setBucket('route', 5, 0, Date.now() - 1000);
35
+ expect(manager.getWaitTime('route')).toBe(0);
36
+ });
37
+
38
+ it('setGlobalReset and getGlobalReset', () => {
39
+ const resetAt = Date.now() + 10000;
40
+ manager.setGlobalReset(resetAt);
41
+ expect(manager.getGlobalReset()).toBe(resetAt);
42
+ });
43
+
44
+ it('getWaitTime considers global reset', () => {
45
+ const resetAt = Date.now() + 3000;
46
+ manager.setGlobalReset(resetAt);
47
+ const wait = manager.getWaitTime('any-route');
48
+ expect(wait).toBeGreaterThan(0);
49
+ });
50
+
51
+ it('updateFromHeaders parses X-RateLimit headers', () => {
52
+ const resetSeconds = Math.floor(Date.now() / 1000) + 60;
53
+ const headers = new Headers({
54
+ 'X-RateLimit-Limit': '5',
55
+ 'X-RateLimit-Remaining': '2',
56
+ 'X-RateLimit-Reset': String(resetSeconds),
57
+ });
58
+ manager.updateFromHeaders('/channels/123', headers);
59
+ const bucket = manager.getBucket('/channels/123');
60
+ expect(bucket?.limit).toBe(5);
61
+ expect(bucket?.remaining).toBe(2);
62
+ });
63
+
64
+ it('updateFromHeaders handles Retry-After', () => {
65
+ const headers = new Headers({ 'Retry-After': '10' });
66
+ manager.updateFromHeaders('/global', headers);
67
+ const bucket = manager.getBucket('/global');
68
+ expect(bucket?.limit).toBe(1);
69
+ expect(bucket?.remaining).toBe(0);
70
+ });
71
+ });
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Tracks rate limit state per bucket (route hash).
3
+ * Delays requests when limit is exceeded.
4
+ */
5
+ export interface RateLimitState {
6
+ limit: number;
7
+ remaining: number;
8
+ resetAt: number;
9
+ }
10
+
11
+ export class RateLimitManager {
12
+ private buckets = new Map<string, RateLimitState>();
13
+ private globalResetAt = 0;
14
+
15
+ getBucket(route: string): RateLimitState | undefined {
16
+ return this.buckets.get(route);
17
+ }
18
+
19
+ setBucket(route: string, limit: number, remaining: number, resetAt: number): void {
20
+ this.buckets.set(route, { limit, remaining, resetAt });
21
+ }
22
+
23
+ setGlobalReset(resetAt: number): void {
24
+ this.globalResetAt = resetAt;
25
+ }
26
+
27
+ getGlobalReset(): number {
28
+ return this.globalResetAt;
29
+ }
30
+
31
+ /** Returns ms to wait before we can send again (0 if no wait). */
32
+ getWaitTime(route: string): number {
33
+ const now = Date.now();
34
+ const globalWait = this.globalResetAt > now ? this.globalResetAt - now : 0;
35
+ const bucket = this.buckets.get(route);
36
+ const bucketWait =
37
+ bucket && bucket.remaining <= 0 && bucket.resetAt > now ? bucket.resetAt - now : 0;
38
+ return Math.max(globalWait, bucketWait);
39
+ }
40
+
41
+ /** Parse rate limit headers and update state. */
42
+ updateFromHeaders(route: string, headers: Headers): void {
43
+ const limit = headers.get('X-RateLimit-Limit');
44
+ const remaining = headers.get('X-RateLimit-Remaining');
45
+ const reset = headers.get('X-RateLimit-Reset');
46
+ if (limit !== null && remaining !== null && reset !== null) {
47
+ const resetMs = parseInt(reset, 10);
48
+ if (!Number.isNaN(resetMs)) {
49
+ const resetAt = resetMs * 1000;
50
+ this.setBucket(route, parseInt(limit, 10), parseInt(remaining, 10), resetAt);
51
+ }
52
+ }
53
+ const retryAfter = headers.get('Retry-After');
54
+ if (retryAfter !== null) {
55
+ const sec = parseInt(retryAfter, 10);
56
+ const resetAt = Date.now() + (Number.isNaN(sec) ? 0 : sec * 1000);
57
+ this.setBucket(route, 1, 0, resetAt);
58
+ }
59
+ }
60
+ }