@ekrist1/vulse 0.1.6-alpha.3 → 0.1.7-alpha.4
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/dist/cli/migrate.d.ts.map +1 -1
- package/dist/cli/migrate.js +3 -4
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +11 -10
- package/dist/core/blueprints/compile.d.ts +1 -1
- package/dist/core/blueprints/compile.d.ts.map +1 -1
- package/dist/core/blueprints/compile.js +3 -3
- package/dist/core/blueprints/mutations.js +2 -2
- package/dist/core/forms/rate-limit.d.ts +1 -1
- package/dist/core/forms/rate-limit.d.ts.map +1 -1
- package/dist/core/forms/rate-limit.js +3 -3
- package/dist/core/forms/unique.d.ts +1 -1
- package/dist/core/forms/unique.d.ts.map +1 -1
- package/dist/core/forms/unique.js +4 -4
- package/dist/core/globals/compile.d.ts +1 -1
- package/dist/core/globals/compile.d.ts.map +1 -1
- package/dist/core/globals/compile.js +2 -2
- package/dist/core/globals/definition.d.ts +1 -1
- package/dist/core/globals/definition.d.ts.map +1 -1
- package/dist/core/globals/definition.js +3 -3
- package/dist/core/repos/globals.js +3 -3
- package/dist/core/sha256.d.ts +3 -0
- package/dist/core/sha256.d.ts.map +1 -0
- package/dist/core/sha256.js +9 -0
- package/dist/integration/index.js +1 -1
- package/dist/integration/install-hook.d.ts.map +1 -1
- package/dist/integration/install-hook.js +6 -4
- package/dist/integration/wrangler-config.d.ts +12 -0
- package/dist/integration/wrangler-config.d.ts.map +1 -0
- package/dist/integration/wrangler-config.js +97 -0
- package/dist/integration/wrangler-patch.d.ts +1 -0
- package/dist/integration/wrangler-patch.d.ts.map +1 -1
- package/dist/integration/wrangler-patch.js +2 -1
- package/dist/server/routes/form-submit.js +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +11 -3
- package/src/admin/assets/logo-mark.svg +5 -0
- package/src/admin/client/active-locale.ts +17 -0
- package/src/admin/client/api.ts +21 -0
- package/src/admin/client/form-from-zod.ts +7 -0
- package/src/admin/client/live-preview-enabled.ts +5 -0
- package/src/admin/components/AdminShell.astro +45 -0
- package/src/admin/components/AuthSettings.vue +60 -0
- package/src/admin/components/BlockEditor.vue +53 -0
- package/src/admin/components/BlueprintEditor.vue +1783 -0
- package/src/admin/components/CollectionKindIcon.vue +26 -0
- package/src/admin/components/CollectionTree.vue +220 -0
- package/src/admin/components/EntryEditorWithPreview.vue +130 -0
- package/src/admin/components/EntryForm.vue +411 -0
- package/src/admin/components/EntryList.vue +121 -0
- package/src/admin/components/EntryStatusBadge.vue +24 -0
- package/src/admin/components/FormEditor.vue +233 -0
- package/src/admin/components/FormList.vue +54 -0
- package/src/admin/components/GlobalSetEditor.vue +272 -0
- package/src/admin/components/GlobalSetList.vue +55 -0
- package/src/admin/components/LivePreviewPanel.vue +171 -0
- package/src/admin/components/LoginForm.vue +53 -0
- package/src/admin/components/MediaLibrary.vue +106 -0
- package/src/admin/components/MediaPicker.vue +49 -0
- package/src/admin/components/RevisionDiff.vue +11 -0
- package/src/admin/components/RevisionList.vue +134 -0
- package/src/admin/components/SeoFields.vue +113 -0
- package/src/admin/components/SetEditor.vue +137 -0
- package/src/admin/components/SetList.vue +32 -0
- package/src/admin/components/SettingsForm.vue +189 -0
- package/src/admin/components/SideNav.vue +152 -0
- package/src/admin/components/SubmissionDetail.vue +45 -0
- package/src/admin/components/SubmissionList.vue +89 -0
- package/src/admin/components/ToastHost.vue +33 -0
- package/src/admin/components/TreeRow.vue +163 -0
- package/src/admin/components/UserEditor.vue +186 -0
- package/src/admin/components/UserList.vue +46 -0
- package/src/admin/components/blocks/BlockItem.vue +32 -0
- package/src/admin/components/blocks/BlockToolbar.vue +12 -0
- package/src/admin/components/blocks/edit/CodeEdit.vue +18 -0
- package/src/admin/components/blocks/edit/EmbedEdit.vue +14 -0
- package/src/admin/components/blocks/edit/HeadingEdit.vue +19 -0
- package/src/admin/components/blocks/edit/ImageEdit.vue +40 -0
- package/src/admin/components/blocks/edit/ListEdit.vue +36 -0
- package/src/admin/components/blocks/edit/ParagraphEdit.vue +14 -0
- package/src/admin/components/blocks/edit/QuoteEdit.vue +18 -0
- package/src/admin/components/fields/BlocksField.vue +123 -0
- package/src/admin/components/fields/BlocksSetsPicker.vue +59 -0
- package/src/admin/components/fields/BoolField.vue +10 -0
- package/src/admin/components/fields/DateField.vue +22 -0
- package/src/admin/components/fields/EntriesField.vue +153 -0
- package/src/admin/components/fields/EntryField.vue +138 -0
- package/src/admin/components/fields/EnumField.vue +81 -0
- package/src/admin/components/fields/FieldRenderer.vue +87 -0
- package/src/admin/components/fields/GridField.vue +173 -0
- package/src/admin/components/fields/LinkField.vue +219 -0
- package/src/admin/components/fields/MediaField.vue +69 -0
- package/src/admin/components/fields/NumberField.vue +12 -0
- package/src/admin/components/fields/ObjectField.vue +18 -0
- package/src/admin/components/fields/RefField.vue +170 -0
- package/src/admin/components/fields/RepeaterField.vue +27 -0
- package/src/admin/components/fields/ReplicatorField.vue +121 -0
- package/src/admin/components/fields/TextField.vue +11 -0
- package/src/admin/components/fields/TextareaField.vue +11 -0
- package/src/admin/components/fields/VulseAccordionGroupNodeView.vue +82 -0
- package/src/admin/components/fields/VulseAccordionNodeView.vue +128 -0
- package/src/admin/components/fields/VulseCalloutNodeView.vue +81 -0
- package/src/admin/components/fields/VulseIframeNodeView.vue +112 -0
- package/src/admin/components/fields/VulseSetNodeView.vue +68 -0
- package/src/admin/components/fields/VulseVideoNodeView.vue +104 -0
- package/src/admin/components/fields/blocks-editor-extensions.ts +26 -0
- package/src/admin/components/fields/emoji-extension.ts +54 -0
- package/src/admin/components/fields/link-extension.ts +48 -0
- package/src/admin/components/fields/set-node-utils.ts +115 -0
- package/src/admin/components/fields/url-utils.ts +85 -0
- package/src/admin/components/fields/vulse-accordion-extension.ts +64 -0
- package/src/admin/components/fields/vulse-accordion-group-extension.ts +49 -0
- package/src/admin/components/fields/vulse-callout-extension.ts +53 -0
- package/src/admin/components/fields/vulse-iframe-extension.ts +96 -0
- package/src/admin/components/fields/vulse-set-extension.ts +66 -0
- package/src/admin/components/fields/vulse-video-extension.ts +65 -0
- package/src/admin/composables/toast.ts +35 -0
- package/src/admin/composables/useEntrySearch.ts +112 -0
- package/src/admin/composables/useSets.ts +31 -0
- package/src/admin/pages/collections/[name]/[id]/revisions.astro +27 -0
- package/src/admin/pages/collections/[name]/[id].astro +90 -0
- package/src/admin/pages/collections/[name]/index.astro +31 -0
- package/src/admin/pages/collections/[name]/new.astro +38 -0
- package/src/admin/pages/forms/[handle]/submissions/[id].astro +9 -0
- package/src/admin/pages/forms/[handle]/submissions/index.astro +9 -0
- package/src/admin/pages/forms/[handle].astro +9 -0
- package/src/admin/pages/forms/index.astro +7 -0
- package/src/admin/pages/forms/new.astro +7 -0
- package/src/admin/pages/index.astro +36 -0
- package/src/admin/pages/login.astro +14 -0
- package/src/admin/pages/media.astro +8 -0
- package/src/admin/pages/schema/[handle].astro +10 -0
- package/src/admin/pages/schema/new.astro +9 -0
- package/src/admin/pages/settings/auth.astro +9 -0
- package/src/admin/pages/settings/globals/[handle].astro +9 -0
- package/src/admin/pages/settings/globals/index.astro +7 -0
- package/src/admin/pages/settings/globals/new.astro +7 -0
- package/src/admin/pages/settings/index.astro +8 -0
- package/src/admin/pages/settings/sets/[handle].astro +9 -0
- package/src/admin/pages/settings/sets/index.astro +7 -0
- package/src/admin/pages/settings/sets/new.astro +7 -0
- package/src/admin/pages/users/[id].astro +10 -0
- package/src/admin/pages/users/index.astro +8 -0
- package/src/admin/styles/admin.css +166 -0
- package/src/core/access.ts +9 -0
- package/src/core/blocks/schema.ts +66 -0
- package/src/core/blueprints/code-to-definition.ts +156 -0
- package/src/core/blueprints/compile.ts +176 -0
- package/src/core/blueprints/define.ts +12 -0
- package/src/core/blueprints/definition.ts +185 -0
- package/src/core/blueprints/load.ts +144 -0
- package/src/core/blueprints/mutations.ts +236 -0
- package/src/core/blueprints/preview-path.ts +33 -0
- package/src/core/blueprints/reflect-fields.ts +305 -0
- package/src/core/blueprints/registry.ts +14 -0
- package/src/core/blueprints/seed.ts +20 -0
- package/src/core/blueprints/select-helpers.ts +30 -0
- package/src/core/blueprints/seo.ts +180 -0
- package/src/core/blueprints/types.ts +59 -0
- package/src/core/blueprints/zod-helpers.ts +86 -0
- package/src/core/db.ts +11 -0
- package/src/core/errors.ts +34 -0
- package/src/core/forms/compile.ts +84 -0
- package/src/core/forms/definition.ts +102 -0
- package/src/core/forms/rate-limit.ts +52 -0
- package/src/core/forms/unique.ts +38 -0
- package/src/core/globals/compile.ts +35 -0
- package/src/core/globals/definition.ts +27 -0
- package/src/core/locales.ts +45 -0
- package/src/core/migrations.ts +48 -0
- package/src/core/parse-content.ts +85 -0
- package/src/core/plugins/definition.ts +150 -0
- package/src/core/preview-content.ts +21 -0
- package/src/core/repos/entries.ts +504 -0
- package/src/core/repos/forms.ts +270 -0
- package/src/core/repos/globals.ts +179 -0
- package/src/core/repos/media.ts +106 -0
- package/src/core/repos/preview-sessions.ts +108 -0
- package/src/core/repos/revisions.ts +60 -0
- package/src/core/repos/settings.ts +23 -0
- package/src/core/schema.ts +244 -0
- package/src/core/sets/compile.ts +12 -0
- package/src/core/sets/definition.ts +10 -0
- package/src/core/sets/service.ts +82 -0
- package/src/core/sets/validate-tree.ts +57 -0
- package/src/core/sha256.ts +10 -0
- package/src/core/slug.ts +30 -0
- package/src/scaffold/collection-write.ts +83 -0
- package/src/scaffold/collection.ts +277 -0
- package/src/server/assets/live-preview-bridge.content.ts +2 -0
- package/src/server/assets/live-preview-bridge.js +535 -0
- package/src/server/better-auth.ts +82 -0
- package/src/server/cf-images.ts +34 -0
- package/src/server/cron.ts +37 -0
- package/src/server/email.ts +17 -0
- package/src/server/endpoints/api-auth.ts +10 -0
- package/src/server/endpoints/api-vulse-blueprints.ts +23 -0
- package/src/server/endpoints/api-vulse-entries-locales.ts +12 -0
- package/src/server/endpoints/api-vulse-entries-move.ts +7 -0
- package/src/server/endpoints/api-vulse-entries-publish.ts +7 -0
- package/src/server/endpoints/api-vulse-entries-tree.ts +7 -0
- package/src/server/endpoints/api-vulse-entries.ts +23 -0
- package/src/server/endpoints/api-vulse-form-handle.ts +30 -0
- package/src/server/endpoints/api-vulse-form-public.ts +7 -0
- package/src/server/endpoints/api-vulse-form-submit.ts +7 -0
- package/src/server/endpoints/api-vulse-form-upload.ts +7 -0
- package/src/server/endpoints/api-vulse-forms.ts +12 -0
- package/src/server/endpoints/api-vulse-globals-handle.ts +20 -0
- package/src/server/endpoints/api-vulse-globals-public-handle.ts +7 -0
- package/src/server/endpoints/api-vulse-globals-public.ts +7 -0
- package/src/server/endpoints/api-vulse-globals-value.ts +8 -0
- package/src/server/endpoints/api-vulse-globals.ts +12 -0
- package/src/server/endpoints/api-vulse-media-file.ts +7 -0
- package/src/server/endpoints/api-vulse-media-id.ts +12 -0
- package/src/server/endpoints/api-vulse-media.ts +12 -0
- package/src/server/endpoints/api-vulse-preview-bridge.ts +11 -0
- package/src/server/endpoints/api-vulse-preview-sessions-id.ts +10 -0
- package/src/server/endpoints/api-vulse-preview-sessions.ts +7 -0
- package/src/server/endpoints/api-vulse-preview-start.ts +7 -0
- package/src/server/endpoints/api-vulse-preview-stop.ts +7 -0
- package/src/server/endpoints/api-vulse-revisions-restore.ts +7 -0
- package/src/server/endpoints/api-vulse-revisions.ts +7 -0
- package/src/server/endpoints/api-vulse-search.ts +7 -0
- package/src/server/endpoints/api-vulse-sets.ts +23 -0
- package/src/server/endpoints/api-vulse-settings.ts +12 -0
- package/src/server/endpoints/api-vulse-users-id.ts +12 -0
- package/src/server/endpoints/api-vulse-users-reset-password.ts +7 -0
- package/src/server/endpoints/api-vulse-users-role.ts +9 -0
- package/src/server/endpoints/api-vulse-users.ts +7 -0
- package/src/server/endpoints/with-runtime.ts +11 -0
- package/src/server/env.ts +23 -0
- package/src/server/envelope.ts +21 -0
- package/src/server/forms/email.ts +11 -0
- package/src/server/forms/process-submission.ts +95 -0
- package/src/server/forms/queue.ts +25 -0
- package/src/server/forms/templates.ts +24 -0
- package/src/server/forms/webhook.ts +19 -0
- package/src/server/handler.ts +66 -0
- package/src/server/image-probe.ts +35 -0
- package/src/server/loader.ts +54 -0
- package/src/server/plugins.ts +214 -0
- package/src/server/preview.ts +25 -0
- package/src/server/r2.ts +13 -0
- package/src/server/routes/blueprints.ts +62 -0
- package/src/server/routes/entries.ts +255 -0
- package/src/server/routes/form-submit.ts +168 -0
- package/src/server/routes/form-upload.ts +100 -0
- package/src/server/routes/forms.ts +88 -0
- package/src/server/routes/globals-public.ts +30 -0
- package/src/server/routes/globals.ts +93 -0
- package/src/server/routes/media.ts +145 -0
- package/src/server/routes/preview-sessions.ts +76 -0
- package/src/server/routes/preview.ts +36 -0
- package/src/server/routes/revisions.ts +29 -0
- package/src/server/routes/search.ts +31 -0
- package/src/server/routes/sets.ts +40 -0
- package/src/server/routes/settings.ts +24 -0
- package/src/server/routes/users.ts +127 -0
- package/src/server/runtime.ts +99 -0
- package/src/server/sdk/collections.ts +98 -0
- package/src/server/sdk/index.ts +25 -0
- package/src/server/sdk/media.ts +11 -0
- package/src/server/sdk/search.ts +90 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { FormDefinition } from '../../core/forms/definition.js'
|
|
2
|
+
import type { SubmissionRow } from '../../core/repos/forms.js'
|
|
3
|
+
|
|
4
|
+
export interface TemplateContext {
|
|
5
|
+
form: FormDefinition
|
|
6
|
+
submission: SubmissionRow
|
|
7
|
+
payload: Record<string, unknown>
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function renderTemplate(template: string, ctx: TemplateContext): string {
|
|
11
|
+
const tokens: Record<string, string> = {
|
|
12
|
+
'form.label': ctx.form.label,
|
|
13
|
+
'submission.id': ctx.submission.id,
|
|
14
|
+
'submission.created_at': ctx.submission.createdAt.toISOString(),
|
|
15
|
+
}
|
|
16
|
+
for (const [key, value] of Object.entries(ctx.payload)) {
|
|
17
|
+
tokens[key] = value === null || value === undefined ? '' : String(value)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return template.replace(/\{\{([^}]+)\}\}/g, (_, key: string) => {
|
|
21
|
+
const trimmed = key.trim()
|
|
22
|
+
return tokens[trimmed] ?? ''
|
|
23
|
+
})
|
|
24
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export async function sendFormWebhook(
|
|
2
|
+
url: string,
|
|
3
|
+
payload: unknown,
|
|
4
|
+
headers: Record<string, string> = {},
|
|
5
|
+
): Promise<void> {
|
|
6
|
+
const controller = new AbortController()
|
|
7
|
+
const timeout = setTimeout(() => controller.abort(), 5000)
|
|
8
|
+
try {
|
|
9
|
+
const res = await fetch(url, {
|
|
10
|
+
method: 'POST',
|
|
11
|
+
headers: { 'content-type': 'application/json', ...headers },
|
|
12
|
+
body: JSON.stringify(payload),
|
|
13
|
+
signal: controller.signal,
|
|
14
|
+
})
|
|
15
|
+
if (!res.ok) throw new Error(`webhook failed: ${res.status}`)
|
|
16
|
+
} finally {
|
|
17
|
+
clearTimeout(timeout)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { z } from 'astro/zod'
|
|
2
|
+
import type { Auth } from './better-auth.js'
|
|
3
|
+
import type { AuthContext, Role } from '../core/blueprints/types.js'
|
|
4
|
+
import { AccessDeniedError, ValidationError } from '../core/errors.js'
|
|
5
|
+
import { fail, ok } from './envelope.js'
|
|
6
|
+
|
|
7
|
+
export interface HandlerCtx<P, B> {
|
|
8
|
+
request: Request
|
|
9
|
+
url: URL
|
|
10
|
+
params: P
|
|
11
|
+
body: B
|
|
12
|
+
auth: AuthContext
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface HandlerOptions<P, B> {
|
|
16
|
+
params?: z.ZodType<P>
|
|
17
|
+
body?: z.ZodType<B>
|
|
18
|
+
requireRole?: Role[]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function defineHandler<P = unknown, B = unknown, R = unknown>(
|
|
22
|
+
auth: Auth,
|
|
23
|
+
opts: HandlerOptions<P, B>,
|
|
24
|
+
fn: (ctx: HandlerCtx<P, B>) => Promise<R>,
|
|
25
|
+
) {
|
|
26
|
+
return async (request: Request, rawParams: Record<string, string> = {}): Promise<Response> => {
|
|
27
|
+
try {
|
|
28
|
+
const url = new URL(request.url)
|
|
29
|
+
|
|
30
|
+
let params: P = rawParams as unknown as P
|
|
31
|
+
if (opts.params) {
|
|
32
|
+
const parsed = opts.params.safeParse(rawParams)
|
|
33
|
+
if (!parsed.success) throw new ValidationError('Invalid params', { issues: parsed.error.issues })
|
|
34
|
+
params = parsed.data
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let body: B = undefined as unknown as B
|
|
38
|
+
if (opts.body && request.method !== 'GET' && request.method !== 'DELETE') {
|
|
39
|
+
const raw = await request.json().catch(() => undefined)
|
|
40
|
+
const parsed = opts.body.safeParse(raw)
|
|
41
|
+
if (!parsed.success) throw new ValidationError('Invalid body', { issues: parsed.error.issues })
|
|
42
|
+
body = parsed.data
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const session = await auth.api.getSession({ headers: request.headers })
|
|
46
|
+
const authCtx: AuthContext = session ? {
|
|
47
|
+
user: {
|
|
48
|
+
id: session.user.id,
|
|
49
|
+
email: session.user.email,
|
|
50
|
+
role: (session.user as { role?: Role }).role ?? 'member',
|
|
51
|
+
},
|
|
52
|
+
} : { user: null }
|
|
53
|
+
|
|
54
|
+
if (opts.requireRole && !authCtx.user) throw new AccessDeniedError('Authentication required')
|
|
55
|
+
if (opts.requireRole && authCtx.user && !opts.requireRole.includes(authCtx.user.role)) {
|
|
56
|
+
throw new AccessDeniedError(`Requires role: ${opts.requireRole.join(' or ')}`)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const result = await fn({ request, url, params, body, auth: authCtx })
|
|
60
|
+
if (result instanceof Response) return result
|
|
61
|
+
return ok(result)
|
|
62
|
+
} catch (err) {
|
|
63
|
+
return fail(err)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** Returns {width,height} from image headers without decoding the full file. */
|
|
2
|
+
export function probeDimensions(buf: ArrayBuffer, mime: string): { width: number; height: number } | null {
|
|
3
|
+
const v = new DataView(buf)
|
|
4
|
+
if (mime === 'image/png') {
|
|
5
|
+
if (v.byteLength < 24) return null
|
|
6
|
+
return { width: v.getUint32(16), height: v.getUint32(20) }
|
|
7
|
+
}
|
|
8
|
+
if (mime === 'image/jpeg') {
|
|
9
|
+
let i = 2
|
|
10
|
+
while (i < v.byteLength) {
|
|
11
|
+
if (v.getUint8(i) !== 0xff) return null
|
|
12
|
+
const marker = v.getUint8(i + 1)
|
|
13
|
+
const len = v.getUint16(i + 2)
|
|
14
|
+
if ((marker >= 0xc0 && marker <= 0xcf) && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc) {
|
|
15
|
+
return { height: v.getUint16(i + 5), width: v.getUint16(i + 7) }
|
|
16
|
+
}
|
|
17
|
+
i += 2 + len
|
|
18
|
+
}
|
|
19
|
+
return null
|
|
20
|
+
}
|
|
21
|
+
if (mime === 'image/webp') {
|
|
22
|
+
if (v.byteLength < 30) return null
|
|
23
|
+
if (String.fromCharCode(v.getUint8(12), v.getUint8(13), v.getUint8(14), v.getUint8(15)) === 'VP8L') {
|
|
24
|
+
const b0 = v.getUint8(21)
|
|
25
|
+
const b1 = v.getUint8(22)
|
|
26
|
+
const b2 = v.getUint8(23)
|
|
27
|
+
const b3 = v.getUint8(24)
|
|
28
|
+
const width = 1 + (((b1 & 0x3f) << 8) | b0)
|
|
29
|
+
const height = 1 + (((b3 & 0x0f) << 10) | (b2 << 2) | ((b1 & 0xc0) >> 6))
|
|
30
|
+
return { width, height }
|
|
31
|
+
}
|
|
32
|
+
return null
|
|
33
|
+
}
|
|
34
|
+
return null
|
|
35
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Loader } from 'astro/loaders'
|
|
2
|
+
import { createDb } from '../core/db.js'
|
|
3
|
+
import { readLocalesConfig } from '../core/locales.js'
|
|
4
|
+
import { EntriesRepo } from '../core/repos/entries.js'
|
|
5
|
+
|
|
6
|
+
export interface VulseLoaderOptions {
|
|
7
|
+
collection: string
|
|
8
|
+
locale?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
declare global {
|
|
12
|
+
// eslint-disable-next-line no-var
|
|
13
|
+
var __VULSE_TEST_DB__: D1Database | undefined
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function resolveBinding(ctx: unknown): D1Database {
|
|
17
|
+
const c = ctx as { _vulseTestBinding?: D1Database }
|
|
18
|
+
if (c._vulseTestBinding) return c._vulseTestBinding
|
|
19
|
+
if (globalThis.__VULSE_TEST_DB__) return globalThis.__VULSE_TEST_DB__
|
|
20
|
+
throw new Error('vulseLoader: no D1 binding available. See https://vulse.dev/docs/loader-binding')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function vulseLoader(opts: VulseLoaderOptions): Loader {
|
|
24
|
+
return {
|
|
25
|
+
name: `vulse-loader-${opts.collection}`,
|
|
26
|
+
load: async (ctx) => {
|
|
27
|
+
const includeDrafts = (ctx as { _vulseIncludeDrafts?: boolean })._vulseIncludeDrafts ?? false
|
|
28
|
+
const db = createDb(resolveBinding(ctx))
|
|
29
|
+
const repo = new EntriesRepo(db)
|
|
30
|
+
const locale = opts.locale ?? (await readLocalesConfig(db)).defaultLocale
|
|
31
|
+
const rows = await repo.list({
|
|
32
|
+
collection: opts.collection,
|
|
33
|
+
locale,
|
|
34
|
+
...(includeDrafts ? {} : { status: 'published' }),
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
ctx.store.clear()
|
|
38
|
+
for (const r of rows) {
|
|
39
|
+
await ctx.store.set({
|
|
40
|
+
id: r.id,
|
|
41
|
+
digest: `v${r.version}`,
|
|
42
|
+
data: {
|
|
43
|
+
...((r.content as Record<string, unknown>) ?? {}),
|
|
44
|
+
id: r.id,
|
|
45
|
+
slug: r.slug,
|
|
46
|
+
status: r.status,
|
|
47
|
+
publishedAt: r.publishedAt?.toISOString() ?? null,
|
|
48
|
+
updatedAt: r.updatedAt.toISOString(),
|
|
49
|
+
},
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { ValidationError } from '../core/errors.js'
|
|
2
|
+
import {
|
|
3
|
+
assertValidPluginId,
|
|
4
|
+
type AuthUserBeforeCreateResult,
|
|
5
|
+
type AuthUserCreateEvent,
|
|
6
|
+
type AuthUserCreateInput,
|
|
7
|
+
type AuthUserCreatedEvent,
|
|
8
|
+
type FormAfterSubmitEvent,
|
|
9
|
+
type FormBeforeSubmitEvent,
|
|
10
|
+
type FormBeforeSubmitResult,
|
|
11
|
+
type FormProcessEvent,
|
|
12
|
+
type MaybePromise,
|
|
13
|
+
type VulseHookName,
|
|
14
|
+
type VulsePlugin,
|
|
15
|
+
type VulsePluginContext,
|
|
16
|
+
} from '../core/plugins/definition.js'
|
|
17
|
+
import { sendFormEmail } from './forms/email.js'
|
|
18
|
+
import type { FormEmailEnv } from './forms/email.js'
|
|
19
|
+
|
|
20
|
+
const DEFAULT_HOOK_TIMEOUT_MS = 5_000
|
|
21
|
+
|
|
22
|
+
interface RegisteredPlugin {
|
|
23
|
+
plugin: VulsePlugin
|
|
24
|
+
order: number
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let registeredPlugins: RegisteredPlugin[] = []
|
|
28
|
+
|
|
29
|
+
function orderedPlugins(): RegisteredPlugin[] {
|
|
30
|
+
return [...registeredPlugins].sort((a, b) => {
|
|
31
|
+
const priority = (b.plugin.priority ?? 0) - (a.plugin.priority ?? 0)
|
|
32
|
+
return priority || a.order - b.order
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function setVulsePlugins(plugins: VulsePlugin[] = []): void {
|
|
37
|
+
const seen = new Set<string>()
|
|
38
|
+
registeredPlugins = plugins.map((plugin, order) => {
|
|
39
|
+
assertValidPluginId(plugin.id)
|
|
40
|
+
if (seen.has(plugin.id)) throw new Error(`Vulse plugin "${plugin.id}" is registered more than once`)
|
|
41
|
+
seen.add(plugin.id)
|
|
42
|
+
return { plugin, order }
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function getVulsePlugins(): VulsePlugin[] {
|
|
47
|
+
return orderedPlugins().map(({ plugin }) => plugin)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function __testResetVulsePlugins(): void {
|
|
51
|
+
registeredPlugins = []
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function loggerFor(pluginId: string): VulsePluginContext['logger'] {
|
|
55
|
+
return {
|
|
56
|
+
debug: (message, data) => console.debug(`[vulse:${pluginId}] ${message}`, data ?? ''),
|
|
57
|
+
info: (message, data) => console.info(`[vulse:${pluginId}] ${message}`, data ?? ''),
|
|
58
|
+
warn: (message, data) => console.warn(`[vulse:${pluginId}] ${message}`, data ?? ''),
|
|
59
|
+
error: (message, data) => console.error(`[vulse:${pluginId}] ${message}`, data ?? ''),
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function pluginContext(pluginId: string, env?: Record<string, unknown>): VulsePluginContext {
|
|
64
|
+
const safeEnv = env ?? {}
|
|
65
|
+
return {
|
|
66
|
+
env: safeEnv,
|
|
67
|
+
logger: loggerFor(pluginId),
|
|
68
|
+
email: {
|
|
69
|
+
send: async (input) => {
|
|
70
|
+
await sendFormEmail(safeEnv as FormEmailEnv, {
|
|
71
|
+
to: input.to,
|
|
72
|
+
subject: input.subject,
|
|
73
|
+
body: input.body ?? input.text ?? input.html ?? '',
|
|
74
|
+
})
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function withTimeout<T>(
|
|
81
|
+
promise: Promise<T>,
|
|
82
|
+
pluginId: string,
|
|
83
|
+
hookName: VulseHookName,
|
|
84
|
+
): Promise<T> {
|
|
85
|
+
let timeout: ReturnType<typeof setTimeout> | undefined
|
|
86
|
+
try {
|
|
87
|
+
return await Promise.race([
|
|
88
|
+
promise,
|
|
89
|
+
new Promise<never>((_, reject) => {
|
|
90
|
+
timeout = setTimeout(() => {
|
|
91
|
+
reject(new Error(`Vulse plugin "${pluginId}" hook "${hookName}" timed out`))
|
|
92
|
+
}, DEFAULT_HOOK_TIMEOUT_MS)
|
|
93
|
+
}),
|
|
94
|
+
])
|
|
95
|
+
} finally {
|
|
96
|
+
if (timeout) clearTimeout(timeout)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function invokeHook<TEvent, TResult>(
|
|
101
|
+
plugin: VulsePlugin,
|
|
102
|
+
hookName: VulseHookName,
|
|
103
|
+
event: TEvent,
|
|
104
|
+
env: Record<string, unknown> | undefined,
|
|
105
|
+
): Promise<TResult | undefined> {
|
|
106
|
+
const hook = plugin.hooks?.[hookName] as
|
|
107
|
+
| ((event: TEvent, ctx: VulsePluginContext) => MaybePromise<TResult>)
|
|
108
|
+
| undefined
|
|
109
|
+
if (!hook) return undefined
|
|
110
|
+
return await withTimeout(
|
|
111
|
+
Promise.resolve(hook(event, pluginContext(plugin.id, env))),
|
|
112
|
+
plugin.id,
|
|
113
|
+
hookName,
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function runContinueHook<TEvent>(
|
|
118
|
+
hookName: VulseHookName,
|
|
119
|
+
event: TEvent,
|
|
120
|
+
env?: Record<string, unknown>,
|
|
121
|
+
): Promise<void> {
|
|
122
|
+
for (const { plugin } of orderedPlugins()) {
|
|
123
|
+
try {
|
|
124
|
+
await invokeHook<TEvent, void>(plugin, hookName, event, env)
|
|
125
|
+
} catch (err) {
|
|
126
|
+
pluginContext(plugin.id, env).logger.error(`Hook "${hookName}" failed`, err)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export async function runFormBeforeSubmitHooks(
|
|
132
|
+
event: FormBeforeSubmitEvent,
|
|
133
|
+
env?: Record<string, unknown>,
|
|
134
|
+
): Promise<{ action: 'continue'; payload: Record<string, unknown> } | { action: 'drop'; reason?: string }> {
|
|
135
|
+
let payload = event.payload
|
|
136
|
+
|
|
137
|
+
for (const { plugin } of orderedPlugins()) {
|
|
138
|
+
const result = await invokeHook<FormBeforeSubmitEvent, FormBeforeSubmitResult>(
|
|
139
|
+
plugin,
|
|
140
|
+
'form:beforeSubmit',
|
|
141
|
+
{ ...event, payload },
|
|
142
|
+
env,
|
|
143
|
+
)
|
|
144
|
+
if (!result) continue
|
|
145
|
+
|
|
146
|
+
if (result.action === 'drop') {
|
|
147
|
+
return { action: 'drop', ...(result.reason ? { reason: result.reason } : {}) }
|
|
148
|
+
}
|
|
149
|
+
if (result.action === 'reject') {
|
|
150
|
+
throw new ValidationError(result.message ?? `Submission rejected by plugin "${plugin.id}"`, { plugin: plugin.id })
|
|
151
|
+
}
|
|
152
|
+
if (result.payload) payload = result.payload
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return { action: 'continue', payload }
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export async function runFormAfterSubmitHooks(
|
|
159
|
+
event: FormAfterSubmitEvent,
|
|
160
|
+
env?: Record<string, unknown>,
|
|
161
|
+
): Promise<void> {
|
|
162
|
+
await runContinueHook('form:afterSubmit', event, env)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export async function runFormBeforeProcessHooks(
|
|
166
|
+
event: FormProcessEvent,
|
|
167
|
+
env?: Record<string, unknown>,
|
|
168
|
+
): Promise<void> {
|
|
169
|
+
for (const { plugin } of orderedPlugins()) {
|
|
170
|
+
await invokeHook<FormProcessEvent, void>(plugin, 'form:beforeProcess', event, env)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export async function runFormAfterProcessHooks(
|
|
175
|
+
event: FormProcessEvent,
|
|
176
|
+
env?: Record<string, unknown>,
|
|
177
|
+
): Promise<void> {
|
|
178
|
+
await runContinueHook('form:afterProcess', event, env)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export async function runAuthUserBeforeCreateHooks(
|
|
182
|
+
event: AuthUserCreateEvent,
|
|
183
|
+
env?: Record<string, unknown>,
|
|
184
|
+
): Promise<AuthUserCreateInput | false | undefined> {
|
|
185
|
+
let user = event.user
|
|
186
|
+
let changed = false
|
|
187
|
+
|
|
188
|
+
for (const { plugin } of orderedPlugins()) {
|
|
189
|
+
const result = await invokeHook<AuthUserCreateEvent, AuthUserBeforeCreateResult>(
|
|
190
|
+
plugin,
|
|
191
|
+
'auth:userBeforeCreate',
|
|
192
|
+
{ user },
|
|
193
|
+
env,
|
|
194
|
+
)
|
|
195
|
+
if (result === false) return false
|
|
196
|
+
if (!result) continue
|
|
197
|
+
if (result.action === 'reject') {
|
|
198
|
+
throw new Error(result.message ?? `User rejected by plugin "${plugin.id}"`)
|
|
199
|
+
}
|
|
200
|
+
if (result.data) {
|
|
201
|
+
user = { ...user, ...result.data }
|
|
202
|
+
changed = true
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return changed ? user : undefined
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export async function runAuthUserAfterCreateHooks(
|
|
210
|
+
event: AuthUserCreatedEvent,
|
|
211
|
+
env?: Record<string, unknown>,
|
|
212
|
+
): Promise<void> {
|
|
213
|
+
await runContinueHook('auth:userAfterCreate', event, env)
|
|
214
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { SignJWT, jwtVerify } from 'jose'
|
|
2
|
+
|
|
3
|
+
const ALG = 'HS256'
|
|
4
|
+
|
|
5
|
+
export async function mintPreviewToken(secret: string, userId: string, ttlSeconds = 60 * 60): Promise<string> {
|
|
6
|
+
const key = new TextEncoder().encode(secret)
|
|
7
|
+
return await new SignJWT({ sub: userId, kind: 'vulse-preview' })
|
|
8
|
+
.setProtectedHeader({ alg: ALG })
|
|
9
|
+
.setExpirationTime(Math.floor(Date.now() / 1000) + ttlSeconds)
|
|
10
|
+
.sign(key)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function verifyPreviewToken(secret: string, token: string): Promise<boolean> {
|
|
14
|
+
try {
|
|
15
|
+
const key = new TextEncoder().encode(secret)
|
|
16
|
+
const { payload } = await jwtVerify(token, key)
|
|
17
|
+
return payload.kind === 'vulse-preview'
|
|
18
|
+
} catch {
|
|
19
|
+
return false
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function previewSecret(env: { VULSE_PREVIEW_SECRET?: string; BETTER_AUTH_SECRET: string }): string {
|
|
24
|
+
return env.VULSE_PREVIEW_SECRET ?? env.BETTER_AUTH_SECRET
|
|
25
|
+
}
|
package/src/server/r2.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { nanoid } from 'nanoid'
|
|
2
|
+
|
|
3
|
+
export interface UploadContext { bucket: R2Bucket }
|
|
4
|
+
|
|
5
|
+
export async function putToR2(ctx: UploadContext, body: ArrayBuffer, mime: string): Promise<{ key: string }> {
|
|
6
|
+
const key = `${new Date().toISOString().slice(0, 10)}/${nanoid()}`
|
|
7
|
+
await ctx.bucket.put(key, body, { httpMetadata: { contentType: mime } })
|
|
8
|
+
return { key }
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function deleteFromR2(ctx: UploadContext, key: string): Promise<void> {
|
|
12
|
+
await ctx.bucket.delete(key)
|
|
13
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { z } from 'astro/zod'
|
|
2
|
+
import type { Auth } from '../better-auth.js'
|
|
3
|
+
import type { VulseDb } from '../../core/db.js'
|
|
4
|
+
import {
|
|
5
|
+
BlueprintDefinitionSchema,
|
|
6
|
+
BlueprintDefinitionWithRenamesSchema,
|
|
7
|
+
} from '../../core/blueprints/definition.js'
|
|
8
|
+
import {
|
|
9
|
+
createBlueprint,
|
|
10
|
+
deleteBlueprint,
|
|
11
|
+
getBlueprintDefinition,
|
|
12
|
+
listBlueprintDefinitions,
|
|
13
|
+
updateBlueprint,
|
|
14
|
+
} from '../../core/blueprints/mutations.js'
|
|
15
|
+
import { _resetRegistry } from '../../core/blueprints/load.js'
|
|
16
|
+
import { _resetRuntime } from '../runtime.js'
|
|
17
|
+
import { defineHandler } from '../handler.js'
|
|
18
|
+
|
|
19
|
+
const paramsHandle = z.object({ handle: z.string() })
|
|
20
|
+
|
|
21
|
+
export function blueprintsRoutes(db: VulseDb, auth: Auth) {
|
|
22
|
+
return {
|
|
23
|
+
list: defineHandler(auth, {}, async () => listBlueprintDefinitions(db)),
|
|
24
|
+
|
|
25
|
+
get: defineHandler(auth, { params: paramsHandle }, async ({ params }) => {
|
|
26
|
+
const def = await getBlueprintDefinition(db, params.handle)
|
|
27
|
+
if (!def) throw new (await import('../../core/errors.js')).NotFoundError('blueprint not found')
|
|
28
|
+
return def
|
|
29
|
+
}),
|
|
30
|
+
|
|
31
|
+
create: defineHandler(auth, {
|
|
32
|
+
body: BlueprintDefinitionSchema,
|
|
33
|
+
requireRole: ['admin'],
|
|
34
|
+
}, async ({ body }) => {
|
|
35
|
+
const out = await createBlueprint(db, body)
|
|
36
|
+
_resetRegistry()
|
|
37
|
+
_resetRuntime()
|
|
38
|
+
return out
|
|
39
|
+
}),
|
|
40
|
+
|
|
41
|
+
update: defineHandler(auth, {
|
|
42
|
+
params: paramsHandle,
|
|
43
|
+
body: BlueprintDefinitionWithRenamesSchema,
|
|
44
|
+
requireRole: ['admin'],
|
|
45
|
+
}, async ({ params, body }) => {
|
|
46
|
+
const out = await updateBlueprint(db, params.handle, body)
|
|
47
|
+
_resetRegistry()
|
|
48
|
+
_resetRuntime()
|
|
49
|
+
return out
|
|
50
|
+
}),
|
|
51
|
+
|
|
52
|
+
delete: defineHandler(auth, {
|
|
53
|
+
params: paramsHandle,
|
|
54
|
+
requireRole: ['admin'],
|
|
55
|
+
}, async ({ params }) => {
|
|
56
|
+
await deleteBlueprint(db, params.handle)
|
|
57
|
+
_resetRegistry()
|
|
58
|
+
_resetRuntime()
|
|
59
|
+
return null
|
|
60
|
+
}),
|
|
61
|
+
}
|
|
62
|
+
}
|