# `plugin/plugin.ts` _(no submodules)_ ## Purpose Owns `createSitePlugin`, the canonical factory that produces the `PluginDescriptor` consumed by `@suaveplan/api-plugin`'s `PluginRegistry`. The factory composes every other module in this package — `SitePluginOptionsSchema` validates options at the boundary, the `SiteService` + `DomainResolverService` are constructed once, the Hono middleware is built and exposed through `ServiceRegistry`, the public + private route trees are mounted under the configured `routePrefix`, and the optional `organization:created` listener is wired when `autoCreateDefaultSite` is on. This file is the single integration point — every consumer of the package builds its descriptor through this one function. The lifecycle is the canonical 4-phase shape: validate-at-construction, build-services in `onInit`, mount-routes in `onReady`, drop-listener in `onClose`. Each phase fails fast with a typed `SiteError` carrying a `SITE.*` code so misconfigurations are visible at registry intake rather than deferred to the first request. ## Features - Zod-validated option boundary — `SITE.INVALID_OPTIONS` on bad config. - Resolves `db` from `ServiceRegistry`; throws `SITE.SERVICE_NOT_AVAILABLE` when missing. - Constructs and registers `siteService`, `domainResolver`, `domainResolverMiddleware`. - Optional `organization:created` listener for default-site auto-bootstrap, with `SITE.BOOTSTRAP_FAILED` event emission on error and the `site.bootstrap_failed_total` counter incremented on each failure. - Route mounting under `config.routePrefix` (defaults to `/api`) with prefix-collision detection via `SITE.ROUTE_PREFIX_COLLISION`. - `optionalDependencies: ["database", "auth", "events"]` so orchestrators topo-sort the site plugin after siblings without making them mandatory. - Emits `site:initialized` and `site:ready` lifecycle events. ## Usage: Basic ```ts import { createSitePlugin } from "@suaveplan/api-plugin-site"; registry.register(createSitePlugin({ maxSitesPerOrg: 25 })); ``` ## Usage: Advanced ```ts import { createSitePlugin } from "@suaveplan/api-plugin-site"; const plugin = createSitePlugin({ routePrefix: "/v1", autoCreateDefaultSite: true, defaultDomainPattern: "{slug}.acme.com", enableDomainResolution: true, }); await registry.register(plugin, { db: surrealAdapter, app: openApiHono }); await registry.start(); // Downstream plugin can now resolve the service: const siteService = ctx.getService("siteService")!; await siteService.create({ organization_id: "organization:abc", name: "Customer Site", domains: ["customer.acme.com"], }); ``` ## API Reference - `createSitePlugin(options?: CreateSitePluginOptions): PluginDescriptor` - `type CreateSitePluginOptions = SitePluginOptionsInput` - The descriptor name is `"site"`, version `"0.1.0"`. ## Implementation Notes - Service handles are stored in closure-scoped variables and bound via a private `bind` helper so the `onReady` hook can reuse the same instances constructed in `onInit`. The `mustService` / `mustResolver` guards trip when an orchestrator runs `onReady` before `onInit` (which the canonical registry never does, but tests / misuse can). - The plugin treats the host event bus as `EventBusLike` so the cumulative event typing is enforced inside the plugin without bleeding into the global `EventBusLike` declaration on the host. - Route mounting is keyed by detecting any existing path starting with `${prefix}/sites/`; a collision raises `SITE.ROUTE_PREFIX_COLLISION` so two plugins can't silently overwrite each other. - `enableDomainResolution: false` suppresses ONLY the public router (which contains the resolve endpoint); the private router always mounts so CRUD remains usable even when public lookup is disabled.