/*
Integrations schema for the system manifest: vendor adapter bindings and integration configuration.
- Do not introduce app-specific runtime composition or deployment behavior into this reusable package source file.
- RFC-0303 Phase 3: extracted from schemas/system.ts as part of the domain split.
*/
import { z } from "zod";
/**
* The `integrations:` block in system.yaml (engineering-owned). Declares which
* outbound channel adapters and (optionally) which CRM adapter this app activates
* through the Integration Port (RFC-0168).
*
* Adapter ids are validated against the closed adapter catalog in
* @warpgogol/werkstatt-site/integration by `integration.config.validate`; each configured
* adapter's required secret names are validated against the generated env schema
* by `integration.secrets.validate`.
*
* When the block is absent, channels self-enable purely by secret presence at
* runtime (back-compat) and the validators are a no-op pass.
*
* Example:
* integrations:
* channels:
* - adapter: telegram
* - adapter: email
* options: { to: "info@example.org" }
* crm:
* adapter: pipedrive
*/
export const systemIntegrationsSchema = z.object({
/** Outbound channel adapter bindings (telegram | email | whatsapp | null). */
channels: z
.array(
z.object({
adapter: z.string().min(1),
options: z.record(z.string(), z.string()).optional(),
}),
)
.optional()
.default([]),
/** Optional CRM adapter binding (pipedrive | null). */
crm: z
.object({
adapter: z.string().min(1),
options: z.record(z.string(), z.string()).optional(),
})
.optional(),
/**
* Optional consent-gated chat widget binding (uchat | null) — RFC-0175.
* `options` carries PUBLIC vendor values only (e.g. uchat needs widgetId or a
* full scriptUrl). Validated by `chat.config.validate`; gated by the
* `integrations.chat` entitlement (RFC-0169). The widget is click-to-load: no
* third-party script/storage exists before the visitor activates the launcher.
*/
chat: z
.object({
adapter: z.string().min(1),
options: z.record(z.string(), z.string()).optional(),
})
.optional(),
/**
* RFC-0176: out-of-process source bindings. When a source is enabled (e.g.
* `uchat`), the inbound route `/api/integration-inbound` is generated and
* requires `INTEGRATION_INBOUND_SECRET` to authenticate posts.
*/
inbound: z
.object({
sources: z.array(z.string().min(1)).default([]),
})
.optional(),
/**
* RFC-0176: source-agnostic destination hub. Each destination declares a kind,
* a vendor, and an execution mode. `gogol-adapter` (default) runs on the
* client's site with the client's tokens; `vendor-native` is executed by an
* upstream vendor. Validated by `integration.config.validate`.
*/
destinations: z
.array(
z.object({
kind: z.string().min(1),
vendor: z.string().min(1),
mode: z.enum(["gogol-adapter", "vendor-native"]).default("gogol-adapter"),
}),
)
.optional()
.default([]),
/**
* RFC-0179: the site's placement on the shared, sharded delivery backbone.
* `region` pins the site's events to a jurisdiction's queue/dedup shard (data
* residency); `tier` is `shared` (the low-volume long tail, hash-fanned across
* the region's shared pool) or `dedicated` (a noisy/high-volume client isolated
* onto its own queue). Both default so existing apps need no edit. Validated by
* `integration.config.validate`; consumed by `integration.infrastructure.generate`
* (RFC-0180) to resolve the `ShardAssignment`.
*/
region: z.enum(["eu", "us"]).optional().default("eu"),
tier: z.enum(["shared", "dedicated"]).optional().default("shared"),
/**
* RFC-0181: EU-resident delivery substrate. `provider: upstash` routes leads via
* Upstash QStash (EU, eu-central-1) with an Upstash Redis (EU) idempotency ledger
* — Cloudflare Queues/KV cannot be EU-pinned. `region: eu` keeps lead PII physically
* in the EU. Validated by `integration.config.validate`.
*/
delivery: z
.object({
provider: z.enum(["upstash"]).default("upstash"),
region: z.enum(["eu"]).default("eu"),
})
.optional(),
/**
* RFC-0188: Visitor Sales Funnel binding. The PLATFORM owns the stage/event/
* transition graph (@warpgogol/werkstatt-site/integration funnel.ts); this block only declares
* which canonical funnel `version` the app renders, which event `sources` feed it
* (e.g. uchat, stripe, operator), and whether the pilot is `enabled`. UChat is the
* conversation runtime — it renders the funnel and requests transitions; it never
* owns the graph, pricing, or canonical state. Make.com is never a source. Validated
* by `funnel.contract.validate` / `funnel.stage.validate` / `funnel.crm.validate`.
* Absent ⇒ the funnel validators are a no-op pass (funnel not yet enabled for the app).
*/
funnel: z
.object({
version: z.string().min(1),
sources: z.array(z.string().min(1)).optional().default([]),
enabled: z.boolean().optional().default(false),
})
.optional(),
});
export type SystemIntegrations = z.infer;