@clipform/mcp-server 2.15.12 → 2.16.0

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.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../config/features.js","../../config/node-types.js","../../config/form-types.js","../../config/help-links.js","../../config/logic-types.js","../../config/video-templates.js","../../config/logic-chain.js","../../config/index.js","../src/lib/caps.ts","../src/lib/api-client.ts","../src/lib/session-context.ts"],"sourcesContent":["/**\n * Feature registry - single source of truth for what Clipform can and cannot do.\n *\n * enabled: runtime gate - does the dashboard/viewer expose this feature?\n * status:\n * 'live' - shipped and working in production\n * 'beta' - available but not fully polished\n * 'planned' - on the roadmap, not yet usable\n *\n * Rules:\n * - Marketing copy, blog posts, and landing pages must only claim features with status 'live'\n * - 'planned' features can be mentioned as \"coming soon\" but never as current capability\n * - When a feature ships: set enabled to true, status to 'live', and stamp wentLiveAt\n * - Everything else derives from this file (PRODUCT.features, FEATURE_FLAGS, content claims)\n *\n * wentLiveAt (optional, 'YYYY-MM-DD'): the date a feature flipped planned -> live. It is\n * the dated capability event the What's New changelog renders (getWhatsNew) and the SEO/GTM\n * skills key off (\"live-but-uncovered -> content backlog\"). Best-effort: features that were\n * already live at this registry's baseline have no known date and stay undated - they're\n * simply absent from the dated timeline until backfilled, never stamped with a wrong date.\n *\n * @type {Record<string, { name: string, description: string, enabled: boolean, status: 'live' | 'beta' | 'planned', category: string, wentLiveAt?: string }>}\n */\nexport const FEATURES = {\n // -- Builder --\n visual_form_builder: {\n name: 'Visual form builder',\n description: 'Drag-and-drop node-based form builder powered by React Flow',\n enabled: true,\n status: 'live',\n category: 'builder',\n },\n form_themes: {\n name: 'Form themes',\n description: 'Customise primary colour, background colour, and font',\n enabled: true,\n status: 'live',\n category: 'builder',\n },\n media_prompts: {\n name: 'Media prompts',\n description: 'Attach video, audio, or images to any question node',\n enabled: true,\n status: 'live',\n category: 'builder',\n },\n consent_documents: {\n name: 'Agreement documents',\n description: 'Attach a PDF agreement to a consent item, read inline in the viewer with scroll-to-accept',\n enabled: false,\n status: 'beta',\n category: 'builder',\n },\n node_logic: {\n name: 'Node routing logic',\n description: 'Define the order nodes appear in - currently supports linear paths (A to B to C)',\n enabled: true,\n status: 'live',\n category: 'builder',\n },\n branching: {\n name: 'Conditional branching',\n description: 'Choice nodes route to different destinations based on which option is picked',\n enabled: true,\n status: 'live',\n wentLiveAt: '2026-06-25',\n category: 'builder',\n },\n funnel: {\n name: 'Lead qualification funnels',\n description: 'Multi-step funnels with conditional routing built on branching',\n enabled: false,\n status: 'planned',\n category: 'builder',\n },\n custom_scores: {\n name: 'Custom scoring',\n description: 'Assign scores to choice options for quizzes and assessments',\n enabled: false,\n status: 'planned',\n category: 'builder',\n },\n text_response: {\n name: 'Text response option',\n description: 'Allow free-text input alongside choice options',\n enabled: false,\n status: 'planned',\n category: 'builder',\n },\n multi_select: {\n name: 'Multi-select choices',\n description: 'Pick-all-that-apply choice questions (checkbox-style selection)',\n enabled: false,\n status: 'planned',\n category: 'builder',\n },\n custom_fonts: {\n name: 'Custom fonts',\n description: 'Upload and use custom fonts in forms',\n enabled: false,\n status: 'planned',\n category: 'builder',\n },\n brand_name: {\n name: 'Custom brand name',\n description: 'Replace Clipform branding with your own brand name',\n enabled: false,\n status: 'planned',\n category: 'builder',\n },\n allow_resume: {\n name: 'Session resume',\n description: 'Respondents can continue a form where they left off',\n enabled: true,\n status: 'live',\n category: 'builder',\n },\n anonymous_builder: {\n name: 'Anonymous builder',\n description: 'Try the builder without an account via /try - an anonymous Supabase session, converted to a real account at publish time',\n // Desktop v1 (#2979) shipped flag-dark: the entry route, publish wall, and\n // conversion modal all land behind this flag. Enabled at launch rollout -\n // the prod Supabase Anonymous Sign-Ins toggle must stay on for /try guest\n // sessions to mint (dev first, then prod after #3016).\n enabled: true,\n status: 'beta',\n category: 'builder',\n },\n\n // -- Node types --\n node_start: {\n name: 'Start node',\n description: 'Entry point for every form',\n enabled: true,\n status: 'live',\n category: 'node-types',\n },\n node_choice: {\n name: 'Multiple choice',\n description: 'Single-select questions with configurable options (multi-select is gated by the multi_select feature)',\n enabled: true,\n status: 'live',\n category: 'node-types',\n },\n node_open: {\n name: 'Open-ended questions',\n description: 'Free-form responses via text, audio, or video',\n enabled: true,\n status: 'live',\n category: 'node-types',\n },\n node_details: {\n name: 'Details',\n description: 'Structured fields (name, email, phone, company) with consent checkboxes',\n enabled: true,\n status: 'live',\n category: 'node-types',\n },\n node_button: {\n name: 'Button',\n description: 'Simple navigation or statement node',\n enabled: true,\n status: 'live',\n category: 'node-types',\n },\n node_redirect: {\n name: 'Redirect',\n description: 'Terminal node that sends respondents to an external URL',\n enabled: true,\n status: 'live',\n category: 'node-types',\n },\n node_end_screen: {\n name: 'End screen',\n description: 'Configurable thank-you screen with messaging, CTAs, and share button',\n enabled: true,\n status: 'live',\n category: 'node-types',\n },\n node_scale: {\n name: 'Scale / rating',\n description: 'Numerical rating questions (1-10, stars, etc.)',\n enabled: false,\n status: 'planned',\n category: 'node-types',\n },\n node_file_upload: {\n name: 'File upload',\n description: 'Collect files from respondents',\n enabled: false,\n status: 'planned',\n category: 'node-types',\n },\n node_booking: {\n name: 'Booking',\n description: 'Calendly or Google Meet scheduling embedded in the form',\n enabled: false,\n status: 'planned',\n category: 'node-types',\n },\n node_draw: {\n name: 'Drawing',\n description: 'Drawing canvas; respondents draw and it is saved as a PNG image',\n // Went GA 2026-06-27 (#1012), pulled back behind the allowlist 2026-07-20\n // (#2289) while pen options / match-to-shape mature. Prod usage at\n // pull-back verified = Clipform Public only, zero external customers.\n // Existing nodes still render (creation-gated only, camera precedent).\n enabled: false,\n status: 'beta',\n wentLiveAt: '2026-06-27',\n category: 'node-types',\n },\n node_camera: {\n name: 'Camera',\n description: 'Capture a photo from the device camera',\n // Scaffolded disabled (#2097): code lands ready, but node.type's DB check\n // constraint doesn't accept 'camera' yet - that's a separate parked\n // holds-migration PR. Flip once the migration has merged (= applied to\n // prod) and the apps have deployed.\n enabled: false,\n status: 'planned',\n category: 'node-types',\n },\n node_conditional: {\n name: 'Conditional',\n description: 'Route the flow to different branches based on Score',\n // Scaffolded disabled (#4105, phase 1 of the #4104 variables engine): code\n // lands ready, but node.type's DB check constraint doesn't accept\n // 'conditional' yet - that's a separate parked holds-migration PR. Flip\n // once the migration has merged (= applied to prod) and the apps have\n // deployed (camera precedent, #2097).\n enabled: false,\n status: 'planned',\n category: 'node-types',\n },\n node_payment: {\n name: 'Payment',\n description: 'Collect payments via Stripe or Paddle inline',\n enabled: true,\n status: 'live',\n wentLiveAt: '2026-09-07',\n category: 'node-types',\n },\n node_binary: {\n name: 'Binary choice',\n description: 'Two-option choice node (yes/no, this/that)',\n enabled: false,\n status: 'planned',\n category: 'node-types',\n },\n node_link_list: {\n name: 'Link list',\n description: 'Terminal node showing a list of links to choose from',\n enabled: false,\n status: 'planned',\n category: 'node-types',\n },\n node_file_download: {\n name: 'File download',\n description: 'Provide files for respondents to download',\n enabled: true,\n status: 'live',\n wentLiveAt: '2026-07-12',\n category: 'node-types',\n },\n node_shopping: {\n name: 'Shopping',\n description: 'Product recommendations with direct checkout',\n enabled: false,\n status: 'planned',\n category: 'node-types',\n },\n\n // -- Response formats --\n text_responses: {\n name: 'Text responses',\n description: 'Respondents type free-form text answers',\n enabled: true,\n status: 'live',\n category: 'responses',\n },\n audio_responses: {\n name: 'Audio responses',\n description: 'Respondents record audio directly in the browser',\n enabled: true,\n status: 'live',\n category: 'responses',\n },\n video_responses: {\n name: 'Video responses',\n description: 'Respondents record video directly in the browser',\n enabled: true,\n status: 'live',\n category: 'responses',\n },\n transcription: {\n name: 'Auto-transcription',\n description: 'Whisper-powered transcription for audio and video responses',\n enabled: true,\n status: 'live',\n category: 'responses',\n },\n\n // -- Analytics --\n analytics_overview: {\n name: 'Analytics dashboard',\n description: 'Views, starts, completions with daily time series',\n enabled: true,\n status: 'live',\n category: 'analytics',\n },\n analytics_demographics: {\n name: 'Demographic breakdown',\n description: 'Country, browser, device, and referrer source analytics',\n enabled: true,\n status: 'live',\n category: 'analytics',\n },\n utm_tracking: {\n name: 'UTM parameter tracking',\n description: 'Track utm_source, utm_medium, utm_campaign on form views',\n enabled: false,\n status: 'planned',\n category: 'analytics',\n },\n csv_export: {\n name: 'CSV export',\n description: 'Export response data to CSV, including transcriptions and media links',\n enabled: true,\n status: 'live',\n wentLiveAt: '2026-06-07',\n category: 'analytics',\n },\n tracked_links: {\n name: 'Tracked links',\n description: 'URL tracking and click-through analytics for shared form links',\n enabled: false,\n status: 'planned',\n category: 'analytics',\n },\n\n // -- Sharing & embedding --\n public_share_links: {\n name: 'Public share links',\n description: 'Share forms via a public URL',\n enabled: true,\n status: 'live',\n category: 'sharing',\n },\n share_node: {\n name: 'Share individual nodes',\n description: 'Share a direct link to a specific node within a form',\n enabled: true,\n status: 'live',\n wentLiveAt: '2026-07-17',\n category: 'sharing',\n },\n public_results: {\n name: 'Public results portal',\n description: 'Share a live, read-only results page with anyone via a link',\n // Ships dark (#2129): code lands ready, gated server-side (apps/api) on\n // this flag OR the FEATURE_COMPANY_ALLOWLIST.public_results company\n // allowlist below (companyHasPublicResults) for per-company dogfood ahead\n // of the global flip - same flag-or-allowlist pattern as node_camera /\n // companyHasNodeType. Flip enabled/status together for GA - the API\n // re-checks this flag on every enable/rotate/read, so flipping it on/off\n // (or editing the allowlist) takes effect immediately, no redeploy needed.\n enabled: false,\n status: 'planned',\n category: 'sharing',\n },\n embed_inline: {\n name: 'Inline embed',\n description: 'Embed forms on external websites via script tag or JavaScript API',\n enabled: true,\n status: 'live',\n wentLiveAt: '2026-05-27',\n category: 'sharing',\n },\n embed_widget: {\n name: 'Embed widget',\n description: 'Floating button that opens a form in a modal overlay',\n enabled: false,\n status: 'planned',\n category: 'sharing',\n },\n dynamic_link: {\n name: 'Dynamic links',\n description: 'A named link that points at a form, so the target can be swapped from the dashboard without changing the embed code or printed link',\n enabled: true,\n status: 'live',\n category: 'sharing',\n wentLiveAt: '2026-08-27',\n },\n custom_domains: {\n name: 'Custom domains',\n description: 'Serve your forms on your own subdomain (forms.yourbrand.com) with vanity paths, instead of a clipform.io link',\n enabled: false,\n status: 'planned',\n category: 'sharing',\n },\n branded_qr: {\n name: 'Branded QR codes',\n description: \"Render a form's share QR in its theme color with the logo centered, instead of plain black\",\n enabled: false,\n status: 'planned',\n category: 'sharing',\n },\n email_share: {\n name: 'Add to an email',\n description: 'Copy-paste image + link snippet for pasting a Clipform into an email campaign',\n enabled: false,\n status: 'planned',\n category: 'sharing',\n },\n remove_branding: {\n name: 'Remove branding',\n description: 'Hide Clipform watermark (Pro plan)',\n enabled: true,\n status: 'live',\n category: 'sharing',\n },\n\n // -- AI --\n ai_form_generation: {\n name: 'AI form generation',\n description: 'Generate complete forms from a text prompt via MCP or dashboard',\n enabled: true,\n status: 'live',\n category: 'ai',\n },\n ai_tts: {\n name: 'AI text-to-speech',\n description: 'Generate voiceover audio for form prompts',\n enabled: true,\n status: 'live',\n category: 'ai',\n },\n\n // -- API --\n public_api: {\n name: 'Public API',\n description: 'REST API for form creation, node management, and response collection',\n enabled: true,\n status: 'live',\n category: 'api',\n },\n api_keys: {\n name: 'API key management',\n description: 'Self-service API key creation in the dashboard',\n enabled: true,\n status: 'live',\n wentLiveAt: '2026-06-22',\n category: 'api',\n },\n\n // -- Integrations --\n webhooks: {\n name: 'Webhooks',\n description: 'Send response data to external URLs on form completion',\n enabled: true,\n status: 'live',\n wentLiveAt: '2026-06-24',\n category: 'integrations',\n },\n // Per-integration lifecycle (split from the old coarse `integrations` flag so\n // each can ship independently). integration-catalog.js carries the metadata\n // and derives is_active from these. None are live yet.\n integration_stripe: {\n name: 'Stripe',\n description: 'Accept payments directly in your forms',\n enabled: true,\n status: 'live',\n wentLiveAt: '2026-09-07',\n category: 'integrations',\n },\n integration_paddle: {\n name: 'Paddle',\n description: 'Accept payments with Paddle',\n enabled: false,\n status: 'planned',\n category: 'integrations',\n },\n integration_square: {\n name: 'Square',\n description: 'Accept payments with Square',\n enabled: false,\n status: 'planned',\n category: 'integrations',\n },\n integration_calendly: {\n name: 'Calendly',\n description: 'Schedule meetings directly in your forms',\n enabled: false,\n status: 'planned',\n category: 'integrations',\n },\n integration_calcom: {\n name: 'Cal.com',\n description: 'Open-source scheduling platform for booking meetings',\n enabled: false,\n status: 'planned',\n category: 'integrations',\n },\n integration_docusign: {\n name: 'DocuSign',\n description: 'Collect signatures in your forms',\n enabled: false,\n status: 'planned',\n category: 'integrations',\n },\n integration_zapier: {\n name: 'Zapier',\n description: 'Connect to 5,000+ apps and automate workflows',\n enabled: true,\n status: 'live',\n wentLiveAt: '2026-06-21',\n category: 'integrations',\n },\n integration_make: {\n name: 'Make',\n description: 'Advanced automation and integrations',\n enabled: true,\n status: 'live',\n wentLiveAt: '2026-06-22',\n category: 'integrations',\n },\n integration_n8n: {\n name: 'n8n',\n description: 'Open-source workflow automation - trigger flows on form responses',\n enabled: true,\n status: 'live',\n wentLiveAt: '2026-06-26',\n category: 'integrations',\n },\n integration_hubspot: {\n name: 'HubSpot',\n description: 'Sync responses to HubSpot CRM',\n enabled: false,\n status: 'planned',\n category: 'integrations',\n },\n integration_salesforce: {\n name: 'Salesforce',\n description: 'Sync responses to Salesforce CRM',\n enabled: false,\n status: 'planned',\n category: 'integrations',\n },\n integration_slack: {\n name: 'Slack',\n description: 'Post a summary of every completed response to a Slack channel',\n enabled: false,\n status: 'beta',\n category: 'integrations',\n },\n integration_shopify: {\n name: 'Shopify',\n description: 'Product recommendations from quiz results with direct checkout',\n enabled: false,\n status: 'planned',\n category: 'integrations',\n },\n integration_google_sheets: {\n name: 'Google Sheets',\n description: 'Sync form responses to a Google Sheet, one row per submission',\n enabled: false,\n status: 'planned',\n category: 'integrations',\n },\n notifications: {\n name: 'Response notifications',\n description: 'Email or webhook alerts when new responses arrive',\n enabled: false,\n status: 'planned',\n category: 'integrations',\n },\n import_typeform: {\n name: 'Typeform importer',\n description: 'Import forms from Typeform into Clipform',\n enabled: false,\n status: 'planned',\n category: 'integrations',\n },\n import_tally: {\n name: 'Tally importer',\n description: 'Import forms from Tally into Clipform',\n enabled: false,\n status: 'planned',\n category: 'integrations',\n },\n};\n\n// `enabled` (runtime gate) and `status` (lifecycle) are two independent axes -\n// but two pairings are incoherent and almost always a dual-write slip:\n// - status 'live' with enabled:false -> shipped-but-hidden (marketing claims it, no one can use it)\n// - status 'planned' with enabled:true -> advertised as \"coming soon\" but actually exposed\n// 'beta' is intentionally free: enabled:true is a public beta, enabled:false a\n// dark beta (e.g. dynamic_link, #2552 - plumbing lands dark, exposed once the\n// share-surface rework #3104 ships). Catch the two bad pairings at module load\n// so a typo can't ship.\nfor (const [key, f] of Object.entries(FEATURES)) {\n if (f.status === 'live' && !f.enabled) {\n throw new Error(`FEATURES.${key}: status 'live' requires enabled:true (shipped features must be exposed)`);\n }\n if (f.status === 'planned' && f.enabled) {\n throw new Error(`FEATURES.${key}: status 'planned' requires enabled:false (planned features must not be exposed)`);\n }\n // wentLiveAt is the dated go-live event (see header). Only a shipped feature has\n // one, and it must be a real ISO date - a free-text slip would silently corrupt\n // the changelog sort order, so fail at load instead.\n if (f.wentLiveAt !== undefined) {\n if (f.status === 'planned') {\n throw new Error(`FEATURES.${key}: wentLiveAt set but status is 'planned' (only shipped features have a go-live date)`);\n }\n if (!/^\\d{4}-\\d{2}-\\d{2}$/.test(f.wentLiveAt)) {\n throw new Error(`FEATURES.${key}: wentLiveAt must be 'YYYY-MM-DD' (got ${JSON.stringify(f.wentLiveAt)})`);\n }\n }\n}\n\n/**\n * Backwards-compatible FEATURE_FLAGS object.\n * Maps the old flag names to FEATURES[key].enabled so existing dashboard code\n * keeps working without changes. Migrate consumers to FEATURES.x.enabled over time.\n */\nexport const FEATURE_FLAGS = {\n get ENABLE_BRANCHING() { return FEATURES.branching.enabled; },\n get ENABLE_CUSTOM_SCORES() { return FEATURES.custom_scores.enabled; },\n get ENABLE_TEXT_RESPONSE() { return FEATURES.text_response.enabled; },\n get ENABLE_MULTI_SELECT() { return FEATURES.multi_select.enabled; },\n get INLINE_EMBED() { return FEATURES.embed_inline.enabled; },\n get WIDGET_EMBED() { return FEATURES.embed_widget.enabled; },\n get WEBHOOKS() { return FEATURES.webhooks.enabled; },\n get INTEGRATIONS() { return Object.entries(FEATURES).some(([k, f]) => k.startsWith('integration_') && f.enabled); },\n get CUSTOM_FONTS() { return FEATURES.custom_fonts.enabled; },\n get TRACKED_LINKS() { return FEATURES.tracked_links.enabled; },\n get DYNAMIC_LINK() { return FEATURES.dynamic_link.enabled; },\n get CUSTOM_DOMAINS() { return FEATURES.custom_domains.enabled; },\n get BRANDED_QR() { return FEATURES.branded_qr.enabled; },\n get EMAIL_SHARE() { return FEATURES.email_share.enabled; },\n get SHARE_NODE() { return FEATURES.share_node.enabled; },\n get BRAND_NAME() { return FEATURES.brand_name.enabled; },\n get CSV_EXPORT() { return FEATURES.csv_export.enabled; },\n get ALLOW_RESUME() { return FEATURES.allow_resume.enabled; },\n get UTM_ANALYTICS() { return FEATURES.utm_tracking.enabled; },\n get API_KEYS() { return FEATURES.api_keys.enabled; },\n get NOTIFICATIONS() { return FEATURES.notifications.enabled; },\n get IMPORT_TYPEFORM() { return FEATURES.import_typeform.enabled; },\n get IMPORT_TALLY() { return FEATURES.import_tally.enabled; },\n get PUBLIC_RESULTS() { return FEATURES.public_results.enabled; },\n};\n\n/**\n * Per-integration limited-rollout allowlists - one entry per gated integration.\n * While an\n * integration's FEATURES flag is off, these companies get early access to test\n * it in live. Each integration switches on independently: flip its flag for\n * global GA, or allowlist a company to dogfood ahead of that. Dev + prod company\n * ids differ (separate Supabase projects).\n */\nexport const INTEGRATION_COMPANY_ALLOWLIST = {\n // Slack is built but not GA (#4141/#4154) - dogfood on the internal\n // \"Clipform Public\" company while FEATURES.integration_slack.enabled stays\n // off, so every other company sees the Coming Soon card until launch.\n integration_slack: ['00000000-0000-4000-b000-000000000001'],\n // integration_zapier + integration_make + integration_n8n removed - all\n // globally live (FEATURES.integration_*.enabled), so companyHasIntegration\n // short-circuits true for everyone; no allowlist needed. (n8n went live with\n // the published Clipform Trigger node, #976.)\n // integration_stripe removed - globally live (FEATURES.integration_stripe.enabled),\n // so companyHasIntegration short-circuits true for everyone; no allowlist needed\n // (payment node GA, #1438).\n};\n\n/**\n * Per-node-type limited-rollout allowlist - same pattern as\n * INTEGRATION_COMPANY_ALLOWLIST, one entry per gated node type (keyed by the\n * FEATURES key, e.g. 'node_payment'). While a node type's FEATURES flag is off,\n * these companies can build with it in live to dogfood ahead of the global flip.\n * Dev + prod company ids differ (separate Supabase projects).\n */\nexport const NODE_COMPANY_ALLOWLIST = {\n // node_payment removed - globally live (FEATURES.node_payment.enabled), so\n // companyHasNodeType short-circuits true for everyone; no allowlist needed\n // (payment node GA, #1438).\n // Camera node dogfood (#2097): build with it in live ahead of the global flip.\n // Creating a camera node also needs the node_type_check constraint (parked\n // migration PR #2103) - applied on dev already, prod at the next deploy.\n node_camera: [\n '51aef9d0-b26d-4f17-bf6f-583dde728ac2', // prod - Andy's Company\n '35c1dccb-22b8-43a6-9181-28987e63ae48', // dev - andy@clipform.io's Company\n '00000000-0000-4000-b000-000000000001', // Clipform Public (system company; same id dev + prod)\n ],\n // Conditional node dogfood (#4105): build with it in live ahead of the\n // global flip. Creating a conditional node also needs the node_type_check\n // constraint widen (parked holds-migration PR) - applied on dev already,\n // prod at the next deploy.\n node_conditional: [\n '51aef9d0-b26d-4f17-bf6f-583dde728ac2', // prod - Andy's Company\n '35c1dccb-22b8-43a6-9181-28987e63ae48', // dev - andy@clipform.io's Company\n '00000000-0000-4000-b000-000000000001', // Clipform Public (system company; same id dev + prod)\n ],\n // File Upload node dogfood (#2905): the results/responses display now renders\n // uploaded files (PR #2907), so build with it in live ahead of the global flip.\n // The node_type_check constraint already permits 'file_upload' (added with the\n // camera migration), so no migration is needed - this is a pure app-config flip.\n node_file_upload: [\n '51aef9d0-b26d-4f17-bf6f-583dde728ac2', // prod - Andy's Company\n '35c1dccb-22b8-43a6-9181-28987e63ae48', // dev - andy@clipform.io's Company\n '00000000-0000-4000-b000-000000000001', // Clipform Public (system company; same id dev + prod)\n ],\n // Draw node pull-back (#2289): went GA 2026-06-27 (#1012), pulled back behind\n // the allowlist while pen options / match-to-shape mature. Zero external\n // customers were using it at pull-back (Clipform Public only), so this list\n // also carries the e2e auth user and a demo company so existing specs/forms\n // keep working.\n node_draw: [\n '51aef9d0-b26d-4f17-bf6f-583dde728ac2', // prod - Andy's Company\n '35c1dccb-22b8-43a6-9181-28987e63ae48', // dev - andy@clipform.io's Company\n '00000000-0000-4000-b000-000000000001', // Clipform Public (system company; same id dev + prod)\n 'd9808c42-05ee-4830-9634-f0b1496bf484', // dev - andycsmith313@gmail.com's Company (e2e auth user; draw e2e specs create draw nodes)\n '9dcb32a1-b52f-4873-a099-ed44abd4959f', // dev - Clipform Zapier (demo company with a draw demo form)\n ],\n};\n\n/**\n * General-purpose limited-rollout allowlist for a FEATURES-gated capability\n * that isn't a node type or an integration (same pattern as\n * NODE_COMPANY_ALLOWLIST / INTEGRATION_COMPANY_ALLOWLIST, keyed by the\n * FEATURES key). Dev + prod company ids differ (separate Supabase projects).\n */\nexport const FEATURE_COMPANY_ALLOWLIST = {\n // Public results portal dogfood (#2129): build with it in live ahead of the\n // global flip. Server-side gate only (apps/api) - see companyHasPublicResults.\n public_results: [\n '51aef9d0-b26d-4f17-bf6f-583dde728ac2', // prod - Andy's Company\n '35c1dccb-22b8-43a6-9181-28987e63ae48', // dev - andy@clipform.io's Company\n '00000000-0000-4000-b000-000000000001', // Clipform Public (system company; same id dev + prod)\n ],\n // Custom domains dogfood (#3443): build with it live ahead of the global\n // flip. Server-side gate only (apps/api) - see companyHasCustomDomains.\n custom_domains: [\n '51aef9d0-b26d-4f17-bf6f-583dde728ac2', // prod - Andy's Company\n '35c1dccb-22b8-43a6-9181-28987e63ae48', // dev - andy@clipform.io's Company\n '00000000-0000-4000-b000-000000000001', // Clipform Public (system company; same id dev + prod)\n ],\n // Agreement documents dogfood (#2760): build with it live ahead of the global\n // flip. Gates the builder's document-attach UI - see companyHasConsentDocuments.\n consent_documents: [\n '51aef9d0-b26d-4f17-bf6f-583dde728ac2', // prod - Andy's Company\n '35c1dccb-22b8-43a6-9181-28987e63ae48', // dev - andy@clipform.io's Company\n '00000000-0000-4000-b000-000000000001', // Clipform Public (system company; same id dev + prod)\n ],\n};\n\n/**\n * Shared resolver for the flag-or-allowlist rollout pattern: a capability is\n * available once its global FEATURES flag is on, or while the company sits on\n * the capability's limited-rollout allowlist. Single source of truth for\n * companyHasIntegration / companyHasNodeType.\n */\nfunction gatedByFlagOrAllowlist(enabled, companyId, allowlist) {\n return enabled || (!!companyId && (allowlist || []).includes(companyId));\n}\n\n/**\n * Whether a company can use an integration: true once its FEATURES flag is\n * globally live, or while the company sits on that integration's allowlist\n * above. Keyed by the FEATURES key (e.g. 'integration_zapier').\n */\nexport function companyHasIntegration(companyId, integrationKey) {\n return gatedByFlagOrAllowlist(\n !!FEATURES[integrationKey]?.enabled,\n companyId,\n INTEGRATION_COMPANY_ALLOWLIST[integrationKey],\n );\n}\n\n/**\n * Whether a company can see the Integrations surface at all: true if ANY\n * integration is available to it (globally live or allowlisted). Gates the\n * Integrations nav tab - the bare\n * FEATURE_FLAGS.INTEGRATIONS getter ignores the per-integration allowlists, so\n * allowlisted companies would never see the tab while every flag is off.\n */\nexport function companyHasAnyIntegration(companyId) {\n return Object.keys(FEATURES)\n .filter((k) => k.startsWith('integration_'))\n .some((k) => companyHasIntegration(companyId, k));\n}\n\n/**\n * Whether a company can build with a node type: true once its FEATURES flag is\n * globally live, or while the company sits on that node type's allowlist above.\n * Takes the node type key (e.g. 'payment'); maps to the FEATURES key\n * `node_<type>`, the same mapping node-types.js uses to derive is_active.\n */\nexport function companyHasNodeType(companyId, nodeType) {\n const featureKey = `node_${nodeType}`;\n return gatedByFlagOrAllowlist(\n !!FEATURES[featureKey]?.enabled,\n companyId,\n NODE_COMPANY_ALLOWLIST[featureKey],\n );\n}\n\n/**\n * Whether a company can turn on the public results portal: true once the\n * FEATURES flag is globally live, or while the company sits on the\n * public_results allowlist above. Server-side gate only - see apps/api's\n * update-form / rotate-results-share / get-public-results routes.\n */\nexport function companyHasPublicResults(companyId) {\n return gatedByFlagOrAllowlist(\n !!FEATURES.public_results?.enabled,\n companyId,\n FEATURE_COMPANY_ALLOWLIST.public_results,\n );\n}\n\n/**\n * Whether a company can use custom domains: true once the FEATURES flag is\n * globally live, or while the company sits on the custom_domains allowlist\n * above. Server-side gate only - see apps/api's domains.ts customDomainsGate.\n */\nexport function companyHasCustomDomains(companyId) {\n return gatedByFlagOrAllowlist(\n !!FEATURES.custom_domains?.enabled,\n companyId,\n FEATURE_COMPANY_ALLOWLIST.custom_domains,\n );\n}\n\n/**\n * Whether a company can attach documents to consent agreements: true once the\n * FEATURES flag is globally live, or while the company sits on the\n * consent_documents allowlist above. Gates the builder's document-attach UI\n * (#2760) - see apps/dashboard ContactNode.\n */\nexport function companyHasConsentDocuments(companyId) {\n return gatedByFlagOrAllowlist(\n !!FEATURES.consent_documents?.enabled,\n companyId,\n FEATURE_COMPANY_ALLOWLIST.consent_documents,\n );\n}\n\nexport function getLiveFeatures() {\n return Object.values(FEATURES).filter(f => f.status === 'live');\n}\n\nexport function getPlannedFeatures() {\n return Object.values(FEATURES).filter(f => f.status === 'planned');\n}\n\nexport function getFeaturesByCategory(category) {\n return Object.values(FEATURES).filter(f => f.category === category);\n}\n\n/**\n * The What's New timeline: live features with a known go-live date, newest first.\n * The single machine-readable source for the user-facing changelog and the SEO/GTM\n * \"what shipped recently\" signal. Returns [key, feature] entries so consumers can\n * derive links/anchors from the key. Features live since the registry baseline have\n * no wentLiveAt and are intentionally excluded - a dated changelog never invents a date.\n */\nexport function getWhatsNew() {\n return Object.entries(FEATURES)\n .filter(([, f]) => f.status === 'live' && f.wentLiveAt)\n .sort(([, a], [, b]) => b.wentLiveAt.localeCompare(a.wentLiveAt));\n}\n","// @vid-master/config/node-types\n// Static node type definitions — single source of truth for all apps.\n// New node types require code changes (new component, renderer, etc.),\n// so this data is version-controlled rather than stored in the database.\n\nimport { FEATURES, companyHasNodeType } from './features.js';\n\n// Canonical answer-format and consent enums (single source of truth). The node\n// schemas below reference these, and all apps import them, so they cannot drift.\nexport const RESPONSE_FORMATS = ['text', 'audio', 'video'];\nexport const CONSENT_TYPES = ['consent', 'opt_in'];\nexport const CONTACT_FIELD_TYPES = ['text', 'textarea', 'email', 'tel', 'url', 'date', 'number'];\n\n// Opinionated default tier bands for the draw node's optional match-to-shape\n// game (#2148) - a builder-authored target scores the respondent's drawing on\n// submit. Shipped as the config default (`config_schema.properties.game.\n// properties.tiers.default` below) so a node with a target but no custom tiers\n// still gets a sensible 5-band scale; the viewer's scorer (apps/viewer/lib/\n// matchShape.ts) falls back to this same list when `config.game.tiers` is\n// absent. No tier-editing UI in v1 (see the issue's Future section).\nexport const DRAW_GAME_DEFAULT_TIERS = [\n { min_score: 0, label: 'Scribble' },\n { min_score: 35, label: 'Warming up' },\n { min_score: 55, label: 'Nice try' },\n { min_score: 75, label: 'Great match' },\n { min_score: 92, label: 'Perfect' },\n];\n\n// Default number of seconds a video-backed node's answer options stay hidden\n// before revealing (#2794). Lives on `node_media.options_reveal_seconds`\n// (real, nullable) - null means \"use this default\", 0 means \"reveal\n// immediately\". Single source of truth for the viewer's RevealGate default\n// and the builder's placeholder/hint when the node has no stored value.\nexport const DEFAULT_OPTIONS_REVEAL_SECONDS = 3;\n\n// Default cap (seconds) on an open node's audio/video recorder (#2838).\n// Lives on `node.config.max_recording_seconds` (JSONB, not the media asset -\n// an open node can collect a video answer with no media asset attached).\n// Single source of truth for the viewer recorder's `maxDuration` fallback\n// and the builder's placeholder/hint when the node has no stored value.\nexport const DEFAULT_MAX_RECORDING_SECONDS = 120;\n\n// Bounds for `max_recording_seconds` - used directly as the config_schema's\n// minimum/maximum below AND exported so the builder's stepper (OpenNode.tsx)\n// clamps to the exact same numbers instead of a hardcoded local copy that\n// could drift from the schema. No unlimited/0 escape hatch (floor of 10).\nexport const MAX_RECORDING_SECONDS_MIN = 10;\n// 300 (restored from 180, #4023; was 300 before #4014 lowered it): at the\n// 720p/1.5Mbps recorder caps, 300s runs well under the now-200 MiB Supabase\n// storage ceiling (SUPABASE_STORAGE_MAX_BYTES), so the tighter #4014 cap is\n// no longer needed. The viewer clamps existing nodes storing a higher value\n// down to this at read time - no data migration.\nexport const MAX_RECORDING_SECONDS_MAX = 300;\n\nexport const NODE_TYPES = {\n start: {\n label: 'Start',\n shorthand: 'Start',\n icon: 'ArrowRight',\n color: '#D1FAE5',\n description: 'Entry point for the form - connects to the first node',\n category: 'start',\n sort_order: 0,\n has_options: false,\n is_terminal: false,\n show_nav_bar: false,\n loading: 'eager',\n supports_prompt: false,\n supports_media: false,\n is_system: true,\n show_in_results: false,\n // Never appears in a completed submission's answers (system node, no\n // output_schema) - false is a no-op default, set for guard-test coverage.\n notify_inline: false,\n default_config: null,\n config_schema: null,\n response_schema: null,\n output_schema: null,\n },\n choice: {\n label: 'Multiple Choice',\n shorthand: 'Multi',\n icon: 'CheckSquare',\n color: '#DBEAFE',\n description: 'Single or multiple choice node with predefined options',\n category: 'input',\n sort_order: 1,\n has_options: true,\n min_options: 1,\n max_options: 6,\n max_option_length: 36,\n is_terminal: false,\n show_nav_bar: true,\n loading: 'eager',\n supports_prompt: true,\n supports_media: true,\n is_system: false,\n show_in_results: true,\n // Short selected-option label - safe to render inline in a notification (#4154).\n notify_inline: true,\n default_config: {\n selection_mode: 'single',\n choice: { enable_branching: false, record_scores: false },\n randomise_options: false,\n show_option_count: false,\n option_display: 'list',\n options: [\n { content: 'Option 1' },\n { content: 'Option 2' },\n ],\n },\n config_schema: {\n type: 'object',\n properties: {\n choice: {\n type: 'object',\n properties: {\n enable_branching: {\n type: 'boolean',\n label: 'Enable branching logic',\n default: true,\n description: 'Allow each option to have its own logic path. When disabled, all options share a single jump action.',\n },\n show_answer_feedback: {\n type: 'boolean',\n label: 'Show answer feedback',\n default: false,\n description: 'Show correct/incorrect feedback after selection (requires scored options).',\n },\n record_scores: {\n type: 'boolean',\n label: 'Mark correct answer',\n default: false,\n description: 'Select which option is the correct answer.',\n },\n },\n },\n selection_mode: {\n enum: ['single', 'multiple'],\n type: 'string',\n label: 'Selection mode',\n default: 'single',\n description: 'Allow single or multiple selections. \"multiple\" is gated by the multi_select feature - while it is off, the API coerces it to \"single\" (the viewer renders single-select only).',\n },\n allow_text_response: {\n type: 'boolean',\n label: 'Allow text response',\n default: false,\n description: 'Allow free-text response in addition to options (single choice only). Gated by the text_response feature - while it is off, the API coerces it to false (the viewer renders no free-text field yet).',\n },\n randomise_options: {\n type: 'boolean',\n label: 'Randomise options',\n default: false,\n description: 'Show options in random order',\n },\n show_option_count: {\n type: 'boolean',\n label: 'Show option count',\n default: false,\n description: 'Display number of options',\n },\n option_display: {\n enum: ['list', 'letters'],\n type: 'string',\n label: 'Option display',\n default: 'list',\n description: 'How options render: full text rows (list), or compact A/B/C/D chips (letters) when the media already shows the labelled options - e.g. a Grid background',\n },\n content_media_type: {\n enum: ['upload', 'recorded'],\n type: 'string',\n label: 'Content media type',\n description: 'How the node media was provided',\n },\n },\n },\n response_schema: {\n type: 'string',\n description: 'Selected option ID',\n },\n output_schema: {\n type: 'object',\n required: ['node_type', 'label', 'option_id'],\n properties: {\n node_type: { type: 'string', const: 'choice' },\n label: { type: 'string', description: 'Human-readable option label' },\n option_id: { type: 'string', format: 'uuid', description: 'Selected option UUID' },\n },\n },\n },\n open: {\n label: 'Open Ended',\n shorthand: 'Open',\n icon: 'Type',\n color: '#DBEAFE',\n description: 'Free-form text responses from users',\n category: 'input',\n sort_order: 2,\n has_options: false,\n is_terminal: false,\n show_nav_bar: true,\n loading: 'eager',\n supports_prompt: true,\n supports_media: true,\n is_system: false,\n show_in_results: true,\n // True for the common short-text case; a channel notification (#4154)\n // still swaps a recorded audio/video answer for a typed placeholder\n // based on the answer's own media type, not this flag.\n notify_inline: true,\n default_config: {\n formats: [\n { order: 0, format: 'text' },\n { order: 1, format: 'audio' },\n { order: 2, format: 'video' },\n ],\n },\n config_schema: {\n type: 'object',\n properties: {\n formats: {\n type: 'array',\n items: {\n type: 'object',\n properties: {\n format: { enum: RESPONSE_FORMATS, type: 'string' },\n order: { type: 'number' },\n },\n },\n label: 'Allowed response formats',\n description: 'Which formats the user can respond with, in display order',\n },\n content_media_type: {\n enum: ['upload', 'recorded'],\n type: 'string',\n label: 'Content media type',\n description: 'How the node media was provided',\n },\n max_recording_seconds: {\n type: 'number',\n default: DEFAULT_MAX_RECORDING_SECONDS,\n minimum: MAX_RECORDING_SECONDS_MIN,\n maximum: MAX_RECORDING_SECONDS_MAX,\n label: 'Max recording length (seconds)',\n description: 'Cap on the audio/video recorder for this node, in seconds',\n },\n },\n },\n response_schema: {\n type: 'object',\n required: ['response_type'],\n properties: {\n text: {\n type: 'string',\n description: 'Text response (when response_type is text) or transcribed text from audio/video',\n },\n media: {\n type: 'object',\n properties: {\n type: { enum: ['audio', 'video'], type: 'string' },\n storage_path: { type: 'string' },\n facing_mode: { enum: ['user', 'environment'], type: 'string' },\n preview_crop: {\n type: 'object',\n properties: {\n stream_width: { type: 'number' },\n stream_height: { type: 'number' },\n preview_width: { type: 'number' },\n preview_height: { type: 'number' },\n },\n },\n },\n description: 'Media response (when response_type is audio or video)',\n },\n response_type: {\n enum: RESPONSE_FORMATS,\n type: 'string',\n },\n transcription: {\n type: 'object',\n properties: {\n text: { type: 'string' },\n language: { type: 'string' },\n duration: { type: 'number' },\n words: { type: 'array' },\n },\n description: 'Whisper transcription data for audio/video responses',\n },\n },\n },\n output_schema: {\n type: 'object',\n required: ['node_type'],\n properties: {\n node_type: { type: 'string', const: 'open' },\n text: { type: 'string', description: 'Text response or transcription' },\n media: {\n type: 'object',\n properties: {\n type: { enum: ['audio', 'video'], type: 'string' },\n content_type: { type: 'string', description: 'MIME type of the recording (e.g. audio/webm, video/mp4)' },\n filename: { type: 'string', description: 'Suggested download filename' },\n // Direct, time-limited signed URL to the recording (~48h, supports\n // HTTP range); minted per delivery, never the internal storage path.\n url: { type: 'string', format: 'uri' },\n download_url: { type: 'string', format: 'uri', description: 'Same link with a forced download (Content-Disposition: attachment)' },\n expires_at: { type: 'string', format: 'date-time', description: 'When url/download_url stop working' },\n },\n },\n transcription: {\n type: 'object',\n properties: {\n text: { type: 'string' },\n language: { type: 'string' },\n segments: {\n type: 'array',\n description: 'Timestamped segments (seconds) for captions/seeking.',\n items: {\n type: 'object',\n properties: {\n start: { type: 'number' },\n end: { type: 'number' },\n text: { type: 'string' },\n },\n },\n },\n },\n },\n },\n },\n },\n scale: {\n label: 'Scale',\n shorthand: 'Scale',\n icon: 'BarChart3',\n color: '#DBEAFE',\n description: 'Numerical rating or scale node (1-10, etc.)',\n category: 'input',\n sort_order: 3,\n has_options: false,\n is_terminal: false,\n show_nav_bar: true,\n loading: 'eager',\n supports_prompt: true,\n supports_media: true,\n is_system: false,\n show_in_results: true,\n // A bare numeric value - short enough to render inline (#4154).\n notify_inline: true,\n default_config: null,\n config_schema: {\n type: 'object',\n properties: {\n max: { type: 'number', label: 'Maximum value', default: 10, required: true },\n min: { type: 'number', label: 'Minimum value', default: 1, required: true },\n step: { min: 0.1, type: 'number', label: 'Step increment', default: 1 },\n left_label: { type: 'string', label: 'Left label', placeholder: 'Not likely' },\n right_label: { type: 'string', label: 'Right label', placeholder: 'Very likely' },\n show_numbers: { type: 'boolean', label: 'Show numbers', default: true },\n },\n },\n response_schema: {\n type: 'number',\n description: 'Selected scale value',\n },\n output_schema: null,\n },\n booking: {\n label: 'Booking',\n shorthand: 'Booking',\n icon: 'Calendar',\n color: '#E9D5FF',\n description: null,\n category: 'action',\n sort_order: 4,\n has_options: false,\n is_terminal: false,\n show_nav_bar: true,\n loading: 'lazy',\n supports_prompt: false,\n supports_media: false,\n is_system: false,\n show_in_results: true,\n // Structured provider data (calendly/google_meet), not a flat value -\n // a notification renders a typed placeholder instead (#4154).\n notify_inline: false,\n default_config: null,\n config_schema: {\n type: 'object',\n oneOf: [\n {\n required: ['calendly'],\n properties: {\n calendly: {\n type: 'object',\n properties: {\n event_name: { type: 'string', label: 'Event name', description: 'Display name for the event' },\n event_type_uri: { type: 'string', label: 'Event type URI', format: 'uri', description: 'Your Calendly event type URI', placeholder: 'https://calendly.com/your-link' },\n },\n },\n },\n },\n {\n required: ['google_meet'],\n properties: {\n google_meet: {\n type: 'object',\n properties: {\n duration: { type: 'number', label: 'Duration (minutes)', default: 30, description: 'Meeting duration in minutes' },\n calendar_id: { type: 'string', label: 'Calendar', description: 'Google Calendar to use for bookings' },\n event_title: { type: 'string', label: 'Event title', default: 'Meeting', description: 'Default title for scheduled meetings' },\n calendar_name: { type: 'string', label: 'Calendar name', description: 'Display name for the selected calendar' },\n },\n },\n },\n },\n ],\n description: 'Booking provider configuration (one provider per node)',\n },\n response_schema: {\n type: 'object',\n oneOf: [\n {\n required: ['calendly'],\n properties: {\n calendly: {\n type: 'object',\n required: ['event_uri', 'scheduled_time'],\n properties: {\n event_uri: { type: 'string', format: 'uri' },\n event_type: { type: 'string' },\n invitee_uri: { type: 'string', format: 'uri' },\n reschedule_url: { type: 'string', format: 'uri' },\n scheduled_time: { type: 'string', format: 'date-time' },\n cancellation_url: { type: 'string', format: 'uri' },\n },\n },\n },\n },\n ],\n description: 'Booking response with provider-specific data',\n },\n output_schema: null,\n },\n details: {\n label: 'Details',\n shorthand: 'Details',\n icon: 'User',\n color: '#CFFAFE',\n description: 'Collect several fields on one screen - name, email, phone, address, date, and more',\n category: 'input',\n sort_order: 5,\n has_options: false,\n is_terminal: false,\n show_nav_bar: true,\n loading: 'lazy',\n supports_prompt: false,\n supports_media: false,\n is_system: false,\n show_in_results: false,\n // Excluded from the aggregate Results charts (a list of emails can't be\n // charted) but DOES show in the per-response Responses table, where each\n // lead's contact fields are actionable (#746).\n show_in_responses: true,\n // Key contact fields (first name, email) are the whole point of a\n // notification for this node type - always inline (#4154).\n notify_inline: true,\n default_config: {\n fields: [\n { id: 'first_name', type: 'first_name', label: 'First Name', enabled: true, required: true },\n { id: 'email', type: 'email', label: 'Email', enabled: true, required: true },\n ],\n // No agreement out of the box - the builder adds one via \"Add agreement\"\n // when they actually need consent, so a fresh node ships no orphan checkbox. (#1378)\n consent_items: [],\n },\n config_schema: {\n type: 'object',\n required: ['fields'],\n properties: {\n title: { type: 'string' },\n fields: {\n type: 'array',\n minItems: 1,\n items: {\n type: 'object',\n required: ['id', 'required'],\n properties: {\n id: { type: 'string' },\n type: { enum: CONTACT_FIELD_TYPES, type: 'string' },\n label: { type: 'string' },\n order: { type: 'number' },\n required: { type: 'boolean', default: true },\n is_custom: { type: 'boolean', default: false },\n },\n },\n },\n description: { type: 'string' },\n consent_items: {\n type: 'array',\n items: {\n type: 'object',\n properties: {\n id: { type: 'string' },\n name: { type: 'string' },\n label: { type: 'string' },\n order: { type: 'number' },\n type: { type: 'string', enum: CONSENT_TYPES },\n // Optional attached agreement document (PDF only in v1, #2760). Served\n // from the public node-media bucket; `version` bumps on file replace.\n document: {\n type: 'object',\n properties: {\n storage_path: { type: 'string' },\n filename: { type: 'string' },\n url: { type: 'string' },\n version: { type: 'string' },\n },\n },\n // When true, the consent checkbox stays disabled until the respondent\n // scrolls the attached document to the end (scrollwrap gating, #2760).\n require_scroll_to_accept: { type: 'boolean' },\n },\n },\n },\n },\n },\n response_schema: {\n type: 'object',\n required: ['fields'],\n properties: {\n fields: {\n type: 'array',\n items: {\n type: 'object',\n required: ['id', 'type', 'label', 'value'],\n properties: {\n id: { type: 'string' },\n type: { type: 'string' },\n label: { type: 'string' },\n value: { type: 'string' },\n },\n },\n description: 'Contact form field entries with metadata',\n },\n consent: {\n type: 'array',\n items: {\n type: 'object',\n required: ['id', 'name', 'label', 'accepted', 'type'],\n properties: {\n id: { type: 'string' },\n name: { type: 'string' },\n label: { type: 'string' },\n accepted: { type: 'boolean' },\n type: { type: 'string', enum: CONSENT_TYPES },\n // Scrollwrap audit trail (#2760), present only for document-gated\n // consent items: whether they scrolled to the end, the exact\n // document version they saw, and when they accepted.\n reached_end: { type: 'boolean' },\n document_version: { type: 'string' },\n accepted_at: { type: 'string' },\n },\n },\n description: 'Consent checkbox entries',\n },\n },\n },\n output_schema: {\n type: 'object',\n required: ['node_type', 'fields'],\n properties: {\n node_type: { type: 'string', const: 'details' },\n fields: {\n type: 'array',\n items: {\n type: 'object',\n required: ['type', 'value'],\n properties: {\n type: { type: 'string' },\n value: { type: 'string' },\n },\n },\n },\n // Consent/opt-in agreements the respondent was shown. Present only when\n // the node has consent items. `accepted` is their choice; `label` is the\n // exact wording they agreed to, snapshotted per response (#2828).\n consent: {\n type: 'array',\n items: {\n type: 'object',\n required: ['name', 'accepted', 'type', 'label'],\n properties: {\n name: { type: 'string' },\n accepted: { type: 'boolean' },\n type: { type: 'string', enum: CONSENT_TYPES },\n label: { type: 'string' },\n // Scrollwrap audit trail (#2760), emitted only for document-gated items.\n reached_end: { type: 'boolean' },\n document_version: { type: 'string' },\n accepted_at: { type: 'string' },\n },\n },\n },\n },\n },\n },\n file_upload: {\n label: 'File Upload',\n shorthand: 'Upload',\n icon: 'Upload',\n color: '#ECFCCB',\n description: 'Collect file uploads from respondents',\n category: 'input',\n sort_order: 6,\n has_options: false,\n is_terminal: false,\n show_nav_bar: true,\n loading: 'lazy',\n supports_prompt: false,\n supports_media: false,\n is_system: false,\n show_in_results: true,\n // Files, not a flat value - a notification renders a typed placeholder (#4154).\n notify_inline: false,\n default_config: null,\n config_schema: {\n type: 'object',\n properties: {\n max_files: { max: 20, min: 1, type: 'number', label: 'Max files', default: 5, description: 'Maximum number of files respondents can upload' },\n // Capped at 50, not 500: respondent file uploads go straight to the Supabase\n // `file-uploads` bucket, and SUPABASE_STORAGE_MAX_BYTES is a hard ceiling there.\n // Anything above it passed validation and then 500'd at the storage layer (#2556).\n max_file_size_mb: { max: 50, min: 1, type: 'number', label: 'Max file size (MB)', default: 50, description: 'Maximum size per file (1-50 MB)' },\n allowed_media_types: {\n type: 'array',\n items: { enum: ['images', 'videos', 'documents', 'all'], type: 'string' },\n label: 'Allowed media types',\n default: ['images', 'documents'],\n enumLabels: {\n all: 'All file types',\n images: 'Images (JPEG, PNG, GIF, WebP, HEIC)',\n videos: 'Videos (MP4, MOV, WebM, AVI)',\n documents: 'Documents (PDF, DOC, DOCX, XLS, XLSX, TXT, CSV)',\n },\n description: 'Select which types of files respondents can upload',\n },\n },\n },\n response_schema: {\n type: 'object',\n required: ['files'],\n properties: {\n files: {\n type: 'array',\n items: {\n type: 'object',\n required: ['id', 'name', 'storage_path', 'url', 'mime_type', 'size', 'uploaded_at'],\n properties: {\n id: { type: 'string', format: 'uuid', description: 'Unique file identifier' },\n url: { type: 'string', format: 'uri', description: 'Signed Supabase Storage URL for download' },\n name: { type: 'string', description: 'Original filename' },\n size: { type: 'integer', minimum: 1, description: 'File size in bytes' },\n mime_type: { type: 'string', description: 'File MIME type' },\n uploaded_at: { type: 'string', format: 'date-time', description: 'ISO timestamp of upload completion' },\n storage_path: { type: 'string', description: 'Full storage path: workspace_id/form_id/node_id/uuid.ext' },\n },\n },\n minItems: 1,\n description: 'Array of uploaded file metadata',\n },\n },\n },\n output_schema: {\n type: 'object',\n required: ['node_type'],\n properties: {\n node_type: { type: 'string', const: 'file_upload' },\n files: {\n type: 'array',\n description: 'The uploaded files, each a canonical media object with a fresh signed link.',\n items: {\n type: 'object',\n properties: {\n id: { type: 'string', description: 'Unique file identifier' },\n filename: { type: 'string', description: 'Original filename' },\n content_type: { type: 'string', description: 'File MIME type' },\n size: { type: 'integer', description: 'File size in bytes' },\n uploaded_at: { type: 'string', format: 'date-time', description: 'ISO timestamp of upload completion' },\n url: { type: 'string', format: 'uri', description: 'Direct, time-limited signed link (~48h, supports HTTP range)' },\n download_url: { type: 'string', format: 'uri', description: 'Same link with a forced download (Content-Disposition: attachment)' },\n expires_at: { type: 'string', format: 'date-time', description: 'When url/download_url stop working' },\n },\n },\n },\n },\n },\n },\n draw: {\n label: 'Drawing',\n shorthand: 'Draw',\n icon: 'Pencil',\n color: '#FEF3C7',\n description: 'Drawing canvas; respondents draw and it is saved as a PNG image',\n category: 'input',\n sort_order: 6.5,\n has_options: false,\n is_terminal: false,\n show_nav_bar: true,\n loading: 'lazy',\n supports_prompt: false,\n supports_media: true,\n is_system: false,\n show_in_results: true,\n // An image, not a flat value - a notification renders a typed placeholder (#4154).\n notify_inline: false,\n default_config: {\n paper_color: '#FAF9F6',\n stroke_colors: ['#1F2937', '#EF4444', '#3B82F6', '#22C55E', '#FACC15'],\n stroke_widths: [3, 6, 12],\n background: { fit: 'contain', opacity: 1 },\n },\n config_schema: {\n type: 'object',\n properties: {\n paper_color: { type: 'string', label: 'Paper colour', default: '#FAF9F6', description: 'Canvas background colour (off-white paper by default)' },\n stroke_colors: {\n type: 'array',\n items: { type: 'string' },\n minItems: 1,\n maxItems: 8,\n label: 'Pen colours',\n default: ['#1F2937', '#EF4444', '#3B82F6', '#22C55E', '#FACC15'],\n description: 'Colour swatches the respondent can draw with. The first entry is the default selection; a single entry locks the pen to that colour and hides the picker.',\n },\n stroke_widths: {\n type: 'array',\n items: { type: 'number', minimum: 1, maximum: 60 },\n minItems: 1,\n maxItems: 5,\n label: 'Pen thicknesses',\n default: [3, 6, 12],\n description: 'Stroke widths (px) the respondent can choose. The middle entry is the default selection; a single entry locks the pen to that width and hides the picker.',\n },\n // The reference image itself is the node's attached media (media_id,\n // via the same media_asset/create-media.ts pipeline every other\n // supports_media node uses) - not a config field, to avoid a\n // duplicated asset ref. This only tunes its display/bake.\n background: {\n type: 'object',\n label: 'Background image',\n description: 'Display settings for the optional reference image behind the drawing canvas',\n properties: {\n fit: { type: 'string', enum: ['cover', 'contain'], default: 'contain', label: 'Fit', description: 'contain letterboxes the image, cover crops it to fill the canvas' },\n opacity: { type: 'number', minimum: 0, maximum: 1, default: 1, label: 'Opacity', description: 'Background image opacity, from 0 (invisible) to 1 (fully opaque)' },\n },\n },\n // Optional match-to-shape game (#2148). Absent entirely by default\n // (not in default_config) - a node with no `game.target` behaves\n // exactly as it did before this key existed. `target.points` is\n // authored once in the builder (a simplified sketch modal, see\n // apps/dashboard) and stored pre-normalized to a [0,1] unit box so the\n // viewer's scorer (apps/viewer/lib/matchShape.ts) and the reveal\n // overlay share the same coordinate frame without re-deriving it.\n game: {\n type: 'object',\n label: 'Match-to-shape game',\n description: 'Optional target shape the drawing is scored against on submit',\n properties: {\n target: {\n type: 'object',\n label: 'Target shape',\n description: 'The outline the respondent is scored against, drawn once by the form builder',\n properties: {\n points: {\n type: 'array',\n minItems: 2,\n maxItems: 500,\n items: {\n type: 'object',\n properties: {\n x: { type: 'number', minimum: 0, maximum: 1 },\n y: { type: 'number', minimum: 0, maximum: 1 },\n },\n },\n description: 'Closed polyline outline, normalized to a [0,1] unit box (aspect-preserving)',\n },\n },\n },\n tiers: {\n type: 'array',\n label: 'Score tiers',\n minItems: 1,\n maxItems: 10,\n items: {\n type: 'object',\n properties: {\n min_score: { type: 'number', minimum: 0, maximum: 100 },\n label: { type: 'string' },\n },\n },\n default: DRAW_GAME_DEFAULT_TIERS,\n description: 'Score bands shown in the reveal, ascending by min_score - defaults to a 5-band Scribble to Perfect scale',\n },\n },\n },\n },\n },\n response_schema: {\n // Mirrors the live `open` node's media response shape (the only complete\n // media path): the sketch image is uploaded to the `media-responses`\n // bucket via the same upload-client pipeline and referenced by\n // storage_path. Exported as PNG (crisp line art); the off-white paper\n // background is composited in before export so it is baked into the file.\n type: 'object',\n required: ['media'],\n properties: {\n media: {\n type: 'object',\n required: ['storage_path', 'type'],\n properties: {\n type: { enum: ['image'], type: 'string', description: 'Always \"image\" for a sketch' },\n storage_path: { type: 'string', description: 'Path in the media-responses bucket: bucket/workspace_id/.../uuid.png' },\n mime_type: { type: 'string', description: 'Image MIME type (image/png)' },\n },\n description: 'The exported sketch, stored as a single image in media-responses',\n },\n // Present only when the node has a configured game.target - absent for\n // every draw node published before #2148 and for one with no target.\n game: {\n type: 'object',\n description: 'Present when the node has a configured target shape - the respondent\\'s match-to-shape score',\n properties: {\n score: { type: 'number', minimum: 0, maximum: 100, description: '0-100 match score against the target outline' },\n avg_deviation: { type: 'number', minimum: 0, description: 'Average normalized deviation from the target outline (lower is better)' },\n tier: { type: 'string', description: 'The configured tier label the score falls into' },\n },\n },\n },\n },\n output_schema: {\n type: 'object',\n required: ['node_type'],\n properties: {\n node_type: { type: 'string', const: 'draw' },\n media: {\n type: 'object',\n properties: {\n type: { enum: ['image'], type: 'string' },\n content_type: { type: 'string', description: 'Image MIME type (e.g. image/png)' },\n filename: { type: 'string', description: 'Suggested download filename' },\n url: { type: 'string', format: 'uri', description: 'Direct, time-limited signed link to the drawing (~48h, supports HTTP range)' },\n download_url: { type: 'string', format: 'uri', description: 'Same link with a forced download (Content-Disposition: attachment)' },\n expires_at: { type: 'string', format: 'date-time', description: 'When url/download_url stop working' },\n },\n },\n // Mirrors response_schema.game (piece 1 of #2148) - transformResponse\n // passes it through unchanged, so this describes the FINAL emitted shape.\n game: {\n type: 'object',\n description: 'Present when the node has a configured target shape - the respondent\\'s match-to-shape score',\n properties: {\n score: { type: 'number', minimum: 0, maximum: 100, description: '0-100 match score against the target outline' },\n avg_deviation: { type: 'number', minimum: 0, description: 'Average normalized deviation from the target outline (lower is better)' },\n tier: { type: 'string', description: 'The configured tier label the score falls into' },\n },\n },\n },\n },\n },\n camera: {\n label: 'Camera',\n shorthand: 'Camera',\n icon: 'Camera',\n color: '#FCE7F3',\n description: 'Capture a photo or video from the device camera',\n category: 'input',\n sort_order: 6.6,\n has_options: false,\n is_terminal: false,\n show_nav_bar: true,\n loading: 'lazy',\n supports_prompt: false,\n supports_media: false,\n is_system: false,\n show_in_results: true,\n // A captured photo/video, not a flat value - a notification renders a\n // typed placeholder (#4154).\n notify_inline: false,\n default_config: null,\n config_schema: {\n type: 'object',\n properties: {\n camera_facing: {\n type: 'string',\n enum: ['user', 'environment'],\n default: 'user',\n label: 'Default camera',\n description: 'Which camera the device opens by default (a hint only - the respondent can still switch)',\n enumLabels: { user: 'Front (selfie)', environment: 'Rear' },\n },\n allow_library_upload: {\n type: 'boolean',\n default: true,\n label: 'Allow choosing from photo library',\n description: 'Show a secondary option to pick an existing photo instead of taking a new one',\n },\n allowed_capture: {\n type: 'array',\n items: { type: 'string', enum: ['photo', 'video'] },\n default: ['photo'],\n label: 'What can respondents capture?',\n description: 'Which capture types this node accepts - photo, video, or both (existing nodes stay photo-only)',\n enumLabels: { photo: 'Photo', video: 'Video' },\n },\n },\n },\n // Reuses file_upload's response shape verbatim (single-item array) so\n // Results/exports/webhooks treat it the same way (scaffold skill: shape reuse).\n // A video submission carries a video mime_type - the shape doesn't change (#2759).\n response_schema: {\n type: 'object',\n required: ['files'],\n properties: {\n files: {\n type: 'array',\n items: {\n type: 'object',\n required: ['id', 'name', 'storage_path', 'url', 'mime_type', 'size', 'uploaded_at'],\n properties: {\n id: { type: 'string', format: 'uuid', description: 'Unique file identifier' },\n url: { type: 'string', format: 'uri', description: 'Signed Supabase Storage URL for download' },\n name: { type: 'string', description: 'Original filename' },\n size: { type: 'integer', minimum: 1, description: 'File size in bytes' },\n mime_type: { type: 'string', description: 'File MIME type (image/* or video/*)' },\n uploaded_at: { type: 'string', format: 'date-time', description: 'ISO timestamp of upload completion' },\n storage_path: { type: 'string', description: 'Bucket-prefixed storage path: respondent-captures/workspace_id/form_id/session_id/uuid.ext' },\n },\n },\n minItems: 1,\n maxItems: 1,\n description: 'The captured photo or video (single-item array, mirroring file_upload)',\n },\n },\n },\n output_schema: {\n type: 'object',\n required: ['node_type'],\n properties: {\n node_type: { type: 'string', const: 'camera' },\n files: {\n type: 'array',\n description: 'The captured photo, as a canonical media object with a fresh signed link.',\n items: {\n type: 'object',\n properties: {\n id: { type: 'string', description: 'Unique file identifier' },\n filename: { type: 'string', description: 'Original filename' },\n content_type: { type: 'string', description: 'File MIME type' },\n size: { type: 'integer', description: 'File size in bytes' },\n uploaded_at: { type: 'string', format: 'date-time', description: 'ISO timestamp of upload completion' },\n url: { type: 'string', format: 'uri', description: 'Direct, time-limited signed link (~48h, supports HTTP range)' },\n download_url: { type: 'string', format: 'uri', description: 'Same link with a forced download (Content-Disposition: attachment)' },\n expires_at: { type: 'string', format: 'date-time', description: 'When url/download_url stop working' },\n },\n },\n },\n },\n },\n },\n // Phase 1 of the variables engine (#4104, this node #4105): routes the\n // flow on a generic built-in Number (internally `score`, derived from\n // node_options.score -> ScoreState.cumulativeScore - see ROUTABLE_VARIABLES\n // in logic-types.js). Response-less and screen-less - the viewer evaluates\n // it transparently (no render) and continues to the matching branch. Each\n // branch is a node_options row (has_options:true); the branch edge is a\n // `jump` action anchored by the option's id (LogicAction.option_id).\n conditional: {\n label: 'Conditional',\n shorthand: 'If',\n icon: 'Split',\n color: '#E0E7FF',\n description: 'Route the flow based on Score',\n category: 'logic',\n sort_order: 6.7,\n has_options: true,\n min_options: 2,\n max_options: 10,\n max_option_length: 40,\n is_terminal: false,\n show_nav_bar: false,\n loading: 'eager',\n supports_prompt: false,\n supports_media: false,\n is_system: false,\n show_in_results: false,\n default_config: {\n options: [\n { content: 'Branch 1' },\n { content: 'Else' },\n ],\n },\n config_schema: null,\n response_schema: null,\n output_schema: null,\n },\n payment: {\n label: 'Payment',\n shorthand: 'Payment',\n icon: 'CreditCard',\n color: '#E9D5FF',\n description: 'Collect a payment from respondents inline, charged to a connected Stripe account',\n category: 'action',\n sort_order: 7,\n has_options: false,\n is_terminal: false,\n show_nav_bar: true,\n loading: 'lazy',\n supports_prompt: false,\n supports_media: false,\n is_system: false,\n show_in_results: true,\n // Config keys that reference a workspace-scoped connected account (Stripe/etc\n // via workspace_integrations). The SoT for save_form to strip on cross-workspace\n // copy (#2064 part 2 - migration, not yet wired). Standing convention for any\n // future OAuth/connected-account node (booking, shopify), not a payment special\n // case - the publish gate's own showcase waiver (graph-validation.js) still\n // names workspace_integration_id directly since it's the only per-type check today.\n connection_config_keys: ['workspace_integration_id'],\n // A payment confirmation line is the whole point of a notification for\n // this node type - always inline (#4154).\n notify_inline: true,\n default_config: { amount: null, currency: 'usd', provider: 'stripe' },\n config_schema: {\n type: 'object',\n required: ['amount', 'currency', 'provider'],\n properties: {\n amount: { type: 'number', label: 'Amount (in cents)', minimum: 50, required: true, description: 'Payment amount in cents (e.g., 5000 = $50.00)' },\n currency: { enum: ['usd', 'eur', 'gbp', 'cad', 'aud'], type: 'string', label: 'Currency', default: 'usd', required: true, description: 'Three-letter ISO currency code' },\n provider: { enum: ['stripe', 'paddle'], type: 'string', label: 'Payment Provider', default: 'stripe', required: true, description: 'Payment provider to use' },\n workspace_integration_id: { type: 'string', label: 'Workspace Integration ID', description: 'UUID of the workspace_integrations record for the connected payment account' },\n title: { type: 'string', label: 'Heading', description: 'Heading shown above the order summary' },\n description: { type: 'string', label: 'Description', description: 'Body text shown below the heading in the order summary' },\n },\n description: 'Payment configuration with provider-agnostic flat structure',\n },\n response_schema: {\n type: 'object',\n oneOf: [\n {\n required: ['stripe'],\n properties: {\n stripe: {\n type: 'object',\n required: ['payment_intent_id', 'status', 'amount', 'currency'],\n properties: {\n name: { type: 'string', description: 'Customer name from Stripe billing details' },\n email: { type: 'string', format: 'email', description: 'Customer email from Stripe billing details' },\n phone: { type: 'string', description: 'Customer phone from Stripe billing details' },\n address: {\n type: 'object',\n description: 'Customer billing address from Stripe (only non-null parts present)',\n properties: {\n line1: { type: 'string' },\n line2: { type: 'string' },\n city: { type: 'string' },\n state: { type: 'string' },\n postal_code: { type: 'string' },\n country: { type: 'string', description: 'Two-letter ISO country code' },\n },\n },\n amount: { type: 'number', description: 'Payment amount in cents' },\n status: { enum: ['pending', 'succeeded', 'failed', 'partially_refunded', 'refunded'], type: 'string', description: 'Payment status, re-derived from the charge on every webhook sync' },\n amount_refunded: { type: 'number', description: 'Amount refunded so far, in cents. Only present once a refund (full or partial) has been issued' },\n currency: { type: 'string', description: 'Three-letter ISO currency code (e.g., usd, eur)' },\n payment_intent_id: { type: 'string', description: 'Stripe payment intent ID' },\n charge_id: { type: 'string', description: 'Stripe charge ID (ch_...) for reconciliation' },\n receipt_url: { type: 'string', format: 'uri', description: 'Stripe-hosted receipt page for this charge' },\n receipt_number: { type: 'string', description: 'Stripe receipt number (present once Stripe emails a receipt)' },\n balance_transaction: { type: 'string', description: 'Stripe balance transaction ID (txn_...) tying the charge to the payout ledger' },\n },\n },\n },\n },\n {\n required: ['square'],\n properties: {\n square: {\n type: 'object',\n properties: {\n amount: { type: 'number' },\n status: { type: 'string' },\n order_id: { type: 'string' },\n payment_id: { type: 'string' },\n },\n },\n },\n },\n {\n required: ['paddle'],\n properties: {\n paddle: {\n type: 'object',\n properties: {\n amount: { type: 'number' },\n status: { type: 'string' },\n order_id: { type: 'string' },\n checkout_id: { type: 'string' },\n },\n },\n },\n },\n ],\n description: 'Payment response with provider-specific data',\n },\n // The FINAL first-class emitted shape (#3796): transformResponse flattens the\n // stored `{ stripe: {...} }` into this provider-agnostic object. Must match\n // transformResponse's payment arm exactly, or validateOutput warns per row.\n // Card PAN/CVC are never present (only brand/last4 exist upstream).\n output_schema: {\n type: 'object',\n required: ['node_type', 'provider', 'status', 'amount', 'currency', 'payment_intent_id'],\n properties: {\n node_type: { type: 'string', const: 'payment' },\n provider: { type: 'string', const: 'stripe', description: 'Payment provider that processed the charge' },\n status: { enum: ['pending', 'succeeded', 'failed', 'partially_refunded', 'refunded'], type: 'string', description: 'Payment status, re-derived from the charge on every webhook sync' },\n amount: { type: 'number', description: 'Payment amount in cents' },\n amount_refunded: { type: 'number', description: 'Amount refunded so far, in cents. Only present once a refund (full or partial) has been issued' },\n currency: { type: 'string', description: 'Three-letter ISO currency code (e.g., usd, eur)' },\n payment_intent_id: { type: 'string', description: 'Stripe payment intent ID (pi_...)' },\n charge_id: { type: 'string', description: 'Stripe charge ID (ch_...) for reconciliation' },\n receipt_url: { type: 'string', format: 'uri', description: 'Stripe-hosted receipt page for this charge' },\n receipt_number: { type: 'string', description: 'Stripe receipt number (present once Stripe emails a receipt)' },\n customer: {\n type: 'object',\n description: 'Billing details the respondent entered at checkout (only non-null parts present)',\n properties: {\n name: { type: 'string', description: 'Customer name from Stripe billing details' },\n email: { type: 'string', format: 'email', description: 'Customer email from Stripe billing details' },\n phone: { type: 'string', description: 'Customer phone from Stripe billing details' },\n address: {\n type: 'object',\n description: 'Customer billing address (only non-null parts present)',\n properties: {\n line1: { type: 'string' },\n line2: { type: 'string' },\n city: { type: 'string' },\n state: { type: 'string' },\n postal_code: { type: 'string' },\n country: { type: 'string', description: 'Two-letter ISO country code' },\n },\n },\n },\n },\n },\n },\n },\n binary: {\n label: 'Binary',\n shorthand: 'A/B',\n icon: 'CircleCheck',\n color: '#DBEAFE',\n description: 'Two-option choice - yes/no, true/false, or this vs that',\n category: 'input',\n sort_order: 1.5,\n has_options: true,\n min_options: 2,\n max_options: 2,\n max_option_length: 12,\n is_terminal: false,\n show_nav_bar: true,\n loading: 'eager',\n supports_prompt: true,\n supports_media: true,\n is_system: false,\n show_in_results: true,\n // Short selected-option label - safe to render inline (#4154).\n notify_inline: true,\n default_config: {\n choice: { enable_branching: false },\n options: [\n { content: 'Yes' },\n { content: 'No' },\n ],\n },\n config_schema: {\n type: 'object',\n properties: {\n choice: {\n type: 'object',\n properties: {\n enable_branching: {\n type: 'boolean',\n label: 'Enable branching logic',\n default: true,\n description: 'Allow each option to have its own logic path.',\n },\n },\n },\n content_media_type: {\n enum: ['upload', 'recorded'],\n type: 'string',\n label: 'Content media type',\n description: 'How the node media was provided',\n },\n },\n },\n response_schema: {\n type: 'string',\n description: 'Selected option ID',\n },\n output_schema: {\n type: 'object',\n required: ['node_type', 'label', 'option_id'],\n properties: {\n node_type: { type: 'string', const: 'binary' },\n label: { type: 'string', description: 'Human-readable option label' },\n option_id: { type: 'string', format: 'uuid', description: 'Selected option UUID' },\n },\n },\n },\n button: {\n label: 'Button',\n shorthand: 'Button',\n icon: 'RectangleHorizontal',\n color: '#DBEAFE',\n description: 'Simple button for acknowledgment or navigation',\n category: 'input',\n sort_order: 3.5,\n has_options: true,\n min_options: 1,\n max_options: 1,\n max_option_length: 28,\n is_terminal: false,\n show_nav_bar: true,\n loading: 'eager',\n supports_prompt: true,\n supports_media: true,\n is_system: false,\n show_in_results: false,\n // Short selected-option label - safe to render inline (#4154).\n notify_inline: true,\n default_config: { options: [{ content: 'Continue' }] },\n config_schema: {\n type: 'object',\n properties: {\n button_text: { type: 'string', label: 'Button text', default: 'Continue' },\n button_style: { enum: ['primary', 'secondary', 'outline'], type: 'string', label: 'Button style', default: 'primary' },\n },\n },\n response_schema: {\n type: 'string',\n description: 'Selected option ID',\n },\n output_schema: {\n type: 'object',\n required: ['node_type', 'label', 'option_id'],\n properties: {\n node_type: { type: 'string', const: 'button' },\n label: { type: 'string', description: 'Human-readable option label' },\n option_id: { type: 'string', format: 'uuid', description: 'Selected option UUID' },\n },\n },\n },\n redirect: {\n label: 'Redirect',\n shorthand: 'Redirect',\n icon: 'ExternalLink',\n color: '#FFEDD5',\n description: 'Redirect users to an external URL',\n category: 'end',\n sort_order: 997,\n has_options: false,\n is_terminal: true,\n show_nav_bar: false,\n loading: 'lazy',\n supports_prompt: false,\n supports_media: false,\n is_system: false,\n show_in_results: false,\n // Terminal node - never appears in a completed submission's answers\n // (filtered by NON_COUNTABLE_FILTER); false is a no-op default.\n notify_inline: false,\n default_config: { url: '', auto_redirect: true },\n config_schema: {\n type: 'object',\n properties: {\n url: { type: 'string', label: 'Redirect URL', placeholder: 'https://example.com' },\n auto_redirect: { type: 'boolean', label: 'Auto-redirect', default: true, description: 'Automatically redirect to the URL instead of showing a button' },\n },\n },\n response_schema: {\n type: 'string',\n description: 'URL the user was redirected to',\n },\n output_schema: null,\n },\n link_list: {\n label: 'Link List',\n shorthand: 'Links',\n icon: 'ExternalLink',\n color: '#FFEDD5',\n description: 'Display a list of links for users to choose from',\n category: 'end',\n sort_order: 998,\n has_options: false,\n is_terminal: true,\n show_nav_bar: false,\n loading: 'lazy',\n supports_prompt: false,\n supports_media: false,\n is_system: false,\n show_in_results: false,\n // Terminal node - never appears in a completed submission's answers\n // (filtered by NON_COUNTABLE_FILTER); false is a no-op default.\n notify_inline: false,\n default_config: { links: [{ id: 'default', url: '', title: '', description: '', order: 0 }], auto_redirect: false },\n config_schema: {\n type: 'object',\n properties: {\n links: {\n type: 'array',\n items: {\n type: 'object',\n required: ['id', 'url'],\n properties: {\n id: { type: 'string' },\n url: { type: 'string', label: 'URL', placeholder: 'https://example.com' },\n title: { type: 'string', label: 'Heading' },\n description: { type: 'string', label: 'Description' },\n order: { type: 'number', label: 'Sort order' },\n },\n },\n label: 'Links',\n minItems: 1,\n },\n auto_redirect: { type: 'boolean', label: 'Auto-redirect', default: false, description: 'Automatically redirect to the URL instead of showing a button' },\n },\n },\n response_schema: {\n type: 'string',\n description: 'URL of the link that was clicked',\n },\n output_schema: null,\n },\n file_download: {\n label: 'File Download',\n shorthand: 'Download',\n icon: 'Download',\n color: '#E9D5FF',\n description: 'Provide a file for respondents to download',\n category: 'action',\n sort_order: 9,\n has_options: false,\n is_terminal: false,\n show_nav_bar: true,\n loading: 'lazy',\n supports_prompt: false,\n supports_media: false,\n is_system: false,\n show_in_results: true,\n // Files, not a flat value - a notification renders a typed placeholder (#4154).\n notify_inline: false,\n default_config: null,\n config_schema: {\n type: 'object',\n properties: {\n files: {\n type: 'array',\n items: {\n type: 'object',\n properties: {\n file_name: { type: 'string', label: 'Original file name', required: true },\n display_name: { type: 'string', label: 'Friendly name shown to respondents' },\n file_path: { type: 'string', label: 'File path in storage', required: true },\n file_size: { type: 'number', label: 'File size in bytes' },\n mime_type: { type: 'string', label: 'MIME type' },\n },\n },\n label: 'Downloadable files',\n },\n button_text: { type: 'string', label: 'Continue button text', default: 'Continue' },\n description: { type: 'string', label: 'Description text' },\n },\n },\n response_schema: {\n type: 'object',\n required: ['files'],\n properties: {\n files: {\n type: 'array',\n items: {\n type: 'object',\n required: ['file_path', 'file_name', 'downloaded_at'],\n properties: {\n file_name: { type: 'string', description: 'Display name of the file' },\n file_path: { type: 'string', description: 'Storage path of the downloaded file' },\n downloaded_at: { type: 'string', format: 'date-time', description: 'Timestamp when download was initiated' },\n },\n },\n description: 'List of files downloaded by respondent',\n },\n },\n },\n output_schema: {\n type: 'object',\n required: ['node_type'],\n properties: {\n node_type: { type: 'string', const: 'file_download' },\n files: {\n type: 'array',\n description: 'The files the respondent downloaded, each a canonical media object with a fresh signed link.',\n items: {\n type: 'object',\n properties: {\n filename: { type: 'string', description: 'Original filename' },\n content_type: { type: 'string', description: 'File MIME type' },\n downloaded_at: { type: 'string', format: 'date-time', description: 'Timestamp when download was initiated' },\n url: { type: 'string', format: 'uri', description: 'Direct, time-limited signed link (~48h, supports HTTP range)' },\n download_url: { type: 'string', format: 'uri', description: 'Same link with a forced download (Content-Disposition: attachment)' },\n expires_at: { type: 'string', format: 'date-time', description: 'When url/download_url stop working' },\n },\n },\n },\n },\n },\n },\n shopping: {\n label: 'Shopping',\n shorthand: 'Shop',\n icon: 'ShoppingBag',\n color: '#E9D5FF',\n description: 'Product recommendations with direct checkout',\n category: 'action',\n sort_order: 8,\n has_options: false,\n is_terminal: true,\n show_nav_bar: true,\n loading: 'lazy',\n supports_prompt: false,\n supports_media: false,\n is_system: false,\n show_in_results: false,\n // No response_schema - never carries a flat answer value; false is a no-op default.\n notify_inline: false,\n default_config: { provider: 'shopify', cta_mode: 'checkout', source_mode: 'manual' },\n config_schema: {\n type: 'object',\n required: ['provider', 'cta_mode'],\n properties: {\n title: { type: 'string', label: 'Heading', description: 'Heading shown above the product list' },\n description: { type: 'string', label: 'Description', description: 'Body text shown below the heading' },\n provider: { enum: ['shopify'], type: 'string', label: 'Provider', default: 'shopify', description: 'Ecommerce provider' },\n workspace_integration_id: { type: 'string', label: 'Workspace Integration ID', description: 'UUID of the workspace_integrations record for the connected store' },\n cta_mode: { enum: ['checkout', 'add_to_cart'], type: 'string', label: 'CTA Mode', default: 'checkout', description: 'Whether to go to checkout or add to cart' },\n source_mode: { enum: ['manual', 'collection'], type: 'string', label: 'Product source', default: 'manual', description: 'How products are selected: manually picked or from a collection' },\n collection_id: { type: 'string', label: 'Collection ID', description: 'Shopify collection GID (when source_mode is collection)' },\n collection_title: { type: 'string', label: 'Collection title', description: 'Display name of the selected collection' },\n products: {\n type: 'array',\n label: 'Selected Products',\n description: 'Products to show (static mode)',\n items: {\n type: 'object',\n properties: {\n product_id: { type: 'string', description: 'Shopify product GID' },\n variant_id: { type: 'string', description: 'Shopify variant GID' },\n title: { type: 'string' },\n handle: { type: 'string' },\n image_url: { type: 'string' },\n price: { type: 'string' },\n currency: { type: 'string' },\n recommended: { type: 'boolean', default: true, description: 'Pre-ticked in viewer' },\n },\n },\n },\n product_mappings: {\n type: 'array',\n label: 'Score-based product mappings',\n description: 'Map score ranges to different product sets. First matching range wins.',\n items: {\n type: 'object',\n required: ['min', 'max'],\n properties: {\n min: { type: 'integer', label: 'Minimum score (inclusive)' },\n max: { type: 'integer', label: 'Maximum score (inclusive)' },\n message: { type: 'string', label: 'Message' },\n products: { type: 'array', description: 'Products for this score range' },\n },\n },\n },\n },\n description: 'Shopping configuration with provider-agnostic structure',\n },\n response_schema: null,\n output_schema: null,\n },\n end_screen: {\n label: 'Ending',\n shorthand: 'End',\n icon: 'Flag',\n color: '#FFEDD5',\n description: 'Final screen shown when form is completed',\n category: 'end',\n sort_order: 999,\n has_options: false,\n is_terminal: true,\n show_nav_bar: false,\n loading: 'eager',\n supports_prompt: false,\n supports_media: false,\n is_system: false,\n show_in_results: false,\n // Terminal node - never appears in a completed submission's answers\n // (filtered by NON_COUNTABLE_FILTER); false is a no-op default.\n notify_inline: false,\n default_config: {\n title: 'Thank you!',\n message: 'Your response has been submitted.',\n },\n config_schema: {\n type: 'object',\n properties: {\n title: { type: 'string', label: 'Title', default: 'Thank you!', description: 'Heading shown on completion' },\n message: { type: 'string', label: 'Message', default: 'Your response has been submitted.', description: 'Message shown on completion' },\n show_score: { type: 'boolean', label: 'Show score', default: false, description: 'Display score on the end screen (e.g. \"You scored 4 out of 5\")' },\n icon: { type: 'string', label: 'Icon', enum: ['tick', 'trophy', 'star', 'crown', 'party', 'none'], default: 'tick', description: 'Icon shown above the title' },\n show_share_button: { type: 'boolean', label: 'Show share button', default: false, description: 'Show a share button above the CTA' },\n cta_type: { type: 'string', label: 'CTA type', enum: ['none', 'restart', 'external_link'], default: 'none', description: 'Primary call-to-action button type' },\n cta_text: { type: 'string', label: 'CTA button text', default: 'Continue', description: 'Button label for the CTA' },\n cta_url: { type: 'string', label: 'CTA URL', format: 'uri', description: 'URL to open (only for external_link CTA type)', placeholder: 'https://example.com' },\n score_ranges: {\n type: 'array',\n label: 'Score-based content',\n description: 'Show different content based on cumulative score. First matching range wins.',\n items: {\n type: 'object',\n required: ['min', 'max', 'title'],\n properties: {\n min: { type: 'integer', label: 'Minimum score (inclusive)' },\n max: { type: 'integer', label: 'Maximum score (inclusive)' },\n title: { type: 'string', label: 'Title' },\n message: { type: 'string', label: 'Message' },\n },\n },\n },\n scoring_results: {\n type: 'array',\n label: 'Category-based results',\n description: 'Results keyed by scoring category. The winning category (highest non-knocked-out score) determines which result is shown.',\n items: {\n type: 'object',\n required: ['category', 'title'],\n properties: {\n category: { type: 'string', label: 'Category key (must match keys used in option scores)' },\n title: { type: 'string', label: 'Title' },\n message: { type: 'string', label: 'Message' },\n cta_url: { type: 'string', label: 'CTA URL', format: 'uri' },\n cta_text: { type: 'string', label: 'CTA button text' },\n },\n },\n },\n },\n },\n response_schema: null,\n output_schema: null,\n },\n};\n\n// Availability is derived from the FEATURES registry (single source of truth).\n// Each node type `x` maps to feature `node_x`; the throw below makes a missing\n// or renamed feature fail loudly at import, and a vitest guards the mapping.\nfor (const [type, def] of Object.entries(NODE_TYPES)) {\n const feature = FEATURES[`node_${type}`];\n if (!feature) {\n throw new Error(`NODE_TYPES.${type}: no matching FEATURES[\"node_${type}\"] entry`);\n }\n def.is_active = feature.enabled;\n}\n\n// Derived exports\n\n/** Sorted list of all node types with their type key included */\nexport const NODE_TYPES_LIST = Object.entries(NODE_TYPES)\n .map(([type, def]) => ({ type, ...def }))\n .sort((a, b) => a.sort_order - b.sort_order);\n\n/**\n * Node types the dashboard treats as active, as full definitions - the single\n * owner of the derivation previously inlined in NodeTypesProvider. A type is\n * active when it is globally active OR the company is allowlisted to dogfood it\n * ahead of the global flip. Mirrors the old dashboard filter exactly: system\n * nodes (e.g. `start`, which is is_active) stay in, so getNodeType('start')\n * still resolves its colour - the node picker filters `is_system` separately.\n * Pass no companyId for the plain globally-active set.\n */\nexport function getActiveNodeTypes(companyId) {\n return NODE_TYPES_LIST.filter(\n t => t.is_active || companyHasNodeType(companyId, t.type)\n );\n}\n\n/**\n * Active, non-system node type KEYS - the user-addable set consumed by the API\n * and MCP server. Derived from Object.entries(NODE_TYPES) (definition order) so\n * the list is byte-identical to the derivations it replaces, including the order\n * that surfaces in z.enum / validation error messages.\n */\nexport const ACTIVE_NODE_TYPES = Object.entries(NODE_TYPES)\n .filter(([, def]) => def.is_active && !def.is_system)\n .map(([type]) => type);\n\n/** All valid node type keys */\nexport const NODE_TYPE_KEYS = Object.keys(NODE_TYPES);\n\n/**\n * Node types -> their connection_config_keys, for types that declare any.\n * The SoT for save_form (cross-workspace copy, #2064 part 2) to strip\n * workspace-scoped connected-account refs from a copied node's config -\n * derived like ACTIVE_NODE_TYPES so no consumer hand-maintains a duplicate list.\n */\nexport const CONNECTION_CONFIG_KEYS = Object.fromEntries(\n Object.entries(NODE_TYPES)\n .filter(([, def]) => Array.isArray(def.connection_config_keys) && def.connection_config_keys.length > 0)\n .map(([type, def]) => [type, def.connection_config_keys])\n);\n\n/** Response schemas keyed by node type */\nexport const RESPONSE_SCHEMAS = Object.fromEntries(\n Object.entries(NODE_TYPES).map(([k, v]) => [k, v.response_schema])\n);\n\n/** Config schemas keyed by node type */\nexport const CONFIG_SCHEMAS = Object.fromEntries(\n Object.entries(NODE_TYPES).map(([k, v]) => [k, v.config_schema])\n);\n\n/** Output schemas keyed by node type */\nexport const OUTPUT_SCHEMAS = Object.fromEntries(\n Object.entries(NODE_TYPES).map(([k, v]) => [k, v.output_schema])\n);\n\n/** Metadata subset used by viewer for layout decisions */\nexport const NODE_TYPE_METADATA = Object.fromEntries(\n Object.entries(NODE_TYPES).map(([k, v]) => [k, {\n supports_prompt: v.supports_prompt,\n supports_media: v.supports_media,\n is_terminal: v.is_terminal,\n show_nav_bar: v.show_nav_bar,\n category: v.category,\n loading: v.loading,\n }])\n);\n\n/** Terminal node type keys (is_terminal === true) */\nexport const TERMINAL_TYPES = Object.entries(NODE_TYPES)\n .filter(([, v]) => v.is_terminal)\n .map(([k]) => k);\n\n/** Node types excluded from counts (system + terminal) */\nexport const NON_COUNTABLE_TYPES = Object.entries(NODE_TYPES)\n .filter(([, v]) => v.is_system || v.is_terminal)\n .map(([k]) => k);\n\n/** Supabase .not() filter string for non-countable types */\nexport const NON_COUNTABLE_FILTER = `(${NON_COUNTABLE_TYPES.map(t => `\"${t}\"`).join(',')})`;\n\n/** Node types excluded from the aggregate Results charts */\nexport const RESULTS_EXCLUDED_TYPES = Object.entries(NODE_TYPES)\n .filter(([, v]) => !v.show_in_results)\n .map(([k]) => k);\n\n/**\n * Node types excluded from the PUBLIC results portal on top of\n * `RESULTS_EXCLUDED_TYPES` (#2129): contact details, file uploads, and payment\n * data never leave the owner-only Results tab, even when a form's results are\n * shared publicly. The `get_public_form_results` RPC already excludes these in\n * SQL - this list is the belt-and-braces filter the API route applies again on\n * the returned rows.\n */\nexport const PUBLIC_RESULTS_EXCLUDED_TYPES = ['details', 'file_upload', 'payment'];\n\n/**\n * Node types that count as a step in the respondent-facing step counter (#488).\n * Only answer-collecting nodes count - the respondent thinks in questions\n * (\"3 of 5\"), not screens. Pure transitions (`button`) and terminals/start are\n * nav-bar or non-nav nodes that advance neither the current step nor the total.\n */\nconst NON_STEP_TYPES = new Set(['button']);\nexport const STEP_COUNTED_TYPES = Object.entries(NODE_TYPES)\n .filter(([k, v]) => v.show_nav_bar && !NON_STEP_TYPES.has(k))\n .map(([k]) => k);\n\n/**\n * Node types excluded from the per-response Responses table.\n * Defaults to the Results exclusions; a type can opt back IN for responses\n * with `show_in_responses: true` (e.g. contact - aggregate-meaningless but\n * actionable per-lead, #746).\n */\nexport const RESPONSES_EXCLUDED_TYPES = Object.entries(NODE_TYPES)\n .filter(([, v]) => !(v.show_in_responses ?? v.show_in_results))\n .map(([k]) => k);\n\n/** Check if a node type is terminal */\nexport function isTerminalType(type) {\n return NODE_TYPE_METADATA[type]?.is_terminal === true;\n}\n\n/** Check if a node type counts as a step in the respondent step counter (#488) */\nexport function countsAsStep(type) {\n return STEP_COUNTED_TYPES.includes(type);\n}\n\n","// @vid-master/config/form-types\n// Static form type definitions — single source of truth for all apps.\n// Workflow prose (step-by-step instructions) and craft guides remain in the\n// MCP server. This registry defines structural metadata only.\n\nimport { FEATURES } from './features.js';\nimport { NODE_TYPES } from './node-types.js';\n\nexport const FORM_TYPES = {\n quiz: {\n label: 'Quiz',\n description: 'Scored knowledge quiz with narrated video questions',\n sort_order: 1,\n category: 'assessment',\n aliases: ['trivia', 'test', 'exam'],\n variants: ['personality', 'comprehension', 'composition'],\n required_features: [],\n required_node_types: ['choice', 'end_screen'],\n default_media_style: 'video',\n guide_uri: 'clipform://guides/quiz',\n guide_description: 'Craft knowledge for writing engaging quizzes - difficulty curves, question psychology, narration style, scoring',\n discovery: [\n { key: 'topic', question: 'What topic or subject area?', fallback: 'General trivia', mode: 'all' },\n { key: 'question_count', question: 'How many questions?', fallback: '8', mode: 'all' },\n { key: 'media_style', question: 'Video with narration (recommended), images, or text only?', fallback: 'Video with narration', mode: 'full' },\n ],\n variant_discovery: {\n personality: [\n { key: 'topic', question: \"What's the theme? (Which X are you?)\", fallback: 'General personality', mode: 'all' },\n { key: 'categories', question: 'What outcome categories? (3-5 results)', fallback: 'Infer 4 categories from the theme', mode: 'full' },\n { key: 'question_count', question: 'How many questions?', fallback: '8', mode: 'all' },\n ],\n comprehension: [\n { key: 'youtube_url', question: \"What's the YouTube URL?\", fallback: 'REQUIRED - cannot proceed without it', mode: 'all' },\n { key: 'question_count', question: 'How many questions?', fallback: '8', mode: 'all' },\n { key: 'audience', question: \"Who's the audience? (kids, teens, adults)\", fallback: 'General adults', mode: 'full' },\n ],\n composition: [\n { key: 'topic', question: 'What topic? (flags, movies, geography, music...)', fallback: 'General trivia', mode: 'all' },\n { key: 'question_count', question: 'How many questions? (composition quizzes use ~1 node per question, plus one more for any optional reveal beat - 5-6 is the sweet spot)', fallback: '5', mode: 'all' },\n ],\n },\n variant_guide_uris: {\n personality: 'clipform://guides/quiz/personality',\n comprehension: 'clipform://guides/quiz/comprehension',\n composition: 'clipform://guides/quiz/composition',\n },\n variant_descriptions: {\n personality: 'Addendum for personality quizzes - category design, option weighting, outcome writing, no right/wrong answers',\n comprehension: 'Addendum for YouTube comprehension quizzes - extracting questions from transcripts, distractor design, audience adaptation',\n composition: 'Addendum for composition quizzes - guess formats built from rendered compositions: mechanic selection, the clue clip + answer feedback default (with an optional reveal payoff node), difficulty design',\n },\n prompt: { name: 'create-quiz', title: 'Create a Quiz', description: 'Build a scored knowledge quiz with narrated video questions' },\n },\n\n survey: {\n label: 'Survey',\n description: 'Feedback survey, NPS form, or research questionnaire',\n sort_order: 2,\n category: 'feedback',\n aliases: ['feedback', 'poll', 'nps', 'questionnaire'],\n variants: [],\n required_features: [],\n required_node_types: ['choice', 'open', 'end_screen'],\n default_media_style: 'text',\n guide_uri: 'clipform://guides/survey',\n guide_description: 'Craft knowledge for feedback surveys, NPS, and research forms - brevity, rating scales, respondent fatigue',\n discovery: [\n { key: 'topic', question: 'What are you measuring? (satisfaction, NPS, feedback on what?)', fallback: 'General satisfaction', mode: 'all' },\n { key: 'anonymous', question: 'Anonymous or identified responses?', fallback: 'Anonymous', mode: 'full' },\n ],\n variant_discovery: {},\n variant_guide_uris: {},\n variant_descriptions: {},\n prompt: { name: 'create-survey', title: 'Create a Survey', description: 'Build a feedback survey, NPS form, or research questionnaire' },\n },\n\n interview: {\n label: 'Interview',\n description: 'Async video interview, case study, or expert input collection',\n sort_order: 3,\n category: 'collection',\n aliases: ['case-study', 'callout'],\n variants: [],\n required_features: [],\n required_node_types: ['open', 'details', 'end_screen'],\n default_media_style: 'video',\n guide_uri: 'clipform://guides/interview',\n guide_description: 'Craft knowledge for building interview forms - warm-up pacing, open questions, consent, video responses',\n discovery: [\n { key: 'purpose', question: 'What are you collecting? (case studies, feedback, expert input)', fallback: 'General responses', mode: 'all' },\n { key: 'response_format', question: 'How should people respond? (video, audio, text, all)', fallback: 'All formats', mode: 'full' },\n { key: 'needs_consent', question: 'Need a consent statement?', fallback: 'Yes', mode: 'full' },\n ],\n variant_discovery: {},\n variant_guide_uris: {},\n variant_descriptions: {},\n prompt: { name: 'create-interview', title: 'Create an Interview', description: 'Build an async video interview for collecting case studies, feedback, or expert input' },\n },\n\n funnel: {\n label: 'Funnel',\n description: 'Lead qualification funnel with conditional routing',\n sort_order: 4,\n category: 'qualification',\n aliases: ['lead-gen', 'qualification', 'lead-magnet'],\n variants: [],\n required_features: ['branching', 'funnel'],\n required_node_types: ['choice', 'details', 'end_screen'],\n default_media_style: 'text',\n guide_uri: 'clipform://guides/funnel',\n guide_description: 'Craft knowledge for lead qualification funnels - planned feature, conditional routing coming soon',\n discovery: [],\n variant_discovery: {},\n variant_guide_uris: {},\n variant_descriptions: {},\n prompt: { name: 'create-funnel', title: 'Create a Funnel', description: 'Lead qualification funnels with branching logic are planned but not yet available' },\n },\n\n testimonial: {\n label: 'Testimonial',\n description: 'Video testimonial and story collection form',\n sort_order: 5,\n category: 'collection',\n aliases: ['story', 'review'],\n variants: [],\n required_features: [],\n required_node_types: ['open', 'details', 'end_screen'],\n default_media_style: 'video',\n guide_uri: 'clipform://guides/testimonial',\n guide_description: 'Craft knowledge for collecting testimonials and customer stories on video - storytelling prompts, comfort techniques, consent',\n discovery: [\n { key: 'use_case', question: 'What stories are you collecting? (customer success, employee, partner)', fallback: 'Customer success stories', mode: 'all' },\n { key: 'media_style', question: 'Video responses (recommended), audio, or text?', fallback: 'Video', mode: 'full' },\n ],\n variant_discovery: {},\n variant_guide_uris: {},\n variant_descriptions: {},\n prompt: { name: 'create-testimonial', title: 'Collect Testimonials', description: 'Build a video testimonial collection form - capture authentic customer stories on camera' },\n },\n\n application: {\n label: 'Application',\n description: 'Structured application form for jobs, programmes, or grants',\n sort_order: 6,\n category: 'collection',\n aliases: ['job-application', 'admission', 'enrollment', 'grant'],\n variants: [],\n required_features: [],\n required_node_types: ['open', 'choice', 'details', 'end_screen'],\n default_media_style: 'video',\n guide_uri: 'clipform://guides/application',\n guide_description: 'Craft knowledge for application and evaluation forms - multi-section structure, video responses for behavioural questions, screening',\n discovery: [\n { key: 'role', question: 'What role or position?', fallback: 'General application', mode: 'all' },\n ],\n variant_discovery: {},\n variant_guide_uris: {},\n variant_descriptions: {},\n prompt: { name: 'create-application', title: 'Create an Application Form', description: 'Build a structured application form with video responses for behavioural questions' },\n },\n\n booking: {\n label: 'Booking',\n description: 'Event registration, course signup, or booking form',\n sort_order: 7,\n category: 'registration',\n aliases: ['registration', 'signup', 'event', 'rsvp', 'workshop'],\n variants: [],\n required_features: [],\n required_node_types: ['details', 'choice', 'end_screen'],\n default_media_style: 'text',\n guide_uri: 'clipform://guides/booking',\n guide_description: 'Craft knowledge for event registration and booking forms - minimal friction, video welcome, confirmation flow',\n discovery: [\n { key: 'event_name', question: \"What's the event name?\", fallback: 'Event registration', mode: 'all' },\n { key: 'event_type', question: 'What type? (workshop, webinar, meetup, conference)', fallback: 'Event', mode: 'full' },\n ],\n variant_discovery: {},\n variant_guide_uris: {},\n variant_descriptions: {},\n prompt: { name: 'create-booking', title: 'Create a Booking Form', description: 'Build an event registration or booking form with a personal video welcome' },\n },\n\n intake: {\n label: 'Client Intake',\n description: 'Client intake and onboarding form for a service business',\n sort_order: 8,\n category: 'collection',\n aliases: ['client-intake', 'onboarding', 'enquiry', 'inquiry', 'new-client'],\n variants: [],\n required_features: [],\n required_node_types: ['details', 'open', 'end_screen'],\n default_media_style: 'video',\n guide_uri: 'clipform://guides/intake',\n guide_description: 'Craft knowledge for client intake and onboarding forms - a warm welcome, minimal contact capture, a video-first question about what the client needs, and a clear next step',\n discovery: [\n { key: 'use_case', question: 'What kind of business or service? (massage, coaching, personal training, agency)', fallback: 'General client intake', mode: 'all' },\n { key: 'response_format', question: 'How should people answer the open question? (video, audio, text, all)', fallback: 'All formats', mode: 'full' },\n ],\n variant_discovery: {},\n variant_guide_uris: {},\n variant_descriptions: {},\n prompt: { name: 'create-intake', title: 'Create a Client Intake Form', description: 'Build a warm client intake or onboarding form that captures contact details and what the client needs' },\n },\n\n 'lead-capture': {\n label: 'Lead Capture',\n description: 'Linear lead capture or enquiry form - turn interest into a contact with minimal friction',\n sort_order: 9,\n category: 'collection',\n aliases: ['lead', 'lead-form', 'enquiry-form', 'waitlist', 'get-a-quote'],\n variants: [],\n required_features: [],\n required_node_types: ['details', 'end_screen'],\n default_media_style: 'text',\n guide_uri: 'clipform://guides/lead-capture',\n guide_description: 'Craft knowledge for linear lead capture and enquiry forms - minimal friction, a light qualifier only if it routes follow-up, and a clear next step (no branching, no scoring)',\n discovery: [\n { key: 'use_case', question: 'What are you capturing leads for? (a service, a product, a waitlist, a quote)', fallback: 'General enquiries', mode: 'all' },\n ],\n variant_discovery: {},\n variant_guide_uris: {},\n variant_descriptions: {},\n prompt: { name: 'create-lead-capture', title: 'Create a Lead Capture Form', description: 'Build a short lead capture or enquiry form that turns interest into a contact' },\n },\n};\n\n// ---------------------------------------------------------------------------\n// Derived exports\n// ---------------------------------------------------------------------------\n\n// Availability is derived from dependencies (single source of truth): a form type\n// is active iff every required feature is enabled and every required node type is\n// active. No hand-set flag to drift - e.g. funnel auto-hides while branching is planned.\nfor (const def of Object.values(FORM_TYPES)) {\n const featuresLive = def.required_features.every((k) => FEATURES[k]?.enabled === true);\n const nodesLive = def.required_node_types.every((t) => NODE_TYPES[t]?.is_active === true);\n def.is_active = featuresLive && nodesLive;\n}\n\nexport const FORM_TYPES_LIST = Object.entries(FORM_TYPES)\n .map(([key, def]) => ({ key, ...def }))\n .sort((a, b) => a.sort_order - b.sort_order);\n\nexport const FORM_TYPE_KEYS = Object.keys(FORM_TYPES);\n\nexport const WORKFLOW_TYPES = FORM_TYPE_KEYS.filter(\n key => FORM_TYPES[key].is_active\n);\n\nexport const FORM_TYPE_ALIASES = Object.fromEntries(\n Object.entries(FORM_TYPES).flatMap(([key, def]) =>\n def.aliases.map(alias => [alias, key])\n )\n);\n\nexport const ALL_VARIANTS = Object.entries(FORM_TYPES).flatMap(\n ([, def]) => def.variants\n);\n\nexport function resolveFormType(input) {\n const normalised = input.toLowerCase().trim();\n if (FORM_TYPES[normalised]) return normalised;\n return FORM_TYPE_ALIASES[normalised] ?? null;\n}\n\nexport function getDiscoveryParams(formType, variant, mode = 'full') {\n const def = FORM_TYPES[formType];\n if (!def) return [];\n\n let params;\n if (variant && def.variant_discovery?.[variant]) {\n params = def.variant_discovery[variant];\n } else {\n params = def.discovery;\n }\n\n if (mode === 'demo') {\n return params.filter(p => p.mode === 'all');\n }\n return params;\n}\n\nexport function isFormTypeAvailable(formType, features) {\n const def = FORM_TYPES[formType];\n if (!def || !def.is_active) return false;\n return def.required_features.every(\n featureKey => features[featureKey]?.enabled === true\n );\n}\n\nexport function getAvailableFormTypes(features) {\n return FORM_TYPE_KEYS.filter(key => isFormTypeAvailable(key, features));\n}\n","// @vid-master/config/help-links\n// Single source of truth for every in-product \"Learn more\" deep-link (#394).\n// In-product copy stays a short, actionable summary; the full explanation\n// lives in docs and we link to it, so the two can't drift.\n//\n// HELP_TOPICS is pure data (docs path + anchor, no base URL) so the docs\n// link-check script (apps/docs/scripts/check-help-links.mjs) can verify every\n// anchor resolves to a real heading without touching env. Consumers use\n// HELP_LINKS from the package root, which composes full URLs off\n// BUSINESS.urls.docs.\n\n/**\n * Keyed by concept. `path` is relative to the docs site root (which already\n * includes the /docs basePath); `anchor` must match a heading slug in the\n * target page (custom [#anchor] syntax or the auto-generated slug).\n */\nexport const HELP_TOPICS = {\n goLive: { path: '/help/sharing', anchor: 'go-live' },\n cantGoLive: { path: '/help/sharing', anchor: 'cant-go-live' },\n liveAndDraft: { path: '/help/managing-forms', anchor: 'live-and-draft' },\n publishingChanges: { path: '/help/managing-forms', anchor: 'publishing-changes' },\n embedding: { path: '/help/embed', anchor: null },\n emailShare: { path: '/help/sharing', anchor: 'share-by-email' },\n publicResults: { path: '/help/public-results', anchor: null },\n trash: { path: '/help/managing-forms', anchor: 'trash' },\n agreementVersions: { path: '/help/responses', anchor: 'agreement-versions' },\n customDomains: { path: '/help/domains', anchor: null },\n customPath: { path: '/help/links', anchor: 'custom-path-on-your-domain' },\n // Per-node-type deep-links into help/node-types.mdx (#3963). Keyed\n // `node_<nodeType>` so nodeHelpLink() (index.js) can look one up straight\n // from a node's type id.\n node_choice: { path: '/help/node-types', anchor: 'choice' },\n node_open: { path: '/help/node-types', anchor: 'open-ended' },\n node_details: { path: '/help/node-types', anchor: 'details' },\n node_button: { path: '/help/node-types', anchor: 'button' },\n node_end_screen: { path: '/help/node-types', anchor: 'end-screen' },\n node_redirect: { path: '/help/node-types', anchor: 'redirect' },\n node_file_download: { path: '/help/node-types', anchor: 'file-download' },\n node_payment: { path: '/help/node-types', anchor: 'payment' },\n};\n\n/** Compose full HELP_LINKS URLs from a docs base URL (no trailing slash). */\nexport function buildHelpLinks(docsBaseUrl) {\n const links = {};\n for (const [key, { path, anchor }] of Object.entries(HELP_TOPICS)) {\n links[key] = `${docsBaseUrl}${path}${anchor ? `#${anchor}` : ''}`;\n }\n return links;\n}\n","// @vid-master/config/logic-types\n// Canonical shape for node branching logic — the VideoAsk-pattern \"actions\" graph\n// stored in node_logic.actions (jsonb). Single source of truth shared by:\n// - apps/viewer (evaluates actions to pick the next node)\n// - apps/api (writes + validates actions)\n// - apps/dashboard (builder edges <-> actions)\n//\n// The types themselves live in logic-types.d.ts. This file exports the runtime\n// constant sets used for validation. The grammar is intentionally small today\n// (one action, one operator); richer branching extends these arrays without a\n// DB migration or API signature change.\n\n/** Action verbs a logic entry can perform. */\nexport const LOGIC_ACTION_TYPES = ['jump'];\n\n/**\n * Condition operators. 'is' is the original choice/binary equality op.\n * 'eq'/'ne'/'gt'/'gte'/'lt'/'lte' are the numeric comparison ops added for\n * the conditional node (#4105) - a branch condition compares a `variable`\n * var against a `constant` var.\n */\nexport const LOGIC_CONDITION_OPS = ['is', 'eq', 'ne', 'gt', 'gte', 'lt', 'lte'];\n\n/**\n * Variable types a condition can reference. 'variable' (#4105) references a\n * built-in routable variable (see ROUTABLE_VARIABLES, e.g. the generic\n * \"score\" number); 'constant' is a literal comparison value.\n */\nexport const LOGIC_VAR_TYPES = ['node', 'option', 'variable', 'constant'];\n\n/** Jump target types. */\nexport const LOGIC_TARGET_TYPES = ['node', 'end_screen', 'redirect', 'link_list'];\n\n/**\n * The v1 fence for routable built-in variables (#4105, phase 1 of the\n * variables engine - #4104). Exactly one today: the generic number derived\n * from node_options.score -> ScoreState.cumulativeScore, presented\n * generically as \"Number\" (Typeform-style) rather than a quiz \"score\".\n * User-defined variables are a later slice - widen this array, no other\n * grammar change, when they land.\n */\nexport const ROUTABLE_VARIABLES = ['score'];\n\n// ── Active-graph semantics ───────────────────────────────────────────────────\n// Toggling branching never deletes node_logic - it changes how the SAME actions\n// are interpreted. A per-option (option-var) edge is live only when its source\n// is a choice/binary node with branching enabled; otherwise it is \"parked\" and\n// the node routes via its node-var/default edge. These helpers are the single\n// source of truth for that rule, so every consumer that walks the logic graph -\n// the publish gate, viewer routing, node ordering and builder validation - reads\n// the identical active graph. (#1021 / #704)\n\n/**\n * Whether per-option (branch) edges are live for a node.\n *\n * Branching is OPT-IN: a choice/binary node routes per-option only when it\n * explicitly sets enable_branching:true. Unset / false -> off (the node routes\n * via its node-var/default edge). This is the single source the publish gate,\n * viewer routing, node ordering and builder validation all derive from, so they\n * read the identical active graph; off-by-default holds even if a brand-new\n * node's config isn't persisted yet. Non-choice nodes always route their edges.\n */\nexport function isBranchingActive(node) {\n if (!node) return true;\n return node.type === 'choice' || node.type === 'binary'\n ? node.config?.choice?.enable_branching === true\n : true;\n}\n\n/** True if `action` routes per-option (its condition references an option var). */\nexport function isOptionEdge(action) {\n return !!action?.condition?.vars?.some(v => v.type === 'option');\n}\n\n/**\n * True if `action` is a parked option edge on `node` - a per-option edge whose\n * source node has branching off. Such edges are excluded from the active graph.\n */\nexport function isInactiveOptionEdge(node, action) {\n return isOptionEdge(action) && !isBranchingActive(node);\n}\n\n// ── Reference integrity ──────────────────────────────────────────────────────\n// \"Every jump target / node-var / option-var must point to something that exists\n// in the form.\" Single source of the rule + message vocabulary, shared by the\n// write-time gate (set-node-logic) and the publish-time validator\n// (graph-validation's dangling_target / stale_option_ref / self_loop apply the\n// same rule at reachable-graph scope). Keeps the API 400 and the publish error\n// speaking the same language. (#1058 / #1270 / #1406)\n\n/**\n * Reference-integrity errors for a single logic action.\n *\n * @param {object} action - a node_logic action ({ details.to.value, condition.vars })\n * @param {Set<string>} nodeIds - all node ids in the form\n * @param {Set<string>} optionIds - option ids on the action's node\n * @param {string} [sourceNodeId] - the node this action lives on. When given, a\n * jump target that equals this node's own id is flagged as `self_loop` instead\n * of `dangling_target` (the target does exist - it's the node itself). Optional\n * for backward compatibility; existing 3-arg callers are unaffected.\n * @returns {Array<{ kind: 'dangling_target' | 'stale_option_ref' | 'self_loop', value: string, reason: string }>}\n * one entry per ref that doesn't resolve; [] when all refs exist.\n */\nexport function actionReferenceErrors(action, nodeIds, optionIds, sourceNodeId) {\n const errors = [];\n\n const to = action?.details?.to?.value;\n if (typeof to === 'string') {\n if (sourceNodeId && to === sourceNodeId) {\n errors.push({ kind: 'self_loop', value: to, reason: `jump target ${to} connects the node to itself - remove the self-referencing connection` });\n } else if (!nodeIds.has(to)) {\n errors.push({ kind: 'dangling_target', value: to, reason: `jump target ${to} not found in this form` });\n }\n }\n\n // Explicit branch anchor (#4105): a conditional's branch edge carries its\n // option (node_options row) id as a top-level field rather than inferring\n // it from a condition var - see buildAdjacencyMap/buildLogicMap. Validated\n // the same way as a condition's option var below.\n if (typeof action?.option_id === 'string' && !optionIds.has(action.option_id)) {\n errors.push({ kind: 'stale_option_ref', value: action.option_id, reason: `references option ${action.option_id} not on this node` });\n }\n\n for (const v of action?.condition?.vars || []) {\n if (typeof v?.value !== 'string') continue;\n if (v.type === 'node' && !nodeIds.has(v.value)) {\n errors.push({ kind: 'dangling_target', value: v.value, reason: `references node ${v.value} not in this form` });\n } else if (v.type === 'option' && !optionIds.has(v.value)) {\n errors.push({ kind: 'stale_option_ref', value: v.value, reason: `references option ${v.value} not on this node` });\n } else if (v.type === 'variable' && !ROUTABLE_VARIABLES.includes(v.value)) {\n errors.push({ kind: 'unresolved_variable', value: v.value, reason: `references variable \"${v.value}\" which is not a routable variable` });\n }\n }\n\n return errors;\n}\n","// @vid-master/config - Video template layer (#1736, Tier 4 of #1731)\n//\n// A video template is DATA + a pure expander, NOT a new Remotion composition\n// and NOT a COMPOSITION_REGISTRY primitive. `buildVideoTemplate(name,\n// controls)` fills a curated, small control surface into a full Scene props\n// object (the layer stack `packages/remotion/src/compositions/scene/Scene.schema.ts`\n// defines) - video templates always render THROUGH the existing Scene\n// composition. Kept dependency-free (no zod/React/Remotion imports) so both\n// the API and the MCP server can import it directly and share one expansion.\n//\n// Adding a video template: add its builder + a VIDEO_TEMPLATE_REGISTRY entry.\n// The builder's output must satisfy Scene.schema.ts's strict `sceneSchema`\n// (see packages/config/test/video-templates.test.js for the shape assertions\n// this package can run without depending on packages/remotion).\n//\n// VIDEO_LOOKS / expandVideoLook (#4038): the ONE named-look SSOT shared by\n// this file's `slideshow` recipe (buildSlideshow) and the `clipform_generate_video`\n// MCP tool / API route - both speak the same look vocabulary so they can't\n// drift into two competing taste layers. Each look COMPOSES atomic\n// vocabularies that already exist in this package (a KEN_BURNS_STYLE_PRESETS\n// motion preset, a SLIDESHOW_COMPOSITION_TRANSITIONS transition, an optional\n// texture/duotone shape, an optional background colour) - never introduce a\n// new atomic dial here, or this recreates the fragmentation it removes.\n// SLIDESHOW_COMPOSITION_TRANSITIONS is DEFINED below (not imported from\n// index.js) - index.js already re-exports from this file, so importing it\n// back here would be a real index.js <-> video-templates.js cycle (#4038\n// review). index.js re-exports it from here instead, so the public import\n// path ('@vid-master/config') is unchanged for every external consumer.\n\n/**\n * Text preset background colours, mirrored from\n * packages/remotion/src/compositions/text/Text.tsx PRESETS so a template's\n * solid-colour bed matches its Text overlay's own palette when no\n * `backgroundImage` control is supplied. Keep in sync if Text's presets\n * change - there is no cross-package import here (config must stay\n * dependency-free of remotion).\n */\nconst PRESET_BG = {\n bold: '#0a0a14',\n elegant: '#1a1a2e',\n minimal: '#fafafa',\n typewriter: '#0d1117',\n neon: '#0a0a0a',\n cinematic: '#000000',\n};\n\nconst PRESET_NAMES = Object.keys(PRESET_BG);\n\n/** Resolve a controls.preset to a known preset, else the caller's fallback\n * (the shared TEMPLATE_PRESET for the text-over-bed family). */\nfunction resolvePreset(preset, fallback) {\n return PRESET_NAMES.includes(preset) ? preset : fallback;\n}\n\n/**\n * Motion themes (#1810): the pacing/energy axis, applied at buildVideoTemplate\n * time across every card so a form's video reads as one piece. LEAN V1 drives\n * per-card hold duration ONLY - `balanced` is the identity (== pre-1810 output),\n * so an omitted/unknown theme changes nothing. Entrance style/speed and\n * card-to-card transitions are deliberately out of scope (see #1810).\n */\nconst THEME_MOTION = {\n calm: { holdMultiplier: 1.4 },\n balanced: { holdMultiplier: 1.0 },\n energetic: { holdMultiplier: 0.7 },\n};\nconst THEME_NAMES = Object.keys(THEME_MOTION);\n\n/** Resolve controls.theme to a known theme's motion params, else `balanced`\n * (mirrors resolvePreset's unknown-falls-back-to-default shape). */\nfunction resolveThemeMotion(theme) {\n return THEME_NAMES.includes(theme) ? THEME_MOTION[theme] : THEME_MOTION.balanced;\n}\n\n/** Apply the resolved theme's hold multiplier to a template's base duration\n * (its `*_DEFAULT_SECONDS` constant). Centralizes the theme -> duration math\n * so every builder calls one helper instead of re-deriving it inline. */\nfunction holdSeconds(base, controls) {\n return base * resolveThemeMotion(controls.theme).holdMultiplier;\n}\n\n/** True when the controls ask for (and supply) an image bed rather than the\n * preset's solid colour. `variant: 'solid'` always forces the solid bed. */\nfunction usesImageBed(controls) {\n return controls.variant !== 'solid'\n && typeof controls.backgroundImage === 'string'\n && controls.backgroundImage.length > 0;\n}\n\n/**\n * The bed layer shared by the text-over-bed templates: a single-image\n * Slideshow when `useImage`, else a solid-colour Slideshow bed painted with\n * the resolved preset's background. Kept in one place so every template's bed\n * stays identical (one expansion, no drift).\n */\nfunction buildBed({ useImage, backgroundImage, resolvedPreset, durationInSeconds }) {\n return {\n composition: 'Slideshow',\n props: {\n items: useImage\n ? [{ type: 'image', url: backgroundImage, effect: 'static' }]\n : [],\n backgroundColor: PRESET_BG[resolvedPreset],\n durationInSeconds,\n transition: { type: 'none', durationInFrames: 0 },\n },\n };\n}\n\n/** A dark wash under the overlay so text stays legible over a busy image bed;\n * no scrim over a solid bed (the preset colour already reads clean). */\nfunction overlayScrim(useImage) {\n return useImage ? { scrim: TEMPLATE_SCRIM } : {};\n}\n\n/**\n * Shared look for the text-over-bed template family (title-card,\n * question-card, stat-highlight, quote-card) so they read as ONE family:\n * every card uses the same preset, the same type scale, and the same\n * entrance - only the words and the optional eyebrow / sub lines differ.\n * Change the look in one place here, not per template. `controls.preset`\n * still overrides a single card when someone wants to break the family.\n */\nconst TEMPLATE_PRESET = 'minimal';\nconst TEMPLATE_EYEBROW_POSITION = 0.32; // small line ABOVE the headline\nconst TEMPLATE_HEADLINE_POSITION = 0.42;\nconst TEMPLATE_SUBLINE_START = 0.6; // first line below the headline\nconst TEMPLATE_SUBLINE_STEP = 0.1; // vertical gap between stacked sub lines\nconst TEMPLATE_SUBLINE_FONT_SIZE = 34;\nconst TEMPLATE_SCRIM = 0.4;\n\n/**\n * Expand the shared text-over-bed layout into Scene layers: a Slideshow bed\n * (solid preset colour, or an image) under a single Text overlay. The Text\n * overlay carries an optional eyebrow, a required headline, and any number of\n * sub lines, all on the shared preset + type scale + entrance (lines omit a\n * per-line `entrance`, so the preset's own entrance drives every card\n * identically). This is the one code path every text template shares - it is\n * what keeps them looking the same.\n */\nfunction buildTextOverBed({ controls, eyebrow, headline, sublines = [], durationInSeconds }) {\n const resolvedPreset = resolvePreset(controls.preset, TEMPLATE_PRESET);\n const useImage = usesImageBed(controls);\n const bed = buildBed({ useImage, backgroundImage: controls.backgroundImage, resolvedPreset, durationInSeconds });\n\n const lines = [];\n if (typeof eyebrow === 'string' && eyebrow) {\n lines.push({ text: eyebrow, position: TEMPLATE_EYEBROW_POSITION, fontSize: TEMPLATE_SUBLINE_FONT_SIZE });\n }\n lines.push({ text: headline, position: TEMPLATE_HEADLINE_POSITION });\n sublines\n .filter((s) => typeof s === 'string' && s)\n .forEach((text, i) => {\n lines.push({ text, position: TEMPLATE_SUBLINE_START + i * TEMPLATE_SUBLINE_STEP, fontSize: TEMPLATE_SUBLINE_FONT_SIZE });\n });\n\n const overlay = {\n composition: 'Text',\n props: { lines, style: { preset: resolvedPreset } },\n ...overlayScrim(useImage),\n };\n\n return { layers: [bed, overlay], durationInSeconds };\n}\n\nconst TITLE_CARD_DEFAULT_SECONDS = 5;\n\n/**\n * title-card: intro/outro card - an optional tagline eyebrow, the title, and\n * an optional subtitle, on the shared template look.\n */\nfunction buildTitleCard(controls) {\n const { title, subtitle, tagline } = controls;\n if (!title || typeof title !== 'string') {\n throw new Error(\"video template 'title-card': controls.title (non-empty string) is required\");\n }\n return buildTextOverBed({\n controls,\n eyebrow: tagline,\n headline: title,\n sublines: [subtitle],\n durationInSeconds: holdSeconds(TITLE_CARD_DEFAULT_SECONDS, controls),\n });\n}\n\nconst OBSCURE_VARIANTS = ['blur', 'pixelate', 'zoom'];\nconst MYSTERY_REVEAL_DEFAULT_SECONDS = 6;\n\n/**\n * mystery-reveal (#1817 rename, was flag-reveal): image bed + the `obscure`\n * Scene-layer effect (#1735). Accepted reframe (#1736 decision 2) - the old\n * vector flag-draw composition is gone; this is \"an image that resolves from\n * obscured to sharp\", `variant` picking the obscure style\n * (blur/pixelate/zoom). Renamed from `flag-reveal` because the mechanic isn't\n * flag-specific - any \"guess what this is\" image reveal fits.\n */\nfunction buildMysteryReveal(controls) {\n const { image, variant, revealSeconds } = controls;\n if (!image || typeof image !== 'string') {\n throw new Error(\"video template 'mystery-reveal': controls.image (non-empty string url) is required\");\n }\n const style = OBSCURE_VARIANTS.includes(variant) ? variant : 'blur';\n const durationInSeconds = holdSeconds(MYSTERY_REVEAL_DEFAULT_SECONDS, controls);\n\n const bed = {\n composition: 'Slideshow',\n props: {\n items: [{ type: 'image', url: image, effect: 'static' }],\n durationInSeconds,\n transition: { type: 'none', durationInFrames: 0 },\n },\n effect: {\n obscure: {\n style,\n ...(typeof revealSeconds === 'number' ? { revealSeconds } : {}),\n },\n },\n };\n\n return { layers: [bed], durationInSeconds };\n}\n\nconst COLOUR_GRID_DEFAULT_SECONDS = 5;\n\n/**\n * colour-grid: Grid with colour cells (#1736 decision 3) - replaces the old\n * bespoke ColorCards swatch-drop composition with the general Grid primitive\n * + its new `color` cell type.\n */\nfunction buildColourGrid(controls) {\n const { colors, labels, variant } = controls;\n if (!Array.isArray(colors) || colors.length === 0) {\n throw new Error(\"video template 'colour-grid': controls.colors (non-empty string[]) is required\");\n }\n const showLabels = variant !== 'plain';\n const cells = colors.map((color, i) => ({\n type: 'color',\n color,\n ...(Array.isArray(labels) && labels[i] ? { label: labels[i] } : {}),\n }));\n\n const bed = {\n composition: 'Grid',\n props: {\n cells,\n showLabels,\n animate: true,\n },\n };\n\n return { layers: [bed], durationInSeconds: holdSeconds(COLOUR_GRID_DEFAULT_SECONDS, controls) };\n}\n\nconst QUESTION_CARD_DEFAULT_SECONDS = 5;\n\n/**\n * question-card: a question framed over an image or solid-colour bed, with an\n * optional helper line beneath it. The everyday workhorse for a form's\n * question nodes. Shares the text-over-bed family look.\n */\nfunction buildQuestionCard(controls) {\n const { question, helper } = controls;\n if (!question || typeof question !== 'string') {\n throw new Error(\"video template 'question-card': controls.question (non-empty string) is required\");\n }\n return buildTextOverBed({\n controls,\n headline: question,\n sublines: [helper],\n durationInSeconds: holdSeconds(QUESTION_CARD_DEFAULT_SECONDS, controls),\n });\n}\n\nconst STAT_HIGHLIGHT_DEFAULT_SECONDS = 5;\n\n/**\n * stat-highlight: one number/stat over a bed, with an optional label and\n * caption beneath (\"10,000+ forms created\"). Shares the text-over-bed family\n * look, so the figure reads in the same type as every other card.\n */\nfunction buildStatHighlight(controls) {\n const { value, label, caption } = controls;\n if (!value || typeof value !== 'string') {\n throw new Error(\"video template 'stat-highlight': controls.value (non-empty string, e.g. \\\"10,000+\\\") is required\");\n }\n return buildTextOverBed({\n controls,\n headline: value,\n sublines: [label, caption],\n durationInSeconds: holdSeconds(STAT_HIGHLIGHT_DEFAULT_SECONDS, controls),\n });\n}\n\nconst QUOTE_CARD_DEFAULT_SECONDS = 6;\n\n/**\n * quote-card: a testimonial or pull-quote over a bed, with optional\n * attribution beneath it. The other half of social proof. Shares the\n * text-over-bed family look.\n */\nfunction buildQuoteCard(controls) {\n const { quote, attribution } = controls;\n if (!quote || typeof quote !== 'string') {\n throw new Error(\"video template 'quote-card': controls.quote (non-empty string) is required\");\n }\n return buildTextOverBed({\n controls,\n headline: `\"${quote}\"`,\n sublines: [attribution ? `- ${attribution}` : undefined],\n durationInSeconds: holdSeconds(QUOTE_CARD_DEFAULT_SECONDS, controls),\n });\n}\n\nconst LIST_CARD_DEFAULT_SECONDS = 6;\nconst LIST_CARD_MAX_ITEMS = 4;\n\n/**\n * list-card: headline + a short bulleted list revealed sequentially - an\n * agenda / steps / \"what you'll get\" card. Shares the text-over-bed family\n * look. Capped to 4 items: sublines start at position 0.6 and step 0.1, so a\n * 5th line would run off-frame.\n */\nfunction buildListCard(controls) {\n const { title, items } = controls;\n if (!title || typeof title !== 'string') {\n throw new Error(\"video template 'list-card': controls.title (non-empty string) is required\");\n }\n if (!Array.isArray(items) || items.length === 0) {\n throw new Error(\"video template 'list-card': controls.items (non-empty string[]) is required\");\n }\n const bullets = items\n .filter((item) => typeof item === 'string' && item)\n .slice(0, LIST_CARD_MAX_ITEMS)\n .map((item) => `• ${item}`);\n return buildTextOverBed({\n controls,\n headline: title,\n sublines: bullets,\n durationInSeconds: holdSeconds(LIST_CARD_DEFAULT_SECONDS, controls),\n });\n}\n\nconst CTA_CARD_DEFAULT_SECONDS = 5;\n\n/**\n * cta-card: closing call-to-action - headline + action line + optional\n * subtext. Shares the text-over-bed family look.\n */\nfunction buildCtaCard(controls) {\n const { headline, action, subtext } = controls;\n if (!headline || typeof headline !== 'string') {\n throw new Error(\"video template 'cta-card': controls.headline (non-empty string) is required\");\n }\n if (!action || typeof action !== 'string') {\n throw new Error(\"video template 'cta-card': controls.action (non-empty string) is required\");\n }\n return buildTextOverBed({\n controls,\n headline,\n sublines: [action, subtext],\n durationInSeconds: holdSeconds(CTA_CARD_DEFAULT_SECONDS, controls),\n });\n}\n\n/**\n * Ken Burns effect names the Slideshow layer's `effect` field accepts\n * (mirrors `packages/remotion/src/compositions/slideshow/Slideshow.tsx`'s\n * `kenBurnsEffectSchema` - kept in sync here, no cross-package import, same\n * pattern as PRESET_BG above).\n */\nconst KEN_BURNS_EFFECTS = [\n 'pan-left', 'pan-right', 'pan-up', 'pan-down',\n 'zoom-in-pan-left', 'zoom-in-pan-right',\n 'zoom-in', 'zoom-out', 'static',\n];\n/** Default cycling effect set for a lively montage when no `controls.effect`\n * is supplied (or it isn't a known effect). */\nconst SLIDESHOW_DEFAULT_EFFECTS = ['zoom-in', 'pan-left', 'zoom-out', 'pan-right'];\n/** Ken Burns style presets (`kenBurnsStyleSchema.preset`) - a DIFFERENT set\n * of names from the Text `PRESET_BG` family above; don't reuse resolvePreset\n * for this template. */\nconst KEN_BURNS_STYLE_PRESETS = ['cinematic', 'dramatic', 'calm', 'documentary', 'dreamy', 'moody', 'energetic'];\nconst SLIDESHOW_SECONDS_PER_IMAGE = 3;\n\n/**\n * The transition types the Slideshow REMOTION COMPOSITION actually renders\n * (packages/remotion/src/compositions/slideshow/Slideshow.tsx) - a narrow\n * subset distinct from the broad ffmpeg `SLIDESHOW_TRANSITIONS` list in\n * index.js (a different, ffmpeg-based transition path - do not widen this to\n * match it). Single source of truth for this narrow set: the API's\n * generate-video route schema, Slideshow.tsx's own zod schema, and this\n * file's `buildSlideshow`/`expandVideoLook` all derive from this constant.\n * Re-exported from index.js so `import { SLIDESHOW_COMPOSITION_TRANSITIONS }\n * from '@vid-master/config'` is unchanged for every external consumer - it\n * lives here, not in index.js, because this is the module that actually\n * consumes it (VIDEO_LOOKS/expandVideoLook below), and index.js already\n * re-exports from this file, so defining it there and importing it back\n * here would be a real import cycle (#4038 review).\n */\nexport const SLIDESHOW_COMPOSITION_TRANSITIONS = ['fade', 'slide', 'wipe', 'none'];\n\n/**\n * VIDEO_LOOKS (#4038): the one named-look table `buildSlideshow` (below) and\n * `clipform_generate_video` (API route + MCP tool) both expand through, so\n * the two consumers of the Slideshow composition speak one taste vocabulary\n * instead of two that can drift. Each entry composes ONLY the atomic\n * vocabularies that already exist in this package/config:\n * - `motionPreset`: a KEN_BURNS_STYLE_PRESETS name (Ken Burns motion)\n * - `transition`: a { type, duration } pair, type one of\n * SLIDESHOW_COMPOSITION_TRANSITIONS\n * - `texture` / `duotone`: the same shapes `KenBurnsStyle` already accepts\n * - `backgroundColor`: a CSS colour string\n * - `randomEffects`: whether item Ken Burns effects should shuffle\n * Never add a field here that isn't one of these existing dials - that would\n * quietly recreate a fourth taste vocabulary.\n *\n * Names are outcome-led (#1731), not dial-descriptive. `clean` is the\n * DEFAULT - fade transition + the 'cinematic' motion preset (which IS the\n * Ken Burns library default, see KEN_BURNS_PRESETS.cinematic = {} in\n * KenBurnsImage.tsx) + no overlay - so a zero-dial call matches today's\n * zero-dial look exactly, never regressing an existing caller.\n */\nconst VIDEO_LOOKS = {\n clean: {\n transition: { type: 'fade', duration: 1 },\n motionPreset: 'cinematic',\n },\n editorial: {\n transition: { type: 'fade', duration: 1 },\n motionPreset: 'calm',\n texture: { type: 'halftone' },\n duotone: { shadow: '#1a1633', highlight: '#f5c542' },\n },\n punchy: {\n transition: { type: 'slide', duration: 0.5 },\n motionPreset: 'energetic',\n randomEffects: true,\n },\n 'bold-poster': {\n transition: { type: 'fade', duration: 1 },\n motionPreset: 'dramatic',\n texture: { type: 'halftone' },\n duotone: { shadow: '#0a0a14', highlight: '#f59e0b' },\n backgroundColor: '#0a0a14',\n },\n};\n\nconst DEFAULT_VIDEO_LOOK = 'clean';\nexport const VIDEO_LOOK_NAMES = Object.keys(VIDEO_LOOKS);\n\n/**\n * Expand a named video look into a base props bundle for the Slideshow\n * composition: `{ transition, defaultStyle?, backgroundColor?, randomEffects? }`.\n * Pure - no defaults are applied by a caller's OTHER controls here, that\n * merge (explicit dial overrides the look) is each caller's own job (see\n * `buildSlideshow` below and apps/api's generate-video route).\n *\n * An unknown or omitted `name` falls back to `clean` (mirrors\n * `resolvePreset`/`resolveThemeMotion`'s unknown-falls-back-to-default shape\n * above) so every caller always gets a fully-defined bundle back.\n */\nexport function expandVideoLook(name) {\n const look = VIDEO_LOOKS[name] || VIDEO_LOOKS[DEFAULT_VIDEO_LOOK];\n const defaultStyle = {};\n // Defense-in-depth: only ever emit a motion preset this package actually\n // recognises, so a VIDEO_LOOKS typo can never leak an invalid preset name.\n if (look.motionPreset && KEN_BURNS_STYLE_PRESETS.includes(look.motionPreset)) {\n defaultStyle.preset = look.motionPreset;\n }\n if (look.texture) defaultStyle.texture = look.texture;\n if (look.duotone) defaultStyle.duotone = look.duotone;\n\n return {\n transition: { type: look.transition.type, duration: look.transition.duration },\n ...(Object.keys(defaultStyle).length > 0 ? { defaultStyle } : {}),\n ...(look.backgroundColor ? { backgroundColor: look.backgroundColor } : {}),\n ...(look.randomEffects !== undefined ? { randomEffects: look.randomEffects } : {}),\n };\n}\n\n/**\n * Normalize one `slideshow` media item to the Slideshow layer's item shape.\n * Accepts either a plain image URL string (the recipe's original `images`\n * contract) or a richer `{ type, url, effect?, fit?, aspect_ratio?,\n * start_from?, volume? }` object mixing images and video clips - the same\n * per-item vocabulary `clipform_generate_video` accepts (#4059), so both\n * surfaces speak one item shape. `fixedEffect` is the single\n * `controls.effect` override (applies to every image lacking its own); a\n * missing effect otherwise cycles the default set by position so a montage\n * doesn't repeat one effect.\n */\nfunction normalizeSlideshowItem(item, index, fixedEffect) {\n if (item && typeof item === 'object' && item.type === 'video') {\n if (typeof item.url !== 'string' || !item.url) {\n throw new Error(\"video template 'slideshow': each video item requires a url\");\n }\n return {\n type: 'video',\n url: item.url,\n ...(typeof item.start_from === 'number' ? { startFrom: item.start_from } : {}),\n ...(typeof item.volume === 'number' ? { volume: item.volume } : {}),\n };\n }\n const url = typeof item === 'string' ? item : item?.url;\n if (typeof url !== 'string' || !url) {\n throw new Error(\"video template 'slideshow': each image item requires a url\");\n }\n const itemEffect = item && typeof item === 'object' && KEN_BURNS_EFFECTS.includes(item.effect)\n ? item.effect\n : (fixedEffect ?? SLIDESHOW_DEFAULT_EFFECTS[index % SLIDESHOW_DEFAULT_EFFECTS.length]);\n const built = { type: 'image', url, effect: itemEffect };\n if (item && typeof item === 'object') {\n if (typeof item.fit === 'string') built.fit = item.fit;\n if (typeof item.aspect_ratio === 'number') built.aspectRatio = item.aspect_ratio;\n }\n return built;\n}\n\n/**\n * slideshow: a multi-image (or mixed image/video) Slideshow bed with Ken Burns\n * motion and a configurable transition - optionally narrated or scored with\n * background audio (#4059), so one recipe covers both a silent montage and a\n * narrated one instead of two near-identical tools.\n *\n * `controls.preset` prefers a VIDEO_LOOK name (see above) - expanded via\n * `expandVideoLook` so this recipe speaks the same looks as\n * `clipform_generate_video`. A raw KEN_BURNS_STYLE_PRESETS name (the recipe's\n * pre-#4038 contract, e.g. 'dramatic') still works too - it only sets the\n * Ken Burns motion, same as before - so an existing caller passing a raw\n * preset isn't silently dropped to 'clean' (#4038 review finding 2). An\n * explicit `controls.transition`/`controls.effect` still wins over whatever\n * the look would otherwise set (escape hatch).\n *\n * Duration: `controls.duration_seconds` (explicit) always wins. Otherwise,\n * when `controls.audio_url` (narration) is given, `durationInSeconds` is left\n * UNSET here on purpose - the render-template route (apps/api) probes the\n * track's real length and syncs the render to it, so a per-image count never\n * fights the voice track. With no narration, duration derives from item count\n * x `secondsPerImage` exactly as before (a `background_audio_url` bed alone,\n * with no `audio_url`, keeps this derived duration - it loops to fill it).\n */\nfunction buildSlideshow(controls) {\n const {\n images, items: itemsControl, effect, transition, preset, secondsPerImage,\n audio_url, background_audio_url, background_audio_volume, duration_seconds,\n } = controls;\n\n const rawItems = Array.isArray(itemsControl) && itemsControl.length > 0 ? itemsControl : images;\n if (!Array.isArray(rawItems) || rawItems.length === 0) {\n throw new Error(\"video template 'slideshow': controls.images (non-empty string[]) or controls.items (non-empty array) is required\");\n }\n\n const fixedEffect = KEN_BURNS_EFFECTS.includes(effect) ? effect : null;\n const items = rawItems.map((item, i) => normalizeSlideshowItem(item, i, fixedEffect));\n\n const hasNarration = typeof audio_url === 'string' && audio_url.length > 0;\n const explicitDuration = typeof duration_seconds === 'number' && duration_seconds > 0\n ? duration_seconds\n : null;\n\n let durationInSeconds;\n if (explicitDuration != null) {\n durationInSeconds = explicitDuration;\n } else if (hasNarration) {\n durationInSeconds = undefined; // synced from the narration track by the render route\n } else {\n const baseSecondsPerImage = typeof secondsPerImage === 'number' && secondsPerImage > 0\n ? secondsPerImage\n : SLIDESHOW_SECONDS_PER_IMAGE;\n durationInSeconds = items.length * holdSeconds(baseSecondsPerImage, controls);\n }\n\n // VIDEO_LOOK names are preferred; a raw Ken Burns preset name (this\n // recipe's original contract) falls back to the old preset-only behaviour\n // instead of being silently dropped. Anything else (unset/unrecognised)\n // keeps the original no-look behaviour (plain 'fade' fallback, no\n // defaultStyle).\n const look = VIDEO_LOOK_NAMES.includes(preset) ? expandVideoLook(preset) : null;\n const rawMotionPreset = !look && KEN_BURNS_STYLE_PRESETS.includes(preset) ? preset : null;\n const resolvedTransitionType = SLIDESHOW_COMPOSITION_TRANSITIONS.includes(transition)\n ? transition\n : (look ? look.transition.type : 'fade');\n const resolvedDefaultStyle = look\n ? look.defaultStyle\n : (rawMotionPreset ? { preset: rawMotionPreset } : undefined);\n\n const bed = {\n composition: 'Slideshow',\n props: {\n items,\n transition: {\n type: resolvedTransitionType,\n durationInFrames: 15,\n },\n // Omitted (not set to undefined) when narration-driven: Scene computes\n // the layer's own window from the scene's synced duration instead, and\n // an explicit `undefined` key would still win the props spread over it.\n ...(durationInSeconds !== undefined ? { durationInSeconds } : {}),\n ...(resolvedDefaultStyle ? { defaultStyle: resolvedDefaultStyle } : {}),\n },\n };\n\n return {\n layers: [bed],\n ...(durationInSeconds !== undefined ? { durationInSeconds } : {}),\n ...(hasNarration ? { audioUrl: audio_url } : {}),\n ...(typeof background_audio_url === 'string' && background_audio_url ? { backgroundAudioUrl: background_audio_url } : {}),\n ...(typeof background_audio_volume === 'number' ? { backgroundAudioVolume: background_audio_volume } : {}),\n };\n}\n\nconst GUESS_CITY_DEFAULT_SECONDS = 7;\nconst MAP_BASEMAPS = ['satellite', 'satellite-streets', 'streets', 'dark', 'light', 'outdoors'];\nconst MAP_MOTIONS = ['zoom-in', 'zoom-out', 'static'];\n\n/**\n * guess-the-city: a globe-to-city descent (Map layer) with place labels\n * hidden, and an optional answer reveal. Duration lives at the scene level\n * only - `mapSchema` is `.strict()` with no `durationInSeconds` field of its\n * own. `accessToken` is deliberately omitted - the API injects the Mapbox\n * token server-side.\n */\nfunction buildGuessTheCity(controls) {\n const { lat, lng, name, motion, basemap, revealName } = controls;\n if (typeof lat !== 'number' || typeof lng !== 'number') {\n throw new Error(\"video template 'guess-the-city': controls.lat and controls.lng (numbers) are required\");\n }\n const durationInSeconds = holdSeconds(GUESS_CITY_DEFAULT_SECONDS, controls);\n\n const bed = {\n composition: 'Map',\n props: {\n motion: MAP_MOTIONS.includes(motion) ? motion : 'zoom-in',\n target: { lat, lng, ...(typeof name === 'string' && name ? { name } : {}) },\n basemap: MAP_BASEMAPS.includes(basemap) ? basemap : 'satellite',\n scene: { mapLabels: false },\n label: revealName === true && name ? name : null,\n },\n };\n\n return { layers: [bed], durationInSeconds };\n}\n\n/**\n * A single Grid cell from a plain control string: an image cell when the value\n * looks like a URL (http(s) or a root/staticFile path - the same test Grid.tsx\n * uses), else a text cell. Lets the grid recipes take a flat string[] control\n * surface ([\"Paris\", \"London\"] or four image urls) and still render either\n * kind. An optional `label` overrides the cell's default A/B/C position badge.\n */\nfunction gridCellFromValue(value, label) {\n const isUrl = /^https?:\\/\\//.test(value) || value.startsWith('/');\n const cell = isUrl ? { type: 'image', url: value } : { type: 'text', text: value };\n return typeof label === 'string' && label ? { ...cell, label } : cell;\n}\n\nconst VERSUS_DEFAULT_SECONDS = 5;\n\n/**\n * versus: a head-to-head \"this or that\" - two options (images or text) side by\n * side in two columns. The comparison itself is the beat; there is no\n * right/wrong reveal (that is pick-the-answer). Renders through the Grid\n * primitive with a fixed 2-column layout.\n */\nfunction buildVersus(controls) {\n const { left, right, leftLabel, rightLabel } = controls;\n if (!left || typeof left !== 'string') {\n throw new Error(\"video template 'versus': controls.left (non-empty string - image url or text) is required\");\n }\n if (!right || typeof right !== 'string') {\n throw new Error(\"video template 'versus': controls.right (non-empty string - image url or text) is required\");\n }\n const bed = {\n composition: 'Grid',\n props: {\n cells: [gridCellFromValue(left, leftLabel), gridCellFromValue(right, rightLabel)],\n layout: { columns: 2 },\n showLabels: true,\n animate: true,\n },\n };\n return { layers: [bed], durationInSeconds: holdSeconds(VERSUS_DEFAULT_SECONDS, controls) };\n}\n\nconst PICK_THE_ANSWER_DEFAULT_SECONDS = 7;\n\n/**\n * pick-the-answer: a multiple-choice question grid (images or text). By\n * DEFAULT this is a plain clue clip - no highlight - meant to sit behind a\n * choice node whose own answer feedback (`show_answer_feedback`) tells the\n * respondent whether they got it right. Set `reveal: true` (+ `correctIndex`)\n * to opt into the spotlight/tick payoff variant instead (Grid's `reveal` with\n * tone 'correct'). `correctIndex` is 0-based and only required when\n * `reveal: true` - it is meaningless on a clue clip. Defaults to a 2-column\n * grid; `columns` overrides it.\n */\nfunction buildPickTheAnswer(controls) {\n const { options, correctIndex, columns, reveal } = controls;\n if (!Array.isArray(options) || options.length === 0) {\n throw new Error(\"video template 'pick-the-answer': controls.options (non-empty (string - image url or text)[]) is required\");\n }\n const showReveal = reveal === true;\n if (showReveal && (typeof correctIndex !== 'number' || correctIndex < 0 || correctIndex >= options.length)) {\n throw new Error(\"video template 'pick-the-answer': controls.correctIndex (0-based index into options) is required when controls.reveal is true\");\n }\n const bed = {\n composition: 'Grid',\n props: {\n cells: options.map((o) => gridCellFromValue(o)),\n layout: { columns: typeof columns === 'number' ? columns : 2 },\n showLabels: true,\n animate: true,\n ...(showReveal ? { reveal: { mode: 'spotlight', highlightIndices: [correctIndex], tone: 'correct' } } : {}),\n },\n };\n return { layers: [bed], durationInSeconds: holdSeconds(PICK_THE_ANSWER_DEFAULT_SECONDS, controls) };\n}\n\nconst ODD_ONE_OUT_DEFAULT_SECONDS = 7;\n\n/**\n * odd-one-out: \"spot the one that doesn't belong\" - a grid of items (images or\n * text). By DEFAULT this is a plain clue clip - no highlight - meant to sit\n * behind a choice node whose own answer feedback (`show_answer_feedback`)\n * tells the respondent whether they found it. Set `reveal: true` (+\n * `oddIndex`) to opt into the spotlight payoff variant instead (Grid's\n * `reveal` spotlight, tone 'correct'). `oddIndex` is 0-based and only\n * required when `reveal: true` - it is meaningless on a clue clip. Layout\n * auto-derives from the item count unless `columns` is given.\n */\nfunction buildOddOneOut(controls) {\n const { items, oddIndex, columns, reveal } = controls;\n if (!Array.isArray(items) || items.length === 0) {\n throw new Error(\"video template 'odd-one-out': controls.items (non-empty (string - image url or text)[]) is required\");\n }\n const showReveal = reveal === true;\n if (showReveal && (typeof oddIndex !== 'number' || oddIndex < 0 || oddIndex >= items.length)) {\n throw new Error(\"video template 'odd-one-out': controls.oddIndex (0-based index into items) is required when controls.reveal is true\");\n }\n const props = {\n cells: items.map((i) => gridCellFromValue(i)),\n showLabels: true,\n animate: true,\n ...(showReveal ? { reveal: { mode: 'spotlight', highlightIndices: [oddIndex], tone: 'correct' } } : {}),\n };\n if (typeof columns === 'number') props.layout = { columns };\n return { layers: [{ composition: 'Grid', props }], durationInSeconds: holdSeconds(ODD_ONE_OUT_DEFAULT_SECONDS, controls) };\n}\n\nconst PHOTO_GRID_DEFAULT_SECONDS = 6;\n\n/**\n * photo-grid: a photo wall / mosaic - several images shown together in one\n * grid (all animate in), with optional per-image captions. Distinct from\n * slideshow, which shows images sequentially with Ken Burns motion. Position\n * badges stay off unless captions are supplied. Layout auto-derives unless\n * `columns` is given.\n */\nfunction buildPhotoGrid(controls) {\n const { images, labels, columns } = controls;\n if (!Array.isArray(images) || images.length === 0) {\n throw new Error(\"video template 'photo-grid': controls.images (non-empty string[] of image urls) is required\");\n }\n const cells = images.map((url, i) => ({\n type: 'image',\n url,\n ...(Array.isArray(labels) && labels[i] ? { label: labels[i] } : {}),\n }));\n const props = {\n cells,\n showLabels: Array.isArray(labels) && labels.some((l) => typeof l === 'string' && l),\n animate: true,\n };\n if (typeof columns === 'number') props.layout = { columns };\n return { layers: [{ composition: 'Grid', props }], durationInSeconds: holdSeconds(PHOTO_GRID_DEFAULT_SECONDS, controls) };\n}\n\n/**\n * Single source of truth for available video templates + their curated\n * control surface (advertised to the MCP via `clipform_list_video_templates`).\n * `tier` follows the same 'public' | 'labs' | 'social' vocabulary as\n * COMPOSITION_REGISTRY. The visual recipes are 'public' (exposed on the prod\n * MCP menu, #1821); the text-card family + colour-grid stay 'labs' (non-prod).\n * Exposure derives from tier via `exposedVideoTemplateNames` (index.js) - the\n * same env gate the compositions use, so raw Scene assembly stays labs while\n * the curated recipe menu is public.\n *\n * `description` and `keywords` (#1817) are the discovery layer: an agent\n * matches a user's natural-language intent (\"I want a geography quiz\") to a\n * recipe by these, not by implementation detail - so `description` always\n * leads with the USE CASE/outcome, never the Scene layers it happens to\n * render through, and `keywords` is a handful of the phrases a user would\n * actually say. Keep both in that voice when adding a recipe.\n *\n * `exampleControls` (#1888) is one worked, ready-to-render controls object per\n * recipe - the SINGLE source consumed by Remotion Studio's \"Templates\" folder\n * (`packages/remotion/src/compositions/index.tsx`), the MCP\n * `clipform_list_video_templates` output (agents succeed far more reliably with\n * one worked example than with bare control names), and the conformance/guard\n * tests. It must include every REQUIRED control (a control name with no `?`) so\n * `buildVideoTemplate(name, exampleControls)` renders without throwing. Image\n * controls use plain `https://picsum.photos/seed/.../1080/1920` URLs - this\n * package stays dependency-free (no `staticFile`), the url renders in every bed\n * (`KenBurnsImage` passes it straight to `<Img>`), and it doubles as a real,\n * copyable example for agents.\n */\nexport const VIDEO_TEMPLATE_REGISTRY = [\n {\n name: 'title-card',\n tier: 'labs',\n description: 'An intro, section break, or outro card: a title with optional tagline and subtitle over an image or solid-colour background.',\n keywords: ['intro card', 'title screen', 'opening', 'outro', 'section header', 'branded end screen'],\n controls: ['title', 'subtitle?', 'tagline?', 'preset?', 'backgroundImage?', 'variant?', 'theme?'],\n exampleControls: {\n title: 'Welcome to Clipform',\n subtitle: 'Video-first forms that actually get answered',\n tagline: 'Get started',\n preset: 'cinematic',\n },\n },\n {\n name: 'mystery-reveal',\n tier: 'public',\n description: 'A \"guess what this is\" reveal - a single image that resolves from obscured (blurred, pixelated, or zoomed-in) to sharp.',\n keywords: ['guess what this is', 'reveal quiz', 'blurred photo', 'zoom and reveal', 'mystery image', 'guess the picture', 'guess the flag'],\n controls: ['image', 'variant?', 'revealSeconds?', 'theme?'],\n exampleControls: {\n image: 'https://picsum.photos/seed/clipform-mystery/1080/1920',\n variant: 'blur',\n },\n },\n {\n name: 'colour-grid',\n tier: 'labs',\n description: 'A colour-matching or palette game: a grid of colour swatches to pick from or reveal.',\n keywords: ['colour matching', 'palette quiz', 'pick the colour', 'swatch game', 'build the flag'],\n controls: ['colors', 'labels?', 'variant?', 'theme?'],\n exampleControls: {\n colors: ['#6366F1', '#EC4899', '#F59E0B', '#10B981'],\n labels: ['Indigo', 'Pink', 'Amber', 'Emerald'],\n },\n },\n {\n name: 'question-card',\n tier: 'labs',\n description: \"A survey or quiz prompt: a question over an image or solid-colour background, with an optional helper line - the workhorse for a form's question nodes.\",\n keywords: ['ask a question', 'survey question', 'quiz prompt', 'poll question', 'form question'],\n controls: ['question', 'helper?', 'preset?', 'backgroundImage?', 'variant?', 'theme?'],\n exampleControls: {\n question: 'What are you hoping to achieve?',\n helper: 'One sentence is plenty',\n preset: 'minimal',\n },\n },\n {\n name: 'stat-highlight',\n tier: 'labs',\n description: 'An impressive-number beat: one big statistic or metric that pops in, with an optional label and caption - social proof and emphasis.',\n keywords: ['big number', 'statistic', 'metric highlight', 'impressive figure', 'social proof number'],\n controls: ['value', 'label?', 'caption?', 'preset?', 'backgroundImage?', 'variant?', 'theme?'],\n exampleControls: {\n value: '10,000+',\n label: 'forms created this month',\n preset: 'bold',\n },\n },\n {\n name: 'quote-card',\n tier: 'labs',\n description: 'A testimonial or pull-quote beat for social proof, with optional attribution.',\n keywords: ['testimonial', 'pull quote', 'social proof', 'customer quote', 'review highlight'],\n controls: ['quote', 'attribution?', 'preset?', 'backgroundImage?', 'variant?', 'theme?'],\n exampleControls: {\n quote: \"The fastest way we've found to talk to customers at scale.\",\n attribution: 'Jamie R., Head of Growth',\n preset: 'elegant',\n },\n },\n {\n name: 'list-card',\n tier: 'labs',\n description: \"An agenda, steps, or \\\"what you'll get\\\" beat: a headline with a short bulleted list revealed sequentially.\",\n keywords: ['bullet list', 'agenda', 'steps', \"what you'll get\", 'checklist', 'how it works'],\n controls: ['title', 'items', 'preset?', 'backgroundImage?', 'variant?', 'theme?'],\n exampleControls: {\n title: \"What you'll get\",\n items: ['A custom quote', 'A 15-minute call', 'A live demo', 'A follow-up plan'],\n preset: 'bold',\n },\n },\n {\n name: 'cta-card',\n tier: 'labs',\n description: 'A closing call-to-action: headline, action line, and optional subtext telling the viewer what to do next.',\n keywords: ['call to action', 'closing screen', 'do this next', 'sign up prompt', 'end card'],\n controls: ['headline', 'action', 'subtext?', 'preset?', 'backgroundImage?', 'variant?', 'theme?'],\n exampleControls: {\n headline: 'Ready to get started?',\n action: 'Sign up free',\n subtext: 'No credit card required',\n preset: 'cinematic',\n },\n },\n {\n name: 'slideshow',\n tier: 'public',\n description: 'A photo and video montage: images and/or video clips with Ken Burns motion and configurable transitions, with optional narration or background audio (audio_url / background_audio_url) synced to the track length. Defaults to silent (motion only) when no audio_url is given - for the richer per-item mixed-media dials or advanced style overrides, use clipform_generate_video instead.',\n keywords: ['photo montage', 'image slideshow', 'ken burns', 'picture story', 'gallery', 'photo gallery', 'narrated montage', 'video montage'],\n controls: ['images', 'items?', 'effect?', 'transition?', 'preset?', 'secondsPerImage?', 'audio_url?', 'background_audio_url?', 'background_audio_volume?', 'duration_seconds?', 'theme?'],\n exampleControls: {\n images: [\n 'https://picsum.photos/seed/clipform-slideshow-1/1080/1920',\n 'https://picsum.photos/seed/clipform-slideshow-2/1080/1920',\n 'https://picsum.photos/seed/clipform-slideshow-3/1080/1920',\n ],\n transition: 'fade',\n },\n },\n {\n name: 'guess-the-city',\n tier: 'public',\n description: \"A map \\\"guess where this is\\\" reveal - the camera flies from a globe down onto a mystery city with place names hidden, then optionally reveals the answer.\",\n keywords: ['guess the location', 'guess the city', 'where in the world', 'geography trivia', 'map flyover', 'guess the country'],\n controls: ['lat', 'lng', 'name?', 'motion?', 'basemap?', 'revealName?', 'theme?'],\n exampleControls: {\n lat: 48.8566,\n lng: 2.3522,\n name: 'Paris',\n motion: 'zoom-in',\n },\n },\n {\n name: 'versus',\n tier: 'public',\n description: 'A head-to-head \"this or that\" comparison: two options side by side for the viewer to pick between.',\n keywords: ['this or that', 'versus', 'a or b', 'which do you prefer', 'head to head', 'pick a side', 'would you rather'],\n controls: ['left', 'right', 'leftLabel?', 'rightLabel?', 'theme?'],\n exampleControls: {\n left: 'https://picsum.photos/seed/clipform-versus-left/1080/1920',\n right: 'https://picsum.photos/seed/clipform-versus-right/1080/1920',\n leftLabel: 'Option A',\n rightLabel: 'Option B',\n },\n },\n {\n name: 'pick-the-answer',\n tier: 'public',\n description: 'A multiple-choice question grid - the everyday quiz beat. Pair with a choice node\\'s own answer feedback for the win/lose beat; set reveal: true for an optional spotlight-the-correct-option-with-a-green-tick payoff variant.',\n keywords: ['multiple choice', 'quiz options', 'pick the answer', 'pick the correct one', 'reveal the answer', 'trivia question', 'question grid'],\n controls: ['options', 'reveal?', 'correctIndex?', 'columns?', 'theme?'],\n exampleControls: {\n options: ['Paris', 'London', 'Rome', 'Berlin'],\n reveal: true,\n correctIndex: 0,\n },\n },\n {\n name: 'odd-one-out',\n tier: 'public',\n description: 'A \"spot the one that does not belong\" grid puzzle. Pair with a choice node\\'s own answer feedback for the win/lose beat; set reveal: true for an optional spotlight-the-odd-item payoff variant.',\n keywords: ['odd one out', 'spot the odd one', 'which does not belong', 'find the different one', 'which is different'],\n controls: ['items', 'reveal?', 'oddIndex?', 'columns?', 'theme?'],\n exampleControls: {\n items: [\n 'https://picsum.photos/seed/clipform-odd-1/1080/1920',\n 'https://picsum.photos/seed/clipform-odd-2/1080/1920',\n 'https://picsum.photos/seed/clipform-odd-3/1080/1920',\n 'https://picsum.photos/seed/clipform-odd-4/1080/1920',\n ],\n reveal: true,\n oddIndex: 2,\n },\n },\n {\n name: 'photo-grid',\n tier: 'public',\n description: 'A photo wall or mosaic: several images shown together in one grid, with optional captions.',\n keywords: ['photo grid', 'photo wall', 'mosaic', 'image grid', 'moodboard', 'gallery grid'],\n controls: ['images', 'columns?', 'labels?', 'theme?'],\n exampleControls: {\n images: [\n 'https://picsum.photos/seed/clipform-photo-1/1080/1920',\n 'https://picsum.photos/seed/clipform-photo-2/1080/1920',\n 'https://picsum.photos/seed/clipform-photo-3/1080/1920',\n 'https://picsum.photos/seed/clipform-photo-4/1080/1920',\n ],\n },\n },\n];\n\nexport const VIDEO_TEMPLATE_NAMES = VIDEO_TEMPLATE_REGISTRY.map((r) => r.name);\n\nconst BUILDERS = {\n 'title-card': buildTitleCard,\n 'mystery-reveal': buildMysteryReveal,\n 'colour-grid': buildColourGrid,\n 'question-card': buildQuestionCard,\n 'stat-highlight': buildStatHighlight,\n 'quote-card': buildQuoteCard,\n 'list-card': buildListCard,\n 'cta-card': buildCtaCard,\n 'slideshow': buildSlideshow,\n 'guess-the-city': buildGuessTheCity,\n 'versus': buildVersus,\n 'pick-the-answer': buildPickTheAnswer,\n 'odd-one-out': buildOddOneOut,\n 'photo-grid': buildPhotoGrid,\n};\n\n/**\n * Expand a named video template + its controls into Scene props (plain\n * JSON, matching Scene.schema.ts's `sceneSchema`). Throws on an unknown\n * video template name or a missing required control - never renders a\n * partial/garbage scene.\n */\nexport function buildVideoTemplate(name, controls) {\n const builder = BUILDERS[name];\n if (!builder) {\n throw new Error(`Unknown video template '${name}'. Known video templates: ${VIDEO_TEMPLATE_NAMES.join(', ')}`);\n }\n return builder(controls || {});\n}\n\n/**\n * The video-template names exposed for a given environment (#1821), mirroring\n * `exposedCompositionIds` (index.js): the 'public' recipes always, plus any\n * non-public ('labs'/'social') recipes only when `includeNonPublic` (non-prod).\n * A pure environment gate - callers resolve NODE_ENV at their own edge (this\n * package stays dependency-free). Resolved server-side only (#1886): the API's\n * render-template route and `exposedVideoTemplatesManifest` (the menu the MCP\n * `clipform_list_video_templates` tool fetches over HTTP) both call this - the\n * MCP server no longer gates on its own NODE_ENV. Never hand-roll a second copy.\n */\nexport function exposedVideoTemplateNames(includeNonPublic) {\n return VIDEO_TEMPLATE_REGISTRY\n .filter((r) => r.tier === 'public' || (includeNonPublic && r.tier !== 'public'))\n .map((r) => r.name);\n}\n","// @vid-master/config/logic-chain\n// Pure, branch-aware graph helpers over node_logic actions - no DB, no React\n// Flow. Moved out of apps/api/src/lib/logic-chain.ts (#1058) so a single\n// implementation backs the DB-backed chain ops; apps/api re-exports these so\n// existing callers/imports are unchanged.\n//\n// The chain helpers historically read/wrote only `actions[0]`, which silently\n// orphaned every other edge on a branched node (#704). These pure functions\n// treat a node's logic as the multi-edge graph it is.\n\nimport { isOptionEdge, isInactiveOptionEdge } from './logic-types.js';\n\n/**\n * Distinct outgoing target node-ids from a node's actions, in action order.\n * When `node` is given, parked option edges (branching off) are skipped so the\n * walk follows only the ACTIVE graph; omit it to treat every edge as live.\n *\n * @param {import('./logic-types.js').LogicAction[] | undefined} actions\n * @param {import('./logic-types.js').BranchingNode} [node]\n * @returns {string[]}\n */\nexport function outgoingTargets(actions, node) {\n const out = [];\n for (const a of actions ?? []) {\n if (node && isInactiveOptionEdge(node, a)) continue;\n const v = a?.details?.to?.value;\n if (v && !out.includes(v)) out.push(v);\n }\n return out;\n}\n\n/**\n * A node is \"branched\" when its logic routes more than one way: per-option\n * (an option-typed condition var) or to more than one distinct target. A node\n * with no logic, or a single node-level jump, is linear. With `node` given, a\n * choice/binary node whose branching is off is linear (its parked option edges\n * don't count) even though the parked edges still exist in node_logic.\n *\n * @param {import('./logic-types.js').LogicAction[] | undefined} actions\n * @param {import('./logic-types.js').BranchingNode} [node]\n * @returns {boolean}\n */\nexport function isBranched(actions, node) {\n if (!actions || actions.length === 0) return false;\n const active = node ? actions.filter((a) => !isInactiveOptionEdge(node, a)) : actions;\n const routesByOption = active.some((a) => isOptionEdge(a));\n return routesByOption || outgoingTargets(active).length > 1;\n}\n\n/**\n * The default (fallback) target of a node: the target of its node-level action\n * (\"everything else -> X\"), else the first action's target, else null when the\n * node is a chain tail / terminal.\n *\n * @param {import('./logic-types.js').LogicAction[] | undefined} actions\n * @returns {import('./logic-types.js').LogicActionDetails['to'] | null}\n */\nexport function defaultTarget(actions) {\n if (!actions || actions.length === 0) return null;\n const nodeLevel = actions.find((a) => {\n const vars = a.condition?.vars;\n return !!vars && vars.length > 0 && vars.every((v) => v.type === 'node');\n });\n return (nodeLevel ?? actions[0])?.details?.to ?? null;\n}\n\n/**\n * Order nodes by BFS from the start node, following ALL outgoing edges (not\n * just the first). Unreachable nodes are appended in stable input order so the\n * caller still receives every node - branched forms used to come back\n * mis-ordered and missing whole branches.\n *\n * @template {{ id: string, type: string, config?: object }} T\n * @param {T[]} nodes\n * @param {Map<string, import('./logic-types.js').LogicAction[]>} logicByNode\n * @returns {T[]}\n */\nexport function bfsOrder(nodes, logicByNode) {\n const byId = new Map(nodes.map((n) => [n.id, n]));\n const ordered = [];\n const seen = new Set();\n\n const start = nodes.find((n) => n.type === 'start');\n if (start) {\n const queue = [start.id];\n while (queue.length) {\n const id = queue.shift();\n if (seen.has(id)) continue;\n seen.add(id);\n const node = byId.get(id);\n if (node) ordered.push(node);\n for (const target of outgoingTargets(logicByNode.get(id), node)) {\n if (!seen.has(target) && byId.has(target)) queue.push(target);\n }\n }\n }\n\n // Append anything the start can't reach (parked/draft branches) so the full\n // node set is always returned, reachable-first.\n for (const node of nodes) if (!seen.has(node.id)) ordered.push(node);\n return ordered;\n}\n\n/**\n * Rewrites for deleting `nodeId`: every action across every node that targets\n * it is re-pointed at the deleted node's default target, so no incoming edge is\n * left dangling. If the deleted node has no default target (it was a tail /\n * terminal), the incoming action is dropped instead. Returns only the entries\n * that changed.\n *\n * @param {Array<{ node_id: string, actions: import('./logic-types.js').LogicAction[] }>} allLogic\n * @param {string} nodeId\n * @returns {Array<{ node_id: string, actions: import('./logic-types.js').LogicAction[] }>}\n */\nexport function computeRemovalRewrites(allLogic, nodeId) {\n const target = defaultTarget(\n allLogic.find((e) => e.node_id === nodeId)?.actions\n );\n const changes = [];\n\n for (const entry of allLogic) {\n if (entry.node_id === nodeId) continue;\n let changed = false;\n const next = [];\n for (const action of entry.actions ?? []) {\n if (action?.details?.to?.value === nodeId) {\n changed = true;\n // Re-point to the deleted node's default target - unless there is no\n // default, or doing so would create a self-loop (a 2-cycle through the\n // deleted node), in which case drop the edge.\n if (target && target.value !== entry.node_id) {\n next.push({ ...action, details: { to: { ...target } } });\n }\n } else {\n next.push(action);\n }\n }\n if (changed) changes.push({ node_id: entry.node_id, actions: next });\n }\n return changes;\n}\n","import { FEATURES, getLiveFeatures } from './features.js';\nimport { HELP_TOPICS, buildHelpLinks } from './help-links.js';\n\n// @vid-master/config - Essential shared configuration\n\n/**\n * Environment Detection\n * Automatically detects dev/localhost vs production\n */\nconst isServer = typeof window === 'undefined';\nconst isBrowser = typeof window !== 'undefined';\n\n// Detect localhost/development environment\nconst isLocalhost = isBrowser && (\n window.location.hostname === 'localhost' ||\n window.location.hostname === '127.0.0.1' ||\n window.location.hostname.includes('local')\n);\n\nconst isDevelopment = isServer\n ? process.env.NODE_ENV !== 'production'\n : isLocalhost;\n\nexport const ENV = {\n isDevelopment,\n isProduction: !isDevelopment,\n isLocalhost: isDevelopment, // alias for clarity\n};\n\n/**\n * Cookie domain for cross-subdomain auth & tracking\n * undefined in dev (browsers reject explicit domain on localhost)\n * .clipform.io in production (covers www., app., and apex)\n */\nexport const COOKIE_DOMAIN = ENV.isDevelopment ? undefined : '.clipform.io';\n\n/**\n * Browser is accessing the app via the Cloudflare dev tunnel\n * (e.g. dashboard-dev.smith-forge.com → routes to local Next.js).\n * In that case browser fetches must use the matching tunnel hostnames,\n * because the browser cannot reach localhost on the dev's machine.\n */\nconst isTunnelBrowser = isBrowser &&\n window.location.hostname.endsWith('.smith-forge.com');\n\n/**\n * Production origins - the single source of truth for Clipform's public URLs.\n * getUrls() returns these in prod (dev/tunnel branches override). Embed adapters\n * that run on third-party platforms and can't import this package (apps/embeds/*,\n * apps/shopify) hardcode these origins; they're pinned to this value by\n * apps/api/src/__tests__/lib/embed-origin-parity.test.ts so a domain change\n * can't silently skip an adapter.\n */\nexport const PROD_URLS = {\n marketing: 'https://www.clipform.io',\n // Dedicated CDN host for the embed SDK, decoupled from the marketing deploy\n // (ships in seconds via packages/embed-sdk `deploy:cf`). www.clipform.io/embed.js\n // stays as the legacy bridge until all installs migrate here.\n embed: 'https://js.clipform.io',\n dashboard: 'https://app.clipform.io',\n viewer: 'https://clipform.io',\n api: 'https://api.clipform.io',\n // Dedicated subdomain for the remote MCP server. Same Render service\n // as `api`, just routed by Host header. Token audience is bound here.\n mcp: 'https://mcp.clipform.io',\n docs: 'https://clipform.io/docs',\n};\n\nexport function getUrls() {\n if (isTunnelBrowser) {\n return {\n marketing: 'https://marketing-dev.smith-forge.com',\n // Dev/tunnel: the marketing dev server 308-redirects /embed.js to js.clipform.io\n // (public/embed.js was retired in #1288), so embeds load the live SDK.\n embed: 'https://marketing-dev.smith-forge.com',\n dashboard: 'https://dashboard-dev.smith-forge.com',\n viewer: 'https://viewer-dev.smith-forge.com',\n api: 'https://api-dev.smith-forge.com',\n mcp: 'https://api-dev.smith-forge.com/mcp',\n docs: 'https://clipform.io/docs',\n };\n }\n\n if (ENV.isDevelopment) {\n // Per-worktree overrides: a parallel worktree can run its own full stack on\n // a distinct port block by setting NEXT_PUBLIC_*_URL in its .env.local (plus\n // a matching `-p` / PORT). Without these the apps resolve each other at the\n // base ports below and would cross-wire to another worktree's servers.\n // Direct `process.env.NEXT_PUBLIC_*` reads (not destructured) so Next inlines\n // them into client bundles - every app that imports this package client-side\n // transpiles it (see each app's next.config transpilePackages).\n const api = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3003';\n return {\n marketing: process.env.NEXT_PUBLIC_MARKETING_URL || 'http://localhost:3002',\n // Local: the marketing dev server redirects /embed.js to js.clipform.io\n // (public/embed.js was retired in #1288), so embeds load the live SDK.\n embed: process.env.NEXT_PUBLIC_MARKETING_URL || 'http://localhost:3002',\n dashboard: process.env.NEXT_PUBLIC_DASHBOARD_URL || 'http://localhost:3000',\n viewer: process.env.NEXT_PUBLIC_VIEWER_URL || 'http://localhost:3001',\n api,\n // No separate MCP host in dev - tools live on the api subpath.\n mcp: api + '/mcp',\n // The docs app serves under basePath /docs (matching prod\n // clipform.io/docs), so the dev URL must include it too.\n docs: process.env.NEXT_PUBLIC_DOCS_URL || 'http://localhost:3004/docs',\n };\n }\n\n // Fresh copy so callers can't mutate the shared PROD_URLS source of truth.\n return { ...PROD_URLS };\n}\n\n/**\n * Core business information - UPDATE THESE AS NEEDED\n */\nexport const BUSINESS = {\n name: \"Clipform\",\n tagline: \"Video-Driven Form Builder\",\n description:\n \"Create engaging forms that combine video content with interactive questions. Capture authentic responses with text, audio, or video recordings.\",\n shortDescription:\n \"Interactive video forms that capture authentic responses. Build engaging forms in minutes.\",\n domain: \"clipform.io\",\n email: {\n support: \"support@clipform.io\",\n sales: \"sales@clipform.io\",\n hello: \"hello@clipform.io\",\n founder: \"andy@clipform.io\",\n // Inbound form-builder address (Cloudflare Email Routing -> email-inbound\n // worker). Also the Reply-To on bot replies so a reply reaches the bot.\n new: \"new@clipform.io\",\n // Vulnerability disclosure inbox (#4011) - advertised in security.txt\n // (RFC 9116) and the docs disclosure policy.\n security: \"security@clipform.io\",\n },\n urls: getUrls(),\n};\n\n/**\n * Two-tier video delivery threshold (#1015): a video at/above this size goes to\n * Mux (adaptive HLS); below it (and short) it's stored direct in Supabase. The\n * API gate (`apps/api/src/lib/video-delivery.ts`) and the uploader client both\n * read this so the client knows whether to send bytes through the API (direct)\n * or straight to Mux (large). Single source - never hardcode the threshold.\n *\n * Set to 20MB from real data (#1495): every uploaded video is short (n=718, p95\n * ~24s, MAX 28s - there's a ~30s recording cap), so by Mux's own \"~2 min\"\n * guidance they all belong on progressive/Supabase. The old 10MB cap sent ~half\n * of them to Mux purely for being big, not long. A max-length (28s) clip\n * compresses to ~12MB at the 3.5Mbps target (#1512), so 20MB fits every\n * compressed clip with headroom - keeping short clips on cheap direct storage\n * and making Mux a rare fallback.\n */\nexport const MEDIA_DIRECT_MAX_BYTES = 20 * 1024 * 1024; // 20 MB\n\n/**\n * Hard ceiling on ANY single object written to Supabase storage, in bytes.\n *\n * This is not our choice - it is the project-global storage limit imposed by the\n * Supabase plan, and it OVERRIDES every per-bucket `file_size_limit` declared in\n * `supabase/config.toml` at upload time. Offering the user more than this just\n * produces an upload that passes every client check and then dies at the storage\n * layer (#2556).\n *\n * Only applies to the Supabase destination. Mux-bound uploads are unaffected and\n * keep their own, larger limits.\n *\n * Raising it means lifting the plan ceiling in the Supabase dashboard FIRST, then\n * raising the bucket declarations in `supabase/config.toml`, then this value.\n * #4023: raised 50 -> 200 MiB now that we're confirmed on Supabase Pro (the\n * global project ceiling was raised to 500 MiB and the `media-responses`\n * bucket to 200 MiB to match).\n */\nexport const SUPABASE_STORAGE_MAX_BYTES = 200 * 1024 * 1024; // 200 MiB\n\n/**\n * Trash (#2596 slice 3): days a trashed form stays recoverable before the\n * purge sweep (a later slice) hard-deletes it. Single source for the\n * dashboard's countdown badge and dialogs - never hardcode 30 there.\n *\n * The docs page (apps/docs/content/docs/help/managing-forms.mdx) states this\n * in prose as a hand-synced copy, NOT a derived value (MDX has no import of\n * this module) - if this constant ever changes, update that prose by hand too.\n */\nexport const TRASH_RETENTION_DAYS = 30;\n\n/**\n * Default resume window (#3885): hours before an incomplete response session\n * is considered expired and cleared, when a form has no explicit\n * `form_metadata.session_expiry_hours` override. Single source for the\n * `form_metadata.session_expiry_hours` column default (kept in step by hand -\n * SQL can't import this), `useFormMetadata`'s optimistic-cache default, the\n * dashboard's \"Keep progress for\" field, viewer's resume-window fallbacks,\n * and the responses table's stale-session threshold. Stored/displayed in\n * hours; the dashboard presents it in days (this / 24).\n */\nexport const DEFAULT_SESSION_EXPIRY_HOURS = 168;\n\n/**\n * On-device video compression settings (#1512, desktop parity #2329) - single\n * source of truth for both the phone upload page (`apps/api/src/routes/v1/media/\n * phone-upload-page.ts`, inline script) and the desktop builder helper\n * (`apps/dashboard/lib/compress-video.ts`). Both re-encode via mediabunny/\n * WebCodecs to these exact settings before the direct-vs-Mux size handshake, so\n * a clip compresses identically regardless of upload surface.\n *\n * VIDEO_COMPRESS_FLOOR_BYTES is a \"won't shrink, skip the generation loss + CPU\"\n * floor - below this a clip can't meaningfully benefit from re-encoding at the\n * bitrate below, and the keep-only-if-smaller guard in both callers already\n * blocks any accidental bloat. Locked at 1 MB (#2329).\n */\nexport const VIDEO_COMPRESS_BITRATE = 3_500_000;\nexport const VIDEO_COMPRESS_HEIGHT = 1080;\nexport const VIDEO_COMPRESS_FPS = 30;\nexport const VIDEO_COMPRESS_FLOOR_BYTES = 1 * 1024 * 1024; // 1 MB\n\n/**\n * In-product \"Learn more\" deep-links (#394) - single source of truth, keyed\n * by concept. Anchors are CI-checked against real docs headings\n * (apps/docs/scripts/check-help-links.mjs). Never hardcode docs URLs in UI.\n */\nexport const HELP_LINKS = buildHelpLinks(BUSINESS.urls.docs);\nexport { HELP_TOPICS };\n\n/**\n * Per-node-type \"Learn more\" deep-link (#3963): looks up `node_<nodeType>` in\n * HELP_LINKS, so a node's settings menu can link straight to its section on\n * help/node-types.mdx. Returns null when the node type has no docs topic\n * (menu renders nothing rather than a dead link).\n */\nexport function nodeHelpLink(nodeType) {\n return HELP_LINKS[`node_${nodeType}`] ?? null;\n}\n\n/** URL builders — single source of truth for public-facing links */\nexport function viewerUrl(shareId) {\n return `${BUSINESS.urls.viewer}/${shareId}`;\n}\n/**\n * The link handed to someone to open/claim a form they built as a guest\n * (bot email + social replies, MCP create-form). Flag-gated on\n * `FEATURES.anonymous_builder` (#2979): on -> `/try`, which drops them\n * straight into the builder to see the product (no signup gate); off -> the\n * legacy `/save` signup wall. `getTemplateCtaUrl` (marketing) mirrors this\n * branch for template CTAs.\n */\nexport function formUrl(shareId) {\n return FEATURES.anonymous_builder.enabled\n ? tryUrl(shareId)\n : `${BUSINESS.urls.dashboard}/save/${shareId}`;\n}\n/**\n * Same-UID claim handoff link (#3330 decision 3) for an MCP-minted anonymous\n * session: opens the dashboard's one-time claim route, which signs the\n * browser back into that exact anon Supabase session (not a copy) so the\n * standard convert flow can flip is_anonymous on it in place. `token` is a\n * one-time, ~15-minute opaque token from mintMcpClaimToken - never a\n * share_id or workspace id.\n */\nexport function mcpClaimUrl(token) {\n return `${BUSINESS.urls.dashboard}/claim/${token}`;\n}\n/**\n * Every dashboard tab a form has (#3405) - single source for the tab path\n * segment, so an MCP/bot deeplink can never drift from the real route. The\n * dashboard route folders under apps/dashboard/app/(app)/[workspaceId]/forms/[formId]/\n * are the ground truth this mirrors. Enforced, not just documentation:\n * dashboardFormUrl throws on any tab not in this list, so a caller's own\n * hardcoded tab string (e.g. get-form.ts's urls object) fails loudly the\n * moment it drifts from this array instead of silently building a dead link.\n */\nexport const DASHBOARD_FORM_TABS = ['build', 'summary', 'responses', 'analytics', 'share', 'share/embed', 'share/dynamic-link'];\n\nexport function dashboardFormUrl(workspaceId, formId, tab = 'build') {\n if (!DASHBOARD_FORM_TABS.includes(tab)) {\n throw new Error(\n `dashboardFormUrl: unknown tab \"${tab}\" - must be one of ${DASHBOARD_FORM_TABS.join(', ')}`\n );\n }\n return `${BUSINESS.urls.dashboard}/${workspaceId}/forms/${formId}/${tab}`;\n}\n\n/** Dashboard workspace root (redirects to the forms list). */\nexport function dashboardWorkspaceUrl(workspaceId) {\n return `${BUSINESS.urls.dashboard}/${workspaceId}`;\n}\n\n/** Dashboard forms list for a workspace. */\nexport function dashboardFormsListUrl(workspaceId) {\n return `${BUSINESS.urls.dashboard}/${workspaceId}/forms`;\n}\n\n/**\n * Dashboard auth routes - single source of truth so a login/signup route rename\n * is one edit here (plus renaming the app/<route> folder). Internal dashboard\n * redirects use AUTH_ROUTES.*; cross-app links (marketing) use the *Url builders.\n */\nexport const AUTH_ROUTES = { login: '/login', signup: '/signup' };\nexport function loginUrl() {\n return `${BUSINESS.urls.dashboard}${AUTH_ROUTES.login}`;\n}\nexport function signupUrl() {\n return `${BUSINESS.urls.dashboard}${AUTH_ROUTES.signup}`;\n}\n\n/**\n * Minimum password length (#3843) - single source of truth for both the\n * client-side floor (AuthForm's password step, SetPasswordCard, the\n * /auth/callback recovery form, ConvertAccountModal) and the server-side\n * one (`minimum_password_length` in supabase/config.toml, dashboard-set on\n * dev + prod - see that file's #2984/#3843 note). Kept in lockstep by\n * apps/api/src/__tests__/lib/min-password-length-sync.test.ts - change both\n * together.\n */\nexport const MIN_PASSWORD_LENGTH = 8;\n\n/**\n * OTP/magic-link resend cooldown, in seconds (#3873) - must equal\n * `[auth.email] max_frequency` in supabase/config.toml AND the hosted\n * Supabase dashboard value (Auth -> Providers -> Email -> \"Rate limit for\n * sending emails\"; Studio's shared default is 60s for both the OTP and\n * magic-link email types). GoTrue rejects an email send inside this window\n * with `over_email_send_rate_limit`, so AuthForm's Resend control disables\n * itself and shows a countdown for this long after every send instead of\n * letting the user hit the server limit. Kept in lockstep by\n * apps/api/src/__tests__/lib/otp-resend-cooldown-sync.test.ts - change both\n * together.\n */\nexport const OTP_RESEND_COOLDOWN_SECONDS = 60;\n\n/**\n * The anonymous-builder entry point (`FEATURES.anonymous_builder`, #2979):\n * mints a guest session and drops straight into the builder, no signup gate.\n * With a `shareId`, links to that template's copy instead of a blank form.\n */\nexport function tryUrl(shareId) {\n return `${BUSINESS.urls.dashboard}/try${shareId ? `?template=${shareId}` : ''}`;\n}\n\n/**\n * The Powered-by CTA's fork entry (#857/#3777): same anonymous-builder mint\n * as `tryUrl`, but carries the source form's shareId as `?fork=` instead of\n * `?template=` - `/try` resolves this into a blank NODE-SHAPE skeleton\n * (`POST /v1/forms/skeleton`) rather than a full content clone (`?template=`'s\n * `/v1/forms/save/:shareId` path). Mirrors `tryImportUrl`'s gating: with the\n * flag off, falls back to the plain dashboard link rather than a dead `/try`.\n */\nexport function tryForkUrl(shareId) {\n return FEATURES.anonymous_builder.enabled\n ? `${BUSINESS.urls.dashboard}/try?fork=${encodeURIComponent(shareId)}`\n : BUSINESS.urls.dashboard;\n}\n\n/**\n * Google Forms host allowlist for pre-navigation validation (#3414 SSOT\n * promotion of #3402's marketing-only copy) - mirrors the API's authoritative\n * check (`apps/api/src/routes/v1/forms/import-google.ts`'s ALLOWED_HOSTS).\n * This is a cosmetic pre-check only (a fast \"that's not a Google Form\" hint\n * before a widget navigates); the API re-validates authoritatively on the\n * other side, so this list must never diverge without a reason.\n */\nexport const GOOGLE_FORM_ALLOWED_HOSTS = new Set(['docs.google.com', 'forms.gle']);\n\n/**\n * Normalizes a scheme-less paste (\"docs.google.com/forms/...\", the common\n * copy from an address bar) by prepending https://. `new URL()` throws on a\n * bare host (so `isGoogleFormUrl` would reject a perfectly valid form link),\n * and the API's `z.string().url()` would reject it too. Call this before\n * validating/sending so both checks see the same value the request body will\n * actually carry.\n */\nexport function normalizeFormUrl(value) {\n return /^[a-zA-Z][a-zA-Z0-9+.-]*:\\/\\//.test(value) ? value : `https://${value}`;\n}\n\n/** True when `value` (already normalized via `normalizeFormUrl`) points at a Google Forms host. */\nexport function isGoogleFormUrl(value) {\n try {\n return GOOGLE_FORM_ALLOWED_HOSTS.has(new URL(value.trim()).hostname);\n } catch {\n return false;\n }\n}\n\n/**\n * The Google Forms import widget's target URL (#3414 SSOT promotion of\n * #3402's marketing-only `getGoogleImportUrl`): redirects into the\n * dashboard's `/try` anonymous lane with the (already normalized) form URL\n * as a query param, which runs the same anon sign-in + captcha +\n * at-limit-gate flow as the template path before calling\n * `POST /v1/forms/import/google`. Consumed by both marketing\n * (`GoogleImportSection`) and docs (`GoogleFormImporter`) - neither app\n * calls the API directly.\n *\n * Mirrors `formUrl`'s `FEATURES.anonymous_builder` gating - unlike the\n * template path, there is no legacy signup-gated fallback for importing, so\n * with the flag off this falls back to the plain dashboard link (same as\n * marketing's `getTemplateCtaUrl`'s missing-share-id case,\n * `apps/marketing/src/lib/template-cta.ts`) rather than a dead `/try`.\n */\nexport function tryImportUrl(url) {\n return FEATURES.anonymous_builder.enabled\n ? `${BUSINESS.urls.dashboard}/try?import=${encodeURIComponent(url)}`\n : BUSINESS.urls.dashboard;\n}\n\n/**\n * Embeddable viewer iframe — single source of truth for embedding a form on a\n * third-party page. Consumed directly by anything inside our bundler (e.g. the\n * oEmbed endpoint in `apps/api`). Two artifacts run OUTSIDE the bundler and so\n * cannot import this — the static SDK (`apps/marketing/public/embed.js`) and\n * the standalone Framer component (`apps/embeds/framer/Clipform.tsx`) — and\n * carry a verbatim copy of `iframeAllow`. A parity test\n * (`apps/api/src/__tests__/lib/embed-allow-parity.test.ts`) fails if either\n * copy drifts from this value, so this stays the source of truth.\n */\nexport const EMBED = {\n // Permissions delegated to the cross-origin viewer iframe: recording\n // (camera/microphone), video (autoplay/fullscreen), Stripe wallets\n // (payment), the end-screen share button (web-share + clipboard-write), and\n // keeping the respondent's screen from auto-dimming mid-session (#2859).\n iframeAllow: 'camera; microphone; autoplay; fullscreen; payment; web-share; clipboard-write; screen-wake-lock',\n // Default 9:16 portrait box — matches embed.js DEFAULT_MAX_WIDTH (460px).\n defaultWidth: 460,\n defaultHeight: 818, // round(460 * 16 / 9)\n};\n\n/** Embeddable viewer URL for a form (the viewer's `display=embed` mode). */\nexport function embedViewerUrl(shareId) {\n return `${BUSINESS.urls.viewer}/${shareId}?display=embed`;\n}\n\n/**\n * Brand assets and visual identity\n */\nexport const BRAND = {\n logo: {\n primary: \"/logo.svg\",\n withText: \"/logo_text.svg\",\n icon: \"/favicon.ico\",\n iconSvg: \"/icon.svg\",\n appleTouchIcon: \"/apple-touch-icon.png\",\n minWidth: \"120px\",\n maxWidth: \"200px\",\n spacing: \"16px\",\n },\n colors: {\n primary: \"#171717\",\n secondary: \"#404040\",\n accent: \"#3182CE\",\n background: \"#FAFAFA\",\n surface: \"#FFFFFF\",\n border: \"#E5E5E5\",\n muted: \"#737373\",\n destructive: \"#DC2626\",\n success: \"#16A34A\",\n },\n fonts: {\n body: \"Inter, sans-serif\",\n headings: \"'Satoshi', Inter, sans-serif\",\n logo: \"'Plus Jakarta Sans', sans-serif\",\n satoshiImport: \"https://api.fontshare.com/v2/css?f[]=satoshi@400,500,700,900&display=swap\",\n },\n};\n\n/**\n * Plan limits — shared between API (enforcement), dashboard (UI gating), and viewer (branding)\n *\n * Keyed by plan_version string (e.g. 'free_v1', 'pro_v1') so we can\n * grandfather old limits when pricing changes. New signups get the version\n * from CURRENT_PLAN_VERSIONS; existing companies keep theirs until\n * explicitly migrated.\n *\n * Per-version billing identity is provider-agnostic (#1027): `price_cents` is\n * the canonical machine-readable price (the `price` string is display-only);\n * `stripe_price_id` and `shopify_plan_handle` map the version to each biller.\n * `shopify_plan_handle` is null until the matching Shopify App Pricing plan is\n * created in the Partner Dashboard.\n */\nexport const PLAN_LIMITS = {\n free_v1: {\n tier: 0,\n name: 'Free',\n price: '$0',\n period: 'forever',\n price_cents: 0,\n stripe_price_id: { test: null, live: null },\n // Shopify App Pricing plan NAME (must match the Partner Dashboard plan\n // exactly - the app_subscriptions/update payload carries this name). Free\n // generates no subscription, so this is only used by the drift check.\n shopify_plan_handle: 'Free',\n // Platform commission on payments collected by a Free-plan creator, in\n // basis points (#3831). Live at 500 (5%) - paid tiers stay 0%. Retained\n // on refund by design (industry standard - Stripe keeps `application_fee`\n // unless `refund_application_fee` is set). See create-payment-intent.ts\n // for the fee gate.\n platform_fee_bps: 500,\n node_limit: null,\n form_limit: 5,\n workspace_limit: 1,\n show_branding: true,\n custom_theme: false,\n dynamic_links: false,\n custom_domain: false,\n response_limit: 100,\n features: [\n 'Up to 5 forms',\n 'Unlimited questions per form',\n // \"completed\" is not a tightening: response_limit has always been 100,\n // but the counter now excludes partials (#2618), so this wording is both\n // accurate and strictly more generous than what it replaced.\n '100 completed responses per month',\n 'Full AI tooling (TTS, media, video)',\n 'Clipform branding',\n ],\n },\n pro_v1: {\n tier: 1,\n name: 'Pro',\n price: '$25',\n period: '/month',\n price_cents: 2500,\n stripe_price_id: { test: 'price_1TZnmhCWdEPD0KoNWWJTc3B6', live: 'price_1TZnrZEJOD9ik3LhhFwGZ6ut' },\n // Must match the Partner Dashboard plan name exactly (drives webhook plan\n // mapping + the price drift check). Keep in step with price_cents above.\n shopify_plan_handle: 'Pro',\n // Paid tier - platform takes 0% (#3831). Free is the only tier with a\n // commission, and only once go-live flips it on.\n platform_fee_bps: 0,\n node_limit: null,\n form_limit: null,\n workspace_limit: null,\n show_branding: false,\n custom_theme: true,\n dynamic_links: true,\n custom_domain: true,\n response_limit: null,\n features: [\n 'Unlimited forms',\n 'Unlimited questions',\n 'Unlimited responses',\n 'Custom themes & colors',\n 'Remove Clipform branding',\n 'Dynamic links - repoint a printed URL to a new form',\n ],\n },\n\n // Banded Pro (#1937). pro_v2_1k IS the current tier-1 version (#2671) - new\n // Pro signups land here; pro_v1 is retained only to grandfather the accounts\n // that predate the flip.\n //\n // Billing is one subscription with TWO Stripe items: this licensed base\n // billed in advance, plus a volume-tiered metered top-up (STRIPE_BAND_PRICE_ID\n // below) billed in arrears - so the band a workspace lands in is decided by\n // usage, never by swapping plan version. Completed responses report to the\n // Stripe usage meter (#2651, apps/api/src/lib/usage-metering.ts) via\n // planMetersUsage below, and checkout appends the metered item whenever a band\n // price is configured for the mode (#2652). The band price now exists in BOTH\n // test and live (STRIPE_BAND_PRICE_ID), so a new Pro checkout is two-item and\n // usage above the base band IS charged once this deploys - the HUMAN LEGAL\n // REVIEW on LEGAL.billing below must clear before that go-live.\n //\n // `response_limit` counts COMPLETED submissions only (#4118: per completed\n // form_session, grandfathered pre-cutover as completed response sets - see\n // count_workspace_responses_this_month); partials never consume allowance.\n //\n // shopify_plan_handle is null until the matching Partner Dashboard plans\n // exist - so planVersionForShopifyPlan still maps Shopify's \"Pro\" to pro_v1\n // ($25 unlimited) while web signups get this band. Creating those plans is\n // what closes that cross-channel gap.\n pro_v2_1k: {\n tier: 1,\n name: 'Pro',\n price: '$24',\n period: '/month',\n price_cents: 2400,\n stripe_price_id: {\n test: 'price_1U0lA6EJOD9ik3Lhube5eBcK',\n live: 'price_1U115SEJOD9ik3LhOU9OkqJk',\n },\n shopify_plan_handle: null,\n // Paid tier - platform takes 0% (#3831).\n platform_fee_bps: 0,\n node_limit: null,\n form_limit: null,\n workspace_limit: null,\n show_branding: false,\n custom_theme: true,\n dynamic_links: true,\n custom_domain: true,\n response_limit: 1000,\n features: [\n 'Unlimited forms',\n 'Unlimited questions',\n '1,000 completed responses per month',\n 'Custom themes & colors',\n 'Remove Clipform branding',\n 'Dynamic links - repoint a printed URL to a new form',\n ],\n },\n pro_v2_5k: {\n tier: 1,\n name: 'Pro',\n price: '$49',\n period: '/month',\n price_cents: 4900,\n stripe_price_id: { test: null, live: null },\n shopify_plan_handle: null,\n // Paid tier - platform takes 0% (#3831).\n platform_fee_bps: 0,\n node_limit: null,\n form_limit: null,\n workspace_limit: null,\n show_branding: false,\n custom_theme: true,\n dynamic_links: true,\n custom_domain: true,\n response_limit: 5000,\n features: [\n 'Unlimited forms',\n 'Unlimited questions',\n '5,000 completed responses per month',\n 'Custom themes & colors',\n 'Remove Clipform branding',\n 'Dynamic links - repoint a printed URL to a new form',\n ],\n },\n pro_v2_20k: {\n tier: 1,\n name: 'Pro',\n price: '$99',\n period: '/month',\n price_cents: 9900,\n stripe_price_id: { test: null, live: null },\n shopify_plan_handle: null,\n // Paid tier - platform takes 0% (#3831).\n platform_fee_bps: 0,\n node_limit: null,\n form_limit: null,\n workspace_limit: null,\n show_branding: false,\n custom_theme: true,\n dynamic_links: true,\n custom_domain: true,\n response_limit: 20000,\n features: [\n 'Unlimited forms',\n 'Unlimited questions',\n '20,000 completed responses per month',\n 'Custom themes & colors',\n 'Remove Clipform branding',\n 'Dynamic links - repoint a printed URL to a new form',\n ],\n },\n};\n\n/**\n * Maps tier integer → version string assigned to new signups.\n *\n * Tier 1 is a BAND (#1937): new Pro signups get pro_v2_1k ($24 / 1,000\n * completed responses). Existing companies keep their stored plan_version, so\n * the pro_v1 accounts predating this stay on unlimited - grandfathered, never\n * migrated. Nothing charges for exceeding a band yet (#2651 meters usage,\n * #2652 makes checkout two-item); until then an over-band customer keeps\n * collecting, stays fully visible, and the excess is absorbed.\n */\nexport const CURRENT_PLAN_VERSIONS = { 0: 'free_v1', 1: 'pro_v2_1k' };\n\n/**\n * Ordered Pro bands for the pricing slider (#1937). DERIVED from PLAN_LIMITS -\n * never define band prices or limits here. Ascending by response_limit.\n *\n * The pricing page renders a final \"talk to us\" stop beyond the last band;\n * that is presentation, not a plan version, so it lives in the component.\n */\nexport const PRO_BANDS = ['pro_v2_1k', 'pro_v2_5k', 'pro_v2_20k'].map(\n (version) => ({ version, ...PLAN_LIMITS[version] })\n);\n\n/**\n * The single volume-tiered METERED Stripe price that carries banded Pro usage\n * (#1937 / #2652). ONE price spans every band: Stripe's `tiers_mode: 'volume'`\n * selects the band from the MAX usage in the period, so the band a customer\n * lands in is decided by usage, never by swapping plan_version. It is therefore\n * a tier-level fact and lives here ONCE, not on each pro_v2_* entry.\n *\n * Deliberately NOT any PLAN_LIMITS[*].stripe_price_id: it is metered and billed\n * in ARREARS (the licensed base is billed in advance), and it must NEVER resolve\n * through planVersionForStripePrice - that maps the LICENSED base price to a\n * plan version, and letting the band price match would reintroduce #2678 (the\n * webhook picking the band item as the \"plan\" item, writing the wrong\n * plan_version and billing period). Checkout appends it as a second\n * subscription item (#2652); the webhook plan projection ignores it.\n *\n * NEVER attach Stripe billing thresholds to this price: combined with volume\n * tiers they can produce NEGATIVE invoice line items (#1937).\n *\n * Both `test` and `live` are set: the live price (volume-tiered $0/$25/$75/$75,\n * metered against `mtr_61VAPLMBymP6O56H441EJOD9ik3LhCfA` on Pro product\n * `prod_UOCjM0hFrt0XYw`, tax_behavior exclusive) was created by hand in the\n * live Stripe dashboard. So LIVE checkout is two-item the moment this deploys -\n * see the deploy-gated HUMAN LEGAL REVIEW note on LEGAL.billing below.\n */\nexport const STRIPE_BAND_PRICE_ID = {\n test: 'price_1U0lEsEJOD9ik3Lhs4ttMplT',\n live: 'price_1U1RABEJOD9ik3LhBDfNGlfZ',\n};\n\n/**\n * Whether a plan hides over-limit responses behind the paygate.\n *\n * Only the free tier redacts. A PAID plan with a finite response_limit\n * (banded Pro - pro_v2_*) escalates the charge to the band reached and keeps\n * everything visible (#1937), so a numeric response_limit alone must never\n * imply redaction. See #2650.\n */\nexport function planRedactsOverLimit(limits) {\n return limits?.tier === 0 && limits?.response_limit != null;\n}\n\n/**\n * Whether a plan's completed responses should be reported to the Stripe usage\n * meter (#2651).\n *\n * A metered band is a PAID plan (tier > 0) with a finite response_limit -\n * i.e. banded Pro (pro_v2_*). Deliberately NOT a `pro_v2_*` string match:\n * config is the source of truth for which versions meter, so a future band\n * (or a grandfathered pro_v1 unlimited account, which must never meter)\n * follows from its shape alone. pro_v1 has `response_limit: null` (unlimited)\n * so it correctly never meters; free_v1 is tier 0 so it correctly never\n * meters either (its cap is enforced by paygate/redaction, not billing).\n */\nexport function planMetersUsage(limits) {\n return limits?.tier > 0 && limits?.response_limit != null;\n}\n\n/**\n * Pricing-card feature lines for a plan version - `PLAN_LIMITS[version].features`\n * plus a derived platform-commission line (#3831), single source so a pricing\n * card never hand-writes the fee copy. Lights up automatically off whatever\n * `platform_fee_bps` carries per tier - no caller change needed. Never\n * hardcode a percentage - always derive it from here.\n *\n * - A tier whose OWN `platform_fee_bps` is > 0 gets its real rate:\n * `${bps / 100}% commission on payments collected`.\n * - Once ANY tier in PLAN_LIMITS carries a commission, every OTHER (0-bps)\n * paid tier (`tier > 0`) gets the contrasting `Keep 100% of payments - no\n * platform fee` line, so Pro reads as the fee-free alternative the moment\n * Free's fee is live. A paid tier never gets its own commission line today\n * (every paid `platform_fee_bps` is permanently 0), but the check is written against\n * the tier's own bps first so a future paid-tier fee would still show its\n * real rate instead of the contrasting copy.\n */\nexport function planFeatureLines(version) {\n const limits = PLAN_LIMITS[version];\n if (!limits) return [];\n\n const bps = limits.platform_fee_bps ?? 0;\n if (bps > 0) {\n return [...limits.features, `${bps / 100}% commission on payments collected`];\n }\n\n const anyFeeActive = Object.values(PLAN_LIMITS).some((p) => (p.platform_fee_bps ?? 0) > 0);\n if (anyFeeActive && limits.tier > 0) {\n return [...limits.features, 'Keep 100% of payments - no platform fee'];\n }\n\n return [...limits.features];\n}\n\n/**\n * Product information and features\n */\nexport const PRODUCT = {\n features: {\n get core() {\n return getLiveFeatures()\n .filter(f => ['node-types', 'responses', 'builder', 'analytics', 'ai'].includes(f.category))\n .map(f => f.name);\n },\n get premium() {\n return getLiveFeatures()\n .filter(f => ['sharing', 'api'].includes(f.category))\n .map(f => f.name);\n },\n },\n // Derived from PLAN_LIMITS (source of truth)\n get pricing() {\n const free = PLAN_LIMITS[CURRENT_PLAN_VERSIONS[0]];\n const pro = PLAN_LIMITS[CURRENT_PLAN_VERSIONS[1]];\n return {\n free: { name: free.name, price: free.price, period: free.period, features: free.features },\n pro: { name: pro.name, price: pro.price, period: pro.period, features: pro.features },\n };\n },\n};\n\n/**\n * Social media links\n */\nexport const SOCIAL = {\n twitter: \"@clipform\",\n linkedin: \"company/clipform\",\n github: \"clipform\",\n urls: {\n twitter: \"https://twitter.com/clipform\",\n linkedin: \"https://linkedin.com/company/clipform\",\n github: \"https://github.com/clipform\",\n },\n};\n\n/**\n * Legal and compliance\n */\n/**\n * Legal facts - single source of truth for /terms and /privacy (#459).\n * Pages derive entity names, contacts, dates, and numbers from here;\n * never hardcode them in page copy.\n *\n * Drafting principle: minimum legally-required disclosure only. No voluntary\n * commitments, no named vendors in public docs (categories of recipients\n * satisfy UK GDPR Art 13(1)(e)), no published DPA until a customer requests\n * one. Shortest legally-sufficient wording everywhere.\n */\nexport const LEGAL = {\n company: \"Reco Ventures Ltd.\",\n companyCountry: \"United Kingdom\",\n governingLaw: \"England and Wales\",\n copyright: \"© 2026 Clipform. All rights reserved.\",\n privacy: \"/privacy\",\n terms: \"/terms\",\n privacyEmail: \"hello@clipform.io\", // GDPR contact - no separate DPO mailbox\n termsUpdated: \"2026-09-08\",\n privacyUpdated: \"2026-08-19\",\n minAge: 18, // contract capacity; no parental-consent handling\n billing: {\n processor: \"Stripe\", // live\n // Banded Pro (#1937/#2652) is a hybrid charge on ONE subscription: a flat\n // monthly base billed in ADVANCE, plus variable usage-based charges for\n // completed responses above the base band, billed in ARREARS at period end\n // and auto-escalated to the band reached. Auto-renews; cancellation is\n // effective at the end of the current period.\n // HUMAN LEGAL REVIEW REQUIRED before go-live (#2652): the rendered /terms\n // prose does not yet describe variable/auto-escalating usage charges, and\n // UK consumer rules want express consent to variable charges captured at\n // checkout.\n model: \"base-plus-usage-recurring\",\n // HUMAN LEGAL REVIEW REQUIRED before go-live (#2652): \"none\" pairs badly\n // with auto-escalating usage charges - the first bot storm or viral spike\n // that inflates a bill needs discretion to exist in writing.\n refunds: \"none\", // except at our sole discretion\n },\n // Total liability capped at fees paid in the prior 12 months (free = zero),\n // plus standard as-is / no indirect damages wording.\n liabilityCapMonths: 12,\n // Account/data deletion completed within this window (incl. Mux assets +\n // Supabase storage; backups may persist briefly beyond).\n deletionDays: 30,\n // Aggregated/anonymized data: rights reserved via quiet drafting - folded\n // into the content licence (terms s5) and lawful-bases (privacy s3) rather\n // than a standalone billboard section. Covers product improvement,\n // benchmarks, published insights; never re-identified.\n //\n // Internal only - NOT rendered anywhere. Kept current so a DPA can be\n // produced the day a customer requests one. Public docs disclose\n // categories of recipients only.\n subprocessors: [\n { name: \"Supabase\", purpose: \"Database and file storage\" },\n { name: \"Mux\", purpose: \"Video hosting and streaming\" },\n { name: \"Vercel\", purpose: \"Web hosting\" },\n { name: \"Render\", purpose: \"API hosting\" },\n { name: \"AWS\", purpose: \"Video rendering (Lambda)\" },\n { name: \"PostHog\", purpose: \"Product analytics and error tracking\" },\n { name: \"Cloudflare\", purpose: \"DNS, CDN, email routing\" },\n { name: \"Stripe\", purpose: \"Payment processing\" },\n { name: \"Anthropic\", purpose: \"AI form generation\" },\n { name: \"OpenAI\", purpose: \"AI form generation\" },\n ],\n};\n\n/**\n * Standard contact form field definitions\n * Source of truth for both dashboard (field picker) and viewer (form rendering)\n */\nexport const CONTACT_FIELDS = [\n { id: 'first_name', label: 'First Name', type: 'text', placeholder: 'Enter first name', order: 1 },\n { id: 'last_name', label: 'Last Name', type: 'text', placeholder: 'Enter last name', order: 2 },\n { id: 'email', label: 'Email Address', type: 'email', placeholder: 'you@example.com', order: 3 },\n { id: 'phone', label: 'Phone Number', type: 'tel', placeholder: '(555) 123-4567', order: 4 },\n];\n\nexport const CONTACT_FIELDS_MAP = Object.fromEntries(\n CONTACT_FIELDS.map(f => [f.id, f])\n);\n\n/**\n * Details-node field icons — single source of truth (#2826). Both the Builder\n * (ContactNode) and the Responses table resolve through resolveContactFieldIcon\n * so a field shows the same icon on both surfaces. Values are Lucide component\n * name strings; each consumer maps the string to the component.\n */\nexport {\n CONTACT_FIELD_TYPE_ICONS,\n CONTACT_FIELD_STANDARD_ICONS,\n CONTACT_FIELD_ICON_FALLBACK,\n resolveContactFieldIcon,\n} from './field-icons.js';\n\n/**\n * The public \"Templates\" workspace. Curated template forms live here (promoted\n * via scripts/promote-form.ts); the template gallery (GET /v1/forms/templates)\n * is scoped to this workspace so a form flagged is_template in ANY other\n * workspace can't leak into it (#1370). Re-exported from\n * apps/api/src/lib/constants.ts so api and scripts/promote-form.ts share one\n * definition.\n */\nexport const CLIPFORM_TEMPLATES_WORKSPACE_ID = \"00000000-0000-4000-b000-000000000100\";\n\n/**\n * Curated allowlist for the dashboard \"New Form -> Select Template\" picker,\n * in display order (#3928). This is the single curation lever for the picker -\n * `is_live` no longer trims it. Every template in the Templates workspace stays\n * `is_live=true` (so its poster/embed/preview resolves via the is_live-gated\n * public path, and marketing pages that embed a template never break); which\n * ones a user sees when creating a form is decided here.\n *\n * PROD `share_id`s. Templates get a fresh share_id when promoted dev->prod\n * (scripts/promote-form.ts mints a new dynamic_link), so these match nothing in\n * dev - GET /v1/forms/templates falls back to showing all templates when the\n * allowlist matches none (see list-templates.ts). So this curates prod; dev\n * shows everything. Every id here MUST be is_live=true or its preview 404s.\n */\nexport const TEMPLATE_PICKER_SHARE_IDS = [\n \"78a5fc05c\", // Client Intake\n \"51e1b300b\", // Customer Feedback\n \"0f9c3f829\", // Event Registration\n \"3986f0fbb\", // Lead Capture\n \"14868ac21\", // Video Testimonial\n \"13fe6ff6a\", // Video Interview\n \"b9d64f65c\", // General Knowledge Quiz\n];\n\n/**\n * Node type definitions — re-exported from node-types.js\n */\nexport {\n NODE_TYPES,\n NODE_TYPES_LIST,\n getActiveNodeTypes,\n ACTIVE_NODE_TYPES,\n NODE_TYPE_KEYS,\n CONNECTION_CONFIG_KEYS,\n RESPONSE_FORMATS,\n CONSENT_TYPES,\n CONTACT_FIELD_TYPES,\n DRAW_GAME_DEFAULT_TIERS,\n DEFAULT_OPTIONS_REVEAL_SECONDS,\n DEFAULT_MAX_RECORDING_SECONDS,\n MAX_RECORDING_SECONDS_MIN,\n MAX_RECORDING_SECONDS_MAX,\n NODE_TYPE_METADATA,\n RESPONSE_SCHEMAS,\n CONFIG_SCHEMAS,\n OUTPUT_SCHEMAS,\n TERMINAL_TYPES,\n NON_COUNTABLE_TYPES,\n NON_COUNTABLE_FILTER,\n RESULTS_EXCLUDED_TYPES,\n PUBLIC_RESULTS_EXCLUDED_TYPES,\n RESPONSES_EXCLUDED_TYPES,\n STEP_COUNTED_TYPES,\n isTerminalType,\n countsAsStep,\n} from './node-types.js';\n\nexport {\n validateAgainstSchema,\n validateConfig,\n validateOptions,\n validateResponse,\n validateOutput,\n collectsResponse,\n} from './validate.js';\n\nexport { FEATURE_FLAGS, FEATURES, getLiveFeatures, getPlannedFeatures, getFeaturesByCategory, getWhatsNew, INTEGRATION_COMPANY_ALLOWLIST, companyHasIntegration, companyHasAnyIntegration, NODE_COMPANY_ALLOWLIST, companyHasNodeType, FEATURE_COMPANY_ALLOWLIST, companyHasPublicResults, companyHasCustomDomains, companyHasConsentDocuments } from './features.js';\n\nexport { ANALYTICS_METRICS, ANALYTICS_INVARIANTS, ANALYTICS_BREAKDOWNS, ANALYTICS_FUNNEL_ORDER, getMetricLabel } from './analytics.js';\n\n/**\n * Form type definitions — re-exported from form-types.js\n */\nexport {\n FORM_TYPES,\n FORM_TYPES_LIST,\n FORM_TYPE_KEYS,\n WORKFLOW_TYPES,\n FORM_TYPE_ALIASES,\n ALL_VARIANTS,\n resolveFormType,\n getDiscoveryParams,\n isFormTypeAvailable,\n getAvailableFormTypes,\n} from './form-types.js';\n\nexport { validateFormForPublish, getNodeContentError, buildAdjacencyMap, getReachableIds, resolveRedirectUrl, resolveLinkListConfig } from './graph-validation.js';\n\n/**\n * Video template layer (#1736) — re-exported from video-templates.js. Data\n * + a pure expander that fills a curated control surface into Scene props;\n * video templates render through the existing Scene composition, never a\n * new primitive.\n */\nexport { VIDEO_TEMPLATE_REGISTRY, VIDEO_TEMPLATE_NAMES, buildVideoTemplate, exposedVideoTemplateNames, VIDEO_LOOK_NAMES, expandVideoLook, SLIDESHOW_COMPOSITION_TRANSITIONS } from './video-templates.js';\n\n/**\n * Form update accept-list — re-exported from form-update-fields.js\n * (source of truth for the fields PATCH /v1/forms/:id accepts, #1219)\n */\nexport { FORM_UPDATE_FIELDS, FORM_UPDATE_FIELD_KEYS } from './form-update-fields.js';\n\n/**\n * Starter-form archetype resolver (#3600) — re-exported from starter-forms.js.\n * Maps a marketing page (use-case/for-page/blog) to a workflow archetype and\n * (once #3599 fills ARCHETYPE_STARTERS) a concrete starter form share_id for\n * \"Build\" CTAs. Blank-safe: every archetype's starter is 'TBD' until #3599\n * lands, so resolveStarterForm returns null everywhere in this PR.\n */\nexport {\n STARTER_ARCHETYPES,\n ARCHETYPE_STARTERS,\n USE_CASE_ARCHETYPE,\n PERSONA_ARCHETYPE,\n BLOG_CATEGORY_ARCHETYPE,\n BASIC_STARTER_SHARE_ID,\n resolveArchetype,\n resolveStarterForm,\n} from './starter-forms.js';\n\n/**\n * Placement tag vocabulary (#2017) — re-exported from placement-tags.js. The\n * closed `for:*`/`usecase:*` namespace mirrored against active `/for/*` and\n * `/use-case/*` marketing pages; write-time enforcement lives in\n * apps/api's set-form-tags route.\n */\nexport {\n PLACEMENT_FOR_ROLES,\n PLACEMENT_USECASE_WORKFLOWS,\n PLACEMENT_TAGS,\n isPlacementNamespace,\n invalidPlacementTags,\n} from './placement-tags.js';\n\n/**\n * Signup origin vocabulary (#4026) — re-exported from signup-origin.js. The\n * first-touch acquisition dimension for account creation (MCP/bot channel,\n * direct web/email, or the /try anonymous builder).\n */\nexport {\n SIGNUP_ORIGINS,\n isSignupOrigin,\n} from './signup-origin.js';\n\n/**\n * Connector signup origin resolver (#4048) - resolves an OAuth `client_id`\n * string (ChatGPT/Claude contain the provider's own domain) to the\n * SignupOrigin a logged-out connector visitor's signup should be tagged\n * with, instead of the generic web/email default. Resolved from the\n * client_id string, NOT a DB lookup - `oauth_clients` RLS has no anon SELECT\n * policy, so a logged-out visitor can never read it.\n */\nexport { connectorOriginFromClientId } from './connector-signup-origin.js';\n\n/**\n * Node logic grammar constants — re-exported from logic-types.js\n */\nexport {\n LOGIC_ACTION_TYPES,\n LOGIC_CONDITION_OPS,\n LOGIC_VAR_TYPES,\n LOGIC_TARGET_TYPES,\n ROUTABLE_VARIABLES,\n isBranchingActive,\n isOptionEdge,\n isInactiveOptionEdge,\n actionReferenceErrors,\n} from './logic-types.js';\n\n/**\n * Pure, branch-aware logic-chain graph helpers — re-exported from\n * logic-chain.js (#1058). apps/api's DB-backed chain ops (getOrderedNodes,\n * insertAfter, removeFromChain, findLastBeforeTerminal) call these.\n */\nexport {\n outgoingTargets,\n isBranched,\n defaultTarget,\n bfsOrder,\n computeRemovalRewrites,\n} from './logic-chain.js';\n\n/**\n * Integration catalog definitions — re-exported from integration-catalog.js\n */\nexport {\n INTEGRATION_CATALOG,\n INTEGRATION_CATALOG_LIST,\n INTEGRATION_SLUGS,\n getIntegrationsByCategory,\n getIntegrationsByTags,\n} from './integration-catalog.js';\n\n/**\n * API key prefixes — shared between API (validation) and dashboard (generation)\n */\nexport const API_KEY_PREFIXES = ['cf_', 'cf_live_', 'cf_test_'];\nexport const API_KEY_PREFIX = 'cf_';\n\n/**\n * OAuth token prefixes — shared between the API's oauth.ts (minting/validation)\n * and api-key-auth.ts (distinguishing an OAuth token from a workspace API key)\n */\nexport const OAUTH_TOKEN_PREFIXES = ['cf_at_', 'cf_rt_', 'cf_code_'];\n\n/**\n * Input validation limits — shared between API routes and MCP server\n */\nexport const VALIDATION_LIMITS = {\n form: {\n title: { max: 1000 },\n nodes: { max: 100 },\n },\n node: {\n prompt: { max: 5000, media_max: 60 },\n options: { max: 50 },\n },\n workspace: {\n name: { max: 100 },\n },\n};\n\n/**\n * Theme defaults and colour validation — single source of truth\n * All colours must be 6-digit hex (#RRGGBB). No rgba, no shorthand.\n */\nexport const HEX_COLOR_REGEX = /^#[0-9A-Fa-f]{6}$/;\n\nexport const THEME_DEFAULTS = {\n primary_color: '#374151',\n background_color: '#F3F4F6',\n font_family: 'Inter',\n};\n\n/**\n * AI-protected form fields — fields the AI must NOT set unless the user\n * explicitly requests it. Everything else is AI-mutable (set freely).\n *\n * Used by MCP tools to guard descriptions and omit defaults.\n */\nexport const AI_PROTECTED_FIELDS = {\n font_family: {\n reason: 'Brand/design decision — do not set unless the user explicitly requests a different font.',\n },\n author: {\n reason: 'Brand identity — do not invent. Only set when the user provides a value.',\n },\n brand_name: {\n reason: 'Brand identity — do not invent. Only set when the user provides a value.',\n },\n logo_url: {\n reason: 'Brand identity — do not invent. Only set when the user provides a value.',\n },\n is_live: {\n reason: 'Going live is a deliberate action. Never auto-publish unless the user asks.',\n },\n};\n\n/**\n * Ken Burns / Slideshow constants — single source of truth.\n * Used by Remotion compositions, API slideshow route, and MCP tools.\n */\nexport const KEN_BURNS_EFFECTS = [\n 'pan-left',\n 'pan-right',\n 'pan-up',\n 'pan-down',\n 'zoom-in-pan-left',\n 'zoom-in-pan-right',\n 'zoom-in',\n 'zoom-out',\n 'static',\n];\n\nexport const KEN_BURNS_PRESETS = [\n 'cinematic',\n 'dramatic',\n 'calm',\n 'documentary',\n 'dreamy',\n 'moody',\n 'energetic',\n];\n\nexport const KEN_BURNS_EASINGS = ['ease-in-out', 'ease-in', 'ease-out', 'linear'];\n\nexport const KEN_BURNS_FIT_MODES = ['cover', 'blur-pad', 'auto'];\n\nexport const SLIDESHOW_TRANSITIONS = [\n 'fade',\n 'fadeblack',\n 'fadewhite',\n 'slideleft',\n 'slideright',\n 'circlecrop',\n 'circleopen',\n 'circleclose',\n 'dissolve',\n 'pixelize',\n 'radial',\n 'smoothleft',\n 'smoothright',\n 'wipeleft',\n 'wiperight',\n 'diagtl',\n 'diagbr',\n 'hblur',\n];\n\n// SLIDESHOW_COMPOSITION_TRANSITIONS (the narrow set Slideshow actually\n// renders, distinct from the broad ffmpeg list above) is DEFINED in\n// video-templates.js, next to the VIDEO_LOOKS code that consumes it, and\n// re-exported below - see that file for why. Defining it here and having\n// video-templates.js import it back created a real index.js <-> video-templates.js\n// import cycle (index.js already re-exports FROM video-templates.js), fixed\n// in #4038 review.\n\n/**\n * Composition IDs that require a Mapbox access token.\n * Map is the primitive (#453); GlobeToCity/CityToGlobe/MapPin are its\n * presets/aliases.\n */\nexport const MAP_COMPOSITIONS = ['Map', 'GlobeToCity', 'CityToGlobe', 'MapPin'];\n\n/**\n * Composition registry - single source of truth for which compositions the MCP\n * server exposes and at what access tier, plus their kind (for docs/tooling).\n *\n * - `tier`: 'public' (everyone) | 'labs' (non-prod, power users/dogfooding) |\n * 'social' (non-prod, marketing templates). The MCP PUBLIC/LABS/SOCIAL\n * lists derive from this (see packages/mcp-server/src/lib/config.ts).\n * - `kind`: 'bed' (full-bleed Scene layer 0) | 'overlay' (Scene asset layer) |\n * 'container' (Scene itself) | 'video' (whole standalone pre-baked video).\n * - `scene_layer`: usable as a Scene layer (#652). SCENE_LAYER_IDS in\n * packages/remotion derives from this - never hand-list layer ids there.\n * `kind` says where a comp sits when layered; `scene_layer` says whether it\n * may be (most beds render standalone only).\n *\n * Variant presets (e.g. a 'neon' Text) are NOT separate entries - they\n * are the base composition + a `preset` prop. Render-only/dev compositions\n * (test cards, Map preset-aliases, StatOverlay) are intentionally absent: they\n * are registered in Remotion but not advertised on the MCP. The GridList/\n * FourImageGrid/OddOneOutReveal Grid layout-aliases that used to live here\n * were deleted outright (#1615) - a list is just Grid with\n * `layout: { columns: 1 }`, not its own primitive. FlagReveal, ColorCards and\n * TitleCard were folded the same way (#1736) - they are VIDEO_TEMPLATE_REGISTRY\n * entries (video-templates.js) that expand onto Slideshow/Grid/Text through Scene,\n * not COMPOSITION_REGISTRY primitives.\n */\nexport const COMPOSITION_REGISTRY = [\n // Beds (full-bleed backgrounds)\n { id: 'Slideshow', tier: 'public', kind: 'bed', scene_layer: true },\n { id: 'Map', tier: 'public', kind: 'bed', scene_layer: true },\n { id: 'Grid', tier: 'public', kind: 'bed', scene_layer: true },\n { id: 'Timeline', tier: 'labs', kind: 'bed' },\n { id: 'Silhouette', tier: 'labs', kind: 'bed', scene_layer: true },\n // Overlays (assets layered on a bed)\n { id: 'Text', tier: 'labs', kind: 'overlay', scene_layer: true },\n { id: 'Rebus', tier: 'labs', kind: 'overlay', scene_layer: true },\n // Container\n { id: 'Scene', tier: 'labs', kind: 'container' },\n // Social marketing templates (gated)\n { id: 'ShortFormQuiz', tier: 'social', kind: 'video' },\n { id: 'ScorecardQuiz', tier: 'social', kind: 'video' },\n { id: 'ScorecardGK', tier: 'social', kind: 'video' },\n { id: 'CountrySilhouetteQuiz', tier: 'social', kind: 'video' },\n];\n\nconst compositionIdsByTier = (tier) =>\n COMPOSITION_REGISTRY.filter((c) => c.tier === tier).map((c) => c.id);\n\nexport const PUBLIC_COMPOSITION_IDS = compositionIdsByTier('public');\nexport const LABS_COMPOSITION_IDS = compositionIdsByTier('labs');\nexport const SOCIAL_COMPOSITION_IDS = compositionIdsByTier('social');\n\n/**\n * The composition surface exposed for a given environment (#1674, #1789).\n * `labs`/`social` are a pure environment gate - not a per-workspace/billing\n * concept - so this takes a single boolean rather than reading `process.env`\n * itself (this package stays dependency-free; callers resolve NODE_ENV at\n * their own edge). Single derivation shared by the MCP server\n * (packages/mcp-server/src/lib/config.ts) and the API's render gate\n * (apps/api/src/routes/internal/render.ts) - never hand-roll a second copy\n * of this list.\n */\nexport function exposedCompositionIds(includeNonPublic) {\n return [\n ...PUBLIC_COMPOSITION_IDS,\n ...(includeNonPublic ? LABS_COMPOSITION_IDS : []),\n ...(includeNonPublic ? SOCIAL_COMPOSITION_IDS : []),\n ];\n}\n\n/** Compositions usable as Scene layers (#652) - the single source for\n * SCENE_LAYER_IDS in packages/remotion and (via sync-schemas) the API's\n * per-layer validation allowlist. */\nexport const SCENE_LAYER_COMPOSITION_IDS = COMPOSITION_REGISTRY\n .filter((c) => c.scene_layer)\n .map((c) => c.id);\n\n/**\n * Minimum plan tier for an MCP tool to appear in tools/list on the remote\n * (OAuth) MCP server (#471). Tools not listed default to 0 (every plan).\n * Presentation-level only - real enforcement (rate limits, plan checks)\n * stays in the API routes. stdio/API-key sessions always see all tools.\n *\n * To gate a tool, add e.g. `clipform_render_composition: 1`.\n */\nexport const MCP_TOOL_TIERS = {};\n\n/**\n * Connect-time MCP toolset partition (#3309). A client may opt into a subset\n * of tools via `/mcp?features=account,forms,media` (apps/api/src/mcp/handler.ts),\n * so `tools/list` only ships the schemas it needs - a form-only client drops\n * the media-tool payload entirely instead of downloading it and ignoring it.\n * Every registered clipform_* tool maps to exactly ONE toolset here - enforced\n * by a parity test that statically walks packages/mcp-server/src (same\n * pattern as MCP_TOOL_TIERS' sibling tests).\n *\n * This is a DIFFERENT axis than apps/api/src/lib/tool-phases.ts: tool-phases\n * groups tools by build STEP (research/build/attach) for the dashboard bot's\n * dynamic per-turn filtering, and its RESEARCH_TOOLS list straddles both\n * `forms` and `media` below. Toolsets group by connect-time CLIENT INTENT\n * (account/forms/research/media) and are static for the life of a session -\n * one grouping per axis, not a re-label of the other.\n *\n * `research` (#3681) holds the content-research helpers that proxy third\n * parties or exist only for generation workflows (news lookup, generation\n * audit log). They are excluded from the Claude Connectors Directory surface\n * (mcp.clipform.io/forms = account + forms) so that surface is pure first\n * party form building - a client with native web search has no use for them.\n */\nexport const MCP_TOOLSETS = ['account', 'forms', 'research', 'media'];\n\nexport const MCP_TOOL_TOOLSETS = {\n // account: workspace/account-scoped read + admin tools.\n clipform_list_forms: 'account',\n clipform_get_form: 'account',\n clipform_delete_form: 'account',\n clipform_whoami: 'account',\n clipform_get_results: 'account',\n clipform_get_responses: 'account',\n\n // forms: form/node structure + craft guidance used while building.\n clipform_create_form: 'forms',\n clipform_import_form: 'forms',\n clipform_add_node: 'forms',\n clipform_update_node: 'forms',\n clipform_delete_node: 'forms',\n clipform_update_form: 'forms',\n clipform_set_logic: 'forms',\n clipform_get_guide: 'forms',\n clipform_get_workflow: 'forms',\n\n // research: third-party content lookup + generation-workflow audit.\n clipform_search_news: 'research',\n clipform_log_generation: 'research',\n\n // media: creative/render/asset tools. fetch_boundary only feeds the Map\n // composition's `boundary` prop, so it lives with the render tools.\n clipform_fetch_boundary: 'media',\n clipform_generate_tts: 'media',\n clipform_generate_video: 'media',\n clipform_render_composition: 'media',\n clipform_render_video_template: 'media',\n clipform_check_render: 'media',\n clipform_upload_media_asset: 'media',\n clipform_complete_media_upload: 'media',\n clipform_attach_node_media: 'media',\n clipform_get_node_media: 'media',\n clipform_delete_node_media: 'media',\n clipform_search_media: 'media',\n clipform_search_music: 'media',\n clipform_list_compositions: 'media',\n clipform_list_assets: 'media',\n clipform_list_video_templates: 'media',\n};\n\n/**\n * Documentation RAG embeddings (the `docs.chunks` vector store powering the\n * dashboard docs chatbot). SINGLE SOURCE OF TRUTH for the OpenAI embedding\n * model + dimensions.\n *\n * BOTH sides must use these or retrieval silently breaks: the ingest side\n * (`scripts/embed-docs.mjs`, which writes the chunk vectors) and the query\n * side (`apps/api/src/routes/v1/docs/chat.ts`, which embeds the user's\n * question). If they drift, questions get embedded in a different vector\n * space than the stored chunks and `match_chunks` returns garbage.\n *\n * Pinned to 1536 dims (not text-embedding-3-large's native 3072) so the\n * vectors fit the `docs.chunks vector(1536)` column AND stay under pgvector's\n * 2000-dim HNSW index limit - no schema migration needed. Changing either\n * value requires a full re-embed (`FULL_REEMBED=1 node scripts/embed-docs.mjs`):\n * a model/dim swap doesn't change content hashes, so the incremental path\n * would skip every chunk.\n */\nexport const DOCS_EMBEDDING_MODEL = 'text-embedding-3-large';\nexport const DOCS_EMBEDDING_DIMENSIONS = 1536;\n","import type { McpToolset } from \"@vid-master/config\";\n\n/**\n * What a connected surface can actually do, derived from the connect-time\n * toolset filter (#3309) so the guidance layers (server instructions, prompt\n * recipes, `clipform_get_workflow`, craft guides) never reference a tool the\n * surface doesn't register (#3681).\n *\n * The Claude Connectors Directory surface (`mcp.clipform.io/forms`) is\n * account + forms only: no media generation and no research helpers. A\n * workflow that still says \"call clipform_generate_tts\" there sends the model\n * at a tool that doesn't exist, which shows up in review as failed tool\n * calls. Every guidance producer takes a ServerCaps and gates on it.\n */\nexport interface ServerCaps {\n /** Narration, video, image, music, render, and asset tools are registered. */\n media: boolean;\n /** News lookup and the generation audit log are registered. */\n research: boolean;\n}\n\n/** Every toolset registered - stdio/npx and the bare remote endpoint. */\nexport const FULL_CAPS: ServerCaps = Object.freeze({ media: true, research: true });\n\nexport function capsFromToolsets(toolsets?: readonly McpToolset[]): ServerCaps {\n if (toolsets === undefined) return FULL_CAPS;\n return {\n media: toolsets.includes(\"media\"),\n research: toolsets.includes(\"research\"),\n };\n}\n","import {\n getMcpAuth,\n MCP_ANONYMOUS_HEADER,\n MCP_AUTH_VIA_HEADER,\n MCP_MINT_PROOF_HEADER,\n} from \"./auth-context.js\";\nimport { reportError, isServerFault, isAuthFault } from \"./telemetry.js\";\n\nfunction getApiBaseUrl(): string {\n const url = process.env.API_URL;\n if (!url) {\n const message =\n \"API_URL must be set (e.g. http://localhost:3003 or https://api.clipform.io)\";\n // Reachable in production only through the in-process reporter\n // (apps/api/src/lib/mcp-telemetry-bridge.ts) - the default HTTP reporter\n // needs this same API_URL to know where to POST, so it can never fire\n // here on its own (telemetry.ts's reportError, #2599 blocker 5).\n reportError({\n error_type: \"config_error\",\n error_message: message,\n tool_name: getMcpAuth()?.tool_name,\n });\n throw new Error(message);\n }\n return url;\n}\n\n\nfunction getAuthHeaders(): Record<string, string> {\n const authCtx = getMcpAuth();\n const apiKey = authCtx?.api_key || process.env.CLIPFORM_API_KEY;\n if (!apiKey) {\n const message = \"CLIPFORM_API_KEY must be set.\";\n reportError({\n error_type: \"config_error\",\n error_message: message,\n tool_name: authCtx?.tool_name,\n });\n throw new Error(message);\n }\n\n const headers: Record<string, string> = { \"Authorization\": `Bearer ${apiKey}` };\n // Propagate anonymity into the bot-key lane (#2983): the app-chat persona\n // for an anonymous Supabase session runs as an API-key caller under the\n // hood (ensureBotApiKey), so without this marker the API can't tell that\n // request apart from a real integration's key and would let it publish /\n // mint credentials that a direct anonymous-JWT call is denied. See\n // MCP_ANONYMOUS_HEADER's own doc comment for why trusting it is safe.\n if (authCtx?.is_anonymous) {\n headers[MCP_ANONYMOUS_HEADER] = \"1\";\n }\n // #3716: propagate the OAuth-connected label onto the bot-key lane's\n // request so /v1/me reports auth_mode \"oauth\" instead of \"api_key\" - see\n // MCP_AUTH_VIA_HEADER's own doc comment for why trusting it is safe.\n if (authCtx?.auth_via === \"oauth\") {\n headers[MCP_AUTH_VIA_HEADER] = \"oauth\";\n }\n // #3330 review fix: propagate the mint proof (if this request's anon lane\n // freshly minted an identity) so create-form.ts can safely offer a\n // same-UID claim link - see MCP_MINT_PROOF_HEADER's doc comment.\n if (authCtx?.mcp_mint_proof) {\n headers[MCP_MINT_PROOF_HEADER] = authCtx.mcp_mint_proof;\n }\n return headers;\n}\n\ninterface ApiOptions {\n method?: string;\n body?: Record<string, unknown>;\n params?: Record<string, string>;\n timeoutMs?: number;\n}\n\ntype ApiResult =\n | { ok: true; data: Record<string, unknown> }\n | { ok: false; status: number; error: string };\n\nexport async function callApi(\n path: string,\n options: ApiOptions = {}\n): Promise<ApiResult> {\n const { body, params, timeoutMs = 30_000 } = options;\n const method = options.method || (body ? \"POST\" : \"GET\");\n\n const base = getApiBaseUrl();\n const prefix = path.startsWith(\"/internal/\") ? \"\" : \"/v1\";\n let url = `${base}${prefix}${path}`;\n if (params) {\n const searchParams = new URLSearchParams(params);\n url += `?${searchParams.toString()}`;\n }\n\n // Creative endpoints (/internal/*) authenticate with the same Bearer API\n // key as everything else. The X-Service-Secret header the MCP used to send\n // is webhook-trigger auth only and was dropped here (#412 F7).\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n ...getAuthHeaders(),\n };\n\n const fetchOptions: RequestInit = {\n method,\n headers,\n signal: AbortSignal.timeout(timeoutMs),\n };\n\n if (body && method !== \"GET\") {\n fetchOptions.body = JSON.stringify(body);\n }\n\n let response: Response;\n try {\n response = await fetch(url, fetchOptions);\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n // A client-side AbortSignal.timeout() abort is a genuine post-ceiling\n // timeout, not a network drop - classify it separately so it doesn't get\n // misfiled as connection_error (#4070). Node 18+/undici names this\n // DOMException \"TimeoutError\"; also match \"AbortError\" / the message\n // substring as a fallback in case the runtime differs. Still reported\n // either way (#2976), just bucketed distinctly.\n const isTimeout =\n err instanceof Error &&\n (err.name === \"TimeoutError\" ||\n err.name === \"AbortError\" ||\n /aborted due to timeout/i.test(message));\n // Full error stays in our own telemetry (internal, never returned to the\n // model) - the tool response gets a stable, friendly message with no\n // internal URL or raw fetch error text (response hygiene, #2976).\n reportError({\n error_type: isTimeout ? \"timeout\" : \"connection_error\",\n error_message: message,\n tool_name: getMcpAuth()?.tool_name,\n api_path: `${prefix}${path}`,\n });\n return {\n ok: false,\n status: 0,\n error: \"Unable to reach the Clipform API right now. Please try again in a moment.\",\n };\n }\n\n if (response.status === 204) {\n return { ok: true, data: {} };\n }\n\n const data = (await response.json()) as Record<string, unknown>;\n\n if (!response.ok) {\n const hint = recoveryHint(response.status, path);\n const apiMsg = (data.error as string) || `API error (${response.status})`;\n const errorMsg = hint ? `${apiMsg} - ${hint}` : apiMsg;\n // The API's own error `code` (index.ts's onError: `{ code, error }`) is\n // what tells a genuine auth 403 (FORBIDDEN) apart from an ordinary\n // business-logic 403 (e.g. PLAN_LIMIT) - see isAuthFault (#2599).\n const errorCode = typeof data.code === \"string\" ? data.code : undefined;\n if (isServerFault(response.status) || isAuthFault(response.status, errorCode)) {\n reportError({\n error_type: `http_${response.status}`,\n error_message: errorMsg,\n tool_name: getMcpAuth()?.tool_name,\n status_code: response.status,\n api_path: `${prefix}${path}`,\n });\n }\n return {\n ok: false,\n status: response.status,\n error: errorMsg,\n };\n }\n\n return { ok: true, data };\n}\n\n/** Next-step hint by status + path so an agent can recover instead of\n * retrying blind (#651). The raw API message stays first, hint second. */\nexport function recoveryHint(status: number, path: string): string | undefined {\n if (status === 401 || status === 403) {\n return \"Check credentials: the key comes from CLIPFORM_API_KEY or the OAuth session - read clipform://context/session for the active workspace and auth mode.\";\n }\n if (status === 404) {\n if (/^\\/forms\\/[^/]+\\/nodes\\//.test(path)) {\n return \"Call clipform_get_form to list this form's nodes and their IDs.\";\n }\n if (/^\\/forms\\/[^/]+/.test(path)) {\n return \"Call clipform_list_forms to find valid form IDs (UUIDs, not the share_id from a form URL).\";\n }\n }\n return undefined;\n}\n\nexport function errorResult(message: string) {\n return {\n content: [{ type: \"text\" as const, text: message }],\n isError: true,\n };\n}\n\nexport function textResult(text: string) {\n return {\n content: [{ type: \"text\" as const, text }],\n };\n}\n\n/** Like textResult, but also carries a machine-readable `structuredContent`.\n * A tool that declares an `outputSchema` MUST return `structuredContent` on\n * every non-error result (the MCP SDK validates it and throws otherwise) - use\n * this for every success/empty path, and `errorResult` (exempt) for failures.\n * Project to the schema's explicit field set here - never pass raw API rows\n * (response-hygiene invariant, #797). */\nexport function structuredResult(\n text: string,\n structuredContent: Record<string, unknown>\n) {\n return {\n content: [{ type: \"text\" as const, text }],\n structuredContent,\n };\n}\n","import { BUSINESS } from \"@vid-master/config\";\nimport { callApi } from \"./api-client.js\";\n\nexport type AuthMode = \"authenticated\" | \"anonymous\";\n\nexport interface SessionContextResult {\n text: string;\n authMode: AuthMode;\n}\n\nexport async function getSessionContextWithAuth(): Promise<SessionContextResult> {\n let result;\n try {\n result = await callApi(\"/me\", { method: \"GET\" });\n } catch {\n return { text: \"\", authMode: \"anonymous\" };\n }\n if (!result.ok) return { text: \"\", authMode: \"anonymous\" };\n\n const me = result.data as any;\n // The API's auth_mode is \"api_key\" | \"oauth\" | \"session\" | \"anonymous\".\n // Anything credentialed counts as authenticated - the old `=== \"oauth\"`\n // check matched nothing the API returns, so API-key sessions were labelled\n // anonymous with a \"sign in to unlock\" nudge right next to their real\n // plan (#412 F6).\n const apiAuthMode: string = (me.auth_mode as string) ?? \"anonymous\";\n const authMode: AuthMode = apiAuthMode === \"anonymous\" ? \"anonymous\" : \"authenticated\";\n const plan = me.plan;\n const lines: string[] = [];\n\n lines.push(\"## Your Session\");\n if (authMode === \"authenticated\") {\n lines.push(\n apiAuthMode === \"api_key\"\n ? \"Auth: API key (workspace-scoped)\"\n : \"Auth: signed in (connected to a Clipform account)\"\n );\n // Workspace NAME only - the workspace UUID and the raw user_id/company_id\n // are internal database identifiers the model never needs (response\n // hygiene / disclosure guard, #2976).\n lines.push(`Workspace: ${me.workspace?.name ?? \"Unknown\"}`);\n } else {\n lines.push(\"Auth: anonymous (not connected to a Clipform account)\");\n if (me.workspace) {\n lines.push(`Workspace: ${me.workspace.name}`);\n }\n }\n lines.push(`Plan: ${plan.name} (tier ${plan.tier})`);\n if (plan.node_limit !== null) {\n lines.push(`Question limit: ${plan.node_limit} per form`);\n } else {\n lines.push(\"Questions: unlimited\");\n }\n if (plan.form_limit !== null) {\n lines.push(`Form limit: ${plan.form_limit} forms`);\n } else {\n lines.push(\"Forms: unlimited\");\n }\n if (plan.custom_theme === false) {\n lines.push(\"Custom themes: not available on this plan\");\n }\n if (authMode === \"anonymous\") {\n // Neutral statement of fact, no upsell/CTA copy (commerce sweep, #2976):\n // an anonymous session's forms are not tied to an account until one signs in.\n lines.push(`\\nAnonymous session: forms are not tied to a Clipform account until you sign in.`);\n }\n\n return { text: lines.join(\"\\n\"), authMode };\n}\n\nexport async function getSessionContext(): Promise<string> {\n const { text } = await getSessionContextWithAuth();\n return text;\n}\n"],"mappings":"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","names":[]}