@beechcms/api 0.4.3 → 0.6.0-preview.1
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/Searching.svg +1 -0
- package/assets/dashboard/assets/index-BSjk0Sx9.js +634 -0
- package/assets/dashboard/assets/index-kRCYVN2B.css +1 -0
- package/assets/dashboard/index.html +2 -2
- package/assets/dashboard/noResult.svg +43 -0
- package/assets/dashboard/working.svg +1 -0
- package/migrations/0000_v040_base.sql +27 -0
- package/migrations/0030_test_seeds.sql +125 -0
- package/package.json +14 -9
- package/src/auth/{in-memory-hash-provider.ts → __fixtures__/in-memory-hash-provider.ts} +4 -0
- package/src/auth/{static-token-service.ts → __fixtures__/static-token-service.ts} +4 -0
- package/src/auth/bcrypt-hash-provider.ts +6 -2
- package/src/auth/constants.ts +4 -0
- package/src/auth/generate-refresh-token.test.ts +8 -4
- package/src/auth/hash-provider.test.ts +14 -5
- package/src/auth/jose-token-service.ts +7 -0
- package/src/auth/jwt-claims-passthrough.test.ts +87 -0
- package/src/auth/login.test.ts +9 -1
- package/src/auth/login.ts +5 -1
- package/src/auth/refresh.ts +7 -1
- package/src/auth/token-service.test.ts +9 -1
- package/src/factory.ts +37 -16
- package/src/features/automations/__tests__/action-executors.test.ts +59 -72
- package/src/features/automations/__tests__/automation-runner.test.ts +4 -0
- package/src/features/automations/__tests__/automation-runner.utils.test.ts +4 -0
- package/src/features/automations/__tests__/automations.handler.test.ts +7 -3
- package/src/features/automations/__tests__/automations.repository.test.ts +4 -0
- package/src/features/automations/__tests__/automations.schema.test.ts +92 -2
- package/src/features/automations/__tests__/context-resolver.test.ts +4 -0
- package/src/features/automations/__tests__/cron-runner.test.ts +11 -7
- package/src/features/automations/__tests__/cron-runner.utils.test.ts +4 -0
- package/src/features/automations/__tests__/set-variable.executor.test.ts +77 -0
- package/src/features/automations/__tests__/template-grammar.test.ts +4 -0
- package/src/features/automations/__tests__/webhook.executor.test.ts +142 -0
- package/src/features/automations/__tests__/when-evaluator.test.ts +4 -0
- package/src/features/automations/__tests__/when-pushdown.test.ts +5 -1
- package/src/features/automations/action-executors/create-entry.executor.ts +4 -0
- package/src/features/automations/action-executors/edit-field.executor.ts +4 -0
- package/src/features/automations/action-executors/index.ts +5 -1
- package/src/features/automations/action-executors/send-mail.executor.ts +13 -2
- package/src/features/automations/action-executors/set-variable.executor.ts +15 -2
- package/src/features/automations/action-executors/webhook.executor.ts +55 -13
- package/src/features/automations/automation-runner.ts +4 -0
- package/src/features/automations/automation-runner.utils.ts +4 -0
- package/src/features/automations/automations.handler.ts +4 -0
- package/src/features/automations/automations.schema.ts +19 -2
- package/src/features/automations/context-resolver.ts +4 -0
- package/src/features/automations/cron-runner.ts +4 -0
- package/src/features/automations/cron-runner.utils.ts +4 -0
- package/src/features/automations/filter-translation.ts +4 -0
- package/src/features/automations/index.ts +4 -0
- package/src/features/automations/template-grammar.ts +4 -0
- package/src/features/automations/var-access-resolver.ts +4 -0
- package/src/features/automations/when-evaluator.ts +4 -0
- package/src/features/automations/when-pushdown.ts +4 -0
- package/src/features/backrefs/__tests__/backrefs.handler.test.ts +359 -0
- package/src/features/backrefs/backrefs.handler.ts +168 -0
- package/src/features/backrefs/d1-backref.repository.ts +84 -0
- package/src/features/backrefs/index.ts +5 -0
- package/src/features/content/constants.ts +6 -0
- package/src/features/content/handlers/bulk.handler.ts +185 -0
- package/src/features/content/handlers/create.ts +19 -55
- package/src/features/content/handlers/delete.ts +12 -37
- package/src/features/content/handlers/facets.ts +4 -0
- package/src/features/content/handlers/get.ts +4 -0
- package/src/features/content/handlers/helpers.test.ts +180 -0
- package/src/features/content/handlers/helpers.ts +93 -0
- package/src/features/content/handlers/list.ts +105 -19
- package/src/features/content/handlers/update.ts +19 -64
- package/src/features/content/index.ts +6 -0
- package/src/features/draft/draft.handler.ts +33 -8
- package/src/features/draft/draft.middleware.ts +4 -0
- package/src/features/draft/index.ts +4 -0
- package/src/features/email/email.provider.ts +4 -0
- package/src/features/email/email.service.ts +50 -45
- package/src/features/email/email.types.ts +12 -1
- package/src/features/email/index.ts +4 -0
- package/src/features/email/providers/resend.ts +4 -0
- package/src/features/email/providers/smtp.ts +55 -0
- package/src/features/email/templates/automation-mail.ts +4 -0
- package/src/features/email/templates/password-changed.ts +4 -0
- package/src/features/email/templates/password-reset.ts +4 -0
- package/src/features/email/templates/shell.ts +4 -0
- package/src/features/notifications/index.ts +4 -0
- package/src/features/notifications/notifications.handler.ts +4 -0
- package/src/features/password-reset/index.ts +5 -1
- package/src/features/password-reset/request.ts +12 -2
- package/src/features/password-reset/reset.ts +12 -2
- package/src/features/rotate-field/index.ts +4 -0
- package/src/features/rotate-field/rotate-field.handler.ts +4 -0
- package/src/features/rotate-field/rotate-field.schema.ts +4 -0
- package/src/features/schema/schema.handler.ts +118 -3
- package/src/features/seeds/index.ts +5 -0
- package/src/features/seeds/seeds.handler.test.ts +632 -0
- package/src/features/seeds/seeds.handler.ts +693 -0
- package/src/features/settings/__tests__/settings.handler.test.ts +555 -0
- package/src/features/settings/settings.handler.ts +98 -8
- package/src/features/setup/index.ts +124 -11
- package/src/features/stats/index.ts +4 -0
- package/src/features/stats/stats.handler.ts +11 -21
- package/src/features/webhooks/index.ts +63 -0
- package/src/index.ts +28 -19
- package/src/media-utils.ts +4 -0
- package/src/middleware/auth-providers.middleware.ts +13 -0
- package/src/middleware/observability.middleware.ts +21 -3
- package/src/middleware/rate-limit.middleware.ts +4 -0
- package/src/middleware/repository.middleware.ts +25 -2
- package/src/middleware/seed-registry.middleware.test.ts +97 -0
- package/src/middleware/seed-registry.middleware.ts +21 -0
- package/src/middleware/storage.middleware.ts +4 -0
- package/src/middleware.ts +5 -7
- package/src/public/access-policy.ts +4 -0
- package/src/public/api-key-middleware.ts +4 -0
- package/src/public/cache-utils.ts +38 -34
- package/src/public/entry-projection.ts +46 -42
- package/src/public/idempotency.ts +23 -19
- package/src/public/index.ts +4 -0
- package/src/public/problem-details.ts +71 -0
- package/src/public/public-add.ts +4 -0
- package/src/public/public-edit.ts +4 -0
- package/src/public/public-errors.ts +4 -0
- package/src/public/public-read.ts +4 -0
- package/src/public/public-routes.ts +4 -0
- package/src/public/query-builder.test.ts +4 -0
- package/src/public/query-builder.ts +4 -0
- package/src/public/rate-limit-middleware.ts +4 -0
- package/src/public/read-list.ts +54 -50
- package/src/public/read-single.ts +48 -44
- package/src/public/response-builder.ts +4 -0
- package/src/public/sanitize.ts +4 -0
- package/src/public/slug-utils.ts +4 -0
- package/src/rate-limit/cloudflare-rate-limiter.test.ts +4 -0
- package/src/rate-limit/cloudflare-rate-limiter.ts +4 -0
- package/src/rate-limit/in-memory-rate-limiter.test.ts +4 -0
- package/src/rate-limit/in-memory-rate-limiter.ts +4 -0
- package/src/rate-limit/no-op-rate-limiter.test.ts +4 -0
- package/src/rate-limit/no-op-rate-limiter.ts +4 -0
- package/src/search-utils.test.ts +4 -0
- package/src/search-utils.ts +5 -1
- package/src/search.ts +5 -1
- package/src/shared/apply-policies.test.ts +4 -0
- package/src/shared/apply-policies.ts +4 -0
- package/src/shared/automations.repository.d1.ts +4 -0
- package/src/shared/background-notification-service.test.ts +4 -0
- package/src/shared/background-notification-service.ts +4 -0
- package/src/shared/base.repository.d1.ts +4 -0
- package/src/shared/content-utils.test.ts +4 -0
- package/src/shared/content-utils.ts +4 -0
- package/src/shared/content.repository.d1.test.ts +147 -1
- package/src/shared/content.repository.d1.ts +535 -66
- package/src/shared/d1-activity-log.repository.test.ts +4 -0
- package/src/shared/d1-activity-log.repository.ts +4 -0
- package/src/shared/d1-activity-logger.test.ts +4 -0
- package/src/shared/d1-activity-logger.ts +4 -0
- package/src/shared/d1-analytics.repository.test.ts +4 -0
- package/src/shared/d1-analytics.repository.ts +4 -0
- package/src/shared/d1-content-scan.repository.test.ts +9 -5
- package/src/shared/d1-content-scan.repository.ts +15 -7
- package/src/shared/d1-notification.repository.test.ts +4 -0
- package/src/shared/d1-notification.repository.ts +4 -0
- package/src/shared/d1-password-reset-token.repository.test.ts +4 -0
- package/src/shared/d1-password-reset-token.repository.ts +4 -0
- package/src/shared/d1-search.repository.test.ts +4 -0
- package/src/shared/d1-search.repository.ts +4 -0
- package/src/shared/d1-session.repository.test.ts +4 -0
- package/src/shared/d1-session.repository.ts +4 -0
- package/src/shared/d1-setup-checklist.repository.ts +36 -0
- package/src/shared/d1-user.repository.test.ts +8 -4
- package/src/shared/d1-user.repository.ts +15 -5
- package/src/shared/d1-widget.repository.test.ts +4 -0
- package/src/shared/d1-widget.repository.ts +4 -0
- package/src/shared/demo-data-sql.ts +54 -0
- package/src/shared/demo-data.repository.d1.ts +20 -0
- package/src/shared/execution-context-scheduler.ts +4 -0
- package/src/shared/fixed-clock.ts +4 -0
- package/src/shared/fts-sync.ts +4 -0
- package/src/shared/idempotency.repository.d1.test.ts +4 -0
- package/src/shared/idempotency.repository.d1.ts +4 -0
- package/src/shared/in-memory-activity-logger.ts +4 -0
- package/src/shared/in-memory-notification-service.ts +4 -0
- package/src/shared/in-memory-seed.repository.ts +51 -0
- package/src/shared/media.repository.d1.test.ts +4 -0
- package/src/shared/media.repository.d1.ts +4 -0
- package/src/shared/qstash-notification-service.test.ts +67 -0
- package/src/shared/qstash-notification-service.ts +55 -0
- package/src/shared/query-utils.ts +4 -0
- package/src/shared/request-utils.ts +4 -0
- package/src/shared/schema-mutator.d1.ts +64 -0
- package/src/shared/seed-layout.repository.d1.test.ts +114 -0
- package/src/shared/seed-layout.repository.d1.ts +62 -0
- package/src/shared/seed-registry-cache.test.ts +68 -0
- package/src/shared/seed-registry-cache.ts +49 -0
- package/src/shared/seed.repository.d1.test.ts +143 -0
- package/src/shared/seed.repository.d1.ts +98 -0
- package/src/shared/sequential-id-generator.ts +11 -0
- package/src/shared/site-settings.repository.d1.ts +53 -0
- package/src/shared/storage/factory.ts +16 -15
- package/src/shared/storage/s3-bucket.ts +34 -2
- package/src/shared/storage-utils.ts +4 -0
- package/src/shared/system-stats.repository.d1.test.ts +4 -0
- package/src/shared/system-stats.repository.d1.ts +5 -1
- package/src/types.ts +23 -3
- package/src/upload.ts +134 -123
- package/src/widget.ts +6 -2
- package/assets/dashboard/assets/index-BKWnlvnV.css +0 -1
- package/assets/dashboard/assets/index-BMkd1Irh.js +0 -629
- package/src/shared/storage/r2-binding-bucket.ts +0 -81
|
@@ -1,3 +1,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
|
+
|
|
1
5
|
import { describe, it, expect } from 'vitest'
|
|
2
6
|
import { parseTemplateKey } from '../template-grammar'
|
|
3
7
|
import type { ParsedKey, VarStep, InlineCondition } from '../template-grammar'
|
|
@@ -0,0 +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,3 +1,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
|
+
|
|
1
5
|
import { describe, it, expect } from 'vitest'
|
|
2
6
|
import type { WhenNode } from '@beechcms/core'
|
|
3
7
|
import { evaluateWhen } from '../when-evaluator'
|
|
@@ -1,3 +1,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
|
+
|
|
1
5
|
import { describe, it, expect } from 'vitest'
|
|
2
6
|
import type { WhenNode, Seed } from '@beechcms/core'
|
|
3
7
|
import { extractPushdownFilters } from '../when-pushdown'
|
|
@@ -11,7 +15,7 @@ function makeSeed(aliases: string[] = []): Seed {
|
|
|
11
15
|
slug: 'orders',
|
|
12
16
|
label: 'Orders',
|
|
13
17
|
displayNameAlias: aliases[0] ?? 'id',
|
|
14
|
-
branches: aliases.map((alias) => ({ alias, label: alias, type: 'text' as const })),
|
|
18
|
+
branches: aliases.map((alias, i) => ({ id: `br_0${i + 1}`, alias, label: alias, type: 'text' as const })),
|
|
15
19
|
}
|
|
16
20
|
}
|
|
17
21
|
|
|
@@ -1,3 +1,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
|
+
|
|
1
5
|
import type { AutomationAction, ContentRepository, Seed, IIdGenerator } from '@beechcms/core'
|
|
2
6
|
|
|
3
7
|
type CreateEntryAction = Extract<AutomationAction, { type: 'create_entry' }>
|
|
@@ -1,3 +1,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
|
+
|
|
1
5
|
import type { AutomationAction, ContentRepository, Seed } from '@beechcms/core'
|
|
2
6
|
import type { ResolvedContext } from '../context-resolver'
|
|
3
7
|
import { interpolate } from '../automation-runner.utils'
|
|
@@ -1,3 +1,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
|
+
|
|
1
5
|
import type { AutomationAction, ContentRepository, Seed, IIdGenerator } from '@beechcms/core'
|
|
2
6
|
import type { ResolvedContext } from '../context-resolver'
|
|
3
7
|
import { executeWebhook } from './webhook.executor'
|
|
@@ -21,7 +25,7 @@ export interface ActionContext {
|
|
|
21
25
|
export async function executeAction(action: AutomationAction, ctx: ActionContext): Promise<void> {
|
|
22
26
|
switch (action.type) {
|
|
23
27
|
case 'set_variable': return executeSetVariable(action, ctx)
|
|
24
|
-
case 'webhook': return executeWebhook(action, ctx.context)
|
|
28
|
+
case 'webhook': return executeWebhook(action, ctx.context, ctx.env)
|
|
25
29
|
case 'send_mail': return executeSendMail(action, ctx.context, ctx.env)
|
|
26
30
|
case 'edit_field': return executeEditField(action, ctx.entry, ctx.context, ctx.repository, ctx.seed)
|
|
27
31
|
case 'create_entry': return executeCreateEntry(action, ctx.entry, ctx.repository, ctx.getSeed, ctx.idGenerator)
|
|
@@ -1,3 +1,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
|
+
|
|
1
5
|
import type { AutomationAction } from '@beechcms/core'
|
|
2
6
|
import type { ResolvedContext } from '../context-resolver'
|
|
3
7
|
import { sendAutomationMail } from '../../email'
|
|
@@ -21,19 +25,26 @@ export async function executeSendMail(
|
|
|
21
25
|
console.warn('[automations:send_mail] placeholders evaluated to default value due to missing concrete data:', missingFields)
|
|
22
26
|
}
|
|
23
27
|
|
|
28
|
+
const useSmtp = env.EMAIL_PROVIDER === 'smtp'
|
|
24
29
|
const apiKey = env.EMAIL_API_KEY ?? env.RESEND_API_KEY
|
|
25
|
-
if (!apiKey) {
|
|
30
|
+
if (!useSmtp && !apiKey) {
|
|
26
31
|
console.error('[automations:send_mail] Execution skipped: Email API key is missing from the environment configuration.')
|
|
27
32
|
return
|
|
28
33
|
}
|
|
29
34
|
|
|
35
|
+
const smtpBaseUrl = env.SMTP_HOST
|
|
36
|
+
? `http://${env.SMTP_HOST}:${env.SMTP_PORT ?? '8025'}`
|
|
37
|
+
: undefined
|
|
38
|
+
|
|
30
39
|
try {
|
|
31
40
|
await sendAutomationMail({
|
|
32
41
|
to,
|
|
33
42
|
subject,
|
|
34
43
|
body,
|
|
35
|
-
apiKey,
|
|
44
|
+
apiKey: apiKey ?? '',
|
|
36
45
|
from: env.EMAIL_FROM,
|
|
46
|
+
provider: env.EMAIL_PROVIDER as 'smtp' | 'resend' | undefined,
|
|
47
|
+
smtpBaseUrl,
|
|
37
48
|
})
|
|
38
49
|
} catch (error) {
|
|
39
50
|
console.error('[automations:send_mail] failed to send automation email via provider:', error)
|
|
@@ -1,3 +1,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
|
+
|
|
1
5
|
import type { AutomationAction, ContentRepository, Seed } from '@beechcms/core'
|
|
2
6
|
import type { ResolvedContext } from '../context-resolver'
|
|
3
7
|
import { conditionToFilterGroup } from '../filter-translation'
|
|
@@ -24,8 +28,17 @@ export async function executeSetVariable(
|
|
|
24
28
|
return
|
|
25
29
|
}
|
|
26
30
|
|
|
27
|
-
|
|
28
|
-
|
|
31
|
+
// ── legacy read normalisation ─────────────────────────────────────────────
|
|
32
|
+
let effectiveFixedId = action.fixed_id
|
|
33
|
+
if (effectiveFixedId === undefined && action.load_type === 'fruit') {
|
|
34
|
+
const idFilter = (action.filters ?? []).find(
|
|
35
|
+
(f) => f.field === 'id' && f.op === 'eq' && typeof f.value === 'string',
|
|
36
|
+
)
|
|
37
|
+
if (idFilter) effectiveFixedId = String(idFilter.value)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (effectiveFixedId !== undefined) {
|
|
41
|
+
const resolvedId = interpolate(effectiveFixedId, ctx.context)
|
|
29
42
|
const { items } = await ctx.repository.findMany(targetSeed, {
|
|
30
43
|
filters: [{ column: 'id', type: 'system', conditions: [{ op: 'eq', value: resolvedId }] }],
|
|
31
44
|
status: null,
|
|
@@ -1,25 +1,67 @@
|
|
|
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
|
+
|
|
1
5
|
import type { AutomationAction } from '@beechcms/core'
|
|
2
6
|
import type { ResolvedContext } from '../context-resolver'
|
|
3
7
|
import { interpolate } from '../automation-runner.utils'
|
|
4
8
|
|
|
5
9
|
type WebhookAction = Extract<AutomationAction, { type: 'webhook' }>
|
|
6
10
|
|
|
11
|
+
const WEBHOOK_TIMEOUT_MS = 8_000
|
|
12
|
+
|
|
13
|
+
let warnedNoSecret = false
|
|
14
|
+
|
|
15
|
+
async function signBody(body: string, secret: string): Promise<string> {
|
|
16
|
+
const encoder = new TextEncoder()
|
|
17
|
+
const key = await crypto.subtle.importKey(
|
|
18
|
+
'raw',
|
|
19
|
+
encoder.encode(secret),
|
|
20
|
+
{ name: 'HMAC', hash: 'SHA-256' },
|
|
21
|
+
false,
|
|
22
|
+
['sign'],
|
|
23
|
+
)
|
|
24
|
+
const sig = await crypto.subtle.sign('HMAC', key, encoder.encode(body))
|
|
25
|
+
return 'sha256=' + Array.from(new Uint8Array(sig))
|
|
26
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
27
|
+
.join('')
|
|
28
|
+
}
|
|
29
|
+
|
|
7
30
|
export async function executeWebhook(
|
|
8
31
|
action: WebhookAction,
|
|
9
32
|
context: ResolvedContext,
|
|
33
|
+
env: Record<string, string | undefined>,
|
|
10
34
|
): Promise<void> {
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
35
|
+
const body = interpolate(action.body_template ?? '{}', context)
|
|
36
|
+
|
|
37
|
+
const secret = env.WEBHOOK_SECRET
|
|
38
|
+
let signatureHeader: Record<string, string> = {}
|
|
39
|
+
if (secret) {
|
|
40
|
+
signatureHeader = { 'X-BeechCMS-Signature': await signBody(body, secret) }
|
|
41
|
+
} else if (!warnedNoSecret) {
|
|
42
|
+
warnedNoSecret = true
|
|
43
|
+
console.warn('[webhook] WEBHOOK_SECRET not set — outgoing webhooks are unsigned')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const response = await fetch(action.url, {
|
|
48
|
+
method: action.method ?? 'POST',
|
|
49
|
+
headers: {
|
|
50
|
+
'Content-Type': 'application/json',
|
|
51
|
+
...(action.headers ?? {}),
|
|
52
|
+
...signatureHeader,
|
|
53
|
+
},
|
|
54
|
+
body,
|
|
55
|
+
signal: AbortSignal.timeout(WEBHOOK_TIMEOUT_MS),
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
if (!response.ok) {
|
|
59
|
+
throw new Error(`Webhook ${action.url} responded ${response.status}`)
|
|
60
|
+
}
|
|
61
|
+
} catch (err) {
|
|
62
|
+
if (err instanceof Error && err.name === 'TimeoutError') {
|
|
63
|
+
throw new Error(`Webhook ${action.url} timed out after ${WEBHOOK_TIMEOUT_MS}ms`)
|
|
64
|
+
}
|
|
65
|
+
throw err
|
|
24
66
|
}
|
|
25
67
|
}
|
|
@@ -1,3 +1,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
|
+
|
|
1
5
|
import type { ResolvedContext } from './context-resolver'
|
|
2
6
|
import { parseTemplateKey } from './template-grammar'
|
|
3
7
|
|
|
@@ -1,3 +1,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
|
+
|
|
1
5
|
/// <reference types="@cloudflare/workers-types" />
|
|
2
6
|
import { Hono } from 'hono'
|
|
3
7
|
import type { Env, Variables } from '../../types'
|
|
@@ -1,6 +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
|
+
|
|
1
5
|
// Mirror: apps/dashboard/src/features/automations/schema/automation.schema.ts — keep structurally identical.
|
|
2
6
|
import { z } from 'zod'
|
|
3
7
|
import type { WhenNode } from '@beechcms/core'
|
|
8
|
+
import { isPrivateHost } from '@beechcms/core'
|
|
4
9
|
import { AUTOMATION_RESERVED_WORDS } from './template-grammar'
|
|
5
10
|
|
|
6
11
|
// ---------------------------------------------------------------------------
|
|
@@ -71,14 +76,26 @@ const setVariableActionSchema = z.object({
|
|
|
71
76
|
filters: z.array(setVariableFilterSchema).default([]),
|
|
72
77
|
order_by: z.string().optional(),
|
|
73
78
|
order: z.enum(['asc', 'desc']).optional(),
|
|
79
|
+
load_type: z.enum(['fruit', 'branch']).optional(), // accepted but ignored at runtime
|
|
74
80
|
})
|
|
75
81
|
|
|
76
82
|
const webhookActionSchema = z.object({
|
|
77
83
|
type: z.literal('webhook'),
|
|
78
|
-
url: z
|
|
84
|
+
url: z
|
|
85
|
+
.string()
|
|
86
|
+
.url()
|
|
87
|
+
.refine((u) => {
|
|
88
|
+
try { return new URL(u).protocol === 'https:' } catch { return false }
|
|
89
|
+
}, { message: 'automations.editor.errors.webhookHttpsRequired' })
|
|
90
|
+
.refine((u) => {
|
|
91
|
+
try { return !isPrivateHost(new URL(u).hostname) } catch { return false }
|
|
92
|
+
}, { message: 'automations.editor.errors.webhookPrivateHostBlocked' }),
|
|
79
93
|
method: z.enum(['POST', 'GET', 'PUT']).optional(),
|
|
80
94
|
headers: z.record(z.string(), z.string()).optional(),
|
|
81
|
-
body_template: z.
|
|
95
|
+
body_template: z.preprocess(
|
|
96
|
+
(v) => (v == null ? '' : v),
|
|
97
|
+
z.string().min(1, 'automations.editor.errors.bodyRequired'),
|
|
98
|
+
),
|
|
82
99
|
})
|
|
83
100
|
|
|
84
101
|
const sendMailActionSchema = z.object({
|
|
@@ -1,3 +1,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
|
+
|
|
1
5
|
import type { Automation } from '@beechcms/core'
|
|
2
6
|
import type { AutomationContextSelector, ParsedKey } from './template-grammar'
|
|
3
7
|
import { resolvePath } from './automation-runner.utils'
|
|
@@ -1,3 +1,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
|
+
|
|
1
5
|
export function cronMatches(expression: string | null, scheduledTime: number): boolean {
|
|
2
6
|
if (!expression) return false
|
|
3
7
|
const parts = expression.trim().split(/\s+/)
|
|
@@ -1,3 +1,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
|
+
|
|
1
5
|
/**
|
|
2
6
|
* Shared helper: converts a TriggerCondition into a FilterGroup accepted by
|
|
3
7
|
* ContentRepository.findMany. Used by cron-runner and context-resolver.
|
|
@@ -1,3 +1,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
|
+
|
|
1
5
|
export { AutomationRunner } from './automation-runner'
|
|
2
6
|
export type { AutomationRunnerDeps } from './automation-runner'
|
|
3
7
|
export { runCronAutomations } from './cron-runner'
|
|
@@ -1,3 +1,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
|
+
|
|
1
5
|
import type { Seed } from '@beechcms/core'
|
|
2
6
|
import type { ParsedKey, InlineCondition } from './template-grammar'
|
|
3
7
|
import { materializeCollection } from './action-executors/set-variable.executor'
|
|
@@ -1,3 +1,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
|
+
|
|
1
5
|
import type { WhenNode, WhenPredicate, WhenGroup, WhenOperand } from '@beechcms/core'
|
|
2
6
|
import type { ResolvedContext } from './context-resolver'
|
|
3
7
|
import { parseTemplateKey } from './template-grammar'
|
|
@@ -1,3 +1,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
|
+
|
|
1
5
|
import type { WhenNode, WhenPredicate, TriggerCondition, FilterGroup, Seed } from '@beechcms/core'
|
|
2
6
|
import { conditionToFilterGroup } from './filter-translation'
|
|
3
7
|
|