Conversation
Replace the hand-wired, position-sensitive plugin route mounting in app.ts with a shared, synchronous primitive. Plugins previously had to be wired into core app.ts by hand, each guarded by a "MUST be registered BEFORE /admin/plugins" comment, so any plugin relying on PluginBuilder.addRoute() (e.g. global-variables, shortcodes) was never mounted and 404'd in production. - Add plugins/mount.ts: registerPluginRoutes() + mountPlugin() + PluginRegisterMustBeSyncError. Synchronous (Hono's SmartRouter locks after the first request), priority-ordered, with duplicate-path warnings. Typed against a structural MountablePlugin to sidestep the src-vs-dist Plugin identity clash. - app.ts: the 7 copy-pasted route-mounting blocks become two registerPluginRoutes() calls. Mount globalVariablesPlugin + shortcodesPlugin (fixes #758). Mount config.plugins.register user plugins before the /admin catch-all so consumers never edit core (#829, #621). - disableAll now consistently gates all plugin mounting (core + user), matching bootstrap behavior and the documented intent; resolves the #829 review mismatch. - types.ts: add sync-only Plugin.register?(app). app.ts: add SonicJSConfig.plugins.register; deprecate directory/autoLoad no-ops. Tests: mount.test.ts (16 unit incl. sync-guard + catch-all shadowing) and mount-integration.test.ts (5 incl. #758 regression + disableAll matrix). Full core suite 1498 passed, 0 failed; tsc clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e 2)
Hooks were dead metadata at runtime: HookSystemImpl only exists inside
PluginManager, which is never instantiated in the running app, and nothing
dispatches lifecycle events. This lands the typed-hook + async-wiring foundation
so plugin hooks and onBoot can be wired for real (the async half of two-phase
boot; route mounting is the sync half from Phase 1).
- plugins/hooks/catalog.ts: typed event catalog (6 content + 3 auth events) ->
payload types; HookEventName / HookPayload<E> / HOOK_EVENT_NAMES / isKnownHookEvent.
- plugins/hooks/typed-hooks.ts: createTypedHooks() -> { on<E>, dispatch<E> } with
catalog inference; structural HookSystemLike so HookSystemImpl/ScopedHookSystem
both satisfy it without casts.
- plugins/hooks/hook-system-singleton.ts: get/set/has/reset + getTypedHooks.
Throw-before-get, idempotent set (multi-app/test safe), reset for isolation;
env-independent access for cron (Phase 4).
- plugins/wire.ts: wireRegisteredPlugins() subscribes all hooks[] then runs each
onBoot (per-plugin error isolation); structural WirablePlugin; createPluginWirer
once-guard for the lazy first-request trigger.
Tests: typed-hooks.test.ts (11 runtime + a tsc-validated type-level block proving
narrowed payloads and rejected unknown events/fields), wire.test.ts (8: subscribe
-> dispatch, all-hooks-before-any-onBoot ordering, error isolation, once-guard
under concurrency). Full core suite 1517 passed, 0 failed; tsc clean.
Activating the wiring in the live app (eager setHookSystem, first-request wire,
real dispatch sites) is deferred to Phase 2b with dedicated integration tests.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bring the hook system to life. Previously hooks were dead metadata at runtime; now createSonicJSApp wires plugins for real: - Eager setHookSystem(new HookSystemImpl()) at construction publishes the singleton (env-independent access for cron later). - Core plugins extracted into shared corePluginsBeforeCatchAll / corePluginsAfterCatchAll arrays, reused for both route mounting and wiring so they never drift (no Plugin[] annotation -> dodges the src/dist Plugin identity clash; both consumers are structural). - A once-guarded first-request middleware (after bootstrap) runs wireRegisteredPlugins exactly once: subscribes every core + user plugin's hooks[] and runs onBoot. Error-isolated; skipped under disableAll. The first request now subscribes the real core plugin hooks. They stay inert until dispatch sites are added, so no existing behavior changes -- but the infrastructure is proven end-to-end. Tests: wire-integration.test.ts (4): singleton published at construction; first request runs onBoot + subscribes hooks (verified by dispatching content:create); wires exactly once across 3 requests; disableAll skips wiring. Full core suite 1521 passed, 0 failed; tsc clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add the isolation boundary that Strapi (namespacing only) and Payload (full config access) lack: a plugin declares the capabilities it needs, and the host hands it a context whose powerful accessors are gated by those declarations. - plugins/capabilities.ts: Phase 1 vocabulary (FIXED_CAPABILITIES + parameterized db:<table>), isKnownCapability/validateCapabilities, SonicCapabilityError, hasCapability/assertCapability, and createCapabilityContext() whose accessors are lazy throwing getters -- ctx.email throws SonicCapabilityError unless email:send was declared. - plugins/singletons/service-singleton.ts: createServiceSingleton<T>(label) generalizing the hook-system-singleton pattern (throw-before-get, idempotent set, reset). Env-independent so cron/scheduled() handlers can reach services. Pure infrastructure, no behavior change. Providers + gated context get wired into the live app with definePlugin() in Phase 5. Tests: capabilities.test.ts (19): db:<table> matching, granted/denied gating, cache read-or-write, lazy providers, and the singleton contract (throw-before-get, idempotent set, cron-reachable without env). Full core suite 1540 passed, 0 failed; tsc clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…se 4)
Let plugins declare scheduled work as data and dispatch it from the Worker's
scheduled() handler. Like Payload's jobs queue, declaring a schedule runs nothing
by itself: on Workers the execution mechanism is a Cron Trigger delivered to
scheduled(), which this fans out to matching plugins.
- plugins/cron.ts: CronDeclaration ({ schedule, hookFamily }) + structural
CronablePlugin (crons[] + async onCronTick). collectCrons/collectCronSchedules
flatten declarations (wrangler sync + diagnostics). dispatchCronTick() matches a
fired expression to plugins, tags each onCronTick with the matched hookFamily
(one call per matching declaration), error-isolated. createScheduledHandler()
returns a CF scheduled(controller, env, ctx) handler reaching services via the
env-independent singletons (cron has no c.env), passing Worker env through and
waitUntil-ing the work.
Tests: cron.test.ts (12): collect/flatten, match-only-fired-cron, well-formed
event, unmatched reporting, multi-cron fan-out, error isolation, and the scheduled
handler (dispatch + waitUntil + env passthrough + lazy list + disabled no-op).
Full core suite 1552 passed, 0 failed; tsc clean.
Wiring scheduled() into the consumer Worker export + wrangler trigger generation
is a DX change deferred to the definePlugin()/docs bundle (Phase 5/7).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The typed authoring entry point that unifies the foundation: a plugin is declared
once and consumed unchanged by mount (routes/register), wire (onBoot), and cron
(crons/onCronTick), plus the legacy metadata fields the admin/registry read.
- plugins/sdk/define-plugin.ts: definePlugin(input) -> DefinedPlugin. Normalizes
id -> name, validates declared capabilities (warns on unknown), marks output
__sonicV3 (+ isDefinedPlugin guard). onBoot/onCronTick receive an ENRICHED
context { hooks, cap, env, raw }: a typed hook facade (ctx.hooks.on with narrowed
payloads) and the capability-gated service context (ctx.cap.email throws
SonicCapabilityError without email:send). The runtime still passes the plain
boot/cron context; definePlugin wraps the author fns. Host providers ride on
raw.providers.
- Exported from plugins/index.ts.
Purely additive — nothing in core uses it yet. Tests: define-plugin.test.ts (13)
+ define-plugin-integration.test.ts (3: mounts via createSonicJSApp, honors
disableAll, onBoot runs exactly once on first request). Full suite 1565 passed,
0 failed; tsc clean.
Next: migrate email onto definePlugin (5b), fix magic-link (5c), add content/auth
dispatch sites (5d) — all behavior-changing, separately tested.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…b-1)
Today email is fractured: OTP hardcodes Resend, email-templates uses SendGrid,
magic-link calls a registry (c.env.plugins?.get('email')) that was never built,
and password-reset doesn't send at all (it returns the reset link in the JSON
response — a token leak). There is no single chokepoint and no core email_log.
This lands the chokepoint (additive; no call sites switched yet):
- services/email: EmailProvider interface + built-in Resend / SendGrid / Console
providers. A dev can pass ANY EmailProvider implementation — "use whatever
provider you want." EmailService.send() normalizes the message, calls the
provider, and records every attempt in email_log (best-effort; logging never
fails a send; a throwing provider is surfaced as a structured failure).
- resolveEmailProvider precedence: explicit instance > named built-in > env
auto-detect (RESEND_API_KEY, then SENDGRID_API_KEY) > Console. An unconfigured
choice degrades to Console with a warning, so a missing key becomes
"logged, not delivered" — never a silent token leak.
- email-service singleton (via createServiceSingleton) for env-independent
access from cron / scheduled() reconciliation.
- Core email_log table: Drizzle schema + migration 037 (bundled). Epoch-ms
integer timestamps; columns for provider/provider_id/error/flow/metadata plus
failed_at_send and delivery_state/delivery_synced_at for the reconciliation cron.
Tests: email-service.test.ts (19): normalization, sent/failed logging + the
failed_at_send path, throwing-provider isolation, logging-never-fails-send,
Resend/SendGrid/Console providers (mocked fetch), and resolveEmailProvider
precedence incl. the console degrade. Full core suite 1584 passed, 0 failed; tsc clean.
Wiring it in + migrating the call sites (OTP, magic-link, and closing the
password-reset leak) is the next increment (Phase 5b-2).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e 5b-2)
Wires the provider-agnostic EmailService into the app and migrates the two real
send paths, fixing a token-leak security bug along the way.
- app.ts: new `email` config ({ provider | providerName | from }) so devs choose
any provider. On first request the app resolves a provider (config > env
auto-detect > console) and publishes the EmailService singleton; its init is
isolated in its own try/catch so it can never block plugin wiring. The
capability boot context now resolves `ctx.cap.email` to this service.
- SECURITY (auth.ts): POST /auth/request-password-reset no longer returns
`reset_link` in the JSON response — that leaked a valid reset token to any
caller. It now emails the link via EmailService (flow: 'password-reset') and
returns only the generic, enumeration-safe message. Delivery failure does not
change the response.
- magic-link: replace the broken `c.env.plugins?.get('email')` lookup (a registry
that never existed, so links were only console-logged) with
getEmailService().send({ flow: 'magic-link' }).
Tests: email-wiring-integration.test.ts (3) through the real createSonicJSApp —
provider initialized on first request; reset response omits reset_link AND the
token, sending via email instead; unknown email stays generic and sends nothing.
Also null-safe initEmailService (fixes wire-integration when requests carry no
env). Full core suite 1587 passed, 0 failed; tsc clean.
OTP migration to the shared EmailService is deferred (it works today via Resend).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… fix) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ettings (Phase 5c)
Consolidates the last ad-hoc email sender. OTP previously read plugins.settings
and called Resend directly; it now goes through the provider-agnostic
EmailService, so OTP sends are logged to email_log like every other flow. It
stays synchronous (caller-direct) — the user can't proceed without the code.
Provider precedence in the app's init now also honors the admin Plugins-page
email config (API key in plugins.settings, not env), so existing installs keep
delivering: config.email > named built-in > env keys > admin-UI DB settings
(Resend) > console. initEmailService is async to read those DB settings.
- services/email/db-settings.ts: loadDbEmailSettings() (never throws) + dbSettingsFrom().
- app.ts: DB-aware async initEmailService.
- otp-login-plugin: send via getEmailService({ flow: 'otp' }).
- lint: silence @typescript-eslint/naming-convention for the colon-bearing hook
event keys (catalog.ts) and the intentional __sonicV3 marker (define-plugin.ts);
the husky pre-commit hook activated mid-effort and these slipped through earlier.
Tests: email-db-settings.test.ts (9). Full core suite 1596 passed, 0 failed; tsc clean.
Verified live: OTP request logs `[email:console] (otp) ...` and writes an email_log row.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Deep-dive comparison of Mark's Infowall plugin SDK (/Users/lane/Dev/refs/infowall-ai-main) vs the shipped SonicJS v3 framework, with a decisive convergence + production-readiness roadmap. Produced via a multi-agent workflow (9 mappers -> 10 adversarially-verified dimension comparisons -> synthesize/critique/revise), then hand-corrected: a synthesis pass had wrongly reported Infowall as "out-of-tree/unopenable" (it searched this workspace instead of the absolute path); the two crux dimensions (hook-event catalog, capability vocabulary) plus topo-sort/once-guard/cron-registry claims were re-verified against the real Infowall source. Thesis: SonicJS is base-of-record (mounting wired, reset-link leak closed, provider- agnostic email, better substrate); harvest Infowall's rigor (real hook dispatch sites, capability enforcement at the subscription boundary, dependency topo-sort, live cron+reconciliation). Three real long poles, all SonicJS-side: inert hook catalog (zero production dispatch), HTTP-gated wire phase unreachable from scheduled(), missing hook- subscription capability gate. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…n-readiness Execution-ready task breakdown (Phase 1 contract alignment → 2 dispatch+enforcement → 3 ordering/cron/reconciliation [PRODUCTION-READY] → 4 structure/distribution [FUTURE-PROOF]). Each task: goal/files/change/tests/done-when + size + parallelism. Decisions locked: SonicJS canonical, name-map hooks, before/after content events, separate cron, SonicJS capability spellings + rename map, unified user.id actor shape, SonicJS substrate. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fixes a live inconsistency: content event payloads exposed `user.userId` while
auth payloads exposed `user.id`, so a plugin reading one field on the wrong
family got undefined. Introduce one canonical `HookActor { id, email, role? }`
used across all content + auth events; content's `userId` becomes `id`.
Type-level assertions (tsc-validated): content `user.id` is string, reading
`user.userId` is now an error, and content/auth actor shapes agree. Core suite green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…lias window (T1.2)
Adopt the before/after content-event model (gate/transform vs side-effect), the
industry-standard shape Payload/Strapi/WordPress use:
- content:before:{create,update,delete} (handlers may mutate or throw to cancel)
- content:after:{create,update,delete,publish}; keep content:read
- add auth:magic-link:consumed, auth:otp:verified
- drop content:save (folded into update)
Ship as a breaking catalog change WITH a one-release deprecation window: the
legacy names (content:create/update/delete/publish/save) still compile and
subscribe via createTypedHooks().on() — they resolve to the canonical name and
emit a one-time deprecation warning. dispatch() is canonical-only (the host owns
dispatch sites). LegacyHookEventPayloads keeps legacy names typed to the canonical
payload; resolveHookEventName/isLegacyHookEvent/LEGACY_EVENT_ALIASES are exported.
Tests: before-hook mutation, legacy-alias fires-on-canonical-dispatch + warns once,
type-level proof that legacy names still compile to the canonical payload. Full
core suite 1598 passed, 0 failed; tsc + lint clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cross-version / cross-fork capability portability. A plugin authored against a
different SDK spelling now loads against the canonical vocabulary without code
changes.
- CAPABILITY_RENAMES (deprecated→canonical) seeded with the sibling fork's
spellings: storage:*→media:*, hooks.cron:register→cron:register,
hooks.{auth,content-read,content-write,email-events}:register→canonical :subscribe.
- normalizeCapability(str)→Capability|null (rename then known-check) and
normalizeCapabilities(list)→{capabilities, unknown} (dedupes, splits unknowns).
- Reserve hooks.email:subscribe in the vocabulary (rename target; gates the email
event family once it ships).
- definePlugin now normalizes declared capabilities first, then warns on the ones
still unknown — and DROPS unknowns from the granted set (an unrecognized name is
not a granted capability). request:intercept has no canonical target and
surfaces as unknown rather than silently gating nothing.
Tests: rename resolution, every rename target is itself known, dedupe + unknown
split, definePlugin normalizes storage:write→media:write without warning, unknowns
dropped. Full core suite 1605 passed, 0 failed; tsc + lint clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ctx.cap.email now resolves to the real EmailService type at compile time — but only when the plugin declared 'email:send'. Accessing an undeclared capability is a compile error, shifting the existing runtime SonicCapabilityError left. - definePlugin<const Caps extends readonly Capability[]>; capabilities inferred as a literal tuple (default readonly [] so omitting = nothing granted). - CapabilityContext<Caps> with WhenGranted/WhenGrantedAny mapping each accessor to its service type, or a branded CapabilityNotDeclared type when absent (not `never`, which would be assignable to anything and defeat the check). - createCapabilityContext is generic; runtime gating still uses the normalized set while the context TYPE reflects the declared tuple. EmailService imported type-only (no runtime coupling). CapabilityProviders.email typed () => EmailService. Tests: tsc-validated narrowing — email:send → EmailService; other/empty caps → compile error on ctx.cap.email. Full core suite 1605 passed, 0 failed; tsc + lint clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a declarative `hooks` map to DefinePluginInput so plugins can subscribe to
lifecycle events without an onBoot body:
definePlugin({ id, version, capabilities: ['hooks.content:subscribe'],
hooks: { 'content:after:create': (payload) => { /* payload narrowed */ } } })
Each entry is keyed by a canonical HookEventName and the handler is narrowed to
that event's payload (DeclarativeHooks = { [E in HookEventName]?: TypedHookHandler<E> }).
definePlugin flattens the map into the wirable hooks[] array (wrapping each handler
to the raw shape, void-coalesced), so they subscribe through the existing wire
phase and fire on dispatch. Imperative ctx.hooks.on() in onBoot remains the
dynamic-subscription escape hatch.
Tests: declarative hook flattens + fires after wiring; type-level per-event payload
narrowing + unknown-event-key rejection. Full core suite 1607 passed, 0 failed;
tsc + lint clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…(T1.6)
The DB-settings (admin Plugins page) email path hardcoded `new ResendProvider`,
so it was Resend-locked and skipped the degrade-to-Console safety, and it dropped
the configured replyTo.
- app.ts initEmailService: resolve the admin-UI key via resolveEmailProvider
({ providerName: 'resend', env: { ...env, RESEND_API_KEY } }) for consistent
provider selection + safe degrade; the no-key branch also goes through the
resolver (console fallback + warning) instead of constructing ConsoleProvider.
- EmailService gains defaultReplyTo, applied when a message omits replyTo; wired
from DbEmailSettings.replyTo so admin-configured reply-to is honored.
Tests: defaultReplyTo applied + per-message override. Full core suite 1608 passed,
0 failed; tsc + lint clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Add enterprise document repository POC plan - Define four-table schema for document types, documents, values, and permissions - Outline implementation phases and test strategy Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
Adds the document repository migration (037), type registry, CRUD service, projection (facets/references), permissions, and repository chokepoint. All multi-statement writes use db.batch for atomicity; version_number is SQL-derived; facet/reference inserts chunk under D1's 100-param limit. 20 unit tests cover draft/publish two-axis model, deny-wins ACL, tenant isolation, and param-limit chunking. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds authenticated admin CRUD routes (/admin/documents) and a read-only public API (/api/documents). Both support cursor pagination on (updated_at, id), scalar filters via generated columns (?filter[field]=value), facet filters (?facet[tags]=homepage), and configurable sort order. Admin routes cover create, save-draft, publish, unpublish, soft-delete, and reindex. Public routes enforce the scheduled_at / expires_at time window. Routes wired into app.ts. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
dispatch-event.ts helper (T2.1–T2.3): a route-facing typed dispatch helper that safely extracts executionCtx from the Hono context (throws in non-Workers environments). fire-and-forget mode runs via waitUntil; in-band mode awaits the handler chain so before-hooks can mutate the payload. Auth dispatch sites (T2.1): auth:registration:completed, auth:password-reset:requested (carries resetToken for custom notification plugins), auth:password-reset:completed. Dispatched from both JSON + HTML registration routes and both reset routes. Magic-link + OTP dispatch sites (T2.2): auth:magic-link:consumed on successful magic-link verify; auth:otp:verified on successful OTP verify. Content dispatch sites (T2.3): content:before:create/update/delete (in-band, payload mutations flow through to the write); content:after:create/update/delete and content:after:publish (fire-and-forget, side-effect plugins). content:read dispatched on GET /:id (fire-and-forget). Capability gate (T2.4): HOOK_CAPABILITY_MAP added to capabilities.ts mapping every catalog event to its required subscription capability. Wire phase A now enforces the gate for v3 plugins (capabilities !== undefined); old PluginBuilder plugins are exempt for backwards compatibility. Non-strict mode warns; strict mode records a SonicCapabilityError in WireResult. SonicCapabilityError.accessedApi (T2.5): optional field, non-breaking. No-dispatch-site CI guard (T2.6): test asserts every HOOK_EVENT_NAMES entry has at least one dispatchHookEvent() call in a non-test source file. Fails if a catalog event ships without a real production dispatch site. Wire-integration rewrite (T2.7): removed manual hooks.dispatch() call; test now fires via a minimal Hono app calling dispatchHookEvent() — the same path production routes use. Would fail if dispatchHookEvent were removed from routes. Tests: +14 new tests. Full suite 1622 passed, 0 failed; tsc + lint clean. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds HTMX-driven admin pages at /admin/documents/ui: - Document type selector landing page - Per-type document list with status filter and cursor pagination - Create/edit form with dynamic fields from queryable_fields config - Publish/unpublish controls with "edit while published" state banner - Version history lazy-loaded via HTMX reveal trigger - Soft-delete / hard-erase (PII types) from list row - Role-aware UI: destructive actions hidden for non-admin users - Form submissions use POST+_method=PUT (no JS required for core flow) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…observability (T3.1–T3.6)
T3.1 — Dependency topo-sort + cycle detection:
- `plugins/topo-sort.ts`: DFS topo-sort with `visiting` stack, throws
`PluginDependencyCycleError` on cycles, warns/throws on missing dep ids (strict).
- `MountablePlugin` + `WirablePlugin` extended with optional `id` + `dependencies`.
- `registerPluginRoutes` + `wireRegisteredPlugins` now sort by dependencies by
default (sortByDependencies=true); old-style plugins without `dependencies`
keep their original declaration order.
T3.2 — bootIsolate extraction:
- HTTP middleware body factored into `boot: BootIsolateFn` closure — the same
once-guarded `initEmailService` + `wirePlugins` call, now exposed on the
returned `SonicJSApp` object.
- `SonicJSApp` type extended with `readonly boot: BootIsolateFn`.
- The HTTP middleware now calls `boot(c.env)`, sharing the once-guard with any
other caller (cron, test harness).
T3.3 — Wire scheduled() end-to-end:
- `createScheduledHandler` gains optional `boot?` parameter. Called before the
first cron dispatch so a cron-first cold isolate has a populated hook bus and
reachable email service; warm isolates return instantly (once-guard).
- `my-sonicjs-app/src/index.ts` restructured to export `{ fetch, scheduled }` —
a proper Worker object instead of a bare Hono app. `scheduled` wires through
`app.boot`.
T3.4 — wrangler.toml [triggers] codegen:
- `plugins/generate-triggers.ts`: `parseCronTriggers`, `updateWranglerTriggers`,
`generateTriggersComment` utilities.
- `my-sonicjs-app/scripts/generate-cron-triggers.ts`: a tsx script that reads
plugin `crons[]` and writes the `[triggers]` section; `--check` mode for CI.
T3.5 — Per-provider reconciliation + observability migration:
- `EmailProvider.reconcile?()` optional method for delivery-state backfill.
- `EmailLogRow` type for reconciliation inputs.
- Migration `038_email_log_observability.sql`: adds `user_id`, `context_type`,
`context_id`, `tenant_id`, `delivery_state`, `delivery_synced_at` (all
nullable, no defaults — forward-only D1 / NULL-safe). Partial index for
reconciliation queries; per-user history index.
T3.6 — CloudflareEmailProvider:
- `services/email/providers/cloudflare.ts`: `CloudflareEmailProvider` wraps the
`send_email` Workers binding (MailChannels / CF Email Routing).
New exports: `createScheduledHandler`, `dispatchCronTick`, `collectCrons`,
`collectCronSchedules`, `getHookSystem`, `topoSort`, `PluginDependencyCycleError`,
`CloudflareEmailProvider`, `parseCronTriggers`, `updateWranglerTriggers`,
`BootIsolateFn`.
Tests: +25 (topo-sort: 11, boot-isolate: 8, generate-triggers: 6).
Full suite: 1647 passed, 0 failed; tsc + lint clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds bootstrapDocumentTypes() called during app startup (idempotent). Registers faq, testimonial, contact_message, and media_asset types with their queryable field configs so /admin/documents/ui is populated on first run without manual seeding. Migration 037 runs first, then types are registered; both paths are gracefully skipped if already done. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds seed-documents.ts with 5 FAQs (4 published, 1 draft), 4 testimonials (3 published, 1 draft), and 3 contact messages. Wired into setup-worktree-db.sh so npm run workspace seeds everything in one step. Also available standalone: npm run seed:documents. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Drops the legacy testimonials table (migration 038) and repoints all data access to the document repository (type_id = 'testimonial'). Changes: - 038_drop_testimonials.sql: DROP TABLE IF EXISTS testimonials - admin-testimonials.ts: CRUD now queries documents table via DocumentsService / raw D1 SQL; same URL paths and template interface - testimonials/index.ts: public /api/testimonials routes rewritten; JSON response shape preserved (id is now string rootId); removed addModel() — no dedicated table - Templates: id field widened from number to string The testimonials admin UI (/admin/testimonials) and public API (/api/testimonials) continue to work; only the storage tier changed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
seed-documents.ts runs before the first HTTP request, so migration 037 (which creates document_types) must be in the Wrangler migrations folder or the seed fails. Copying both 037 and 038 here ensures setup:db applies them before the seed script runs. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ening (T4.1–T4.5) T4.1 — Stop committing dist/ + fix src/dist type identity: - Root .gitignore: add `packages/*/dist/` so build artifacts are never committed. - `git rm -r --cached packages/core/dist/`: stop tracking existing dist files. - tsconfig.json: add `@sonicjs-cms/core` → `./src` path alias so in-tree self-imports from core plugins resolve to the same types as the rest of src, eliminating the dual `Plugin` identity that forced structural casts everywhere. T4.3 — Versioning / semver compat gate: - `DefinePluginInput.sonicjsVersionRange?`: semver range the plugin declares for SonicJS core compatibility (e.g. `'^3.0.0'`). - `definePlugin()` validates the plugin's own `version` field (warns on invalid semver) and checks `sonicjsVersionRange` against the running core version at definition time (warns on mismatch). Both use a minimal in-tree semver helper — no npm `semver` dep (bundle-size constrained on Workers). - `DefinedPlugin.sonicjsVersionRange` carries the range through to the runtime. T4.4 — DB activation reflection + email_log admin browser: - `wire.ts` Phase C (best-effort): after wiring, upserts each booted plugin into the `plugins` DB table (`INSERT ... ON CONFLICT DO UPDATE`) so the admin view reflects what is actually running. Non-fatal — a DB error in reflection never aborts wiring. - `/admin/settings/email-log`: paginated HTML browser showing all email_log rows with status, delivery_state, flow, provider, recipient, subject, and user. Uses migration 037+038 columns; renders an empty-state if the table is missing. - `/admin/settings/email-log/api`: JSON endpoint for the same data, filterable by flow/status, paginated by limit/offset. T4.5 — Shared author mock harness: - `__tests__/utils/mock-factories.ts`: typed mock primitives for plugin authors: - `makeMockD1Database(opts)` — D1-shaped mock (static rows or resolver fn) - `makeMockKVNamespace()` — in-memory KV with put/get/delete/list - `makeMockHonoContext(opts)` — Hono context mock with json/html/redirect/vars - `makeMockEmailProvider()` — recording EmailProvider, captures `.sent[]` - `makeMockHookSystem()` — a real HookSystemImpl instance (not a stub) Previously 5+ inline fakes existed with varying shapes; this replaces them. Tests: +18 (T4.1 type-identity check, T4.3 semver gate 5-case, T4.5 mocks 12-case). Full suite: 1665 passed, 0 failed; tsc (non-test files) clean. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Hides the Email Log menu entry; routes and APIs remain intact. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Port derived from worktree dir name via cksum % 1000 + 9000. Same port every restart; no manual config needed per worktree. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…reenfield installs (#886) - New lexical-editor plugin using definePlugin (v3 SDK), activated by default - Blog post collection content field changed from quill to lexical type - isPluginActive reads from documents table (authoritative) — removes dependency on plugins table which may not exist on fresh installs - loadContentEditorFlags includes lexical alongside tinymce/quill/mdxeditor so document-backed edit forms get lexicalEnabled correctly - PluginBootstrapService auto-installs plugins with defaultActive:true from manifest - manifest-registry generator now emits defaultActive field from manifest.json - Better Auth baseURL derived from request when BETTER_AUTH_URL env var absent (fixes warning on dev with random/deterministic per-worktree ports) - test-cleanup wraps DELETE FROM media in try/catch (table decommissioned) - activatePlugin/deactivatePlugin sync plugins table for legacy isPluginActive callers - getActivePlugins reads from documents table - pencil-square icon added to plugin-menu ICON_SVG map Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
) Internal collections (source=system/plugin or cfg.internal=true) were leaking into the dropdown. Extracts canonical filtering into collection-loader so /admin/collections and the new-content menu share one definition of "visible collection". - add isCodeCollectionInternal / isDbDocTypeInternal predicates - add getVisibleCollections(db) using those predicates - admin-collections.ts uses shared predicates (no logic change) - admin-content.ts passes newContentCollections to template - content-list template renders dropdown from newContentCollections Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(media): show plugin-not-enabled warning in media selector When media plugin is inactive or uninstalled, /admin/media/selector now returns a friendly warning with a link to /admin/plugins instead of crashing with "Error loading media files". Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: update worktree db config and regen migrations bundle Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…ests (#895) - Plugin filter state (category, status, search, sort) now syncs to URL params via history.replaceState so page reload after install retains the user's selection; initFiltersFromURL restores state on load. - Migrate 02c-otp-login.spec.ts to Better Auth emailOTP endpoints: POST /auth/email-otp/send-verification-otp + POST /auth/sign-in/email-otp - Migrate 02d-magic-link-auth.spec.ts to Better Auth magicLink endpoints: POST /auth/sign-in/magic-link + GET /auth/magic-link/verify - Add 79-plugin-filter-url.spec.ts (7 E2E tests covering URL sync, reload persistence, multi-select, and sort param behaviour). Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(admin): sidebar plugin sub-nav shows only active plugins The sidebar fell back to resolvePluginMenuItems() from the singleton, which is populated at startup with ALL registered plugins regardless of status. Replace the fallback with an empty array so the DYNAMIC_PLUGIN_MENU marker is always rendered when no dynamicMenuItems are passed, letting pluginMenuMiddleware inject only DB-active plugins via post-render replacement. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(admin): use plugin id (not codeName) for active-status DB query REGISTRY_MENU_PLUGINS now maps p.id instead of p.codeName so the slug IN-list matches how plugins are stored in the documents table (slug = id). Fixes ai-search and any plugin where id != codeName showing as inactive. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Reduces console noise during dev. Retains 'Build complete!' message. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Adds a new `user` field type that lets editors link a blog post author to a system user via live autocomplete, or type a free-form custom name. - New `user` FieldType in collection-config - `blog-posts.collection.ts` author field changed string → user - `dynamic-field.template.ts`: renders text+hidden input pair with HTMX autocomplete dropdown (zinc-800 dark bg, box-shadow, no border flash) - `admin-api.ts`: GET /admin/api/users/search (JSON), /search-html (HTMX buttons), /:id (display name lookup for reload) - On load: fetches display name from user ID so edit page shows name not slug - On submit: syncs display value → hidden when typing custom (non-user) name - 7 Playwright E2E tests covering API, field render, dropdown, and custom name Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…es (#899) * feat(email): v3 email-plugin — CF Email Service + auth-event hooks + templates Port of mmcintosh/sonicjs-infowall-merge email plugin work: - v3 email-plugin using definePlugin SDK (replaces legacy PluginBuilder) - Auth-event hooks: registration, password-reset requested/completed - Cron reconciliation hook (every 5 min, email-reconciliation family) - CF Email Service adapter + CfGraphqlClient for delivery polling - Email log service + reconciliation service - SonicJS-dark transactional email templates (_layout, welcome, otp, password-reset, password-changed, test-email, verification, invitation) - Admin routes: /admin/email/settings, /admin/email/test - Unit tests for all services, hooks, and templates Source commits (mmcintosh/sonicjs-infowall-merge): f23e11ed feat(email): v3 email-plugin — CF Email Service + auth-event hooks 3ff50a54 feat(email): SonicJS-dark visual identity for transactional emails e9e22e4d feat(email): add /admin/settings/email tab b29136de feat(email): self-hosted setup UX a2eb99f3 feat(auth): email verification flow (+ 18 follow-up fixes and cleanups) Co-Authored-By: Lane Campbell <ldc0618@gmail.com> * chore(email): adapt mmcintosh plugin to our type system + plugin SDK Bridges the type/API gap between mmcintosh/sonicjs-infowall-merge's email plugin (prev commit) and our canonical v3 plugin SDK: - index.ts: switch to onBoot + onCronTick (our SDK pattern); auth hooks registered with env-closure factory functions so handlers get D1 access; capability names corrected (hooks.cron:register→cron:register, hooks.auth:register→hooks.auth:subscribe); displayName→name - hooks: rewrite to factory pattern makeOn*() capturing env; fix arg order (ctx,event)→(payload); map event.userId→payload.user.id; fix SendResult shape (status/errorCode→ok/error); fix EmailMessage.purpose→flow; default expiresAt (1h) and when (Date.now()) for fields not in payloads - on-cron-tick: fix arg order (ctx,event)→(event,ctx); replace typed import with structural { env? } shape; guard ctx.env possibly undefined - routes/admin: fix email-service-singleton path (add email/ subdir); replace permissions.hasRole('admin') with user.role check; adapt send() call to our EmailMessage shape; fix result.ok check - services/site-config: remove getCategorySettings() (not in our SettingsService); use env.PUBLIC_URL directly - sdk/types: add EmailService/SendEmailOptions/SendEmailResult types for the plugin's CF Email Service EmailServiceImpl - core-plugins/index: re-export emailPluginV3 as emailPlugin for compat - app.ts: import emailPluginV3 as emailPlugin No behavior change for Resend/SendGrid deployments. CF Email Service path requires EMAIL + CF_ZONE_ID bindings (unchanged from mmcintosh design). * feat(email): provider selector — Resend or CF Email Service, env var overrides - Add `provider` field to EmailSettings (resend | cloudflare) with per-provider conditional fields in the admin UI (Resend API key vs CF Account ID / token) - Amber env var override notes on every overrideable field in settings UI - Wire CloudflareEmailProvider into app.ts initEmailService — detects real CF binding (object) vs string var, reads new resendApiKey alongside legacy apiKey - Register email_log document type in onBoot to satisfy FK constraint on send - Fix EmailSettingsService.load() to catch D1 errors (table missing pre-migration) - Surface real CF error messages in test email UI instead of generic fallback - Fix adminMenu path (/admin/plugins/email) and regenerate manifest-registry - wrangler.toml: add [[send_email]] binding with remote = true Co-Authored-By: MarkMac <mmcintosh@infowall.com> Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: MarkMac <mmcintosh@infowall.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs(www): update all docs and blog posts from v2 to v3
Comprehensive audit and update of the SonicJS documentation website
covering 63 files across doc pages, blog tutorials, guides, and comparisons.
Key changes:
- Delete 8 orphan "Protocol" chat-template pages (groups, messages, contacts,
conversations, errors, attachments, sdks, pagination)
- Global: scaffold cmd → npx create-sonicjs@latest; /api/content/<x> →
/api/<x>; R2 BUCKET→MEDIA_BUCKET; db:migrate:prod→db:migrate; demo creds
- PluginBuilder.create/addRoute/addHook/lifecycle/build → definePlugin() +
registerPlugins() across all affected pages and posts
- defineCollection({fields}) → CollectionConfig with schema.properties
- Database page: Drizzle framed as content layer → Document Model (5 tables,
q_* generated columns, 2 migrations); remove users/collections/content tables
- Collections page: delete all UI-creation content; code-only via
registerCollections(); /admin/collections read-only
- Plugins/development guide: full rewrite from PluginBuilder/manifest.json
to definePlugin SDK (register, menu, onBoot, capabilities, document_type)
- Blog guides: plugins-extending full rewrite; plugin-architecture and
d1-database-deep-dive premise-level corrections; getting-started, building-
a-blog, building-rest-api, deploy, file-uploads blockers fixed
- Changelog/home: v3.0.0-beta entries added; LATEST badge moved to v3
- Roadmap: shipped features moved (multi-tenant, 2FA, Lexical, definePlugin)
- Auth: JWT sections preserved (hybrid JWT+Better Auth is real); only fix
wrong demo creds, add author role, fix createSonicJS→registerPlugins
- Comparison posts: PluginBuilder blocks, stale API paths, dated roadmap
quarters, "multi-tenant still building" all corrected
- Tier 3: security X-Frame-Options→SAMEORIGIN; telemetry endpoint/opt-out;
field-types document model note; workflow/webhooks experimental banners;
forms contentId→null; ai-agents duplicate block removed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs(www): resolve VERIFY queue + fix hook names and config interface
- Fix wrong hook event names: content:beforeSave/afterSave → content:save,
content:afterPublish → content:publish, content:beforeReturn → content:read
(verified against HOOKS constant in mogadishu-v4 packages/core)
- Fix SonicJSConfig.plugins interface: directory/autoLoad → register:Plugin[]
(the real v3 field; directory/autoLoad were from old main-branch interface)
- Restore adminAccessRoles to SonicJSConfig (real field, was incorrectly removed)
- Fix plugins/core auth config: remove fictional tokenExpiration/refreshExpiration
settings; clarify env-var-based config + no separate refreshToken in response
- Fix defineCollection → CollectionConfig in field-types, directus-vs-payload,
strapi-vs-contentful, best-open-source-practice blog posts
- Fix coding-standards: add Document Model vs Drizzle boundary note
- Fix templating: annotate admin-layout-v2 as legacy, docs-layout as non-existent
- Fix plugins/core: auth plugin config block updated to v3 env-var pattern
- Remove resolved TODO comment in caching strategy guide
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…del (#901) plugin_activity_log table was defined in schema.ts but never created in any migration, so all logActivity() inserts silently failed (try/catch) and the Activity Log tab was always empty. - Register plugin_activity document type in bootstrapDocumentTypes with q_plugin_activity_plugin_id and q_plugin_activity_action queryable fields - logActivity() now inserts into documents table (type_id=plugin_activity) - getPluginActivity() queries documents via json_extract(data, '$.pluginId') - Fix route mapping: derive message from details JSON, pass userId not user_email - Guard item.message in template to prevent "undefined" text rendering - Add E2E spec 68 for plugin activity log tab Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Dead code — plugin had no runtime behavior (commented-out routes, console.log-only activate/deactivate). The static component showcase page served no purpose that reading existing template files doesn't already cover. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace analytics_events table with analytics_event document type. EventTrackingService now writes via DocumentsService.create() and queries raw SQL against documents table. Remove hardcoded stub routes. Bootstrap registers analytics_event type with q_evt_* generated columns. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
#904) Database Tools functionality is built into core — the plugin entry had is_core: false, causing it to appear in the Plugins tab instead of the System tab. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Legacy feature table, not document model — violates v3 architecture. No active users, no tests, no migration path planned.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove seed data generator plugin: delete plugin directory, drop /admin/seed-data route from app.ts, remove export and ID from core-plugins index, remove isSeedDataPlugin UI block from admin plugin settings template. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…el (#908) - Gate "Profile Information" on edit/new user forms behind both defineUserProfile() being called AND plugin being active in DB - Remove hardcoded bio field from new user form; bio now belongs in the profile model if the developer wants it - Plugin detail page explains code-defined data model pattern, points dev to my-sonicjs-app/src/user-profile.model.ts with defineUserProfile() example and live configured-fields table - Add user-profile.model.ts stub to starter app (commented out by default); index.ts imports it as the canonical edit point - Remove legacy my-sonicjs-app Drizzle user_profiles table schema and CRUD route (conflicts with document model; was dead code) - New user form falls back to showing all profile fields when registrationFields not specified (matches edit form behavior) Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace legacy security_events table with documents repository. - Rewrite SecurityAuditService to write via DocumentsService and query documents table using q_sa_* virtual generated columns - Register security_event document type in bootstrapDocumentTypes with 6 queryable fields (event_type, severity, user_id, email, ip_address, blocked) - Fix pre-existing routing bug: mount /api/security-audit and /admin/plugins/security-audit routes before generic /api/:collection/:id catch-all to prevent 404s on plugin-specific API endpoints - Export admin/api route handlers from plugin index - Add E2E spec 80 covering dashboard, event log, API stats/events, settings Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Mark workflow-plugin as hidden in manifest.json so the auto-generator skips it. Routes are dead code; plugin is not on the critical path for the document model migration. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(content-form): prevent nested form UI on HTMX validation errors
Validation error responses returned the full admin layout page, which
HTMX swapped into #form-messages — causing the entire page to render
inside itself. Fix uses HX-Retarget + HX-Reswap headers to replace
#content-form-page (the form content wrapper) with a partial response
that omits the layout. Also removes the duplicate #form-messages div
inside the form (invalid HTML).
Affected paths: POST /admin/content, PUT /admin/content/:id (both
document-backed and legacy variants).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(setup): local-only fallback when Cloudflare auth unavailable
setup-worktree-db.sh now detects missing Cloudflare credentials via
`wrangler whoami` and falls back to a local-only path: skips remote D1
create/migrate, applies migrations with --local, and seeds admin via
seed-local.mjs (better-sqlite3 + node:crypto, no getPlatformProxy).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(auth): assign RBAC role on registration to allow admin access
New users registered via the sign-up form got `role='admin'` in
`auth_user` but no `rbac_user_roles` document, so `requireRbac('portal',
'access')` returned false and redirected with "You do not have
permission to access this area".
Add `RbacService.addUserRoleByName(userId, role)` immediately after
user creation in the registration handler, matching the pattern already
used by the seed endpoint.
Also renames root `workspace` script → `reset` and adds `--local` flag
to `wrangler dev` for offline-only setups.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace legacy `redirects`/`redirect_analytics` tables with the document
model. All redirect data now lives in `documents` (type_id='redirect'),
with `q_redir_*` virtual columns for source, destination, status_code,
is_active, and match_type.
- RedirectService rewritten to query documents table; hit analytics
stored as hitCount/lastHitAt in document data JSON
- Middleware recordHitAsync and warmRedirectCache updated to use documents
- Plugin onBoot registers 'redirect' document type via DocumentTypeRegistry
(auto-creates virtual columns via ensureScalarSchema)
- Mount redirect middleware (app.use('*')) so HTTP redirects actually fire
- Add duplicate source URL check in create()
- Export redirectPlugin from core and register in app entry point
- Add E2E spec (80-redirect-management.spec.ts): 7 tests covering page
load, create, HTTP redirect firing, edit, delete, and duplicate error
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(versioning): make versioning opt-in per document type Document types default to edit-in-place (no history rows). Set `settings.versioning: true` or `CollectionConfig.versioning: true` to opt in to version history and restore. - saveDraft skips version row creation when versioning=false - Versioning plugin routes 404 for non-opted-in types - Content/documents forms show single "Update" button when versioning off - Collections admin lists Versioning column; drops Source and Created - blog_post defaults versioning off - faq collection added as versioning-on example Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(ui): constrain date picker width and improve dark mode icon - Date/datetime inputs get !w-auto to override w-full — sizes to content - dark:[color-scheme:dark] makes native calendar icon render white on dark bg - Versioning column badges match content status style (green/zinc ring pills) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore(collections): remove faq example, simplify blog-posts schema faq collection served as versioning demo — removed since opt-in is now documented via settings.versioning. blog-posts trimmed to core fields (excerpt, featuredImage, status fields dropped). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
- Flip admin-media and api-media reads/writes to document-primary via MediaDocumentService - Register media_asset document type in bootstrapDocumentTypes (seed) - Add getByRootId, softDeleteRoot, updateMetadata, search to MediaDocumentService - Legacy media table writes wrapped in non-fatal try/catch (R12 compat) - doc.rootId is canonical file id; stored as media.id for dual-write - Rewrite admin-media and api-media integration tests to assert on documents table - Add E2E spec 81-media-document-model.spec.ts - Copy URL button label → "Copy Relative URL" Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
… opt-in (#916) * fix(api): gate collection in meta behind ?include=collection Reduces default payload size; callers opt in via ?include=collection. Cache key updated to prevent cross-contamination between variants. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: merge origin/v3 (versioning opt-in, simplified blog-posts, media document model) --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…diness) (#894) * fix(v3): restore test gate + fix document-model regressions for prod readiness Re-enable the disabled test suite and fix the bugs it was hiding across the content, media, auth, and settings paths of the v3 document model. Test gate: - Un-stub packages/core test scripts (real vitest); CI re-enables type-check + triggers on v3 - Fix the real-DB harness (d1-sqlite applyScalarSchema) and migrate tests to the code-only collection registry / current APIs -> 1530 unit tests green, 0 failed Runtime bugs fixed (all live-verified): - test-cleanup: stop querying the dropped `collections` table; guard legacy tables; fail-closed environment gate - /api/documents: register before the /api/:collection wildcard that shadowed it (was 404) - /api OpenAPI: report the real package version (was hardcoded 0.1.0) - admin content: stop leaking internal/auth document types into the content-model filter dropdown and the new-content collection selector - media: flip api-media + admin-media to the document model (media_asset) so greenfield media upload/library/delete no longer 500; seed the media_asset document type (was missing -> FK fail) - auth: isRegistrationEnabled reads the plugin document instead of the dropped `plugins` table, so "disable registration" is actually enforced (was a silent no-op) E2E: - Fix loginAsAdmin (Better Auth Origin) and the createTestContent helper hang (collections are code-only); refresh stale specs (03 dashboard -> content/analytics, 05 content, 23 content-api-crud auth, 68 profile) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(v3): QA prod-readiness — auth, dashboard, E2E test fixes Core fixes: - auth.ts: delegate POST /auth/login to Better Auth sign-in/email - admin-users.ts: redirect /admin → /admin/dashboard (was /admin/content) - app.ts: mount adminDashboardRoutes; move registerPluginRoutes(testimonials) before app.route('/api') so /:collection catch-all doesn't shadow it - csrf.ts: exempt /test-seed-defaults from CSRF check - test-cleanup.ts: add POST /test-seed-defaults for E2E global-setup New: - page-blocks.collection.ts: code-only collection for TDZ/blocks E2E tests - my-sonicjs-app/src/index.ts: register pageBlocksCollection E2E fixes: - global-setup.ts: call /test-seed-defaults after cleanup to restore welcome post - 05-content: use form#content-form (strict mode fix — 3 forms on page) - 27-settings-general: button text is "Save Changes" not "Save All Changes" - 39-slug-generation: set lexical hidden input via page.evaluate() (not textarea) - 41-reference-fields: skip (field editor removed for code-only collections) - 42-dynamic-field-tdz: fix seoTitleInput selector (skip hidden type="hidden"); use toHaveCount for hidden inputs; text block now has heading+body fields - 65-default-blog-post-seed: handled by test-seed-defaults - 66-auth-better-auth: fix seed-admin assertion (body.users not body.user) - 71-home-link-redirect: update URL expectation + skip catalyst nav tests Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
) High-level feasibility/effort analysis for running SonicJS outside Cloudflare. Key findings: no Durable Objects in use, D1->SQLite shim already exists as a test util, tiny R2/Queue surface. Tiered recommendation (Docker/SQLite -> Turso -> Postgres) with the deployment targets each tier unlocks. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Merges the v3 branch into main, replacing the 2.x codebase with the document model architecture.
What's changing
content,media,testimonials, etc.)document_types,documents,document_references,document_facets,document_permissionsMediaDocumentServiceVersion
3.0.0-beta.2— publishes underbetadist-tag on npm, notlatest. Existingcreate-sonicjsusers on 2.x are unaffected.Pre-merge checklist
#915main-only fix already present in v3#898docs fix merged to v3#891,#893bugs tracked as v3 issues#892closed (not applicable to v3)