@beechcms/api 0.5.0 → 0.6.0-preview.2

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 (225) hide show
  1. package/assets/dashboard/BeechLogo.svg +18 -18
  2. package/assets/dashboard/BeechLogoLIght.svg +48 -48
  3. package/assets/dashboard/Searching.svg +1 -0
  4. package/assets/dashboard/assets/chart-BrJFdsGZ.js +39 -0
  5. package/assets/dashboard/assets/index-CIaoMxuB.css +1 -0
  6. package/assets/dashboard/assets/index-wTKgygsg.js +636 -0
  7. package/assets/dashboard/assets/pie-chart-recharts-BEaE1FOE.js +1 -0
  8. package/assets/dashboard/assets/timeseries-chart-recharts-DmWM6imF.js +1 -0
  9. package/assets/dashboard/beechLogoDark.svg +48 -48
  10. package/assets/dashboard/index.html +18 -18
  11. package/assets/dashboard/noResult.svg +43 -0
  12. package/assets/dashboard/sol.svg +3 -3
  13. package/assets/dashboard/undraw_enter_nwx3.svg +36 -36
  14. package/assets/dashboard/working.svg +1 -0
  15. package/migrations/0000_v040_base.sql +265 -238
  16. package/migrations/0029_automations.sql +14 -14
  17. package/migrations/0030_test_seeds.sql +125 -125
  18. package/package.json +13 -9
  19. package/src/auth/{in-memory-hash-provider.ts → __fixtures__/in-memory-hash-provider.ts} +17 -13
  20. package/src/auth/{static-token-service.ts → __fixtures__/static-token-service.ts} +22 -18
  21. package/src/auth/bcrypt-hash-provider.ts +24 -20
  22. package/src/auth/constants.ts +14 -10
  23. package/src/auth/generate-refresh-token.test.ts +23 -19
  24. package/src/auth/hash-provider.test.ts +55 -46
  25. package/src/auth/jose-token-service.ts +62 -55
  26. package/src/auth/jwt-claims-passthrough.test.ts +87 -0
  27. package/src/auth/login.test.ts +100 -92
  28. package/src/auth/login.ts +78 -74
  29. package/src/auth/refresh.ts +11 -5
  30. package/src/auth/token-service.test.ts +90 -82
  31. package/src/factory.ts +370 -347
  32. package/src/features/automations/__tests__/action-executors.test.ts +255 -268
  33. package/src/features/automations/__tests__/automation-runner.test.ts +196 -192
  34. package/src/features/automations/__tests__/automation-runner.utils.test.ts +60 -56
  35. package/src/features/automations/__tests__/automations.handler.test.ts +264 -260
  36. package/src/features/automations/__tests__/automations.repository.test.ts +163 -159
  37. package/src/features/automations/__tests__/automations.schema.test.ts +224 -134
  38. package/src/features/automations/__tests__/context-resolver.test.ts +126 -122
  39. package/src/features/automations/__tests__/cron-runner.test.ts +267 -263
  40. package/src/features/automations/__tests__/cron-runner.utils.test.ts +144 -140
  41. package/src/features/automations/__tests__/set-variable.executor.test.ts +383 -306
  42. package/src/features/automations/__tests__/template-grammar.test.ts +308 -304
  43. package/src/features/automations/__tests__/webhook.executor.test.ts +142 -0
  44. package/src/features/automations/__tests__/when-evaluator.test.ts +274 -270
  45. package/src/features/automations/__tests__/when-pushdown.test.ts +281 -277
  46. package/src/features/automations/action-executors/create-entry.executor.ts +27 -23
  47. package/src/features/automations/action-executors/edit-field.executor.ts +26 -22
  48. package/src/features/automations/action-executors/index.ts +37 -33
  49. package/src/features/automations/action-executors/send-mail.executor.ts +53 -42
  50. package/src/features/automations/action-executors/set-variable.executor.ts +159 -146
  51. package/src/features/automations/action-executors/webhook.executor.ts +67 -25
  52. package/src/features/automations/automation-runner.ts +85 -81
  53. package/src/features/automations/automation-runner.utils.ts +47 -43
  54. package/src/features/automations/automations.handler.ts +197 -193
  55. package/src/features/automations/automations.schema.ts +177 -160
  56. package/src/features/automations/context-resolver.ts +152 -148
  57. package/src/features/automations/cron-runner.ts +140 -136
  58. package/src/features/automations/cron-runner.utils.ts +44 -40
  59. package/src/features/automations/filter-translation.ts +46 -42
  60. package/src/features/automations/index.ts +16 -12
  61. package/src/features/automations/template-grammar.ts +245 -241
  62. package/src/features/automations/var-access-resolver.ts +140 -136
  63. package/src/features/automations/when-evaluator.ts +87 -83
  64. package/src/features/automations/when-pushdown.ts +57 -53
  65. package/src/features/backrefs/__tests__/backrefs.handler.test.ts +359 -0
  66. package/src/features/backrefs/backrefs.handler.ts +168 -0
  67. package/src/features/backrefs/d1-backref.repository.ts +84 -0
  68. package/src/features/backrefs/index.ts +5 -0
  69. package/src/features/content/constants.ts +16 -10
  70. package/src/features/content/handlers/bulk.handler.ts +185 -0
  71. package/src/features/content/handlers/create.ts +129 -165
  72. package/src/features/content/handlers/delete.ts +62 -87
  73. package/src/features/content/handlers/facets.ts +49 -45
  74. package/src/features/content/handlers/get.ts +120 -116
  75. package/src/features/content/handlers/helpers.test.ts +180 -0
  76. package/src/features/content/handlers/helpers.ts +93 -0
  77. package/src/features/content/handlers/list.ts +174 -88
  78. package/src/features/content/handlers/update.ts +174 -219
  79. package/src/features/content/index.ts +26 -20
  80. package/src/features/dashboard-layout/__tests__/dashboard-layout.handler.test.ts +388 -0
  81. package/src/features/dashboard-layout/dashboard-layout.handler.ts +206 -0
  82. package/src/features/dashboard-layout/index.ts +5 -0
  83. package/src/features/draft/draft.handler.ts +195 -170
  84. package/src/features/draft/draft.middleware.ts +66 -62
  85. package/src/features/draft/index.ts +5 -1
  86. package/src/features/email/email.provider.ts +42 -38
  87. package/src/features/email/email.service.ts +98 -93
  88. package/src/features/email/email.types.ts +119 -108
  89. package/src/features/email/index.ts +33 -29
  90. package/src/features/email/providers/resend.ts +67 -63
  91. package/src/features/email/providers/smtp.ts +55 -0
  92. package/src/features/email/templates/automation-mail.ts +19 -15
  93. package/src/features/email/templates/password-changed.ts +63 -59
  94. package/src/features/email/templates/password-reset.ts +68 -64
  95. package/src/features/email/templates/shell.ts +96 -92
  96. package/src/features/notifications/index.ts +5 -1
  97. package/src/features/notifications/notifications.handler.ts +105 -101
  98. package/src/features/password-reset/index.ts +19 -15
  99. package/src/features/password-reset/request.ts +92 -82
  100. package/src/features/password-reset/reset.ts +102 -92
  101. package/src/features/rotate-field/index.ts +5 -1
  102. package/src/features/rotate-field/rotate-field.handler.ts +132 -128
  103. package/src/features/rotate-field/rotate-field.schema.ts +17 -13
  104. package/src/features/schema/schema.handler.ts +131 -16
  105. package/src/features/seeds/index.ts +5 -0
  106. package/src/features/seeds/seeds.handler.test.ts +1199 -0
  107. package/src/features/seeds/seeds.handler.ts +558 -0
  108. package/src/features/settings/__tests__/settings.handler.test.ts +555 -0
  109. package/src/features/settings/settings.handler.ts +392 -302
  110. package/src/features/setup/index.ts +288 -91
  111. package/src/features/stats/index.ts +5 -1
  112. package/src/features/stats/stats.handler.ts +420 -430
  113. package/src/features/webhooks/index.ts +63 -0
  114. package/src/index.ts +65 -56
  115. package/src/media-utils.ts +82 -78
  116. package/src/middleware/auth-providers.middleware.test.ts +85 -0
  117. package/src/middleware/auth-providers.middleware.ts +45 -32
  118. package/src/middleware/observability.middleware.test.ts +93 -0
  119. package/src/middleware/observability.middleware.ts +70 -52
  120. package/src/middleware/rate-limit.middleware.ts +45 -41
  121. package/src/middleware/repository.middleware.ts +117 -91
  122. package/src/middleware/seed-registry.middleware.test.ts +97 -0
  123. package/src/middleware/seed-registry.middleware.ts +21 -0
  124. package/src/middleware/storage.middleware.ts +25 -21
  125. package/src/middleware.ts +45 -47
  126. package/src/public/access-policy.ts +27 -23
  127. package/src/public/api-key-middleware.ts +57 -53
  128. package/src/public/cache-utils.ts +38 -34
  129. package/src/public/entry-projection.ts +46 -42
  130. package/src/public/idempotency.ts +23 -19
  131. package/src/public/index.ts +16 -12
  132. package/src/public/problem-details.ts +124 -53
  133. package/src/public/public-add.ts +114 -110
  134. package/src/public/public-edit.ts +163 -159
  135. package/src/public/public-errors.ts +19 -15
  136. package/src/public/public-read.ts +63 -59
  137. package/src/public/public-routes.ts +88 -84
  138. package/src/public/query-builder.test.ts +224 -220
  139. package/src/public/query-builder.ts +156 -152
  140. package/src/public/rate-limit-middleware.ts +34 -30
  141. package/src/public/read-list.ts +54 -50
  142. package/src/public/read-single.ts +48 -44
  143. package/src/public/response-builder.ts +30 -26
  144. package/src/public/sanitize.ts +69 -65
  145. package/src/public/slug-utils.ts +18 -14
  146. package/src/rate-limit/cloudflare-rate-limiter.test.ts +30 -26
  147. package/src/rate-limit/cloudflare-rate-limiter.ts +15 -11
  148. package/src/rate-limit/in-memory-rate-limiter.test.ts +37 -33
  149. package/src/rate-limit/in-memory-rate-limiter.ts +17 -13
  150. package/src/rate-limit/no-op-rate-limiter.test.ts +22 -18
  151. package/src/rate-limit/no-op-rate-limiter.ts +11 -7
  152. package/src/search-utils.test.ts +211 -207
  153. package/src/search-utils.ts +213 -209
  154. package/src/search.ts +65 -61
  155. package/src/shared/apply-policies.test.ts +81 -77
  156. package/src/shared/apply-policies.ts +67 -63
  157. package/src/shared/automations.repository.d1.ts +150 -146
  158. package/src/shared/background-notification-service.test.ts +62 -58
  159. package/src/shared/background-notification-service.ts +52 -48
  160. package/src/shared/base.repository.d1.ts +32 -28
  161. package/src/shared/content-utils.test.ts +165 -161
  162. package/src/shared/content-utils.ts +86 -82
  163. package/src/shared/content.repository.d1.test.ts +507 -312
  164. package/src/shared/content.repository.d1.ts +851 -382
  165. package/src/shared/d1-activity-log.repository.test.ts +140 -136
  166. package/src/shared/d1-activity-log.repository.ts +105 -101
  167. package/src/shared/d1-activity-logger.test.ts +86 -82
  168. package/src/shared/d1-activity-logger.ts +67 -63
  169. package/src/shared/d1-analytics.repository.test.ts +78 -74
  170. package/src/shared/d1-analytics.repository.ts +85 -81
  171. package/src/shared/d1-content-scan.repository.test.ts +80 -76
  172. package/src/shared/d1-content-scan.repository.ts +37 -29
  173. package/src/shared/d1-notification.repository.test.ts +128 -124
  174. package/src/shared/d1-notification.repository.ts +118 -114
  175. package/src/shared/d1-password-reset-token.repository.test.ts +81 -77
  176. package/src/shared/d1-password-reset-token.repository.ts +56 -52
  177. package/src/shared/d1-search.repository.test.ts +87 -83
  178. package/src/shared/d1-search.repository.ts +88 -84
  179. package/src/shared/d1-session.repository.test.ts +125 -121
  180. package/src/shared/d1-session.repository.ts +102 -98
  181. package/src/shared/d1-setup-checklist.repository.ts +36 -0
  182. package/src/shared/d1-user.repository.test.ts +151 -147
  183. package/src/shared/d1-user.repository.ts +119 -109
  184. package/src/shared/d1-widget.repository.test.ts +267 -217
  185. package/src/shared/d1-widget.repository.ts +385 -337
  186. package/src/shared/dashboard-layout.repository.d1.test.ts +135 -0
  187. package/src/shared/dashboard-layout.repository.d1.ts +58 -0
  188. package/src/shared/demo-data-sql.ts +216 -0
  189. package/src/shared/demo-data.repository.d1.ts +20 -0
  190. package/src/shared/execution-context-scheduler.ts +13 -9
  191. package/src/shared/fixed-clock.ts +25 -21
  192. package/src/shared/fts-sync.ts +8 -4
  193. package/src/shared/idempotency.repository.d1.test.ts +83 -79
  194. package/src/shared/idempotency.repository.d1.ts +60 -56
  195. package/src/shared/in-memory-activity-logger.ts +19 -15
  196. package/src/shared/in-memory-notification-service.ts +19 -15
  197. package/src/shared/in-memory-seed.repository.ts +58 -0
  198. package/src/shared/media.repository.d1.test.ts +107 -103
  199. package/src/shared/media.repository.d1.ts +68 -64
  200. package/src/shared/qstash-notification-service.test.ts +67 -0
  201. package/src/shared/qstash-notification-service.ts +55 -0
  202. package/src/shared/query-utils.ts +141 -137
  203. package/src/shared/request-utils.ts +26 -22
  204. package/src/shared/schema-mutator.d1.test.ts +142 -0
  205. package/src/shared/schema-mutator.d1.ts +64 -0
  206. package/src/shared/seed-layout.repository.d1.test.ts +114 -0
  207. package/src/shared/seed-layout.repository.d1.ts +62 -0
  208. package/src/shared/seed-registry-cache.test.ts +68 -0
  209. package/src/shared/seed-registry-cache.ts +49 -0
  210. package/src/shared/seed.repository.d1.test.ts +143 -0
  211. package/src/shared/seed.repository.d1.ts +98 -0
  212. package/src/shared/sequential-id-generator.ts +33 -22
  213. package/src/shared/site-settings.repository.d1.ts +53 -0
  214. package/src/shared/storage/factory.ts +41 -40
  215. package/src/shared/storage/s3-bucket.ts +195 -163
  216. package/src/shared/storage-utils.ts +40 -36
  217. package/src/shared/system-stats.repository.d1.test.ts +58 -54
  218. package/src/shared/system-stats.repository.d1.ts +48 -44
  219. package/src/types.ts +90 -67
  220. package/src/upload.ts +198 -187
  221. package/src/widget.test.ts +85 -0
  222. package/src/widget.ts +251 -208
  223. package/assets/dashboard/assets/index-B1mUfgiK.css +0 -1
  224. package/assets/dashboard/assets/index-CHxxc_id.js +0 -629
  225. package/src/shared/storage/r2-binding-bucket.ts +0 -81
@@ -1,65 +1,69 @@
1
- import { validateAndSanitizeSeedPayload } from '@beechcms/core'
2
- import type { Seed, ValidationDetail } from '@beechcms/core'
3
-
4
- type PublicSanitizeSuccess = {
5
- ok: true
6
- data: Record<string, unknown>
7
- }
8
-
9
- type PublicSanitizeFailure = {
10
- ok: false
11
- status: 400 | 422
12
- code: 'validation_failed' | 'dangerous_content'
13
- message: string
14
- details?: ValidationDetail[]
15
- }
16
-
17
- export type PublicSanitizeResult = PublicSanitizeSuccess | PublicSanitizeFailure
18
-
19
- /**
20
- * Public API Adapter: uses the core foundation and maps errors into the sprint format.
21
- */
22
- export function sanitizePublicPayload(
23
- seed: Seed,
24
- payload: Record<string, unknown>,
25
- options: {
26
- allowNull?: boolean
27
- operation?: 'create' | 'update'
28
- requireAtLeastOneValidField?: boolean
29
- enforceRequiredFields?: boolean
30
- } = {}
31
- ): PublicSanitizeResult {
32
- const operation = options.operation ?? 'create'
33
- const result = validateAndSanitizeSeedPayload(seed, payload, {
34
- allowNull: options.allowNull ?? false,
35
- operation,
36
- requireAtLeastOneValidField: options.requireAtLeastOneValidField ?? true,
37
- enforceRequiredFields: options.enforceRequiredFields ?? true,
38
- })
39
-
40
- if (result.dangerousFields.length > 0) {
41
- const field = result.dangerousFields[0]
42
- return {
43
- ok: false,
44
- status: 422,
45
- code: 'dangerous_content',
46
- message: `Content rejected: dangerous markup detected in field '${field}'`,
47
- }
48
- }
49
-
50
- if (result.details.length > 0) {
51
- return {
52
- ok: false,
53
- status: 400,
54
- code: 'validation_failed',
55
- message: 'Validation failed',
56
- details: result.details,
57
- }
58
- }
59
-
60
- return {
61
- ok: true,
62
- data: result.data,
63
- }
64
- }
65
-
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
+ // See LICENSE in the repository root for license terms.
4
+
5
+ import { validateAndSanitizeSeedPayload } from '@beechcms/core'
6
+ import type { Seed, ValidationDetail } from '@beechcms/core'
7
+
8
+ type PublicSanitizeSuccess = {
9
+ ok: true
10
+ data: Record<string, unknown>
11
+ }
12
+
13
+ type PublicSanitizeFailure = {
14
+ ok: false
15
+ status: 400 | 422
16
+ code: 'validation_failed' | 'dangerous_content'
17
+ message: string
18
+ details?: ValidationDetail[]
19
+ }
20
+
21
+ export type PublicSanitizeResult = PublicSanitizeSuccess | PublicSanitizeFailure
22
+
23
+ /**
24
+ * Public API Adapter: uses the core foundation and maps errors into the sprint format.
25
+ */
26
+ export function sanitizePublicPayload(
27
+ seed: Seed,
28
+ payload: Record<string, unknown>,
29
+ options: {
30
+ allowNull?: boolean
31
+ operation?: 'create' | 'update'
32
+ requireAtLeastOneValidField?: boolean
33
+ enforceRequiredFields?: boolean
34
+ } = {}
35
+ ): PublicSanitizeResult {
36
+ const operation = options.operation ?? 'create'
37
+ const result = validateAndSanitizeSeedPayload(seed, payload, {
38
+ allowNull: options.allowNull ?? false,
39
+ operation,
40
+ requireAtLeastOneValidField: options.requireAtLeastOneValidField ?? true,
41
+ enforceRequiredFields: options.enforceRequiredFields ?? true,
42
+ })
43
+
44
+ if (result.dangerousFields.length > 0) {
45
+ const field = result.dangerousFields[0]
46
+ return {
47
+ ok: false,
48
+ status: 422,
49
+ code: 'dangerous_content',
50
+ message: `Content rejected: dangerous markup detected in field '${field}'`,
51
+ }
52
+ }
53
+
54
+ if (result.details.length > 0) {
55
+ return {
56
+ ok: false,
57
+ status: 400,
58
+ code: 'validation_failed',
59
+ message: 'Validation failed',
60
+ details: result.details,
61
+ }
62
+ }
63
+
64
+ return {
65
+ ok: true,
66
+ data: result.data,
67
+ }
68
+ }
69
+
@@ -1,14 +1,18 @@
1
- import { slugify, generateEntrySlug } from '@beechcms/core'
2
-
3
- /**
4
- * Converts a string into a URL-safe slug.
5
- * Logic moved to @beechcms/core for consistency between Dashboard and API.
6
- */
7
- export { slugify }
8
-
9
- /**
10
- * Generates a slug from a title/name or a UUID-like fallback.
11
- * Logic moved to @beechcms/core for consistency between Dashboard and API.
12
- */
13
- export { generateEntrySlug }
14
-
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
+ // See LICENSE in the repository root for license terms.
4
+
5
+ import { slugify, generateEntrySlug } from '@beechcms/core'
6
+
7
+ /**
8
+ * Converts a string into a URL-safe slug.
9
+ * Logic moved to @beechcms/core for consistency between Dashboard and API.
10
+ */
11
+ export { slugify }
12
+
13
+ /**
14
+ * Generates a slug from a title/name or a UUID-like fallback.
15
+ * Logic moved to @beechcms/core for consistency between Dashboard and API.
16
+ */
17
+ export { generateEntrySlug }
18
+
@@ -1,26 +1,30 @@
1
- import { describe, it, expect, vi } from 'vitest'
2
- import { CloudflareRateLimiter } from './cloudflare-rate-limiter'
3
-
4
- describe('CloudflareRateLimiter', () => {
5
- it('returns isAllowed: true when the Cloudflare binding grants the request', async () => {
6
- const mockBinding = { limit: vi.fn().mockResolvedValue({ success: true }) }
7
- const limiter = new CloudflareRateLimiter(mockBinding as any)
8
- const result = await limiter.checkLimit('192.168.1.1:login')
9
- expect(result.isAllowed).toBe(true)
10
- })
11
-
12
- it('returns isAllowed: false when the Cloudflare binding blocks the request', async () => {
13
- const mockBinding = { limit: vi.fn().mockResolvedValue({ success: false }) }
14
- const limiter = new CloudflareRateLimiter(mockBinding as any)
15
- const result = await limiter.checkLimit('192.168.1.1:login')
16
- expect(result.isAllowed).toBe(false)
17
- })
18
-
19
- it('forwards the exact key to the binding so per-key accounting is correct', async () => {
20
- const mockBinding = { limit: vi.fn().mockResolvedValue({ success: true }) }
21
- const limiter = new CloudflareRateLimiter(mockBinding as any)
22
- const key = '10.0.0.1:some-seed:publicApiRead'
23
- await limiter.checkLimit(key)
24
- expect(mockBinding.limit).toHaveBeenCalledWith({ key })
25
- })
26
- })
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
+ // See LICENSE in the repository root for license terms.
4
+
5
+ import { describe, it, expect, vi } from 'vitest'
6
+ import { CloudflareRateLimiter } from './cloudflare-rate-limiter'
7
+
8
+ describe('CloudflareRateLimiter', () => {
9
+ it('returns isAllowed: true when the Cloudflare binding grants the request', async () => {
10
+ const mockBinding = { limit: vi.fn().mockResolvedValue({ success: true }) }
11
+ const limiter = new CloudflareRateLimiter(mockBinding as any)
12
+ const result = await limiter.checkLimit('192.168.1.1:login')
13
+ expect(result.isAllowed).toBe(true)
14
+ })
15
+
16
+ it('returns isAllowed: false when the Cloudflare binding blocks the request', async () => {
17
+ const mockBinding = { limit: vi.fn().mockResolvedValue({ success: false }) }
18
+ const limiter = new CloudflareRateLimiter(mockBinding as any)
19
+ const result = await limiter.checkLimit('192.168.1.1:login')
20
+ expect(result.isAllowed).toBe(false)
21
+ })
22
+
23
+ it('forwards the exact key to the binding so per-key accounting is correct', async () => {
24
+ const mockBinding = { limit: vi.fn().mockResolvedValue({ success: true }) }
25
+ const limiter = new CloudflareRateLimiter(mockBinding as any)
26
+ const key = '10.0.0.1:some-seed:publicApiRead'
27
+ await limiter.checkLimit(key)
28
+ expect(mockBinding.limit).toHaveBeenCalledWith({ key })
29
+ })
30
+ })
@@ -1,11 +1,15 @@
1
- /// <reference types="@cloudflare/workers-types" />
2
- import type { IRateLimiter, RateLimitResult } from '@beechcms/core'
3
-
4
- export class CloudflareRateLimiter implements IRateLimiter {
5
- constructor(private readonly binding: RateLimit) {}
6
-
7
- async checkLimit(key: string): Promise<RateLimitResult> {
8
- const { success } = await this.binding.limit({ key })
9
- return { isAllowed: success }
10
- }
11
- }
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
+ // See LICENSE in the repository root for license terms.
4
+
5
+ /// <reference types="@cloudflare/workers-types" />
6
+ import type { IRateLimiter, RateLimitResult } from '@beechcms/core'
7
+
8
+ export class CloudflareRateLimiter implements IRateLimiter {
9
+ constructor(private readonly binding: RateLimit) {}
10
+
11
+ async checkLimit(key: string): Promise<RateLimitResult> {
12
+ const { success } = await this.binding.limit({ key })
13
+ return { isAllowed: success }
14
+ }
15
+ }
@@ -1,33 +1,37 @@
1
- import { describe, it, expect } from 'vitest'
2
- import { InMemoryRateLimiter } from './in-memory-rate-limiter'
3
-
4
- describe('InMemoryRateLimiter', () => {
5
- it('allows requests up to the configured maximum hit count', async () => {
6
- const limiter = new InMemoryRateLimiter(3)
7
- expect((await limiter.checkLimit('key')).isAllowed).toBe(true)
8
- expect((await limiter.checkLimit('key')).isAllowed).toBe(true)
9
- expect((await limiter.checkLimit('key')).isAllowed).toBe(true)
10
- })
11
-
12
- it('blocks the very next request after maxAllowedHits is reached', async () => {
13
- const limiter = new InMemoryRateLimiter(2)
14
- await limiter.checkLimit('key')
15
- await limiter.checkLimit('key')
16
- expect((await limiter.checkLimit('key')).isAllowed).toBe(false)
17
- })
18
-
19
- it('tracks hit counts independently for each key', async () => {
20
- const limiter = new InMemoryRateLimiter(1)
21
- expect((await limiter.checkLimit('ip-a')).isAllowed).toBe(true)
22
- expect((await limiter.checkLimit('ip-b')).isAllowed).toBe(true)
23
- // Both keys exhausted independently
24
- expect((await limiter.checkLimit('ip-a')).isAllowed).toBe(false)
25
- expect((await limiter.checkLimit('ip-b')).isAllowed).toBe(false)
26
- })
27
-
28
- it('maxAllowedHits = 1 allows exactly one request before blocking', async () => {
29
- const limiter = new InMemoryRateLimiter(1)
30
- expect((await limiter.checkLimit('k')).isAllowed).toBe(true)
31
- expect((await limiter.checkLimit('k')).isAllowed).toBe(false)
32
- })
33
- })
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
+ // See LICENSE in the repository root for license terms.
4
+
5
+ import { describe, it, expect } from 'vitest'
6
+ import { InMemoryRateLimiter } from './in-memory-rate-limiter'
7
+
8
+ describe('InMemoryRateLimiter', () => {
9
+ it('allows requests up to the configured maximum hit count', async () => {
10
+ const limiter = new InMemoryRateLimiter(3)
11
+ expect((await limiter.checkLimit('key')).isAllowed).toBe(true)
12
+ expect((await limiter.checkLimit('key')).isAllowed).toBe(true)
13
+ expect((await limiter.checkLimit('key')).isAllowed).toBe(true)
14
+ })
15
+
16
+ it('blocks the very next request after maxAllowedHits is reached', async () => {
17
+ const limiter = new InMemoryRateLimiter(2)
18
+ await limiter.checkLimit('key')
19
+ await limiter.checkLimit('key')
20
+ expect((await limiter.checkLimit('key')).isAllowed).toBe(false)
21
+ })
22
+
23
+ it('tracks hit counts independently for each key', async () => {
24
+ const limiter = new InMemoryRateLimiter(1)
25
+ expect((await limiter.checkLimit('ip-a')).isAllowed).toBe(true)
26
+ expect((await limiter.checkLimit('ip-b')).isAllowed).toBe(true)
27
+ // Both keys exhausted independently
28
+ expect((await limiter.checkLimit('ip-a')).isAllowed).toBe(false)
29
+ expect((await limiter.checkLimit('ip-b')).isAllowed).toBe(false)
30
+ })
31
+
32
+ it('maxAllowedHits = 1 allows exactly one request before blocking', async () => {
33
+ const limiter = new InMemoryRateLimiter(1)
34
+ expect((await limiter.checkLimit('k')).isAllowed).toBe(true)
35
+ expect((await limiter.checkLimit('k')).isAllowed).toBe(false)
36
+ })
37
+ })
@@ -1,13 +1,17 @@
1
- import type { IRateLimiter, RateLimitResult } from '@beechcms/core'
2
-
3
- export class InMemoryRateLimiter implements IRateLimiter {
4
- private readonly hitCounts = new Map<string, number>()
5
-
6
- constructor(private readonly maxAllowedHits: number) {}
7
-
8
- async checkLimit(key: string): Promise<RateLimitResult> {
9
- const currentHits = (this.hitCounts.get(key) ?? 0) + 1
10
- this.hitCounts.set(key, currentHits)
11
- return { isAllowed: currentHits <= this.maxAllowedHits }
12
- }
13
- }
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
+ // See LICENSE in the repository root for license terms.
4
+
5
+ import type { IRateLimiter, RateLimitResult } from '@beechcms/core'
6
+
7
+ export class InMemoryRateLimiter implements IRateLimiter {
8
+ private readonly hitCounts = new Map<string, number>()
9
+
10
+ constructor(private readonly maxAllowedHits: number) {}
11
+
12
+ async checkLimit(key: string): Promise<RateLimitResult> {
13
+ const currentHits = (this.hitCounts.get(key) ?? 0) + 1
14
+ this.hitCounts.set(key, currentHits)
15
+ return { isAllowed: currentHits <= this.maxAllowedHits }
16
+ }
17
+ }
@@ -1,18 +1,22 @@
1
- import { describe, it, expect } from 'vitest'
2
- import { NoOpRateLimiter } from './no-op-rate-limiter'
3
-
4
- describe('NoOpRateLimiter', () => {
5
- it('always returns isAllowed: true regardless of the key', async () => {
6
- const limiter = new NoOpRateLimiter()
7
- for (let i = 0; i < 100; i++) {
8
- expect((await limiter.checkLimit('any-key')).isAllowed).toBe(true)
9
- }
10
- })
11
-
12
- it('allows all keys without accumulating state', async () => {
13
- const limiter = new NoOpRateLimiter()
14
- expect((await limiter.checkLimit('ip-a')).isAllowed).toBe(true)
15
- expect((await limiter.checkLimit('ip-b')).isAllowed).toBe(true)
16
- expect((await limiter.checkLimit('ip-a')).isAllowed).toBe(true)
17
- })
18
- })
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
+ // See LICENSE in the repository root for license terms.
4
+
5
+ import { describe, it, expect } from 'vitest'
6
+ import { NoOpRateLimiter } from './no-op-rate-limiter'
7
+
8
+ describe('NoOpRateLimiter', () => {
9
+ it('always returns isAllowed: true regardless of the key', async () => {
10
+ const limiter = new NoOpRateLimiter()
11
+ for (let i = 0; i < 100; i++) {
12
+ expect((await limiter.checkLimit('any-key')).isAllowed).toBe(true)
13
+ }
14
+ })
15
+
16
+ it('allows all keys without accumulating state', async () => {
17
+ const limiter = new NoOpRateLimiter()
18
+ expect((await limiter.checkLimit('ip-a')).isAllowed).toBe(true)
19
+ expect((await limiter.checkLimit('ip-b')).isAllowed).toBe(true)
20
+ expect((await limiter.checkLimit('ip-a')).isAllowed).toBe(true)
21
+ })
22
+ })
@@ -1,7 +1,11 @@
1
- import type { IRateLimiter, RateLimitResult } from '@beechcms/core'
2
-
3
- export class NoOpRateLimiter implements IRateLimiter {
4
- async checkLimit(_key: string): Promise<RateLimitResult> {
5
- return { isAllowed: true }
6
- }
7
- }
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
+ // See LICENSE in the repository root for license terms.
4
+
5
+ import type { IRateLimiter, RateLimitResult } from '@beechcms/core'
6
+
7
+ export class NoOpRateLimiter implements IRateLimiter {
8
+ async checkLimit(_key: string): Promise<RateLimitResult> {
9
+ return { isAllowed: true }
10
+ }
11
+ }