Files
suaveplan/openspec/archive/2026-05-19-add-api-plugin-site/proposal.md
2026-05-27 23:29:53 +01:00

8.8 KiB

Add @suaveplan/api-plugin-site

Package: @suaveplan/api-plugin-site Status: PROPOSED Type: new-package (migration from suaveplan)

  • Spec delta: ./specs/api-plugin-site/spec.md

Why

Genesis needs the multi-site / multi-domain capability @suaveplan/api-plugin-site@1.0.21 ships upstream so consumer apps can persist site records (name / slug / theme / settings / push) under SurrealDB, resolve incoming Host headers to a site through a single canonical service, and expose public + private OpenAPI Hono routes for site management. The upstream package additionally bootstraps a default site on organization:created and emits site:settings:updated whenever settings change so CDNs and downstream consumers can react.

The upstream library exists at /mnt/s/web/suaveplan/packages/server/api-plugin-site (11 src files, partial test coverage, three as any casts in plugin.ts, runtime-monkey-patched updateSettings, missing-domain bug in addDomain, no OTel surface, no typed error registry). This change lands a single, enterprise-grade @suaveplan/api-plugin-site@0.1.0 that fixes every flagged smell from the upstream enterprise audit (/mnt/s/web/suaveplan/enterprise/api-plugin-site.md) at parity with the upstream feature surface.

What changes

  • ADDED: SITE_CODES / SiteError — defineCodes("SITE", […]) registry of 14 codes (INVALID_OPTIONS, SERVICE_NOT_AVAILABLE, SITE_NOT_FOUND, SITE_LIMIT_EXCEEDED, DOMAIN_LIMIT_EXCEEDED, DOMAIN_ALREADY_CLAIMED, LAST_DOMAIN_REMOVAL, DOMAIN_NOT_FOUND, INVALID_DOMAIN, VALIDATION, BOOTSTRAP_FAILED, STORAGE_ERROR, ROUTE_PREFIX_COLLISION, RESOLUTION_FAILED) extending SuaveplanError.
  • ADDED: SiteSchema, CreateSiteSchema, UpdateSiteSchema (.strict), SitePublicInfoSchema, SiteIdParamSchema, SiteSettingsSchema, SiteThemeSchema, SiteStateNumberSchema — inlined Zod surface for the site entity.
  • ADDED: DomainSchema, AddDomainRequestSchema/ResponseSchema, RemoveDomainRequestSchema/ResponseSchema, DomainResolveParamSchema/ResponseSchema — inlined Zod surface for the domain entity.
  • ADDED: ApplePushConfigSchema, FirebasePushConfigSchema, PushConfigSchema, PushConfigValidationResponseSchema, validatePushConfig — push-config schema + pure validator mirroring the SurrealDB fn::site::push::validate.
  • ADDED: SitePluginOptionsSchema, SitePluginConfig, SitePluginOptionsInput, DEFAULT_CONFIG, mergeConfig — Zod-validated boundary for plugin options with frozen defaults.
  • ADDED: normalizeDomain — pure, mirroring fn::site::domain::normalize.
  • ADDED: SiteDb — structural db port consumed by services (positional-bind query + typed create / update).
  • ADDED: SiteService — canonical CRUD: create, createDefaultSite, getById, requireById, listByOrganization, countByOrganization, update, updateSettings, updateState, addDomain, removeDomain, archive, getActive, search. Emits site:settings:updated natively (no monkey-patching). Caps enforced via plugin config. Bugfix: addDomain now persists the new domain (upstream produced an unused local).
  • ADDED: DomainResolverService — resolve envelope, isDomainTaken, resolveOrThrow. Wraps SurrealDB fn::domain::search. Emits OTel counter + duration histogram.
  • ADDED: createDomainResolverMiddleware — Hono middleware factory injecting site / organization / siteId / organizationId into request context.
  • ADDED: createPublicSiteRoutes — GET /sites/resolve/{domain}, GET /sites/{id}/public.
  • ADDED: createPrivateSiteRoutes — POST /sites, PATCH /sites/{id}, POST /sites/{id}/domains, DELETE /sites/{id}/domains.
  • ADDED: createSitePlugin — definePlugin<SitePluginConfig> factory wrapping the genesis PluginDescriptor with the canonical 4-phase lifecycle. Subscribes to organization:created when autoCreateDefaultSite is on, emits site:bootstrap:failed on failure, registers three services (siteService, domainResolver, domainResolverMiddleware), mounts route trees under configurable routePrefix.
  • ADDED: getSiteInstruments / resetSiteInstruments / SiteInstruments — OTel bundle (5 instruments) with cache reset for test rebinding.
  • ADDED: services/registry-augmentation.ts — declares siteService, domainResolver, domainResolverMiddleware, app, db on ServiceRegistry.
  • ADDED: surql/tables/site.surql — per-table migration with state constants, indices, and the fn::domain::search function.

Source mapping

Upstream source Genesis location Notes
src/index.ts src/index.ts Re-exports broadened to expose every documented symbol.
src/plugin.ts src/plugin/plugin.ts Rewritten with definePlugin<SitePluginConfig>() from @suaveplan/api-plugin; every as any removed; monkey-patch dropped; site:bootstrap:failed event added.
src/config.ts src/schemas/options.ts Switched from hand-rolled mergeConfig to Zod-validated boundary; DEFAULT_CONFIG frozen.
src/middleware/domain-resolver.middleware.ts src/middleware/domain-resolver.ts Uses normalizeDomain before resolve.
src/routes/public.routes.ts src/routes/public-routes.ts OpenAPI route definitions preserved; sanitised public projection.
src/routes/private.routes.ts src/routes/private-routes.ts Adds DELETE /sites/{id}/domains; bodies Zod-validated through @hono/zod-openapi.
src/services/site.service.ts src/service/site.ts addDomain persistence bug fixed; event emit moved into the service; OTel counters added; structural SiteDb port.
src/services/domain-resolver.service.ts src/service/domain-resolver.ts OTel counter + histogram; codes embedded in result envelope; resolveOrThrow helper.
src/types/site.types.ts src/schemas/site.ts + src/schemas/domain.ts + src/schemas/push.ts + src/types/types.ts Type-only file split into Zod schemas (per-feature) and runtime-free types.
surql/tables/site.surql surql/tables/site.surql Trimmed fn::site::* helpers that the application-tier now owns (normalizeDomain, createDefaultSite) — covered by ### Excluded.

Excluded from this migration

The genesis 0.1.0 ships at functional parity with the upstream public surface (services + routes + middleware + plugin + migration). The §3 enterprise-doc enhancements that depend on packages not yet landed on genesis are scoped out with concrete justifications — NOT silently dropped:

  • fn::site::* SurQL helper suite (e.g. fn::site::create, fn::site::push::validate, fn::site::settings::update) — application-tier SiteService now owns every call site. Keeping those helpers in the migration would duplicate logic between two writers and contradict §0a's "no implicit drops"; the application tier IS the canonical writer in genesis (matching the recent api-plugin-import / api-plugin-search precedent). The migration ships only fn::domain::search because the resolver still delegates pattern matching to the database.
  • JWT / permissions / rate-limit / audit middleware wiring — depends on @suaveplan/api-plugin-rate-limit + @suaveplan/api-plugin-audit already landed in genesis, but the integration knob is the host app's app.use(prefix, middleware) Hono pattern. The site plugin does NOT hard-depend on them so excluding integration from THIS migration does not block consumers from layering them on top.
  • ACME / Let's Encrypt automation, edge-CDN cache busting, sitemap auto-gen, theme draft/publish workflow — every item is a §4 future enhancement, not a §2 current-state capability. None are part of the upstream 1.0.21 shipped feature surface.
  • Benchmarks under src/__benchmarks__/ — the monorepo benchmark wave is a separate gate; the site plugin will receive its mitata harness in that wave.

Migration

No external consumers exist yet (genesis-only). For internal consumers:

  • Old: import { createSitePlugin } from "@suaveplan/api-plugin-site" from the upstream 1.0.x line.
  • New: same import, but the descriptor's lifecycle hooks now use the genesis PluginContext (typed getOwnConfig, augmented ServiceRegistry["siteService"|"domainResolver"|"domainResolverMiddleware"]).
  • Event-bus events are now strongly typed via SiteEventMap; consumers that wrote bus.on("site:settings:updated" as any, ...) should drop the cast.
  • The addDomain bug-fix means a site's domains array is actually updated after a successful call. Consumers that worked around the bug by manually rewriting the row should remove the workaround.

Impact

Affected dependents

bunx turbo ls --filter=...@suaveplan/api-plugin-site enumerates zero dependents on genesis today (this is the first landing). The package will be picked up by future genesis-original packages (e.g. api host orchestrator). No dependent sweep required for this change.