@beechcms/api 0.6.0-preview.1 → 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.
- package/assets/dashboard/BeechLogo.svg +18 -18
- package/assets/dashboard/BeechLogoLIght.svg +48 -48
- package/assets/dashboard/assets/chart-BrJFdsGZ.js +39 -0
- package/assets/dashboard/assets/index-CIaoMxuB.css +1 -0
- package/assets/dashboard/assets/index-wTKgygsg.js +636 -0
- package/assets/dashboard/assets/pie-chart-recharts-BEaE1FOE.js +1 -0
- package/assets/dashboard/assets/timeseries-chart-recharts-DmWM6imF.js +1 -0
- package/assets/dashboard/beechLogoDark.svg +48 -48
- package/assets/dashboard/index.html +18 -18
- package/assets/dashboard/noResult.svg +42 -42
- package/assets/dashboard/sol.svg +3 -3
- package/assets/dashboard/undraw_enter_nwx3.svg +36 -36
- package/migrations/0000_v040_base.sql +265 -265
- package/migrations/0029_automations.sql +14 -14
- package/migrations/0030_test_seeds.sql +125 -125
- package/package.json +2 -2
- package/src/auth/__fixtures__/in-memory-hash-provider.ts +17 -17
- package/src/auth/__fixtures__/static-token-service.ts +22 -22
- package/src/auth/bcrypt-hash-provider.ts +24 -24
- package/src/auth/constants.ts +14 -14
- package/src/auth/generate-refresh-token.test.ts +23 -23
- package/src/auth/hash-provider.test.ts +55 -55
- package/src/auth/jose-token-service.ts +62 -62
- package/src/auth/jwt-claims-passthrough.test.ts +87 -87
- package/src/auth/login.test.ts +100 -100
- package/src/auth/login.ts +78 -78
- package/src/auth/refresh.ts +11 -11
- package/src/auth/token-service.test.ts +90 -90
- package/src/factory.ts +370 -368
- package/src/features/automations/__tests__/action-executors.test.ts +255 -255
- package/src/features/automations/__tests__/automation-runner.test.ts +196 -196
- package/src/features/automations/__tests__/automation-runner.utils.test.ts +60 -60
- package/src/features/automations/__tests__/automations.handler.test.ts +264 -264
- package/src/features/automations/__tests__/automations.repository.test.ts +163 -163
- package/src/features/automations/__tests__/automations.schema.test.ts +224 -224
- package/src/features/automations/__tests__/context-resolver.test.ts +126 -126
- package/src/features/automations/__tests__/cron-runner.test.ts +267 -267
- package/src/features/automations/__tests__/cron-runner.utils.test.ts +144 -144
- package/src/features/automations/__tests__/set-variable.executor.test.ts +383 -383
- package/src/features/automations/__tests__/template-grammar.test.ts +308 -308
- package/src/features/automations/__tests__/webhook.executor.test.ts +142 -142
- package/src/features/automations/__tests__/when-evaluator.test.ts +274 -274
- package/src/features/automations/__tests__/when-pushdown.test.ts +281 -281
- package/src/features/automations/action-executors/create-entry.executor.ts +27 -27
- package/src/features/automations/action-executors/edit-field.executor.ts +26 -26
- package/src/features/automations/action-executors/index.ts +37 -37
- package/src/features/automations/action-executors/send-mail.executor.ts +53 -53
- package/src/features/automations/action-executors/set-variable.executor.ts +159 -159
- package/src/features/automations/action-executors/webhook.executor.ts +67 -67
- package/src/features/automations/automation-runner.ts +85 -85
- package/src/features/automations/automation-runner.utils.ts +47 -47
- package/src/features/automations/automations.handler.ts +197 -197
- package/src/features/automations/automations.schema.ts +177 -177
- package/src/features/automations/context-resolver.ts +152 -152
- package/src/features/automations/cron-runner.ts +140 -140
- package/src/features/automations/cron-runner.utils.ts +44 -44
- package/src/features/automations/filter-translation.ts +46 -46
- package/src/features/automations/index.ts +16 -16
- package/src/features/automations/template-grammar.ts +245 -245
- package/src/features/automations/var-access-resolver.ts +140 -140
- package/src/features/automations/when-evaluator.ts +87 -87
- package/src/features/automations/when-pushdown.ts +57 -57
- package/src/features/backrefs/__tests__/backrefs.handler.test.ts +359 -359
- package/src/features/backrefs/backrefs.handler.ts +168 -168
- package/src/features/backrefs/d1-backref.repository.ts +84 -84
- package/src/features/backrefs/index.ts +5 -5
- package/src/features/content/constants.ts +16 -16
- package/src/features/content/handlers/bulk.handler.ts +185 -185
- package/src/features/content/handlers/create.ts +129 -129
- package/src/features/content/handlers/delete.ts +62 -62
- package/src/features/content/handlers/facets.ts +49 -49
- package/src/features/content/handlers/get.ts +120 -120
- package/src/features/content/handlers/helpers.test.ts +180 -180
- package/src/features/content/handlers/helpers.ts +93 -93
- package/src/features/content/handlers/list.ts +174 -174
- package/src/features/content/handlers/update.ts +174 -174
- package/src/features/content/index.ts +26 -26
- package/src/features/dashboard-layout/__tests__/dashboard-layout.handler.test.ts +388 -0
- package/src/features/dashboard-layout/dashboard-layout.handler.ts +206 -0
- package/src/features/dashboard-layout/index.ts +5 -0
- package/src/features/draft/draft.handler.ts +195 -195
- package/src/features/draft/draft.middleware.ts +66 -66
- package/src/features/draft/index.ts +5 -5
- package/src/features/email/email.provider.ts +42 -42
- package/src/features/email/email.service.ts +98 -98
- package/src/features/email/email.types.ts +119 -119
- package/src/features/email/index.ts +33 -33
- package/src/features/email/providers/resend.ts +67 -67
- package/src/features/email/providers/smtp.ts +55 -55
- package/src/features/email/templates/automation-mail.ts +19 -19
- package/src/features/email/templates/password-changed.ts +63 -63
- package/src/features/email/templates/password-reset.ts +68 -68
- package/src/features/email/templates/shell.ts +96 -96
- package/src/features/notifications/index.ts +5 -5
- package/src/features/notifications/notifications.handler.ts +105 -105
- package/src/features/password-reset/index.ts +19 -19
- package/src/features/password-reset/request.ts +92 -92
- package/src/features/password-reset/reset.ts +102 -102
- package/src/features/rotate-field/index.ts +5 -5
- package/src/features/rotate-field/rotate-field.handler.ts +132 -132
- package/src/features/rotate-field/rotate-field.schema.ts +17 -17
- package/src/features/schema/schema.handler.ts +131 -131
- package/src/features/seeds/index.ts +5 -5
- package/src/features/seeds/seeds.handler.test.ts +1199 -632
- package/src/features/seeds/seeds.handler.ts +558 -693
- package/src/features/settings/__tests__/settings.handler.test.ts +555 -555
- package/src/features/settings/settings.handler.ts +392 -392
- package/src/features/setup/index.ts +288 -204
- package/src/features/stats/index.ts +5 -5
- package/src/features/stats/stats.handler.ts +420 -420
- package/src/features/webhooks/index.ts +63 -63
- package/src/index.ts +65 -65
- package/src/media-utils.ts +82 -82
- package/src/middleware/auth-providers.middleware.test.ts +85 -0
- package/src/middleware/auth-providers.middleware.ts +45 -45
- package/src/middleware/observability.middleware.test.ts +93 -0
- package/src/middleware/observability.middleware.ts +70 -70
- package/src/middleware/rate-limit.middleware.ts +45 -45
- package/src/middleware/repository.middleware.ts +117 -114
- package/src/middleware/seed-registry.middleware.test.ts +97 -97
- package/src/middleware/seed-registry.middleware.ts +21 -21
- package/src/middleware/storage.middleware.ts +25 -25
- package/src/middleware.ts +45 -45
- package/src/public/access-policy.ts +27 -27
- package/src/public/api-key-middleware.ts +57 -57
- package/src/public/cache-utils.ts +38 -38
- package/src/public/entry-projection.ts +46 -46
- package/src/public/idempotency.ts +23 -23
- package/src/public/index.ts +16 -16
- package/src/public/problem-details.ts +124 -124
- package/src/public/public-add.ts +114 -114
- package/src/public/public-edit.ts +163 -163
- package/src/public/public-errors.ts +19 -19
- package/src/public/public-read.ts +63 -63
- package/src/public/public-routes.ts +88 -88
- package/src/public/query-builder.test.ts +224 -224
- package/src/public/query-builder.ts +156 -156
- package/src/public/rate-limit-middleware.ts +34 -34
- package/src/public/read-list.ts +54 -54
- package/src/public/read-single.ts +48 -48
- package/src/public/response-builder.ts +30 -30
- package/src/public/sanitize.ts +69 -69
- package/src/public/slug-utils.ts +18 -18
- package/src/rate-limit/cloudflare-rate-limiter.test.ts +30 -30
- package/src/rate-limit/cloudflare-rate-limiter.ts +15 -15
- package/src/rate-limit/in-memory-rate-limiter.test.ts +37 -37
- package/src/rate-limit/in-memory-rate-limiter.ts +17 -17
- package/src/rate-limit/no-op-rate-limiter.test.ts +22 -22
- package/src/rate-limit/no-op-rate-limiter.ts +11 -11
- package/src/search-utils.test.ts +211 -211
- package/src/search-utils.ts +213 -213
- package/src/search.ts +65 -65
- package/src/shared/apply-policies.test.ts +81 -81
- package/src/shared/apply-policies.ts +67 -67
- package/src/shared/automations.repository.d1.ts +150 -150
- package/src/shared/background-notification-service.test.ts +62 -62
- package/src/shared/background-notification-service.ts +52 -52
- package/src/shared/base.repository.d1.ts +32 -32
- package/src/shared/content-utils.test.ts +165 -165
- package/src/shared/content-utils.ts +86 -86
- package/src/shared/content.repository.d1.test.ts +507 -458
- package/src/shared/content.repository.d1.ts +851 -851
- package/src/shared/d1-activity-log.repository.test.ts +140 -140
- package/src/shared/d1-activity-log.repository.ts +105 -105
- package/src/shared/d1-activity-logger.test.ts +86 -86
- package/src/shared/d1-activity-logger.ts +67 -67
- package/src/shared/d1-analytics.repository.test.ts +78 -78
- package/src/shared/d1-analytics.repository.ts +85 -85
- package/src/shared/d1-content-scan.repository.test.ts +80 -80
- package/src/shared/d1-content-scan.repository.ts +37 -37
- package/src/shared/d1-notification.repository.test.ts +128 -128
- package/src/shared/d1-notification.repository.ts +118 -118
- package/src/shared/d1-password-reset-token.repository.test.ts +81 -81
- package/src/shared/d1-password-reset-token.repository.ts +56 -56
- package/src/shared/d1-search.repository.test.ts +87 -87
- package/src/shared/d1-search.repository.ts +88 -88
- package/src/shared/d1-session.repository.test.ts +125 -125
- package/src/shared/d1-session.repository.ts +102 -102
- package/src/shared/d1-setup-checklist.repository.ts +36 -36
- package/src/shared/d1-user.repository.test.ts +151 -151
- package/src/shared/d1-user.repository.ts +119 -119
- package/src/shared/d1-widget.repository.test.ts +267 -221
- package/src/shared/d1-widget.repository.ts +385 -341
- package/src/shared/dashboard-layout.repository.d1.test.ts +135 -0
- package/src/shared/dashboard-layout.repository.d1.ts +58 -0
- package/src/shared/demo-data-sql.ts +216 -54
- package/src/shared/demo-data.repository.d1.ts +20 -20
- package/src/shared/execution-context-scheduler.ts +13 -13
- package/src/shared/fixed-clock.ts +25 -25
- package/src/shared/fts-sync.ts +8 -8
- package/src/shared/idempotency.repository.d1.test.ts +83 -83
- package/src/shared/idempotency.repository.d1.ts +60 -60
- package/src/shared/in-memory-activity-logger.ts +19 -19
- package/src/shared/in-memory-notification-service.ts +19 -19
- package/src/shared/in-memory-seed.repository.ts +58 -51
- package/src/shared/media.repository.d1.test.ts +107 -107
- package/src/shared/media.repository.d1.ts +68 -68
- package/src/shared/qstash-notification-service.test.ts +67 -67
- package/src/shared/qstash-notification-service.ts +55 -55
- package/src/shared/query-utils.ts +141 -141
- package/src/shared/request-utils.ts +26 -26
- package/src/shared/schema-mutator.d1.test.ts +142 -0
- package/src/shared/schema-mutator.d1.ts +64 -64
- package/src/shared/seed-layout.repository.d1.test.ts +114 -114
- package/src/shared/seed-layout.repository.d1.ts +62 -62
- package/src/shared/seed-registry-cache.test.ts +68 -68
- package/src/shared/seed-registry-cache.ts +49 -49
- package/src/shared/seed.repository.d1.test.ts +143 -143
- package/src/shared/seed.repository.d1.ts +98 -98
- package/src/shared/sequential-id-generator.ts +33 -33
- package/src/shared/site-settings.repository.d1.ts +53 -53
- package/src/shared/storage/factory.ts +41 -41
- package/src/shared/storage/s3-bucket.ts +195 -195
- package/src/shared/storage-utils.ts +40 -40
- package/src/shared/system-stats.repository.d1.test.ts +58 -58
- package/src/shared/system-stats.repository.d1.ts +48 -48
- package/src/types.ts +90 -87
- package/src/upload.ts +198 -198
- package/src/widget.test.ts +85 -0
- package/src/widget.ts +251 -212
- package/assets/dashboard/assets/index-BSjk0Sx9.js +0 -634
- package/assets/dashboard/assets/index-kRCYVN2B.css +0 -1
package/src/public/sanitize.ts
CHANGED
|
@@ -1,69 +1,69 @@
|
|
|
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
|
+
// 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
|
+
|
package/src/public/slug-utils.ts
CHANGED
|
@@ -1,18 +1,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 { 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
|
+
// 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,30 +1,30 @@
|
|
|
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
|
+
// 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,15 +1,15 @@
|
|
|
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
|
+
// 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,37 +1,37 @@
|
|
|
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
|
+
// 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,17 +1,17 @@
|
|
|
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
|
+
// 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,22 +1,22 @@
|
|
|
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
|
+
// 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,11 +1,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
|
-
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
|
-
}
|
|
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
|
+
}
|