mcp-scraper 0.66.0 → 0.66.2

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.
Files changed (88) hide show
  1. package/CHANGELOG.md +32 -1
  2. package/README.md +2 -2
  3. package/dist/{analytics-repository-IKE53S3E.js → analytics-repository-RUXBFSFX.js} +1 -2
  4. package/dist/bin/api-server.cjs +2898 -2075
  5. package/dist/bin/api-server.js +4 -5
  6. package/dist/bin/mcp-scraper-cli.cjs +1 -2
  7. package/dist/bin/mcp-scraper-cli.js +6 -7
  8. package/dist/bin/mcp-scraper-install.cjs +7 -3
  9. package/dist/bin/mcp-scraper-install.js +2 -3
  10. package/dist/bin/mcp-stdio-server.cjs +1669 -97
  11. package/dist/bin/mcp-stdio-server.js +11 -12
  12. package/dist/bin/paa-harvest.cjs +0 -1
  13. package/dist/bin/paa-harvest.js +4 -5
  14. package/dist/{chunk-VIWT35S6.js → chunk-3EYALCYK.js} +1 -2
  15. package/dist/{chunk-4LINSJNI.js → chunk-3TKKJQB4.js} +1 -2
  16. package/dist/{chunk-4VG3NUUY.js → chunk-55B3T5L2.js} +23 -28
  17. package/dist/{chunk-FG6LJGJ7.js → chunk-77LQV73B.js} +0 -1
  18. package/dist/{chunk-N54JS5WP.js → chunk-ABYBPODM.js} +1 -2
  19. package/dist/chunk-ALEBMPOW.js +6 -0
  20. package/dist/{chunk-2XUXVKT4.js → chunk-AYJHI2FK.js} +0 -1
  21. package/dist/{chunk-AAGVOPP5.js → chunk-CETDPNMZ.js} +24 -25
  22. package/dist/{chunk-5BEPOFKG.js → chunk-CNLLTD5B.js} +0 -1
  23. package/dist/{chunk-P7YFUNKR.js → chunk-DBWWIK7O.js} +3 -4
  24. package/dist/{chunk-EKJ65E3Z.js → chunk-DNM65UCK.js} +0 -1
  25. package/dist/{chunk-3YSEJTMC.js → chunk-GGZEC22A.js} +0 -1
  26. package/dist/{chunk-AJ4YGHG2.js → chunk-K7Y27ND3.js} +3 -14
  27. package/dist/{chunk-XPFRP5QT.js → chunk-KBTUPRTF.js} +3 -4
  28. package/dist/{chunk-5PIO7QBG.js → chunk-KXRWY7DZ.js} +3 -4
  29. package/dist/{chunk-DCWXVAQT.js → chunk-OZDCTNUV.js} +6 -2
  30. package/dist/{chunk-BIUQ6O2Y.js → chunk-OZJMVCDK.js} +0 -1
  31. package/dist/{chunk-X7UG2JCF.js → chunk-PGJQDMC2.js} +1 -2
  32. package/dist/{chunk-DKIG2TEE.js → chunk-PMCT2YQM.js} +2 -3
  33. package/dist/{chunk-G3P3ZDB4.js → chunk-RJ7JVYKU.js} +0 -1
  34. package/dist/{chunk-WD73OFL3.js → chunk-VXLU74YZ.js} +1 -2
  35. package/dist/{chunk-SRRI2HJL.js → chunk-YKL3O7IU.js} +1742 -104
  36. package/dist/{db-BRI465XQ.js → db-4RVZ3NYZ.js} +9 -10
  37. package/dist/{extract-bundle-QAI7JYV2.js → extract-bundle-EA5URLXN.js} +8 -9
  38. package/dist/index.cjs +0 -1
  39. package/dist/index.js +4 -5
  40. package/dist/{lead-list-enrichment-repository-ODWGRVCQ.js → lead-list-enrichment-repository-IBXNWM6H.js} +2 -3
  41. package/dist/{location-data-repository-67MMEVZQ.js → location-data-repository-WHHQUCUI.js} +3 -4
  42. package/dist/{server-MTKWBSAR.js → server-Q7VYGSGQ.js} +450 -1264
  43. package/dist/{site-extract-repository-J3RH3MOS.js → site-extract-repository-NTM4MDQN.js} +4 -5
  44. package/dist/{worker-K7MPPQ7I.js → worker-UTBUWEXQ.js} +8 -9
  45. package/package.json +4 -1
  46. package/dist/analytics-repository-IKE53S3E.js.map +0 -1
  47. package/dist/bin/api-server.cjs.map +0 -1
  48. package/dist/bin/api-server.js.map +0 -1
  49. package/dist/bin/mcp-scraper-cli.cjs.map +0 -1
  50. package/dist/bin/mcp-scraper-cli.js.map +0 -1
  51. package/dist/bin/mcp-scraper-install.cjs.map +0 -1
  52. package/dist/bin/mcp-scraper-install.js.map +0 -1
  53. package/dist/bin/mcp-stdio-server.cjs.map +0 -1
  54. package/dist/bin/mcp-stdio-server.js.map +0 -1
  55. package/dist/bin/paa-harvest.cjs.map +0 -1
  56. package/dist/bin/paa-harvest.js.map +0 -1
  57. package/dist/chunk-2XUXVKT4.js.map +0 -1
  58. package/dist/chunk-3YSEJTMC.js.map +0 -1
  59. package/dist/chunk-4LINSJNI.js.map +0 -1
  60. package/dist/chunk-4VG3NUUY.js.map +0 -1
  61. package/dist/chunk-5BEPOFKG.js.map +0 -1
  62. package/dist/chunk-5PIO7QBG.js.map +0 -1
  63. package/dist/chunk-AAGVOPP5.js.map +0 -1
  64. package/dist/chunk-AJ4YGHG2.js.map +0 -1
  65. package/dist/chunk-BIUQ6O2Y.js.map +0 -1
  66. package/dist/chunk-DCWXVAQT.js.map +0 -1
  67. package/dist/chunk-DKIG2TEE.js.map +0 -1
  68. package/dist/chunk-EKJ65E3Z.js.map +0 -1
  69. package/dist/chunk-FG6LJGJ7.js.map +0 -1
  70. package/dist/chunk-G3P3ZDB4.js.map +0 -1
  71. package/dist/chunk-HDYOVGC6.js +0 -7
  72. package/dist/chunk-HDYOVGC6.js.map +0 -1
  73. package/dist/chunk-N54JS5WP.js.map +0 -1
  74. package/dist/chunk-P7YFUNKR.js.map +0 -1
  75. package/dist/chunk-SRRI2HJL.js.map +0 -1
  76. package/dist/chunk-VIWT35S6.js.map +0 -1
  77. package/dist/chunk-WD73OFL3.js.map +0 -1
  78. package/dist/chunk-X7UG2JCF.js.map +0 -1
  79. package/dist/chunk-XPFRP5QT.js.map +0 -1
  80. package/dist/db-BRI465XQ.js.map +0 -1
  81. package/dist/extract-bundle-QAI7JYV2.js.map +0 -1
  82. package/dist/index.cjs.map +0 -1
  83. package/dist/index.js.map +0 -1
  84. package/dist/lead-list-enrichment-repository-ODWGRVCQ.js.map +0 -1
  85. package/dist/location-data-repository-67MMEVZQ.js.map +0 -1
  86. package/dist/server-MTKWBSAR.js.map +0 -1
  87. package/dist/site-extract-repository-J3RH3MOS.js.map +0 -1
  88. package/dist/worker-K7MPPQ7I.js.map +0 -1
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/api/analytics-repository.ts","../src/api/session.ts","../src/api/analytics-attribution.ts"],"sourcesContent":["import { createHash, createHmac, randomBytes, randomUUID } from \"node:crypto\";\nimport { Pool, type PoolClient, type QueryResultRow } from \"pg\";\nimport { getSessionSecret } from \"./session.js\";\nimport { resolveAnalyticsAttribution } from \"./analytics-attribution.js\";\n\nexport type AnalyticsRole = \"viewer\" | \"editor\" | \"owner\";\nexport type PixelStatus = \"active\" | \"disabled\";\nexport type PixelDomainState =\n \"observed_unverified\" | \"approved\" | \"rejected\" | \"revoked\";\n\nexport class AnalyticsRepositoryError extends Error {\n constructor(\n public readonly code: string,\n message: string,\n public readonly status = 400,\n ) {\n super(message);\n this.name = \"AnalyticsRepositoryError\";\n }\n}\n\nlet pool: Pool | null = null;\n\nfunction databaseUrl(): string {\n const value = process.env.ANALYTICS_DATABASE_URL?.trim();\n if (!value) {\n throw new AnalyticsRepositoryError(\n \"analytics_database_unavailable\",\n \"Site Analytics is not configured.\",\n 503,\n );\n }\n return value;\n}\n\nexport function getAnalyticsPool(): Pool {\n if (!pool) pool = new Pool({ connectionString: databaseUrl(), max: 8 });\n return pool;\n}\n\nexport async function closeAnalyticsPool(): Promise<void> {\n if (!pool) return;\n const current = pool;\n pool = null;\n await current.end();\n}\n\nexport async function migrateAnalytics(): Promise<void> {\n const db = getAnalyticsPool();\n const client = await db.connect();\n await client.query(\"BEGIN\");\n try {\n await client.query(`\n CREATE TABLE IF NOT EXISTS analytics_schema_migrations (\n version text PRIMARY KEY,\n applied_at timestamptz NOT NULL DEFAULT now()\n )\n `);\n await client.query(`\n CREATE TABLE IF NOT EXISTS analytics_sites (\n id uuid PRIMARY KEY,\n slug text NOT NULL,\n name text NOT NULL,\n owner_user_id bigint NOT NULL,\n timezone text NOT NULL DEFAULT 'UTC',\n status text NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'disabled')),\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now(),\n UNIQUE(owner_user_id, slug)\n )\n `);\n await client.query(`\n CREATE TABLE IF NOT EXISTS analytics_site_grants (\n site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,\n user_id bigint NOT NULL,\n role text NOT NULL CHECK (role IN ('viewer', 'editor', 'owner')),\n created_at timestamptz NOT NULL DEFAULT now(),\n PRIMARY KEY(site_id, user_id)\n )\n `);\n await client.query(`\n CREATE TABLE IF NOT EXISTS analytics_pixels (\n id uuid PRIMARY KEY,\n site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,\n public_id text NOT NULL UNIQUE,\n name text NOT NULL,\n environment text NOT NULL DEFAULT 'production',\n status text NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'disabled')),\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now(),\n disabled_at timestamptz\n )\n `);\n await client.query(`\n CREATE TABLE IF NOT EXISTS analytics_pixel_domains (\n id uuid PRIMARY KEY,\n pixel_id uuid NOT NULL REFERENCES analytics_pixels(id) ON DELETE CASCADE,\n hostname text NOT NULL,\n state text NOT NULL DEFAULT 'observed_unverified'\n CHECK (state IN ('observed_unverified', 'approved', 'rejected', 'revoked')),\n first_seen_at timestamptz NOT NULL DEFAULT now(),\n last_seen_at timestamptz NOT NULL DEFAULT now(),\n accepted_count bigint NOT NULL DEFAULT 0,\n rejected_count bigint NOT NULL DEFAULT 0,\n updated_at timestamptz NOT NULL DEFAULT now(),\n UNIQUE(pixel_id, hostname)\n )\n `);\n await client.query(`\n CREATE TABLE IF NOT EXISTS analytics_events (\n id uuid PRIMARY KEY,\n event_id text NOT NULL,\n site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,\n pixel_id uuid NOT NULL REFERENCES analytics_pixels(id) ON DELETE CASCADE,\n hostname text NOT NULL,\n visitor_id text,\n session_id text,\n event_name text NOT NULL,\n occurred_at timestamptz NOT NULL,\n received_at timestamptz NOT NULL DEFAULT now(),\n path text NOT NULL DEFAULT '/',\n canonical_url text,\n title text,\n referrer text,\n source text,\n medium text,\n campaign text,\n device_class text,\n properties jsonb NOT NULL DEFAULT '{}'::jsonb,\n bot_status text NOT NULL DEFAULT 'unknown',\n UNIQUE(site_id, event_id)\n )\n `);\n await client.query(`\n CREATE TABLE IF NOT EXISTS analytics_conversions (\n id uuid PRIMARY KEY,\n source_event_id text NOT NULL,\n site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,\n pixel_id uuid REFERENCES analytics_pixels(id) ON DELETE SET NULL,\n session_id text,\n conversion_kind text NOT NULL,\n value_minor bigint NOT NULL DEFAULT 0,\n currency text NOT NULL DEFAULT 'USD',\n occurred_at timestamptz NOT NULL,\n created_at timestamptz NOT NULL DEFAULT now(),\n UNIQUE(site_id, source_event_id)\n )\n `);\n await client.query(`\n CREATE TABLE IF NOT EXISTS analytics_ingestion_receipts (\n id uuid PRIMARY KEY,\n request_id text NOT NULL UNIQUE,\n site_id uuid REFERENCES analytics_sites(id) ON DELETE SET NULL,\n pixel_id uuid REFERENCES analytics_pixels(id) ON DELETE SET NULL,\n hostname text,\n accepted_count integer NOT NULL DEFAULT 0,\n rejected_count integer NOT NULL DEFAULT 0,\n reason_codes jsonb NOT NULL DEFAULT '[]'::jsonb,\n created_at timestamptz NOT NULL DEFAULT now()\n )\n `);\n await client.query(`\n CREATE TABLE IF NOT EXISTS analytics_rollups_daily (\n site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,\n pixel_id uuid NOT NULL REFERENCES analytics_pixels(id) ON DELETE CASCADE,\n hostname text NOT NULL,\n bucket date NOT NULL,\n source text NOT NULL DEFAULT '(direct)',\n medium text NOT NULL DEFAULT '(none)',\n campaign text NOT NULL DEFAULT '(none)',\n pageviews integer NOT NULL DEFAULT 0,\n events integer NOT NULL DEFAULT 0,\n visitors integer NOT NULL DEFAULT 0,\n sessions integer NOT NULL DEFAULT 0,\n refreshed_at timestamptz NOT NULL DEFAULT now(),\n PRIMARY KEY(site_id, pixel_id, hostname, bucket, source, medium, campaign)\n )\n `);\n await client.query(`\n CREATE TABLE IF NOT EXISTS analytics_rollup_runs (\n id uuid PRIMARY KEY,\n window_start timestamptz NOT NULL,\n window_end timestamptz NOT NULL,\n status text NOT NULL CHECK (status IN ('running', 'complete', 'failed')),\n row_count integer NOT NULL DEFAULT 0,\n started_at timestamptz NOT NULL DEFAULT now(),\n completed_at timestamptz,\n error_code text\n )\n `);\n await client.query(`\n CREATE TABLE IF NOT EXISTS analytics_exports (\n id uuid PRIMARY KEY,\n site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,\n requested_by_user_id bigint NOT NULL,\n idempotency_key text,\n report text NOT NULL,\n format text NOT NULL CHECK (format IN ('csv', 'json', 'markdown')),\n filters jsonb NOT NULL,\n content text NOT NULL,\n created_at timestamptz NOT NULL DEFAULT now()\n )\n `);\n await client.query(`\n CREATE TABLE IF NOT EXISTS analytics_campaign_links (\n id uuid PRIMARY KEY,\n site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,\n pixel_id uuid REFERENCES analytics_pixels(id) ON DELETE SET NULL,\n name text NOT NULL,\n destination_url text NOT NULL,\n short_code text NOT NULL UNIQUE,\n utm_source text NOT NULL,\n utm_medium text NOT NULL,\n utm_campaign text NOT NULL,\n utm_term text,\n utm_content text,\n ad_group text,\n ad_name text,\n creative_id text,\n status text NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'archived')),\n created_by_user_id bigint NOT NULL,\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now()\n )\n `);\n await client.query(`\n CREATE TABLE IF NOT EXISTS analytics_campaign_clicks (\n id uuid PRIMARY KEY,\n link_id uuid NOT NULL REFERENCES analytics_campaign_links(id) ON DELETE CASCADE,\n occurred_at timestamptz NOT NULL DEFAULT now(),\n referrer text\n )\n `);\n await client.query(`\n CREATE TABLE IF NOT EXISTS analytics_forms (\n id uuid PRIMARY KEY,\n public_id text NOT NULL UNIQUE,\n site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,\n pixel_id uuid NOT NULL REFERENCES analytics_pixels(id) ON DELETE RESTRICT,\n name text NOT NULL,\n slug text NOT NULL,\n fields jsonb NOT NULL,\n brand jsonb NOT NULL,\n submit_label text NOT NULL DEFAULT 'Submit',\n success_message text NOT NULL DEFAULT 'Thank you. Your response was received.',\n redirect_url text,\n consent_text text,\n status text NOT NULL DEFAULT 'draft' CHECK (status IN ('draft', 'published', 'archived')),\n version integer NOT NULL DEFAULT 1,\n created_by_user_id bigint NOT NULL,\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now(),\n UNIQUE(site_id, slug)\n )\n `);\n await client.query(`\n CREATE TABLE IF NOT EXISTS analytics_form_submissions (\n id uuid PRIMARY KEY,\n form_id uuid NOT NULL REFERENCES analytics_forms(id) ON DELETE CASCADE,\n site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,\n pixel_id uuid NOT NULL REFERENCES analytics_pixels(id) ON DELETE RESTRICT,\n visitor_id text,\n session_id text,\n crm_person_ref text,\n source text,\n medium text,\n campaign text,\n occurred_at timestamptz NOT NULL DEFAULT now()\n )\n `);\n await client.query(\n `ALTER TABLE analytics_form_submissions ADD COLUMN IF NOT EXISTS crm_delivery_status text NOT NULL DEFAULT 'pending'`,\n );\n await client.query(`\n CREATE TABLE IF NOT EXISTS analytics_form_delivery_jobs (\n id uuid PRIMARY KEY,\n submission_id uuid NOT NULL UNIQUE REFERENCES analytics_form_submissions(id) ON DELETE CASCADE,\n owner_user_id bigint NOT NULL,\n payload_ciphertext text NOT NULL,\n status text NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'delivered', 'failed', 'expired')),\n attempts integer NOT NULL DEFAULT 0,\n last_error_code text,\n next_attempt_at timestamptz NOT NULL DEFAULT now(),\n expires_at timestamptz NOT NULL DEFAULT now() + interval '7 days',\n created_at timestamptz NOT NULL DEFAULT now(),\n delivered_at timestamptz\n )\n `);\n await client.query(`\n CREATE TABLE IF NOT EXISTS analytics_people (\n id uuid PRIMARY KEY,\n site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,\n crm_person_ref text NOT NULL,\n first_seen_at timestamptz NOT NULL DEFAULT now(),\n last_seen_at timestamptz NOT NULL DEFAULT now(),\n UNIQUE(site_id, crm_person_ref)\n )\n `);\n await client.query(`\n CREATE TABLE IF NOT EXISTS analytics_identity_nodes (\n id uuid PRIMARY KEY,\n site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,\n kind text NOT NULL CHECK (kind IN ('visitor_id','session_id','email','phone','customer_id','click_id','device_signal','network_signal')),\n value_hmac text NOT NULL,\n first_seen_at timestamptz NOT NULL DEFAULT now(),\n last_seen_at timestamptz NOT NULL DEFAULT now(),\n UNIQUE(site_id, kind, value_hmac)\n )\n `);\n await client.query(`\n CREATE TABLE IF NOT EXISTS analytics_identity_edges (\n id uuid PRIMARY KEY,\n site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,\n person_id uuid NOT NULL REFERENCES analytics_people(id) ON DELETE CASCADE,\n identity_node_id uuid NOT NULL REFERENCES analytics_identity_nodes(id) ON DELETE CASCADE,\n evidence_kind text NOT NULL,\n confidence numeric(4,3) NOT NULL CHECK (confidence >= 0 AND confidence <= 1),\n consent_scope text NOT NULL DEFAULT 'analytics',\n first_seen_at timestamptz NOT NULL DEFAULT now(),\n last_seen_at timestamptz NOT NULL DEFAULT now(),\n UNIQUE(person_id, identity_node_id, evidence_kind)\n )\n `);\n await client.query(\n `ALTER TABLE analytics_events ADD COLUMN IF NOT EXISTS click_ids jsonb NOT NULL DEFAULT '{}'::jsonb`,\n );\n await client.query(\n `ALTER TABLE analytics_form_submissions ADD COLUMN IF NOT EXISTS click_ids jsonb NOT NULL DEFAULT '{}'::jsonb`,\n );\n await client.query(\n `ALTER TABLE analytics_form_submissions ADD COLUMN IF NOT EXISTS person_id uuid REFERENCES analytics_people(id) ON DELETE SET NULL`,\n );\n await client.query(\n `ALTER TABLE analytics_events ADD COLUMN IF NOT EXISTS person_id uuid REFERENCES analytics_people(id) ON DELETE SET NULL`,\n );\n await client.query(\n `ALTER TABLE analytics_events ADD COLUMN IF NOT EXISTS channel_family text`,\n );\n await client.query(\n `ALTER TABLE analytics_events ADD COLUMN IF NOT EXISTS platform text`,\n );\n await client.query(\n `ALTER TABLE analytics_events ADD COLUMN IF NOT EXISTS campaign_id text`,\n );\n await client.query(\n `ALTER TABLE analytics_events ADD COLUMN IF NOT EXISTS ad_set_id text`,\n );\n await client.query(\n `ALTER TABLE analytics_events ADD COLUMN IF NOT EXISTS ad_id text`,\n );\n await client.query(\n `ALTER TABLE analytics_events ADD COLUMN IF NOT EXISTS creative_id text`,\n );\n await client.query(\n `ALTER TABLE analytics_events ADD COLUMN IF NOT EXISTS placement text`,\n );\n await client.query(\n `ALTER TABLE analytics_events ADD COLUMN IF NOT EXISTS utm_term text`,\n );\n await client.query(\n `ALTER TABLE analytics_events ADD COLUMN IF NOT EXISTS utm_content text`,\n );\n await client.query(\n `ALTER TABLE analytics_events ADD COLUMN IF NOT EXISTS engaged_ms integer`,\n );\n await client.query(\n `ALTER TABLE analytics_events ADD COLUMN IF NOT EXISTS scroll_depth smallint`,\n );\n await client.query(\n `ALTER TABLE analytics_conversions ADD COLUMN IF NOT EXISTS person_id uuid REFERENCES analytics_people(id) ON DELETE SET NULL`,\n );\n await client.query(\n `ALTER TABLE analytics_sites ADD COLUMN IF NOT EXISTS business_model text NOT NULL DEFAULT 'leadgen' CHECK (business_model IN ('leadgen','saas','ecommerce'))`,\n );\n await client.query(`\n CREATE TABLE IF NOT EXISTS analytics_ad_spend_daily (\n site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,\n bucket date NOT NULL,\n platform text NOT NULL,\n campaign text NOT NULL DEFAULT '(all)',\n spend_minor bigint NOT NULL CHECK (spend_minor >= 0),\n currency text NOT NULL DEFAULT 'USD',\n source_ref text,\n updated_at timestamptz NOT NULL DEFAULT now(),\n PRIMARY KEY(site_id, bucket, platform, campaign)\n )\n `);\n await client.query(`\n CREATE TABLE IF NOT EXISTS analytics_crm_imports (\n id uuid PRIMARY KEY,\n site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,\n requested_by_user_id bigint NOT NULL,\n source_system text NOT NULL,\n filename text NOT NULL,\n status text NOT NULL DEFAULT 'queued' CHECK (status IN ('queued','processing','complete','partial','failed')),\n accepted_count integer NOT NULL DEFAULT 0,\n rejected_count integer NOT NULL DEFAULT 0,\n created_at timestamptz NOT NULL DEFAULT now(),\n completed_at timestamptz\n )\n `);\n await client.query(`\n CREATE TABLE IF NOT EXISTS analytics_crm_import_rows (\n id uuid PRIMARY KEY,\n import_id uuid NOT NULL REFERENCES analytics_crm_imports(id) ON DELETE CASCADE,\n crm_person_ref text NOT NULL,\n payload_ciphertext text NOT NULL,\n delivery_status text NOT NULL DEFAULT 'pending' CHECK (delivery_status IN ('pending','delivered','failed','expired')),\n attempts integer NOT NULL DEFAULT 0,\n last_error_code text,\n created_at timestamptz NOT NULL DEFAULT now(),\n delivered_at timestamptz,\n UNIQUE(import_id, crm_person_ref)\n )\n `);\n await client.query(`\n CREATE TABLE IF NOT EXISTS analytics_activation_destinations (\n id uuid PRIMARY KEY,\n site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,\n platform text NOT NULL CHECK (platform IN ('meta','google','tiktok','reddit')),\n name text NOT NULL,\n connection_ref text,\n external_dataset_id text,\n status text NOT NULL DEFAULT 'active' CHECK (status IN ('active','paused','archived')),\n event_mapping jsonb NOT NULL DEFAULT '{}'::jsonb,\n created_by_user_id bigint NOT NULL,\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now()\n )\n `);\n await client.query(`\n CREATE TABLE IF NOT EXISTS analytics_activation_jobs (\n id uuid PRIMARY KEY,\n destination_id uuid NOT NULL REFERENCES analytics_activation_destinations(id) ON DELETE CASCADE,\n conversion_id uuid NOT NULL REFERENCES analytics_conversions(id) ON DELETE CASCADE,\n person_id uuid REFERENCES analytics_people(id) ON DELETE SET NULL,\n payload_ciphertext text NOT NULL,\n status text NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','delivered','failed','expired')),\n attempts integer NOT NULL DEFAULT 0,\n last_error_code text,\n next_attempt_at timestamptz NOT NULL DEFAULT now(),\n created_at timestamptz NOT NULL DEFAULT now(),\n delivered_at timestamptz,\n UNIQUE(destination_id, conversion_id)\n )\n `);\n await client.query(`\n CREATE TABLE IF NOT EXISTS analytics_thorbit_entitlements (\n user_id bigint PRIMARY KEY,\n thorbit_org_id text,\n connection_source text NOT NULL DEFAULT 'trial' CHECK (connection_source IN ('trial','api_key','thorbit_session')),\n eligible boolean NOT NULL DEFAULT false,\n reason text NOT NULL,\n plan_id text,\n subscription_status text,\n monthly_recurring_revenue_cents integer NOT NULL DEFAULT 0,\n minimum_monthly_recurring_revenue_cents integer NOT NULL DEFAULT 5000,\n verified_at timestamptz NOT NULL,\n refresh_after timestamptz NOT NULL,\n access_expires_at timestamptz NOT NULL,\n trial_started_at timestamptz NOT NULL DEFAULT now(),\n trial_ends_at timestamptz NOT NULL DEFAULT (now() + interval '30 days'),\n last_refresh_error text,\n updated_at timestamptz NOT NULL DEFAULT now()\n )\n `);\n await client.query(\n `ALTER TABLE analytics_thorbit_entitlements ALTER COLUMN thorbit_org_id DROP NOT NULL`,\n );\n await client.query(\n `ALTER TABLE analytics_thorbit_entitlements ADD COLUMN IF NOT EXISTS trial_started_at timestamptz NOT NULL DEFAULT now()`,\n );\n await client.query(\n `ALTER TABLE analytics_thorbit_entitlements ADD COLUMN IF NOT EXISTS trial_ends_at timestamptz NOT NULL DEFAULT (now() + interval '30 days')`,\n );\n await client.query(\n `ALTER TABLE analytics_thorbit_entitlements DROP CONSTRAINT IF EXISTS analytics_thorbit_entitlements_connection_source_check`,\n );\n await client.query(\n `ALTER TABLE analytics_thorbit_entitlements ADD CONSTRAINT analytics_thorbit_entitlements_connection_source_check CHECK (connection_source IN ('trial','api_key','thorbit_session'))`,\n );\n await client.query(`\n CREATE TABLE IF NOT EXISTS analytics_thorbit_connect_states (\n state_hash text PRIMARY KEY,\n user_id bigint NOT NULL,\n expires_at timestamptz NOT NULL,\n consumed_at timestamptz,\n created_at timestamptz NOT NULL DEFAULT now()\n )\n `);\n await client.query(\n `ALTER TABLE analytics_exports ADD COLUMN IF NOT EXISTS idempotency_key text`,\n );\n await client.query(\n `ALTER TABLE analytics_events ADD COLUMN IF NOT EXISTS country_code text`,\n );\n await client.query(\n `ALTER TABLE analytics_events ADD COLUMN IF NOT EXISTS region_code text`,\n );\n await client.query(`CREATE UNIQUE INDEX IF NOT EXISTS analytics_exports_owner_idempotency\n ON analytics_exports(requested_by_user_id, idempotency_key) WHERE idempotency_key IS NOT NULL`);\n await client.query(\n \"CREATE INDEX IF NOT EXISTS analytics_events_site_time ON analytics_events(site_id, occurred_at DESC)\",\n );\n await client.query(\n \"CREATE INDEX IF NOT EXISTS analytics_events_pixel_time ON analytics_events(pixel_id, occurred_at DESC)\",\n );\n await client.query(\n \"CREATE INDEX IF NOT EXISTS analytics_events_site_hostname_time ON analytics_events(site_id, hostname, occurred_at DESC)\",\n );\n await client.query(\n \"CREATE INDEX IF NOT EXISTS analytics_events_acquisition ON analytics_events(site_id, source, medium, campaign, occurred_at DESC)\",\n );\n await client.query(\n \"CREATE INDEX IF NOT EXISTS analytics_conversions_site_time ON analytics_conversions(site_id, occurred_at DESC)\",\n );\n await client.query(`\n INSERT INTO analytics_schema_migrations(version)\n VALUES ('2026-08-05.1'), ('2026-08-05.2'), ('2026-08-05.3'), ('2026-08-05.4'), ('2026-08-05.5'), ('2026-08-05.6'), ('2026-08-05.7'), ('2026-08-05.8'), ('2026-08-05.9'), ('2026-08-05.10'), ('2026-08-05.11'), ('2026-08-05.12'), ('2026-08-05.13'), ('2026-08-05.14')\n ON CONFLICT (version) DO NOTHING\n `);\n await client.query(\"COMMIT\");\n } catch (error) {\n await client.query(\"ROLLBACK\");\n throw error;\n } finally {\n client.release();\n }\n}\n\nfunction normalizeSlug(value: string): string {\n const slug = value\n .trim()\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\")\n .slice(0, 80);\n return slug || `site-${randomBytes(4).toString(\"hex\")}`;\n}\n\nexport function normalizeObservedHostname(\n origin: string | null | undefined,\n): string | null {\n if (!origin || origin === \"null\") return null;\n try {\n const url = new URL(origin);\n if (url.protocol !== \"http:\" && url.protocol !== \"https:\") return null;\n return url.hostname.toLowerCase().replace(/\\.$/, \"\");\n } catch {\n return null;\n }\n}\n\nfunction publicPixelId(): string {\n return `px_${randomBytes(18).toString(\"base64url\")}`;\n}\n\nfunction mapRows<T>(rows: QueryResultRow[]): T[] {\n return rows as T[];\n}\n\nasync function accessFor(\n client: Pool | PoolClient,\n siteId: string,\n userId: number,\n): Promise<AnalyticsRole> {\n const result = await client.query<{ role: AnalyticsRole }>(\n `SELECT role FROM analytics_site_grants WHERE site_id = $1 AND user_id = $2`,\n [siteId, userId],\n );\n const role = result.rows[0]?.role;\n if (!role)\n throw new AnalyticsRepositoryError(\n \"analytics_site_not_found\",\n \"Site not found.\",\n 404,\n );\n return role;\n}\n\nasync function requireEditor(\n client: Pool | PoolClient,\n siteId: string,\n userId: number,\n): Promise<AnalyticsRole> {\n const role = await accessFor(client, siteId, userId);\n if (role === \"viewer\")\n throw new AnalyticsRepositoryError(\n \"analytics_forbidden\",\n \"Editor access is required.\",\n 403,\n );\n return role;\n}\n\nexport interface AnalyticsSiteRow {\n id: string;\n slug: string;\n name: string;\n timezone: string;\n status: string;\n role: AnalyticsRole;\n pixel_count: number;\n last_event_at: string | null;\n business_model: \"leadgen\" | \"saas\" | \"ecommerce\";\n}\n\nexport async function createAnalyticsSite(input: {\n userId: number;\n name: string;\n slug?: string;\n timezone?: string;\n}): Promise<AnalyticsSiteRow> {\n const db = getAnalyticsPool();\n const client = await db.connect();\n const id = randomUUID();\n const baseSlug = normalizeSlug(input.slug || input.name);\n try {\n await client.query(\"BEGIN\");\n let slug = baseSlug;\n for (let attempt = 0; attempt < 3; attempt += 1) {\n try {\n await client.query(\n `INSERT INTO analytics_sites(id, slug, name, owner_user_id, timezone)\n VALUES ($1, $2, $3, $4, $5)`,\n [id, slug, input.name.trim(), input.userId, input.timezone || \"UTC\"],\n );\n break;\n } catch (error) {\n const code = (error as { code?: string }).code;\n if (code !== \"23505\" || attempt === 2) throw error;\n slug = `${baseSlug}-${randomBytes(2).toString(\"hex\")}`;\n }\n }\n await client.query(\n `INSERT INTO analytics_site_grants(site_id, user_id, role) VALUES ($1, $2, 'owner')`,\n [id, input.userId],\n );\n await client.query(\"COMMIT\");\n return {\n id,\n slug,\n name: input.name.trim(),\n timezone: input.timezone || \"UTC\",\n status: \"active\",\n role: \"owner\",\n pixel_count: 0,\n last_event_at: null,\n business_model: \"leadgen\",\n };\n } catch (error) {\n await client.query(\"ROLLBACK\");\n throw error;\n } finally {\n client.release();\n }\n}\n\nexport async function listAnalyticsSites(\n userId: number,\n): Promise<AnalyticsSiteRow[]> {\n const result = await getAnalyticsPool().query(\n `SELECT s.id, s.slug, s.name, s.timezone, s.status, s.business_model, g.role,\n count(DISTINCT p.id)::int AS pixel_count,\n max(e.occurred_at)::text AS last_event_at\n FROM analytics_sites s\n JOIN analytics_site_grants g ON g.site_id = s.id AND g.user_id = $1\n LEFT JOIN analytics_pixels p ON p.site_id = s.id\n LEFT JOIN analytics_events e ON e.site_id = s.id\n GROUP BY s.id, g.role\n ORDER BY s.created_at ASC`,\n [userId],\n );\n return mapRows<AnalyticsSiteRow>(result.rows);\n}\n\nexport async function updateAnalyticsBusinessModel(input: {\n siteId: string;\n userId: number;\n businessModel: \"leadgen\" | \"saas\" | \"ecommerce\";\n}) {\n const db = getAnalyticsPool();\n await requireEditor(db, input.siteId, input.userId);\n await db.query(\n \"UPDATE analytics_sites SET business_model = $1, updated_at = now() WHERE id = $2\",\n [input.businessModel, input.siteId],\n );\n}\n\nexport async function upsertAnalyticsAdSpend(input: {\n siteId: string;\n userId: number;\n rows: Array<{\n date: string;\n platform: string;\n campaign?: string;\n spendMinor: number;\n currency?: string;\n sourceRef?: string;\n }>;\n}) {\n const db = getAnalyticsPool();\n await requireEditor(db, input.siteId, input.userId);\n for (const row of input.rows) {\n await db.query(\n `INSERT INTO analytics_ad_spend_daily(site_id, bucket, platform, campaign, spend_minor, currency, source_ref)\n VALUES ($1,$2::date,$3,$4,$5,$6,$7)\n ON CONFLICT(site_id,bucket,platform,campaign) DO UPDATE SET spend_minor=EXCLUDED.spend_minor,currency=EXCLUDED.currency,source_ref=EXCLUDED.source_ref,updated_at=now()`,\n [\n input.siteId,\n row.date,\n row.platform.slice(0, 80),\n row.campaign?.slice(0, 240) || \"(all)\",\n row.spendMinor,\n (row.currency || \"USD\").toUpperCase(),\n row.sourceRef?.slice(0, 500) || null,\n ],\n );\n }\n return { accepted: input.rows.length };\n}\n\nexport async function analyticsBusinessMetrics(\n siteId: string,\n userId: number,\n start: string,\n end: string,\n) {\n const db = getAnalyticsPool();\n await accessFor(db, siteId, userId);\n const site = await db.query<{\n business_model: \"leadgen\" | \"saas\" | \"ecommerce\";\n }>(\"SELECT business_model FROM analytics_sites WHERE id=$1\", [siteId]);\n const result = await db.query<{\n conversions: number;\n customers: number;\n leads: number;\n purchases: number;\n trials: number;\n subscriptions: number;\n churns: number;\n revenue_minor: string;\n repeat_customers: number;\n }>(\n `SELECT count(*)::int AS conversions, count(DISTINCT person_id)::int AS customers,\n count(*) FILTER (WHERE conversion_kind IN ('form_submitted','lead','qualified_lead'))::int AS leads,\n count(*) FILTER (WHERE conversion_kind IN ('purchase','order_completed'))::int AS purchases,\n count(*) FILTER (WHERE conversion_kind IN ('trial_started','demo_started'))::int AS trials,\n count(*) FILTER (WHERE conversion_kind IN ('subscription_started','subscription_renewed'))::int AS subscriptions,\n count(*) FILTER (WHERE conversion_kind IN ('subscription_cancelled','churned'))::int AS churns,\n COALESCE(sum(value_minor),0)::bigint AS revenue_minor,\n (SELECT count(*)::int FROM (SELECT person_id FROM analytics_conversions WHERE site_id=$1 AND person_id IS NOT NULL AND conversion_kind IN ('purchase','order_completed') AND occurred_at >= $2 AND occurred_at < $3 GROUP BY person_id HAVING count(*) > 1) r) AS repeat_customers\n FROM analytics_conversions WHERE site_id=$1 AND occurred_at >= $2 AND occurred_at < $3`,\n [siteId, start, end],\n );\n const spend = await db.query<{ spend_minor: string }>(\n \"SELECT COALESCE(sum(spend_minor),0)::bigint AS spend_minor FROM analytics_ad_spend_daily WHERE site_id=$1 AND bucket >= $2::date AND bucket < $3::date\",\n [siteId, start, end],\n );\n const row = result.rows[0];\n const spendMinor = Number(spend.rows[0]?.spend_minor || 0);\n const revenueMinor = Number(row.revenue_minor || 0);\n return {\n businessModel: site.rows[0]?.business_model || \"leadgen\",\n period: { start, end },\n metrics: {\n ...row,\n revenue_minor: revenueMinor,\n spend_minor: spendMinor,\n roas: spendMinor > 0 ? revenueMinor / spendMinor : null,\n cost_per_lead:\n spendMinor > 0 && row.leads > 0 ? spendMinor / row.leads : null,\n average_order_value:\n row.purchases > 0 ? revenueMinor / row.purchases : null,\n repeat_customer_rate:\n row.customers > 0 ? row.repeat_customers / row.customers : null,\n churn_rate: row.subscriptions > 0 ? row.churns / row.subscriptions : null,\n },\n missingInputs: spendMinor > 0 ? [] : [\"ad_spend\"],\n };\n}\n\nexport async function analyticsForecast(\n siteId: string,\n userId: number,\n horizonMonths = 12,\n) {\n const db = getAnalyticsPool();\n await accessFor(db, siteId, userId);\n const rows = await db.query<{\n month: string;\n revenue_minor: string;\n spend_minor: string;\n }>(\n `WITH months AS (SELECT generate_series(date_trunc('month', now()) - interval '11 months', date_trunc('month', now()), interval '1 month') AS month)\n SELECT m.month::date::text AS month,\n COALESCE((SELECT sum(value_minor) FROM analytics_conversions c WHERE c.site_id=$1 AND c.occurred_at >= m.month AND c.occurred_at < m.month+interval '1 month'),0)::bigint AS revenue_minor,\n COALESCE((SELECT sum(spend_minor) FROM analytics_ad_spend_daily s WHERE s.site_id=$1 AND s.bucket >= m.month::date AND s.bucket < (m.month+interval '1 month')::date),0)::bigint AS spend_minor\n FROM months m ORDER BY m.month`,\n [siteId],\n );\n const history = rows.rows.map((row) => ({\n month: row.month,\n revenueMinor: Number(row.revenue_minor),\n spendMinor: Number(row.spend_minor),\n }));\n const nonEmpty = history.filter(\n (row) => row.revenueMinor > 0 || row.spendMinor > 0,\n );\n if (nonEmpty.length < 3)\n return {\n status: \"insufficient_data\",\n history,\n forecast: [],\n horizonMonths,\n missingInputs: [\"at_least_3_months_of_revenue_or_spend\"],\n };\n const values = history.map((row) => row.revenueMinor);\n const n = values.length;\n const meanX = (n - 1) / 2;\n const meanY = values.reduce((a, b) => a + b, 0) / n;\n const slope =\n values.reduce(\n (sum, value, index) => sum + (index - meanX) * (value - meanY),\n 0,\n ) / values.reduce((sum, _value, index) => sum + (index - meanX) ** 2, 0);\n const intercept = meanY - slope * meanX;\n const residual = Math.sqrt(\n values.reduce(\n (sum, value, index) => sum + (value - (intercept + slope * index)) ** 2,\n 0,\n ) / n,\n );\n const spendMonths = history.filter((row) => row.spendMinor > 0);\n const avgSpend = spendMonths.length\n ? spendMonths.reduce((sum, row) => sum + row.spendMinor, 0) /\n spendMonths.length\n : 0;\n const lastMonth = new Date(`${history.at(-1)?.month}T00:00:00Z`);\n const forecast = Array.from(\n { length: Math.min(Math.max(horizonMonths, 1), 36) },\n (_, offset) => {\n const date = new Date(\n Date.UTC(\n lastMonth.getUTCFullYear(),\n lastMonth.getUTCMonth() + offset + 1,\n 1,\n ),\n );\n const base = Math.max(0, intercept + slope * (n + offset));\n return {\n month: date.toISOString().slice(0, 10),\n lowerRevenueMinor: Math.max(0, Math.round(base - residual)),\n baseRevenueMinor: Math.round(base),\n upperRevenueMinor: Math.round(base + residual),\n projectedSpendMinor: Math.round(avgSpend),\n baseRoas: avgSpend > 0 ? base / avgSpend : null,\n };\n },\n );\n return {\n status: \"modeled\",\n method: \"monthly_linear_trend_v1\",\n history,\n forecast,\n horizonMonths: forecast.length,\n missingInputs: avgSpend > 0 ? [] : [\"ad_spend\"],\n caveats: [\n \"Scenario range reflects historical residual variation, not a guarantee.\",\n ],\n };\n}\n\nexport async function deleteAnalyticsSite(input: {\n siteId: string;\n userId: number;\n}): Promise<void> {\n const db = getAnalyticsPool();\n const role = await accessFor(db, input.siteId, input.userId);\n if (role !== \"owner\")\n throw new AnalyticsRepositoryError(\n \"analytics_forbidden\",\n \"Owner access is required.\",\n 403,\n );\n const result = await db.query(\n `DELETE FROM analytics_sites WHERE id = $1 AND owner_user_id = $2`,\n [input.siteId, input.userId],\n );\n if (!result.rowCount)\n throw new AnalyticsRepositoryError(\n \"analytics_site_not_found\",\n \"Site not found.\",\n 404,\n );\n}\n\nexport interface AnalyticsPixelRow {\n id: string;\n site_id: string;\n public_id: string;\n name: string;\n environment: string;\n status: PixelStatus;\n created_at: string;\n last_event_at: string | null;\n domains: Array<{\n hostname: string;\n state: PixelDomainState;\n first_seen_at: string;\n last_seen_at: string;\n accepted_count: number;\n rejected_count: number;\n }>;\n}\n\nexport async function createAnalyticsPixel(input: {\n siteId: string;\n userId: number;\n name: string;\n environment?: string;\n}): Promise<AnalyticsPixelRow> {\n const db = getAnalyticsPool();\n await requireEditor(db, input.siteId, input.userId);\n const row = await db.query<AnalyticsPixelRow>(\n `INSERT INTO analytics_pixels(id, site_id, public_id, name, environment)\n VALUES ($1, $2, $3, $4, $5)\n RETURNING id, site_id, public_id, name, environment, status, created_at::text, NULL::text AS last_event_at`,\n [\n randomUUID(),\n input.siteId,\n publicPixelId(),\n input.name.trim(),\n input.environment || \"production\",\n ],\n );\n return { ...row.rows[0], domains: [] };\n}\n\nexport async function listAnalyticsPixels(\n siteId: string,\n userId: number,\n): Promise<AnalyticsPixelRow[]> {\n const db = getAnalyticsPool();\n await accessFor(db, siteId, userId);\n const [pixels, domains] = await Promise.all([\n db.query<Omit<AnalyticsPixelRow, \"domains\">>(\n `SELECT p.id, p.site_id, p.public_id, p.name, p.environment, p.status,\n p.created_at::text, max(e.occurred_at)::text AS last_event_at\n FROM analytics_pixels p\n LEFT JOIN analytics_events e ON e.pixel_id = p.id\n WHERE p.site_id = $1\n GROUP BY p.id\n ORDER BY p.created_at ASC`,\n [siteId],\n ),\n db.query<AnalyticsPixelRow[\"domains\"][number] & { pixel_id: string }>(\n `SELECT pixel_id, hostname, state, first_seen_at::text, last_seen_at::text,\n accepted_count::int, rejected_count::int\n FROM analytics_pixel_domains\n WHERE pixel_id IN (SELECT id FROM analytics_pixels WHERE site_id = $1)\n ORDER BY last_seen_at DESC`,\n [siteId],\n ),\n ]);\n return pixels.rows.map((pixel) => ({\n ...pixel,\n domains: domains.rows\n .filter((domain) => domain.pixel_id === pixel.id)\n .map(({ pixel_id: _pixelId, ...domain }) => domain),\n }));\n}\n\nexport async function updateAnalyticsPixel(input: {\n siteId: string;\n pixelId: string;\n userId: number;\n name?: string;\n environment?: string;\n status?: PixelStatus;\n}): Promise<void> {\n const db = getAnalyticsPool();\n await requireEditor(db, input.siteId, input.userId);\n const result = await db.query(\n `UPDATE analytics_pixels\n SET name = COALESCE($3, name),\n environment = COALESCE($4, environment),\n status = COALESCE($5, status),\n disabled_at = CASE WHEN $5 = 'disabled' THEN now() WHEN $5 = 'active' THEN NULL ELSE disabled_at END,\n updated_at = now()\n WHERE id = $1 AND site_id = $2`,\n [\n input.pixelId,\n input.siteId,\n input.name ?? null,\n input.environment ?? null,\n input.status ?? null,\n ],\n );\n if (!result.rowCount)\n throw new AnalyticsRepositoryError(\n \"analytics_pixel_not_found\",\n \"Pixel not found.\",\n 404,\n );\n}\n\nexport async function setAnalyticsPixelDomainState(input: {\n siteId: string;\n pixelId: string;\n userId: number;\n hostname: string;\n state: \"approved\" | \"rejected\" | \"revoked\";\n}): Promise<void> {\n const db = getAnalyticsPool();\n await requireEditor(db, input.siteId, input.userId);\n const hostname = normalizeObservedHostname(`https://${input.hostname}`);\n if (!hostname)\n throw new AnalyticsRepositoryError(\n \"invalid_hostname\",\n \"Enter a valid hostname.\",\n );\n const result = await db.query(\n `INSERT INTO analytics_pixel_domains(id, pixel_id, hostname, state)\n SELECT $1, p.id, $4, $5 FROM analytics_pixels p WHERE p.id = $2 AND p.site_id = $3\n ON CONFLICT(pixel_id, hostname) DO UPDATE SET state = EXCLUDED.state, updated_at = now()\n RETURNING id`,\n [randomUUID(), input.pixelId, input.siteId, hostname, input.state],\n );\n if (!result.rowCount)\n throw new AnalyticsRepositoryError(\n \"analytics_pixel_not_found\",\n \"Pixel not found.\",\n 404,\n );\n}\n\nexport interface IngestionEvent {\n eventId: string;\n eventName: string;\n occurredAt: string;\n visitorId?: string;\n sessionId?: string;\n path?: string;\n canonicalUrl?: string;\n title?: string;\n referrer?: string;\n source?: string;\n medium?: string;\n campaign?: string;\n clickIds?: Record<string, string>;\n deviceClass?: string;\n properties?: Record<string, unknown>;\n engagedMs?: number;\n scrollDepth?: number;\n}\n\nexport const MAX_ENGAGED_MS = 30 * 60 * 1_000;\nexport const ENGAGED_SESSION_MS = 10_000;\n\nfunction boundedEngagedMs(value: number | undefined): number | null {\n return typeof value === \"number\" && Number.isFinite(value) && value >= 0\n ? Math.min(Math.trunc(value), MAX_ENGAGED_MS)\n : null;\n}\n\nfunction boundedScrollDepth(value: number | undefined): number | null {\n return typeof value === \"number\" && Number.isFinite(value) && value >= 0\n ? Math.min(Math.trunc(value), 100)\n : null;\n}\n\nconst blockedPropertyName =\n /(email|phone|password|token|secret|authorization|cookie|name|address|form|body)/i;\n\nexport function normalizeAnalyticsPath(value: string | undefined): string {\n if (!value) return \"/\";\n try {\n const parsed = new URL(value, \"https://analytics.invalid\");\n return parsed.pathname.slice(0, 2000) || \"/\";\n } catch {\n return \"/\";\n }\n}\n\nexport function normalizeAnalyticsUrl(\n value: string | undefined,\n): string | null {\n if (!value) return null;\n try {\n const parsed = new URL(value);\n if (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\")\n return null;\n parsed.search = \"\";\n parsed.hash = \"\";\n return parsed.toString().slice(0, 3000);\n } catch {\n return null;\n }\n}\n\nexport function sanitizeAnalyticsProperties(\n value: Record<string, unknown> | undefined,\n): Record<string, string | number | boolean | null> {\n const output: Record<string, string | number | boolean | null> = {};\n for (const [key, property] of Object.entries(value ?? {}).slice(0, 24)) {\n if (!/^[a-z][a-z0-9_]{0,63}$/i.test(key) || blockedPropertyName.test(key))\n continue;\n if (\n property === null ||\n typeof property === \"boolean\" ||\n typeof property === \"number\"\n )\n output[key] = property;\n else if (typeof property === \"string\") output[key] = property.slice(0, 300);\n }\n return output;\n}\n\nexport interface IngestionResult {\n requestId: string;\n accepted: number;\n rejected: number;\n reasonCodes: string[];\n hostname: string | null;\n siteId: string | null;\n pixelId: string | null;\n}\n\nexport async function ingestAnalyticsEvents(input: {\n publicPixelId: string;\n origin: string | null;\n events: IngestionEvent[];\n geography?: { countryCode?: string | null; regionCode?: string | null };\n}): Promise<IngestionResult> {\n const db = getAnalyticsPool();\n const requestId = `air_${randomUUID()}`;\n const hostname = normalizeObservedHostname(input.origin);\n const pixelResult = await db.query<{\n id: string;\n site_id: string;\n status: PixelStatus;\n entitled: boolean;\n }>(\n `SELECT p.id, p.site_id, p.status,\n COALESCE(\n (e.eligible AND e.access_expires_at > now()) OR e.trial_ends_at > now(),\n false\n ) AS entitled\n FROM analytics_pixels p\n JOIN analytics_sites s ON s.id = p.site_id\n LEFT JOIN analytics_thorbit_entitlements e ON e.user_id = s.owner_user_id\n WHERE p.public_id = $1`,\n [input.publicPixelId],\n );\n const pixel = pixelResult.rows[0];\n if (!pixel || pixel.status !== \"active\") {\n const reasons = [pixel ? \"pixel_disabled\" : \"pixel_unknown\"];\n await db.query(\n `INSERT INTO analytics_ingestion_receipts(id, request_id, site_id, pixel_id, hostname, rejected_count, reason_codes)\n VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb)`,\n [\n randomUUID(),\n requestId,\n pixel?.site_id ?? null,\n pixel?.id ?? null,\n hostname,\n input.events.length,\n JSON.stringify(reasons),\n ],\n );\n return {\n requestId,\n accepted: 0,\n rejected: input.events.length,\n reasonCodes: reasons,\n hostname,\n siteId: pixel?.site_id ?? null,\n pixelId: pixel?.id ?? null,\n };\n }\n if (!pixel.entitled) {\n const reasons = [\"thorbit_entitlement_required\"];\n await db.query(\n `INSERT INTO analytics_ingestion_receipts(id, request_id, site_id, pixel_id, hostname, rejected_count, reason_codes)\n VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb)`,\n [\n randomUUID(),\n requestId,\n pixel.site_id,\n pixel.id,\n hostname,\n input.events.length,\n JSON.stringify(reasons),\n ],\n );\n return {\n requestId,\n accepted: 0,\n rejected: input.events.length,\n reasonCodes: reasons,\n hostname,\n siteId: pixel.site_id,\n pixelId: pixel.id,\n };\n }\n if (!hostname) {\n const reasons = [\"origin_missing_or_invalid\"];\n await db.query(\n `INSERT INTO analytics_ingestion_receipts(id, request_id, site_id, pixel_id, rejected_count, reason_codes)\n VALUES ($1, $2, $3, $4, $5, $6::jsonb)`,\n [\n randomUUID(),\n requestId,\n pixel.site_id,\n pixel.id,\n input.events.length,\n JSON.stringify(reasons),\n ],\n );\n return {\n requestId,\n accepted: 0,\n rejected: input.events.length,\n reasonCodes: reasons,\n hostname,\n siteId: pixel.site_id,\n pixelId: pixel.id,\n };\n }\n\n const domainResult = await db.query<{ state: PixelDomainState }>(\n `INSERT INTO analytics_pixel_domains(id, pixel_id, hostname, state, rejected_count)\n VALUES ($1, $2, $3, 'observed_unverified', 0)\n ON CONFLICT(pixel_id, hostname) DO UPDATE\n SET last_seen_at = now(), updated_at = now()\n RETURNING state`,\n [randomUUID(), pixel.id, hostname],\n );\n if (domainResult.rows[0]?.state !== \"approved\") {\n await db.query(\n `UPDATE analytics_pixel_domains\n SET rejected_count = rejected_count + $3, last_seen_at = now(), updated_at = now()\n WHERE pixel_id = $1 AND hostname = $2`,\n [pixel.id, hostname, input.events.length],\n );\n const reasons = [\"domain_unapproved\"];\n await db.query(\n `INSERT INTO analytics_ingestion_receipts(id, request_id, site_id, pixel_id, hostname, rejected_count, reason_codes)\n VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb)`,\n [\n randomUUID(),\n requestId,\n pixel.site_id,\n pixel.id,\n hostname,\n input.events.length,\n JSON.stringify(reasons),\n ],\n );\n return {\n requestId,\n accepted: 0,\n rejected: input.events.length,\n reasonCodes: reasons,\n hostname,\n siteId: pixel.site_id,\n pixelId: pixel.id,\n };\n }\n\n const client = await db.connect();\n let accepted = 0;\n let rejected = 0;\n const reasonCodes = new Set<string>();\n try {\n await client.query(\"BEGIN\");\n for (const event of input.events) {\n try {\n const attribution = resolveAnalyticsAttribution(event);\n const resolvedPerson =\n event.visitorId || event.sessionId\n ? await client.query<{ person_id: string }>(\n `SELECT e.person_id FROM analytics_identity_nodes n JOIN analytics_identity_edges e ON e.identity_node_id = n.id\n WHERE n.site_id = $1 AND ((n.kind = 'visitor_id' AND n.value_hmac = $2) OR (n.kind = 'session_id' AND n.value_hmac = $3))\n ORDER BY e.confidence DESC, e.last_seen_at DESC LIMIT 1`,\n [\n pixel.site_id,\n event.visitorId\n ? identityHmac(\"visitor_id\", event.visitorId)\n : \"\",\n event.sessionId\n ? identityHmac(\"session_id\", event.sessionId)\n : \"\",\n ],\n )\n : { rows: [] };\n const inserted = await client.query(\n `INSERT INTO analytics_events(\n id, event_id, site_id, pixel_id, hostname, visitor_id, session_id, event_name,\n occurred_at, path, canonical_url, title, referrer, source, medium, campaign,\n device_class, properties, country_code, region_code, click_ids, person_id,\n channel_family, platform, campaign_id, ad_set_id, ad_id, creative_id, placement, utm_term, utm_content,\n engaged_ms, scroll_depth\n ) VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18::jsonb, $19, $20, $21::jsonb, $22,\n $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33\n ) ON CONFLICT(site_id, event_id) DO NOTHING`,\n [\n randomUUID(),\n event.eventId,\n pixel.site_id,\n pixel.id,\n hostname,\n event.visitorId ?? null,\n event.sessionId ?? null,\n event.eventName,\n event.occurredAt,\n normalizeAnalyticsPath(event.path || event.canonicalUrl),\n normalizeAnalyticsUrl(event.canonicalUrl),\n event.title?.slice(0, 500) ?? null,\n normalizeAnalyticsUrl(event.referrer),\n event.source?.slice(0, 180) ?? null,\n event.medium?.slice(0, 180) ?? null,\n event.campaign?.slice(0, 240) ?? null,\n event.deviceClass ?? null,\n JSON.stringify(sanitizeAnalyticsProperties(event.properties)),\n normalizeGeographyCode(input.geography?.countryCode, 2),\n normalizeGeographyCode(input.geography?.regionCode, 8),\n JSON.stringify(sanitizeClickIds(event.clickIds)),\n resolvedPerson.rows[0]?.person_id ?? null,\n attribution.channelFamily,\n attribution.platform,\n attribution.campaignId,\n attribution.adSetId,\n attribution.adId,\n attribution.creativeId,\n attribution.placement,\n attribution.term,\n attribution.content,\n boundedEngagedMs(event.engagedMs),\n boundedScrollDepth(event.scrollDepth),\n ],\n );\n if (inserted.rowCount) accepted += 1;\n else {\n rejected += 1;\n reasonCodes.add(\"duplicate_event\");\n }\n } catch {\n rejected += 1;\n reasonCodes.add(\"event_insert_failed\");\n }\n }\n await client.query(\n `UPDATE analytics_pixel_domains\n SET accepted_count = accepted_count + $3,\n rejected_count = rejected_count + $4,\n last_seen_at = now(), updated_at = now()\n WHERE pixel_id = $1 AND hostname = $2`,\n [pixel.id, hostname, accepted, rejected],\n );\n await client.query(\n `INSERT INTO analytics_ingestion_receipts(\n id, request_id, site_id, pixel_id, hostname, accepted_count, rejected_count, reason_codes\n ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb)`,\n [\n randomUUID(),\n requestId,\n pixel.site_id,\n pixel.id,\n hostname,\n accepted,\n rejected,\n JSON.stringify([...reasonCodes]),\n ],\n );\n await client.query(\"COMMIT\");\n } catch (error) {\n await client.query(\"ROLLBACK\");\n throw error;\n } finally {\n client.release();\n }\n return {\n requestId,\n accepted,\n rejected,\n reasonCodes: [...reasonCodes],\n hostname,\n siteId: pixel.site_id,\n pixelId: pixel.id,\n };\n}\n\nexport interface AnalyticsFilters {\n start: string;\n end: string;\n timezone?: string;\n pixelIds?: string[];\n hostnames?: string[];\n source?: string;\n medium?: string;\n campaign?: string;\n eventName?: string;\n attributionModel?: \"first_touch\" | \"last_touch\";\n channelFamily?: string;\n platform?: string;\n referrer?: string;\n path?: string;\n deviceClass?: string;\n countryCode?: string;\n regionCode?: string;\n conversionKind?: string;\n}\n\nfunction normalizeGeographyCode(\n value: string | null | undefined,\n max: number,\n): string | null {\n const normalized = value?.trim().toUpperCase();\n return normalized && /^[A-Z0-9-]+$/.test(normalized)\n ? normalized.slice(0, max)\n : null;\n}\n\nexport interface AnalyticsPageInput {\n limit?: number;\n cursor?: string;\n}\n\nexport interface AnalyticsPageInfo {\n nextCursor: string | null;\n hasNextPage: boolean;\n}\n\nfunction pageFingerprint(\n scope: string,\n siteId: string,\n filters: AnalyticsFilters,\n): string {\n return createHash(\"sha256\")\n .update(JSON.stringify({ scope, siteId, filters }))\n .digest(\"base64url\")\n .slice(0, 18);\n}\n\nfunction decodePageOffset(\n cursor: string | undefined,\n fingerprint: string,\n): number {\n if (!cursor) return 0;\n try {\n const parsed = JSON.parse(\n Buffer.from(cursor, \"base64url\").toString(\"utf8\"),\n ) as { v?: number; f?: string; o?: number };\n if (\n parsed.v !== 1 ||\n parsed.f !== fingerprint ||\n !Number.isInteger(parsed.o) ||\n Number(parsed.o) < 0\n )\n throw new Error(\"invalid\");\n return Number(parsed.o);\n } catch {\n throw new AnalyticsRepositoryError(\n \"invalid_cursor\",\n \"The pagination cursor is invalid for these filters.\",\n );\n }\n}\n\nfunction paginateRows<T>(\n rows: T[],\n input: AnalyticsPageInput | undefined,\n fingerprint: string,\n): { items: T[]; pageInfo: AnalyticsPageInfo } {\n if (!input)\n return { items: rows, pageInfo: { hasNextPage: false, nextCursor: null } };\n const limit = Math.min(100, Math.max(1, Math.trunc(input?.limit ?? 25)));\n const offset = decodePageOffset(input?.cursor, fingerprint);\n const items = rows.slice(offset, offset + limit);\n const hasNextPage = offset + items.length < rows.length;\n return {\n items,\n pageInfo: {\n hasNextPage,\n nextCursor: hasNextPage\n ? Buffer.from(\n JSON.stringify({ v: 1, f: fingerprint, o: offset + items.length }),\n ).toString(\"base64url\")\n : null,\n },\n };\n}\n\nexport async function createAnalyticsConversion(input: {\n userId: number;\n siteId: string;\n pixelId?: string;\n sessionId?: string;\n sourceEventId: string;\n conversionKind: string;\n valueMinor?: number;\n currency?: string;\n occurredAt: string;\n}): Promise<{ id: string; created: boolean }> {\n const db = getAnalyticsPool();\n await requireEditor(db, input.siteId, input.userId);\n if (input.pixelId) {\n const pixel = await db.query(\n `SELECT 1 FROM analytics_pixels WHERE id = $1 AND site_id = $2`,\n [input.pixelId, input.siteId],\n );\n if (!pixel.rowCount)\n throw new AnalyticsRepositoryError(\n \"analytics_pixel_not_found\",\n \"Pixel not found.\",\n 404,\n );\n }\n const id = randomUUID();\n const resolvedPerson = input.sessionId\n ? await db.query<{ person_id: string }>(\n `SELECT e.person_id FROM analytics_identity_nodes n JOIN analytics_identity_edges e ON e.identity_node_id = n.id\n WHERE n.site_id = $1 AND n.kind = 'session_id' AND n.value_hmac = $2 ORDER BY e.confidence DESC LIMIT 1`,\n [input.siteId, identityHmac(\"session_id\", input.sessionId)],\n )\n : { rows: [] };\n const result = await db.query<{ id: string }>(\n `INSERT INTO analytics_conversions(\n id, source_event_id, site_id, pixel_id, session_id, conversion_kind, value_minor, currency, occurred_at, person_id\n ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)\n ON CONFLICT(site_id, source_event_id) DO NOTHING\n RETURNING id`,\n [\n id,\n input.sourceEventId,\n input.siteId,\n input.pixelId ?? null,\n input.sessionId ?? null,\n input.conversionKind,\n input.valueMinor ?? 0,\n (input.currency || \"USD\").toUpperCase(),\n input.occurredAt,\n resolvedPerson.rows[0]?.person_id ?? null,\n ],\n );\n return { id: result.rows[0]?.id ?? id, created: Boolean(result.rowCount) };\n}\n\nfunction eventFilterSql(siteId: string, filters: AnalyticsFilters) {\n const values: unknown[] = [siteId, filters.start, filters.end];\n const clauses = [\n \"e.site_id = $1\",\n \"e.occurred_at >= $2::timestamptz\",\n \"e.occurred_at < $3::timestamptz\",\n ];\n const add = (sql: string, value: unknown) => {\n values.push(value);\n clauses.push(sql.replace(\"?\", `$${values.length}`));\n };\n if (filters.pixelIds?.length)\n add(\"e.pixel_id = ANY(?::uuid[])\", filters.pixelIds);\n if (filters.hostnames?.length)\n add(\"e.hostname = ANY(?::text[])\", filters.hostnames);\n if (filters.source) add(`COALESCE(e.source, '(direct)') = ?`, filters.source);\n if (filters.medium) add(`COALESCE(e.medium, '(none)') = ?`, filters.medium);\n if (filters.campaign)\n add(`COALESCE(e.campaign, '(none)') = ?`, filters.campaign);\n if (filters.eventName) add(\"e.event_name = ?\", filters.eventName);\n if (filters.channelFamily)\n add(`COALESCE(e.channel_family, 'other') = ?`, filters.channelFamily);\n if (filters.platform)\n add(`COALESCE(e.platform, e.source, '(direct)') = ?`, filters.platform);\n if (filters.referrer)\n add(`COALESCE(e.referrer, '') ILIKE '%' || ? || '%'`, filters.referrer);\n if (filters.path) add(`e.path = ?`, filters.path);\n if (filters.deviceClass)\n add(`COALESCE(e.device_class, 'unknown') = ?`, filters.deviceClass);\n if (filters.countryCode)\n add(\n `COALESCE(e.country_code, 'unknown') = ?`,\n filters.countryCode.toUpperCase(),\n );\n if (filters.regionCode)\n add(\n `COALESCE(e.region_code, 'unknown') = ?`,\n filters.regionCode.toUpperCase(),\n );\n return { where: clauses.join(\" AND \"), values };\n}\n\nfunction conversionFilterSql(siteId: string, filters: AnalyticsFilters) {\n const values: unknown[] = [siteId, filters.start, filters.end];\n const clauses = [\n \"c.site_id = $1\",\n \"c.occurred_at >= $2::timestamptz\",\n \"c.occurred_at < $3::timestamptz\",\n ];\n const add = (sql: string, value: unknown) => {\n values.push(value);\n clauses.push(sql.replaceAll(\"?\", `$${values.length}`));\n };\n if (filters.pixelIds?.length)\n add(\"c.pixel_id = ANY(?::uuid[])\", filters.pixelIds);\n if (filters.hostnames?.length)\n add(\n \"EXISTS (SELECT 1 FROM analytics_events ce WHERE ce.site_id = c.site_id AND ce.session_id = c.session_id AND ce.hostname = ANY(?::text[]))\",\n filters.hostnames,\n );\n if (filters.source)\n add(`COALESCE(first_event.source, '(direct)') = ?`, filters.source);\n if (filters.medium)\n add(`COALESCE(first_event.medium, '(none)') = ?`, filters.medium);\n if (filters.campaign)\n add(`COALESCE(first_event.campaign, '(none)') = ?`, filters.campaign);\n if (filters.eventName)\n add(\n \"EXISTS (SELECT 1 FROM analytics_events ce WHERE ce.site_id = c.site_id AND ce.session_id = c.session_id AND ce.event_name = ?)\",\n filters.eventName,\n );\n if (filters.channelFamily)\n add(\n `EXISTS (SELECT 1 FROM analytics_events ce WHERE ce.site_id = c.site_id AND ce.session_id = c.session_id AND COALESCE(ce.channel_family, 'other') = ?)`,\n filters.channelFamily,\n );\n if (filters.platform)\n add(\n `EXISTS (SELECT 1 FROM analytics_events ce WHERE ce.site_id = c.site_id AND ce.session_id = c.session_id AND COALESCE(ce.platform, ce.source, '(direct)') = ?)`,\n filters.platform,\n );\n if (filters.referrer)\n add(\n `EXISTS (SELECT 1 FROM analytics_events ce WHERE ce.site_id = c.site_id AND ce.session_id = c.session_id AND COALESCE(ce.referrer, '') ILIKE '%' || ? || '%')`,\n filters.referrer,\n );\n if (filters.path)\n add(\n `EXISTS (SELECT 1 FROM analytics_events ce WHERE ce.site_id = c.site_id AND ce.session_id = c.session_id AND ce.path = ?)`,\n filters.path,\n );\n if (filters.deviceClass)\n add(\n `EXISTS (SELECT 1 FROM analytics_events ce WHERE ce.site_id = c.site_id AND ce.session_id = c.session_id AND COALESCE(ce.device_class, 'unknown') = ?)`,\n filters.deviceClass,\n );\n if (filters.countryCode)\n add(\n `EXISTS (SELECT 1 FROM analytics_events ce WHERE ce.site_id = c.site_id AND ce.session_id = c.session_id AND COALESCE(ce.country_code, 'unknown') = ?)`,\n filters.countryCode.toUpperCase(),\n );\n if (filters.regionCode)\n add(\n `EXISTS (SELECT 1 FROM analytics_events ce WHERE ce.site_id = c.site_id AND ce.session_id = c.session_id AND COALESCE(ce.region_code, 'unknown') = ?)`,\n filters.regionCode.toUpperCase(),\n );\n if (filters.conversionKind)\n add(`c.conversion_kind = ?`, filters.conversionKind);\n return { where: clauses.join(\" AND \"), values };\n}\n\nexport async function analyticsOverview(\n siteId: string,\n userId: number,\n filters: AnalyticsFilters,\n) {\n const db = getAnalyticsPool();\n await accessFor(db, siteId, userId);\n const query = eventFilterSql(siteId, filters);\n const result = await db.query<{\n visitors: number;\n sessions: number;\n pageviews: number;\n events: number;\n first_event_at: string | null;\n last_event_at: string | null;\n }>(\n `SELECT count(DISTINCT visitor_id)::int AS visitors,\n count(DISTINCT session_id)::int AS sessions,\n count(*) FILTER (WHERE event_name = 'page_view')::int AS pageviews,\n count(*)::int AS events,\n min(occurred_at)::text AS first_event_at,\n max(occurred_at)::text AS last_event_at\n FROM analytics_events e WHERE ${query.where}`,\n query.values,\n );\n const conversionQuery = conversionFilterSql(siteId, filters);\n const conversions = await db.query<{\n conversions: number;\n revenue_minor: number;\n }>(\n `SELECT count(*)::int AS conversions, COALESCE(sum(c.value_minor), 0)::int AS revenue_minor\n FROM analytics_conversions c\n LEFT JOIN LATERAL (\n SELECT e.source, e.medium, e.campaign FROM analytics_events e\n WHERE e.site_id = c.site_id AND e.session_id = c.session_id\n ORDER BY e.occurred_at ASC LIMIT 1\n ) first_event ON true\n WHERE ${conversionQuery.where}`,\n conversionQuery.values,\n );\n const row = result.rows[0];\n const conversion = conversions.rows[0];\n return {\n ...row,\n conversions: conversion.conversions,\n revenueMinor: conversion.revenue_minor,\n conversionRate:\n row.sessions > 0 ? conversion.conversions / row.sessions : 0,\n dataFreshThrough: row.last_event_at,\n };\n}\n\nexport async function analyticsTimeseries(\n siteId: string,\n userId: number,\n filters: AnalyticsFilters,\n) {\n const db = getAnalyticsPool();\n await accessFor(db, siteId, userId);\n const query = eventFilterSql(siteId, filters);\n const timezone = filters.timezone || \"UTC\";\n const timezoneParameter = `$${query.values.length + 1}`;\n const result = await db.query(\n `SELECT date_trunc('day', e.occurred_at AT TIME ZONE ${timezoneParameter})::date::text AS date,\n count(DISTINCT e.visitor_id)::int AS visitors,\n count(DISTINCT e.session_id)::int AS sessions,\n count(*) FILTER (WHERE e.event_name = 'page_view')::int AS pageviews,\n count(*)::int AS events\n FROM analytics_events e\n WHERE ${query.where}\n GROUP BY 1 ORDER BY 1`,\n [...query.values, timezone],\n );\n const conversionQuery = conversionFilterSql(siteId, filters);\n const conversionTimezoneParameter = `$${conversionQuery.values.length + 1}`;\n const conversions = await db.query<{\n date: string;\n conversions: number;\n revenue_minor: string;\n }>(\n `SELECT date_trunc('day', c.occurred_at AT TIME ZONE ${conversionTimezoneParameter})::date::text AS date,\n count(*)::int AS conversions,\n COALESCE(sum(c.value_minor), 0)::bigint AS revenue_minor\n FROM analytics_conversions c\n LEFT JOIN LATERAL (\n SELECT e.source, e.medium, e.campaign FROM analytics_events e\n WHERE e.site_id = c.site_id AND e.session_id = c.session_id\n ORDER BY e.occurred_at ASC LIMIT 1\n ) first_event ON true\n WHERE ${conversionQuery.where}\n GROUP BY 1 ORDER BY 1`,\n [...conversionQuery.values, timezone],\n );\n const byDate = new Map<string, Record<string, string | number>>();\n for (const row of result.rows)\n byDate.set(String(row.date), { ...row, conversions: 0, revenueMinor: 0 });\n for (const row of conversions.rows) {\n const current = byDate.get(row.date) ?? {\n date: row.date,\n visitors: 0,\n sessions: 0,\n pageviews: 0,\n events: 0,\n };\n byDate.set(row.date, {\n ...current,\n conversions: row.conversions,\n revenueMinor: Number(row.revenue_minor),\n });\n }\n const points = [...byDate.values()].sort((left, right) =>\n String(left.date).localeCompare(String(right.date)),\n );\n return { points, timezone, dataFreshThrough: points.at(-1)?.date ?? null };\n}\n\nexport async function analyticsAcquisition(\n siteId: string,\n userId: number,\n filters: AnalyticsFilters,\n page?: AnalyticsPageInput,\n) {\n const db = getAnalyticsPool();\n await accessFor(db, siteId, userId);\n const query = eventFilterSql(siteId, filters);\n const result = await db.query(\n `SELECT COALESCE(e.source, '(direct)') AS source,\n COALESCE(e.medium, '(none)') AS medium,\n COALESCE(e.campaign, '(none)') AS campaign,\n count(DISTINCT e.visitor_id)::int AS visitors,\n count(DISTINCT e.session_id)::int AS sessions,\n count(*) FILTER (WHERE e.event_name = 'page_view')::int AS pageviews\n FROM analytics_events e\n WHERE ${query.where}\n GROUP BY 1, 2, 3\n ORDER BY sessions DESC, visitors DESC\n LIMIT 1000`,\n query.values,\n );\n const values: unknown[] = [siteId, filters.start, filters.end];\n const touchClauses = [\n \"c.site_id = $1\",\n \"c.occurred_at >= $2::timestamptz\",\n \"c.occurred_at < $3::timestamptz\",\n ];\n const addTouch = (sql: string, value: unknown) => {\n values.push(value);\n touchClauses.push(sql.replace(\"?\", `$${values.length}`));\n };\n if (filters.pixelIds?.length)\n addTouch(\"touch.pixel_id = ANY(?::uuid[])\", filters.pixelIds);\n if (filters.hostnames?.length)\n addTouch(\"touch.hostname = ANY(?::text[])\", filters.hostnames);\n if (filters.source)\n addTouch(`COALESCE(touch.source, '(direct)') = ?`, filters.source);\n if (filters.medium)\n addTouch(`COALESCE(touch.medium, '(none)') = ?`, filters.medium);\n if (filters.campaign)\n addTouch(`COALESCE(touch.campaign, '(none)') = ?`, filters.campaign);\n if (filters.channelFamily)\n addTouch(\n `COALESCE(touch.channel_family, 'other') = ?`,\n filters.channelFamily,\n );\n if (filters.platform)\n addTouch(\n `COALESCE(touch.platform, touch.source, '(direct)') = ?`,\n filters.platform,\n );\n if (filters.referrer)\n addTouch(\n `COALESCE(touch.referrer, '') ILIKE '%' || ? || '%'`,\n filters.referrer,\n );\n if (filters.path) addTouch(`touch.path = ?`, filters.path);\n if (filters.deviceClass)\n addTouch(\n `COALESCE(touch.device_class, 'unknown') = ?`,\n filters.deviceClass,\n );\n if (filters.countryCode)\n addTouch(\n `COALESCE(touch.country_code, 'unknown') = ?`,\n filters.countryCode.toUpperCase(),\n );\n if (filters.regionCode)\n addTouch(\n `COALESCE(touch.region_code, 'unknown') = ?`,\n filters.regionCode.toUpperCase(),\n );\n if (filters.conversionKind)\n addTouch(`c.conversion_kind = ?`, filters.conversionKind);\n const direction = filters.attributionModel === \"last_touch\" ? \"DESC\" : \"ASC\";\n const attributed = await db.query<{\n source: string;\n medium: string;\n campaign: string;\n conversions: number;\n revenue_minor: number;\n }>(\n `SELECT COALESCE(touch.source, '(direct)') AS source,\n COALESCE(touch.medium, '(none)') AS medium,\n COALESCE(touch.campaign, '(none)') AS campaign,\n count(*)::int AS conversions, COALESCE(sum(c.value_minor), 0)::bigint AS revenue_minor\n FROM analytics_conversions c\n JOIN LATERAL (\n SELECT e.* FROM analytics_events e\n WHERE e.site_id = c.site_id AND e.occurred_at <= c.occurred_at\n AND ((c.person_id IS NOT NULL AND e.person_id = c.person_id)\n OR (c.person_id IS NULL AND e.session_id = c.session_id))\n ORDER BY e.occurred_at ${direction} LIMIT 1\n ) touch ON true\n WHERE ${touchClauses.join(\" AND \")}\n GROUP BY 1,2,3`,\n values,\n );\n const outcomes = new Map(\n attributed.rows.map((row) => [\n `${row.source}\\u0000${row.medium}\\u0000${row.campaign}`,\n row,\n ]),\n );\n const rows = result.rows.map((row: any) => {\n const outcome = outcomes.get(\n `${row.source}\\u0000${row.medium}\\u0000${row.campaign}`,\n );\n const conversions = Number(outcome?.conversions ?? 0);\n return {\n ...row,\n conversions,\n conversionRate: row.sessions > 0 ? conversions / row.sessions : 0,\n revenueMinor: Number(outcome?.revenue_minor ?? 0),\n };\n });\n for (const outcome of attributed.rows) {\n const key = `${outcome.source}\\u0000${outcome.medium}\\u0000${outcome.campaign}`;\n if (\n !result.rows.some(\n (row: any) =>\n `${row.source}\\u0000${row.medium}\\u0000${row.campaign}` === key,\n )\n ) {\n rows.push({\n source: outcome.source,\n medium: outcome.medium,\n campaign: outcome.campaign,\n visitors: 0,\n sessions: 0,\n pageviews: 0,\n conversions: Number(outcome.conversions),\n conversionRate: 0,\n revenueMinor: Number(outcome.revenue_minor),\n });\n }\n }\n rows.sort(\n (a: any, b: any) =>\n Number(b.sessions) - Number(a.sessions) ||\n Number(b.conversions) - Number(a.conversions),\n );\n const paged = paginateRows(\n rows,\n page,\n pageFingerprint(\"acquisition\", siteId, filters),\n );\n return {\n channels: paged.items,\n pageInfo: paged.pageInfo,\n dataFreshThrough: new Date().toISOString(),\n };\n}\n\nconst inferredFamilySql = `COALESCE(e.channel_family, CASE\n WHEN lower(COALESCE(e.source,'') || ' ' || COALESCE(e.referrer,'')) ~ '(perplexity|chatgpt|openai|claude|anthropic|grok|xai|ai[_ -]?overview|google[_ -]?ai|gemini)' THEN 'llm'\n WHEN lower(COALESCE(e.source,'') || ' ' || COALESCE(e.referrer,'')) ~ '(facebook|instagram|linkedin|tiktok|youtube|youtu\\\\.be|twitter|x\\\\.com)' THEN 'social'\n WHEN lower(COALESCE(e.source,'') || ' ' || COALESCE(e.referrer,'')) ~ '(^|[^a-z])(g2|bbb)([^a-z]|$)|trustpilot|capterra|yelp|better business bureau' THEN 'review'\n WHEN COALESCE(e.source,'') = '' THEN 'direct'\n WHEN lower(COALESCE(e.medium,'')) ~ 'email|newsletter' THEN 'email'\n ELSE 'other' END)`;\n\nexport async function analyticsChannelBreakdown(\n siteId: string,\n userId: number,\n filters: AnalyticsFilters,\n) {\n const db = getAnalyticsPool();\n await accessFor(db, siteId, userId);\n const query = eventFilterSql(siteId, filters);\n const result = await db.query(\n `\n SELECT ${inferredFamilySql} AS family,\n COALESCE(e.platform, NULLIF(e.source,''), '(direct)') AS platform,\n COALESCE(NULLIF(e.campaign,''), '(none)') AS campaign,\n COALESCE(NULLIF(e.campaign_id,''), '(unknown)') AS campaign_id,\n COALESCE(NULLIF(e.ad_set_id,''), '(unknown)') AS ad_set_id,\n COALESCE(NULLIF(e.ad_id,''), '(unknown)') AS ad_id,\n COALESCE(NULLIF(e.creative_id,''), '(unknown)') AS creative_id,\n count(DISTINCT e.visitor_id)::int AS visitors,\n count(DISTINCT e.session_id)::int AS sessions,\n count(*) FILTER (WHERE e.event_name = 'page_view')::int AS pageviews,\n count(*)::int AS events\n FROM analytics_events e\n WHERE ${query.where}\n GROUP BY 1,2,3,4,5,6,7\n ORDER BY sessions DESC, events DESC\n LIMIT 2000`,\n query.values,\n );\n const families = [\"llm\", \"social\", \"review\"] as const;\n const sections = Object.fromEntries(\n families.map((family) => [\n family,\n result.rows.filter((row) => row.family === family),\n ]),\n );\n return {\n sections,\n rows: result.rows,\n dataFreshThrough: new Date().toISOString(),\n };\n}\n\nexport const ANALYTICS_CONTENT_SORTS = [\n \"path\",\n \"visitors\",\n \"sessions\",\n \"pageviews\",\n \"entrances\",\n \"exits\",\n \"engagement_events\",\n \"conversions\",\n \"avg_seconds_on_page\",\n \"avg_scroll_depth\",\n \"bounce_rate\",\n \"exit_rate\",\n \"engagement_rate\",\n \"pageviews_per_session\",\n] as const;\n\nexport type AnalyticsContentSort = (typeof ANALYTICS_CONTENT_SORTS)[number];\n\nexport interface AnalyticsContentOptions {\n sort?: AnalyticsContentSort;\n direction?: \"asc\" | \"desc\";\n search?: string;\n}\n\nexport function normalizeContentOptions(\n options: AnalyticsContentOptions | undefined,\n): Required<Pick<AnalyticsContentOptions, \"sort\" | \"direction\">> & {\n search: string | null;\n} {\n const sort = ANALYTICS_CONTENT_SORTS.includes(options?.sort as never)\n ? (options!.sort as AnalyticsContentSort)\n : \"pageviews\";\n return {\n sort,\n direction: options?.direction === \"asc\" ? \"asc\" : \"desc\",\n search: options?.search?.trim().slice(0, 300) || null,\n };\n}\n\nexport async function analyticsContent(\n siteId: string,\n userId: number,\n filters: AnalyticsFilters,\n page?: AnalyticsPageInput,\n options?: AnalyticsContentOptions,\n) {\n const db = getAnalyticsPool();\n await accessFor(db, siteId, userId);\n const query = eventFilterSql(siteId, filters);\n const table = normalizeContentOptions(options);\n const searchParam = `$${query.values.length + 1}`;\n const order =\n table.sort === \"path\"\n ? `metrics.path ${table.direction === \"asc\" ? \"ASC\" : \"DESC\"}`\n : `${table.sort} ${table.direction === \"asc\" ? \"ASC\" : \"DESC\"} NULLS LAST, metrics.path ASC`;\n const result = await db.query(\n `WITH filtered AS (\n SELECT e.* FROM analytics_events e WHERE ${query.where}\n ), page_metrics AS (\n SELECT path, count(DISTINCT visitor_id)::int AS visitors,\n count(DISTINCT session_id)::int AS sessions,\n count(*) FILTER (WHERE event_name = 'page_view')::int AS pageviews,\n count(*) FILTER (WHERE event_name NOT IN ('page_view', 'page_engagement'))::int AS engagement_events,\n count(*)::int AS events\n FROM filtered GROUP BY path\n ), session_bounds AS (\n SELECT session_id,\n (array_agg(path ORDER BY occurred_at ASC, id ASC))[1] AS entrance_path,\n (array_agg(path ORDER BY occurred_at DESC, id DESC))[1] AS exit_path\n FROM filtered WHERE session_id IS NOT NULL GROUP BY session_id\n ), session_pageviews AS (\n SELECT session_id, count(*) FILTER (WHERE event_name = 'page_view')::int AS pageviews\n FROM filtered WHERE session_id IS NOT NULL GROUP BY session_id\n ), page_dwell AS (\n SELECT session_id, path,\n sum(engaged_ms)::bigint AS engaged_ms,\n max(scroll_depth)::int AS scroll_depth\n FROM filtered\n WHERE event_name = 'page_engagement' AND session_id IS NOT NULL\n GROUP BY session_id, path\n ), dwell_metrics AS (\n SELECT path,\n avg(engaged_ms) FILTER (WHERE engaged_ms IS NOT NULL) AS avg_engaged_ms,\n avg(scroll_depth) FILTER (WHERE scroll_depth IS NOT NULL) AS avg_scroll,\n count(*) FILTER (WHERE engaged_ms IS NOT NULL)::int AS measured_pageviews\n FROM page_dwell GROUP BY path\n ), entrances AS (\n SELECT entrance_path AS path, count(*)::int AS entrances FROM session_bounds GROUP BY entrance_path\n ), exits AS (\n SELECT exit_path AS path, count(*)::int AS exits FROM session_bounds GROUP BY exit_path\n ), bounces AS (\n SELECT b.entrance_path AS path, count(*)::int AS bounces\n FROM session_bounds b JOIN session_pageviews p USING(session_id)\n WHERE p.pageviews <= 1\n GROUP BY b.entrance_path\n ), converted_sessions AS (\n SELECT DISTINCT c.session_id\n FROM analytics_conversions c\n WHERE c.site_id = $1 AND c.occurred_at >= $2::timestamptz AND c.occurred_at < $3::timestamptz\n AND c.session_id IS NOT NULL\n ), path_sessions AS (\n SELECT DISTINCT path, session_id FROM filtered WHERE session_id IS NOT NULL\n ), engaged AS (\n SELECT ps.path,\n count(*) FILTER (\n WHERE sp.pageviews >= 2\n OR COALESCE(pd.engaged_ms, 0) >= ${ENGAGED_SESSION_MS}\n OR cs.session_id IS NOT NULL\n )::int AS engaged_sessions\n FROM path_sessions ps\n JOIN session_pageviews sp USING(session_id)\n LEFT JOIN page_dwell pd ON pd.session_id = ps.session_id AND pd.path = ps.path\n LEFT JOIN converted_sessions cs ON cs.session_id = ps.session_id\n GROUP BY ps.path\n ), assisted AS (\n SELECT f.path, count(DISTINCT c.id)::int AS conversions\n FROM analytics_conversions c JOIN filtered f ON f.session_id = c.session_id\n WHERE c.site_id = $1 AND c.occurred_at >= $2::timestamptz AND c.occurred_at < $3::timestamptz\n GROUP BY f.path\n )\n SELECT metrics.*,\n COALESCE(entrances.entrances, 0)::int AS entrances,\n COALESCE(exits.exits, 0)::int AS exits,\n COALESCE(assisted.conversions, 0)::int AS conversions,\n COALESCE(dwell.measured_pageviews, 0)::int AS measured_pageviews,\n CASE WHEN dwell.avg_engaged_ms IS NULL THEN NULL\n ELSE round((dwell.avg_engaged_ms / 1000.0)::numeric, 1) END AS avg_seconds_on_page,\n CASE WHEN dwell.avg_scroll IS NULL THEN NULL\n ELSE round(dwell.avg_scroll::numeric, 1) END AS avg_scroll_depth,\n CASE WHEN COALESCE(entrances.entrances, 0) = 0 THEN NULL\n ELSE round(COALESCE(bounces.bounces, 0)::numeric * 100 / entrances.entrances, 1) END AS bounce_rate,\n CASE WHEN metrics.pageviews = 0 THEN NULL\n ELSE round(COALESCE(exits.exits, 0)::numeric * 100 / metrics.pageviews, 1) END AS exit_rate,\n CASE WHEN metrics.sessions = 0 THEN NULL\n ELSE round(COALESCE(engaged.engaged_sessions, 0)::numeric * 100 / metrics.sessions, 1) END AS engagement_rate,\n CASE WHEN metrics.sessions = 0 THEN NULL\n ELSE round(metrics.pageviews::numeric / metrics.sessions, 2) END AS pageviews_per_session\n FROM page_metrics metrics\n LEFT JOIN entrances USING(path)\n LEFT JOIN exits USING(path)\n LEFT JOIN assisted USING(path)\n LEFT JOIN bounces USING(path)\n LEFT JOIN engaged USING(path)\n LEFT JOIN dwell_metrics dwell USING(path)\n WHERE ${searchParam}::text IS NULL OR metrics.path ILIKE '%' || ${searchParam} || '%'\n ORDER BY ${order}\n LIMIT 1000`,\n [...query.values, table.search],\n );\n const paged = paginateRows(\n result.rows,\n page,\n pageFingerprint(\n `content:${table.sort}:${table.direction}:${table.search ?? \"\"}`,\n siteId,\n filters,\n ),\n );\n return {\n content: paged.items,\n pageInfo: paged.pageInfo,\n table,\n dataFreshThrough: new Date().toISOString(),\n };\n}\n\nexport async function analyticsEventCounts(\n siteId: string,\n userId: number,\n filters: AnalyticsFilters,\n page?: AnalyticsPageInput,\n) {\n const db = getAnalyticsPool();\n await accessFor(db, siteId, userId);\n const query = eventFilterSql(siteId, filters);\n const [result, samples] = await Promise.all([\n db.query(\n `SELECT e.event_name AS name, count(*)::int AS count, max(e.occurred_at)::text AS last_seen_at\n FROM analytics_events e\n WHERE ${query.where}\n GROUP BY e.event_name ORDER BY count DESC, e.event_name ASC\n LIMIT 1000`,\n query.values,\n ),\n db.query(\n `SELECT e.event_id, e.event_name, e.path, e.hostname,\n COALESCE(e.source, '(direct)') AS source,\n COALESCE(e.campaign, '(none)') AS campaign,\n e.occurred_at::text\n FROM analytics_events e WHERE ${query.where}\n ORDER BY e.occurred_at DESC, e.id DESC LIMIT 25`,\n query.values,\n ),\n ]);\n const receiptValues: unknown[] = [siteId, filters.start, filters.end];\n const receiptClauses = [\n \"r.site_id = $1\",\n \"r.created_at >= $2::timestamptz\",\n \"r.created_at < $3::timestamptz\",\n ];\n const addReceipt = (clause: string, value: unknown) => {\n receiptValues.push(value);\n receiptClauses.push(clause.replace(\"?\", `$${receiptValues.length}`));\n };\n if (filters.pixelIds?.length)\n addReceipt(\"r.pixel_id = ANY(?::uuid[])\", filters.pixelIds);\n if (filters.hostnames?.length)\n addReceipt(\"r.hostname = ANY(?::text[])\", filters.hostnames);\n const rejections = await db.query(\n `SELECT reason.code, count(*)::int AS requests,\n COALESCE(sum(r.rejected_count), 0)::int AS rejected_events,\n max(r.created_at)::text AS last_seen_at\n FROM analytics_ingestion_receipts r\n CROSS JOIN LATERAL jsonb_array_elements_text(r.reason_codes) AS reason(code)\n WHERE ${receiptClauses.join(\" AND \")} AND r.rejected_count > 0\n GROUP BY reason.code ORDER BY rejected_events DESC, reason.code ASC`,\n receiptValues,\n );\n const paged = paginateRows(\n result.rows,\n page,\n pageFingerprint(\"events\", siteId, filters),\n );\n return {\n events: paged.items,\n samples: samples.rows,\n rejections: rejections.rows,\n pageInfo: paged.pageInfo,\n dataFreshThrough: samples.rows[0]?.occurred_at ?? null,\n };\n}\n\nexport async function analyticsConversions(\n siteId: string,\n userId: number,\n filters: AnalyticsFilters,\n page?: AnalyticsPageInput,\n) {\n const db = getAnalyticsPool();\n await accessFor(db, siteId, userId);\n const query = conversionFilterSql(siteId, filters);\n const result = await db.query(\n `SELECT c.id, c.source_event_id, c.pixel_id, c.session_id, c.conversion_kind,\n c.value_minor::int, c.currency, c.occurred_at::text,\n COALESCE(first_event.source, '(direct)') AS source,\n COALESCE(first_event.medium, '(none)') AS medium,\n COALESCE(first_event.campaign, '(none)') AS campaign\n FROM analytics_conversions c\n LEFT JOIN LATERAL (\n SELECT e.source, e.medium, e.campaign\n FROM analytics_events e\n WHERE e.site_id = c.site_id AND e.session_id = c.session_id\n ORDER BY e.occurred_at ASC LIMIT 1\n ) first_event ON true\n WHERE ${query.where}\n ORDER BY c.occurred_at DESC\n LIMIT 1000`,\n query.values,\n );\n const summary = result.rows.reduce(\n (current, row) => ({\n count: current.count + 1,\n valueMinor: current.valueMinor + Number(row.value_minor ?? 0),\n }),\n { count: 0, valueMinor: 0 },\n );\n const paged = paginateRows(\n result.rows,\n page,\n pageFingerprint(\"conversions\", siteId, filters),\n );\n return {\n conversions: paged.items,\n pageInfo: paged.pageInfo,\n summary,\n dataFreshThrough: result.rows[0]?.occurred_at ?? null,\n };\n}\n\nexport async function analyticsPaths(\n siteId: string,\n userId: number,\n filters: AnalyticsFilters,\n page?: AnalyticsPageInput,\n) {\n const db = getAnalyticsPool();\n await accessFor(db, siteId, userId);\n const query = conversionFilterSql(siteId, filters);\n const result = await db.query(\n `SELECT c.session_id, c.person_id,\n string_agg(COALESCE(path_event.platform, path_event.source, '(direct)') || ' / ' || COALESCE(path_event.medium, '(none)') ||\n CASE WHEN path_event.campaign IS NOT NULL THEN ' / ' || path_event.campaign ELSE '' END ||\n CASE WHEN path_event.ad_id IS NOT NULL THEN ' / ad:' || path_event.ad_id ELSE '' END,\n ' → ' ORDER BY path_event.occurred_at) AS path,\n count(path_event.id)::int AS touchpoints,\n 1::int AS conversions,\n c.value_minor::int AS value_minor\n FROM analytics_conversions c\n LEFT JOIN LATERAL (\n SELECT e.source, e.medium, e.campaign FROM analytics_events e\n WHERE e.site_id = c.site_id AND e.session_id = c.session_id\n ORDER BY e.occurred_at ASC LIMIT 1\n ) first_event ON true\n JOIN analytics_events path_event ON path_event.site_id = c.site_id AND\n ((c.person_id IS NOT NULL AND path_event.person_id = c.person_id) OR (c.person_id IS NULL AND path_event.session_id = c.session_id))\n WHERE ${query.where}\n GROUP BY c.id\n ORDER BY conversions DESC, value_minor DESC\n LIMIT 1000`,\n query.values,\n );\n const paged = paginateRows(\n result.rows,\n page,\n pageFingerprint(\"paths\", siteId, filters),\n );\n return {\n paths: paged.items,\n pageInfo: paged.pageInfo,\n dataFreshThrough: new Date().toISOString(),\n };\n}\n\nexport type AnalyticsDimension =\n \"device\" | \"source\" | \"country\" | \"region\" | \"weekday_hour\";\n\nexport async function analyticsDimensions(\n siteId: string,\n userId: number,\n filters: AnalyticsFilters,\n dimension: AnalyticsDimension,\n page?: AnalyticsPageInput,\n) {\n const db = getAnalyticsPool();\n await accessFor(db, siteId, userId);\n const query = eventFilterSql(siteId, filters);\n const select =\n dimension === \"device\"\n ? `COALESCE(NULLIF(e.device_class, ''), 'unknown') AS key, COALESCE(NULLIF(e.device_class, ''), 'Unknown') AS label`\n : dimension === \"source\"\n ? `COALESCE(NULLIF(e.source, ''), '(direct)') AS key, COALESCE(NULLIF(e.source, ''), '(direct)') AS label`\n : dimension === \"country\"\n ? `COALESCE(NULLIF(e.country_code, ''), 'unknown') AS key, COALESCE(NULLIF(e.country_code, ''), 'Unknown') AS label`\n : dimension === \"region\"\n ? `COALESCE(NULLIF(e.region_code, ''), 'unknown') AS key, COALESCE(NULLIF(e.region_code, ''), 'Unknown') AS label`\n : `extract(dow from e.occurred_at)::int::text || '-' || extract(hour from e.occurred_at)::int::text AS key,\n to_char(e.occurred_at, 'Dy') || ' ' || lpad(extract(hour from e.occurred_at)::int::text, 2, '0') || ':00' AS label`;\n const result = await db.query<{\n key: string;\n label: string;\n visitors: number;\n sessions: number;\n pageviews: number;\n events: number;\n value: number;\n }>(\n `SELECT ${select},\n count(DISTINCT e.visitor_id)::int AS visitors,\n count(DISTINCT e.session_id)::int AS sessions,\n count(*) FILTER (WHERE e.event_name = 'page_view')::int AS pageviews,\n count(*)::int AS events,\n count(*)::int AS value\n FROM analytics_events e\n WHERE ${query.where}\n GROUP BY 1, 2\n ORDER BY value DESC, key ASC\n LIMIT 1000`,\n query.values,\n );\n const paged = paginateRows(\n result.rows,\n page,\n pageFingerprint(`dimension:${dimension}`, siteId, filters),\n );\n const chartRecommendation =\n dimension === \"device\"\n ? \"donut\"\n : dimension === \"weekday_hour\"\n ? \"heatmap\"\n : dimension === \"country\" || dimension === \"region\"\n ? \"choropleth_or_bar\"\n : \"horizontal_bar\";\n return {\n dimension,\n unit: \"events\",\n chartRecommendation,\n rows: paged.items,\n pageInfo: paged.pageInfo,\n dataFreshThrough: new Date().toISOString(),\n };\n}\n\nexport interface AnalyticsCampaignLinkRow {\n id: string;\n site_id: string;\n pixel_id: string | null;\n name: string;\n destination_url: string;\n short_code: string;\n utm_source: string;\n utm_medium: string;\n utm_campaign: string;\n utm_term: string | null;\n utm_content: string | null;\n ad_group: string | null;\n ad_name: string | null;\n creative_id: string | null;\n status: \"active\" | \"archived\";\n created_at: string;\n click_count: number;\n tagged_url: string;\n}\n\nfunction buildTaggedCampaignUrl(\n row: Pick<\n AnalyticsCampaignLinkRow,\n | \"destination_url\"\n | \"utm_source\"\n | \"utm_medium\"\n | \"utm_campaign\"\n | \"utm_term\"\n | \"utm_content\"\n | \"ad_group\"\n | \"ad_name\"\n | \"creative_id\"\n >,\n): string {\n const url = new URL(row.destination_url);\n for (const [key, value] of [\n [\"utm_source\", row.utm_source],\n [\"utm_medium\", row.utm_medium],\n [\"utm_campaign\", row.utm_campaign],\n [\"utm_term\", row.utm_term],\n [\"utm_content\", row.utm_content],\n [\"ad_group\", row.ad_group],\n [\"ad_name\", row.ad_name],\n [\"creative_id\", row.creative_id],\n ] as const)\n if (value) url.searchParams.set(key, value);\n return url.toString();\n}\n\nfunction campaignLinkWithUrl(\n row: Omit<AnalyticsCampaignLinkRow, \"tagged_url\">,\n): AnalyticsCampaignLinkRow {\n return { ...row, tagged_url: buildTaggedCampaignUrl(row) };\n}\n\nexport async function createAnalyticsCampaignLink(input: {\n siteId: string;\n userId: number;\n pixelId?: string;\n name: string;\n destinationUrl: string;\n shortCode?: string;\n source: string;\n medium: string;\n campaign: string;\n term?: string;\n content?: string;\n adGroup?: string;\n adName?: string;\n creativeId?: string;\n}): Promise<AnalyticsCampaignLinkRow> {\n const db = getAnalyticsPool();\n await requireEditor(db, input.siteId, input.userId);\n let destination: URL;\n try {\n destination = new URL(input.destinationUrl);\n } catch {\n throw new AnalyticsRepositoryError(\n \"invalid_destination_url\",\n \"Enter a valid destination URL.\",\n );\n }\n if (![\"http:\", \"https:\"].includes(destination.protocol))\n throw new AnalyticsRepositoryError(\n \"invalid_destination_url\",\n \"Destination URL must use HTTP or HTTPS.\",\n );\n if (input.pixelId) {\n const pixel = await db.query(\n \"SELECT 1 FROM analytics_pixels WHERE id = $1 AND site_id = $2\",\n [input.pixelId, input.siteId],\n );\n if (!pixel.rowCount)\n throw new AnalyticsRepositoryError(\n \"analytics_pixel_not_found\",\n \"Pixel not found.\",\n 404,\n );\n }\n const shortCode = (\n input.shortCode\n ?.trim()\n .toLowerCase()\n .replace(/[^a-z0-9_-]/g, \"-\") ||\n randomBytes(5).toString(\"base64url\").toLowerCase()\n ).slice(0, 48);\n try {\n const result = await db.query<Omit<AnalyticsCampaignLinkRow, \"tagged_url\">>(\n `INSERT INTO analytics_campaign_links(\n id, site_id, pixel_id, name, destination_url, short_code, utm_source, utm_medium,\n utm_campaign, utm_term, utm_content, ad_group, ad_name, creative_id, created_by_user_id\n ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15)\n RETURNING *, 0::int AS click_count`,\n [\n randomUUID(),\n input.siteId,\n input.pixelId ?? null,\n input.name.trim(),\n destination.toString(),\n shortCode,\n input.source.trim(),\n input.medium.trim(),\n input.campaign.trim(),\n input.term?.trim() || null,\n input.content?.trim() || null,\n input.adGroup?.trim() || null,\n input.adName?.trim() || null,\n input.creativeId?.trim() || null,\n input.userId,\n ],\n );\n return campaignLinkWithUrl(result.rows[0]);\n } catch (error) {\n if ((error as { code?: string }).code === \"23505\")\n throw new AnalyticsRepositoryError(\n \"campaign_short_code_taken\",\n \"That short code is already in use.\",\n 409,\n );\n throw error;\n }\n}\n\nexport async function listAnalyticsCampaignLinks(\n siteId: string,\n userId: number,\n page?: AnalyticsPageInput,\n) {\n const db = getAnalyticsPool();\n await accessFor(db, siteId, userId);\n const result = await db.query<Omit<AnalyticsCampaignLinkRow, \"tagged_url\">>(\n `SELECT l.*, count(c.id)::int AS click_count\n FROM analytics_campaign_links l LEFT JOIN analytics_campaign_clicks c ON c.link_id = l.id\n WHERE l.site_id = $1\n GROUP BY l.id ORDER BY l.created_at DESC, l.id DESC LIMIT 1000`,\n [siteId],\n );\n const rows = result.rows.map(campaignLinkWithUrl);\n const filters: AnalyticsFilters = { start: \"all\", end: \"all\" };\n const paged = paginateRows(\n rows,\n page,\n pageFingerprint(\"campaign-links\", siteId, filters),\n );\n return { links: paged.items, pageInfo: paged.pageInfo };\n}\n\nexport async function archiveAnalyticsCampaignLink(input: {\n siteId: string;\n linkId: string;\n userId: number;\n}): Promise<void> {\n const db = getAnalyticsPool();\n await requireEditor(db, input.siteId, input.userId);\n const result = await db.query(\n `UPDATE analytics_campaign_links SET status = 'archived', updated_at = now() WHERE id = $1 AND site_id = $2`,\n [input.linkId, input.siteId],\n );\n if (!result.rowCount)\n throw new AnalyticsRepositoryError(\n \"campaign_link_not_found\",\n \"Campaign link not found.\",\n 404,\n );\n}\n\nexport async function resolveAnalyticsCampaignLink(\n shortCode: string,\n referrer?: string | null,\n): Promise<string | null> {\n const db = getAnalyticsPool();\n const result = await db.query<Omit<AnalyticsCampaignLinkRow, \"tagged_url\">>(\n `SELECT l.*, 0::int AS click_count FROM analytics_campaign_links l WHERE l.short_code = $1 AND l.status = 'active'`,\n [shortCode],\n );\n const row = result.rows[0];\n if (!row) return null;\n await db.query(\n `INSERT INTO analytics_campaign_clicks(id, link_id, referrer) VALUES ($1, $2, $3)`,\n [randomUUID(), row.id, normalizeAnalyticsUrl(referrer || undefined)],\n );\n return buildTaggedCampaignUrl(row);\n}\n\nexport interface AnalyticsFormField {\n id: string;\n type:\n | \"text\"\n | \"email\"\n | \"tel\"\n | \"textarea\"\n | \"select\"\n | \"checkbox\"\n | \"radio\"\n | \"number\";\n label: string;\n required: boolean;\n options?: string[];\n}\n\nexport interface AnalyticsFormRow {\n id: string;\n public_id: string;\n site_id: string;\n pixel_id: string;\n name: string;\n slug: string;\n fields: AnalyticsFormField[];\n brand: {\n primaryColor: string;\n backgroundColor: string;\n textColor: string;\n radius: number;\n };\n submit_label: string;\n success_message: string;\n redirect_url: string | null;\n consent_text: string | null;\n status: \"draft\" | \"published\" | \"archived\";\n version: number;\n created_at: string;\n submission_count: number;\n}\n\nexport async function createAnalyticsForm(input: {\n siteId: string;\n userId: number;\n pixelId: string;\n name: string;\n fields: AnalyticsFormField[];\n brand: AnalyticsFormRow[\"brand\"];\n submitLabel?: string;\n successMessage?: string;\n redirectUrl?: string;\n consentText?: string;\n publish?: boolean;\n}): Promise<AnalyticsFormRow> {\n const db = getAnalyticsPool();\n await requireEditor(db, input.siteId, input.userId);\n const pixel = await db.query(\n \"SELECT 1 FROM analytics_pixels WHERE id = $1 AND site_id = $2\",\n [input.pixelId, input.siteId],\n );\n if (!pixel.rowCount)\n throw new AnalyticsRepositoryError(\n \"analytics_pixel_not_found\",\n \"Pixel not found.\",\n 404,\n );\n const baseSlug = normalizeSlug(input.name);\n const slug = `${baseSlug}-${randomBytes(3).toString(\"hex\")}`;\n const publicId = `form_${randomBytes(18).toString(\"base64url\")}`;\n const result = await db.query<AnalyticsFormRow>(\n `INSERT INTO analytics_forms(id, public_id, site_id, pixel_id, name, slug, fields, brand, submit_label, success_message, redirect_url, consent_text, status, created_by_user_id)\n VALUES ($1,$2,$3,$4,$5,$6,$7::jsonb,$8::jsonb,$9,$10,$11,$12,$13,$14)\n RETURNING *, 0::int AS submission_count`,\n [\n randomUUID(),\n publicId,\n input.siteId,\n input.pixelId,\n input.name.trim(),\n slug,\n JSON.stringify(input.fields),\n JSON.stringify(input.brand),\n input.submitLabel?.trim() || \"Submit\",\n input.successMessage?.trim() || \"Thank you. Your response was received.\",\n input.redirectUrl?.trim() || null,\n input.consentText?.trim() || null,\n input.publish ? \"published\" : \"draft\",\n input.userId,\n ],\n );\n return result.rows[0];\n}\n\nexport async function listAnalyticsForms(\n siteId: string,\n userId: number,\n page?: AnalyticsPageInput,\n) {\n const db = getAnalyticsPool();\n await accessFor(db, siteId, userId);\n const result = await db.query<AnalyticsFormRow>(\n `SELECT f.*, count(s.id)::int AS submission_count FROM analytics_forms f\n LEFT JOIN analytics_form_submissions s ON s.form_id = f.id\n WHERE f.site_id = $1 GROUP BY f.id ORDER BY f.created_at DESC, f.id DESC LIMIT 1000`,\n [siteId],\n );\n const paged = paginateRows(\n result.rows,\n page,\n pageFingerprint(\"forms\", siteId, { start: \"all\", end: \"all\" }),\n );\n return { forms: paged.items, pageInfo: paged.pageInfo };\n}\n\nexport async function getPublicAnalyticsForm(\n publicId: string,\n): Promise<\n (AnalyticsFormRow & { owner_user_id: number; pixel_public_id: string }) | null\n> {\n const result = await getAnalyticsPool().query<\n AnalyticsFormRow & { owner_user_id: number; pixel_public_id: string }\n >(\n `SELECT f.*, s.owner_user_id, p.public_id AS pixel_public_id,\n (SELECT count(*)::int FROM analytics_form_submissions fs WHERE fs.form_id = f.id) AS submission_count\n FROM analytics_forms f JOIN analytics_sites s ON s.id = f.site_id JOIN analytics_pixels p ON p.id = f.pixel_id\n WHERE f.public_id = $1 AND f.status = 'published'`,\n [publicId],\n );\n return result.rows[0] ?? null;\n}\n\nexport async function recordAnalyticsFormSubmission(input: {\n form: AnalyticsFormRow;\n visitorId?: string;\n sessionId?: string;\n crmPersonRef?: string;\n crmDeliveryStatus?: \"pending\" | \"delivered\" | \"failed\";\n source?: string;\n medium?: string;\n campaign?: string;\n clickIds?: Record<string, string>;\n}): Promise<{ submissionId: string }> {\n const id = randomUUID();\n await getAnalyticsPool().query(\n `INSERT INTO analytics_form_submissions(id, form_id, site_id, pixel_id, visitor_id, session_id, crm_person_ref, source, medium, campaign, crm_delivery_status, click_ids)\n VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12::jsonb)`,\n [\n id,\n input.form.id,\n input.form.site_id,\n input.form.pixel_id,\n input.visitorId ?? null,\n input.sessionId ?? null,\n input.crmPersonRef ?? null,\n input.source?.slice(0, 180) ?? null,\n input.medium?.slice(0, 180) ?? null,\n input.campaign?.slice(0, 240) ?? null,\n input.crmDeliveryStatus ?? \"pending\",\n JSON.stringify(sanitizeClickIds(input.clickIds)),\n ],\n );\n return { submissionId: id };\n}\n\nconst clickIdKeys = new Set([\n \"fbclid\",\n \"gclid\",\n \"gbraid\",\n \"wbraid\",\n \"ttclid\",\n \"rdt_cid\",\n \"msclkid\",\n]);\n\nexport function sanitizeClickIds(\n value: Record<string, string> | undefined,\n): Record<string, string> {\n const output: Record<string, string> = {};\n for (const [key, raw] of Object.entries(value ?? {})) {\n if (!clickIdKeys.has(key) || typeof raw !== \"string\") continue;\n const normalized = raw.trim();\n if (normalized) output[key] = normalized.slice(0, 500);\n }\n return output;\n}\n\nfunction identityHmac(kind: string, value: string): string {\n return createHmac(\"sha256\", getSessionSecret())\n .update(`${kind}:${value.trim().toLowerCase()}`)\n .digest(\"hex\");\n}\n\nexport async function linkAnalyticsFormIdentity(input: {\n submissionId?: string;\n siteId: string;\n crmPersonRef: string;\n visitorId?: string;\n sessionId?: string;\n email?: string;\n phone?: string;\n clickIds?: Record<string, string>;\n}): Promise<{ personId: string; linkedSignals: number }> {\n const client = await getAnalyticsPool().connect();\n try {\n await client.query(\"BEGIN\");\n const person = await client.query<{ id: string }>(\n `INSERT INTO analytics_people(id, site_id, crm_person_ref) VALUES ($1,$2,$3)\n ON CONFLICT(site_id, crm_person_ref) DO UPDATE SET last_seen_at = now() RETURNING id`,\n [randomUUID(), input.siteId, input.crmPersonRef],\n );\n const personId = person.rows[0].id;\n const signals: Array<{\n kind: string;\n value: string;\n evidence: string;\n confidence: number;\n }> = [];\n if (input.visitorId)\n signals.push({\n kind: \"visitor_id\",\n value: input.visitorId,\n evidence: \"first_party_form\",\n confidence: 0.98,\n });\n if (input.sessionId)\n signals.push({\n kind: \"session_id\",\n value: input.sessionId,\n evidence: \"first_party_form\",\n confidence: 0.95,\n });\n if (input.email)\n signals.push({\n kind: \"email\",\n value: input.email,\n evidence: \"submitted_email\",\n confidence: 1,\n });\n if (input.phone)\n signals.push({\n kind: \"phone\",\n value: input.phone,\n evidence: \"submitted_phone\",\n confidence: 0.99,\n });\n for (const [platform, clickId] of Object.entries(\n sanitizeClickIds(input.clickIds),\n )) {\n signals.push({\n kind: \"click_id\",\n value: `${platform}:${clickId}`,\n evidence: platform,\n confidence: 0.9,\n });\n }\n for (const signal of signals) {\n const node = await client.query<{ id: string }>(\n `INSERT INTO analytics_identity_nodes(id, site_id, kind, value_hmac) VALUES ($1,$2,$3,$4)\n ON CONFLICT(site_id, kind, value_hmac) DO UPDATE SET last_seen_at = now() RETURNING id`,\n [\n randomUUID(),\n input.siteId,\n signal.kind,\n identityHmac(signal.kind, signal.value),\n ],\n );\n await client.query(\n `INSERT INTO analytics_identity_edges(id, site_id, person_id, identity_node_id, evidence_kind, confidence)\n VALUES ($1,$2,$3,$4,$5,$6)\n ON CONFLICT(person_id, identity_node_id, evidence_kind) DO UPDATE SET last_seen_at = now(), confidence = greatest(analytics_identity_edges.confidence, EXCLUDED.confidence)`,\n [\n randomUUID(),\n input.siteId,\n personId,\n node.rows[0].id,\n signal.evidence,\n signal.confidence,\n ],\n );\n }\n if (input.submissionId)\n await client.query(\n \"UPDATE analytics_form_submissions SET person_id = $1 WHERE id = $2 AND site_id = $3\",\n [personId, input.submissionId, input.siteId],\n );\n if (input.visitorId || input.sessionId) {\n await client.query(\n `UPDATE analytics_events SET person_id = $1 WHERE site_id = $2 AND person_id IS NULL\n AND (($3::text IS NOT NULL AND visitor_id = $3) OR ($4::text IS NOT NULL AND session_id = $4))`,\n [\n personId,\n input.siteId,\n input.visitorId ?? null,\n input.sessionId ?? null,\n ],\n );\n await client.query(\n `UPDATE analytics_conversions SET person_id = $1 WHERE site_id = $2 AND person_id IS NULL AND $3::text IS NOT NULL AND session_id = $3`,\n [personId, input.siteId, input.sessionId ?? null],\n );\n }\n await client.query(\"COMMIT\");\n return { personId, linkedSignals: signals.length };\n } catch (error) {\n await client.query(\"ROLLBACK\");\n throw error;\n } finally {\n client.release();\n }\n}\n\nexport async function listAnalyticsPeople(\n siteId: string,\n userId: number,\n page?: AnalyticsPageInput,\n) {\n const db = getAnalyticsPool();\n await accessFor(db, siteId, userId);\n const result = await db.query(\n `SELECT p.id, p.first_seen_at, p.last_seen_at, true AS crm_linked,\n (SELECT count(*)::int FROM analytics_identity_edges e WHERE e.person_id = p.id) AS signal_count,\n (SELECT count(*)::int FROM analytics_form_submissions f WHERE f.person_id = p.id) AS form_submission_count,\n (SELECT count(*)::int FROM analytics_conversions c WHERE c.person_id = p.id) AS conversion_count,\n (SELECT COALESCE(sum(c.value_minor), 0)::bigint FROM analytics_conversions c WHERE c.person_id = p.id) AS revenue_minor\n FROM analytics_people p WHERE p.site_id = $1 ORDER BY p.last_seen_at DESC, p.id DESC LIMIT 1000`,\n [siteId],\n );\n const paged = paginateRows(\n result.rows,\n page,\n pageFingerprint(\"people\", siteId, { start: \"all\", end: \"all\" }),\n );\n return { people: paged.items, pageInfo: paged.pageInfo };\n}\n\nexport async function getAnalyticsPersonJourney(\n siteId: string,\n personId: string,\n userId: number,\n page?: AnalyticsPageInput,\n) {\n const db = getAnalyticsPool();\n await accessFor(db, siteId, userId);\n const exists = await db.query(\n \"SELECT 1 FROM analytics_people WHERE id = $1 AND site_id = $2\",\n [personId, siteId],\n );\n if (!exists.rowCount)\n throw new AnalyticsRepositoryError(\n \"analytics_person_not_found\",\n \"Attributed person not found.\",\n 404,\n );\n const result = await db.query(\n `SELECT * FROM (\n SELECT e.occurred_at, 'event'::text AS kind, e.event_name AS label, e.source, e.medium, e.campaign, e.path, 0::bigint AS value_minor, NULL::text AS currency\n FROM analytics_events e WHERE e.site_id = $1 AND e.person_id = $2\n UNION ALL\n SELECT f.occurred_at, 'form_submission', af.name, f.source, f.medium, f.campaign, NULL, 0, NULL\n FROM analytics_form_submissions f JOIN analytics_forms af ON af.id = f.form_id WHERE f.site_id = $1 AND f.person_id = $2\n UNION ALL\n SELECT c.occurred_at, 'conversion', c.conversion_kind, NULL, NULL, NULL, NULL, c.value_minor, c.currency\n FROM analytics_conversions c WHERE c.site_id = $1 AND c.person_id = $2\n ) journey ORDER BY occurred_at DESC LIMIT 1000`,\n [siteId, personId],\n );\n const paged = paginateRows(\n result.rows,\n page,\n pageFingerprint(\"person_journey\", siteId, {\n start: personId,\n end: personId,\n }),\n );\n return { personId, touchpoints: paged.items, pageInfo: paged.pageInfo };\n}\n\nexport interface AnalyticsCrmImportRow {\n crmPersonRef: string;\n payloadCiphertext: string;\n}\n\nexport async function createAnalyticsCrmImport(input: {\n siteId: string;\n userId: number;\n sourceSystem: string;\n filename: string;\n rows: AnalyticsCrmImportRow[];\n rejectedCount: number;\n}): Promise<{\n id: string;\n status: string;\n acceptedCount: number;\n rejectedCount: number;\n}> {\n const db = getAnalyticsPool();\n await requireEditor(db, input.siteId, input.userId);\n const client = await db.connect();\n const id = randomUUID();\n try {\n await client.query(\"BEGIN\");\n await client.query(\n `INSERT INTO analytics_crm_imports(id, site_id, requested_by_user_id, source_system, filename, accepted_count, rejected_count)\n VALUES ($1,$2,$3,$4,$5,$6,$7)`,\n [\n id,\n input.siteId,\n input.userId,\n input.sourceSystem.slice(0, 80),\n input.filename.slice(0, 240),\n input.rows.length,\n input.rejectedCount,\n ],\n );\n for (const row of input.rows) {\n await client.query(\n `INSERT INTO analytics_crm_import_rows(id, import_id, crm_person_ref, payload_ciphertext)\n VALUES ($1,$2,$3,$4) ON CONFLICT(import_id, crm_person_ref) DO NOTHING`,\n [randomUUID(), id, row.crmPersonRef, row.payloadCiphertext],\n );\n }\n await client.query(\"COMMIT\");\n return {\n id,\n status: \"queued\",\n acceptedCount: input.rows.length,\n rejectedCount: input.rejectedCount,\n };\n } catch (error) {\n await client.query(\"ROLLBACK\");\n throw error;\n } finally {\n client.release();\n }\n}\n\nexport async function listAnalyticsCrmImports(\n siteId: string,\n userId: number,\n page?: AnalyticsPageInput,\n) {\n const db = getAnalyticsPool();\n await accessFor(db, siteId, userId);\n const result = await db.query(\n `SELECT id, source_system, filename, status, accepted_count, rejected_count, created_at, completed_at\n FROM analytics_crm_imports WHERE site_id = $1 ORDER BY created_at DESC, id DESC LIMIT 1000`,\n [siteId],\n );\n const paged = paginateRows(\n result.rows,\n page,\n pageFingerprint(\"crm_imports\", siteId, { start: \"all\", end: \"all\" }),\n );\n return { imports: paged.items, pageInfo: paged.pageInfo };\n}\n\nexport async function createAnalyticsActivationDestination(input: {\n siteId: string;\n userId: number;\n platform: \"meta\" | \"google\" | \"tiktok\" | \"reddit\";\n name: string;\n connectionRef?: string;\n externalDatasetId?: string;\n eventMapping?: Record<string, string>;\n}) {\n const db = getAnalyticsPool();\n await requireEditor(db, input.siteId, input.userId);\n const result = await db.query(\n `INSERT INTO analytics_activation_destinations(id, site_id, platform, name, connection_ref, external_dataset_id, event_mapping, created_by_user_id)\n VALUES ($1,$2,$3,$4,$5,$6,$7::jsonb,$8) RETURNING *`,\n [\n randomUUID(),\n input.siteId,\n input.platform,\n input.name.trim(),\n input.connectionRef?.trim() || null,\n input.externalDatasetId?.trim() || null,\n JSON.stringify(input.eventMapping ?? {}),\n input.userId,\n ],\n );\n return result.rows[0];\n}\n\nexport async function listAnalyticsActivationDestinations(\n siteId: string,\n userId: number,\n) {\n const db = getAnalyticsPool();\n await accessFor(db, siteId, userId);\n const result = await db.query(\n `SELECT d.*, count(j.id)::int AS delivery_count,\n count(j.id) FILTER (WHERE j.status = 'pending')::int AS pending_count,\n count(j.id) FILTER (WHERE j.status = 'delivered')::int AS delivered_count,\n count(j.id) FILTER (WHERE j.status = 'failed')::int AS failed_count\n FROM analytics_activation_destinations d LEFT JOIN analytics_activation_jobs j ON j.destination_id = d.id\n WHERE d.site_id = $1 AND d.status <> 'archived' GROUP BY d.id ORDER BY d.created_at DESC`,\n [siteId],\n );\n return result.rows;\n}\n\nexport async function archiveAnalyticsActivationDestination(input: {\n siteId: string;\n destinationId: string;\n userId: number;\n}) {\n const db = getAnalyticsPool();\n await requireEditor(db, input.siteId, input.userId);\n const result = await db.query(\n `UPDATE analytics_activation_destinations SET status = 'archived', updated_at = now() WHERE id = $1 AND site_id = $2`,\n [input.destinationId, input.siteId],\n );\n if (!result.rowCount)\n throw new AnalyticsRepositoryError(\n \"analytics_activation_destination_not_found\",\n \"Activation destination not found.\",\n 404,\n );\n}\n\nexport async function queueAnalyticsActivation(input: {\n siteId: string;\n conversionId: string;\n personId?: string;\n payloadCiphertext: string;\n}): Promise<number> {\n const db = getAnalyticsPool();\n const destinations = await db.query<{ id: string }>(\n `SELECT id FROM analytics_activation_destinations WHERE site_id = $1 AND status = 'active'`,\n [input.siteId],\n );\n let queued = 0;\n for (const destination of destinations.rows) {\n const result = await db.query(\n `INSERT INTO analytics_activation_jobs(id, destination_id, conversion_id, person_id, payload_ciphertext)\n VALUES ($1,$2,$3,$4,$5) ON CONFLICT(destination_id, conversion_id) DO NOTHING`,\n [\n randomUUID(),\n destination.id,\n input.conversionId,\n input.personId ?? null,\n input.payloadCiphertext,\n ],\n );\n queued += result.rowCount ?? 0;\n }\n return queued;\n}\n\nexport async function queueAnalyticsFormDelivery(input: {\n submissionId: string;\n ownerUserId: number;\n payloadCiphertext: string;\n errorCode?: string;\n}): Promise<string> {\n const id = randomUUID();\n await getAnalyticsPool().query(\n `INSERT INTO analytics_form_delivery_jobs(id, submission_id, owner_user_id, payload_ciphertext, last_error_code)\n VALUES ($1,$2,$3,$4,$5)`,\n [\n id,\n input.submissionId,\n input.ownerUserId,\n input.payloadCiphertext,\n input.errorCode ?? null,\n ],\n );\n return id;\n}\n\nexport interface AnalyticsFormDeliveryJob {\n id: string;\n submissionId: string;\n ownerUserId: number;\n payloadCiphertext: string;\n attempts: number;\n}\n\nexport async function claimAnalyticsFormDeliveryJobs(\n limit = 10,\n): Promise<AnalyticsFormDeliveryJob[]> {\n const result = await getAnalyticsPool().query<{\n id: string;\n submission_id: string;\n owner_user_id: string;\n payload_ciphertext: string;\n attempts: number;\n }>(\n `WITH due AS (\n SELECT id FROM analytics_form_delivery_jobs\n WHERE status = 'pending' AND next_attempt_at <= now() AND expires_at > now()\n ORDER BY next_attempt_at, created_at\n FOR UPDATE SKIP LOCKED LIMIT $1\n )\n UPDATE analytics_form_delivery_jobs j\n SET attempts = j.attempts + 1,\n next_attempt_at = now() + interval '5 minutes'\n FROM due WHERE j.id = due.id\n RETURNING j.id, j.submission_id, j.owner_user_id, j.payload_ciphertext, j.attempts`,\n [Math.min(Math.max(limit, 1), 50)],\n );\n return result.rows.map((row) => ({\n id: row.id,\n submissionId: row.submission_id,\n ownerUserId: Number(row.owner_user_id),\n payloadCiphertext: row.payload_ciphertext,\n attempts: row.attempts,\n }));\n}\n\nexport async function completeAnalyticsFormDelivery(\n jobId: string,\n submissionId: string,\n): Promise<void> {\n const db = getAnalyticsPool();\n const client = await db.connect();\n try {\n await client.query(\"BEGIN\");\n await client.query(\n `UPDATE analytics_form_delivery_jobs SET status='delivered', delivered_at=now(), last_error_code=NULL WHERE id=$1`,\n [jobId],\n );\n await client.query(\n `UPDATE analytics_form_submissions SET crm_delivery_status='delivered' WHERE id=$1`,\n [submissionId],\n );\n await client.query(\"COMMIT\");\n } catch (error) {\n await client.query(\"ROLLBACK\");\n throw error;\n } finally {\n client.release();\n }\n}\n\nexport async function deferAnalyticsFormDelivery(\n jobId: string,\n attempts: number,\n errorCode: string,\n): Promise<void> {\n const terminal = attempts >= 8;\n const delayMinutes = Math.min(5 * 2 ** Math.max(0, attempts - 1), 360);\n await getAnalyticsPool().query(\n `UPDATE analytics_form_delivery_jobs\n SET status = CASE WHEN expires_at <= now() THEN 'expired' WHEN $2 THEN 'failed' ELSE 'pending' END,\n last_error_code = $3,\n next_attempt_at = now() + ($4::text || ' minutes')::interval\n WHERE id = $1`,\n [jobId, terminal, errorCode.slice(0, 80), String(delayMinutes)],\n );\n}\n\nexport async function isAnalyticsFormPlacementApproved(\n pixelId: string,\n placementUrl: string | undefined,\n): Promise<boolean> {\n if (!placementUrl) return false;\n let hostname: string | null = null;\n try {\n hostname = new URL(placementUrl).hostname.toLowerCase();\n } catch {\n return false;\n }\n const result = await getAnalyticsPool().query(\n `SELECT 1 FROM analytics_pixel_domains WHERE pixel_id = $1 AND hostname = $2 AND state = 'approved'`,\n [pixelId, hostname],\n );\n return Boolean(result.rowCount);\n}\n\nexport async function analyticsHealth(siteId: string, userId: number) {\n const db = getAnalyticsPool();\n const pixels = await listAnalyticsPixels(siteId, userId);\n const lastEventAt =\n pixels\n .map((pixel) => pixel.last_event_at)\n .filter(Boolean)\n .sort()\n .at(-1) ?? null;\n const [conversion, rollup, receipts] = await Promise.all([\n db.query<{ last_conversion_at: string | null }>(\n `SELECT max(occurred_at)::text AS last_conversion_at FROM analytics_conversions WHERE site_id = $1`,\n [siteId],\n ),\n db.query<{ last_rollup_at: string | null }>(\n `SELECT max(refreshed_at)::text AS last_rollup_at FROM analytics_rollups_daily WHERE site_id = $1`,\n [siteId],\n ),\n db.query<{ accepted: number; rejected: number }>(\n `SELECT COALESCE(sum(accepted_count), 0)::int AS accepted,\n COALESCE(sum(rejected_count), 0)::int AS rejected\n FROM analytics_ingestion_receipts WHERE site_id = $1 AND created_at >= now() - interval '24 hours'`,\n [siteId],\n ),\n ]);\n const recent = receipts.rows[0];\n const totalRecent = recent.accepted + recent.rejected;\n const rejectionRate = totalRecent ? recent.rejected / totalRecent : 0;\n const stale = Boolean(\n lastEventAt && Date.now() - Date.parse(lastEventAt) > 48 * 60 * 60 * 1000,\n );\n const hasMismatch = pixels.some((pixel) =>\n pixel.domains.some((domain) => domain.state === \"observed_unverified\"),\n );\n return {\n status:\n pixels.length === 0\n ? \"not_installed\"\n : hasMismatch && !lastEventAt\n ? \"domain_mismatch\"\n : rejectionRate > 0.25\n ? \"rejecting\"\n : stale\n ? \"stale\"\n : lastEventAt\n ? \"healthy\"\n : \"waiting\",\n pixels,\n lastEventAt,\n lastConversionAt: conversion.rows[0]?.last_conversion_at ?? null,\n lastRollupAt: rollup.rows[0]?.last_rollup_at ?? null,\n acceptedLast24Hours: recent.accepted,\n rejectedLast24Hours: recent.rejected,\n rejectionRate,\n checkedAt: new Date().toISOString(),\n };\n}\n\nexport async function refreshAnalyticsDailyRollups(input: {\n start: string;\n end: string;\n}): Promise<{ runId: string; rows: number }> {\n const db = getAnalyticsPool();\n const runId = randomUUID();\n await db.query(\n `INSERT INTO analytics_rollup_runs(id, window_start, window_end, status) VALUES ($1, $2, $3, 'running')`,\n [runId, input.start, input.end],\n );\n const client = await db.connect();\n try {\n await client.query(\"BEGIN\");\n await client.query(\n `DELETE FROM analytics_rollups_daily WHERE bucket >= $1::date AND bucket <= $2::date`,\n [input.start, input.end],\n );\n const inserted = await client.query(\n `INSERT INTO analytics_rollups_daily(\n site_id, pixel_id, hostname, bucket, source, medium, campaign,\n pageviews, events, visitors, sessions, refreshed_at\n )\n SELECT site_id, pixel_id, hostname, occurred_at::date,\n COALESCE(source, '(direct)'), COALESCE(medium, '(none)'), COALESCE(campaign, '(none)'),\n count(*) FILTER (WHERE event_name = 'page_view')::int,\n count(*)::int,\n count(DISTINCT visitor_id)::int,\n count(DISTINCT session_id)::int,\n now()\n FROM analytics_events\n WHERE occurred_at >= $1::timestamptz AND occurred_at < $2::timestamptz\n GROUP BY 1, 2, 3, 4, 5, 6, 7\n ON CONFLICT (site_id, pixel_id, hostname, bucket, source, medium, campaign)\n DO UPDATE SET\n pageviews = EXCLUDED.pageviews,\n events = EXCLUDED.events,\n visitors = EXCLUDED.visitors,\n sessions = EXCLUDED.sessions,\n refreshed_at = EXCLUDED.refreshed_at`,\n [input.start, input.end],\n );\n await client.query(\"COMMIT\");\n const rows = inserted.rowCount ?? 0;\n await db.query(\n `UPDATE analytics_rollup_runs SET status = 'complete', row_count = $2, completed_at = now() WHERE id = $1`,\n [runId, rows],\n );\n return { runId, rows };\n } catch (error) {\n await client.query(\"ROLLBACK\");\n const pgCode = (error as { code?: string })?.code;\n const detail = error instanceof Error ? error.message : String(error);\n console.error(JSON.stringify({\n event: \"analytics_rollup_failed\",\n runId,\n pgCode: pgCode ?? null,\n message: detail.slice(0, 400),\n }));\n await db.query(\n `UPDATE analytics_rollup_runs SET status = 'failed', completed_at = now(), error_code = $2 WHERE id = $1`,\n [runId, (pgCode ? `pg_${pgCode}` : \"rollup_failed\").slice(0, 60)],\n );\n throw error;\n } finally {\n client.release();\n }\n}\n\nexport async function refreshAnalyticsDailyRollupsIfDue(\n now = new Date(),\n): Promise<\n | { status: \"complete\"; runId: string; rows: number }\n | { status: \"not_due\" | \"already_running\"; rows: 0 }\n> {\n const db = getAnalyticsPool();\n const lockClient = await db.connect();\n const advisoryLockId = 2_026_080_501;\n let locked = false;\n try {\n const lock = await lockClient.query<{ acquired: boolean }>(\n \"SELECT pg_try_advisory_lock($1) AS acquired\",\n [advisoryLockId],\n );\n locked = Boolean(lock.rows[0]?.acquired);\n if (!locked) return { status: \"already_running\", rows: 0 };\n const recent = await lockClient.query<{ completed_at: string }>(\n `SELECT completed_at FROM analytics_rollup_runs\n WHERE status = 'complete' AND completed_at > $1::timestamptz\n ORDER BY completed_at DESC LIMIT 1`,\n [new Date(now.getTime() - 55 * 60_000).toISOString()],\n );\n if (recent.rows[0]) return { status: \"not_due\", rows: 0 };\n const end = new Date(now);\n const start = new Date(end.getTime() - 3 * 24 * 60 * 60 * 1000);\n const result = await refreshAnalyticsDailyRollups({\n start: start.toISOString(),\n end: end.toISOString(),\n });\n return { status: \"complete\", ...result };\n } finally {\n if (locked)\n await lockClient\n .query(\"SELECT pg_advisory_unlock($1)\", [advisoryLockId])\n .catch(() => undefined);\n lockClient.release();\n }\n}\n\nexport type AnalyticsExportReport =\n \"overview\" | \"acquisition\" | \"content\" | \"conversions\";\nexport type AnalyticsExportFormat = \"csv\" | \"json\" | \"markdown\";\ntype AnalyticsExportArtifact = {\n id: string;\n report: AnalyticsExportReport;\n format: AnalyticsExportFormat;\n mediaType: string;\n filename: string;\n content: string;\n};\n\nfunction csvCell(value: unknown): string {\n const text = value == null ? \"\" : String(value);\n return /[\\n\\r,\\\"]/.test(text) ? `\"${text.replaceAll('\"', '\"\"')}\"` : text;\n}\n\nfunction rowsToCsv(rows: Array<Record<string, unknown>>): string {\n const columns = [...new Set(rows.flatMap((row) => Object.keys(row)))];\n if (!columns.length) return \"\";\n return [\n columns.join(\",\"),\n ...rows.map((row) =>\n columns.map((column) => csvCell(row[column])).join(\",\"),\n ),\n ].join(\"\\n\");\n}\n\nfunction markdownHeading(column: string): string {\n return column\n .split(\"_\")\n .map((word, index) => (index ? word : word.charAt(0).toUpperCase() + word.slice(1)))\n .join(\" \");\n}\n\nfunction markdownCell(value: unknown): string {\n if (value === null || value === undefined) return \"—\";\n return String(value).replaceAll(\"|\", \"\\\\|\").replaceAll(\"\\n\", \" \");\n}\n\nfunction rowsToMarkdown(rows: Array<Record<string, unknown>>): string[] {\n const columns = [...new Set(rows.flatMap((row) => Object.keys(row)))];\n if (!columns.length) return [\"No rows matched this window and filter set.\"];\n return [\n `| ${columns.map(markdownHeading).join(\" | \")} |`,\n `| ${columns.map(() => \"---\").join(\" | \")} |`,\n ...rows.map(\n (row) => `| ${columns.map((column) => markdownCell(row[column])).join(\" | \")} |`,\n ),\n ];\n}\n\nexport async function createAnalyticsExport(input: {\n siteId: string;\n userId: number;\n report: AnalyticsExportReport;\n format: AnalyticsExportFormat;\n filters: AnalyticsFilters;\n idempotencyKey?: string;\n}): Promise<AnalyticsExportArtifact> {\n const site = (await listAnalyticsSites(input.userId)).find(\n (candidate) => candidate.id === input.siteId,\n );\n if (!site)\n throw new AnalyticsRepositoryError(\n \"analytics_site_not_found\",\n \"Site not found.\",\n 404,\n );\n const describeArtifact = (\n artifact: Omit<AnalyticsExportArtifact, \"mediaType\" | \"filename\">,\n ): AnalyticsExportArtifact => {\n const extension = artifact.format === \"markdown\" ? \"md\" : artifact.format;\n return {\n ...artifact,\n mediaType:\n artifact.format === \"csv\"\n ? \"text/csv\"\n : artifact.format === \"json\"\n ? \"application/json\"\n : \"text/markdown\",\n filename: `${site.slug}-${artifact.report}.${extension}`,\n };\n };\n if (input.idempotencyKey) {\n const existing = await getAnalyticsPool().query<{\n id: string;\n report: AnalyticsExportReport;\n format: AnalyticsExportFormat;\n content: string;\n }>(\n `SELECT id, report, format, content FROM analytics_exports\n WHERE requested_by_user_id = $1 AND idempotency_key = $2`,\n [input.userId, input.idempotencyKey],\n );\n const artifact = existing.rows[0];\n if (artifact) return describeArtifact(artifact);\n }\n const data =\n input.report === \"overview\"\n ? await analyticsOverview(input.siteId, input.userId, input.filters)\n : input.report === \"acquisition\"\n ? await analyticsAcquisition(input.siteId, input.userId, input.filters)\n : input.report === \"content\"\n ? await analyticsContent(input.siteId, input.userId, input.filters)\n : await analyticsConversions(\n input.siteId,\n input.userId,\n input.filters,\n );\n const rows =\n input.report === \"overview\"\n ? [data as Record<string, unknown>]\n : input.report === \"acquisition\"\n ? (data as { channels: Array<Record<string, unknown>> }).channels\n : input.report === \"content\"\n ? (data as { content: Array<Record<string, unknown>> }).content\n : (data as { conversions: Array<Record<string, unknown>> })\n .conversions;\n let content: string;\n if (input.format === \"json\") {\n content = JSON.stringify(\n {\n version: 1,\n site: { id: site.id, name: site.name },\n report: input.report,\n filters: input.filters,\n data,\n },\n null,\n 2,\n );\n } else if (input.format === \"csv\") {\n content = rowsToCsv(rows);\n } else {\n const overview = await analyticsOverview(\n input.siteId,\n input.userId,\n input.filters,\n );\n content = [\n `# ${site.name} Site Analytics`,\n \"\",\n `Report: ${input.report}`,\n `Window: ${input.filters.start} to ${input.filters.end}`,\n `Fresh through: ${overview.dataFreshThrough ?? \"No accepted events\"}`,\n \"\",\n `- Visitors: ${overview.visitors}`,\n `- Sessions: ${overview.sessions}`,\n `- Pageviews: ${overview.pageviews}`,\n `- Verified conversions: ${overview.conversions}`,\n `- Verified revenue: ${(overview.revenueMinor / 100).toFixed(2)}`,\n \"\",\n ...(input.report === \"overview\"\n ? []\n : [`## ${markdownHeading(input.report)} rows`, \"\", ...rowsToMarkdown(rows), \"\"]),\n `Generated ${new Date().toISOString()} from the governed ${input.report} report contract.`,\n ].join(\"\\n\");\n }\n const id = randomUUID();\n const inserted = await getAnalyticsPool().query<{\n id: string;\n report: AnalyticsExportReport;\n format: AnalyticsExportFormat;\n content: string;\n }>(\n `INSERT INTO analytics_exports(id, site_id, requested_by_user_id, idempotency_key, report, format, filters, content)\n VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb, $8)\n ON CONFLICT (requested_by_user_id, idempotency_key) WHERE idempotency_key IS NOT NULL DO NOTHING\n RETURNING id, report, format, content`,\n [\n id,\n input.siteId,\n input.userId,\n input.idempotencyKey ?? null,\n input.report,\n input.format,\n JSON.stringify(input.filters),\n content,\n ],\n );\n const artifact = inserted.rows[0];\n if (artifact) return describeArtifact(artifact);\n const replay = await getAnalyticsPool().query<{\n id: string;\n report: AnalyticsExportReport;\n format: AnalyticsExportFormat;\n content: string;\n }>(\n `SELECT id, report, format, content FROM analytics_exports\n WHERE requested_by_user_id = $1 AND idempotency_key = $2`,\n [input.userId, input.idempotencyKey],\n );\n if (!replay.rows[0]) {\n throw new AnalyticsRepositoryError(\n \"analytics_export_conflict\",\n \"The export could not be created or replayed.\",\n 409,\n );\n }\n return describeArtifact(replay.rows[0]);\n}\n","import { createHmac, timingSafeEqual } from 'node:crypto'\n\nconst isProduction = () => process.env.NODE_ENV === 'production' || process.env.VERCEL === '1'\n\nexport function getSessionSecret(): string {\n const configured = process.env.SESSION_SECRET?.trim()\n if (configured) return configured\n if (isProduction()) throw new Error('SESSION_SECRET is not set — add it to your Vercel env vars (see src/api/env.ts)')\n return 'dev-secret-change-me'\n}\n\nconst secret = () => getSessionSecret()\n\nfunction safeEqualHex(a: string, b: string): boolean {\n if (a.length !== b.length) return false\n try {\n return timingSafeEqual(Buffer.from(a, 'hex'), Buffer.from(b, 'hex'))\n } catch {\n return false\n }\n}\n\nexport function signSession(userId: number | bigint): string {\n const payload = String(userId)\n const sig = createHmac('sha256', secret()).update(payload).digest('hex')\n return `${payload}.${sig}`\n}\n\nexport function verifySession(token: string): number | null {\n const dot = token.lastIndexOf('.')\n if (dot === -1) return null\n const payload = token.slice(0, dot)\n const sig = token.slice(dot + 1)\n const expected = createHmac('sha256', secret()).update(payload).digest('hex')\n if (!safeEqualHex(sig, expected)) return null\n const id = parseInt(payload)\n return isNaN(id) ? null : id\n}\n\nexport function sessionCookie(token: string): string {\n const secure = isProduction() ? '; Secure' : ''\n return `session=${token}; HttpOnly${secure}; Path=/; SameSite=Strict; Max-Age=${60 * 60 * 24 * 30}`\n}\n\nexport function clearCookie(): string {\n return 'session=; HttpOnly; Path=/; Max-Age=0'\n}\n","export type AnalyticsChannelFamily = 'llm' | 'social' | 'review' | 'search' | 'email' | 'referral' | 'direct' | 'other'\n\nexport interface ResolvedAnalyticsAttribution {\n channelFamily: AnalyticsChannelFamily\n platform: string\n source: string\n medium: string\n campaign: string | null\n campaignId: string | null\n adSetId: string | null\n adId: string | null\n creativeId: string | null\n placement: string | null\n term: string | null\n content: string | null\n}\n\nconst aliases: Array<[RegExp, AnalyticsChannelFamily, string]> = [\n [/perplexity/i, 'llm', 'Perplexity'], [/chatgpt|chat\\.openai|openai/i, 'llm', 'ChatGPT'],\n [/claude|anthropic/i, 'llm', 'Claude'], [/\\bgrok\\b|xai/i, 'llm', 'Grok'],\n [/ai[ _-]?overview|google[ _-]?ai|\\bsge\\b|gemini/i, 'llm', 'Google AI Overview'],\n [/instagram/i, 'social', 'Instagram'], [/facebook|\\bfb\\b|meta/i, 'social', 'Facebook'],\n [/linkedin/i, 'social', 'LinkedIn'], [/tiktok/i, 'social', 'TikTok'],\n [/youtube|youtu\\.be/i, 'social', 'YouTube'], [/twitter|x\\.com/i, 'social', 'X'],\n [/\\bg2\\b|g2\\.com/i, 'review', 'G2'], [/trustpilot/i, 'review', 'Trustpilot'],\n [/\\bbbb\\b|better business bureau/i, 'review', 'BBB'], [/capterra/i, 'review', 'Capterra'], [/yelp/i, 'review', 'Yelp'],\n [/google|bing|duckduckgo|yahoo/i, 'search', 'Search'],\n]\n\nfunction clean(value: unknown, max = 240): string | null {\n return typeof value === 'string' && value.trim() ? value.trim().slice(0, max) : null\n}\nfunction referrerHost(referrer?: string): string {\n try { return referrer ? new URL(referrer).hostname.replace(/^www\\./, '') : '' } catch { return '' }\n}\n\nexport function resolveAnalyticsAttribution(input: {\n source?: string; medium?: string; campaign?: string; referrer?: string\n clickIds?: Record<string, string>; properties?: Record<string, unknown>\n}): ResolvedAnalyticsAttribution {\n const properties = input.properties ?? {}\n const source = clean(input.source, 180) || referrerHost(input.referrer) || '(direct)'\n const medium = clean(input.medium, 180) || (source === '(direct)' ? '(none)' : 'referral')\n const clickIds = input.clickIds ?? {}\n const haystack = `${source} ${medium} ${referrerHost(input.referrer)}`\n let channelFamily: AnalyticsChannelFamily = source === '(direct)' ? 'direct' : /email|newsletter/i.test(medium) ? 'email' : 'referral'\n let platform = source === '(direct)' ? 'Direct' : source\n for (const [pattern, family, label] of aliases) if (pattern.test(haystack)) { channelFamily = family; platform = label; break }\n if (clickIds.fbclid) { channelFamily = 'social'; platform = /instagram/i.test(haystack) ? 'Instagram' : 'Facebook' }\n else if (clickIds.ttclid) { channelFamily = 'social'; platform = 'TikTok' }\n else if (clickIds.rdt_cid) { channelFamily = 'social'; platform = 'Reddit' }\n else if (clickIds.gclid || clickIds.gbraid || clickIds.wbraid) { channelFamily = 'search'; platform = 'Google' }\n else if (clickIds.msclkid) { channelFamily = 'search'; platform = 'Microsoft Ads' }\n return {\n channelFamily, platform, source, medium, campaign: clean(input.campaign),\n campaignId: clean(properties.campaign_id ?? properties.utm_id),\n adSetId: clean(properties.adset_id ?? properties.ad_set_id ?? properties.adgroup_id ?? properties.ad_group_id),\n adId: clean(properties.ad_id), creativeId: clean(properties.creative_id), placement: clean(properties.placement),\n term: clean(properties.utm_term), content: clean(properties.utm_content),\n }\n}\n"],"mappings":"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","names":["createHmac","createHmac","artifact"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/api/site-extract-content-store.ts","../src/api/site-extract-artifacts.ts","../src/api/site-extract-repository.ts"],"sourcesContent":["import { createHash } from 'node:crypto'\nimport { gunzipSync, gzipSync } from 'node:zlib'\nimport type { PageData } from './site-extractor.js'\nimport {\n createSiteExtractContentChunkArtifact,\n readOwnedSiteExtractContentChunk,\n} from './site-extract-artifacts.js'\n\nconst MAX_PAGES_PER_CHUNK = 10\nconst MAX_UNCOMPRESSED_CHUNK_BYTES = 20 * 1024 * 1024\n\nexport interface SiteExtractContentRef {\n artifactId: string\n artifactSha256: string\n pageId: string\n uncompressedBytes: number\n}\n\nexport interface StoredSitePageContent {\n pageId: string\n url: string\n html: string\n mainHtml: string\n markdown: string\n htmlSha256: string\n markdownSha256: string\n}\n\ninterface ContentChunk {\n version: 'site-extract-content.v1'\n pages: StoredSitePageContent[]\n}\n\nfunction sha256(value: string | Buffer): string {\n return createHash('sha256').update(value).digest('hex')\n}\n\nfunction pageContent(page: PageData): StoredSitePageContent | null {\n if (!page.acquiredHtml || page.extractionStatus === 'failed') return null\n const pageId = page.pageId ?? sha256(page.archivedUrl ?? page.url)\n return {\n pageId,\n url: page.archivedUrl ?? page.url,\n html: page.acquiredHtml,\n mainHtml: page.mainHtml ?? '',\n markdown: page.bodyMarkdown,\n htmlSha256: page.htmlSha256 ?? sha256(page.acquiredHtml),\n markdownSha256: page.markdownSha256 ?? sha256(page.bodyMarkdown),\n }\n}\n\nfunction serializedChunk(pages: StoredSitePageContent[]): Buffer {\n return Buffer.from(JSON.stringify({ version: 'site-extract-content.v1', pages } satisfies ContentChunk))\n}\n\nexport async function persistSiteExtractPageContent(input: {\n ownerId: string\n jobId: string\n createdAt: string\n pages: PageData[]\n}): Promise<Map<string, SiteExtractContentRef>> {\n const contentPages = input.pages.flatMap(page => {\n const content = pageContent(page)\n return content ? [content] : []\n })\n const refs = new Map<string, SiteExtractContentRef>()\n let chunk: StoredSitePageContent[] = []\n let chunkNumber = 0\n\n const flush = async () => {\n if (!chunk.length) return\n const uncompressed = serializedChunk(chunk)\n if (uncompressed.length > MAX_UNCOMPRESSED_CHUNK_BYTES) {\n throw new Error(`site export content chunk exceeds ${MAX_UNCOMPRESSED_CHUNK_BYTES} bytes`)\n }\n const identity = sha256(chunk.map(page => page.pageId).join('\\0')).slice(0, 16)\n const pointer = await createSiteExtractContentChunkArtifact({\n ownerId: input.ownerId,\n jobId: input.jobId,\n createdAt: input.createdAt,\n chunkKey: `content-${String(chunkNumber).padStart(4, '0')}-${identity}`,\n content: gzipSync(uncompressed, { level: 6 }),\n })\n for (const page of chunk) {\n refs.set(page.url, {\n artifactId: pointer.artifactId,\n artifactSha256: pointer.sha256,\n pageId: page.pageId,\n uncompressedBytes: uncompressed.length,\n })\n }\n chunkNumber++\n chunk = []\n }\n\n for (const page of contentPages) {\n const candidate = [...chunk, page]\n if (chunk.length > 0 && (candidate.length > MAX_PAGES_PER_CHUNK || serializedChunk(candidate).length > MAX_UNCOMPRESSED_CHUNK_BYTES)) {\n await flush()\n }\n chunk.push(page)\n if (serializedChunk(chunk).length > MAX_UNCOMPRESSED_CHUNK_BYTES) {\n throw new Error(`page ${page.pageId} exceeds the durable content chunk limit`)\n }\n }\n await flush()\n return refs\n}\n\nexport function createSiteExtractContentReader(ownerId: string): (ref: SiteExtractContentRef) => Promise<StoredSitePageContent> {\n const cache = new Map<string, ContentChunk>()\n return async ref => {\n let chunk = cache.get(ref.artifactId)\n if (!chunk) {\n const compressed = await readOwnedSiteExtractContentChunk({ artifactId: ref.artifactId, ownerId })\n if (!compressed) throw new Error('site export content chunk is missing or unauthorized')\n if (sha256(compressed) !== ref.artifactSha256) throw new Error('site export content chunk checksum mismatch')\n const decoded = gunzipSync(compressed)\n if (decoded.length > MAX_UNCOMPRESSED_CHUNK_BYTES) throw new Error('site export content chunk exceeds its read limit')\n chunk = JSON.parse(decoded.toString('utf8')) as ContentChunk\n if (chunk.version !== 'site-extract-content.v1' || !Array.isArray(chunk.pages)) {\n throw new Error('site export content chunk has an unsupported contract')\n }\n cache.set(ref.artifactId, chunk)\n while (cache.size > 2) cache.delete(cache.keys().next().value!)\n }\n const page = chunk.pages.find(candidate => candidate.pageId === ref.pageId)\n if (!page) throw new Error('site export page is missing from its content chunk')\n if (sha256(page.html) !== page.htmlSha256 || sha256(page.markdown) !== page.markdownSha256) {\n throw new Error('site export page checksum mismatch')\n }\n return page\n }\n}\n","import {\n createPrivateArtifact,\n createPrivateArtifactFromStream,\n privateArtifactOwnerId,\n renewPrivateArtifactDownload,\n readPrivateArtifactBuffer,\n type PrivateArtifactPolicy,\n} from './private-artifacts.js'\nimport type { ExtractJobArtifact } from './site-extract-repository.js'\nimport type { Readable } from 'node:stream'\nimport type { PrivateArtifactPointer } from './private-artifacts.js'\n\nexport const SITE_EXTRACT_ARTIFACT_PREFIX = 'site-extracts/'\nexport const SITE_EXTRACT_ARTIFACT_TTL_MS = 7 * 24 * 60 * 60 * 1_000\nexport const SITE_EXTRACT_DOWNLOAD_TTL_MS = 15 * 60 * 1_000\n\nfunction siteExtractArtifactToken(): string | null {\n return process.env.SITE_EXTRACT_ARTIFACT_READ_WRITE_TOKEN?.trim()\n || process.env.PRIVATE_ARTIFACT_READ_WRITE_TOKEN?.trim()\n || process.env.CONNECTED_DATA_READ_WRITE_TOKEN?.trim()\n || process.env.CONNECTED_DATA_BLOB_READ_WRITE_TOKEN?.trim()\n || null\n}\n\nfunction hostedByEnvironment(): boolean {\n return process.env.VERCEL === '1' || process.env.NODE_ENV === 'production'\n}\n\nfunction policy(): PrivateArtifactPolicy {\n return {\n prefix: SITE_EXTRACT_ARTIFACT_PREFIX,\n artifactTtlMs: SITE_EXTRACT_ARTIFACT_TTL_MS,\n downloadTtlMs: SITE_EXTRACT_DOWNLOAD_TTL_MS,\n token: siteExtractArtifactToken(),\n }\n}\n\nexport function siteExtractArtifactOwnerId(artifactId: string): string | null {\n return privateArtifactOwnerId(artifactId, SITE_EXTRACT_ARTIFACT_PREFIX)\n}\n\nexport async function createSiteExtractBundleArtifact(args: {\n ownerId: string\n jobId: string\n createdAt: Date | string | number\n content: Buffer\n}): Promise<ExtractJobArtifact> {\n const pointer = await createPrivateArtifact({\n policy: policy(),\n ownerId: args.ownerId,\n artifactKey: `${args.jobId}.zip`,\n createdAt: args.createdAt,\n filename: `${args.jobId}-site-export.zip`,\n contentType: 'application/zip',\n content: args.content,\n })\n return {\n key: pointer.artifactId,\n url: pointer.downloadUrl ?? '',\n bytes: pointer.bytes,\n contentType: pointer.contentType,\n filename: pointer.filename,\n sha256: pointer.sha256,\n expiresAt: pointer.expiresAt,\n downloadUrlExpiresAt: pointer.downloadUrlExpiresAt,\n kind: 'bundle',\n }\n}\n\nexport async function createSiteExtractBundleArtifactStream(args: {\n ownerId: string\n jobId: string\n createdAt: Date | string | number\n content: Readable\n}): Promise<ExtractJobArtifact> {\n const pointer = await createPrivateArtifactFromStream({\n policy: policy(),\n ownerId: args.ownerId,\n artifactKey: `${args.jobId}.zip`,\n createdAt: args.createdAt,\n filename: `${args.jobId}-site-export.zip`,\n contentType: 'application/zip',\n content: args.content,\n })\n return {\n key: pointer.artifactId,\n url: pointer.downloadUrl ?? '',\n bytes: pointer.bytes,\n contentType: pointer.contentType,\n filename: pointer.filename,\n sha256: pointer.sha256,\n expiresAt: pointer.expiresAt,\n downloadUrlExpiresAt: pointer.downloadUrlExpiresAt,\n kind: 'bundle',\n }\n}\n\nexport async function createSiteExtractImageArtifact(args: {\n ownerId: string\n jobId: string\n imageId: string\n filename: string\n contentType: string\n content: Buffer\n sourceUrl: string\n sourcePage: string\n}): Promise<ExtractJobArtifact> {\n const pointer = await createPrivateArtifact({\n policy: policy(),\n ownerId: args.ownerId,\n scopeSegments: [args.jobId, 'images'],\n artifactKey: `${args.imageId}.bin`,\n createdAt: new Date(),\n filename: args.filename,\n contentType: args.contentType,\n content: args.content,\n })\n return {\n key: pointer.artifactId,\n url: pointer.downloadUrl ?? '',\n bytes: pointer.bytes,\n contentType: pointer.contentType,\n filename: pointer.filename,\n sha256: pointer.sha256,\n expiresAt: pointer.expiresAt,\n downloadUrlExpiresAt: pointer.downloadUrlExpiresAt,\n kind: 'image',\n imageId: args.imageId,\n sourceUrl: args.sourceUrl,\n sourcePage: args.sourcePage,\n }\n}\n\nexport async function createSiteExtractContentChunkArtifact(args: {\n ownerId: string\n jobId: string\n createdAt: Date | string | number\n chunkKey: string\n content: Buffer\n}): Promise<PrivateArtifactPointer> {\n return createPrivateArtifact({\n policy: policy(),\n ownerId: args.ownerId,\n scopeSegments: [args.jobId, 'content'],\n artifactKey: `${args.chunkKey}.json.gz`,\n createdAt: args.createdAt,\n filename: `${args.chunkKey}.json.gz`,\n contentType: 'application/gzip',\n content: args.content,\n })\n}\n\nexport async function renewSiteExtractArtifactDownload(args: {\n artifactId: string\n ownerId: string\n}): Promise<{ downloadUrl: string; downloadUrlExpiresAt: string; expiresAt: string } | null> {\n return renewPrivateArtifactDownload({\n policy: policy(),\n artifactId: args.artifactId,\n ownerId: args.ownerId,\n })\n}\n\nexport async function readSiteExtractArtifactBuffer(artifactId: string): Promise<Buffer | null> {\n return readPrivateArtifactBuffer({\n policy: policy(),\n artifactId,\n maxBytes: 50 * 1024 * 1024,\n })\n}\n\n/**\n * Owner-authorized read entrypoint for site-export artifacts. Keep the ownership\n * check adjacent to the storage read so callers cannot accidentally validate an\n * ID and then dereference a different one.\n */\nexport async function readOwnedSiteExtractArtifactBuffer(args: {\n artifactId: string\n ownerId: string\n}): Promise<Buffer | null> {\n if (siteExtractArtifactOwnerId(args.artifactId) !== args.ownerId) return null\n return readSiteExtractArtifactBuffer(args.artifactId)\n}\n\nexport async function readOwnedSiteExtractContentChunk(args: {\n artifactId: string\n ownerId: string\n}): Promise<Buffer | null> {\n if (siteExtractArtifactOwnerId(args.artifactId) !== args.ownerId) return null\n return readPrivateArtifactBuffer({\n policy: policy(),\n artifactId: args.artifactId,\n maxBytes: 25 * 1024 * 1024,\n })\n}\n\nexport async function readOwnedSiteExtractImageArtifact(args: {\n artifactId: string\n ownerId: string\n}): Promise<Buffer | null> {\n if (siteExtractArtifactOwnerId(args.artifactId) !== args.ownerId) return null\n return readPrivateArtifactBuffer({\n policy: policy(),\n artifactId: args.artifactId,\n maxBytes: 10 * 1024 * 1024,\n })\n}\n\nexport async function cleanupExpiredSiteExtractArtifacts(args: {\n now?: Date\n force?: boolean\n} = {}): Promise<{ deleted: number; store: 'private-vercel-blob' | 'local' | 'none'; skipped?: boolean }> {\n const now = args.now ?? new Date()\n const token = siteExtractArtifactToken()\n if (!token) return { deleted: 0, store: hostedByEnvironment() ? 'none' : 'local' }\n if (!args.force && !(now.getUTCHours() === 3 && now.getUTCMinutes() === 21)) {\n return { deleted: 0, store: 'private-vercel-blob', skipped: true }\n }\n const cutoff = now.getTime() - SITE_EXTRACT_ARTIFACT_TTL_MS\n const { list, del } = await import('@vercel/blob')\n let cursor: string | undefined\n let deleted = 0\n for (let page = 0; page < 20; page += 1) {\n const result = await list({ prefix: SITE_EXTRACT_ARTIFACT_PREFIX, token, limit: 1_000, cursor })\n const expired = result.blobs.filter(blob => new Date(blob.uploadedAt).getTime() <= cutoff)\n if (expired.length > 0) {\n await del(expired.map(blob => blob.pathname), { token })\n deleted += expired.length\n }\n if (!result.hasMore || !result.cursor) break\n cursor = result.cursor\n }\n return { deleted, store: 'private-vercel-blob' }\n}\n","import { CREDIT_LOT_TTL, getDb, settleDebitMcIdempotent } from './db.js'\nimport {\n parsePublicErrorEnvelope,\n sanitizeVendorName,\n serializePublicErrorEnvelope,\n type PublicErrorEnvelope,\n} from '../errors.js'\nimport { LedgerOperation } from './rates.js'\nimport type { PageData } from './site-extractor.js'\nimport { persistSiteExtractPageContent } from './site-extract-content-store.js'\n\nexport type ExtractJobStatus = 'pending' | 'running' | 'complete' | 'partial' | 'failed'\n\nexport interface ExtractJobArtifact {\n key: string\n url: string\n bytes: number\n contentType: string\n kind?: 'bundle' | 'image'\n imageId?: string\n sourceUrl?: string\n sourcePage?: string\n filename?: string\n sha256?: string\n expiresAt?: string\n downloadUrlExpiresAt?: string | null\n}\n\nexport interface ExtractJobRow {\n id: string\n userId: number | null\n idempotencyKey: string | null\n requestFingerprint: string | null\n status: ExtractJobStatus\n startUrl: string\n options: Record<string, unknown>\n totalUrls: number\n doneUrls: number\n attemptedUrls: number\n successfulUrls: number\n failedUrls: number\n remainingUrls: number\n artifacts: ExtractJobArtifact[] | null\n error: string | null\n publicError: PublicErrorEnvelope | null\n billedMc: number | null\n createdAt: string\n updatedAt: string\n}\n\nexport function extractJobLimitInfo(job: Pick<ExtractJobRow, 'options' | 'totalUrls'>): {\n requestedMaxPages: number\n effectiveMaxPages: number\n creditLimited: boolean\n creditTruncated: boolean\n} {\n const effectiveMaxPages = Math.max(1, Number(job.options.effectiveMaxPages ?? job.options.maxPages ?? 1))\n const requestedMaxPages = Math.max(effectiveMaxPages, Number(job.options.requestedMaxPages ?? effectiveMaxPages))\n const creditLimited = requestedMaxPages > effectiveMaxPages\n return {\n requestedMaxPages,\n effectiveMaxPages,\n creditLimited,\n // If discovery exhausted below the funded cap, the smaller hold did not\n // actually truncate this crawl. Hitting the cap is conservatively partial.\n creditTruncated: creditLimited && job.totalUrls >= effectiveMaxPages,\n }\n}\n\nexport function terminalExtractJobStatus(\n progress: Pick<ExtractJobRow, 'successfulUrls' | 'failedUrls' | 'remainingUrls'>,\n creditTruncated = false,\n): Extract<'complete' | 'partial' | 'failed', ExtractJobStatus> {\n if (progress.successfulUrls === 0) return 'failed'\n if (progress.failedUrls > 0 || progress.remainingUrls > 0 || creditTruncated) return 'partial'\n return 'complete'\n}\n\nfunction rowToJob(r: Record<string, unknown>): ExtractJobRow {\n const totalUrls = Number(r.total_urls ?? 0)\n const legacyProgress = r.attempted_urls == null\n const attemptedUrls = Number(legacyProgress ? (r.done_urls ?? 0) : r.attempted_urls)\n const successfulUrls = Number(legacyProgress ? (r.done_urls ?? 0) : (r.successful_urls ?? 0))\n const failedUrls = Number(r.failed_urls ?? Math.max(0, attemptedUrls - successfulUrls))\n return {\n id: String(r.id),\n userId: r.user_id != null ? Number(r.user_id) : null,\n idempotencyKey: r.idempotency_key != null ? String(r.idempotency_key) : null,\n requestFingerprint: r.request_fingerprint != null ? String(r.request_fingerprint) : null,\n status: (r.status != null ? String(r.status) : 'pending') as ExtractJobStatus,\n startUrl: String(r.start_url ?? ''),\n options: r.options ? JSON.parse(String(r.options)) : {},\n totalUrls,\n doneUrls: attemptedUrls,\n attemptedUrls,\n successfulUrls,\n failedUrls,\n remainingUrls: Math.max(0, totalUrls - attemptedUrls),\n artifacts: r.artifacts ? JSON.parse(String(r.artifacts)) : null,\n error: r.error != null ? String(r.error) : null,\n publicError: parsePublicErrorEnvelope(r.public_error_json),\n billedMc: r.billed_mc != null ? Number(r.billed_mc) : null,\n createdAt: String(r.created_at ?? ''),\n updatedAt: String(r.updated_at ?? ''),\n }\n}\n\nexport async function createExtractJob(jobId: string, userId: number, startUrl: string, options: Record<string, unknown>): Promise<void> {\n const db = getDb()\n await db.execute({\n sql: `INSERT INTO site_extract_jobs (id, user_id, status, start_url, options, created_at, updated_at)\n VALUES (?, ?, 'pending', ?, ?, datetime('now'), datetime('now'))`,\n args: [jobId, userId, startUrl, JSON.stringify(options)],\n })\n}\n\nexport async function getExtractJobByIdempotencyKey(userId: number, idempotencyKey: string): Promise<ExtractJobRow | null> {\n const db = getDb()\n const res = await db.execute({\n sql: `SELECT * FROM site_extract_jobs WHERE user_id = ? AND idempotency_key = ? LIMIT 1`,\n args: [userId, idempotencyKey],\n })\n return res.rows[0] ? rowToJob(res.rows[0] as unknown as Record<string, unknown>) : null\n}\n\nexport async function createOrGetExtractJob(input: {\n jobId: string\n userId: number\n startUrl: string\n options: Record<string, unknown>\n idempotencyKey: string\n requestFingerprint: string\n}): Promise<{ job: ExtractJobRow; created: boolean; conflict: boolean }> {\n const db = getDb()\n const inserted = await db.execute({\n sql: `INSERT OR IGNORE INTO site_extract_jobs\n (id, user_id, status, start_url, options, idempotency_key, request_fingerprint, created_at, updated_at)\n VALUES (?, ?, 'pending', ?, ?, ?, ?, datetime('now'), datetime('now'))`,\n args: [\n input.jobId,\n input.userId,\n input.startUrl,\n JSON.stringify(input.options),\n input.idempotencyKey,\n input.requestFingerprint,\n ],\n })\n const job = await getExtractJobByIdempotencyKey(input.userId, input.idempotencyKey)\n if (!job) throw new Error('idempotent extract job was not persisted')\n return {\n job,\n created: inserted.rowsAffected === 1,\n conflict: job.requestFingerprint !== input.requestFingerprint,\n }\n}\n\nexport async function getExtractJob(jobId: string): Promise<ExtractJobRow | null> {\n const db = getDb()\n const res = await db.execute({ sql: `SELECT * FROM site_extract_jobs WHERE id = ?`, args: [jobId] })\n return res.rows[0] ? rowToJob(res.rows[0] as unknown as Record<string, unknown>) : null\n}\n\nexport async function listExtractJobs(userId: number): Promise<ExtractJobRow[]> {\n const db = getDb()\n const res = await db.execute({ sql: `SELECT * FROM site_extract_jobs WHERE user_id = ? ORDER BY created_at DESC LIMIT 50`, args: [userId] })\n return res.rows.map(r => rowToJob(r as unknown as Record<string, unknown>))\n}\n\nexport async function listUnsettledExtractJobs(limit = 25): Promise<ExtractJobRow[]> {\n const db = getDb()\n const res = await db.execute({\n sql: `SELECT * FROM site_extract_jobs\n WHERE billed_mc IS NULL AND status IN ('complete', 'partial', 'failed')\n AND (next_settlement_at IS NULL OR next_settlement_at <= datetime('now'))\n ORDER BY updated_at ASC LIMIT ?`,\n args: [Math.max(1, Math.min(100, Math.round(limit)))],\n })\n return res.rows.map(row => rowToJob(row as unknown as Record<string, unknown>))\n}\n\nexport async function listFundedPendingExtractJobs(limit = 25, staleMinutes = 2): Promise<ExtractJobRow[]> {\n const db = getDb()\n const safeStaleMinutes = Math.max(1, Math.min(60, Math.round(staleMinutes)))\n const res = await db.execute({\n sql: `SELECT j.* FROM site_extract_jobs j\n JOIN billing_debits d\n ON d.idempotency_key = json_extract(j.options, '$.debitKey')\n AND d.user_id = j.user_id\n AND d.status = 'applied'\n WHERE j.status = 'pending'\n AND j.billed_mc IS NULL\n AND (j.next_dispatch_at IS NULL OR j.next_dispatch_at <= datetime('now'))\n AND j.updated_at <= datetime('now', ?)\n ORDER BY j.updated_at ASC LIMIT ?`,\n args: [`-${safeStaleMinutes} minutes`, Math.max(1, Math.min(100, Math.round(limit)))],\n })\n return res.rows.map(row => rowToJob(row as unknown as Record<string, unknown>))\n}\n\nexport async function markExtractJobDispatchAttempt(jobId: string): Promise<ExtractJobRow | null> {\n const db = getDb()\n await db.execute({\n sql: `UPDATE site_extract_jobs\n SET dispatch_attempts = dispatch_attempts + 1,\n updated_at = datetime('now'),\n next_dispatch_at = datetime('now', '+5 minutes'),\n dispatch_error = NULL,\n status = CASE\n WHEN dispatch_attempts + 1 >= 10 OR created_at <= datetime('now', '-6 hours') THEN 'failed'\n ELSE status\n END,\n error = CASE\n WHEN dispatch_attempts + 1 >= 10 OR created_at <= datetime('now', '-6 hours')\n THEN 'Background crawl was acknowledged repeatedly but never started; the hold will be refunded.'\n ELSE error\n END\n WHERE id = ? AND status = 'pending'`,\n args: [jobId],\n })\n return getExtractJob(jobId)\n}\n\nexport async function recordExtractJobDispatchFailure(jobId: string, error: string): Promise<ExtractJobRow | null> {\n const db = getDb()\n const safeError = sanitizeVendorName(error).slice(0, 1000)\n await db.execute({\n sql: `UPDATE site_extract_jobs SET\n dispatch_attempts = dispatch_attempts + 1,\n dispatch_error = ?,\n next_dispatch_at = datetime('now', '+5 minutes'),\n updated_at = datetime('now'),\n status = CASE\n WHEN dispatch_attempts + 1 >= 10 OR created_at <= datetime('now', '-6 hours') THEN 'failed'\n ELSE status\n END,\n error = CASE\n WHEN dispatch_attempts + 1 >= 10 OR created_at <= datetime('now', '-6 hours')\n THEN 'Background crawl could not be dispatched after repeated attempts; the hold will be refunded.'\n ELSE error\n END\n WHERE id = ? AND status = 'pending'`,\n args: [safeError, jobId],\n })\n return getExtractJob(jobId)\n}\n\nexport async function recordExtractSettlementFailure(jobId: string, error: string): Promise<void> {\n const db = getDb()\n await db.execute({\n sql: `UPDATE site_extract_jobs SET\n settlement_attempts = settlement_attempts + 1,\n settlement_error = ?,\n next_settlement_at = datetime('now', '+15 minutes'),\n updated_at = datetime('now')\n WHERE id = ? AND billed_mc IS NULL`,\n args: [sanitizeVendorName(error).slice(0, 1000), jobId],\n })\n}\n\n/** Running jobs update updated_at after every durable crawl batch. A job with\n * no heartbeat for this lease window is no longer executing safely and must\n * become terminal so the credit hold can be settled by reconciliation. */\nexport async function listStaleRunningExtractJobs(limit = 25, staleMinutes = 60): Promise<ExtractJobRow[]> {\n const db = getDb()\n const safeStaleMinutes = Math.max(15, Math.min(24 * 60, Math.round(staleMinutes)))\n const res = await db.execute({\n sql: `SELECT j.* FROM site_extract_jobs j\n LEFT JOIN billing_debits d\n ON d.idempotency_key = json_extract(j.options, '$.debitKey')\n AND d.user_id = j.user_id\n AND d.status = 'applied'\n WHERE j.status = 'running'\n AND j.billed_mc IS NULL\n AND (\n json_extract(j.options, '$.debitKey') IS NULL\n OR d.idempotency_key IS NOT NULL\n )\n AND j.updated_at <= datetime('now', ?)\n ORDER BY j.updated_at ASC LIMIT ?`,\n args: [`-${safeStaleMinutes} minutes`, Math.max(1, Math.min(100, Math.round(limit)))],\n })\n return res.rows.map(row => rowToJob(row as unknown as Record<string, unknown>))\n}\n\nexport async function failStaleRunningExtractJob(jobId: string, staleMinutes = 60): Promise<boolean> {\n const db = getDb()\n const safeStaleMinutes = Math.max(15, Math.min(24 * 60, Math.round(staleMinutes)))\n const result = await db.execute({\n sql: `UPDATE site_extract_jobs\n SET status = 'failed',\n error = 'Background crawl stopped without a heartbeat; completed pages were preserved and settlement is pending.',\n public_error_json = ?,\n updated_at = datetime('now')\n WHERE id = ?\n AND status = 'running'\n AND billed_mc IS NULL\n AND updated_at <= datetime('now', ?)`,\n args: [\n serializePublicErrorEnvelope({\n error_code: 'extraction_failed',\n error_type: 'extraction',\n message: 'The background crawl stopped without a heartbeat. Completed pages were preserved; retry after settlement completes.',\n retryable: true,\n charge_status: 'refund_pending',\n }),\n jobId,\n `-${safeStaleMinutes} minutes`,\n ],\n })\n return result.rowsAffected === 1\n}\n\n/** Claims a failed job before rebuilding its artifact so concurrent support\n * retries cannot assemble and publish two bundles for the same job. */\nexport async function claimFailedExtractJobForRefinalize(jobId: string): Promise<ExtractJobRow | null> {\n const db = getDb()\n const claimed = await db.execute({\n sql: `UPDATE site_extract_jobs\n SET status = 'running', updated_at = datetime('now')\n WHERE id = ? AND status = 'failed'`,\n args: [jobId],\n })\n if (claimed.rowsAffected !== 1) return null\n return getExtractJob(jobId)\n}\n\nexport async function abandonExtractSettlement(jobId: string, error: string): Promise<void> {\n const db = getDb()\n await db.execute({\n sql: `UPDATE site_extract_jobs SET billed_mc = 0, settlement_error = ?, updated_at = datetime('now')\n WHERE id = ? AND billed_mc IS NULL`,\n args: [sanitizeVendorName(error).slice(0, 1000), jobId],\n })\n}\n\nexport async function setExtractJobTotal(jobId: string, totalUrls: number): Promise<void> {\n const db = getDb()\n await db.execute({\n sql: `UPDATE site_extract_jobs\n SET status = 'running', total_urls = MAX(total_urls, ?), updated_at = datetime('now')\n WHERE id = ? AND status IN ('pending', 'running')`,\n args: [totalUrls, jobId],\n })\n}\n\nexport async function saveExtractPages(jobId: string, pages: PageData[]): Promise<void> {\n if (pages.length === 0) return\n const db = getDb()\n const job = await getExtractJob(jobId)\n const contentRefs = job?.userId != null\n ? await persistSiteExtractPageContent({\n ownerId: String(job.userId),\n jobId,\n createdAt: job.createdAt,\n pages,\n })\n : new Map()\n await db.batch([\n ...pages.map(p => {\n const {\n discoveryLinks: _ephemeralDiscoveryLinks,\n acquiredHtml: _durableHtml,\n mainHtml: _durableMainHtml,\n ...storedPage\n } = p\n storedPage.contentRef = contentRefs.get(p.archivedUrl ?? p.url) ?? p.contentRef\n return {\n sql: `INSERT INTO site_extract_pages (job_id, url, page)\n SELECT ?, ?, ?\n WHERE EXISTS (\n SELECT 1 FROM site_extract_jobs\n WHERE id = ? AND status IN ('pending', 'running')\n )\n ON CONFLICT(job_id, url) DO UPDATE SET page = excluded.page\n WHERE NOT CASE\n WHEN json_valid(site_extract_pages.page) = 0 THEN 0\n WHEN json_extract(site_extract_pages.page, '$.extractionStatus') IS NOT NULL\n THEN json_extract(site_extract_pages.page, '$.extractionStatus') = 'successful'\n ELSE COALESCE(\n json_extract(site_extract_pages.page, '$.status') >= 200\n AND json_extract(site_extract_pages.page, '$.status') < 300\n AND json_extract(site_extract_pages.page, '$.wordCount') > 0,\n 0\n )\n END\n OR CASE\n WHEN json_valid(excluded.page) = 0 THEN 0\n WHEN json_extract(excluded.page, '$.extractionStatus') IS NOT NULL\n THEN json_extract(excluded.page, '$.extractionStatus') = 'successful'\n ELSE COALESCE(\n json_extract(excluded.page, '$.status') >= 200\n AND json_extract(excluded.page, '$.status') < 300\n AND json_extract(excluded.page, '$.wordCount') > 0,\n 0\n )\n END`,\n args: [jobId, p.archivedUrl ?? p.url, JSON.stringify(storedPage), jobId],\n }\n }),\n {\n sql: `UPDATE site_extract_jobs SET\n done_urls = (SELECT COUNT(*) FROM site_extract_pages WHERE job_id = ?),\n attempted_urls = (SELECT COUNT(*) FROM site_extract_pages WHERE job_id = ?),\n successful_urls = (\n SELECT COUNT(*) FROM site_extract_pages\n WHERE job_id = ? AND CASE\n WHEN json_extract(page, '$.extractionStatus') IS NOT NULL\n THEN json_extract(page, '$.extractionStatus') = 'successful'\n ELSE json_extract(page, '$.status') >= 200\n AND json_extract(page, '$.status') < 300\n AND json_extract(page, '$.wordCount') > 0\n END\n ),\n failed_urls = (\n SELECT COUNT(*) FROM site_extract_pages\n WHERE job_id = ? AND NOT CASE\n WHEN json_extract(page, '$.extractionStatus') IS NOT NULL\n THEN json_extract(page, '$.extractionStatus') = 'successful'\n ELSE COALESCE(\n json_extract(page, '$.status') >= 200\n AND json_extract(page, '$.status') < 300\n AND json_extract(page, '$.wordCount') > 0,\n 0\n )\n END\n ),\n updated_at = datetime('now')\n WHERE id = ? AND status IN ('pending', 'running')`,\n args: [jobId, jobId, jobId, jobId, jobId],\n },\n ], 'write')\n}\n\nexport async function listExtractPages(jobId: string): Promise<PageData[]> {\n const db = getDb()\n const result = await db.execute({\n sql: `SELECT page FROM site_extract_pages WHERE job_id = ? ORDER BY rowid`,\n args: [jobId],\n })\n return result.rows.map(row => JSON.parse(String((row as unknown as Record<string, unknown>).page)) as PageData)\n}\n\nexport async function getExtractedPages(jobId: string): Promise<PageData[]> {\n const db = getDb()\n const res = await db.execute({ sql: `SELECT page FROM site_extract_pages WHERE job_id = ?`, args: [jobId] })\n return res.rows.map(r => JSON.parse(String((r as unknown as Record<string, unknown>).page)) as PageData)\n}\n\n/** Projects only bounded image URLs in SQL so large background jobs do not\n * deserialize every page body merely to run the optional image audit. */\nexport async function getExtractedImageLinks(jobId: string, limit = 181): Promise<string[]> {\n const db = getDb()\n const safeLimit = Math.max(1, Math.min(5_001, Math.round(limit)))\n const res = await db.execute({\n sql: `SELECT DISTINCT CAST(images.value AS TEXT) AS url\n FROM site_extract_pages p,\n json_each(CASE WHEN json_valid(p.page) THEN p.page ELSE '{}' END, '$.imageLinks') images\n WHERE p.job_id = ?\n AND images.type = 'text'\n AND length(CAST(images.value AS TEXT)) BETWEEN 1 AND 4096\n LIMIT ?`,\n args: [jobId, safeLimit],\n })\n return res.rows.map(row => String(row.url))\n}\n\nexport async function countSuccessfulPages(jobId: string): Promise<number> {\n const db = getDb()\n const res = await db.execute({\n sql: `SELECT COUNT(*) AS n FROM site_extract_pages\n WHERE job_id = ? AND CASE\n WHEN json_extract(page, '$.extractionStatus') IS NOT NULL\n THEN json_extract(page, '$.extractionStatus') = 'successful'\n ELSE COALESCE(\n json_extract(page, '$.status') >= 200\n AND json_extract(page, '$.status') < 300\n AND json_extract(page, '$.wordCount') > 0,\n 0\n )\n END`,\n args: [jobId],\n })\n return Number((res.rows[0] as unknown as Record<string, unknown>)?.n ?? 0)\n}\n\nexport async function getExtractedUrls(jobId: string): Promise<Set<string>> {\n const db = getDb()\n const res = await db.execute({ sql: `SELECT url FROM site_extract_pages WHERE job_id = ?`, args: [jobId] })\n return new Set(res.rows.map(r => String((r as unknown as Record<string, unknown>).url)))\n}\n\nexport async function finishExtractJob(\n jobId: string,\n artifacts: ExtractJobRow['artifacts'],\n status: Extract<'complete' | 'partial' | 'failed', ExtractJobStatus>,\n error: string | null = null,\n allowFailedRecovery = false,\n publicError: PublicErrorEnvelope | null = null,\n): Promise<boolean> {\n const db = getDb()\n const result = await db.execute({\n sql: `UPDATE site_extract_jobs\n SET status = ?, artifacts = ?, error = ?, public_error_json = ?, updated_at = datetime('now')\n WHERE id = ? AND (status IN ('pending', 'running') OR (? = 1 AND status = 'failed'))`,\n args: [\n status,\n JSON.stringify(artifacts ?? []),\n error ? sanitizeVendorName(error).slice(0, 2000) : null,\n serializePublicErrorEnvelope(publicError),\n jobId,\n allowFailedRecovery ? 1 : 0,\n ],\n })\n return result.rowsAffected === 1\n}\n\nexport async function completeExtractJob(jobId: string, artifacts: ExtractJobRow['artifacts']): Promise<void> {\n await finishExtractJob(jobId, artifacts, 'complete')\n}\n\nexport async function failExtractJob(jobId: string, error: string, publicError?: PublicErrorEnvelope): Promise<void> {\n const db = getDb()\n await db.execute({\n sql: `UPDATE site_extract_jobs\n SET status = 'failed', error = ?, public_error_json = ?, updated_at = datetime('now')\n WHERE id = ? AND status IN ('pending', 'running')`,\n args: [sanitizeVendorName(error).slice(0, 2000), serializePublicErrorEnvelope(publicError), jobId],\n })\n}\n\nexport async function failUnfundedExtractJob(jobId: string, error: string, publicError?: PublicErrorEnvelope): Promise<void> {\n const db = getDb()\n await db.execute({\n sql: `UPDATE site_extract_jobs\n SET status = 'failed', billed_mc = 0, error = ?, public_error_json = ?, updated_at = datetime('now')\n WHERE id = ? AND status = 'pending' AND billed_mc IS NULL`,\n args: [sanitizeVendorName(error).slice(0, 2000), serializePublicErrorEnvelope(publicError), jobId],\n })\n}\n\nexport async function setExtractJobPublicError(jobId: string, publicError: PublicErrorEnvelope | null): Promise<void> {\n await getDb().execute({\n sql: `UPDATE site_extract_jobs SET public_error_json = ?, updated_at = datetime('now')\n WHERE id = ? AND status IN ('complete', 'partial', 'failed')`,\n args: [serializePublicErrorEnvelope(publicError), jobId],\n })\n}\n\nexport async function settleExtractJob(jobId: string, userId: number, refundMc: number, netChargeMc: number, reference: string): Promise<void> {\n const db = getDb()\n const job = await getExtractJob(jobId)\n if (!job || job.billedMc != null) return\n const debitKey = typeof job.options.debitKey === 'string' ? job.options.debitKey : null\n if (debitKey) {\n const settlement = await settleDebitMcIdempotent(\n userId,\n debitKey,\n Math.max(0, netChargeMc),\n LedgerOperation.EXTRACT_SITE_REFUND,\n reference,\n 'extract_site',\n )\n await db.execute({\n sql: `UPDATE site_extract_jobs SET billed_mc = ?, updated_at = datetime('now') WHERE id = ? AND billed_mc IS NULL`,\n args: [settlement.final_amount_mc, jobId],\n })\n return\n }\n\n // Backward compatibility for jobs created before idempotent holds were\n // introduced. Claiming billed_mc=-1 and applying the refund happen in one\n // transaction, so two finalizers cannot both credit the wallet.\n const safeRefundMc = Math.max(0, refundMc)\n const safeNetChargeMc = Math.max(0, netChargeMc)\n await db.batch([\n {\n sql: `UPDATE site_extract_jobs SET billed_mc = -1 WHERE id = ? AND billed_mc IS NULL`,\n args: [jobId],\n },\n {\n sql: `INSERT INTO credit_lots (user_id, amount_mc, remaining_mc, source, expires_at)\n SELECT ?, ?, ?, ?, datetime(j.created_at, ?)\n FROM site_extract_jobs j\n WHERE j.id = ?\n AND j.billed_mc = -1\n AND ? > 0\n AND changes() = 1\n AND datetime(j.created_at, ?) > datetime('now')`,\n args: [\n userId,\n safeRefundMc,\n safeRefundMc,\n LedgerOperation.EXTRACT_SITE_REFUND,\n CREDIT_LOT_TTL,\n jobId,\n safeRefundMc,\n CREDIT_LOT_TTL,\n ],\n },\n {\n sql: `UPDATE users SET balance_mc = balance_mc + ?\n WHERE id = ? AND ? > 0 AND changes() = 1`,\n args: [safeRefundMc, userId, safeRefundMc],\n },\n {\n sql: `INSERT INTO ledger (user_id, amount_mc, operation, description)\n SELECT ?, ?, ?, ? WHERE ? > 0 AND changes() = 1`,\n args: [userId, safeRefundMc, LedgerOperation.EXTRACT_SITE_REFUND, reference, safeRefundMc],\n },\n {\n sql: `UPDATE site_extract_jobs SET billed_mc = ?, updated_at = datetime('now')\n WHERE id = ? AND billed_mc = -1`,\n args: [safeNetChargeMc, jobId],\n },\n ], 'write')\n}\n"],"mappings":"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","names":[]}