@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
|
@@ -1,142 +1,142 @@
|
|
|
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, beforeEach } from 'vitest'
|
|
6
|
-
import { executeWebhook } from '../action-executors/webhook.executor'
|
|
7
|
-
import type { ResolvedContext } from '../context-resolver'
|
|
8
|
-
|
|
9
|
-
function makeContext(entry: Record<string, unknown> = {}, event = 'create'): ResolvedContext {
|
|
10
|
-
return {
|
|
11
|
-
triggerEntry: entry,
|
|
12
|
-
lookup(parsed) {
|
|
13
|
-
if (parsed.kind === 'simple') {
|
|
14
|
-
const key = parsed.path.startsWith('this.') ? parsed.path.slice(5) : parsed.path
|
|
15
|
-
if (key === 'trigger_event') return event
|
|
16
|
-
return entry[key] ?? null
|
|
17
|
-
}
|
|
18
|
-
return null
|
|
19
|
-
},
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const baseContext = makeContext({ id: 'entry-1', title: 'Hello' })
|
|
24
|
-
|
|
25
|
-
const baseAction = {
|
|
26
|
-
type: 'webhook' as const,
|
|
27
|
-
url: 'https://example.com/hook',
|
|
28
|
-
body_template: '{"static":true}',
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function makeOkResponse(status = 200): Response {
|
|
32
|
-
return { ok: true, status } as unknown as Response
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function makeErrorResponse(status: number): Response {
|
|
36
|
-
return { ok: false, status } as unknown as Response
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
beforeEach(() => {
|
|
40
|
-
vi.restoreAllMocks()
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
describe('executeWebhook', () => {
|
|
44
|
-
it('signs request when WEBHOOK_SECRET is set', async () => {
|
|
45
|
-
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(makeOkResponse())
|
|
46
|
-
|
|
47
|
-
await executeWebhook(baseAction, baseContext, { WEBHOOK_SECRET: 'test-secret' })
|
|
48
|
-
|
|
49
|
-
expect(fetchSpy).toHaveBeenCalledOnce()
|
|
50
|
-
const [, init] = fetchSpy.mock.calls[0]
|
|
51
|
-
const headers = init?.headers as Record<string, string>
|
|
52
|
-
expect(headers['X-BeechCMS-Signature']).toMatch(/^sha256=[0-9a-f]{64}$/)
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
it('omits signature header and warns once when WEBHOOK_SECRET is absent', async () => {
|
|
56
|
-
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(makeOkResponse())
|
|
57
|
-
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
58
|
-
|
|
59
|
-
await executeWebhook(baseAction, baseContext, {})
|
|
60
|
-
await executeWebhook(baseAction, baseContext, {})
|
|
61
|
-
|
|
62
|
-
const [, init] = fetchSpy.mock.calls[0]
|
|
63
|
-
const headers = init?.headers as Record<string, string>
|
|
64
|
-
expect(headers['X-BeechCMS-Signature']).toBeUndefined()
|
|
65
|
-
expect(warnSpy).toHaveBeenCalledOnce()
|
|
66
|
-
expect(warnSpy.mock.calls[0][0]).toContain('WEBHOOK_SECRET')
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
it('overwrites user-supplied X-BeechCMS-Signature header with computed value', async () => {
|
|
70
|
-
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(makeOkResponse())
|
|
71
|
-
|
|
72
|
-
const action = {
|
|
73
|
-
...baseAction,
|
|
74
|
-
headers: { 'X-BeechCMS-Signature': 'sha256=user-supplied-value' },
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
await executeWebhook(action, baseContext, { WEBHOOK_SECRET: 'test-secret' })
|
|
78
|
-
|
|
79
|
-
const [, init] = fetchSpy.mock.calls[0]
|
|
80
|
-
const headers = init?.headers as Record<string, string>
|
|
81
|
-
expect(headers['X-BeechCMS-Signature']).toMatch(/^sha256=[0-9a-f]{64}$/)
|
|
82
|
-
expect(headers['X-BeechCMS-Signature']).not.toBe('sha256=user-supplied-value')
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
it('throws with status code when endpoint responds with 500', async () => {
|
|
86
|
-
vi.spyOn(globalThis, 'fetch').mockResolvedValue(makeErrorResponse(500))
|
|
87
|
-
|
|
88
|
-
await expect(executeWebhook(baseAction, baseContext, {}))
|
|
89
|
-
.rejects.toThrow('500')
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
it('interpolates body_template placeholders before sending', async () => {
|
|
93
|
-
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(makeOkResponse())
|
|
94
|
-
|
|
95
|
-
const action = { ...baseAction, body_template: '{"event":"{{trigger_event}}","id":"{{id}}"}' }
|
|
96
|
-
const ctx = makeContext({ id: 'abc-123' }, 'update')
|
|
97
|
-
|
|
98
|
-
await executeWebhook(action, ctx, {})
|
|
99
|
-
|
|
100
|
-
const [, init] = fetchSpy.mock.calls[0]
|
|
101
|
-
const body = init?.body as string
|
|
102
|
-
expect(body).toContain('abc-123')
|
|
103
|
-
expect(body).toContain('update')
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
it('recalculates HMAC and matches X-BeechCMS-Signature header', async () => {
|
|
107
|
-
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(makeOkResponse())
|
|
108
|
-
const secret = 'verify-secret'
|
|
109
|
-
|
|
110
|
-
await executeWebhook(baseAction, baseContext, { WEBHOOK_SECRET: secret })
|
|
111
|
-
|
|
112
|
-
const [, init] = fetchSpy.mock.calls[0]
|
|
113
|
-
const headers = init?.headers as Record<string, string>
|
|
114
|
-
const sentBody = init?.body as string
|
|
115
|
-
const sentSig = headers['X-BeechCMS-Signature']
|
|
116
|
-
|
|
117
|
-
const encoder = new TextEncoder()
|
|
118
|
-
const key = await crypto.subtle.importKey(
|
|
119
|
-
'raw',
|
|
120
|
-
encoder.encode(secret),
|
|
121
|
-
{ name: 'HMAC', hash: 'SHA-256' },
|
|
122
|
-
false,
|
|
123
|
-
['sign'],
|
|
124
|
-
)
|
|
125
|
-
const sig = await crypto.subtle.sign('HMAC', key, encoder.encode(sentBody))
|
|
126
|
-
const expected = 'sha256=' + Array.from(new Uint8Array(sig))
|
|
127
|
-
.map((b) => b.toString(16).padStart(2, '0'))
|
|
128
|
-
.join('')
|
|
129
|
-
|
|
130
|
-
expect(sentSig).toBe(expected)
|
|
131
|
-
})
|
|
132
|
-
|
|
133
|
-
it('throws timeout error when fetch takes longer than 8 seconds', async () => {
|
|
134
|
-
vi.spyOn(globalThis, 'fetch').mockImplementation(() => {
|
|
135
|
-
const err = new DOMException('The operation was aborted.', 'TimeoutError')
|
|
136
|
-
return Promise.reject(err)
|
|
137
|
-
})
|
|
138
|
-
|
|
139
|
-
await expect(executeWebhook(baseAction, baseContext, {}))
|
|
140
|
-
.rejects.toThrow('timed out after 8000ms')
|
|
141
|
-
})
|
|
142
|
-
})
|
|
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, beforeEach } from 'vitest'
|
|
6
|
+
import { executeWebhook } from '../action-executors/webhook.executor'
|
|
7
|
+
import type { ResolvedContext } from '../context-resolver'
|
|
8
|
+
|
|
9
|
+
function makeContext(entry: Record<string, unknown> = {}, event = 'create'): ResolvedContext {
|
|
10
|
+
return {
|
|
11
|
+
triggerEntry: entry,
|
|
12
|
+
lookup(parsed) {
|
|
13
|
+
if (parsed.kind === 'simple') {
|
|
14
|
+
const key = parsed.path.startsWith('this.') ? parsed.path.slice(5) : parsed.path
|
|
15
|
+
if (key === 'trigger_event') return event
|
|
16
|
+
return entry[key] ?? null
|
|
17
|
+
}
|
|
18
|
+
return null
|
|
19
|
+
},
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const baseContext = makeContext({ id: 'entry-1', title: 'Hello' })
|
|
24
|
+
|
|
25
|
+
const baseAction = {
|
|
26
|
+
type: 'webhook' as const,
|
|
27
|
+
url: 'https://example.com/hook',
|
|
28
|
+
body_template: '{"static":true}',
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function makeOkResponse(status = 200): Response {
|
|
32
|
+
return { ok: true, status } as unknown as Response
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function makeErrorResponse(status: number): Response {
|
|
36
|
+
return { ok: false, status } as unknown as Response
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
beforeEach(() => {
|
|
40
|
+
vi.restoreAllMocks()
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
describe('executeWebhook', () => {
|
|
44
|
+
it('signs request when WEBHOOK_SECRET is set', async () => {
|
|
45
|
+
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(makeOkResponse())
|
|
46
|
+
|
|
47
|
+
await executeWebhook(baseAction, baseContext, { WEBHOOK_SECRET: 'test-secret' })
|
|
48
|
+
|
|
49
|
+
expect(fetchSpy).toHaveBeenCalledOnce()
|
|
50
|
+
const [, init] = fetchSpy.mock.calls[0]
|
|
51
|
+
const headers = init?.headers as Record<string, string>
|
|
52
|
+
expect(headers['X-BeechCMS-Signature']).toMatch(/^sha256=[0-9a-f]{64}$/)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('omits signature header and warns once when WEBHOOK_SECRET is absent', async () => {
|
|
56
|
+
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(makeOkResponse())
|
|
57
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
58
|
+
|
|
59
|
+
await executeWebhook(baseAction, baseContext, {})
|
|
60
|
+
await executeWebhook(baseAction, baseContext, {})
|
|
61
|
+
|
|
62
|
+
const [, init] = fetchSpy.mock.calls[0]
|
|
63
|
+
const headers = init?.headers as Record<string, string>
|
|
64
|
+
expect(headers['X-BeechCMS-Signature']).toBeUndefined()
|
|
65
|
+
expect(warnSpy).toHaveBeenCalledOnce()
|
|
66
|
+
expect(warnSpy.mock.calls[0][0]).toContain('WEBHOOK_SECRET')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('overwrites user-supplied X-BeechCMS-Signature header with computed value', async () => {
|
|
70
|
+
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(makeOkResponse())
|
|
71
|
+
|
|
72
|
+
const action = {
|
|
73
|
+
...baseAction,
|
|
74
|
+
headers: { 'X-BeechCMS-Signature': 'sha256=user-supplied-value' },
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
await executeWebhook(action, baseContext, { WEBHOOK_SECRET: 'test-secret' })
|
|
78
|
+
|
|
79
|
+
const [, init] = fetchSpy.mock.calls[0]
|
|
80
|
+
const headers = init?.headers as Record<string, string>
|
|
81
|
+
expect(headers['X-BeechCMS-Signature']).toMatch(/^sha256=[0-9a-f]{64}$/)
|
|
82
|
+
expect(headers['X-BeechCMS-Signature']).not.toBe('sha256=user-supplied-value')
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('throws with status code when endpoint responds with 500', async () => {
|
|
86
|
+
vi.spyOn(globalThis, 'fetch').mockResolvedValue(makeErrorResponse(500))
|
|
87
|
+
|
|
88
|
+
await expect(executeWebhook(baseAction, baseContext, {}))
|
|
89
|
+
.rejects.toThrow('500')
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('interpolates body_template placeholders before sending', async () => {
|
|
93
|
+
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(makeOkResponse())
|
|
94
|
+
|
|
95
|
+
const action = { ...baseAction, body_template: '{"event":"{{trigger_event}}","id":"{{id}}"}' }
|
|
96
|
+
const ctx = makeContext({ id: 'abc-123' }, 'update')
|
|
97
|
+
|
|
98
|
+
await executeWebhook(action, ctx, {})
|
|
99
|
+
|
|
100
|
+
const [, init] = fetchSpy.mock.calls[0]
|
|
101
|
+
const body = init?.body as string
|
|
102
|
+
expect(body).toContain('abc-123')
|
|
103
|
+
expect(body).toContain('update')
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('recalculates HMAC and matches X-BeechCMS-Signature header', async () => {
|
|
107
|
+
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(makeOkResponse())
|
|
108
|
+
const secret = 'verify-secret'
|
|
109
|
+
|
|
110
|
+
await executeWebhook(baseAction, baseContext, { WEBHOOK_SECRET: secret })
|
|
111
|
+
|
|
112
|
+
const [, init] = fetchSpy.mock.calls[0]
|
|
113
|
+
const headers = init?.headers as Record<string, string>
|
|
114
|
+
const sentBody = init?.body as string
|
|
115
|
+
const sentSig = headers['X-BeechCMS-Signature']
|
|
116
|
+
|
|
117
|
+
const encoder = new TextEncoder()
|
|
118
|
+
const key = await crypto.subtle.importKey(
|
|
119
|
+
'raw',
|
|
120
|
+
encoder.encode(secret),
|
|
121
|
+
{ name: 'HMAC', hash: 'SHA-256' },
|
|
122
|
+
false,
|
|
123
|
+
['sign'],
|
|
124
|
+
)
|
|
125
|
+
const sig = await crypto.subtle.sign('HMAC', key, encoder.encode(sentBody))
|
|
126
|
+
const expected = 'sha256=' + Array.from(new Uint8Array(sig))
|
|
127
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
128
|
+
.join('')
|
|
129
|
+
|
|
130
|
+
expect(sentSig).toBe(expected)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('throws timeout error when fetch takes longer than 8 seconds', async () => {
|
|
134
|
+
vi.spyOn(globalThis, 'fetch').mockImplementation(() => {
|
|
135
|
+
const err = new DOMException('The operation was aborted.', 'TimeoutError')
|
|
136
|
+
return Promise.reject(err)
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
await expect(executeWebhook(baseAction, baseContext, {}))
|
|
140
|
+
.rejects.toThrow('timed out after 8000ms')
|
|
141
|
+
})
|
|
142
|
+
})
|