mcp-scraper 0.46.0 → 0.48.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +2 -2
  2. package/dist/analytics-repository-WWXVU3VW.js +119 -0
  3. package/dist/bin/api-server.cjs +7745 -1934
  4. package/dist/bin/api-server.cjs.map +1 -1
  5. package/dist/bin/api-server.js +8 -4
  6. package/dist/bin/api-server.js.map +1 -1
  7. package/dist/bin/mcp-scraper-cli.cjs +1 -1
  8. package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
  9. package/dist/bin/mcp-scraper-cli.js +1 -1
  10. package/dist/bin/mcp-scraper-install.cjs +2 -2
  11. package/dist/bin/mcp-scraper-install.cjs.map +1 -1
  12. package/dist/bin/mcp-scraper-install.js +2 -2
  13. package/dist/bin/mcp-stdio-server.cjs +1950 -1050
  14. package/dist/bin/mcp-stdio-server.cjs.map +1 -1
  15. package/dist/bin/mcp-stdio-server.js +5 -5
  16. package/dist/bin/paa-harvest.cjs.map +1 -1
  17. package/dist/bin/paa-harvest.js +3 -3
  18. package/dist/{chunk-JFOT5JEL.js → chunk-6DPIE262.js} +2 -2
  19. package/dist/chunk-IAXQIHNS.js +7 -0
  20. package/dist/chunk-IAXQIHNS.js.map +1 -0
  21. package/dist/{chunk-RJMOOII7.js → chunk-MFE6RMIA.js} +2 -2
  22. package/dist/{chunk-5X7CJEK3.js → chunk-MFNGUM4L.js} +2 -2
  23. package/dist/chunk-MFNGUM4L.js.map +1 -0
  24. package/dist/{chunk-T5AFM4G5.js → chunk-PODASLGT.js} +2 -2
  25. package/dist/{chunk-BP2IUIAH.js → chunk-PTABZABJ.js} +2260 -1287
  26. package/dist/chunk-PTABZABJ.js.map +1 -0
  27. package/dist/{chunk-QSUGPYGQ.js → chunk-QAH7WMYC.js} +2 -2
  28. package/dist/{chunk-QSUGPYGQ.js.map → chunk-QAH7WMYC.js.map} +1 -1
  29. package/dist/{chunk-WT2UHDSE.js → chunk-R66PJOZW.js} +2 -2
  30. package/dist/chunk-YW2LXLDU.js +2865 -0
  31. package/dist/chunk-YW2LXLDU.js.map +1 -0
  32. package/dist/{chunk-OVD4E4AP.js → chunk-ZEMHL36J.js} +2 -2
  33. package/dist/{db-4ABEYNDM.js → db-A4YUPV4Z.js} +2 -2
  34. package/dist/{extract-bundle-DYMQRQWG.js → extract-bundle-2HONVC5T.js} +3 -3
  35. package/dist/index.cjs.map +1 -1
  36. package/dist/index.js +3 -3
  37. package/dist/{location-data-repository-HDNCU4Z4.js → location-data-repository-YWFJGNIP.js} +3 -3
  38. package/dist/{server-SMEHT3OU.js → server-EVTCL3KB.js} +2297 -329
  39. package/dist/server-EVTCL3KB.js.map +1 -0
  40. package/dist/{site-extract-repository-G723PZIL.js → site-extract-repository-STNOI5OH.js} +3 -3
  41. package/dist/site-extract-repository-STNOI5OH.js.map +1 -0
  42. package/dist/{worker-4LZGRHDA.js → worker-4MHZS3OB.js} +5 -5
  43. package/package.json +4 -2
  44. package/dist/chunk-5X7CJEK3.js.map +0 -1
  45. package/dist/chunk-BP2IUIAH.js.map +0 -1
  46. package/dist/chunk-TQKQK7OV.js +0 -7
  47. package/dist/chunk-TQKQK7OV.js.map +0 -1
  48. package/dist/server-SMEHT3OU.js.map +0 -1
  49. /package/dist/{db-4ABEYNDM.js.map → analytics-repository-WWXVU3VW.js.map} +0 -0
  50. /package/dist/{chunk-JFOT5JEL.js.map → chunk-6DPIE262.js.map} +0 -0
  51. /package/dist/{chunk-RJMOOII7.js.map → chunk-MFE6RMIA.js.map} +0 -0
  52. /package/dist/{chunk-T5AFM4G5.js.map → chunk-PODASLGT.js.map} +0 -0
  53. /package/dist/{chunk-WT2UHDSE.js.map → chunk-R66PJOZW.js.map} +0 -0
  54. /package/dist/{chunk-OVD4E4AP.js.map → chunk-ZEMHL36J.js.map} +0 -0
  55. /package/dist/{location-data-repository-HDNCU4Z4.js.map → db-A4YUPV4Z.js.map} +0 -0
  56. /package/dist/{extract-bundle-DYMQRQWG.js.map → extract-bundle-2HONVC5T.js.map} +0 -0
  57. /package/dist/{site-extract-repository-G723PZIL.js.map → location-data-repository-YWFJGNIP.js.map} +0 -0
  58. /package/dist/{worker-4LZGRHDA.js.map → worker-4MHZS3OB.js.map} +0 -0
@@ -10,14 +10,68 @@ import {
10
10
  importHostedCensusPlacesCsv,
11
11
  importHostedZipGroupsCsv,
12
12
  listActiveHostedLocationDatasets
13
- } from "./chunk-WT2UHDSE.js";
13
+ } from "./chunk-R66PJOZW.js";
14
+ import {
15
+ AnalyticsRepositoryError,
16
+ analyticsAcquisition,
17
+ analyticsBusinessMetrics,
18
+ analyticsChannelBreakdown,
19
+ analyticsContent,
20
+ analyticsConversions,
21
+ analyticsDimensions,
22
+ analyticsEventCounts,
23
+ analyticsForecast,
24
+ analyticsHealth,
25
+ analyticsOverview,
26
+ analyticsPaths,
27
+ analyticsTimeseries,
28
+ archiveAnalyticsActivationDestination,
29
+ archiveAnalyticsCampaignLink,
30
+ claimAnalyticsFormDeliveryJobs,
31
+ completeAnalyticsFormDelivery,
32
+ createAnalyticsActivationDestination,
33
+ createAnalyticsCampaignLink,
34
+ createAnalyticsConversion,
35
+ createAnalyticsCrmImport,
36
+ createAnalyticsExport,
37
+ createAnalyticsForm,
38
+ createAnalyticsPixel,
39
+ createAnalyticsSite,
40
+ deferAnalyticsFormDelivery,
41
+ deleteAnalyticsSite,
42
+ getAnalyticsPersonJourney,
43
+ getAnalyticsPool,
44
+ getPublicAnalyticsForm,
45
+ getSessionSecret,
46
+ ingestAnalyticsEvents,
47
+ isAnalyticsFormPlacementApproved,
48
+ linkAnalyticsFormIdentity,
49
+ listAnalyticsActivationDestinations,
50
+ listAnalyticsCampaignLinks,
51
+ listAnalyticsCrmImports,
52
+ listAnalyticsForms,
53
+ listAnalyticsPeople,
54
+ listAnalyticsPixels,
55
+ listAnalyticsSites,
56
+ queueAnalyticsActivation,
57
+ queueAnalyticsFormDelivery,
58
+ recordAnalyticsFormSubmission,
59
+ refreshAnalyticsDailyRollupsIfDue,
60
+ resolveAnalyticsCampaignLink,
61
+ setAnalyticsPixelDomainState,
62
+ signSession,
63
+ updateAnalyticsBusinessModel,
64
+ updateAnalyticsPixel,
65
+ upsertAnalyticsAdSpend,
66
+ verifySession
67
+ } from "./chunk-YW2LXLDU.js";
14
68
  import {
15
69
  classifyHarvestProblem,
16
70
  createHarvestAttemptRecorder,
17
71
  deliverWebhook,
18
72
  harvestProblemResponse,
19
73
  serializeHarvestProblem
20
- } from "./chunk-OVD4E4AP.js";
74
+ } from "./chunk-ZEMHL36J.js";
21
75
  import {
22
76
  AIModeSelectors,
23
77
  AIOverviewSelectors,
@@ -44,10 +98,10 @@ import {
44
98
  resolveKernelProxyId,
45
99
  serpArrivalUrl,
46
100
  withRuntimeUserAgent
47
- } from "./chunk-RJMOOII7.js";
101
+ } from "./chunk-MFE6RMIA.js";
48
102
  import {
49
103
  ApiErrorResponseSchema,
50
- ArtifactTemplateConfigSchema,
104
+ ArtifactTemplatePresetKeySchema,
51
105
  CommonsClaimPublicationInputSchema,
52
106
  CommonsPreparePublicationInputSchema,
53
107
  CommonsPublishEditorialInputSchema,
@@ -55,15 +109,22 @@ import {
55
109
  CreateArtifactTemplateInputSchema,
56
110
  CreateEditorialReadingRoomInputSchema,
57
111
  CreateScheduledRunViewLinkInputSchema,
112
+ EDITORIAL_READING_ROOM_TEMPLATE_PRESET,
113
+ EditorialArtifactTemplateConfigSchema,
58
114
  EditorialReadingRoomV1InputSchema,
59
115
  GetScheduledRunResponseSchema,
60
116
  HttpMcpToolExecutor,
61
117
  InternalScheduledArtifactRenderRequestSchema,
62
118
  ListScheduledRunsQuerySchema,
63
119
  LocalSourcebookSchemaTypeSchema,
120
+ NEWSROOM_PUBLISHER_TEMPLATE_PRESET,
121
+ NewsroomPublisherTemplateConfigSchema,
122
+ NewsroomPublisherV1InputSchema,
123
+ PERSONAL_AUTHORITY_TEMPLATE_PRESET,
124
+ PersonalAuthorityTemplateConfigSchema,
125
+ PersonalAuthorityV1InputSchema,
64
126
  PublicScheduledRunTokenSchema,
65
127
  RenewEditorialReadingRoomDownloadInputSchema,
66
- SCHEDULED_ARTIFACT_TEMPLATE_PRESET,
67
128
  ScheduledResultsMcpExecutor,
68
129
  UpdateArtifactTemplateInputSchema,
69
130
  WIZPER_LANGUAGES,
@@ -94,7 +155,7 @@ import {
94
155
  resolveDeploymentProfile,
95
156
  resolveLocalSourcebookSchemaType,
96
157
  transcribeMediaUrl
97
- } from "./chunk-BP2IUIAH.js";
158
+ } from "./chunk-PTABZABJ.js";
98
159
  import {
99
160
  auditImageUrls,
100
161
  auditImages,
@@ -122,7 +183,7 @@ import {
122
183
  recordKernelSession,
123
184
  recordVendorUsage,
124
185
  runWithCostContext
125
- } from "./chunk-T5AFM4G5.js";
186
+ } from "./chunk-PODASLGT.js";
126
187
  import {
127
188
  listWorkflowDefinitions,
128
189
  runWorkflow,
@@ -150,7 +211,7 @@ import {
150
211
  } from "./chunk-3ZUBQQPQ.js";
151
212
  import {
152
213
  PACKAGE_VERSION
153
- } from "./chunk-TQKQK7OV.js";
214
+ } from "./chunk-IAXQIHNS.js";
154
215
  import {
155
216
  abandonExtractSettlement,
156
217
  countSuccessfulPages,
@@ -173,7 +234,7 @@ import {
173
234
  setExtractJobTotal,
174
235
  settleExtractJob,
175
236
  terminalExtractJobStatus
176
- } from "./chunk-JFOT5JEL.js";
237
+ } from "./chunk-6DPIE262.js";
177
238
  import {
178
239
  BROWSER_OPEN_MIN_BALANCE_MC,
179
240
  CONCURRENCY_PRICE_ID,
@@ -330,7 +391,7 @@ import {
330
391
  startHarvestAttempt,
331
392
  upsertPageSnapshot,
332
393
  verifyPassword
333
- } from "./chunk-5X7CJEK3.js";
394
+ } from "./chunk-MFNGUM4L.js";
334
395
 
335
396
  // src/blog/registry.ts
336
397
  var posts = [
@@ -2690,38 +2751,38 @@ function renderCard(card) {
2690
2751
  </div>
2691
2752
  </div>`;
2692
2753
  }
2693
- function renderSection(section, tocTitle) {
2694
- const calloutHtml = section.callout ? `
2754
+ function renderSection(section2, tocTitle) {
2755
+ const calloutHtml = section2.callout ? `
2695
2756
  <div class="callout">
2696
- <p class="callout-eyebrow">${section.callout.eyebrow}</p>
2697
- <h3 class="callout-heading">${section.callout.heading}</h3>
2698
- <p class="callout-body">${section.callout.body}</p>
2757
+ <p class="callout-eyebrow">${section2.callout.eyebrow}</p>
2758
+ <h3 class="callout-heading">${section2.callout.heading}</h3>
2759
+ <p class="callout-body">${section2.callout.body}</p>
2699
2760
  </div>` : "";
2700
- const cardsHtml = section.cards.map(renderCard).join("\n ");
2701
- const interactiveHtml = section.interactiveHtml ? `
2702
- <div class="section-interactive">${section.interactiveHtml}</div>` : "";
2703
- return `<section class="post-section" id="${section.id}" aria-label="${tocTitle}">
2761
+ const cardsHtml = section2.cards.map(renderCard).join("\n ");
2762
+ const interactiveHtml = section2.interactiveHtml ? `
2763
+ <div class="section-interactive">${section2.interactiveHtml}</div>` : "";
2764
+ return `<section class="post-section" id="${section2.id}" aria-label="${tocTitle}">
2704
2765
  <div class="section-header">
2705
- <span class="section-num">${section.num}</span>
2706
- <h2 class="section-title">${section.title}${section.titleItalic ? ` <em>${section.titleItalic}</em>` : ""}</h2>
2707
- <p class="section-deck">${section.deck}</p>
2766
+ <span class="section-num">${section2.num}</span>
2767
+ <h2 class="section-title">${section2.title}${section2.titleItalic ? ` <em>${section2.titleItalic}</em>` : ""}</h2>
2768
+ <p class="section-deck">${section2.deck}</p>
2708
2769
  </div>${calloutHtml}
2709
2770
  <div class="card-stack">
2710
2771
  ${cardsHtml}
2711
2772
  </div>${interactiveHtml}
2712
2773
  </section>`;
2713
2774
  }
2714
- function tocItem(section) {
2715
- const label = `${section.title}${section.titleItalic ? " " + section.titleItalic : ""}`;
2716
- const subItems = section.cards.map((card) => `<li>
2775
+ function tocItem(section2) {
2776
+ const label = `${section2.title}${section2.titleItalic ? " " + section2.titleItalic : ""}`;
2777
+ const subItems = section2.cards.map((card) => `<li>
2717
2778
  <a href="#${card.id}" class="toc-sub-link" data-toc="${card.id}">
2718
2779
  <span class="toc-sub-num">${card.num}</span>
2719
2780
  <span class="toc-sub-label">${card.question}</span>
2720
2781
  </a>
2721
2782
  </li>`).join("\n ");
2722
2783
  return `<li>
2723
- <a href="#${section.id}" class="toc-link" data-toc="${section.id}">
2724
- <span class="toc-num">${section.num}</span>
2784
+ <a href="#${section2.id}" class="toc-link" data-toc="${section2.id}">
2785
+ <span class="toc-num">${section2.num}</span>
2725
2786
  <span class="toc-label">${label}</span>
2726
2787
  </a>
2727
2788
  <ul class="toc-sub-list">
@@ -6268,7 +6329,7 @@ async function extractSite(opts) {
6268
6329
  }
6269
6330
 
6270
6331
  // src/api/server.ts
6271
- import { Hono as Hono35 } from "hono";
6332
+ import { Hono as Hono36 } from "hono";
6272
6333
  import { serve as serveInngest } from "inngest/hono";
6273
6334
 
6274
6335
  // src/inngest/client.ts
@@ -8676,7 +8737,7 @@ var siteExtractFn = inngest.createFunction(
8676
8737
  });
8677
8738
  }) : null;
8678
8739
  const finalized = await step.run("finalize", async () => {
8679
- const { assembleExtractArtifacts } = await import("./extract-bundle-DYMQRQWG.js");
8740
+ const { assembleExtractArtifacts } = await import("./extract-bundle-2HONVC5T.js");
8680
8741
  const current = await getExtractJob(jobId2);
8681
8742
  if (!current) throw new Error("extract job disappeared before finalization");
8682
8743
  const stored = await assembleExtractArtifacts(current, { branding, imageAudit });
@@ -9618,7 +9679,7 @@ async function resolveUsZipsPath(requestedPath) {
9618
9679
  async function loadZipGroups(stateAbbr, requestedPath, warnings, sourceMode) {
9619
9680
  const mode = resolvedLocationDataMode({ zipSource: sourceMode });
9620
9681
  if (mode === "hosted") {
9621
- const { getHostedZipGroups } = await import("./location-data-repository-HDNCU4Z4.js");
9682
+ const { getHostedZipGroups } = await import("./location-data-repository-YWFJGNIP.js");
9622
9683
  const hosted = await getHostedZipGroups(stateAbbr);
9623
9684
  if (!hosted.dataset) {
9624
9685
  throw new DirectoryLocationError(
@@ -9681,7 +9742,7 @@ async function resolveDirectoryMarkets(options, config = {}) {
9681
9742
  const sourceUrl = censusStateUrl(state.fips);
9682
9743
  const warnings = [];
9683
9744
  if (resolvedLocationDataMode(config) === "hosted") {
9684
- const { queryHostedLocationMarkets } = await import("./location-data-repository-HDNCU4Z4.js");
9745
+ const { queryHostedLocationMarkets } = await import("./location-data-repository-YWFJGNIP.js");
9685
9746
  const hosted = await queryHostedLocationMarkets({
9686
9747
  state: state.abbr,
9687
9748
  minPopulation: options.minPopulation,
@@ -14247,8 +14308,8 @@ siteAuditApp.get("/jobs", async (c) => {
14247
14308
  import { Hono as Hono2 } from "hono";
14248
14309
  var internalMemoryApp = new Hono2();
14249
14310
  internalMemoryApp.post("/ai-debit", async (c) => {
14250
- const secret2 = c.req.header("x-internal-secret");
14251
- if (!secret2 || secret2 !== process.env.INTERNAL_MEMORY_DEBIT_SECRET) {
14311
+ const secret = c.req.header("x-internal-secret");
14312
+ if (!secret || secret !== process.env.INTERNAL_MEMORY_DEBIT_SECRET) {
14252
14313
  return c.json({ error: "unauthorized" }, 401);
14253
14314
  }
14254
14315
  const body = await c.req.json().catch(() => ({}));
@@ -14339,9 +14400,9 @@ var UnifiedBillingError = class extends Error {
14339
14400
  details;
14340
14401
  };
14341
14402
  function billingSecret() {
14342
- const secret2 = process.env.INTERNAL_BILLING_SECRET?.trim() || process.env.INTERNAL_MEMORY_DEBIT_SECRET?.trim();
14343
- if (!secret2) throw new UnifiedBillingError("unauthorized", "internal billing secret is not configured", 503);
14344
- return secret2;
14403
+ const secret = process.env.INTERNAL_BILLING_SECRET?.trim() || process.env.INTERNAL_MEMORY_DEBIT_SECRET?.trim();
14404
+ if (!secret) throw new UnifiedBillingError("unauthorized", "internal billing secret is not configured", 503);
14405
+ return secret;
14345
14406
  }
14346
14407
  function normalizeIdentity(identity) {
14347
14408
  const normalized = identity.trim().toLowerCase();
@@ -14761,42 +14822,6 @@ async function voidScheduledRunAuthorization(args) {
14761
14822
 
14762
14823
  // src/api/memory.ts
14763
14824
  import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from "crypto";
14764
-
14765
- // src/api/session.ts
14766
- import { createHmac as createHmac2, timingSafeEqual as timingSafeEqual2 } from "crypto";
14767
- var isProduction = () => process.env.NODE_ENV === "production" || process.env.VERCEL === "1";
14768
- function getSessionSecret() {
14769
- const configured = process.env.SESSION_SECRET?.trim();
14770
- if (configured) return configured;
14771
- if (isProduction()) throw new Error("SESSION_SECRET is not set \u2014 add it to your Vercel env vars (see src/api/env.ts)");
14772
- return "dev-secret-change-me";
14773
- }
14774
- var secret = () => getSessionSecret();
14775
- function safeEqualHex(a, b) {
14776
- if (a.length !== b.length) return false;
14777
- try {
14778
- return timingSafeEqual2(Buffer.from(a, "hex"), Buffer.from(b, "hex"));
14779
- } catch {
14780
- return false;
14781
- }
14782
- }
14783
- function signSession(userId) {
14784
- const payload = String(userId);
14785
- const sig = createHmac2("sha256", secret()).update(payload).digest("hex");
14786
- return `${payload}.${sig}`;
14787
- }
14788
- function verifySession(token4) {
14789
- const dot = token4.lastIndexOf(".");
14790
- if (dot === -1) return null;
14791
- const payload = token4.slice(0, dot);
14792
- const sig = token4.slice(dot + 1);
14793
- const expected = createHmac2("sha256", secret()).update(payload).digest("hex");
14794
- if (!safeEqualHex(sig, expected)) return null;
14795
- const id = parseInt(payload);
14796
- return isNaN(id) ? null : id;
14797
- }
14798
-
14799
- // src/api/memory.ts
14800
14825
  import { provisionDefaultsTool } from "mcpscraper-memory-tools/tools/vaults/provision-defaults";
14801
14826
  import { setScheduleEntitlementTool } from "mcpscraper-memory-tools/tools/schedule/set-schedule-entitlement";
14802
14827
  var MEMORY_BASE_URL = () => (process.env.MCP_MEMORY_BASE_URL ?? "https://memory.mcpscraper.dev").replace(/\/$/, "");
@@ -14814,10 +14839,10 @@ function isMemoryOperator(email) {
14814
14839
  function encKey() {
14815
14840
  return scryptSync(getSessionSecret(), "mcp-memory-key-v1", 32);
14816
14841
  }
14817
- function encryptMemoryKey(secret2) {
14842
+ function encryptMemoryKey(secret) {
14818
14843
  const iv = randomBytes(12);
14819
14844
  const cipher = createCipheriv("aes-256-gcm", encKey(), iv);
14820
- const enc = Buffer.concat([cipher.update(secret2, "utf8"), cipher.final()]);
14845
+ const enc = Buffer.concat([cipher.update(secret, "utf8"), cipher.final()]);
14821
14846
  const tag = cipher.getAuthTag();
14822
14847
  return `${iv.toString("base64")}:${tag.toString("base64")}:${enc.toString("base64")}`;
14823
14848
  }
@@ -16071,8 +16096,8 @@ var YouTubeExtractor = class {
16071
16096
  const sections = slr?.contents ?? [];
16072
16097
  const videos = [];
16073
16098
  const channels = [];
16074
- for (const section of sections) {
16075
- const contents = section.itemSectionRenderer?.contents ?? [];
16099
+ for (const section2 of sections) {
16100
+ const contents = section2.itemSectionRenderer?.contents ?? [];
16076
16101
  for (const item of contents) {
16077
16102
  const rec = item;
16078
16103
  if (rec.videoRenderer) {
@@ -16434,15 +16459,15 @@ function mediaResolverConfigured() {
16434
16459
  var RESOLVE_TIMEOUT_MS = 24e4;
16435
16460
  async function resolveYouTubeAudio(videoId) {
16436
16461
  const base = mediaResolverUrl();
16437
- const secret2 = mediaResolverSecret();
16438
- if (!base || !secret2) return null;
16462
+ const secret = mediaResolverSecret();
16463
+ if (!base || !secret) return null;
16439
16464
  const controller = new AbortController();
16440
16465
  const timer = setTimeout(() => controller.abort(), RESOLVE_TIMEOUT_MS);
16441
16466
  const startedMs = Date.now();
16442
16467
  try {
16443
16468
  const resp = await fetch(`${base.replace(/\/+$/, "")}/resolve-audio`, {
16444
16469
  method: "POST",
16445
- headers: { "content-type": "application/json", "x-frames-secret": secret2 },
16470
+ headers: { "content-type": "application/json", "x-frames-secret": secret },
16446
16471
  body: JSON.stringify({ videoUrl: `https://www.youtube.com/watch?v=${videoId}` }),
16447
16472
  signal: controller.signal
16448
16473
  });
@@ -22224,7 +22249,7 @@ async function reconcileDirectoryWorkflowSettlements(limit = 25) {
22224
22249
  }
22225
22250
 
22226
22251
  // src/api/workflow-routes.ts
22227
- import { createHmac as createHmac3 } from "crypto";
22252
+ import { createHmac as createHmac2 } from "crypto";
22228
22253
  import { readFile as readFile2 } from "fs/promises";
22229
22254
  import { Hono as Hono19 } from "hono";
22230
22255
  import { z as z28 } from "zod";
@@ -22310,9 +22335,9 @@ async function readManifestFromSummary(summary) {
22310
22335
  }
22311
22336
  }
22312
22337
  function webhookSignature(body, timestamp) {
22313
- const secret2 = process.env.MCP_SCRAPER_WEBHOOK_SECRET?.trim();
22314
- if (!secret2) return null;
22315
- return createHmac3("sha256", secret2).update(`${timestamp}.${body}`).digest("hex");
22338
+ const secret = process.env.MCP_SCRAPER_WEBHOOK_SECRET?.trim();
22339
+ if (!secret) return null;
22340
+ return createHmac2("sha256", secret).update(`${timestamp}.${body}`).digest("hex");
22316
22341
  }
22317
22342
  async function deliverWorkflowWebhook(input) {
22318
22343
  if (!input.webhookUrl) return;
@@ -22750,8 +22775,8 @@ async function dispatchDueWorkflowSchedules(apiUrl, limit = 3) {
22750
22775
  return { dispatched: results.length, results };
22751
22776
  }
22752
22777
  workflowApp.post("/cron/dispatch", async (c) => {
22753
- const secret2 = c.req.header("authorization");
22754
- if (!process.env.CRON_SECRET || secret2 !== `Bearer ${process.env.CRON_SECRET}`) {
22778
+ const secret = c.req.header("authorization");
22779
+ if (!process.env.CRON_SECRET || secret !== `Bearer ${process.env.CRON_SECRET}`) {
22755
22780
  return c.json({ error: "Unauthorized" }, 401);
22756
22781
  }
22757
22782
  return c.json(await dispatchDueWorkflowSchedules(originFromUrl(c.req.url)));
@@ -27478,10 +27503,10 @@ oauthApp.post("/oauth/authorize", async (c) => {
27478
27503
  return typeof v === "string" ? v : void 0;
27479
27504
  };
27480
27505
  const p = parseAuthParams(get);
27481
- const action = get("action") ?? "";
27506
+ const action2 = get("action") ?? "";
27482
27507
  const valid = await validateAuthRequest(p);
27483
27508
  if (!valid.ok) return c.text(valid.message, valid.status);
27484
- if (action === "login") {
27509
+ if (action2 === "login") {
27485
27510
  const email = (get("email") ?? "").trim().toLowerCase();
27486
27511
  const password = get("password") ?? "";
27487
27512
  const user2 = email ? await withOAuthDatabaseDeadline("login-user", getUserByEmail(email)) : void 0;
@@ -27495,8 +27520,8 @@ oauthApp.post("/oauth/authorize", async (c) => {
27495
27520
  }
27496
27521
  const user = await sessionUser(getCookie(c, "session"));
27497
27522
  if (!user) return c.html(renderLogin(p, "Your session expired. Please sign in again."));
27498
- if (action === "deny") return redirectWithError(p.redirect_uri, p.state, "access_denied");
27499
- if (action !== "approve") return c.text("unsupported action", 400);
27523
+ if (action2 === "deny") return redirectWithError(p.redirect_uri, p.state, "access_denied");
27524
+ if (action2 !== "approve") return c.text("unsupported action", 400);
27500
27525
  const scope = negotiateScope(p.scope, user, p.resource);
27501
27526
  const code = `code_${randomBytes2(32).toString("base64url")}`;
27502
27527
  const expiresAt = new Date(Date.now() + CODE_TTL_SECONDS * 1e3).toISOString();
@@ -28552,12 +28577,12 @@ vaultApp.post("/api/communications/review", async (c) => {
28552
28577
  const token4 = typeof body.token === "string" ? body.token : void 0;
28553
28578
  if (!validToken(token4)) return c.json({ ok: false, error: "missing or malformed Vault App link" }, 401);
28554
28579
  const path5 = stringValue(body.path, 320);
28555
- const action = stringValue(body.action, 24);
28580
+ const action2 = stringValue(body.action, 24);
28556
28581
  const title = typeof body.title === "string" ? stringValue(body.title, 320) : void 0;
28557
28582
  const content = typeof body.content === "string" && body.content.length <= 1e5 ? body.content : void 0;
28558
28583
  const feedback = typeof body.feedback === "string" ? body.feedback.trim().slice(0, 1e4) : void 0;
28559
28584
  const baseRevision = typeof body.baseRevision === "number" && Number.isInteger(body.baseRevision) ? body.baseRevision : void 0;
28560
- const normalizedAction = action === "accept" ? "approve" : action;
28585
+ const normalizedAction = action2 === "accept" ? "approve" : action2;
28561
28586
  if (!path5 || !normalizedAction || !["approve", "deny", "edit"].includes(normalizedAction) || body.title !== void 0 && !title) {
28562
28587
  return c.json({ ok: false, error: "valid path, action, and optional draft fields are required" }, 400);
28563
28588
  }
@@ -30690,30 +30715,30 @@ function renderEditorialReadingRoomV1(input, config, now = /* @__PURE__ */ new D
30690
30715
  startLabel: "Start reading"
30691
30716
  },
30692
30717
  deck: input.dek ?? `Prepared for ${input.generatedFor}.`,
30693
- articles: input.sections.map((section, index) => {
30718
+ articles: input.sections.map((section2, index) => {
30694
30719
  const provenance = [
30695
30720
  "",
30696
30721
  "## Sources",
30697
30722
  "",
30698
- ...section.sourceLabels.map((label) => `- ${label}`),
30699
- ...section.sourceRefs.map((ref) => `- ${scheduledSourceRef(ref)}`)
30723
+ ...section2.sourceLabels.map((label) => `- ${label}`),
30724
+ ...section2.sourceRefs.map((ref) => `- ${scheduledSourceRef(ref)}`)
30700
30725
  ].join("\n");
30701
- const withFiles = index === input.sections.length - 1 && filesMarkdown ? `${section.markdown}
30726
+ const withFiles = index === input.sections.length - 1 && filesMarkdown ? `${section2.markdown}
30702
30727
 
30703
30728
  ${provenance}
30704
30729
 
30705
- ${filesMarkdown}` : `${section.markdown}
30730
+ ${filesMarkdown}` : `${section2.markdown}
30706
30731
 
30707
30732
  ${provenance}`;
30708
30733
  return {
30709
- slug: section.id,
30734
+ slug: section2.id,
30710
30735
  category: "Report",
30711
- kicker: section.summary ?? "Scheduled result",
30736
+ kicker: section2.summary ?? "Scheduled result",
30712
30737
  order: index + 1,
30713
- title: section.title,
30714
- summary: section.summary ?? section.title,
30738
+ title: section2.title,
30739
+ summary: section2.summary ?? section2.title,
30715
30740
  sourceType: "Scheduled result",
30716
- sourceLabel: section.sourceLabels.join(" \xB7 ").slice(0, 500),
30741
+ sourceLabel: section2.sourceLabels.join(" \xB7 ").slice(0, 500),
30717
30742
  markdown: withFiles
30718
30743
  };
30719
30744
  }),
@@ -31295,7 +31320,7 @@ async function prepareCommonsEntity(input, user) {
31295
31320
  presentHeadings: headings,
31296
31321
  recommendedSections: contract.recommendedSections,
31297
31322
  optionalSections: contract.optionalSections,
31298
- missingRecommendedSections: contract.recommendedSections.filter((section) => !presentHeadingLabels.has(section.toLowerCase())),
31323
+ missingRecommendedSections: contract.recommendedSections.filter((section2) => !presentHeadingLabels.has(section2.toLowerCase())),
31299
31324
  rule: "Use the entity profile as a guide, but omit any section or subsection that is not supported by source evidence."
31300
31325
  },
31301
31326
  instructions: [
@@ -31368,8 +31393,8 @@ async function validateCommonsEntity(input, user) {
31368
31393
  headings,
31369
31394
  placeholderSections,
31370
31395
  requiredOmissionRule: "Omit unsupported profile sections; do not publish sections whose only content is that the source lacks evidence.",
31371
- supportedRecommendedSections: contract.recommendedSections.filter((section) => headings.some((heading) => heading.text.toLowerCase() === section.toLowerCase())),
31372
- unsupportedRecommendedSections: contract.recommendedSections.filter((section) => !headings.some((heading) => heading.text.toLowerCase() === section.toLowerCase()))
31396
+ supportedRecommendedSections: contract.recommendedSections.filter((section2) => headings.some((heading) => heading.text.toLowerCase() === section2.toLowerCase())),
31397
+ unsupportedRecommendedSections: contract.recommendedSections.filter((section2) => !headings.some((heading) => heading.text.toLowerCase() === section2.toLowerCase()))
31373
31398
  },
31374
31399
  tagResolutions: resolveCommonsTags(
31375
31400
  uniqueStrings([...input.tags ?? [], ...input.keywords ?? []]).slice(0, 20),
@@ -31894,7 +31919,7 @@ function unsupportedPlaceholderSections(markdown) {
31894
31919
  /insufficient evidence/i,
31895
31920
  /source material does not/i
31896
31921
  ];
31897
- return sections.filter((section) => placeholderPatterns.some((pattern) => pattern.test(section.body)) && wordCount(section.body) <= 160).map((section) => section.heading);
31922
+ return sections.filter((section2) => placeholderPatterns.some((pattern) => pattern.test(section2.body)) && wordCount(section2.body) <= 160).map((section2) => section2.heading);
31898
31923
  }
31899
31924
  function markdownSectionBodies(markdown) {
31900
31925
  const lines = String(markdown || "").split(/\r?\n/);
@@ -32378,18 +32403,18 @@ function normalizeBody(bodyMarkdown, contentSections) {
32378
32403
  const body = String(bodyMarkdown ?? "").trim();
32379
32404
  if (body) return body.slice(0, 5e5);
32380
32405
  const sections = normalizeContentSections(contentSections);
32381
- return sections.map((section) => `## ${section.heading}
32406
+ return sections.map((section2) => `## ${section2.heading}
32382
32407
 
32383
- ${section.body}`).join("\n\n").slice(0, 5e5);
32408
+ ${section2.body}`).join("\n\n").slice(0, 5e5);
32384
32409
  }
32385
32410
  function normalizeContentSections(sections) {
32386
32411
  if (!Array.isArray(sections)) return [];
32387
- return sections.map((section, index) => ({
32388
- id: normalizeSlug(section.id || section.heading),
32389
- heading: requiredText(section.heading, "section.heading", 1, 180),
32390
- body: requiredText(section.body, "section.body", 1, 1e5),
32391
- position: Number.isFinite(section.position) ? Number(section.position) : index + 1,
32392
- citations: normalizeCitations(section.citations)
32412
+ return sections.map((section2, index) => ({
32413
+ id: normalizeSlug(section2.id || section2.heading),
32414
+ heading: requiredText(section2.heading, "section.heading", 1, 180),
32415
+ body: requiredText(section2.body, "section.body", 1, 1e5),
32416
+ position: Number.isFinite(section2.position) ? Number(section2.position) : index + 1,
32417
+ citations: normalizeCitations(section2.citations)
32393
32418
  })).slice(0, 80);
32394
32419
  }
32395
32420
  function normalizeArticleSections(sections) {
@@ -33558,18 +33583,1700 @@ function xmlEscape(value) {
33558
33583
  return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
33559
33584
  }
33560
33585
 
33561
- // src/api/scheduled-result-routes.ts
33586
+ // src/api/analytics-routes.ts
33562
33587
  import { Hono as Hono32 } from "hono";
33588
+ import { createMiddleware as createMiddleware4 } from "hono/factory";
33589
+ import { z as z34 } from "zod";
33590
+ import { createHash as createHash15 } from "crypto";
33591
+ import Papa2 from "papaparse";
33592
+
33593
+ // src/api/analytics-entitlement.ts
33594
+ import {
33595
+ createHash as createHash14,
33596
+ createHmac as createHmac3,
33597
+ randomBytes as randomBytes3,
33598
+ timingSafeEqual as timingSafeEqual2
33599
+ } from "crypto";
33563
33600
  import { z as z33 } from "zod";
33601
+ var THORBIT_PRODUCT_URL = "https://thorbit.ai";
33602
+ var REFRESH_INTERVAL_MS = 15 * 60 * 1e3;
33603
+ var ELIGIBLE_GRACE_MS = 24 * 60 * 60 * 1e3;
33604
+ var TRIAL_LENGTH_MS = 30 * 24 * 60 * 60 * 1e3;
33605
+ var ThorbitEntitlementSchema = z33.object({
33606
+ product: z33.literal("x_ray_pixel"),
33607
+ eligible: z33.boolean(),
33608
+ reason: z33.string().min(1).max(120),
33609
+ organizationId: z33.string().min(1).max(64),
33610
+ planId: z33.string().max(32).nullable(),
33611
+ subscriptionStatus: z33.string().max(20).nullable(),
33612
+ monthlyRecurringRevenueCents: z33.number().int().min(0),
33613
+ minimumMonthlyRecurringRevenueCents: z33.number().int().min(5e3),
33614
+ checkedAt: z33.string().datetime()
33615
+ }).strict();
33616
+ var AssertionSchema = z33.object({
33617
+ version: z33.literal(1),
33618
+ issuer: z33.literal("thorbit"),
33619
+ audience: z33.literal("mcp-scraper:x-ray"),
33620
+ issuedAt: z33.number().int().positive(),
33621
+ expiresAt: z33.number().int().positive(),
33622
+ entitlement: ThorbitEntitlementSchema
33623
+ }).strict();
33624
+ function thorbitBaseUrl() {
33625
+ return (process.env.THORBIT_X_RAY_BASE_URL?.trim() || THORBIT_PRODUCT_URL).replace(/\/$/, "");
33626
+ }
33627
+ function bridgeSecret() {
33628
+ return process.env.THORBIT_X_RAY_BRIDGE_SECRET?.trim() ?? "";
33629
+ }
33630
+ function bridgeConfigured() {
33631
+ return bridgeSecret().length >= 32;
33632
+ }
33633
+ function hint(value) {
33634
+ return value.length <= 8 ? value : `${value.slice(0, 4)}\u2026${value.slice(-4)}`;
33635
+ }
33636
+ function viewFromRow(row, options = {}) {
33637
+ const now = Date.now();
33638
+ const withinGrace = row ? Date.parse(row.access_expires_at) > now : false;
33639
+ const trialActive = Boolean(row && Date.parse(row.trial_ends_at) > now);
33640
+ const paidAccess = Boolean(row?.eligible && withinGrace);
33641
+ const access2 = paidAccess || trialActive;
33642
+ const status = paidAccess ? options.stale ? "stale" : "eligible" : trialActive ? "trial" : !row || row.connection_source === "trial" ? "trial_expired" : options.bridgeError ? "bridge_unavailable" : "ineligible";
33643
+ const trialStartedAt = row?.trial_started_at ?? new Date(now).toISOString();
33644
+ const trialEndsAt = row?.trial_ends_at ?? new Date(now).toISOString();
33645
+ return {
33646
+ product: "x_ray_pixel",
33647
+ productName: "Thorbit X-Ray Pixel",
33648
+ access: access2,
33649
+ status,
33650
+ reason: trialActive ? "trial_active" : row?.reason ?? "trial_expired",
33651
+ planId: row?.plan_id ?? null,
33652
+ subscriptionStatus: row?.subscription_status ?? null,
33653
+ monthlyRecurringRevenueCents: Number(row?.monthly_recurring_revenue_cents ?? 0),
33654
+ minimumMonthlyRecurringRevenueCents: Number(row?.minimum_monthly_recurring_revenue_cents ?? 5e3),
33655
+ verifiedAt: row?.verified_at ?? null,
33656
+ connectionSource: row?.connection_source === "trial" ? null : row?.connection_source ?? null,
33657
+ thorbitOrganizationHint: row?.thorbit_org_id ? hint(row.thorbit_org_id) : null,
33658
+ trialStartedAt,
33659
+ trialEndsAt,
33660
+ trialDaysRemaining: Math.max(0, Math.ceil((Date.parse(trialEndsAt) - now) / (24 * 60 * 60 * 1e3))),
33661
+ bridgeConfigured: bridgeConfigured(),
33662
+ billing: {
33663
+ standalonePriceUsdMonthly: 50,
33664
+ includedWithThorbitSubscriptionAtOrAboveUsdMonthly: 50,
33665
+ consumesMcpScraperCredits: false
33666
+ },
33667
+ owner: {
33668
+ name: "Thorbit",
33669
+ productUrl: THORBIT_PRODUCT_URL,
33670
+ host: "MCP Scraper"
33671
+ }
33672
+ };
33673
+ }
33674
+ async function storedEntitlement(userId) {
33675
+ const result = await getAnalyticsPool().query(
33676
+ "SELECT * FROM analytics_thorbit_entitlements WHERE user_id = $1",
33677
+ [userId]
33678
+ );
33679
+ return result.rows[0] ?? null;
33680
+ }
33681
+ async function ensureTrial(userId) {
33682
+ const startedAt = /* @__PURE__ */ new Date();
33683
+ const endsAt = new Date(startedAt.getTime() + TRIAL_LENGTH_MS);
33684
+ const result = await getAnalyticsPool().query(
33685
+ `INSERT INTO analytics_thorbit_entitlements(
33686
+ user_id, thorbit_org_id, connection_source, eligible, reason, plan_id,
33687
+ subscription_status, monthly_recurring_revenue_cents,
33688
+ minimum_monthly_recurring_revenue_cents, verified_at, refresh_after,
33689
+ access_expires_at, trial_started_at, trial_ends_at, last_refresh_error, updated_at
33690
+ ) VALUES ($1,NULL,'trial',false,'trial_active',NULL,NULL,0,5000,$2,$3,$3,$2,$3,NULL,now())
33691
+ ON CONFLICT(user_id) DO UPDATE SET user_id=excluded.user_id
33692
+ RETURNING *`,
33693
+ [userId, startedAt.toISOString(), endsAt.toISOString()]
33694
+ );
33695
+ return result.rows[0];
33696
+ }
33697
+ async function persistEntitlement(userId, source, entitlement) {
33698
+ const checked = new Date(entitlement.checkedAt);
33699
+ const refreshAfter = new Date(checked.getTime() + REFRESH_INTERVAL_MS);
33700
+ const accessExpiresAt = new Date(
33701
+ checked.getTime() + (entitlement.eligible ? ELIGIBLE_GRACE_MS : REFRESH_INTERVAL_MS)
33702
+ );
33703
+ const result = await getAnalyticsPool().query(
33704
+ `INSERT INTO analytics_thorbit_entitlements(
33705
+ user_id, thorbit_org_id, connection_source, eligible, reason, plan_id,
33706
+ subscription_status, monthly_recurring_revenue_cents,
33707
+ minimum_monthly_recurring_revenue_cents, verified_at, refresh_after,
33708
+ access_expires_at, last_refresh_error, updated_at
33709
+ ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,NULL,now())
33710
+ ON CONFLICT(user_id) DO UPDATE SET
33711
+ thorbit_org_id=excluded.thorbit_org_id,
33712
+ connection_source=excluded.connection_source,
33713
+ eligible=excluded.eligible,
33714
+ reason=excluded.reason,
33715
+ plan_id=excluded.plan_id,
33716
+ subscription_status=excluded.subscription_status,
33717
+ monthly_recurring_revenue_cents=excluded.monthly_recurring_revenue_cents,
33718
+ minimum_monthly_recurring_revenue_cents=excluded.minimum_monthly_recurring_revenue_cents,
33719
+ verified_at=excluded.verified_at,
33720
+ refresh_after=excluded.refresh_after,
33721
+ access_expires_at=excluded.access_expires_at,
33722
+ last_refresh_error=NULL,
33723
+ updated_at=now()
33724
+ RETURNING *`,
33725
+ [
33726
+ userId,
33727
+ entitlement.organizationId,
33728
+ source,
33729
+ entitlement.eligible,
33730
+ entitlement.reason,
33731
+ entitlement.planId,
33732
+ entitlement.subscriptionStatus,
33733
+ entitlement.monthlyRecurringRevenueCents,
33734
+ entitlement.minimumMonthlyRecurringRevenueCents,
33735
+ checked.toISOString(),
33736
+ refreshAfter.toISOString(),
33737
+ accessExpiresAt.toISOString()
33738
+ ]
33739
+ );
33740
+ return result.rows[0];
33741
+ }
33742
+ async function requestEntitlement(method, options) {
33743
+ const headers = { accept: "application/json" };
33744
+ let body;
33745
+ if (method === "POST") {
33746
+ headers.authorization = `Bearer ${options.apiKey ?? ""}`;
33747
+ } else {
33748
+ headers.authorization = `Bearer ${bridgeSecret()}`;
33749
+ headers["content-type"] = "application/json";
33750
+ body = JSON.stringify({ organizationId: options.organizationId });
33751
+ }
33752
+ const response = await fetch(`${thorbitBaseUrl()}/api/v1/x-ray/entitlement`, {
33753
+ method,
33754
+ headers,
33755
+ body,
33756
+ signal: AbortSignal.timeout(8e3)
33757
+ });
33758
+ const payload = await response.json().catch(() => null);
33759
+ if (!response.ok) {
33760
+ const message = payload && typeof payload === "object" && "error" in payload ? String(payload.error) : `Thorbit entitlement check failed (${response.status})`;
33761
+ throw new Error(message);
33762
+ }
33763
+ const parsed = z33.object({ entitlement: ThorbitEntitlementSchema }).safeParse(payload);
33764
+ if (!parsed.success) throw new Error("Thorbit returned an invalid entitlement assertion");
33765
+ return parsed.data.entitlement;
33766
+ }
33767
+ async function resolveAnalyticsEntitlement(userId) {
33768
+ const row = await storedEntitlement(userId) ?? await ensureTrial(userId);
33769
+ if (row.connection_source === "trial" || !row.thorbit_org_id) return viewFromRow(row);
33770
+ if (Date.parse(row.refresh_after) > Date.now()) return viewFromRow(row);
33771
+ if (!bridgeConfigured()) return viewFromRow(row, { stale: true, bridgeError: true });
33772
+ try {
33773
+ const entitlement = await requestEntitlement("PUT", {
33774
+ organizationId: row.thorbit_org_id
33775
+ });
33776
+ return viewFromRow(
33777
+ await persistEntitlement(userId, row.connection_source, entitlement)
33778
+ );
33779
+ } catch (error) {
33780
+ await getAnalyticsPool().query(
33781
+ "UPDATE analytics_thorbit_entitlements SET last_refresh_error=$2, updated_at=now() WHERE user_id=$1",
33782
+ [userId, error instanceof Error ? error.message.slice(0, 240) : "refresh_failed"]
33783
+ );
33784
+ return viewFromRow(row, { stale: true, bridgeError: true });
33785
+ }
33786
+ }
33787
+ async function analyticsOwnerHasCurrentEntitlement(userId) {
33788
+ const result = await getAnalyticsPool().query(
33789
+ `SELECT EXISTS(
33790
+ SELECT 1 FROM analytics_thorbit_entitlements
33791
+ WHERE user_id=$1 AND (
33792
+ (eligible=true AND access_expires_at > now()) OR trial_ends_at > now()
33793
+ )
33794
+ ) AS entitled`,
33795
+ [userId]
33796
+ );
33797
+ return Boolean(result.rows[0]?.entitled);
33798
+ }
33799
+ async function connectAnalyticsWithThorbitApiKey(userId, apiKey) {
33800
+ const entitlement = await requestEntitlement("POST", { apiKey });
33801
+ return viewFromRow(await persistEntitlement(userId, "api_key", entitlement));
33802
+ }
33803
+ function dashboardCallbackUrl() {
33804
+ const value = process.env.ANALYTICS_DASHBOARD_URL?.trim() || (process.env.NODE_ENV === "production" ? "https://mcpscraper.dev/dashboard/analytics" : "http://localhost:3000/dashboard/analytics");
33805
+ return new URL(value).toString();
33806
+ }
33807
+ async function createThorbitConnectUrl(userId) {
33808
+ if (!bridgeConfigured()) throw new Error("Thorbit X-Ray account bridge is not configured");
33809
+ const state = randomBytes3(32).toString("base64url");
33810
+ const stateHash = createHash14("sha256").update(state).digest("hex");
33811
+ await getAnalyticsPool().query(
33812
+ `INSERT INTO analytics_thorbit_connect_states(state_hash,user_id,expires_at)
33813
+ VALUES ($1,$2,now()+interval '10 minutes')
33814
+ ON CONFLICT(state_hash) DO NOTHING`,
33815
+ [stateHash, userId]
33816
+ );
33817
+ const url = new URL(`${thorbitBaseUrl()}/api/v1/x-ray/entitlement`);
33818
+ url.searchParams.set("redirect_uri", dashboardCallbackUrl());
33819
+ url.searchParams.set("state", state);
33820
+ return url.toString();
33821
+ }
33822
+ function verifyThorbitAssertion(token4) {
33823
+ const [encoded, signature] = token4.split(".");
33824
+ if (!encoded || !signature || !bridgeConfigured()) throw new Error("Invalid Thorbit assertion");
33825
+ const expected = createHmac3("sha256", bridgeSecret()).update(encoded).digest();
33826
+ const actual = Buffer.from(signature, "base64url");
33827
+ if (actual.length !== expected.length || !timingSafeEqual2(actual, expected)) {
33828
+ throw new Error("Invalid Thorbit assertion signature");
33829
+ }
33830
+ const parsed = AssertionSchema.safeParse(
33831
+ JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"))
33832
+ );
33833
+ if (!parsed.success || parsed.data.expiresAt <= Math.floor(Date.now() / 1e3)) {
33834
+ throw new Error("Expired or invalid Thorbit assertion");
33835
+ }
33836
+ return parsed.data.entitlement;
33837
+ }
33838
+ async function consumeThorbitConnectCallback(input) {
33839
+ const stateHash = createHash14("sha256").update(input.state).digest("hex");
33840
+ const client2 = await getAnalyticsPool().connect();
33841
+ try {
33842
+ await client2.query("BEGIN");
33843
+ const state = await client2.query(
33844
+ `UPDATE analytics_thorbit_connect_states
33845
+ SET consumed_at=now()
33846
+ WHERE state_hash=$1 AND user_id=$2 AND consumed_at IS NULL AND expires_at > now()
33847
+ RETURNING user_id`,
33848
+ [stateHash, input.userId]
33849
+ );
33850
+ if (!state.rowCount) throw new Error("Thorbit connection state is invalid or expired");
33851
+ const entitlement = verifyThorbitAssertion(input.assertion);
33852
+ await client2.query("COMMIT");
33853
+ return viewFromRow(
33854
+ await persistEntitlement(input.userId, "thorbit_session", entitlement)
33855
+ );
33856
+ } catch (error) {
33857
+ await client2.query("ROLLBACK");
33858
+ throw error;
33859
+ } finally {
33860
+ client2.release();
33861
+ }
33862
+ }
33863
+ async function disconnectAnalyticsEntitlement(userId) {
33864
+ await getAnalyticsPool().query(
33865
+ `UPDATE analytics_thorbit_entitlements
33866
+ SET thorbit_org_id=NULL,
33867
+ connection_source='trial',
33868
+ eligible=false,
33869
+ reason=CASE WHEN trial_ends_at > now() THEN 'trial_active' ELSE 'trial_expired' END,
33870
+ plan_id=NULL,
33871
+ subscription_status=NULL,
33872
+ monthly_recurring_revenue_cents=0,
33873
+ last_refresh_error=NULL,
33874
+ updated_at=now()
33875
+ WHERE user_id=$1`,
33876
+ [userId]
33877
+ );
33878
+ }
33879
+
33880
+ // src/api/analytics-routes.ts
33881
+ var analyticsApp = new Hono32();
33882
+ var auth3 = createApiKeyAuth();
33883
+ var entitlementGuard = createMiddleware4(async (c, next) => {
33884
+ const entitlement = await resolveAnalyticsEntitlement(Number(c.get("user").id));
33885
+ if (!entitlement.access) {
33886
+ return c.json(
33887
+ {
33888
+ ok: false,
33889
+ code: "thorbit_entitlement_required",
33890
+ error: "Thorbit X-Ray Pixel requires an active $50+ Thorbit subscription.",
33891
+ entitlement
33892
+ },
33893
+ 402
33894
+ );
33895
+ }
33896
+ await next();
33897
+ });
33898
+ var ThorbitApiKeySchema = z34.object({ apiKey: z34.string().trim().min(20).max(512) }).strict();
33899
+ var ThorbitCallbackSchema = z34.object({
33900
+ state: z34.string().min(20).max(300),
33901
+ assertion: z34.string().min(40).max(8e3)
33902
+ }).strict();
33903
+ var SiteInputSchema = z34.object({
33904
+ name: z34.string().trim().min(1).max(120),
33905
+ slug: z34.string().trim().min(1).max(80).optional(),
33906
+ timezone: z34.string().trim().min(1).max(80).optional()
33907
+ }).strict();
33908
+ var PixelInputSchema = z34.object({
33909
+ name: z34.string().trim().min(1).max(120),
33910
+ environment: z34.string().trim().min(1).max(60).optional()
33911
+ }).strict();
33912
+ var PixelUpdateSchema = z34.object({
33913
+ name: z34.string().trim().min(1).max(120).optional(),
33914
+ environment: z34.string().trim().min(1).max(60).optional(),
33915
+ status: z34.enum(["active", "disabled"]).optional()
33916
+ }).strict().refine(
33917
+ (value) => Object.keys(value).length > 0,
33918
+ "Provide at least one change."
33919
+ );
33920
+ var EventSchema = z34.object({
33921
+ eventId: z34.string().trim().min(8).max(160),
33922
+ eventName: z34.string().trim().regex(/^[a-z][a-z0-9_]{1,79}$/),
33923
+ occurredAt: z34.string().datetime(),
33924
+ visitorId: z34.string().trim().max(160).optional(),
33925
+ sessionId: z34.string().trim().max(160).optional(),
33926
+ path: z34.string().trim().max(2e3).optional(),
33927
+ canonicalUrl: z34.string().url().max(3e3).optional(),
33928
+ title: z34.string().trim().max(500).optional(),
33929
+ referrer: z34.string().trim().max(3e3).optional(),
33930
+ source: z34.string().trim().max(180).optional(),
33931
+ medium: z34.string().trim().max(180).optional(),
33932
+ campaign: z34.string().trim().max(240).optional(),
33933
+ clickIds: z34.record(z34.string(), z34.string().trim().max(500)).optional(),
33934
+ deviceClass: z34.enum(["desktop", "tablet", "mobile", "unknown"]).optional(),
33935
+ properties: z34.record(z34.string(), z34.unknown()).optional()
33936
+ }).strict();
33937
+ var IngestionSchema = z34.object({
33938
+ pixelId: z34.string().trim().min(8).max(160),
33939
+ events: z34.array(EventSchema).min(1).max(20)
33940
+ }).strict();
33941
+ var ConversionSchema = z34.object({
33942
+ siteId: z34.string().uuid(),
33943
+ pixelId: z34.string().uuid().optional(),
33944
+ sessionId: z34.string().trim().max(160).optional(),
33945
+ sourceEventId: z34.string().trim().min(8).max(200),
33946
+ conversionKind: z34.string().trim().regex(/^[a-z][a-z0-9_]{1,79}$/),
33947
+ valueMinor: z34.number().int().min(0).max(2147483647).optional(),
33948
+ currency: z34.string().trim().regex(/^[A-Za-z]{3}$/).optional(),
33949
+ occurredAt: z34.string().datetime()
33950
+ }).strict();
33951
+ var ExportSchema = z34.object({
33952
+ report: z34.enum(["overview", "acquisition", "content", "conversions"]).default("overview"),
33953
+ format: z34.enum(["csv", "json", "markdown"]).default("csv"),
33954
+ idempotencyKey: z34.string().trim().min(8).max(160).optional()
33955
+ }).strict();
33956
+ var CampaignLinkSchema = z34.object({
33957
+ pixelId: z34.string().uuid().optional(),
33958
+ name: z34.string().trim().min(1).max(120),
33959
+ destinationUrl: z34.string().url().max(3e3),
33960
+ shortCode: z34.string().trim().min(3).max(48).optional(),
33961
+ source: z34.string().trim().min(1).max(180),
33962
+ medium: z34.string().trim().min(1).max(180),
33963
+ campaign: z34.string().trim().min(1).max(240),
33964
+ term: z34.string().trim().max(240).optional(),
33965
+ content: z34.string().trim().max(240).optional(),
33966
+ adGroup: z34.string().trim().max(240).optional(),
33967
+ adName: z34.string().trim().max(240).optional(),
33968
+ creativeId: z34.string().trim().max(240).optional()
33969
+ }).strict();
33970
+ var FormFieldSchema = z34.object({
33971
+ id: z34.string().trim().regex(/^[a-z][a-z0-9_]{0,47}$/),
33972
+ type: z34.enum([
33973
+ "text",
33974
+ "email",
33975
+ "tel",
33976
+ "textarea",
33977
+ "select",
33978
+ "checkbox",
33979
+ "radio",
33980
+ "number"
33981
+ ]),
33982
+ label: z34.string().trim().min(1).max(120),
33983
+ required: z34.boolean().default(false),
33984
+ options: z34.array(z34.string().trim().min(1).max(120)).max(30).optional()
33985
+ }).strict();
33986
+ var FormInputSchema = z34.object({
33987
+ pixelId: z34.string().uuid(),
33988
+ name: z34.string().trim().min(1).max(120),
33989
+ fields: z34.array(FormFieldSchema).min(1).max(30).refine(
33990
+ (fields) => new Set(fields.map((field) => field.id)).size === fields.length,
33991
+ "Field IDs must be unique."
33992
+ ),
33993
+ brand: z34.object({
33994
+ primaryColor: z34.string().regex(/^#[0-9a-f]{6}$/i),
33995
+ backgroundColor: z34.string().regex(/^#[0-9a-f]{6}$/i),
33996
+ textColor: z34.string().regex(/^#[0-9a-f]{6}$/i),
33997
+ radius: z34.number().int().min(0).max(32)
33998
+ }).strict(),
33999
+ submitLabel: z34.string().trim().min(1).max(80).optional(),
34000
+ successMessage: z34.string().trim().min(1).max(500).optional(),
34001
+ redirectUrl: z34.string().url().max(3e3).optional(),
34002
+ consentText: z34.string().trim().max(1e3).optional(),
34003
+ publish: z34.boolean().optional()
34004
+ }).strict();
34005
+ var FormSubmissionSchema = z34.object({
34006
+ data: z34.record(
34007
+ z34.string(),
34008
+ z34.union([z34.string().max(5e3), z34.number(), z34.boolean()])
34009
+ ).refine((value) => Object.keys(value).length <= 30),
34010
+ visitorId: z34.string().trim().max(160).optional(),
34011
+ sessionId: z34.string().trim().max(160).optional(),
34012
+ source: z34.string().trim().max(180).optional(),
34013
+ medium: z34.string().trim().max(180).optional(),
34014
+ campaign: z34.string().trim().max(240).optional(),
34015
+ clickIds: z34.record(z34.string(), z34.string().trim().max(500)).optional(),
34016
+ placementUrl: z34.string().url().max(3e3),
34017
+ website: z34.string().max(0).optional()
34018
+ }).strict();
34019
+ var CrmImportSchema = z34.object({
34020
+ sourceSystem: z34.enum([
34021
+ "hubspot",
34022
+ "salesforce",
34023
+ "gohighlevel",
34024
+ "zoho",
34025
+ "pipedrive",
34026
+ "keap",
34027
+ "other"
34028
+ ]),
34029
+ filename: z34.string().trim().min(1).max(240),
34030
+ csv: z34.string().min(1).max(8e6),
34031
+ mapping: z34.object({
34032
+ email: z34.string().trim().min(1).max(240).optional(),
34033
+ firstName: z34.string().trim().min(1).max(240).optional(),
34034
+ lastName: z34.string().trim().min(1).max(240).optional(),
34035
+ name: z34.string().trim().min(1).max(240).optional(),
34036
+ phone: z34.string().trim().min(1).max(240).optional(),
34037
+ company: z34.string().trim().min(1).max(240).optional(),
34038
+ externalId: z34.string().trim().min(1).max(240).optional()
34039
+ }).strict()
34040
+ }).strict();
34041
+ var ActivationDestinationSchema = z34.object({
34042
+ platform: z34.enum(["meta", "google", "tiktok", "reddit"]),
34043
+ name: z34.string().trim().min(1).max(120),
34044
+ connectionRef: z34.string().trim().max(240).optional(),
34045
+ externalDatasetId: z34.string().trim().max(240).optional(),
34046
+ eventMapping: z34.record(z34.string(), z34.string().trim().max(120)).optional()
34047
+ }).strict();
34048
+ var BusinessModelSchema = z34.object({ businessModel: z34.enum(["leadgen", "saas", "ecommerce"]) }).strict();
34049
+ var AdSpendSchema = z34.object({
34050
+ rows: z34.array(
34051
+ z34.object({
34052
+ date: z34.string().date(),
34053
+ platform: z34.string().trim().min(1).max(80),
34054
+ campaign: z34.string().trim().max(240).optional(),
34055
+ spendMinor: z34.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
34056
+ currency: z34.string().regex(/^[A-Za-z]{3}$/).optional(),
34057
+ sourceRef: z34.string().max(500).optional()
34058
+ }).strict()
34059
+ ).min(1).max(5e3)
34060
+ }).strict();
34061
+ function safeOrigin(origin) {
34062
+ if (!origin || origin === "null") return null;
34063
+ try {
34064
+ const url = new URL(origin);
34065
+ return url.protocol === "http:" || url.protocol === "https:" ? url.origin : null;
34066
+ } catch {
34067
+ return null;
34068
+ }
34069
+ }
34070
+ function dashboardOriginAllowed(origin) {
34071
+ if (origin === "https://mcpscraper.dev" || origin === "https://www.mcpscraper.dev")
34072
+ return true;
34073
+ if (process.env.NODE_ENV !== "production" && /^http:\/\/(localhost|127\.0\.0\.1):\d+$/.test(origin))
34074
+ return true;
34075
+ return (process.env.ALLOWED_ORIGINS ?? process.env.APP_ORIGIN ?? "").split(",").map((value) => value.trim()).includes(origin);
34076
+ }
34077
+ analyticsApp.use("*", async (c, next) => {
34078
+ const origin = safeOrigin(c.req.header("origin"));
34079
+ const publicIngestion = c.req.path === "/v1/events";
34080
+ if (origin && (publicIngestion || dashboardOriginAllowed(origin))) {
34081
+ c.header("Access-Control-Allow-Origin", origin);
34082
+ c.header("Vary", "Origin");
34083
+ c.header(
34084
+ "Access-Control-Allow-Methods",
34085
+ "GET, POST, PATCH, DELETE, OPTIONS"
34086
+ );
34087
+ c.header("Access-Control-Allow-Headers", "content-type, x-api-key");
34088
+ c.header("Access-Control-Max-Age", "600");
34089
+ }
34090
+ if (c.req.method === "OPTIONS") return c.body(null, 204);
34091
+ await next();
34092
+ });
34093
+ function analyticsPixelSdk() {
34094
+ return `(()=>{const s=document.currentScript;if(!s)return;const pixel=s.dataset.pixel;if(!pixel)return;const endpoint=new URL('/analytics/v1/events',s.src).toString(),ttl=18e5;const key='mcp_analytics_visitor',sessionKey='mcp_analytics_session',touchKey='mcp_analytics_touch';const id=()=>crypto.randomUUID?crypto.randomUUID():'e_'+Date.now().toString(36)+Math.random().toString(36).slice(2);let consent=s.dataset.consent==='granted'&&!navigator.globalPrivacyControl,visitor=null,session=null;const read=k=>{try{return localStorage.getItem(k)}catch{return null}},write=(k,v)=>{try{localStorage.setItem(k,v)}catch{}};const ensure=()=>{if(!consent)return;visitor=read(key)||id();const now=Date.now();let saved={};try{saved=JSON.parse(read(sessionKey)||'{}')}catch{}session=saved.id&&now-Number(saved.seen||0)<ttl?saved.id:id();write(key,visitor);write(sessionKey,JSON.stringify({id:session,seen:now}))};const attribution=()=>{const q=new URLSearchParams(location.search),clickIds={},properties={},map={utm_term:'utm_term',utm_content:'utm_content',utm_id:'campaign_id',campaign_id:'campaign_id',adset_id:'ad_set_id',ad_set_id:'ad_set_id',adgroup_id:'ad_group_id',ad_group_id:'ad_group_id',ad_id:'ad_id',creative_id:'creative_id',placement:'placement'};for(const k of ['fbclid','gclid','gbraid','wbraid','ttclid','rdt_cid','msclkid']){const v=q.get(k);if(v)clickIds[k]=v}for(const [from,to] of Object.entries(map)){const v=q.get(from);if(v)properties[to]=v}const current={source:q.get('utm_source')||undefined,medium:q.get('utm_medium')||undefined,campaign:q.get('utm_campaign')||undefined,clickIds,properties};if(current.source||Object.keys(clickIds).length)write(touchKey,JSON.stringify(current));else{try{return JSON.parse(read(touchKey)||'null')||current}catch{}}return current};const send=(name,properties={})=>{if(!consent||navigator.globalPrivacyControl)return;ensure();const a=attribution();const body=JSON.stringify({pixelId:pixel,events:[{eventId:id(),eventName:name,occurredAt:new Date().toISOString(),visitorId:visitor,sessionId:session,path:location.pathname,canonicalUrl:location.origin+location.pathname,title:document.title,referrer:document.referrer||undefined,...a,deviceClass:innerWidth<640?'mobile':innerWidth<1024?'tablet':'desktop',properties:{...a.properties,...properties}}]});if(navigator.sendBeacon&&navigator.sendBeacon(endpoint,new Blob([body],{type:'application/json'})))return;fetch(endpoint,{method:'POST',headers:{'content-type':'application/json'},body,keepalive:true,credentials:'omit'}).catch(()=>{})};const api={page:()=>send('page_view'),track:send,consent:state=>{consent=(state==='granted'||state===true)&&!navigator.globalPrivacyControl;if(consent){ensure();api.page()}},debug:()=>({pixel,endpoint,consent,gpc:Boolean(navigator.globalPrivacyControl),visitorId:visitor,sessionId:session,hostname:location.hostname,path:location.pathname,...attribution()})};window.mcpAnalytics=api;ensure();if(consent)api.page();let current=location.href;const changed=()=>{if(location.href!==current){current=location.href;api.page()}};for(const method of ['pushState','replaceState']){const original=history[method];history[method]=function(...args){const result=original.apply(this,args);queueMicrotask(changed);return result}}addEventListener('popstate',changed)})();`;
34095
+ }
34096
+ function reportFilters(url) {
34097
+ const end = url.searchParams.get("end") || (/* @__PURE__ */ new Date()).toISOString();
34098
+ const defaultStart = new Date(
34099
+ Date.parse(end) - 30 * 24 * 60 * 60 * 1e3
34100
+ ).toISOString();
34101
+ const list = (name) => url.searchParams.getAll(name).flatMap((value) => value.split(",")).map((value) => value.trim()).filter(Boolean);
34102
+ return {
34103
+ start: url.searchParams.get("start") || defaultStart,
34104
+ end,
34105
+ timezone: validTimezone(url.searchParams.get("timezone")),
34106
+ pixelIds: list("pixelId"),
34107
+ hostnames: list("hostname"),
34108
+ source: url.searchParams.get("source") || void 0,
34109
+ medium: url.searchParams.get("medium") || void 0,
34110
+ campaign: url.searchParams.get("campaign") || void 0,
34111
+ eventName: url.searchParams.get("eventName") || void 0,
34112
+ attributionModel: url.searchParams.get("attributionModel") === "last_touch" ? "last_touch" : "first_touch",
34113
+ channelFamily: url.searchParams.get("channelFamily") || void 0,
34114
+ platform: url.searchParams.get("platform") || void 0,
34115
+ referrer: url.searchParams.get("referrer") || void 0,
34116
+ path: url.searchParams.get("path") || void 0,
34117
+ deviceClass: url.searchParams.get("deviceClass") || void 0,
34118
+ countryCode: url.searchParams.get("countryCode") || void 0,
34119
+ regionCode: url.searchParams.get("regionCode") || void 0,
34120
+ conversionKind: url.searchParams.get("conversionKind") || void 0
34121
+ };
34122
+ }
34123
+ function validTimezone(value) {
34124
+ if (!value) return "UTC";
34125
+ try {
34126
+ new Intl.DateTimeFormat("en-US", { timeZone: value }).format(/* @__PURE__ */ new Date());
34127
+ return value;
34128
+ } catch {
34129
+ throw new AnalyticsRepositoryError(
34130
+ "invalid_timezone",
34131
+ "Choose a valid IANA timezone."
34132
+ );
34133
+ }
34134
+ }
34135
+ function validateReportWindow(filters) {
34136
+ const start = Date.parse(filters.start);
34137
+ const end = Date.parse(filters.end);
34138
+ if (!Number.isFinite(start) || !Number.isFinite(end) || start >= end) {
34139
+ throw new AnalyticsRepositoryError(
34140
+ "invalid_date_range",
34141
+ "Choose a valid date range."
34142
+ );
34143
+ }
34144
+ if (end - start > 366 * 24 * 60 * 60 * 1e3) {
34145
+ throw new AnalyticsRepositoryError(
34146
+ "date_range_too_large",
34147
+ "Date ranges are limited to 366 days."
34148
+ );
34149
+ }
34150
+ }
34151
+ function reportPage(url) {
34152
+ const rawLimit = Number(url.searchParams.get("limit") || 25);
34153
+ return {
34154
+ limit: Number.isFinite(rawLimit) ? Math.min(100, Math.max(1, Math.trunc(rawLimit))) : 25,
34155
+ cursor: url.searchParams.get("cursor") || void 0
34156
+ };
34157
+ }
34158
+ function pixelSnippet(publicId) {
34159
+ return `<script async src="https://mcpscraper.dev/analytics/v1/pixel.js" data-pixel="${publicId}" data-consent="required"></script>`;
34160
+ }
34161
+ function formSnippet(publicId) {
34162
+ return `<div data-mcp-form="${publicId}"></div><script async src="https://mcpscraper.dev/analytics/v1/form.js?form=${publicId}"></script>`;
34163
+ }
34164
+ function escapeHtml6(value) {
34165
+ return String(value ?? "").replace(
34166
+ /[&<>"']/g,
34167
+ (character) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[character]
34168
+ );
34169
+ }
34170
+ function renderPublicForm(form, placementUrl) {
34171
+ if (!form)
34172
+ return "<!doctype html><title>Form unavailable</title><p>Form unavailable.</p>";
34173
+ const fields = form.fields.map((field) => {
34174
+ const required = field.required ? " required" : "";
34175
+ const options = (field.options ?? []).map(
34176
+ (option) => `<option value="${escapeHtml6(option)}">${escapeHtml6(option)}</option>`
34177
+ ).join("");
34178
+ const control = field.type === "textarea" ? `<textarea id="${escapeHtml6(field.id)}" name="${escapeHtml6(field.id)}"${required}></textarea>` : field.type === "select" ? `<select id="${escapeHtml6(field.id)}" name="${escapeHtml6(field.id)}"${required}><option value="">Choose\u2026</option>${options}</select>` : field.type === "checkbox" ? `<input id="${escapeHtml6(field.id)}" name="${escapeHtml6(field.id)}" type="checkbox" value="true"${required}>` : `<input id="${escapeHtml6(field.id)}" name="${escapeHtml6(field.id)}" type="${escapeHtml6(field.type)}"${required}>`;
34179
+ return `<label${field.type === "checkbox" ? ' class="check"' : ""}><span>${escapeHtml6(field.label)}${field.required ? " *" : ""}</span>${control}</label>`;
34180
+ }).join("");
34181
+ const brand = form.brand;
34182
+ return `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><style>:root{font-family:ui-sans-serif,system-ui;color:${escapeHtml6(brand.textColor)};background:${escapeHtml6(brand.backgroundColor)}}*{box-sizing:border-box}body{margin:0;padding:18px}form{display:grid;gap:14px}label{display:grid;gap:6px;font-size:13px;font-weight:650}.check{grid-template-columns:auto 1fr;align-items:center}.check span{grid-column:2}.check input{grid-column:1;grid-row:1}input,textarea,select{width:100%;min-height:44px;padding:10px 12px;border:1px solid #ccd3df;border-radius:${brand.radius}px;background:white;color:inherit;font:inherit}textarea{min-height:110px;resize:vertical}button{min-height:46px;border:0;border-radius:${brand.radius}px;color:white;background:${escapeHtml6(brand.primaryColor)};font:inherit;font-weight:750;cursor:pointer}.consent{margin:0;color:#667085;font-size:11px;line-height:1.45}.notice{display:none;padding:12px;border-radius:${brand.radius}px;background:#edf8f2;color:#17613c}.hp{position:absolute!important;left:-9999px!important}</style></head><body><form id="mcp-form">${fields}<label class="hp">Website<input name="website" autocomplete="off" tabindex="-1"></label>${form.consent_text ? `<p class="consent">${escapeHtml6(form.consent_text)}</p>` : ""}<button>${escapeHtml6(form.submit_label)}</button><div class="notice" role="status"></div></form><script>(()=>{const form=document.querySelector('#mcp-form'),notice=form.querySelector('.notice'),q=new URLSearchParams(location.search);form.addEventListener('submit',async event=>{event.preventDefault();const button=form.querySelector('button');button.disabled=true;const raw=Object.fromEntries(new FormData(form).entries()),website=String(raw.website||''),clickIds={};delete raw.website;for(const k of ['fbclid','gclid','gbraid','wbraid','ttclid','rdt_cid','msclkid']){const v=q.get(k);if(v)clickIds[k]=v}try{const response=await fetch('/analytics/forms/${escapeHtml6(form.public_id)}/submissions',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({data:raw,website,placementUrl:${JSON.stringify(placementUrl)},visitorId:q.get('visitor')||undefined,sessionId:q.get('session')||undefined,source:q.get('source')||undefined,medium:q.get('medium')||undefined,campaign:q.get('campaign')||undefined,clickIds})});if(!response.ok)throw new Error('submit');notice.textContent=${JSON.stringify(form.success_message)};notice.style.display='block';form.reset();${form.redirect_url ? `setTimeout(()=>{top.location.href=${JSON.stringify(form.redirect_url)}},700);` : ""}}catch{notice.textContent='Your response could not be submitted. Please try again.';notice.style.display='block'}finally{button.disabled=false}})})()</script></body></html>`;
34183
+ }
34184
+ analyticsApp.get(
34185
+ "/health/live",
34186
+ (c) => c.json({ status: "live", checkedAt: (/* @__PURE__ */ new Date()).toISOString() }, 200, {
34187
+ "Cache-Control": "no-store"
34188
+ })
34189
+ );
34190
+ analyticsApp.get("/health/ready", async (c) => {
34191
+ const startedAt = Date.now();
34192
+ try {
34193
+ await getAnalyticsPool().query("SELECT 1");
34194
+ return c.json(
34195
+ {
34196
+ status: "ready",
34197
+ database: true,
34198
+ latencyMs: Date.now() - startedAt,
34199
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString()
34200
+ },
34201
+ 200,
34202
+ { "Cache-Control": "no-store" }
34203
+ );
34204
+ } catch (error) {
34205
+ return c.json(
34206
+ {
34207
+ status: "not_ready",
34208
+ database: false,
34209
+ error: error instanceof Error ? error.message : String(error),
34210
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString()
34211
+ },
34212
+ 503,
34213
+ { "Cache-Control": "no-store" }
34214
+ );
34215
+ }
34216
+ });
34217
+ analyticsApp.get("/v1/pixel.js", (c) => {
34218
+ c.header("Content-Type", "application/javascript; charset=utf-8");
34219
+ c.header("Cache-Control", "public, max-age=300");
34220
+ return c.body(analyticsPixelSdk());
34221
+ });
34222
+ analyticsApp.get("/v1/form.js", async (c) => {
34223
+ const form = await getPublicAnalyticsForm(c.req.query("form") || "");
34224
+ if (!form)
34225
+ return c.body("/* Form unavailable. */", 404, {
34226
+ "Content-Type": "application/javascript; charset=utf-8"
34227
+ });
34228
+ if (!await analyticsOwnerHasCurrentEntitlement(Number(form.owner_user_id)))
34229
+ return c.body("/* Thorbit X-Ray Pixel subscription required. */", 402, {
34230
+ "Content-Type": "application/javascript; charset=utf-8",
34231
+ "Cache-Control": "no-store"
34232
+ });
34233
+ const script = `(()=>{const s=document.currentScript,form=${JSON.stringify(form.public_id)},pixel=${JSON.stringify(form.pixel_public_id)};if(!document.querySelector('script[data-pixel="'+pixel+'"]')){const p=document.createElement('script');p.async=true;p.src=new URL('/analytics/v1/pixel.js',s.src);p.dataset.pixel=pixel;p.dataset.consent='required';document.head.appendChild(p)}const target=document.querySelector('[data-mcp-form="'+form+'"]');if(!target)return;const frame=document.createElement('iframe'),params=new URLSearchParams({placement:location.href}),q=new URLSearchParams(location.search);try{const visitor=localStorage.getItem('mcp_analytics_visitor'),session=JSON.parse(localStorage.getItem('mcp_analytics_session')||'{}').id;if(visitor)params.set('visitor',visitor);if(session)params.set('session',session)}catch{}for(const [from,to] of [['utm_source','source'],['utm_medium','medium'],['utm_campaign','campaign'],['fbclid','fbclid'],['gclid','gclid'],['gbraid','gbraid'],['wbraid','wbraid'],['ttclid','ttclid'],['rdt_cid','rdt_cid'],['msclkid','msclkid']]){const value=q.get(from);if(value)params.set(to,value)}frame.title=${JSON.stringify(form.name)};frame.src=new URL('/analytics/forms/'+form+'/embed?'+params,s.src);frame.style.cssText='width:100%;min-height:520px;border:0;display:block';frame.loading='lazy';target.replaceChildren(frame)})();`;
34234
+ return c.body(script, 200, {
34235
+ "Content-Type": "application/javascript; charset=utf-8",
34236
+ "Cache-Control": "public, max-age=300"
34237
+ });
34238
+ });
34239
+ analyticsApp.get("/forms/:publicId/embed", async (c) => {
34240
+ const form = await getPublicAnalyticsForm(c.req.param("publicId"));
34241
+ if (!form)
34242
+ return c.html(
34243
+ "<!doctype html><title>Form unavailable</title><p>Form unavailable.</p>",
34244
+ 404
34245
+ );
34246
+ if (!await analyticsOwnerHasCurrentEntitlement(Number(form.owner_user_id)))
34247
+ return c.html(
34248
+ '<!doctype html><meta name=viewport content="width=device-width"><style>body{font:15px system-ui;padding:24px;color:#475467}strong{display:block;color:#101828;margin-bottom:6px}</style><strong>Form unavailable</strong>This Thorbit X-Ray Pixel account needs an active subscription.',
34249
+ 402
34250
+ );
34251
+ const placementUrl = c.req.query("placement") || "";
34252
+ if (!await isAnalyticsFormPlacementApproved(form.pixel_id, placementUrl))
34253
+ return c.html(
34254
+ "<!doctype html><title>Form unavailable</title><p>This form is not approved for this hostname.</p>",
34255
+ 403
34256
+ );
34257
+ c.header(
34258
+ "Content-Security-Policy",
34259
+ "default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; connect-src 'self'; form-action 'none'; base-uri 'none'"
34260
+ );
34261
+ c.header("Cache-Control", "no-store");
34262
+ return c.html(renderPublicForm(form, placementUrl));
34263
+ });
34264
+ analyticsApp.post("/forms/:publicId/submissions", async (c) => {
34265
+ const parsed = FormSubmissionSchema.safeParse(
34266
+ await c.req.json().catch(() => null)
34267
+ );
34268
+ if (!parsed.success)
34269
+ return c.json(
34270
+ {
34271
+ ok: false,
34272
+ error: "Invalid form submission.",
34273
+ issues: parsed.error.issues
34274
+ },
34275
+ 400
34276
+ );
34277
+ const form = await getPublicAnalyticsForm(c.req.param("publicId"));
34278
+ if (!form) return c.json({ ok: false, error: "Form unavailable." }, 404);
34279
+ if (!await analyticsOwnerHasCurrentEntitlement(Number(form.owner_user_id)))
34280
+ return c.json(
34281
+ { ok: false, error: "This Thorbit X-Ray Pixel form is not currently active." },
34282
+ 402
34283
+ );
34284
+ if (!await isAnalyticsFormPlacementApproved(
34285
+ form.pixel_id,
34286
+ parsed.data.placementUrl
34287
+ ))
34288
+ return c.json({ ok: false, error: "Form hostname is not approved." }, 403);
34289
+ const allowed = new Map(
34290
+ form.fields.map((field) => [field.id, field])
34291
+ );
34292
+ for (const key of Object.keys(parsed.data.data))
34293
+ if (!allowed.has(key))
34294
+ return c.json(
34295
+ { ok: false, error: "Submission contains an unknown field." },
34296
+ 400
34297
+ );
34298
+ for (const field of allowed.values())
34299
+ if (field.required && (parsed.data.data[field.id] === void 0 || parsed.data.data[field.id] === ""))
34300
+ return c.json({ ok: false, error: `${field.label} is required.` }, 400);
34301
+ const user = await getUserById(form.owner_user_id);
34302
+ if (!user)
34303
+ return c.json({ ok: false, error: "Form owner unavailable." }, 503);
34304
+ const data = parsed.data.data;
34305
+ const email = typeof data.email === "string" ? data.email.trim().toLowerCase() : "";
34306
+ const firstName = typeof data.first_name === "string" ? data.first_name.trim() : "";
34307
+ const lastName = typeof data.last_name === "string" ? data.last_name.trim() : "";
34308
+ const fullName = [firstName, lastName].filter(Boolean).join(" ") || (typeof data.name === "string" ? data.name.trim() : "") || email || "Website lead";
34309
+ const identitySeed = email || `${fullName}:${Date.now()}`;
34310
+ const suffix2 = createHash15("sha256").update(identitySeed).digest("hex").slice(0, 12);
34311
+ const path5 = `Leads/person-${suffix2}`;
34312
+ const { key: memoryKey, error: memoryKeyError } = await getOrCreateUserMemoryKey(user);
34313
+ const existing = memoryKey ? await memoryCall("getTool", { vault: "People", path: path5 }, memoryKey) : { ok: false, error: memoryKeyError || "memory_credential_unavailable" };
34314
+ const props = {
34315
+ ...existing.note?.props ?? {},
34316
+ type: "person",
34317
+ status: "active",
34318
+ person_name: fullName,
34319
+ ...firstName ? { first_name: firstName } : {},
34320
+ ...lastName ? { last_name: lastName } : {},
34321
+ ...email ? { email } : {},
34322
+ ...typeof data.phone === "string" && data.phone.trim() ? { phone: data.phone.trim(), text_phone: data.phone.trim() } : {},
34323
+ ...typeof data.company === "string" && data.company.trim() ? { company: data.company.trim() } : {},
34324
+ attribution_site_id: form.site_id,
34325
+ attribution_form_id: form.id,
34326
+ attribution_form_name: form.name,
34327
+ attributed_source: parsed.data.source || "(direct)",
34328
+ attributed_medium: parsed.data.medium || "(none)",
34329
+ attributed_campaign: parsed.data.campaign || "(none)",
34330
+ form_fields: data,
34331
+ source_type: "analytics_form",
34332
+ source_ref: `form:${form.public_id}`,
34333
+ summary: `Website lead from ${form.name}`,
34334
+ embedding_summary: `${fullName} submitted ${form.name}`,
34335
+ related: [],
34336
+ related_vault_notes: [],
34337
+ embed: true,
34338
+ embed_priority: "normal"
34339
+ };
34340
+ const crmPayload = {
34341
+ vault: "People",
34342
+ path: path5,
34343
+ title: fullName,
34344
+ content: existing.note?.content || `# ${fullName}
34345
+
34346
+ ## Identity
34347
+ Website lead captured through ${form.name}.
34348
+
34349
+ ## Relationship
34350
+ Attribution is linked to Site Analytics.
34351
+
34352
+ ## Known Context
34353
+ Submitted the published form.
34354
+
34355
+ ## Interaction History
34356
+ - ${(/* @__PURE__ */ new Date()).toISOString()}: Form submitted.
34357
+
34358
+ ## Open Loops
34359
+ - Review and follow up.`,
34360
+ props,
34361
+ ...existing.note ? { baseRevision: existing.note.revision } : {}
34362
+ };
34363
+ const saved = memoryKey ? await memoryCall(
34364
+ "putTool",
34365
+ crmPayload,
34366
+ memoryKey
34367
+ ) : { ok: false, error: memoryKeyError || "memory_credential_unavailable" };
34368
+ const crmDeliveryStatus = saved.ok ? "delivered" : "pending";
34369
+ const receipt = await recordAnalyticsFormSubmission({
34370
+ form,
34371
+ visitorId: parsed.data.visitorId,
34372
+ sessionId: parsed.data.sessionId,
34373
+ crmPersonRef: `People::${path5}`,
34374
+ crmDeliveryStatus,
34375
+ source: parsed.data.source,
34376
+ medium: parsed.data.medium,
34377
+ campaign: parsed.data.campaign,
34378
+ clickIds: parsed.data.clickIds
34379
+ });
34380
+ const phone = typeof data.phone === "string" ? data.phone.trim() : "";
34381
+ const identityGraph = await linkAnalyticsFormIdentity({
34382
+ submissionId: receipt.submissionId,
34383
+ siteId: form.site_id,
34384
+ crmPersonRef: `People::${path5}`,
34385
+ visitorId: parsed.data.visitorId,
34386
+ sessionId: parsed.data.sessionId,
34387
+ email: email || void 0,
34388
+ phone: phone || void 0,
34389
+ clickIds: parsed.data.clickIds
34390
+ });
34391
+ if (!saved.ok) {
34392
+ await queueAnalyticsFormDelivery({
34393
+ submissionId: receipt.submissionId,
34394
+ ownerUserId: form.owner_user_id,
34395
+ payloadCiphertext: encryptMemoryKey(JSON.stringify(crmPayload)),
34396
+ errorCode: memoryKey ? "memory_write_failed" : "memory_credential_unavailable"
34397
+ });
34398
+ }
34399
+ const conversion = await createAnalyticsConversion({
34400
+ userId: form.owner_user_id,
34401
+ siteId: form.site_id,
34402
+ pixelId: form.pixel_id,
34403
+ sessionId: parsed.data.sessionId,
34404
+ sourceEventId: `form:${receipt.submissionId}`,
34405
+ conversionKind: "form_submitted",
34406
+ occurredAt: (/* @__PURE__ */ new Date()).toISOString()
34407
+ });
34408
+ const match = {
34409
+ ...email ? { emailSha256: createHash15("sha256").update(email).digest("hex") } : {},
34410
+ ...phone ? {
34411
+ phoneSha256: createHash15("sha256").update(phone.replace(/\D/g, "")).digest("hex")
34412
+ } : {}
34413
+ };
34414
+ const activationQueued = await queueAnalyticsActivation({
34415
+ siteId: form.site_id,
34416
+ conversionId: conversion.id,
34417
+ personId: identityGraph.personId,
34418
+ payloadCiphertext: encryptMemoryKey(
34419
+ JSON.stringify({
34420
+ eventId: conversion.id,
34421
+ eventName: "form_submitted",
34422
+ eventTime: (/* @__PURE__ */ new Date()).toISOString(),
34423
+ actionSource: "website",
34424
+ sourceUrl: parsed.data.placementUrl,
34425
+ match,
34426
+ clickIds: parsed.data.clickIds ?? {},
34427
+ attribution: {
34428
+ source: parsed.data.source,
34429
+ medium: parsed.data.medium,
34430
+ campaign: parsed.data.campaign
34431
+ }
34432
+ })
34433
+ )
34434
+ });
34435
+ return c.json(
34436
+ {
34437
+ ok: true,
34438
+ submissionId: receipt.submissionId,
34439
+ crmDeliveryStatus,
34440
+ attribution: {
34441
+ personId: identityGraph.personId,
34442
+ linkedSignals: identityGraph.linkedSignals
34443
+ },
34444
+ activation: { queuedDestinations: activationQueued },
34445
+ message: form.success_message,
34446
+ redirectUrl: form.redirect_url
34447
+ },
34448
+ saved.ok ? 201 : 202
34449
+ );
34450
+ });
34451
+ analyticsApp.get("/l/:shortCode", async (c) => {
34452
+ const destination = await resolveAnalyticsCampaignLink(
34453
+ c.req.param("shortCode"),
34454
+ c.req.header("referer")
34455
+ );
34456
+ if (!destination)
34457
+ return c.json(
34458
+ {
34459
+ ok: false,
34460
+ code: "campaign_link_not_found",
34461
+ error: "Campaign link not found."
34462
+ },
34463
+ 404
34464
+ );
34465
+ c.header("Cache-Control", "no-store");
34466
+ return c.redirect(destination, 302);
34467
+ });
34468
+ analyticsApp.post("/v1/events", async (c) => {
34469
+ const contentLength = Number(c.req.header("content-length") || 0);
34470
+ if (contentLength > 32 * 1024)
34471
+ return c.json(
34472
+ { accepted: 0, rejected: 1, reasonCodes: ["payload_too_large"] },
34473
+ 413
34474
+ );
34475
+ const parsed = IngestionSchema.safeParse(
34476
+ await c.req.json().catch(() => null)
34477
+ );
34478
+ if (!parsed.success) {
34479
+ return c.json(
34480
+ {
34481
+ accepted: 0,
34482
+ rejected: 1,
34483
+ reasonCodes: ["invalid_payload"],
34484
+ issues: parsed.error.issues
34485
+ },
34486
+ 400
34487
+ );
34488
+ }
34489
+ const result = await ingestAnalyticsEvents({
34490
+ publicPixelId: parsed.data.pixelId,
34491
+ origin: c.req.header("origin") ?? null,
34492
+ events: parsed.data.events,
34493
+ geography: {
34494
+ countryCode: c.req.header("x-vercel-ip-country") || c.req.header("cf-ipcountry") || null,
34495
+ regionCode: c.req.header("x-vercel-ip-country-region") || null
34496
+ }
34497
+ });
34498
+ return c.json(result, 202);
34499
+ });
34500
+ analyticsApp.get("/entitlement", auth3, async (c) => {
34501
+ c.header("Cache-Control", "private, no-store");
34502
+ return c.json({
34503
+ entitlement: await resolveAnalyticsEntitlement(Number(c.get("user").id))
34504
+ });
34505
+ });
34506
+ analyticsApp.post("/entitlement/connect", auth3, async (c) => {
34507
+ c.header("Cache-Control", "private, no-store");
34508
+ try {
34509
+ return c.json({
34510
+ connectUrl: await createThorbitConnectUrl(Number(c.get("user").id))
34511
+ });
34512
+ } catch (error) {
34513
+ return c.json(
34514
+ {
34515
+ error: error instanceof Error ? error.message : "Could not start Thorbit sign-in."
34516
+ },
34517
+ 503
34518
+ );
34519
+ }
34520
+ });
34521
+ analyticsApp.post("/entitlement/api-key", auth3, async (c) => {
34522
+ c.header("Cache-Control", "private, no-store");
34523
+ const parsed = ThorbitApiKeySchema.safeParse(
34524
+ await c.req.json().catch(() => null)
34525
+ );
34526
+ if (!parsed.success)
34527
+ return c.json({ error: "Enter a valid Thorbit API key." }, 400);
34528
+ try {
34529
+ return c.json({
34530
+ entitlement: await connectAnalyticsWithThorbitApiKey(
34531
+ Number(c.get("user").id),
34532
+ parsed.data.apiKey
34533
+ )
34534
+ });
34535
+ } catch (error) {
34536
+ return c.json(
34537
+ {
34538
+ error: error instanceof Error ? error.message : "Thorbit could not verify this API key."
34539
+ },
34540
+ 401
34541
+ );
34542
+ }
34543
+ });
34544
+ analyticsApp.post("/entitlement/callback", auth3, async (c) => {
34545
+ c.header("Cache-Control", "private, no-store");
34546
+ const parsed = ThorbitCallbackSchema.safeParse(
34547
+ await c.req.json().catch(() => null)
34548
+ );
34549
+ if (!parsed.success)
34550
+ return c.json({ error: "Invalid Thorbit connection response." }, 400);
34551
+ try {
34552
+ return c.json({
34553
+ entitlement: await consumeThorbitConnectCallback({
34554
+ userId: Number(c.get("user").id),
34555
+ state: parsed.data.state,
34556
+ assertion: parsed.data.assertion
34557
+ })
34558
+ });
34559
+ } catch (error) {
34560
+ return c.json(
34561
+ {
34562
+ error: error instanceof Error ? error.message : "Could not connect the Thorbit account."
34563
+ },
34564
+ 400
34565
+ );
34566
+ }
34567
+ });
34568
+ analyticsApp.delete("/entitlement", auth3, async (c) => {
34569
+ await disconnectAnalyticsEntitlement(Number(c.get("user").id));
34570
+ return c.json({ ok: true });
34571
+ });
34572
+ analyticsApp.use("/v1/conversions", auth3, entitlementGuard);
34573
+ analyticsApp.use("/sites", auth3, entitlementGuard);
34574
+ analyticsApp.use("/sites/*", auth3, entitlementGuard);
34575
+ analyticsApp.post("/v1/conversions", auth3, async (c) => {
34576
+ const parsed = ConversionSchema.safeParse(
34577
+ await c.req.json().catch(() => null)
34578
+ );
34579
+ if (!parsed.success)
34580
+ return c.json(
34581
+ { error: "Invalid conversion.", issues: parsed.error.issues },
34582
+ 400
34583
+ );
34584
+ const conversion = await createAnalyticsConversion({
34585
+ userId: Number(c.get("user").id),
34586
+ ...parsed.data
34587
+ });
34588
+ return c.json({ ok: true, conversion }, conversion.created ? 201 : 200);
34589
+ });
34590
+ analyticsApp.get("/sites", auth3, async (c) => {
34591
+ const sites = await listAnalyticsSites(Number(c.get("user").id));
34592
+ return c.json({ ok: true, sites });
34593
+ });
34594
+ analyticsApp.post("/sites", auth3, async (c) => {
34595
+ const parsed = SiteInputSchema.safeParse(
34596
+ await c.req.json().catch(() => null)
34597
+ );
34598
+ if (!parsed.success)
34599
+ return c.json(
34600
+ { error: "Invalid Site details.", issues: parsed.error.issues },
34601
+ 400
34602
+ );
34603
+ const site = await createAnalyticsSite({
34604
+ userId: Number(c.get("user").id),
34605
+ ...parsed.data
34606
+ });
34607
+ return c.json({ ok: true, site }, 201);
34608
+ });
34609
+ analyticsApp.delete("/sites/:siteId", auth3, async (c) => {
34610
+ await deleteAnalyticsSite({
34611
+ siteId: c.req.param("siteId"),
34612
+ userId: Number(c.get("user").id)
34613
+ });
34614
+ return c.json({ ok: true });
34615
+ });
34616
+ analyticsApp.patch("/sites/:siteId/business-model", auth3, async (c) => {
34617
+ const parsed = BusinessModelSchema.safeParse(
34618
+ await c.req.json().catch(() => null)
34619
+ );
34620
+ if (!parsed.success)
34621
+ return c.json(
34622
+ { ok: false, error: "Choose a supported business model." },
34623
+ 400
34624
+ );
34625
+ await updateAnalyticsBusinessModel({
34626
+ siteId: c.req.param("siteId"),
34627
+ userId: Number(c.get("user").id),
34628
+ ...parsed.data
34629
+ });
34630
+ return c.json({ ok: true, businessModel: parsed.data.businessModel });
34631
+ });
34632
+ analyticsApp.post("/sites/:siteId/ad-spend", auth3, async (c) => {
34633
+ const parsed = AdSpendSchema.safeParse(await c.req.json().catch(() => null));
34634
+ if (!parsed.success)
34635
+ return c.json(
34636
+ {
34637
+ ok: false,
34638
+ error: "Invalid ad-spend rows.",
34639
+ issues: parsed.error.issues
34640
+ },
34641
+ 400
34642
+ );
34643
+ const result = await upsertAnalyticsAdSpend({
34644
+ siteId: c.req.param("siteId"),
34645
+ userId: Number(c.get("user").id),
34646
+ ...parsed.data
34647
+ });
34648
+ return c.json({ ok: true, result }, 202);
34649
+ });
34650
+ analyticsApp.get("/sites/:siteId/business-metrics", auth3, async (c) => {
34651
+ const url = new URL(c.req.url);
34652
+ const filters = reportFilters(url);
34653
+ validateReportWindow(filters);
34654
+ const data = await analyticsBusinessMetrics(
34655
+ c.req.param("siteId"),
34656
+ Number(c.get("user").id),
34657
+ filters.start,
34658
+ filters.end
34659
+ );
34660
+ return c.json({ ok: true, data, filters });
34661
+ });
34662
+ analyticsApp.get("/sites/:siteId/forecast", auth3, async (c) => {
34663
+ const horizon = Math.min(
34664
+ Math.max(Number(c.req.query("horizonMonths") || 12), 1),
34665
+ 36
34666
+ );
34667
+ const data = await analyticsForecast(
34668
+ c.req.param("siteId"),
34669
+ Number(c.get("user").id),
34670
+ horizon
34671
+ );
34672
+ return c.json({ ok: true, data });
34673
+ });
34674
+ analyticsApp.get("/sites/:siteId/pixels", auth3, async (c) => {
34675
+ const pixels = await listAnalyticsPixels(
34676
+ c.req.param("siteId"),
34677
+ Number(c.get("user").id)
34678
+ );
34679
+ return c.json({
34680
+ ok: true,
34681
+ pixels: pixels.map((pixel) => ({
34682
+ ...pixel,
34683
+ snippet: pixelSnippet(pixel.public_id)
34684
+ }))
34685
+ });
34686
+ });
34687
+ analyticsApp.post("/sites/:siteId/pixels", auth3, async (c) => {
34688
+ const parsed = PixelInputSchema.safeParse(
34689
+ await c.req.json().catch(() => null)
34690
+ );
34691
+ if (!parsed.success)
34692
+ return c.json(
34693
+ { error: "Invalid Pixel details.", issues: parsed.error.issues },
34694
+ 400
34695
+ );
34696
+ const pixel = await createAnalyticsPixel({
34697
+ siteId: c.req.param("siteId"),
34698
+ userId: Number(c.get("user").id),
34699
+ ...parsed.data
34700
+ });
34701
+ return c.json(
34702
+ { ok: true, pixel: { ...pixel, snippet: pixelSnippet(pixel.public_id) } },
34703
+ 201
34704
+ );
34705
+ });
34706
+ analyticsApp.get("/sites/:siteId/campaign-links", auth3, async (c) => {
34707
+ const url = new URL(c.req.url);
34708
+ const data = await listAnalyticsCampaignLinks(
34709
+ c.req.param("siteId"),
34710
+ Number(c.get("user").id),
34711
+ reportPage(url)
34712
+ );
34713
+ return c.json({
34714
+ ok: true,
34715
+ data: {
34716
+ ...data,
34717
+ links: data.links.map((link) => ({
34718
+ ...link,
34719
+ short_url: `${url.origin}/analytics/l/${link.short_code}`
34720
+ }))
34721
+ }
34722
+ });
34723
+ });
34724
+ analyticsApp.get("/sites/:siteId/forms", auth3, async (c) => {
34725
+ const url = new URL(c.req.url);
34726
+ const data = await listAnalyticsForms(
34727
+ c.req.param("siteId"),
34728
+ Number(c.get("user").id),
34729
+ reportPage(url)
34730
+ );
34731
+ return c.json({
34732
+ ok: true,
34733
+ data: {
34734
+ ...data,
34735
+ forms: data.forms.map((form) => ({
34736
+ ...form,
34737
+ snippet: formSnippet(form.public_id),
34738
+ embed_url: `${url.origin}/analytics/forms/${form.public_id}/embed`
34739
+ }))
34740
+ }
34741
+ });
34742
+ });
34743
+ analyticsApp.post("/sites/:siteId/forms", auth3, async (c) => {
34744
+ const parsed = FormInputSchema.safeParse(
34745
+ await c.req.json().catch(() => null)
34746
+ );
34747
+ if (!parsed.success)
34748
+ return c.json(
34749
+ { ok: false, error: "Invalid form.", issues: parsed.error.issues },
34750
+ 400
34751
+ );
34752
+ const form = await createAnalyticsForm({
34753
+ siteId: c.req.param("siteId"),
34754
+ userId: Number(c.get("user").id),
34755
+ ...parsed.data
34756
+ });
34757
+ return c.json(
34758
+ {
34759
+ ok: true,
34760
+ form: {
34761
+ ...form,
34762
+ snippet: formSnippet(form.public_id),
34763
+ embed_url: `${new URL(c.req.url).origin}/analytics/forms/${form.public_id}/embed`
34764
+ }
34765
+ },
34766
+ 201
34767
+ );
34768
+ });
34769
+ analyticsApp.get("/sites/:siteId/crm-imports", auth3, async (c) => {
34770
+ const data = await listAnalyticsCrmImports(
34771
+ c.req.param("siteId"),
34772
+ Number(c.get("user").id),
34773
+ reportPage(new URL(c.req.url))
34774
+ );
34775
+ return c.json({ ok: true, data });
34776
+ });
34777
+ analyticsApp.get("/sites/:siteId/people", auth3, async (c) => {
34778
+ const data = await listAnalyticsPeople(
34779
+ c.req.param("siteId"),
34780
+ Number(c.get("user").id),
34781
+ reportPage(new URL(c.req.url))
34782
+ );
34783
+ return c.json({ ok: true, data });
34784
+ });
34785
+ analyticsApp.get("/sites/:siteId/people/:personId/journey", auth3, async (c) => {
34786
+ const data = await getAnalyticsPersonJourney(
34787
+ c.req.param("siteId"),
34788
+ c.req.param("personId"),
34789
+ Number(c.get("user").id),
34790
+ reportPage(new URL(c.req.url))
34791
+ );
34792
+ return c.json({ ok: true, data });
34793
+ });
34794
+ analyticsApp.post("/sites/:siteId/crm-imports", auth3, async (c) => {
34795
+ const parsed = CrmImportSchema.safeParse(
34796
+ await c.req.json().catch(() => null)
34797
+ );
34798
+ if (!parsed.success)
34799
+ return c.json(
34800
+ { ok: false, error: "Invalid CRM import.", issues: parsed.error.issues },
34801
+ 400
34802
+ );
34803
+ const csv = Papa2.parse(parsed.data.csv, {
34804
+ header: true,
34805
+ skipEmptyLines: "greedy",
34806
+ transformHeader: (header) => header.trim()
34807
+ });
34808
+ if (csv.errors.length)
34809
+ return c.json(
34810
+ {
34811
+ ok: false,
34812
+ error: "CSV could not be parsed.",
34813
+ issues: csv.errors.slice(0, 10)
34814
+ },
34815
+ 400
34816
+ );
34817
+ if (csv.data.length > 2500)
34818
+ return c.json(
34819
+ { ok: false, error: "Import files are limited to 2,500 rows per batch." },
34820
+ 400
34821
+ );
34822
+ const pick = (row, header) => header ? String(row[header] ?? "").trim() : "";
34823
+ const queued = [];
34824
+ const identities = [];
34825
+ let rejectedCount = 0;
34826
+ for (const [index, row] of csv.data.entries()) {
34827
+ const email = pick(row, parsed.data.mapping.email).toLowerCase();
34828
+ const firstName = pick(row, parsed.data.mapping.firstName);
34829
+ const lastName = pick(row, parsed.data.mapping.lastName);
34830
+ const explicitName = pick(row, parsed.data.mapping.name);
34831
+ const fullName = explicitName || [firstName, lastName].filter(Boolean).join(" ");
34832
+ const phone = pick(row, parsed.data.mapping.phone);
34833
+ const company = pick(row, parsed.data.mapping.company);
34834
+ const externalId = pick(row, parsed.data.mapping.externalId);
34835
+ if (!email && !phone && !externalId && !fullName) {
34836
+ rejectedCount += 1;
34837
+ continue;
34838
+ }
34839
+ const digest2 = createHash15("sha256").update(
34840
+ `${parsed.data.sourceSystem}:${externalId || email || phone || `${fullName}:${index}`}`
34841
+ ).digest("hex").slice(0, 16);
34842
+ const path5 = `Leads/person-${digest2}`;
34843
+ const crmPersonRef = `People::${path5}`;
34844
+ const props = {
34845
+ type: "person",
34846
+ status: "active",
34847
+ person_name: fullName || email || phone || "Imported contact",
34848
+ ...firstName ? { first_name: firstName } : {},
34849
+ ...lastName ? { last_name: lastName } : {},
34850
+ ...email ? { email } : {},
34851
+ ...phone ? { phone, text_phone: phone } : {},
34852
+ ...company ? { company } : {},
34853
+ ...externalId ? { external_id: externalId } : {},
34854
+ import_fields: row,
34855
+ source_type: "crm_csv_import",
34856
+ source_ref: `${parsed.data.sourceSystem}:${digest2}`,
34857
+ summary: `Imported from ${parsed.data.sourceSystem}`,
34858
+ embedding_summary: `${fullName || "Contact"} imported from ${parsed.data.sourceSystem}`,
34859
+ related: [],
34860
+ related_vault_notes: [],
34861
+ embed: true,
34862
+ embed_priority: "normal"
34863
+ };
34864
+ const title = fullName || email || phone || "Imported contact";
34865
+ const payload = {
34866
+ vault: "People",
34867
+ path: path5,
34868
+ title,
34869
+ content: `# ${title}
34870
+
34871
+ ## Identity
34872
+ Imported from ${parsed.data.sourceSystem}.
34873
+
34874
+ ## Relationship
34875
+ Review and qualify this contact.
34876
+
34877
+ ## Known Context
34878
+ Imported through the Site Analytics CRM workflow.
34879
+
34880
+ ## Interaction History
34881
+ - ${(/* @__PURE__ */ new Date()).toISOString()}: Imported from ${parsed.data.filename}.
34882
+
34883
+ ## Open Loops
34884
+ - Verify attribution and follow-up status.`,
34885
+ props
34886
+ };
34887
+ queued.push({
34888
+ crmPersonRef,
34889
+ payloadCiphertext: encryptMemoryKey(JSON.stringify(payload))
34890
+ });
34891
+ identities.push({
34892
+ crmPersonRef,
34893
+ email: email || void 0,
34894
+ phone: phone || void 0
34895
+ });
34896
+ }
34897
+ const imported = await createAnalyticsCrmImport({
34898
+ siteId: c.req.param("siteId"),
34899
+ userId: Number(c.get("user").id),
34900
+ sourceSystem: parsed.data.sourceSystem,
34901
+ filename: parsed.data.filename,
34902
+ rows: queued,
34903
+ rejectedCount
34904
+ });
34905
+ for (const identity of identities) {
34906
+ await linkAnalyticsFormIdentity({
34907
+ siteId: c.req.param("siteId"),
34908
+ ...identity
34909
+ });
34910
+ }
34911
+ return c.json({ ok: true, import: imported }, 202);
34912
+ });
34913
+ analyticsApp.get("/sites/:siteId/activation-destinations", auth3, async (c) => {
34914
+ const destinations = await listAnalyticsActivationDestinations(
34915
+ c.req.param("siteId"),
34916
+ Number(c.get("user").id)
34917
+ );
34918
+ return c.json({ ok: true, destinations });
34919
+ });
34920
+ analyticsApp.post("/sites/:siteId/activation-destinations", auth3, async (c) => {
34921
+ const parsed = ActivationDestinationSchema.safeParse(
34922
+ await c.req.json().catch(() => null)
34923
+ );
34924
+ if (!parsed.success)
34925
+ return c.json(
34926
+ {
34927
+ ok: false,
34928
+ error: "Invalid activation destination.",
34929
+ issues: parsed.error.issues
34930
+ },
34931
+ 400
34932
+ );
34933
+ const destination = await createAnalyticsActivationDestination({
34934
+ siteId: c.req.param("siteId"),
34935
+ userId: Number(c.get("user").id),
34936
+ ...parsed.data
34937
+ });
34938
+ return c.json({ ok: true, destination }, 201);
34939
+ });
34940
+ analyticsApp.delete(
34941
+ "/sites/:siteId/activation-destinations/:destinationId",
34942
+ auth3,
34943
+ async (c) => {
34944
+ await archiveAnalyticsActivationDestination({
34945
+ siteId: c.req.param("siteId"),
34946
+ destinationId: c.req.param("destinationId"),
34947
+ userId: Number(c.get("user").id)
34948
+ });
34949
+ return c.json({ ok: true });
34950
+ }
34951
+ );
34952
+ analyticsApp.post("/sites/:siteId/campaign-links", auth3, async (c) => {
34953
+ const parsed = CampaignLinkSchema.safeParse(
34954
+ await c.req.json().catch(() => null)
34955
+ );
34956
+ if (!parsed.success)
34957
+ return c.json(
34958
+ {
34959
+ ok: false,
34960
+ error: "Invalid campaign link.",
34961
+ issues: parsed.error.issues
34962
+ },
34963
+ 400
34964
+ );
34965
+ const link = await createAnalyticsCampaignLink({
34966
+ siteId: c.req.param("siteId"),
34967
+ userId: Number(c.get("user").id),
34968
+ ...parsed.data
34969
+ });
34970
+ return c.json(
34971
+ {
34972
+ ok: true,
34973
+ link: {
34974
+ ...link,
34975
+ short_url: `${new URL(c.req.url).origin}/analytics/l/${link.short_code}`
34976
+ }
34977
+ },
34978
+ 201
34979
+ );
34980
+ });
34981
+ analyticsApp.delete(
34982
+ "/sites/:siteId/campaign-links/:linkId",
34983
+ auth3,
34984
+ async (c) => {
34985
+ await archiveAnalyticsCampaignLink({
34986
+ siteId: c.req.param("siteId"),
34987
+ linkId: c.req.param("linkId"),
34988
+ userId: Number(c.get("user").id)
34989
+ });
34990
+ return c.json({ ok: true });
34991
+ }
34992
+ );
34993
+ analyticsApp.patch("/sites/:siteId/pixels/:pixelId", auth3, async (c) => {
34994
+ const parsed = PixelUpdateSchema.safeParse(
34995
+ await c.req.json().catch(() => null)
34996
+ );
34997
+ if (!parsed.success)
34998
+ return c.json(
34999
+ { error: "Invalid Pixel changes.", issues: parsed.error.issues },
35000
+ 400
35001
+ );
35002
+ await updateAnalyticsPixel({
35003
+ siteId: c.req.param("siteId"),
35004
+ pixelId: c.req.param("pixelId"),
35005
+ userId: Number(c.get("user").id),
35006
+ ...parsed.data
35007
+ });
35008
+ return c.json({ ok: true });
35009
+ });
35010
+ analyticsApp.post(
35011
+ "/sites/:siteId/pixels/:pixelId/domains/:hostname/approve",
35012
+ auth3,
35013
+ async (c) => {
35014
+ await setAnalyticsPixelDomainState({
35015
+ siteId: c.req.param("siteId"),
35016
+ pixelId: c.req.param("pixelId"),
35017
+ userId: Number(c.get("user").id),
35018
+ hostname: c.req.param("hostname"),
35019
+ state: "approved"
35020
+ });
35021
+ return c.json({ ok: true });
35022
+ }
35023
+ );
35024
+ analyticsApp.post(
35025
+ "/sites/:siteId/pixels/:pixelId/domains/:hostname/reject",
35026
+ auth3,
35027
+ async (c) => {
35028
+ await setAnalyticsPixelDomainState({
35029
+ siteId: c.req.param("siteId"),
35030
+ pixelId: c.req.param("pixelId"),
35031
+ userId: Number(c.get("user").id),
35032
+ hostname: c.req.param("hostname"),
35033
+ state: "rejected"
35034
+ });
35035
+ return c.json({ ok: true });
35036
+ }
35037
+ );
35038
+ for (const [path5, load] of [
35039
+ ["/sites/:siteId/overview", analyticsOverview],
35040
+ ["/sites/:siteId/timeseries", analyticsTimeseries],
35041
+ ["/sites/:siteId/acquisition", analyticsAcquisition],
35042
+ ["/sites/:siteId/content", analyticsContent],
35043
+ ["/sites/:siteId/events", analyticsEventCounts]
35044
+ ]) {
35045
+ analyticsApp.get(path5, auth3, async (c) => {
35046
+ const url = new URL(c.req.url);
35047
+ const filters = reportFilters(url);
35048
+ validateReportWindow(filters);
35049
+ const paginated = path5.endsWith("/acquisition") || path5.endsWith("/content") || path5.endsWith("/events");
35050
+ const data = await load(
35051
+ c.req.param("siteId"),
35052
+ Number(c.get("user").id),
35053
+ filters,
35054
+ paginated ? reportPage(url) : void 0
35055
+ );
35056
+ return c.json({ ok: true, data, filters });
35057
+ });
35058
+ }
35059
+ analyticsApp.get("/sites/:siteId/dashboard", auth3, async (c) => {
35060
+ const url = new URL(c.req.url);
35061
+ const filters = reportFilters(url);
35062
+ validateReportWindow(filters);
35063
+ const siteId = c.req.param("siteId");
35064
+ const userId = Number(c.get("user").id);
35065
+ const [
35066
+ overview,
35067
+ timeseries,
35068
+ acquisition,
35069
+ devices,
35070
+ activity,
35071
+ geography,
35072
+ content,
35073
+ conversions
35074
+ ] = await Promise.all([
35075
+ analyticsOverview(siteId, userId, filters),
35076
+ analyticsTimeseries(siteId, userId, filters),
35077
+ analyticsAcquisition(siteId, userId, filters, { limit: 25 }),
35078
+ analyticsDimensions(siteId, userId, filters, "device", { limit: 10 }),
35079
+ analyticsDimensions(siteId, userId, filters, "weekday_hour", {
35080
+ limit: 100
35081
+ }),
35082
+ analyticsDimensions(siteId, userId, filters, "region", { limit: 60 }),
35083
+ analyticsContent(siteId, userId, filters, { limit: 8 }),
35084
+ analyticsConversions(siteId, userId, filters, { limit: 8 })
35085
+ ]);
35086
+ let comparison = null;
35087
+ if (url.searchParams.get("compare") === "1") {
35088
+ const start = Date.parse(filters.start);
35089
+ const end = Date.parse(filters.end);
35090
+ const duration = end - start;
35091
+ const previousFilters = {
35092
+ ...filters,
35093
+ start: new Date(start - duration).toISOString(),
35094
+ end: new Date(start).toISOString()
35095
+ };
35096
+ const [previousOverview, previousTimeseries] = await Promise.all([
35097
+ analyticsOverview(siteId, userId, previousFilters),
35098
+ analyticsTimeseries(siteId, userId, previousFilters)
35099
+ ]);
35100
+ comparison = { overview: previousOverview, timeseries: previousTimeseries };
35101
+ }
35102
+ return c.json({
35103
+ ok: true,
35104
+ data: {
35105
+ overview,
35106
+ timeseries,
35107
+ acquisition,
35108
+ devices,
35109
+ activity,
35110
+ geography,
35111
+ content,
35112
+ conversions,
35113
+ comparison
35114
+ },
35115
+ filters
35116
+ });
35117
+ });
35118
+ analyticsApp.get("/sites/:siteId/dimensions", auth3, async (c) => {
35119
+ const url = new URL(c.req.url);
35120
+ const filters = reportFilters(url);
35121
+ validateReportWindow(filters);
35122
+ const dimension = url.searchParams.get("dimension") || "device";
35123
+ if (!["device", "source", "country", "region", "weekday_hour"].includes(
35124
+ dimension
35125
+ )) {
35126
+ return c.json(
35127
+ {
35128
+ ok: false,
35129
+ code: "invalid_dimension",
35130
+ error: "Choose a supported dimension."
35131
+ },
35132
+ 400
35133
+ );
35134
+ }
35135
+ const data = await analyticsDimensions(
35136
+ c.req.param("siteId"),
35137
+ Number(c.get("user").id),
35138
+ filters,
35139
+ dimension,
35140
+ reportPage(url)
35141
+ );
35142
+ return c.json({ ok: true, data, filters });
35143
+ });
35144
+ analyticsApp.get("/sites/:siteId/conversions", auth3, async (c) => {
35145
+ const url = new URL(c.req.url);
35146
+ const filters = reportFilters(url);
35147
+ validateReportWindow(filters);
35148
+ const data = await analyticsConversions(
35149
+ c.req.param("siteId"),
35150
+ Number(c.get("user").id),
35151
+ filters,
35152
+ reportPage(url)
35153
+ );
35154
+ return c.json({ ok: true, data, filters });
35155
+ });
35156
+ analyticsApp.get("/sites/:siteId/paths", auth3, async (c) => {
35157
+ const url = new URL(c.req.url);
35158
+ const filters = reportFilters(url);
35159
+ validateReportWindow(filters);
35160
+ const data = await analyticsPaths(
35161
+ c.req.param("siteId"),
35162
+ Number(c.get("user").id),
35163
+ filters,
35164
+ reportPage(url)
35165
+ );
35166
+ return c.json({ ok: true, data, filters });
35167
+ });
35168
+ analyticsApp.get("/sites/:siteId/channel-breakdown", auth3, async (c) => {
35169
+ const filters = reportFilters(new URL(c.req.url));
35170
+ const data = await analyticsChannelBreakdown(
35171
+ c.req.param("siteId"),
35172
+ Number(c.get("user").id),
35173
+ filters
35174
+ );
35175
+ return c.json({ ok: true, data });
35176
+ });
35177
+ analyticsApp.get("/sites/:siteId/health", auth3, async (c) => {
35178
+ const health = await analyticsHealth(
35179
+ c.req.param("siteId"),
35180
+ Number(c.get("user").id)
35181
+ );
35182
+ return c.json({ ok: true, health }, 200, { "Cache-Control": "no-store" });
35183
+ });
35184
+ analyticsApp.post("/sites/:siteId/exports", auth3, async (c) => {
35185
+ const parsed = ExportSchema.safeParse(await c.req.json().catch(() => ({})));
35186
+ if (!parsed.success)
35187
+ return c.json(
35188
+ {
35189
+ ok: false,
35190
+ error: "Invalid export request.",
35191
+ issues: parsed.error.issues
35192
+ },
35193
+ 400
35194
+ );
35195
+ const filters = reportFilters(new URL(c.req.url));
35196
+ validateReportWindow(filters);
35197
+ const artifact = await createAnalyticsExport({
35198
+ siteId: c.req.param("siteId"),
35199
+ userId: Number(c.get("user").id),
35200
+ filters,
35201
+ ...parsed.data,
35202
+ idempotencyKey: c.req.header("idempotency-key") || parsed.data.idempotencyKey
35203
+ });
35204
+ return c.json({ ok: true, artifact }, 201);
35205
+ });
35206
+ analyticsApp.onError((error, c) => {
35207
+ if (error instanceof AnalyticsRepositoryError) {
35208
+ return c.json(
35209
+ { ok: false, code: error.code, error: error.message },
35210
+ error.status
35211
+ );
35212
+ }
35213
+ console.error(
35214
+ "[analytics]",
35215
+ error instanceof Error ? error.message : String(error)
35216
+ );
35217
+ return c.json(
35218
+ {
35219
+ ok: false,
35220
+ code: "analytics_internal_error",
35221
+ error: "Site Analytics could not complete the request."
35222
+ },
35223
+ 500
35224
+ );
35225
+ });
35226
+
35227
+ // src/api/analytics-delivery.ts
35228
+ function safeDeliveryError(error) {
35229
+ if (!error) return "memory_write_failed";
35230
+ if (error.includes("configured") || error.includes("credential")) return "memory_credential_unavailable";
35231
+ if (error.includes("401") || error.includes("403")) return "memory_authorization_failed";
35232
+ if (error.includes("429")) return "memory_rate_limited";
35233
+ return "memory_write_failed";
35234
+ }
35235
+ async function deliverPendingAnalyticsForms(limit = 10) {
35236
+ const jobs = await claimAnalyticsFormDeliveryJobs(limit);
35237
+ let delivered = 0;
35238
+ let deferred = 0;
35239
+ for (const job of jobs) {
35240
+ const payloadText = decryptMemoryKey(job.payloadCiphertext);
35241
+ const user = await getUserById(job.ownerUserId);
35242
+ if (!payloadText || !user) {
35243
+ await deferAnalyticsFormDelivery(job.id, job.attempts, payloadText ? "owner_not_found" : "payload_unreadable");
35244
+ deferred += 1;
35245
+ continue;
35246
+ }
35247
+ let payload;
35248
+ try {
35249
+ payload = JSON.parse(payloadText);
35250
+ } catch {
35251
+ await deferAnalyticsFormDelivery(job.id, job.attempts, "payload_invalid");
35252
+ deferred += 1;
35253
+ continue;
35254
+ }
35255
+ const { key, error } = await getOrCreateUserMemoryKey(user);
35256
+ const result = key ? await memoryCall("putTool", payload, key) : { ok: false, error };
35257
+ if (result.ok) {
35258
+ await completeAnalyticsFormDelivery(job.id, job.submissionId);
35259
+ delivered += 1;
35260
+ } else {
35261
+ await deferAnalyticsFormDelivery(job.id, job.attempts, safeDeliveryError(result.error));
35262
+ deferred += 1;
35263
+ }
35264
+ }
35265
+ return { claimed: jobs.length, delivered, deferred };
35266
+ }
35267
+
35268
+ // src/api/scheduled-result-routes.ts
35269
+ import { Hono as Hono33 } from "hono";
35270
+ import { z as z35 } from "zod";
33564
35271
 
33565
35272
  // src/api/scheduled-artifact-owner.ts
33566
- import { createHash as createHash14 } from "crypto";
35273
+ import { createHash as createHash16 } from "crypto";
33567
35274
  function scheduledArtifactOwnerIdForApiKey(apiKey) {
33568
- return createHash14("sha256").update(apiKey).digest("hex").slice(0, 24);
35275
+ return createHash16("sha256").update(apiKey).digest("hex").slice(0, 24);
33569
35276
  }
33570
35277
 
33571
35278
  // src/api/scheduled-run-view-links.ts
33572
- import { createHash as createHash15, randomBytes as randomBytes3, randomUUID as randomUUID17 } from "crypto";
35279
+ import { createHash as createHash17, randomBytes as randomBytes4, randomUUID as randomUUID17 } from "crypto";
33573
35280
  var schemaReady3 = false;
33574
35281
  async function ensureScheduledRunViewLinksSchema() {
33575
35282
  if (schemaReady3) return;
@@ -33582,13 +35289,36 @@ async function ensureScheduledRunViewLinksSchema() {
33582
35289
  artifact_id TEXT NOT NULL,
33583
35290
  title TEXT NOT NULL,
33584
35291
  summary TEXT NOT NULL,
33585
- renderer_key TEXT NOT NULL CHECK (renderer_key = 'editorial_reading_room_v1'),
35292
+ renderer_key TEXT NOT NULL CHECK (renderer_key IN ('editorial_reading_room_v1','personal_authority_v1','newsroom_publisher_v1')),
33586
35293
  token_hash TEXT NOT NULL UNIQUE,
33587
35294
  expires_at TEXT NOT NULL,
33588
35295
  revoked_at TEXT,
33589
35296
  created_at TEXT NOT NULL
33590
35297
  )
33591
35298
  `);
35299
+ const schema = await db.execute({
35300
+ sql: `SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'scheduled_run_view_links' LIMIT 1`,
35301
+ args: []
35302
+ });
35303
+ const createSql = String(schema.rows[0]?.sql ?? "");
35304
+ if (!createSql.includes("newsroom_publisher_v1")) {
35305
+ await db.batch([
35306
+ `DROP TABLE IF EXISTS scheduled_run_view_links_v3`,
35307
+ `CREATE TABLE scheduled_run_view_links_v3 (
35308
+ share_id TEXT PRIMARY KEY, owner_id TEXT NOT NULL, run_id TEXT NOT NULL,
35309
+ artifact_id TEXT NOT NULL, title TEXT NOT NULL, summary TEXT NOT NULL,
35310
+ renderer_key TEXT NOT NULL CHECK (renderer_key IN ('editorial_reading_room_v1','personal_authority_v1','newsroom_publisher_v1')),
35311
+ token_hash TEXT NOT NULL UNIQUE, expires_at TEXT NOT NULL, revoked_at TEXT,
35312
+ created_at TEXT NOT NULL
35313
+ )`,
35314
+ `INSERT INTO scheduled_run_view_links_v3
35315
+ SELECT share_id, owner_id, run_id, artifact_id, title, summary, renderer_key,
35316
+ token_hash, expires_at, revoked_at, created_at
35317
+ FROM scheduled_run_view_links`,
35318
+ `DROP TABLE scheduled_run_view_links`,
35319
+ `ALTER TABLE scheduled_run_view_links_v3 RENAME TO scheduled_run_view_links`
35320
+ ], "write");
35321
+ }
33592
35322
  await db.execute(`
33593
35323
  CREATE INDEX IF NOT EXISTS scheduled_run_view_links_owner_run
33594
35324
  ON scheduled_run_view_links(owner_id, run_id, created_at DESC)
@@ -33600,7 +35330,7 @@ async function ensureScheduledRunViewLinksSchema() {
33600
35330
  schemaReady3 = true;
33601
35331
  }
33602
35332
  function tokenHash2(token4) {
33603
- return createHash15("sha256").update(token4).digest("hex");
35333
+ return createHash17("sha256").update(token4).digest("hex");
33604
35334
  }
33605
35335
  function mapRow(row) {
33606
35336
  return {
@@ -33610,7 +35340,7 @@ function mapRow(row) {
33610
35340
  artifactId: String(row.artifact_id),
33611
35341
  title: String(row.title),
33612
35342
  summary: String(row.summary),
33613
- rendererKey: "editorial_reading_room_v1",
35343
+ rendererKey: row.renderer_key === "newsroom_publisher_v1" ? "newsroom_publisher_v1" : row.renderer_key === "personal_authority_v1" ? "personal_authority_v1" : "editorial_reading_room_v1",
33614
35344
  tokenHash: String(row.token_hash),
33615
35345
  expiresAt: String(row.expires_at),
33616
35346
  revokedAt: row.revoked_at == null ? null : String(row.revoked_at),
@@ -33620,7 +35350,7 @@ function mapRow(row) {
33620
35350
  async function createScheduledRunViewLink(input) {
33621
35351
  await ensureScheduledRunViewLinksSchema();
33622
35352
  const now = input.now ?? /* @__PURE__ */ new Date();
33623
- const token4 = randomBytes3(32).toString("base64url");
35353
+ const token4 = randomBytes4(32).toString("base64url");
33624
35354
  const record = {
33625
35355
  shareId: randomUUID17(),
33626
35356
  ownerId: input.ownerId,
@@ -33686,12 +35416,262 @@ async function revokeScheduledRunViewLink(ownerId, runId, shareId, now = /* @__P
33686
35416
  return result.rowsAffected > 0;
33687
35417
  }
33688
35418
 
35419
+ // src/personal-authority/render.ts
35420
+ import { createHash as createHash18 } from "crypto";
35421
+ function escapeHtml7(value) {
35422
+ return value.replace(/[&<>"']/g, (character) => ({
35423
+ "&": "&amp;",
35424
+ "<": "&lt;",
35425
+ ">": "&gt;",
35426
+ '"': "&quot;",
35427
+ "'": "&#39;"
35428
+ })[character] ?? character);
35429
+ }
35430
+ function safeHref(value) {
35431
+ const trimmed = value.trim();
35432
+ return /^(?:https?:|mailto:|tel:|#|\/)/i.test(trimmed) ? escapeHtml7(trimmed) : "#";
35433
+ }
35434
+ function initials(name) {
35435
+ return name.split(/\s+/).filter(Boolean).slice(0, 2).map((part) => part[0]?.toUpperCase()).join("") || "PA";
35436
+ }
35437
+ function action(action2, secondary = false) {
35438
+ return `<a class="button${secondary ? " button--secondary" : ""}" href="${safeHref(action2.href)}">${escapeHtml7(action2.label)} <span aria-hidden="true">\u2197</span></a>`;
35439
+ }
35440
+ function section(enabled, html) {
35441
+ return enabled ? html : "";
35442
+ }
35443
+ function renderPersonalAuthorityV1(input, config, generatedAt) {
35444
+ const flags = config.featureFlags;
35445
+ const brand = config.brandName || input.name;
35446
+ const generatedAtIso = generatedAt.toISOString();
35447
+ const social = flags.socialLinks && input.socialLinks.length > 0 ? `<nav class="social" aria-label="Professional profiles">${input.socialLinks.map((link) => `<a href="${safeHref(link.href)}">${escapeHtml7(link.label)}</a>`).join("")}</nav>` : "";
35448
+ const navigation = flags.navigation ? `<button class="menu" type="button" aria-expanded="false" aria-controls="nav">Menu</button><nav id="nav" class="nav" aria-label="Primary navigation"><a href="#about">About</a><a href="#services">Work</a><a href="#proof">Proof</a><a href="#contact">Contact</a></nav>` : "";
35449
+ const authority = input.authority;
35450
+ const consultation = input.consultation;
35451
+ const learning = input.learning;
35452
+ const services = input.services;
35453
+ const proof = input.proof;
35454
+ const finalCta = input.finalCta;
35455
+ const html = `<!doctype html>
35456
+ <html lang="en" data-theme="${config.theme}">
35457
+ <head>
35458
+ <meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
35459
+ <meta name="description" content="${escapeHtml7(input.summary)}"><title>${escapeHtml7(input.name)} \u2014 ${escapeHtml7(input.professionalTitle)}</title>
35460
+ <style>
35461
+ :root{--blue:#2f66eb;--blue-soft:#dbe6ff;--ink:#050708;--paper:#f7f8fb;--white:#fff;--gray:#9ca5b5;--line:#27303b;--serif:Georgia,'Times New Roman',serif;--sans:Arial,Helvetica,sans-serif}*{box-sizing:border-box}html{scroll-behavior:smooth}body{margin:0;background:var(--ink);color:var(--white);font-family:var(--sans);line-height:1.55}a{color:inherit}.skip{position:absolute;left:-9999px}.skip:focus{left:16px;top:16px;z-index:9;background:#fff;color:#000;padding:10px}.header{height:88px;display:flex;align-items:center;justify-content:space-between;padding:0 clamp(20px,4vw,72px);border-bottom:1px solid var(--line);position:relative;z-index:3;background:rgba(5,7,8,.96)}.brand{display:flex;align-items:center;gap:12px;text-decoration:none;font-family:var(--serif);font-size:24px}.mark{width:40px;height:40px;border-radius:50%;display:grid;place-items:center;background:var(--blue);font-family:var(--sans);font-weight:700}.nav{display:flex;align-items:center;gap:28px}.nav a{text-decoration:none;color:#c8ced8;font-size:14px}.nav a:hover{color:#fff}.menu{display:none;background:transparent;color:#fff;border:1px solid #5b6471;padding:10px 14px}.hero{display:grid;grid-template-columns:minmax(0,.9fr) minmax(0,1.25fr);min-height:760px;border-bottom:3px solid var(--blue)}.portrait{display:grid;place-items:end center;background:radial-gradient(circle at 50% 38%,#273243 0,#11161d 38%,#060708 70%);overflow:hidden}.portrait-card{width:min(82%,560px);aspect-ratio:4/5;display:grid;place-items:center;align-self:end;border:1px solid #263142;border-bottom:0;background:linear-gradient(145deg,#252d38,#0b0d10 58%);font-family:var(--serif);font-size:clamp(80px,13vw,180px);color:#d9e4ff;text-shadow:0 0 42px rgba(47,102,235,.5)}.hero-copy{padding:clamp(44px,7vw,110px) clamp(24px,6vw,100px);display:flex;flex-direction:column;justify-content:center;background:linear-gradient(135deg,#080a0d,#101318)}h1,h2,h3,p{margin-top:0}.hero h1{font-family:var(--serif);font-size:clamp(58px,8vw,116px);line-height:.92;font-weight:400;color:var(--blue);margin-bottom:24px}.role{font-family:var(--serif);font-size:clamp(28px,3.8vw,52px);line-height:1.05;border-left:5px solid var(--blue);padding-left:22px;max-width:820px}.summary{color:#c6cbd3;font-size:clamp(17px,1.5vw,22px);max-width:760px}.actions{display:flex;gap:12px;flex-wrap:wrap;margin:24px 0}.button{display:inline-flex;align-items:center;justify-content:space-between;gap:28px;min-height:58px;padding:0 26px;background:var(--blue);border:1px solid var(--blue);text-decoration:none;font-weight:700}.button--secondary{background:transparent;border-color:#5c6572}.social{display:flex;gap:10px;flex-wrap:wrap}.social a{border:1px solid #39414d;border-radius:999px;padding:8px 13px;text-decoration:none;color:#d8dce4;font-size:13px}.light{background:var(--paper);color:#111722;padding:clamp(70px,10vw,150px) clamp(20px,6vw,90px)}.light h2,.section h2{font-family:var(--serif);font-size:clamp(42px,6vw,82px);line-height:1.02;font-weight:400;max-width:1080px}.light p{max-width:920px;color:#4e596a;font-size:18px}.section{padding:clamp(70px,9vw,130px) clamp(20px,6vw,90px);border-bottom:1px solid var(--line)}.section-head{max-width:920px;margin-bottom:46px}.eyebrow{text-transform:uppercase;letter-spacing:.18em;font-size:12px;color:#aab2bf}.consult{display:grid;grid-template-columns:1.3fr .7fr;gap:50px;background:linear-gradient(130deg,#060708,#111721)}.accent-card{min-height:360px;border:1px solid #2a3545;background:linear-gradient(145deg,rgba(47,102,235,.22),transparent 55%);display:grid;place-items:center;font-family:var(--serif);font-size:96px;color:var(--blue)}.grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px}.card{border:1px solid #323a46;padding:26px;min-height:220px;background:#0b0e12}.card h3{font-family:var(--serif);font-size:30px}.card p{color:#b9c0ca}.learning{background:#020303}.topic-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:1px;background:#29313c;border:1px solid #29313c}.topic{background:#050708;padding:30px}.topic strong{display:block;color:var(--blue);text-transform:uppercase;font-size:11px;letter-spacing:.15em}.topic h3{font-family:var(--serif);font-size:30px;margin:8px 0}.topic p{color:#aeb6c1}.proof{background:linear-gradient(180deg,#0a0d11,#050607)}.quotes{display:flex;gap:14px;overflow:auto;scroll-snap-type:x mandatory;padding-bottom:12px}.quote{min-width:min(420px,86vw);scroll-snap-align:start;border:1px solid #343d49;padding:30px;background:#0c1015}.quote q{font-family:var(--serif);font-size:30px;line-height:1.25}.quote span{display:block;margin-top:24px;color:#aeb5c0;font-size:12px;text-transform:uppercase;letter-spacing:.12em}.cta{text-align:center;border-top:3px solid var(--blue);background:radial-gradient(circle at 50% 0,rgba(47,102,235,.25),transparent 50%)}.cta h2{margin-left:auto;margin-right:auto}.footer{display:grid;grid-template-columns:1fr auto;gap:40px;padding:46px clamp(20px,6vw,90px);color:#abb3bf}.footer strong{color:#fff;font-family:var(--serif);font-size:26px}.generated{font-size:11px;color:#737d8b;margin-top:10px}html[data-theme='slate']{--blue:#7f95b8;--blue-soft:#dfe6ef}html[data-theme='monochrome']{--blue:#fff;--blue-soft:#e7e7e7}.sr-only{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0)}
35462
+ @media(max-width:760px){.header{height:78px}.menu{display:block}.nav{display:none;position:absolute;left:0;right:0;top:77px;padding:18px 20px;background:#090b0e;border-bottom:1px solid #303844;flex-direction:column;align-items:stretch}.nav.open{display:flex}.hero{grid-template-columns:1fr;min-height:0}.portrait{min-height:390px}.portrait-card{width:72%;max-height:360px}.hero-copy{padding:48px 20px 66px}.hero h1{font-size:64px}.role{font-size:34px}.actions{display:grid}.button{width:100%}.consult{grid-template-columns:1fr}.accent-card{min-height:220px}.grid,.topic-grid{grid-template-columns:1fr}.light,.section{padding:64px 20px}.light h2,.section h2{font-size:46px}.footer{grid-template-columns:1fr}}
35463
+ </style></head>
35464
+ <body><a class="skip" href="#main">Skip to main content</a>
35465
+ <header class="header"><a class="brand" href="#top"><span class="mark">${escapeHtml7(initials(input.name).slice(0, 1))}</span><strong>${escapeHtml7(brand)}</strong></a>${navigation}</header>
35466
+ <main id="main"><section class="hero" id="top"><div class="portrait" aria-label="Portrait placeholder"><div class="portrait-card" aria-hidden="true">${escapeHtml7(initials(input.name))}</div></div><div class="hero-copy"><h1>${escapeHtml7(input.name)}</h1><p class="role">${escapeHtml7(input.professionalTitle)}</p><p class="summary">${escapeHtml7(input.summary)}</p><div class="actions">${action(input.primaryAction)}${input.secondaryAction ? action(input.secondaryAction, true) : ""}</div>${social}</div></section>
35467
+ ${section(flags.authority && Boolean(authority), `<section class="light" id="about"><h2>${escapeHtml7(authority?.heading ?? "")}</h2>${authority?.paragraphs.map((paragraph) => `<p>${escapeHtml7(paragraph)}</p>`).join("") ?? ""}</section>`)}
35468
+ ${section(flags.consultation && Boolean(consultation), `<section class="section consult"><div><p class="eyebrow">${escapeHtml7(consultation?.eyebrow ?? "Start a conversation")}</p><h2>${escapeHtml7(consultation?.heading ?? "")}</h2><p class="summary">${escapeHtml7(consultation?.summary ?? "")}</p>${consultation ? action(consultation.action) : ""}</div><div class="accent-card" aria-label="Image placeholder">${escapeHtml7(initials(input.name))}</div></section>`)}
35469
+ ${section(flags.learning && Boolean(learning), `<section class="section learning"><div class="section-head"><h2>${escapeHtml7(learning?.heading ?? "")}</h2><p class="summary">${escapeHtml7(learning?.summary ?? "")}</p></div><div class="topic-grid">${learning?.topics.map((topic) => `<article class="topic"><strong>${escapeHtml7(topic.title)}</strong><h3>${escapeHtml7(topic.headline)}</h3><p>${escapeHtml7(topic.summary)}</p>${topic.href ? `<a href="${safeHref(topic.href)}">Explore \u2192</a>` : ""}</article>`).join("") ?? ""}</div></section>`)}
35470
+ ${section(flags.services && Boolean(services), `<section class="section" id="services"><div class="section-head"><h2>${escapeHtml7(services?.heading ?? "")}</h2><p class="summary">${escapeHtml7(services?.summary ?? "")}</p></div><div class="grid">${services?.offers.map((offer) => `<article class="card"><h3>${escapeHtml7(offer.title)}</h3><p>${escapeHtml7(offer.description)}</p>${offer.action ? action(offer.action, true) : ""}</article>`).join("") ?? ""}</div></section>`)}
35471
+ ${section(flags.proof && Boolean(proof), `<section class="section proof" id="proof"><div class="section-head"><h2>${escapeHtml7(proof?.heading ?? "")}</h2><p class="summary">${escapeHtml7(proof?.summary ?? "")}</p></div><div class="quotes">${proof?.testimonials.map((item) => `<article class="quote"><q>${escapeHtml7(item.quote)}</q><span>${escapeHtml7(item.person)}${item.role ? ` / ${escapeHtml7(item.role)}` : ""}</span></article>`).join("") ?? ""}</div></section>`)}
35472
+ ${section(flags.finalCta && Boolean(finalCta), `<section class="section cta" id="contact"><p class="eyebrow">${escapeHtml7(finalCta?.eyebrow ?? "Ready when you are")}</p><h2>${escapeHtml7(finalCta?.heading ?? "")}</h2><p class="summary">${escapeHtml7(finalCta?.summary ?? "")}</p>${finalCta ? action(finalCta.action) : ""}</section>`)}
35473
+ </main><footer class="footer"><div><strong>${escapeHtml7(input.name)}</strong><div>${escapeHtml7(input.professionalTitle)}</div>${config.showGeneratedAt ? `<div class="generated">Generated ${escapeHtml7(generatedAtIso)}</div>` : ""}</div><div>${input.location ? `<div>${escapeHtml7(input.location)}</div>` : ""}${input.email ? `<a href="mailto:${escapeHtml7(input.email)}">${escapeHtml7(input.email)}</a>` : ""}${input.phone ? `<div>${escapeHtml7(input.phone)}</div>` : ""}</div></footer>
35474
+ <script>document.querySelector('.menu')?.addEventListener('click',function(){var nav=document.querySelector('.nav');var open=this.getAttribute('aria-expanded')!=='true';this.setAttribute('aria-expanded',String(open));nav?.classList.toggle('open',open)});document.querySelectorAll('.nav a').forEach(function(link){link.addEventListener('click',function(){document.querySelector('.nav')?.classList.remove('open');document.querySelector('.menu')?.setAttribute('aria-expanded','false')})});</script></body></html>`;
35475
+ const bytes = Buffer.byteLength(html, "utf8");
35476
+ return {
35477
+ html,
35478
+ filename: `${input.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "personal-authority"}.html`,
35479
+ bytes,
35480
+ sha256: createHash18("sha256").update(html).digest("hex"),
35481
+ generatedAt: generatedAtIso
35482
+ };
35483
+ }
35484
+
35485
+ // src/newsroom-publisher/render.ts
35486
+ import { createHash as createHash19 } from "crypto";
35487
+ function escapeHtml8(value) {
35488
+ return value.replace(/[&<>"']/g, (character) => ({
35489
+ "&": "&amp;",
35490
+ "<": "&lt;",
35491
+ ">": "&gt;",
35492
+ '"': "&quot;",
35493
+ "'": "&#39;"
35494
+ })[character] ?? character);
35495
+ }
35496
+ function safeHref2(value) {
35497
+ const trimmed = value.trim();
35498
+ return /^(?:https?:|mailto:|tel:|#|\/)/i.test(trimmed) ? escapeHtml8(trimmed) : "#";
35499
+ }
35500
+ function formatDate(value) {
35501
+ const date = new Date(value);
35502
+ return Number.isNaN(date.getTime()) ? escapeHtml8(value) : new Intl.DateTimeFormat("en", { month: "short", day: "numeric", year: "numeric" }).format(date);
35503
+ }
35504
+ function initials2(value) {
35505
+ return value.split(/\s+/).filter(Boolean).slice(0, 2).map((part) => part[0]).join("").toUpperCase();
35506
+ }
35507
+ function storyMedia(story, className = "") {
35508
+ if (story.imageUrl) {
35509
+ return `<div class="story-media ${className}"><img src="${safeHref2(story.imageUrl)}" alt="${escapeHtml8(story.imageAlt || story.title)}" loading="lazy"></div>`;
35510
+ }
35511
+ const tone = [...story.title].reduce((total, character) => total + character.charCodeAt(0), 0) % 5;
35512
+ return `<div class="story-media story-media--placeholder story-media--tone-${tone} ${className}" role="img" aria-label="Editorial image placeholder for ${escapeHtml8(story.title)}"><i></i><b></b><span>${escapeHtml8(story.category)}</span></div>`;
35513
+ }
35514
+ function byline(story, showBylines) {
35515
+ const author = story.author.href ? `<a href="${safeHref2(story.author.href)}">${escapeHtml8(story.author.name)}</a>` : escapeHtml8(story.author.name);
35516
+ return `<div class="byline">${showBylines ? `<span>By ${author}</span>` : ""}<time datetime="${escapeHtml8(story.publishedAt)}">${formatDate(story.publishedAt)}</time>${story.sponsored ? "<b>Sponsored</b>" : ""}</div>`;
35517
+ }
35518
+ function storyCard(story, showBylines, variant = "") {
35519
+ return `<article class="story-card ${variant}">
35520
+ <a class="story-card__media" href="${safeHref2(story.href)}">${storyMedia(story)}</a>
35521
+ <div class="story-card__body"><a class="kicker" href="${safeHref2(story.href)}">${escapeHtml8(story.category)}</a><h3><a href="${safeHref2(story.href)}">${escapeHtml8(story.title)}</a></h3>${story.dek ? `<p>${escapeHtml8(story.dek)}</p>` : ""}${byline(story, showBylines)}</div>
35522
+ </article>`;
35523
+ }
35524
+ function renderNewsroomPublisherV1(input, config, generatedAt) {
35525
+ const flags = config.featureFlags;
35526
+ const generatedAtIso = generatedAt.toISOString();
35527
+ const brand = config.brandName || input.publication.name;
35528
+ const [lead, ...supporting] = input.featured;
35529
+ const primaryPages = [
35530
+ { label: "Home", href: input.publication.url },
35531
+ input.primaryPages.about,
35532
+ input.primaryPages.editor,
35533
+ input.primaryPages.press,
35534
+ input.primaryPages.news,
35535
+ input.primaryPages.contact
35536
+ ];
35537
+ const trustPages = Object.values(input.trustPages);
35538
+ const ticker = flags.breakingTicker && input.ticker ? `<div class="ticker"><div><b>${escapeHtml8(input.ticker.label)}</b><a href="${safeHref2(input.ticker.href || "#latest")}">${escapeHtml8(input.ticker.headline)}</a><span>${formatDate(generatedAtIso)}</span></div></div>` : "";
35539
+ const masthead = `<nav class="utility-nav" aria-label="Publication pages"><div><span>${escapeHtml8(input.publication.location || "Independent newsroom")}</span>${primaryPages.slice(1).map((link) => `<a href="${safeHref2(link.href)}">${escapeHtml8(link.label)}</a>`).join("")}</div></nav><header class="masthead">
35540
+ <a class="wordmark" href="${safeHref2(input.publication.url)}"><span>${escapeHtml8(brand)}</span><small>${escapeHtml8(input.publication.tagline)}</small></a>
35541
+ <div class="masthead__date"><span>Independent journalism</span><time datetime="${generatedAtIso}">${formatDate(generatedAtIso)}</time></div>
35542
+ <div class="masthead__tools"><button type="button" aria-label="Search">Search</button><button type="button" aria-label="Open menu">Menu</button></div>
35543
+ </header>`;
35544
+ const navigation = flags.navigation ? `<nav class="desk-nav" aria-label="Publication categories"><div><a class="desk-nav__home" href="#top">Home</a>${input.categories.map((category) => `<a href="${safeHref2(category.href)}">${escapeHtml8(category.label)}</a>`).join("")}<a class="desk-nav__latest" href="#latest">Latest</a></div></nav>` : "";
35545
+ const leadGrid = flags.leadGrid ? `<section class="lead-grid" aria-label="Top stories">
35546
+ <div class="lead-grid__label"><span>Today\u2019s edition</span><b>Top stories</b></div>
35547
+ <article class="lead-story"><a class="lead-story__media" href="${safeHref2(lead.href)}">${storyMedia(lead, "story-media--lead")}</a><div class="lead-story__body"><a class="kicker" href="${safeHref2(lead.href)}">${escapeHtml8(lead.category)}</a><h1><a href="${safeHref2(lead.href)}">${escapeHtml8(lead.title)}</a></h1>${lead.dek ? `<p>${escapeHtml8(lead.dek)}</p>` : ""}${byline(lead, config.showBylines)}<a class="read-story" href="${safeHref2(lead.href)}">Read the full report <span>\u2197</span></a></div></article>
35548
+ <div class="lead-support">${supporting.slice(0, 4).map((story, index) => `<div class="lead-support__item"><span>${String(index + 1).padStart(2, "0")}</span>${storyCard(story, config.showBylines, "story-card--support")}</div>`).join("")}</div>
35549
+ </section>` : "";
35550
+ const latest = flags.latestNews && input.latest.length > 0 ? `<section class="latest" id="latest">
35551
+ <div class="section-title"><div><span>Live desk</span><h2>Latest news</h2></div><a href="#top">Back to top \u2191</a></div>
35552
+ <div class="latest__layout"><div class="latest__feed">${input.latest.map((story, index) => `<article class="feed-story"><span>${String(index + 1).padStart(2, "0")}</span><div><a class="kicker" href="${safeHref2(story.href)}">${escapeHtml8(story.category)}</a><h3><a href="${safeHref2(story.href)}">${escapeHtml8(story.title)}</a></h3>${story.dek ? `<p>${escapeHtml8(story.dek)}</p>` : ""}${byline(story, config.showBylines)}</div>${storyMedia(story, "story-media--feed")}</article>`).join("")}</div>
35553
+ <aside class="editor-note"><span>From the newsroom</span><h3>Reporting with context, sources, and accountable bylines.</h3><p>${escapeHtml8(input.editorial.disclosure)}</p>${trustPages.slice(0, 4).map((link) => `<a href="${safeHref2(link.href)}">${escapeHtml8(link.label)} \u2197</a>`).join("")}</aside></div>
35554
+ </section>` : "";
35555
+ const sectionLayouts = ["panorama", "mosaic", "tiles"];
35556
+ const sections = flags.categorySections ? input.sections.map((section2, sectionIndex) => {
35557
+ const layout = sectionLayouts[sectionIndex % sectionLayouts.length];
35558
+ const cards = section2.stories.map((story, index) => storyCard(
35559
+ story,
35560
+ config.showBylines,
35561
+ layout === "panorama" && index === 0 ? "story-card--panorama" : layout === "mosaic" && index === 0 ? "story-card--mosaic-lead" : ""
35562
+ )).join("");
35563
+ return `<section class="desk-section desk-section--${sectionIndex % 2 ? "tint" : "plain"} desk-section--layout-${layout}">
35564
+ <div class="section-title"><div><span>Coverage desk \xB7 ${String(sectionIndex + 1).padStart(2, "0")}</span><h2>${escapeHtml8(section2.name)}</h2></div>${section2.href ? `<a href="${safeHref2(section2.href)}">View all ${escapeHtml8(section2.name)} \u2192</a>` : ""}</div>
35565
+ <div class="desk-section__grid desk-section__grid--${layout}">${cards}</div>
35566
+ </section>`;
35567
+ }).join("") : "";
35568
+ const newsletter = flags.newsletter && input.newsletter ? `<section class="newsletter"><div><span>${escapeHtml8(input.newsletter.eyebrow || "Reader briefing")}</span><h2>${escapeHtml8(input.newsletter.heading)}</h2><p>${escapeHtml8(input.newsletter.summary)}</p></div><a href="${safeHref2(input.newsletter.action.href)}">${escapeHtml8(input.newsletter.action.label)} \u2192</a></section>` : "";
35569
+ const pressRoom = flags.pressRoom && input.pressRoom ? `<section class="press-room" id="press-room"><div class="press-room__intro"><span>Separate from editorial coverage</span><h2>${escapeHtml8(input.pressRoom.heading)}</h2><p>${escapeHtml8(input.pressRoom.summary)}</p><a href="${safeHref2(input.pressRoom.action.href)}">${escapeHtml8(input.pressRoom.action.label)} \u2192</a></div><div class="release-list">${input.pressRoom.releases.map((release) => `<article><time datetime="${escapeHtml8(release.publishedAt)}">${formatDate(release.publishedAt)}</time><span>${escapeHtml8(release.originatingCity)}</span><h3><a href="${safeHref2(release.href)}">${escapeHtml8(release.headline)}</a></h3></article>`).join("") || "<p>No releases are currently published.</p>"}</div></section>` : "";
35570
+ const trustCenter = `<section class="trust-center" id="trust"><div class="trust-center__intro"><span>Public record</span><h2>How this newsroom works.</h2><p>Ownership, editorial practice, technology use, commercial relationships, and reader rights should be easy to find\u2014and written to be understood.</p></div><div class="trust-center__grid">${trustPages.map((link, index) => `<a href="${safeHref2(link.href)}"><span>${String(index + 1).padStart(2, "0")}</span><strong>${escapeHtml8(link.label)}</strong><small>Read \u2192</small></a>`).join("")}</div></section>`;
35571
+ const trustFooter = flags.trustFooter ? `<footer class="site-footer"><div class="editor"><span class="editor__avatar">${escapeHtml8(initials2(input.editorial.managingEditor.name))}</span><div><small>${escapeHtml8(input.editorial.managingEditor.role)}</small><h2>${input.editorial.managingEditor.href ? `<a href="${safeHref2(input.editorial.managingEditor.href)}">${escapeHtml8(input.editorial.managingEditor.name)}</a>` : escapeHtml8(input.editorial.managingEditor.name)}</h2><p>${escapeHtml8(input.editorial.managingEditor.bio)}</p></div></div><div class="footer-links"><h3>${escapeHtml8(brand)}</h3><p>${escapeHtml8(input.publication.tagline)}</p><nav>${primaryPages.map((link) => `<a href="${safeHref2(link.href)}">${escapeHtml8(link.label)}</a>`).join("")}</nav>${config.showGeneratedAt ? `<time datetime="${generatedAtIso}">Updated ${formatDate(generatedAtIso)}</time>` : ""}</div></footer>` : "";
35572
+ const html = `<!doctype html>
35573
+ <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="robots" content="noindex,nofollow"><title>${escapeHtml8(brand)} \xB7 News Publisher</title><link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link href="https://fonts.googleapis.com/css2?family=Archivo:wdth,wght@75..125,300..900&family=Instrument+Serif:ital@0;1&family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">
35574
+ <style>
35575
+ :root{--ink:#0b0b0c;--soft-ink:#222326;--muted:#696b70;--paper:#fff;--cream:#f4f0e8;--wash:#f5f5f3;--line:#d8d8d4;--signal:#275cf3;--acid:#d9ff43;--dark:#0b0b0c;--serif:"Instrument Serif",Georgia,serif;--sans:"Archivo",Arial,sans-serif;--mono:"JetBrains Mono",monospace;font-family:var(--sans);color:var(--ink);background:var(--paper)}*{box-sizing:border-box}html{scroll-behavior:smooth}body{margin:0;background:var(--paper);line-height:1.45}body[data-theme=journal]{--cream:#eee7da;--signal:#9d2d25;--acid:#f0c84f}body[data-theme=midnight]{--paper:#e8e9ec;--cream:#dfe1e6;--wash:#d8dae0;--line:#b9bdc6;--signal:#224bcc}a{color:inherit;text-decoration:none}a,button{transition:color .18s ease,background .18s ease,opacity .18s ease,transform .18s ease}button{font:inherit}.utility-nav{color:#d2d2d2;background:var(--dark);border-bottom:1px solid #292929}.utility-nav>div{max-width:1180px;height:35px;margin:auto;display:flex;align-items:center;justify-content:flex-end;gap:20px;padding:0 18px;overflow:auto}.utility-nav span{margin-right:auto;color:#8d8d8d}.utility-nav a,.utility-nav span{font:600 8px/1 var(--mono);letter-spacing:.08em;text-transform:uppercase;white-space:nowrap}.utility-nav a:hover{color:#fff}.ticker{color:var(--ink);background:var(--acid);padding:0 18px}.ticker>div{max-width:1180px;height:36px;margin:auto;display:flex;align-items:center;gap:16px;font:600 10px/1 var(--mono)}.ticker b{padding:5px 7px;color:#fff;background:var(--ink);font-size:8px;letter-spacing:.1em;text-transform:uppercase}.ticker a{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.ticker a:hover{text-decoration:underline}.ticker span{margin-left:auto;opacity:.58;font-size:8px;text-transform:uppercase}.masthead{max-width:1180px;height:126px;margin:auto;display:grid;grid-template-columns:minmax(0,1fr) auto auto;align-items:center;gap:42px;padding:0 18px}.wordmark{display:grid;justify-self:start}.wordmark span{font:400 clamp(40px,5vw,68px)/.82 var(--serif);letter-spacing:-.055em}.wordmark small{max-width:430px;margin-top:11px;color:var(--muted);font:600 8px/1 var(--mono);letter-spacing:.1em;text-transform:uppercase}.masthead__date{display:grid;gap:3px;text-align:right}.masthead__date span,.masthead__date time{font:600 8px/1.4 var(--mono);letter-spacing:.08em;text-transform:uppercase}.masthead__date span{color:var(--signal)}.masthead__date time{color:var(--muted)}.masthead__tools{display:flex;border:1px solid var(--ink)}.masthead__tools button{height:38px;padding:0 13px;border:0;background:transparent;font:700 9px/1 var(--mono);text-transform:uppercase;cursor:pointer}.masthead__tools button+button{border-left:1px solid var(--ink)}.masthead__tools button:hover{color:#fff;background:var(--ink)}.desk-nav{position:sticky;top:0;z-index:20;color:var(--ink);background:rgba(255,255,255,.96);border-top:1px solid var(--ink);border-bottom:1px solid var(--ink);backdrop-filter:blur(10px)}.desk-nav>div{max-width:1180px;height:43px;margin:auto;display:flex;align-items:center;overflow-x:auto;padding:0 18px}.desk-nav a{height:42px;display:grid;place-items:center;padding:0 17px;border-right:1px solid var(--line);font:650 9px/1 var(--mono);letter-spacing:.04em;text-transform:uppercase;white-space:nowrap}.desk-nav a:first-child{border-left:1px solid var(--line)}.desk-nav a:hover,.desk-nav__home{color:#fff!important;background:var(--ink)}.desk-nav__latest{margin-left:auto;color:var(--signal);border-left:1px solid var(--line)}main{max-width:1180px;margin:auto;padding:30px 18px 90px}.lead-grid{margin:0 -18px;padding:0 18px 42px;color:#fff;background:var(--dark);border-bottom:1px solid var(--ink)}.lead-grid__label{height:50px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid #303030}.lead-grid__label span,.lead-grid__label b{font:600 8px/1 var(--mono);letter-spacing:.12em;text-transform:uppercase}.lead-grid__label span{color:var(--acid)}.lead-grid__label b{color:#8f8f8f}.lead-story{display:grid;grid-template-columns:minmax(0,1.28fr) minmax(340px,.72fr);min-height:500px;border-bottom:1px solid #303030}.lead-story__media{display:block;margin-left:calc((1180px - 100vw)/2);min-width:0}.story-media{position:relative;aspect-ratio:16/10;overflow:hidden;background:#e2e2df}.story-media img{width:100%;height:100%;display:block;object-fit:cover;filter:saturate(.88) contrast(1.03);transition:transform .55s cubic-bezier(.2,.7,.2,1)}a:hover .story-media img{transform:scale(1.025)}.story-media--lead{height:100%;min-height:500px;aspect-ratio:auto}.story-media--placeholder{isolation:isolate;display:block;background:#d9d7d1}.story-media--placeholder:after{position:absolute;z-index:1;inset:0;background:linear-gradient(145deg,transparent 35%,rgba(0,0,0,.14));content:""}.story-media--placeholder i,.story-media--placeholder b{position:absolute;display:block}.story-media--placeholder i{width:72%;aspect-ratio:1;right:-20%;top:-32%;border:clamp(22px,5vw,72px) solid currentColor;border-radius:50%;opacity:.78}.story-media--placeholder b{width:42%;height:115%;left:15%;bottom:-45%;background:currentColor;transform:rotate(32deg);opacity:.88}.story-media--placeholder span{position:absolute;z-index:2;left:18px;bottom:15px;padding:6px 8px;color:#fff;background:var(--ink);font:600 8px/1 var(--mono);letter-spacing:.12em;text-transform:uppercase}.story-media--tone-0{color:#123fce;background:#a5c8ff}.story-media--tone-1{color:#ef4d25;background:#ffd9bd}.story-media--tone-2{color:#907000;background:#f3df36}.story-media--tone-3{color:#1a7857;background:#b8e8cc}.story-media--tone-4{color:#7d3de0;background:#dfc5ff}.lead-story__body{display:flex;flex-direction:column;justify-content:center;padding:45px 0 45px 40px}.kicker{display:inline-flex;width:max-content;color:var(--signal);font:700 8px/1 var(--mono);letter-spacing:.11em;text-transform:uppercase}.lead-grid .kicker{color:var(--acid)}.lead-story h1{max-width:540px;margin:13px 0 18px;font:400 clamp(48px,5vw,76px)/.92 var(--serif);letter-spacing:-.052em}.lead-story p{max-width:490px;margin:0;color:#bdbdbd;font-size:15px;line-height:1.55}.byline{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-top:16px;color:var(--muted);font:600 8px/1.4 var(--mono);letter-spacing:.05em;text-transform:uppercase}.byline span{color:var(--ink);font-weight:700}.lead-grid .byline{color:#848484}.lead-grid .byline span{color:#e7e7e7}.byline b{padding:4px 6px;color:var(--ink);background:var(--acid)}.read-story{width:max-content;margin-top:34px;padding-bottom:6px;border-bottom:1px solid #777;font:650 10px/1 var(--mono);text-transform:uppercase}.read-story span{color:var(--acid)}.read-story:hover{border-color:#fff}.lead-support{display:grid;grid-template-columns:repeat(4,1fr);gap:0}.lead-support__item{display:grid;grid-template-columns:25px minmax(0,1fr);gap:12px;padding:22px 18px 0 0;border-right:1px solid #303030}.lead-support__item+.lead-support__item{padding-left:18px}.lead-support__item:last-child{border-right:0}.lead-support__item>span{color:#666;font:600 9px/1 var(--mono)}.story-card{min-width:0}.story-card h3{margin:7px 0;font:400 26px/1.02 var(--serif);letter-spacing:-.025em}.story-card p{margin:9px 0;color:var(--muted);font-size:12px}.story-card__body{padding-top:11px}.story-card--support .story-card__media{display:none}.story-card--support .story-card__body{padding:0}.story-card--support h3{margin-top:8px;color:#fff;font-size:21px}.story-card--support p,.story-card--support .byline span{display:none}.story-card--support .byline{color:#777}.section-title{display:flex;align-items:end;justify-content:space-between;gap:30px;margin-bottom:24px;padding-bottom:11px;border-bottom:2px solid var(--ink)}.section-title span,.newsletter span,.press-room__intro>span,.trust-center__intro>span{display:block;color:var(--signal);font:700 8px/1 var(--mono);letter-spacing:.12em;text-transform:uppercase}.section-title h2,.newsletter h2,.press-room h2,.trust-center h2,.site-footer h2{margin:7px 0 0;font:400 clamp(38px,5vw,58px)/.95 var(--serif);letter-spacing:-.04em}.section-title>a{padding-bottom:4px;border-bottom:1px solid var(--ink);font:650 9px/1 var(--mono);text-transform:uppercase}.latest{padding:64px 0}.latest__layout{display:grid;grid-template-columns:minmax(0,1fr) 300px;gap:55px}.latest__feed{border-top:1px solid var(--ink)}.feed-story{display:grid;grid-template-columns:34px minmax(0,1fr) 180px;gap:18px;align-items:start;padding:19px 0;border-bottom:1px solid var(--line)}.feed-story>span{color:#a0a0a0;font:600 9px/1 var(--mono)}.feed-story h3{margin:7px 0;font:400 29px/1.02 var(--serif);letter-spacing:-.025em}.feed-story p{margin:8px 0;color:var(--muted);font-size:12px}.story-media--feed{aspect-ratio:4/3}.editor-note{position:sticky;top:68px;align-self:start;padding:25px;color:#fff;background:var(--dark);border-top:5px solid var(--acid)}.editor-note>span{color:var(--acid);font:600 8px/1 var(--mono);letter-spacing:.12em;text-transform:uppercase}.editor-note h3{margin:13px 0;font:400 31px/.98 var(--serif)}.editor-note p{color:#aaa;font-size:12px}.editor-note a{display:block;padding:11px 0;border-top:1px solid #333;font:600 9px/1 var(--mono);text-transform:uppercase}.editor-note a:hover{color:var(--acid)}.desk-section{margin:0 -18px;padding:58px 18px;border-top:1px solid var(--ink)}.desk-section--tint{background:var(--cream)}.desk-section__grid{display:grid;grid-template-columns:repeat(4,1fr);gap:26px 14px}.story-card--wide{grid-column:span 2;grid-row:span 2}.story-card--wide h3{max-width:530px;font-size:40px}.story-card--wide p{max-width:540px;font-size:13px}.desk-section__grid>.story-card:not(.story-card--wide) .story-media{aspect-ratio:1}.newsletter{position:relative;margin:64px 0;display:grid;grid-template-columns:1fr auto;align-items:end;gap:50px;overflow:hidden;padding:clamp(38px,6vw,72px);color:var(--ink);background:var(--acid)}.newsletter:after{position:absolute;width:260px;height:260px;right:18%;top:-160px;border:55px solid var(--ink);border-radius:50%;content:"";opacity:.08}.newsletter span{color:var(--ink)}.newsletter h2{max-width:760px;font-size:clamp(44px,6vw,72px)}.newsletter p{max-width:680px;margin-bottom:0;color:#303030}.newsletter>a,.press-room__intro>a{position:relative;z-index:1;display:inline-flex;padding:14px 18px;color:#fff;background:var(--ink);border:1px solid var(--ink);font:700 9px/1 var(--mono);text-transform:uppercase}.newsletter>a:hover,.press-room__intro>a:hover{color:var(--ink);background:transparent}.press-room{display:grid;grid-template-columns:.7fr 1.3fr;gap:56px;padding:62px 0;border-top:6px solid var(--ink)}.press-room__intro p{max-width:390px;color:var(--muted)}.release-list{border-top:1px solid var(--ink)}.release-list article{display:grid;grid-template-columns:105px 105px 1fr;gap:15px;align-items:start;padding:20px 0;border-bottom:1px solid var(--line)}.release-list time,.release-list span{color:var(--muted);font:500 8px/1.4 var(--mono);text-transform:uppercase}.release-list h3{margin:0;font:400 22px/1.04 var(--serif)}.trust-center{margin:20px -18px -90px;padding:70px max(18px,calc((100vw - 1144px)/2)) 82px;color:#fff;background:var(--dark)}.trust-center__intro{max-width:810px;margin-bottom:38px}.trust-center__intro>span{color:var(--acid)}.trust-center__intro p{max-width:670px;color:#aaa}.trust-center__grid{display:grid;grid-template-columns:repeat(2,1fr);border-top:1px solid #444}.trust-center__grid a{display:grid;grid-template-columns:32px minmax(0,1fr) auto;align-items:center;gap:15px;min-height:70px;padding:0 18px 0 0;border-bottom:1px solid #333}.trust-center__grid a:nth-child(odd){margin-right:28px}.trust-center__grid a:hover{color:var(--acid)}.trust-center__grid span,.trust-center__grid small{color:#696969;font:600 8px/1 var(--mono)}.trust-center__grid strong{font:400 20px/1 var(--serif)}.site-footer{display:grid;grid-template-columns:1.15fr .85fr;gap:80px;padding:70px max(24px,calc((100vw - 1144px)/2));color:var(--ink);background:var(--cream);border-top:1px solid var(--ink)}.editor{display:grid;grid-template-columns:78px 1fr;gap:22px}.editor__avatar{width:78px;height:78px;display:grid;place-items:center;color:#fff;background:var(--signal);font:400 27px/1 var(--serif)}.editor small{color:var(--signal);font:700 8px/1 var(--mono);letter-spacing:.08em;text-transform:uppercase}.editor h2{font-size:38px}.editor p,.footer-links p{max-width:620px;color:var(--muted)}.footer-links h3{margin-top:0;font:400 29px/1 var(--serif)}.footer-links nav{display:grid;grid-template-columns:1fr 1fr;border-top:1px solid var(--ink)}.footer-links nav a{padding:9px 0;border-bottom:1px solid var(--line);font:600 8px/1 var(--mono);text-transform:uppercase}.footer-links time{display:block;margin-top:24px;color:var(--muted);font:500 8px/1 var(--mono)}@media(max-width:900px){.masthead{grid-template-columns:1fr auto;height:110px}.masthead__date{display:none}.lead-story{grid-template-columns:1fr}.lead-story__media{margin:0}.lead-story__body{padding:35px 0}.lead-support{grid-template-columns:1fr 1fr}.lead-support__item:nth-child(2){border-right:0}.lead-support__item:nth-child(n+3){border-top:1px solid #303030}.latest__layout,.press-room,.site-footer{grid-template-columns:1fr}.editor-note{position:static}.desk-section__grid{grid-template-columns:1fr 1fr}.story-card--wide{grid-column:1/-1}.site-footer{gap:40px}.feed-story{grid-template-columns:30px 1fr 150px}}@media(max-width:620px){.utility-nav span,.utility-nav a:nth-of-type(-n+2),.ticker span{display:none}.ticker>div{height:34px}.masthead{height:92px;gap:14px}.wordmark span{font-size:38px}.wordmark small{max-width:220px;font-size:6px}.masthead__tools button:first-child{display:none}.desk-nav>div{padding:0}.desk-nav a{padding:0 13px}.lead-grid{padding-bottom:20px}.lead-grid__label{height:40px}.story-media--lead{min-height:320px}.lead-story{min-height:0}.lead-story h1{font-size:48px}.lead-story__body{padding:27px 0 34px}.lead-support{grid-template-columns:1fr}.lead-support__item,.lead-support__item+.lead-support__item{padding:17px 0;border-right:0;border-top:1px solid #303030}.story-card--support h3{font-size:24px}.section-title{align-items:start}.section-title>a{display:none}.latest{padding:48px 0}.feed-story{grid-template-columns:25px 1fr}.story-media--feed{grid-column:2}.feed-story h3{font-size:26px}.desk-section__grid{grid-template-columns:1fr}.story-card--wide{grid-column:auto;grid-row:auto}.story-card--wide h3{font-size:34px}.newsletter{grid-template-columns:1fr}.newsletter h2{font-size:48px}.release-list article{grid-template-columns:1fr}.trust-center__grid{grid-template-columns:1fr}.trust-center__grid a:nth-child(odd){margin-right:0}.site-footer{padding:50px 20px}.editor{grid-template-columns:1fr}.footer-links nav{grid-template-columns:1fr}}
35576
+ /* Tampa-inspired publication palette */
35577
+ :root{--ink:#12312f;--soft-ink:#294744;--muted:#697b77;--paper:#fffdf8;--cream:#f8f2e8;--wash:#f3eee5;--line:#d8d3c8;--signal:#ee7057;--acid:#f0b45d;--dark:#082f2c}
35578
+ body[data-theme=journal]{--dark:#102d2a;--ink:#172f2d;--cream:#eee7da;--signal:#bd5b47;--acid:#e4aa52}
35579
+ body[data-theme=midnight]{--paper:#e8e9e4;--cream:#dfe3dc;--wash:#d8ddd6;--line:#b9c2bb;--dark:#072a28;--signal:#df6c56}
35580
+ .utility-nav{color:#d7e0dc;border-bottom-color:rgba(255,255,255,.14)}
35581
+ .lead-grid__label,.lead-story{border-bottom-color:rgba(255,255,255,.17)}
35582
+ .lead-grid__label b{color:#90aaa4}
35583
+ .lead-story__media .story-media:not(.story-media--placeholder):after{position:absolute;inset:0;background:linear-gradient(90deg,rgba(8,47,44,.38),transparent 64%),linear-gradient(0deg,rgba(8,47,44,.26),transparent 58%);content:""}
35584
+ .story-media--tone-0{color:#0b514b;background:#8fb8ac}.story-media--tone-1{color:#d65f49;background:#f5c2a9}.story-media--tone-2{color:#a97022;background:#f1d59b}.story-media--tone-3{color:#255f52;background:#b8d9c9}.story-media--tone-4{color:#9a5f48;background:#dfc2ad}
35585
+ body{overflow-x:hidden}.lead-story__body,.lead-support__item,.story-card__body{min-width:0}.lead-story h1,.story-card h3,.feed-story h3{overflow-wrap:anywhere}
35586
+ .desk-section__grid--panorama{grid-template-columns:repeat(4,1fr)}.story-card--panorama{grid-column:1/-1;display:grid;grid-template-columns:minmax(0,1.65fr) minmax(250px,.65fr);gap:28px;margin-bottom:12px;padding-bottom:28px;border-bottom:1px solid var(--line)}.story-card--panorama .story-media{aspect-ratio:2.2/1}.story-card--panorama .story-card__body{display:flex;flex-direction:column;justify-content:center;padding:0}.story-card--panorama h3{font-size:48px}.story-card--panorama p{font-size:14px}.desk-section__grid--mosaic{grid-template-columns:repeat(4,1fr);grid-auto-flow:dense}.story-card--mosaic-lead{grid-column:span 2;grid-row:span 2}.story-card--mosaic-lead .story-media{aspect-ratio:1}.story-card--mosaic-lead h3{font-size:42px}.desk-section__grid--mosaic>.story-card:not(.story-card--mosaic-lead) .story-media{aspect-ratio:1.35}.desk-section__grid--tiles{grid-template-columns:repeat(4,1fr)}.desk-section__grid--tiles .story-media{aspect-ratio:1.15}.desk-section__grid--tiles>.story-card:nth-child(3n+2){margin-top:38px}
35587
+ @media(max-width:620px){main{width:100%}.story-media--lead{height:360px;min-height:0}.lead-story__body,.lead-support,.latest,.feed-story{width:100%;max-width:calc(100vw - 36px)}.feed-story>div{min-width:0}.lead-story h1,.story-card h3,.feed-story h3,.story-card h3 a,.feed-story h3 a{white-space:normal;word-break:break-word;overflow-wrap:anywhere}.lead-story h1{max-width:100%;font-size:40px;line-height:.94}.lead-support__item{width:100%;max-width:100%;grid-template-columns:24px minmax(0,1fr);overflow:hidden}.story-card--support,.story-card--support .story-card__body,.story-card--support h3{width:100%;max-width:calc(100vw - 92px)}.feed-story>div,.feed-story h3,.feed-story p{max-width:calc(100vw - 100px)}}
35588
+ @media(max-width:900px){.story-card--panorama{grid-template-columns:1fr}.desk-section__grid--mosaic{grid-template-columns:repeat(2,1fr)}.desk-section__grid--tiles{grid-template-columns:repeat(2,1fr)}}
35589
+ @media(max-width:620px){.desk-section__grid--panorama,.desk-section__grid--mosaic,.desk-section__grid--tiles{grid-template-columns:1fr}.story-card--panorama{display:block}.story-card--panorama h3,.story-card--mosaic-lead h3{font-size:34px}.story-card--mosaic-lead{grid-column:auto;grid-row:auto}.desk-section__grid--tiles>.story-card:nth-child(3n+2){margin-top:0}}
35590
+ </style></head><body data-theme="${escapeHtml8(config.theme)}" id="top" data-scheduled-renderer="newsroom_publisher_v1">${ticker}${masthead}${navigation}<main>${leadGrid}${latest}${sections}${newsletter}${pressRoom}${trustCenter}</main>${trustFooter}</body></html>`;
35591
+ const bytes = Buffer.byteLength(html);
35592
+ const sha2563 = createHash19("sha256").update(html).digest("hex");
35593
+ const filename = `${brand.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "newsroom"}-news-site.html`;
35594
+ return { html, filename, bytes, sha256: sha2563, generatedAt: generatedAtIso };
35595
+ }
35596
+
35597
+ // src/scheduled-artifacts/scheduled-artifact-renderer-registry.ts
35598
+ var editorialReadingRoomV1 = {
35599
+ rendererKey: "editorial_reading_room_v1",
35600
+ rendererVersion: 1,
35601
+ async render(rawInput, rawConfig, generatedAt) {
35602
+ const input = EditorialReadingRoomV1InputSchema.parse(rawInput);
35603
+ const config = EditorialArtifactTemplateConfigSchema.parse(rawConfig);
35604
+ const rendered = renderEditorialReadingRoomV1(input, config, generatedAt);
35605
+ if (rendered.bytes > 2e6) {
35606
+ throw new Error("Scheduled artifact HTML must be 2,000,000 bytes or fewer.");
35607
+ }
35608
+ return {
35609
+ html: rendered.html,
35610
+ filename: rendered.filename,
35611
+ title: input.title,
35612
+ bytes: rendered.bytes,
35613
+ sha256: rendered.sha256,
35614
+ generatedAt: rendered.generatedAt
35615
+ };
35616
+ }
35617
+ };
35618
+ var personalAuthorityV1 = {
35619
+ rendererKey: "personal_authority_v1",
35620
+ rendererVersion: 1,
35621
+ async render(rawInput, rawConfig, generatedAt) {
35622
+ const input = PersonalAuthorityV1InputSchema.parse(rawInput);
35623
+ const config = PersonalAuthorityTemplateConfigSchema.parse(rawConfig);
35624
+ const rendered = renderPersonalAuthorityV1(input, config, generatedAt);
35625
+ if (rendered.bytes > 2e6) {
35626
+ throw new Error("Scheduled artifact HTML must be 2,000,000 bytes or fewer.");
35627
+ }
35628
+ return {
35629
+ ...rendered,
35630
+ title: input.name
35631
+ };
35632
+ }
35633
+ };
35634
+ var newsroomPublisherV1 = {
35635
+ rendererKey: "newsroom_publisher_v1",
35636
+ rendererVersion: 1,
35637
+ async render(rawInput, rawConfig, generatedAt) {
35638
+ const input = NewsroomPublisherV1InputSchema.parse(rawInput);
35639
+ const config = NewsroomPublisherTemplateConfigSchema.parse(rawConfig);
35640
+ const rendered = renderNewsroomPublisherV1(input, config, generatedAt);
35641
+ if (rendered.bytes > 2e6) {
35642
+ throw new Error("Scheduled artifact HTML must be 2,000,000 bytes or fewer.");
35643
+ }
35644
+ return {
35645
+ ...rendered,
35646
+ title: `${input.publication.name} News Publisher`
35647
+ };
35648
+ }
35649
+ };
35650
+ var scheduledArtifactRendererRegistry = Object.freeze({
35651
+ "editorial_reading_room_v1:1": editorialReadingRoomV1,
35652
+ "personal_authority_v1:1": personalAuthorityV1,
35653
+ "newsroom_publisher_v1:1": newsroomPublisherV1
35654
+ });
35655
+ function getScheduledArtifactRenderer(rendererKey, rendererVersion) {
35656
+ return scheduledArtifactRendererRegistry[`${rendererKey}:${rendererVersion}`] ?? null;
35657
+ }
35658
+ var scheduledArtifactTemplatePresets = [
35659
+ EDITORIAL_READING_ROOM_TEMPLATE_PRESET,
35660
+ PERSONAL_AUTHORITY_TEMPLATE_PRESET,
35661
+ NEWSROOM_PUBLISHER_TEMPLATE_PRESET
35662
+ ];
35663
+
33689
35664
  // src/api/scheduled-result-routes.ts
33690
- var TemplateParamsSchema = z33.object({ templateId: z33.string().uuid() }).strict();
33691
- var RunParamsSchema = z33.object({ runId: z33.string().min(1).max(200) }).strict();
33692
- var ViewLinkParamsSchema = z33.object({
33693
- runId: z33.string().min(1).max(200),
33694
- shareId: z33.string().uuid()
35665
+ var TemplateParamsSchema = z35.object({ templateId: z35.string().uuid() }).strict();
35666
+ var RunParamsSchema = z35.object({ runId: z35.string().min(1).max(200) }).strict();
35667
+ var ViewLinkParamsSchema = z35.object({
35668
+ runId: z35.string().min(1).max(200),
35669
+ shareId: z35.string().uuid()
35670
+ }).strict();
35671
+ var ArtifactTemplateListQuerySchema = z35.object({
35672
+ status: z35.enum(["active", "archived", "all"]).default("active"),
35673
+ query: z35.string().trim().min(1).max(120).optional(),
35674
+ presetKey: ArtifactTemplatePresetKeySchema.optional()
33695
35675
  }).strict();
33696
35676
  function apiError(error, errorCode = "invalid_request") {
33697
35677
  return { ok: false, error, errorCode };
@@ -33720,25 +35700,36 @@ function safeMemoryFailure(result) {
33720
35700
  errorCode in messages ? errorCode : "internal_error"
33721
35701
  );
33722
35702
  }
33723
- var scheduledResultApp = new Hono32();
35703
+ var scheduledResultApp = new Hono33();
33724
35704
  scheduledResultApp.use("/artifact-templates", createApiKeyAuth());
33725
35705
  scheduledResultApp.use("/artifact-templates/*", createApiKeyAuth());
33726
35706
  scheduledResultApp.use("/schedule-runs", createApiKeyAuth());
33727
35707
  scheduledResultApp.use("/schedule-runs/*", createApiKeyAuth());
33728
35708
  scheduledResultApp.get("/artifact-templates", async (c) => {
33729
- const status = c.req.query("status") ?? "active";
33730
- if (!["active", "archived", "all"].includes(status)) {
33731
- return c.json(apiError("status must be active, archived, or all."), 400);
33732
- }
35709
+ const parsed = ArtifactTemplateListQuerySchema.safeParse(c.req.query());
35710
+ if (!parsed.success) return c.json(apiError("The template search filters are invalid."), 400);
35711
+ const { status, query: searchText, presetKey } = parsed.data;
33733
35712
  const { key, error } = await memoryContext(c);
33734
35713
  if (!key) return c.json(apiError(error ?? "Memory is unavailable.", "internal_error"), 503);
33735
35714
  const result = await memoryCall("list_artifact_templates", { status }, key);
33736
35715
  if (!result.ok) return c.json(safeMemoryFailure(result), memoryStatus(result));
33737
35716
  c.header("Cache-Control", "private, no-store");
33738
- return c.json({
33739
- ...result,
33740
- presets: Array.isArray(result.presets) ? result.presets : [SCHEDULED_ARTIFACT_TEMPLATE_PRESET]
33741
- });
35717
+ const normalizedQuery = searchText?.toLocaleLowerCase();
35718
+ const presets = scheduledArtifactTemplatePresets.filter((preset) => {
35719
+ if (presetKey && preset.presetKey !== presetKey) return false;
35720
+ if (!normalizedQuery) return true;
35721
+ return [preset.name, preset.description, preset.presetKey].some((value) => value.toLocaleLowerCase().includes(normalizedQuery));
35722
+ });
35723
+ const templates = (Array.isArray(result.templates) ? result.templates : []).filter((template) => {
35724
+ if (!template || typeof template !== "object") return false;
35725
+ const record = template;
35726
+ const currentVersion = record.currentVersion && typeof record.currentVersion === "object" ? record.currentVersion : null;
35727
+ const rendererKey = typeof currentVersion?.rendererKey === "string" ? currentVersion.rendererKey : "";
35728
+ if (presetKey && rendererKey !== presetKey) return false;
35729
+ if (!normalizedQuery) return true;
35730
+ return [record.name, record.description, rendererKey, currentVersion?.authoringInstructions].filter((value) => typeof value === "string").some((value) => value.toLocaleLowerCase().includes(normalizedQuery));
35731
+ });
35732
+ return c.json({ ok: true, presets, templates });
33742
35733
  });
33743
35734
  scheduledResultApp.post("/artifact-templates", async (c) => {
33744
35735
  const parsed = CreateArtifactTemplateInputSchema.safeParse(await c.req.json().catch(() => null));
@@ -33856,10 +35847,10 @@ scheduledResultApp.delete("/schedule-runs/:runId/view-links/:shareId", async (c)
33856
35847
  });
33857
35848
 
33858
35849
  // src/api/public-scheduled-result-routes.ts
33859
- import { Hono as Hono33 } from "hono";
35850
+ import { Hono as Hono34 } from "hono";
33860
35851
 
33861
35852
  // src/scheduled-artifacts/scheduled-run-artifact-store.ts
33862
- import { createHash as createHash16 } from "crypto";
35853
+ import { createHash as createHash20 } from "crypto";
33863
35854
  var SCHEDULED_RUN_ARTIFACT_PREFIX = "scheduled-run-artifacts/";
33864
35855
  var SCHEDULED_RUN_ARTIFACT_DOWNLOAD_TTL_MS = 15 * 60 * 1e3;
33865
35856
  var SCHEDULED_RUN_ARTIFACT_MAX_BYTES = 2e6;
@@ -33875,7 +35866,7 @@ function policy3() {
33875
35866
  };
33876
35867
  }
33877
35868
  function runStorageSegment(runId) {
33878
- return /^[a-zA-Z0-9_-]{1,160}$/.test(runId) ? runId : `run-${createHash16("sha256").update(runId).digest("hex").slice(0, 32)}`;
35869
+ return /^[a-zA-Z0-9_-]{1,160}$/.test(runId) ? runId : `run-${createHash20("sha256").update(runId).digest("hex").slice(0, 32)}`;
33879
35870
  }
33880
35871
  async function createScheduledRunArtifact(args) {
33881
35872
  if (args.rendered.bytes > SCHEDULED_RUN_ARTIFACT_MAX_BYTES) {
@@ -33918,7 +35909,7 @@ async function readScheduledRunArtifact(args) {
33918
35909
  }
33919
35910
 
33920
35911
  // src/api/public-scheduled-result-routes.ts
33921
- var publicScheduledResultApp = new Hono33();
35912
+ var publicScheduledResultApp = new Hono34();
33922
35913
  function allowedFrameAncestors() {
33923
35914
  const configured = (process.env.SCHEDULED_ARTIFACT_FRAME_ANCESTORS ?? "").split(",").map((value) => value.trim()).filter((value) => /^https?:\/\/[^/\s]+$/.test(value));
33924
35915
  const origins = configured.length ? configured : ["https://mcpscraper.dev", "https://www.mcpscraper.dev"];
@@ -33979,10 +35970,10 @@ publicScheduledResultApp.get("/scheduled-run-view-links/:token/content", async (
33979
35970
  });
33980
35971
 
33981
35972
  // src/api/scheduled-artifact-internal-routes.ts
33982
- import { Hono as Hono34 } from "hono";
35973
+ import { Hono as Hono35 } from "hono";
33983
35974
 
33984
35975
  // src/api/scheduler-integration-auth.ts
33985
- import { createHash as createHash17, createHmac as createHmac4, timingSafeEqual as timingSafeEqual3 } from "crypto";
35976
+ import { createHash as createHash21, createHmac as createHmac4, timingSafeEqual as timingSafeEqual3 } from "crypto";
33986
35977
 
33987
35978
  // src/api/service-connections.ts
33988
35979
  import { randomUUID as randomUUID18 } from "crypto";
@@ -34275,12 +36266,12 @@ var SchedulerIntegrationAuthError = class extends Error {
34275
36266
  }
34276
36267
  };
34277
36268
  function signingSecret() {
34278
- const secret2 = process.env.SCHEDULER_INTEGRATION_SIGNING_SECRET?.trim();
34279
- if (!secret2) throw new SchedulerIntegrationAuthError("not_configured");
34280
- return secret2;
36269
+ const secret = process.env.SCHEDULER_INTEGRATION_SIGNING_SECRET?.trim();
36270
+ if (!secret) throw new SchedulerIntegrationAuthError("not_configured");
36271
+ return secret;
34281
36272
  }
34282
36273
  function schedulerIntegrationSignature(args) {
34283
- const bodyHash = createHash17("sha256").update(args.body).digest("hex");
36274
+ const bodyHash = createHash21("sha256").update(args.body).digest("hex");
34284
36275
  return createHmac4("sha256", args.secret).update(`${args.method.toUpperCase()}
34285
36276
  ${args.path}
34286
36277
  ${args.timestamp}
@@ -34335,36 +36326,8 @@ async function verifySchedulerIntegrationRequest(request, rawBody) {
34335
36326
  return { requestId };
34336
36327
  }
34337
36328
 
34338
- // src/scheduled-artifacts/scheduled-artifact-renderer-registry.ts
34339
- var editorialReadingRoomV1 = {
34340
- rendererKey: "editorial_reading_room_v1",
34341
- rendererVersion: 1,
34342
- async render(rawInput, rawConfig, generatedAt) {
34343
- const input = EditorialReadingRoomV1InputSchema.parse(rawInput);
34344
- const config = ArtifactTemplateConfigSchema.parse(rawConfig);
34345
- const rendered = renderEditorialReadingRoomV1(input, config, generatedAt);
34346
- if (rendered.bytes > 2e6) {
34347
- throw new Error("Scheduled artifact HTML must be 2,000,000 bytes or fewer.");
34348
- }
34349
- return {
34350
- html: rendered.html,
34351
- filename: rendered.filename,
34352
- title: input.title,
34353
- bytes: rendered.bytes,
34354
- sha256: rendered.sha256,
34355
- generatedAt: rendered.generatedAt
34356
- };
34357
- }
34358
- };
34359
- var scheduledArtifactRendererRegistry = Object.freeze({
34360
- "editorial_reading_room_v1:1": editorialReadingRoomV1
34361
- });
34362
- function getScheduledArtifactRenderer(rendererKey, rendererVersion) {
34363
- return scheduledArtifactRendererRegistry[`${rendererKey}:${rendererVersion}`] ?? null;
34364
- }
34365
-
34366
36329
  // src/api/scheduled-artifact-internal-routes.ts
34367
- var scheduledArtifactInternalApp = new Hono34();
36330
+ var scheduledArtifactInternalApp = new Hono35();
34368
36331
  function errorPayload(errorCode, error, requestId) {
34369
36332
  return ApiErrorResponseSchema.parse({
34370
36333
  ok: false,
@@ -34484,7 +36447,7 @@ function startSiteAuditWorker() {
34484
36447
  }
34485
36448
 
34486
36449
  // src/api/server.ts
34487
- import { createMiddleware as createMiddleware4 } from "hono/factory";
36450
+ import { createMiddleware as createMiddleware5 } from "hono/factory";
34488
36451
  import { getCookie as getCookie2, setCookie as setCookie2, deleteCookie } from "hono/cookie";
34489
36452
  import Stripe2 from "stripe";
34490
36453
 
@@ -34564,13 +36527,13 @@ async function reconcileSiteExtractSettlements(limit = 25) {
34564
36527
  }
34565
36528
 
34566
36529
  // src/api/page-diff.ts
34567
- import { createHash as createHash18 } from "crypto";
36530
+ import { createHash as createHash22 } from "crypto";
34568
36531
  import { diffLines } from "diff";
34569
36532
  var MAX_SNAPSHOT_CONTENT_CHARS = 25e4;
34570
36533
  var MAX_DIFF_HUNKS = 200;
34571
36534
  var MAX_DIFF_LINES_PER_RESPONSE = 2e3;
34572
36535
  function sha256Hex(value) {
34573
- return createHash18("sha256").update(value).digest("hex");
36536
+ return createHash22("sha256").update(value).digest("hex");
34574
36537
  }
34575
36538
  function truncateForStorage(value, maxChars = MAX_SNAPSHOT_CONTENT_CHARS) {
34576
36539
  if (value.length <= maxChars) return { value, truncated: false };
@@ -34684,10 +36647,10 @@ async function persistScrapeBody(user, opts) {
34684
36647
  }
34685
36648
 
34686
36649
  // src/api/scrape-image-sink.ts
34687
- import { createHash as createHash19 } from "crypto";
36650
+ import { createHash as createHash23 } from "crypto";
34688
36651
  var MAX_IMAGES_PER_SCRAPE = 25;
34689
36652
  function idempotencyKey2(userId, vault, input) {
34690
- return `scrape-image-${createHash19("sha256").update(`${userId}\0${vault}\0${input.sourceKind}\0${input.sourceUrl}\0${input.imageUrl ?? ""}\0${input.imageBase64 ?? ""}`).digest("hex")}`;
36653
+ return `scrape-image-${createHash23("sha256").update(`${userId}\0${vault}\0${input.sourceKind}\0${input.sourceUrl}\0${input.imageUrl ?? ""}\0${input.imageBase64 ?? ""}`).digest("hex")}`;
34691
36654
  }
34692
36655
  async function persistScrapeImagesToMemory(user, inputs, vault) {
34693
36656
  const selected = inputs.slice(0, MAX_IMAGES_PER_SCRAPE);
@@ -35159,7 +37122,7 @@ function inferredWaybackCapturedAt(content) {
35159
37122
  }
35160
37123
 
35161
37124
  // src/api/connection-memory-import.ts
35162
- import { createHash as createHash20 } from "crypto";
37125
+ import { createHash as createHash24 } from "crypto";
35163
37126
  var CONNECTION_MEMORY_IMPORT_MAX_ARGS_BYTES = 64 * 1024;
35164
37127
  var CONNECTION_MEMORY_IMPORT_MAX_RESULT_BYTES = 1e6;
35165
37128
  var CONNECTION_MEMORY_IMPORT_MAX_STRING_CHARS = 256e3;
@@ -35189,7 +37152,7 @@ function canonicalJson(value) {
35189
37152
  return JSON.stringify(value);
35190
37153
  }
35191
37154
  function sha2562(value) {
35192
- return createHash20("sha256").update(value).digest("hex");
37155
+ return createHash24("sha256").update(value).digest("hex");
35193
37156
  }
35194
37157
  function sensitiveKey(key) {
35195
37158
  const normalized = key.toLowerCase().replace(/[^a-z0-9]/g, "");
@@ -36200,7 +38163,7 @@ async function dbUsage(identity, plan) {
36200
38163
  }
36201
38164
 
36202
38165
  // src/api/nango-control.ts
36203
- import { createHash as createHash22, randomUUID as randomUUID22 } from "crypto";
38166
+ import { createHash as createHash26, randomUUID as randomUUID22 } from "crypto";
36204
38167
 
36205
38168
  // src/api/slack-archive-analysis.ts
36206
38169
  function isRecord2(value) {
@@ -36574,7 +38537,7 @@ async function exportSlackChannelPage(input, dependencies) {
36574
38537
  }
36575
38538
 
36576
38539
  // src/api/main-nango-transport.ts
36577
- import { createHash as createHash21, randomUUID as randomUUID21 } from "crypto";
38540
+ import { createHash as createHash25, randomUUID as randomUUID21 } from "crypto";
36578
38541
  import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client";
36579
38542
 
36580
38543
  // src/api/connected-tool-policy.ts
@@ -36707,9 +38670,9 @@ function arrayAt(payload, key) {
36707
38670
  return Array.isArray(data?.[key]) ? data[key] : [];
36708
38671
  }
36709
38672
  function nangoSecret() {
36710
- const secret2 = process.env.NANGO_SECRET_KEY?.trim();
36711
- if (!secret2) throw new MainNangoTransportError("Nango is not configured.", 503, "nango_not_configured");
36712
- return secret2;
38673
+ const secret = process.env.NANGO_SECRET_KEY?.trim();
38674
+ if (!secret) throw new MainNangoTransportError("Nango is not configured.", 503, "nango_not_configured");
38675
+ return secret;
36713
38676
  }
36714
38677
  function nangoMcpUrl() {
36715
38678
  try {
@@ -36967,7 +38930,7 @@ async function listNangoToolsDirect(identity, connectionId) {
36967
38930
  });
36968
38931
  const readTools = [...policies.values()].filter((policy4) => policy4.classification === "read").map((policy4) => policy4.name);
36969
38932
  const actionTools = [...policies.values()].filter((policy4) => policy4.classification === "action").map((policy4) => policy4.name);
36970
- const revision = createHash21("sha256").update(JSON.stringify(tools.map((tool) => ({ name: tool.name, inputSchema: tool.inputSchema })))).digest("hex");
38933
+ const revision = createHash25("sha256").update(JSON.stringify(tools.map((tool) => ({ name: tool.name, inputSchema: tool.inputSchema })))).digest("hex");
36971
38934
  await updateServiceConnectionTools(connection.id, readTools, actionTools, revision);
36972
38935
  const refreshed = await getOwnedServiceConnection(identity, connection.id);
36973
38936
  return { connection: refreshed ?? { ...connection, readTools, actionTools, toolRevision: revision }, tools };
@@ -36995,7 +38958,7 @@ async function callNangoToolDirect(args) {
36995
38958
  ratePolicyVersion: CONNECTED_USAGE_RATE_POLICY_VERSION
36996
38959
  });
36997
38960
  const requestId = args.requestId?.trim() || randomUUID21();
36998
- const idempotencyKey3 = `main-nango:${createHash21("sha256").update(args.identity.toLowerCase()).update("\0").update(connection.id).update("\0").update(args.tool).update("\0").update(requestId).digest("hex")}`;
38961
+ const idempotencyKey3 = `main-nango:${createHash25("sha256").update(args.identity.toLowerCase()).update("\0").update(connection.id).update("\0").update(args.tool).update("\0").update(requestId).digest("hex")}`;
36999
38962
  const startedAt = /* @__PURE__ */ new Date();
37000
38963
  const started = performance.now();
37001
38964
  let result;
@@ -37020,7 +38983,7 @@ async function callNangoToolDirect(args) {
37020
38983
  toolName: args.tool,
37021
38984
  operationKind: args.operationKind ?? args.classification,
37022
38985
  outcome: providerError ? "error" : "partial",
37023
- requestId: requestId.length <= 200 ? requestId : createHash21("sha256").update(requestId).digest("hex"),
38986
+ requestId: requestId.length <= 200 ? requestId : createHash25("sha256").update(requestId).digest("hex"),
37024
38987
  startedAt: startedAt.toISOString(),
37025
38988
  completedAt: completedAt.toISOString()
37026
38989
  }
@@ -37062,7 +39025,7 @@ async function describeNangoToolDirect(identity, connectionId, toolName) {
37062
39025
  providerContractHash: MAIN_INTEGRATION_CONTRACT_HASH,
37063
39026
  protocolVersion: null,
37064
39027
  schemaSource: "live_tools_list",
37065
- schemaHash: createHash21("sha256").update(JSON.stringify(projected)).digest("hex"),
39028
+ schemaHash: createHash25("sha256").update(JSON.stringify(projected)).digest("hex"),
37066
39029
  fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
37067
39030
  };
37068
39031
  }
@@ -37208,9 +39171,9 @@ function controlBaseUrl() {
37208
39171
  return (process.env.NANGO_CONTROL_URL?.trim() || DEFAULT_NANGO_CONTROL_URL).replace(/\/$/, "");
37209
39172
  }
37210
39173
  function controlSecret() {
37211
- const secret2 = process.env.SCHEDULE_INTEGRATIONS_SECRET?.trim();
37212
- if (!secret2) throw new NangoControlError("Scheduled service connections are not configured.", 503);
37213
- return secret2;
39174
+ const secret = process.env.SCHEDULE_INTEGRATIONS_SECRET?.trim();
39175
+ if (!secret) throw new NangoControlError("Scheduled service connections are not configured.", 503);
39176
+ return secret;
37214
39177
  }
37215
39178
  function isRecord4(value) {
37216
39179
  return !!value && typeof value === "object" && !Array.isArray(value);
@@ -37786,8 +39749,8 @@ async function setScheduleConnectionActionsEnabled(identity, connectionId, enabl
37786
39749
  return data.connection.actionsEnabled === true;
37787
39750
  }
37788
39751
  async function callScheduleConnectionAction(identity, connectionId, input, tool, idempotencyKey3) {
37789
- const requestId = `main-connected-action:${createHash22("sha256").update(identity).update("\0").update(idempotencyKey3?.trim() || randomUUID22()).digest("hex")}`;
37790
- const requestDigest = createHash22("sha256").update(connectionId).update("\0").update(tool?.trim() ?? "").update("\0").update(canonicalJson2(input)).digest("hex");
39752
+ const requestId = `main-connected-action:${createHash26("sha256").update(identity).update("\0").update(idempotencyKey3?.trim() || randomUUID22()).digest("hex")}`;
39753
+ const requestDigest = createHash26("sha256").update(connectionId).update("\0").update(tool?.trim() ?? "").update("\0").update(canonicalJson2(input)).digest("hex");
37791
39754
  if (mainOwnsIntegrations()) {
37792
39755
  const selectedTool = tool?.trim();
37793
39756
  if (!selectedTool) throw new NangoControlError("An action tool is required.", 400, "invalid_request", false);
@@ -37938,7 +39901,7 @@ function canonicalJson2(value) {
37938
39901
  return JSON.stringify(value);
37939
39902
  }
37940
39903
  function projectedToolSchemaHash(tool) {
37941
- return createHash22("sha256").update(canonicalJson2(tool)).digest("hex");
39904
+ return createHash26("sha256").update(canonicalJson2(tool)).digest("hex");
37942
39905
  }
37943
39906
  async function describeNangoTool(identity, connectionId, tool, fresh) {
37944
39907
  if (mainOwnsIntegrations()) {
@@ -38217,7 +40180,7 @@ async function callMainOwnedExportPage(identity, input) {
38217
40180
  }
38218
40181
 
38219
40182
  // src/api/resend-control.ts
38220
- import { createHash as createHash23 } from "crypto";
40183
+ import { createHash as createHash27 } from "crypto";
38221
40184
  var DEFAULT_CONNECTION_CONTROL_URL = "https://mcp-scraper-scheduler.vercel.app";
38222
40185
  var RESEND_PROVIDER_CONFIG_KEY = "resend";
38223
40186
  var RESEND_LOGO_URL = "https://cdn.resend.com/brand/resend-icon-black.svg";
@@ -38257,9 +40220,9 @@ function controlBaseUrl2() {
38257
40220
  return (process.env.SCHEDULE_INTEGRATIONS_CONTROL_URL?.trim() || process.env.NANGO_CONTROL_URL?.trim() || DEFAULT_CONNECTION_CONTROL_URL).replace(/\/$/, "");
38258
40221
  }
38259
40222
  function controlSecret2() {
38260
- const secret2 = process.env.SCHEDULE_INTEGRATIONS_SECRET?.trim();
38261
- if (!secret2) throw new ResendControlError("Resend connections are not configured.", 503);
38262
- return secret2;
40223
+ const secret = process.env.SCHEDULE_INTEGRATIONS_SECRET?.trim();
40224
+ if (!secret) throw new ResendControlError("Resend connections are not configured.", 503);
40225
+ return secret;
38263
40226
  }
38264
40227
  function isRecord5(value) {
38265
40228
  return !!value && typeof value === "object" && !Array.isArray(value);
@@ -38476,7 +40439,7 @@ async function callResendRead(identity, connectionId, tool, args) {
38476
40439
  return isRecord5(data) ? data.result ?? data : data;
38477
40440
  }
38478
40441
  async function callResendAction(identity, connectionId, tool, input, idempotencyKey3) {
38479
- const requestId = `main-resend-action:${createHash23("sha256").update(identity).update("\0").update(idempotencyKey3.trim()).digest("hex")}`;
40442
+ const requestId = `main-resend-action:${createHash27("sha256").update(identity).update("\0").update(idempotencyKey3.trim()).digest("hex")}`;
38480
40443
  const body = await controlRequest2("/api/internal/resend/actions/call", {
38481
40444
  method: "POST",
38482
40445
  headers: { "x-request-id": requestId },
@@ -38544,7 +40507,7 @@ async function callResendExportPage(identity, input) {
38544
40507
 
38545
40508
  // src/api/server.ts
38546
40509
  var secureCookies2 = process.env.NODE_ENV === "production" || process.env.VERCEL === "1";
38547
- var isProduction2 = secureCookies2;
40510
+ var isProduction = secureCookies2;
38548
40511
  var sessionCookieOptions2 = {
38549
40512
  httpOnly: true,
38550
40513
  secure: secureCookies2,
@@ -38558,7 +40521,7 @@ function configuredOrigins() {
38558
40521
  const trimmed = raw.trim();
38559
40522
  if (trimmed) origins.add(trimmed);
38560
40523
  }
38561
- if (!isProduction2) {
40524
+ if (!isProduction) {
38562
40525
  origins.add("http://localhost:5173");
38563
40526
  origins.add("http://localhost:3000");
38564
40527
  origins.add("http://localhost:63316");
@@ -38584,13 +40547,13 @@ async function enforceRateLimit(c, scope, key, limit, windowSeconds) {
38584
40547
  { "Retry-After": String(result.resetSeconds) }
38585
40548
  );
38586
40549
  }
38587
- var requireAllowedOrigin = createMiddleware4(async (c, next) => {
40550
+ var requireAllowedOrigin = createMiddleware5(async (c, next) => {
38588
40551
  const origin = c.req.header("origin");
38589
40552
  if (!origin) return next();
38590
40553
  if (!configuredOrigins().has(origin)) return c.json({ error: "Origin not allowed" }, 403);
38591
40554
  return next();
38592
40555
  });
38593
- var auth3 = createMiddleware4(async (c, next) => {
40556
+ var auth4 = createMiddleware5(async (c, next) => {
38594
40557
  const key = c.req.header("x-api-key");
38595
40558
  if (!key) return c.json({ error: "Missing API key" }, 401);
38596
40559
  const user = await getUserByApiKey(key);
@@ -38602,7 +40565,7 @@ var auth3 = createMiddleware4(async (c, next) => {
38602
40565
  if (cost && cost.userId == null) cost.userId = Number(refreshed.id);
38603
40566
  return next();
38604
40567
  });
38605
- var sessionAuth = createMiddleware4(async (c, next) => {
40568
+ var sessionAuth = createMiddleware5(async (c, next) => {
38606
40569
  const token4 = getCookie2(c, "session");
38607
40570
  if (!token4) return c.json({ error: "Not authenticated" }, 401);
38608
40571
  const userId = verifySession(token4);
@@ -38614,7 +40577,7 @@ var sessionAuth = createMiddleware4(async (c, next) => {
38614
40577
  c.set("sessionUser", { ...refreshed, balance_mc: balanceMc });
38615
40578
  return next();
38616
40579
  });
38617
- var requireIntegrationsTier = createMiddleware4(async (c, next) => {
40580
+ var requireIntegrationsTier = createMiddleware5(async (c, next) => {
38618
40581
  const user = c.get("user");
38619
40582
  if (!user.subscription_tier) {
38620
40583
  return c.json({
@@ -38625,27 +40588,27 @@ var requireIntegrationsTier = createMiddleware4(async (c, next) => {
38625
40588
  }
38626
40589
  return next();
38627
40590
  });
38628
- var requirePaidSchedulingTier = createMiddleware4(async (c, next) => {
40591
+ var requirePaidSchedulingTier = createMiddleware5(async (c, next) => {
38629
40592
  const user = c.get("user");
38630
40593
  if (!hasPaidSchedulingAccess(user)) return c.json(paidPlanRequiredPayload(), 403);
38631
40594
  return next();
38632
40595
  });
38633
- var app = new Hono35();
40596
+ var app = new Hono36();
38634
40597
  var deploymentProfile = resolveDeploymentProfile();
38635
40598
  var STRIPE_API_VERSION = "2026-02-25.clover";
38636
40599
  function permitsLocalApiRequest(request) {
38637
40600
  return (deploymentProfile === "development" || deploymentProfile === "test" || process.env.MCP_SCRAPER_ALLOW_PRIVATE_NETWORK === "1") && request.headers.get("x-mcp-scraper-local-network") === "1" && isLoopbackBaseUrl(request.url);
38638
40601
  }
38639
40602
  function requireStripeSecret() {
38640
- const secret2 = process.env.STRIPE_SECRET_KEY?.trim();
38641
- if (secret2) return secret2;
40603
+ const secret = process.env.STRIPE_SECRET_KEY?.trim();
40604
+ if (secret) return secret;
38642
40605
  if (process.env.NODE_ENV === "production" || process.env.VERCEL === "1") return null;
38643
40606
  return null;
38644
40607
  }
38645
40608
  async function createSignupStripeCustomer(email) {
38646
- const secret2 = requireStripeSecret();
38647
- if (!secret2) return null;
38648
- const stripeClient = new Stripe2(secret2, { apiVersion: STRIPE_API_VERSION });
40609
+ const secret = requireStripeSecret();
40610
+ if (!secret) return null;
40611
+ const stripeClient = new Stripe2(secret, { apiVersion: STRIPE_API_VERSION });
38649
40612
  const customer = await stripeClient.customers.create({
38650
40613
  email,
38651
40614
  description: "MCP Scraper free account",
@@ -38796,9 +40759,9 @@ app.post("/account/delete", requireAllowedOrigin, sessionAuth, async (c) => {
38796
40759
  return c.json({ error: "Could not remove every connected account \u2014 please retry before deleting the account." }, 502);
38797
40760
  }
38798
40761
  }
38799
- const secret2 = requireStripeSecret();
38800
- if (secret2 && user.stripe_customer_id) {
38801
- const stripeClient = new Stripe2(secret2, { apiVersion: STRIPE_API_VERSION });
40762
+ const secret = requireStripeSecret();
40763
+ if (secret && user.stripe_customer_id) {
40764
+ const stripeClient = new Stripe2(secret, { apiVersion: STRIPE_API_VERSION });
38802
40765
  const subs = await stripeClient.subscriptions.list({ customer: user.stripe_customer_id, status: "all", limit: 100 });
38803
40766
  for (const sub of subs.data) {
38804
40767
  if (sub.status === "active" || sub.status === "trialing" || sub.status === "past_due") {
@@ -38935,7 +40898,7 @@ function maybeProvisionInBackground(user, vaultCount) {
38935
40898
  }
38936
40899
  })();
38937
40900
  }
38938
- app.get("/memory/overview", auth3, async (c) => {
40901
+ app.get("/memory/overview", auth4, async (c) => {
38939
40902
  const user = c.get("user");
38940
40903
  const identity = resolveMemoryIdentity(user);
38941
40904
  const plan = memoryPlanForReads(user);
@@ -38963,7 +40926,7 @@ app.get("/memory/overview", auth3, async (c) => {
38963
40926
  return c.json({ ok: false, error: message }, 502);
38964
40927
  }
38965
40928
  });
38966
- app.get("/memory/vaults", auth3, async (c) => {
40929
+ app.get("/memory/vaults", auth4, async (c) => {
38967
40930
  const user = c.get("user");
38968
40931
  try {
38969
40932
  const vaults = await dbListVaults(resolveMemoryIdentity(user));
@@ -38973,7 +40936,7 @@ app.get("/memory/vaults", auth3, async (c) => {
38973
40936
  return c.json({ ok: false, error: err instanceof Error ? err.message : "could not load vaults" }, 502);
38974
40937
  }
38975
40938
  });
38976
- app.post("/memory/mcp-call", auth3, async (c) => {
40939
+ app.post("/memory/mcp-call", auth4, async (c) => {
38977
40940
  const user = c.get("user");
38978
40941
  const body = await c.req.json().catch(() => ({}));
38979
40942
  if (!body.toolName) return c.json({ ok: false, error: "toolName is required" }, 400);
@@ -38994,13 +40957,13 @@ app.post("/memory/mcp-call", auth3, async (c) => {
38994
40957
  const result = await memoryCall(body.toolName, body.args ?? {}, key);
38995
40958
  return c.json(result);
38996
40959
  });
38997
- app.get("/memory/approved-senders", auth3, async (c) => {
40960
+ app.get("/memory/approved-senders", auth4, async (c) => {
38998
40961
  const { key, error } = await getOrCreateUserMemoryKey(c.get("user"));
38999
40962
  if (!key) return c.json({ ok: false, error: error ?? "memory unavailable" }, 502);
39000
40963
  const r = await memoryCall("listApprovedSendersTool", {}, key);
39001
40964
  return c.json(r);
39002
40965
  });
39003
- app.post("/memory/approved-senders", auth3, async (c) => {
40966
+ app.post("/memory/approved-senders", auth4, async (c) => {
39004
40967
  const { key, error } = await getOrCreateUserMemoryKey(c.get("user"));
39005
40968
  if (!key) return c.json({ ok: false, error: error ?? "memory unavailable" }, 502);
39006
40969
  const body = await c.req.json().catch(() => ({}));
@@ -39009,14 +40972,14 @@ app.post("/memory/approved-senders", auth3, async (c) => {
39009
40972
  const r = await memoryCall("approveSenderTool", { senderIdentity }, key);
39010
40973
  return c.json(r);
39011
40974
  });
39012
- app.delete("/memory/approved-senders/:identity", auth3, async (c) => {
40975
+ app.delete("/memory/approved-senders/:identity", auth4, async (c) => {
39013
40976
  const { key, error } = await getOrCreateUserMemoryKey(c.get("user"));
39014
40977
  if (!key) return c.json({ ok: false, error: error ?? "memory unavailable" }, 502);
39015
40978
  const senderIdentity = c.req.param("identity");
39016
40979
  const r = await memoryCall("removeApprovedSenderTool", { senderIdentity }, key);
39017
40980
  return c.json(r);
39018
40981
  });
39019
- app.get("/memory/connect", auth3, async (c) => {
40982
+ app.get("/memory/connect", auth4, async (c) => {
39020
40983
  const { key, error } = await getOrCreateUserMemoryKey(c.get("user"));
39021
40984
  if (!key) return c.json({ error: error ?? "memory unavailable" }, 502);
39022
40985
  const url = "https://memory.mcpscraper.dev/mcp";
@@ -39031,7 +40994,7 @@ app.get("/memory/connect", auth3, async (c) => {
39031
40994
  const codeCommand = `claude mcp add --transport http memory ${url} --header "Authorization: Bearer ${key}"`;
39032
40995
  return c.json({ ok: true, url, key, config, codeCommand });
39033
40996
  });
39034
- app.post("/memory/vault", auth3, async (c) => {
40997
+ app.post("/memory/vault", auth4, async (c) => {
39035
40998
  const { key, error } = await getOrCreateUserMemoryKey(c.get("user"));
39036
40999
  if (!key) return c.json({ error: error ?? "memory unavailable" }, 502);
39037
41000
  const body = await c.req.json().catch(() => ({}));
@@ -39040,7 +41003,7 @@ app.post("/memory/vault", auth3, async (c) => {
39040
41003
  const result = await memoryCall("addVaultTool", { vault }, key);
39041
41004
  return c.json(result, result.ok ? 200 : 502);
39042
41005
  });
39043
- app.get("/memory/notes", auth3, async (c) => {
41006
+ app.get("/memory/notes", auth4, async (c) => {
39044
41007
  const user = c.get("user");
39045
41008
  const identity = resolveMemoryIdentity(user);
39046
41009
  try {
@@ -39056,7 +41019,7 @@ app.get("/memory/notes", auth3, async (c) => {
39056
41019
  return c.json({ ok: false, error: err instanceof Error ? err.message : "could not load notes" }, 502);
39057
41020
  }
39058
41021
  });
39059
- app.get("/memory/note", auth3, async (c) => {
41022
+ app.get("/memory/note", auth4, async (c) => {
39060
41023
  const user = c.get("user");
39061
41024
  const vault = c.req.query("vault")?.trim();
39062
41025
  const path5 = c.req.query("path");
@@ -39070,7 +41033,7 @@ app.get("/memory/note", auth3, async (c) => {
39070
41033
  return c.json({ ok: false, error: err instanceof Error ? err.message : "could not load note" }, 502);
39071
41034
  }
39072
41035
  });
39073
- app.get("/memory/universe", auth3, async (c) => {
41036
+ app.get("/memory/universe", auth4, async (c) => {
39074
41037
  const user = c.get("user");
39075
41038
  const includeConnections = c.req.query("includeConnections") === "1";
39076
41039
  try {
@@ -39080,7 +41043,7 @@ app.get("/memory/universe", auth3, async (c) => {
39080
41043
  return c.json({ ok: false, error: err instanceof Error ? err.message : "could not build universe" }, 502);
39081
41044
  }
39082
41045
  });
39083
- app.put("/memory/note", auth3, async (c) => {
41046
+ app.put("/memory/note", auth4, async (c) => {
39084
41047
  const { key, error } = await getOrCreateUserMemoryKey(c.get("user"));
39085
41048
  if (!key) return c.json({ error: error ?? "memory unavailable" }, 502);
39086
41049
  const body = await c.req.json().catch(() => ({}));
@@ -39088,7 +41051,7 @@ app.put("/memory/note", auth3, async (c) => {
39088
41051
  const result = await memoryCall("putTool", { vault: body.vault, path: body.path, title: body.title, content: body.content }, key);
39089
41052
  return c.json(result, result.ok ? 200 : 502);
39090
41053
  });
39091
- app.delete("/memory/note", auth3, async (c) => {
41054
+ app.delete("/memory/note", auth4, async (c) => {
39092
41055
  const { key, error } = await getOrCreateUserMemoryKey(c.get("user"));
39093
41056
  if (!key) return c.json({ error: error ?? "memory unavailable" }, 502);
39094
41057
  const vault = c.req.query("vault");
@@ -39097,7 +41060,7 @@ app.delete("/memory/note", auth3, async (c) => {
39097
41060
  const result = await memoryCall("deleteNoteTool", { vault, path: path5 }, key);
39098
41061
  return c.json(result, result.ok ? 200 : 502);
39099
41062
  });
39100
- app.get("/memory/note/download", auth3, async (c) => {
41063
+ app.get("/memory/note/download", auth4, async (c) => {
39101
41064
  const user = c.get("user");
39102
41065
  const vault = c.req.query("vault")?.trim();
39103
41066
  const path5 = c.req.query("path");
@@ -39114,7 +41077,7 @@ app.get("/memory/note/download", auth3, async (c) => {
39114
41077
  }
39115
41078
  });
39116
41079
  });
39117
- app.get("/memory/storage", auth3, async (c) => {
41080
+ app.get("/memory/storage", auth4, async (c) => {
39118
41081
  const user = c.get("user");
39119
41082
  try {
39120
41083
  const result = await dbStorage(resolveMemoryIdentity(user), memoryPlanForReads(user));
@@ -39123,7 +41086,7 @@ app.get("/memory/storage", auth3, async (c) => {
39123
41086
  return c.json({ ok: false, error: err instanceof Error ? err.message : "could not load storage" }, 502);
39124
41087
  }
39125
41088
  });
39126
- app.get("/memory/billing", auth3, async (c) => {
41089
+ app.get("/memory/billing", auth4, async (c) => {
39127
41090
  const user = c.get("user");
39128
41091
  const plans = Object.values(MEMORY_PLANS).map((p) => ({ plan: p.plan, label: p.label, price_id: p.price_id, interval: p.interval, monthly_usd: p.monthly_usd, quota_gb: p.quota_bytes / 1e9 }));
39129
41092
  return c.json({
@@ -39134,7 +41097,7 @@ app.get("/memory/billing", auth3, async (c) => {
39134
41097
  plans
39135
41098
  });
39136
41099
  });
39137
- app.post("/memory/checkout", auth3, async (c) => {
41100
+ app.post("/memory/checkout", auth4, async (c) => {
39138
41101
  try {
39139
41102
  const user = c.get("user");
39140
41103
  const body = await c.req.json().catch(() => ({}));
@@ -39146,9 +41109,9 @@ app.post("/memory/checkout", auth3, async (c) => {
39146
41109
  error_code: "memory_already_included"
39147
41110
  }, 409);
39148
41111
  }
39149
- const secret2 = process.env.STRIPE_SECRET_KEY?.trim();
39150
- if (!secret2) return c.json({ error: "Stripe is not configured." }, 503);
39151
- const stripeClient = new Stripe2(secret2, { apiVersion: STRIPE_API_VERSION });
41112
+ const secret = process.env.STRIPE_SECRET_KEY?.trim();
41113
+ if (!secret) return c.json({ error: "Stripe is not configured." }, 503);
41114
+ const stripeClient = new Stripe2(secret, { apiVersion: STRIPE_API_VERSION });
39152
41115
  let customerId = user.stripe_customer_id;
39153
41116
  if (!customerId) {
39154
41117
  const customer = await stripeClient.customers.create({ email: user.email });
@@ -39177,13 +41140,13 @@ app.post("/memory/checkout", auth3, async (c) => {
39177
41140
  return c.json({ error: message }, 500);
39178
41141
  }
39179
41142
  });
39180
- app.post("/memory/portal", auth3, async (c) => {
41143
+ app.post("/memory/portal", auth4, async (c) => {
39181
41144
  try {
39182
41145
  const user = c.get("user");
39183
41146
  if (!user.stripe_customer_id) return c.json({ error: "No billing account yet \u2014 subscribe first." }, 409);
39184
- const secret2 = process.env.STRIPE_SECRET_KEY?.trim();
39185
- if (!secret2) return c.json({ error: "Stripe is not configured." }, 503);
39186
- const stripeClient = new Stripe2(secret2, { apiVersion: STRIPE_API_VERSION });
41147
+ const secret = process.env.STRIPE_SECRET_KEY?.trim();
41148
+ if (!secret) return c.json({ error: "Stripe is not configured." }, 503);
41149
+ const stripeClient = new Stripe2(secret, { apiVersion: STRIPE_API_VERSION });
39187
41150
  const session = await stripeClient.billingPortal.sessions.create({
39188
41151
  customer: user.stripe_customer_id,
39189
41152
  return_url: `${appOrigin()}/memory`
@@ -39195,7 +41158,7 @@ app.post("/memory/portal", auth3, async (c) => {
39195
41158
  return c.json({ error: message }, 500);
39196
41159
  }
39197
41160
  });
39198
- app.post("/schedule-checkout", auth3, async (c) => {
41161
+ app.post("/schedule-checkout", auth4, async (c) => {
39199
41162
  return c.json({
39200
41163
  error_code: "scheduled_actions_now_credit_metered",
39201
41164
  error: "Scheduled Actions is included and no longer requires a separate subscription.",
@@ -39355,7 +41318,7 @@ function renderResendOAuthResult(ok) {
39355
41318
  const message = ok ? "Your Resend account is connected privately to MCP Scraper. You can close this tab." : "The connection could not be completed. Close this tab and try again from Integrations.";
39356
41319
  return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="referrer" content="no-referrer"><title>${title}</title><style>:root{color-scheme:light dark}*{box-sizing:border-box}body{margin:0;min-height:100vh;display:grid;place-items:center;padding:24px;background:#f5f2ee;color:#26211d;font-family:ui-sans-serif,system-ui,-apple-system,sans-serif}.card{width:min(100%,440px);padding:32px;border:1px solid #d8d0c8;border-radius:16px;background:#fff;box-shadow:0 18px 55px rgba(31,24,18,.1)}.mark{width:38px;height:38px;display:grid;place-items:center;border-radius:10px;background:#26211d;color:#fff;font-size:20px;font-weight:700}h1{margin:18px 0 8px;font-size:24px;line-height:1.15}p{margin:0;color:#71675f;line-height:1.55}a{display:inline-flex;margin-top:20px;color:#b44d36;font-weight:650;text-underline-offset:3px}@media(prefers-color-scheme:dark){body{background:#171411;color:#f4eee8}.card{background:#211d19;border-color:#3b342d}.mark{background:#f4eee8;color:#211d19}p{color:#b9aea5}}</style></head><body><main class="card"><div class="mark" aria-hidden="true">R</div><h1>${title}</h1><p>${message}</p><a href="/dashboard/integrations">Return to Integrations</a></main><script>if(window.opener){try{window.opener.postMessage({type:'mcp-scraper:resend-oauth',ok:${ok}},window.location.origin)}catch(e){}}setTimeout(function(){window.close()},1200)</script></body></html>`;
39357
41320
  }
39358
- app.get("/schedule-status", auth3, requirePaidSchedulingTier, async (c) => {
41321
+ app.get("/schedule-status", auth4, requirePaidSchedulingTier, async (c) => {
39359
41322
  try {
39360
41323
  const user = c.get("user");
39361
41324
  const { key, error } = await getOrCreateUserMemoryKey(user);
@@ -39375,14 +41338,14 @@ app.get("/schedule-status", auth3, requirePaidSchedulingTier, async (c) => {
39375
41338
  return c.json({ error: message }, 500);
39376
41339
  }
39377
41340
  });
39378
- app.get("/schedule-connection-catalog", auth3, async (c) => {
41341
+ app.get("/schedule-connection-catalog", auth4, async (c) => {
39379
41342
  try {
39380
41343
  return c.json({ ok: true, catalog: await combinedConnectionCatalog() });
39381
41344
  } catch (err) {
39382
41345
  return scheduleConnectionError(c, err, "Unable to load the service connection catalog.");
39383
41346
  }
39384
41347
  });
39385
- app.get("/schedule-connections", auth3, async (c) => {
41348
+ app.get("/schedule-connections", auth4, async (c) => {
39386
41349
  try {
39387
41350
  const user = c.get("user");
39388
41351
  const connections = await combinedConnections(user.email);
@@ -39398,7 +41361,7 @@ app.get("/schedule-connections", auth3, async (c) => {
39398
41361
  return scheduleConnectionError(c, err, "Unable to load service connections.");
39399
41362
  }
39400
41363
  });
39401
- app.post("/schedule-connections/billing/reconcile", auth3, requireIntegrationsTier, async (c) => {
41364
+ app.post("/schedule-connections/billing/reconcile", auth4, requireIntegrationsTier, async (c) => {
39402
41365
  const user = c.get("user");
39403
41366
  try {
39404
41367
  return c.json({
@@ -39409,7 +41372,7 @@ app.post("/schedule-connections/billing/reconcile", auth3, requireIntegrationsTi
39409
41372
  return scheduleConnectionError(c, err, "Unable to synchronize connected-account billing.");
39410
41373
  }
39411
41374
  });
39412
- app.post("/schedule-connections/session", auth3, requireIntegrationsTier, async (c) => {
41375
+ app.post("/schedule-connections/session", auth4, requireIntegrationsTier, async (c) => {
39413
41376
  const user = c.get("user");
39414
41377
  const body = await c.req.json().catch(() => ({}));
39415
41378
  const providerConfigKey = providerConfigKeyFrom(body.providerConfigKey);
@@ -39424,7 +41387,7 @@ app.post("/schedule-connections/session", auth3, requireIntegrationsTier, async
39424
41387
  return scheduleConnectionError(c, err, "Unable to start the service connection flow.");
39425
41388
  }
39426
41389
  });
39427
- app.post("/schedule-connections/:id/reconnect-session", auth3, requireIntegrationsTier, async (c) => {
41390
+ app.post("/schedule-connections/:id/reconnect-session", auth4, requireIntegrationsTier, async (c) => {
39428
41391
  const user = c.get("user");
39429
41392
  const body = await c.req.json().catch(() => ({}));
39430
41393
  const providerConfigKey = providerConfigKeyFrom(body.providerConfigKey);
@@ -39435,7 +41398,7 @@ app.post("/schedule-connections/:id/reconnect-session", auth3, requireIntegratio
39435
41398
  return scheduleConnectionError(c, err, "Unable to start the service reconnection flow.");
39436
41399
  }
39437
41400
  });
39438
- app.delete("/schedule-connections/:id", auth3, async (c) => {
41401
+ app.delete("/schedule-connections/:id", auth4, async (c) => {
39439
41402
  const user = c.get("user");
39440
41403
  const body = await c.req.json().catch(() => ({}));
39441
41404
  const providerConfigKey = providerConfigKeyFrom(body.providerConfigKey);
@@ -39461,7 +41424,7 @@ app.delete("/schedule-connections/:id", auth3, async (c) => {
39461
41424
  return scheduleConnectionError(c, err, "Unable to disconnect this service connection.");
39462
41425
  }
39463
41426
  });
39464
- app.post("/schedule-connections/:id/enable-actions", auth3, requireIntegrationsTier, async (c) => {
41427
+ app.post("/schedule-connections/:id/enable-actions", auth4, requireIntegrationsTier, async (c) => {
39465
41428
  const user = c.get("user");
39466
41429
  const body = await c.req.json().catch(() => ({}));
39467
41430
  if (typeof body.enabled !== "boolean") return c.json({ ok: false, error: "enabled must be a boolean." }, 400);
@@ -39473,7 +41436,7 @@ app.post("/schedule-connections/:id/enable-actions", auth3, requireIntegrationsT
39473
41436
  return scheduleConnectionError(c, err, "Unable to update this connection's action setting.");
39474
41437
  }
39475
41438
  });
39476
- app.post("/schedule-connections/:id/test", auth3, requireIntegrationsTier, async (c) => {
41439
+ app.post("/schedule-connections/:id/test", auth4, requireIntegrationsTier, async (c) => {
39477
41440
  const user = c.get("user");
39478
41441
  const body = await c.req.json().catch(() => ({}));
39479
41442
  const providerConfigKey = providerConfigKeyFrom(body.providerConfigKey);
@@ -39563,7 +41526,7 @@ app.get("/oauth/resend/callback", async (c) => {
39563
41526
  return c.html(renderResendOAuthResult(false), 502);
39564
41527
  }
39565
41528
  });
39566
- app.post("/schedule-connections/actions/slack/send-message", auth3, requireIntegrationsTier, async (c) => {
41529
+ app.post("/schedule-connections/actions/slack/send-message", auth4, requireIntegrationsTier, async (c) => {
39567
41530
  const user = c.get("user");
39568
41531
  const body = await c.req.json().catch(() => ({}));
39569
41532
  const connectionId = providerConfigKeyFrom(body.connectionId);
@@ -39583,7 +41546,7 @@ app.post("/schedule-connections/actions/slack/send-message", auth3, requireInteg
39583
41546
  return scheduleConnectionError(c, err, "Unable to send the Slack message.");
39584
41547
  }
39585
41548
  });
39586
- app.post("/schedule-connections/actions/gmail/send-message", auth3, requireIntegrationsTier, async (c) => {
41549
+ app.post("/schedule-connections/actions/gmail/send-message", auth4, requireIntegrationsTier, async (c) => {
39587
41550
  const user = c.get("user");
39588
41551
  const body = await c.req.json().catch(() => ({}));
39589
41552
  const connectionId = providerConfigKeyFrom(body.connectionId);
@@ -39619,7 +41582,7 @@ function parseEmailAddress(headerValue) {
39619
41582
  const bare = value.match(/^([^\s<>]+@[^\s<>]+)$/);
39620
41583
  return bare ? { email: bare[1].trim().toLowerCase(), name: null } : null;
39621
41584
  }
39622
- app.post("/schedule-connections/actions/gmail/search-contacts", auth3, requireIntegrationsTier, async (c) => {
41585
+ app.post("/schedule-connections/actions/gmail/search-contacts", auth4, requireIntegrationsTier, async (c) => {
39623
41586
  const user = c.get("user");
39624
41587
  const body = await c.req.json().catch(() => ({}));
39625
41588
  const connectionId = providerConfigKeyFrom(body.connectionId);
@@ -39673,7 +41636,7 @@ app.post("/schedule-connections/actions/gmail/search-contacts", auth3, requireIn
39673
41636
  return scheduleConnectionError(c, err, "Unable to search Gmail contacts.");
39674
41637
  }
39675
41638
  });
39676
- app.post("/schedule-connections/actions/google-calendar/create-event", auth3, requireIntegrationsTier, async (c) => {
41639
+ app.post("/schedule-connections/actions/google-calendar/create-event", auth4, requireIntegrationsTier, async (c) => {
39677
41640
  const user = c.get("user");
39678
41641
  const body = await c.req.json().catch(() => ({}));
39679
41642
  const connectionId = providerConfigKeyFrom(body.connectionId);
@@ -39703,7 +41666,7 @@ app.post("/schedule-connections/actions/google-calendar/create-event", auth3, re
39703
41666
  return scheduleConnectionError(c, err, "Unable to create the calendar event.");
39704
41667
  }
39705
41668
  });
39706
- app.post("/schedule-connections/actions/zoom/create-meeting", auth3, requireIntegrationsTier, async (c) => {
41669
+ app.post("/schedule-connections/actions/zoom/create-meeting", auth4, requireIntegrationsTier, async (c) => {
39707
41670
  const user = c.get("user");
39708
41671
  const body = await c.req.json().catch(() => ({}));
39709
41672
  const connectionId = providerConfigKeyFrom(body.connectionId);
@@ -39729,7 +41692,7 @@ app.post("/schedule-connections/actions/zoom/create-meeting", auth3, requireInte
39729
41692
  return scheduleConnectionError(c, err, "Unable to create the Zoom meeting.");
39730
41693
  }
39731
41694
  });
39732
- app.post("/schedule-connections/actions/read", auth3, requireIntegrationsTier, async (c) => {
41695
+ app.post("/schedule-connections/actions/read", auth4, requireIntegrationsTier, async (c) => {
39733
41696
  const user = c.get("user");
39734
41697
  const body = await c.req.json().catch(() => ({}));
39735
41698
  const connectionId = providerConfigKeyFrom(body.connectionId);
@@ -39746,7 +41709,7 @@ app.post("/schedule-connections/actions/read", auth3, requireIntegrationsTier, a
39746
41709
  return scheduleConnectionError(c, err, "Unable to read from this connection.");
39747
41710
  }
39748
41711
  });
39749
- app.post("/schedule-connections/actions/import-memory", auth3, requireIntegrationsTier, async (c) => {
41712
+ app.post("/schedule-connections/actions/import-memory", auth4, requireIntegrationsTier, async (c) => {
39750
41713
  const user = c.get("user");
39751
41714
  const body = await c.req.json().catch(() => ({}));
39752
41715
  const connectionId = providerConfigKeyFrom(body.connectionId);
@@ -39803,7 +41766,7 @@ app.post("/schedule-connections/actions/import-memory", auth3, requireIntegratio
39803
41766
  return scheduleConnectionError(c, err, "Unable to import this connected-service snapshot into Memory.");
39804
41767
  }
39805
41768
  });
39806
- app.post("/schedule-connections/actions/describe", auth3, requireIntegrationsTier, async (c) => {
41769
+ app.post("/schedule-connections/actions/describe", auth4, requireIntegrationsTier, async (c) => {
39807
41770
  const user = c.get("user");
39808
41771
  const body = await c.req.json().catch(() => ({}));
39809
41772
  const connectionId = providerConfigKeyFrom(body.connectionId);
@@ -39820,7 +41783,7 @@ app.post("/schedule-connections/actions/describe", auth3, requireIntegrationsTie
39820
41783
  return scheduleConnectionError(c, err, "Unable to describe this connection tool.");
39821
41784
  }
39822
41785
  });
39823
- app.post("/schedule-connections/actions/export", auth3, requireIntegrationsTier, async (c) => {
41786
+ app.post("/schedule-connections/actions/export", auth4, requireIntegrationsTier, async (c) => {
39824
41787
  const user = c.get("user");
39825
41788
  const body = await c.req.json().catch(() => ({}));
39826
41789
  const connectionId = providerConfigKeyFrom(body.connectionId);
@@ -39907,7 +41870,7 @@ app.post("/schedule-connections/actions/export", auth3, requireIntegrationsTier,
39907
41870
  return scheduleConnectionError(c, err, "Unable to export this connected service.");
39908
41871
  }
39909
41872
  });
39910
- app.post("/schedule-connections/actions/export-search-console-table", auth3, requireIntegrationsTier, async (c) => {
41873
+ app.post("/schedule-connections/actions/export-search-console-table", auth4, requireIntegrationsTier, async (c) => {
39911
41874
  const user = c.get("user");
39912
41875
  const body = await c.req.json().catch(() => ({}));
39913
41876
  try {
@@ -39932,7 +41895,7 @@ app.post("/schedule-connections/actions/export-search-console-table", auth3, req
39932
41895
  return c.json({ ok: false, error: "Unable to export the persisted Search Console table." }, 502);
39933
41896
  }
39934
41897
  });
39935
- app.post("/schedule-connections/actions/export-download", auth3, requireIntegrationsTier, async (c) => {
41898
+ app.post("/schedule-connections/actions/export-download", auth4, requireIntegrationsTier, async (c) => {
39936
41899
  const user = c.get("user");
39937
41900
  const body = await c.req.json().catch(() => ({}));
39938
41901
  const artifactId = typeof body.artifactId === "string" ? body.artifactId.trim() : "";
@@ -39949,7 +41912,7 @@ app.post("/schedule-connections/actions/export-download", auth3, requireIntegrat
39949
41912
  return c.json({ ok: false, error: "Unable to renew this artifact download." }, 502);
39950
41913
  }
39951
41914
  });
39952
- app.post("/schedule-connections/actions/call", auth3, requireIntegrationsTier, async (c) => {
41915
+ app.post("/schedule-connections/actions/call", auth4, requireIntegrationsTier, async (c) => {
39953
41916
  const user = c.get("user");
39954
41917
  const body = await c.req.json().catch(() => ({}));
39955
41918
  const connectionId = providerConfigKeyFrom(body.connectionId);
@@ -39989,7 +41952,7 @@ app.post("/integrations/actions/read", (c) => forwardIntegrationAlias(c, "/sched
39989
41952
  app.post("/integrations/actions/describe", (c) => forwardIntegrationAlias(c, "/schedule-connections/actions/describe"));
39990
41953
  app.post("/integrations/actions/call", (c) => forwardIntegrationAlias(c, "/schedule-connections/actions/call"));
39991
41954
  app.post("/integrations/actions/export", (c) => forwardIntegrationAlias(c, "/schedule-connections/actions/export"));
39992
- app.post("/schedule-link", auth3, requirePaidSchedulingTier, async (c) => {
41955
+ app.post("/schedule-link", auth4, requirePaidSchedulingTier, async (c) => {
39993
41956
  try {
39994
41957
  const user = c.get("user");
39995
41958
  const credentials = await syncScheduledActionCredentials(user);
@@ -40006,19 +41969,19 @@ app.post("/schedule-link", auth3, requirePaidSchedulingTier, async (c) => {
40006
41969
  return c.json({ error: message }, 500);
40007
41970
  }
40008
41971
  });
40009
- app.get("/schedule-actions", auth3, async (c) => {
41972
+ app.get("/schedule-actions", auth4, async (c) => {
40010
41973
  const user = c.get("user");
40011
41974
  const { key, error } = await getOrCreateUserMemoryKey(user);
40012
41975
  if (!key) return c.json({ ok: false, error: error ?? "memory unavailable" }, 503);
40013
41976
  const r = await memoryCall("listScheduledActionsTool", {}, key);
40014
41977
  if (!r.ok || !Array.isArray(r.actions)) return c.json(r);
40015
41978
  try {
40016
- const actions = await Promise.all(r.actions.map(async (action) => {
40017
- const id = typeof action.id === "string" ? action.id : "";
40018
- const executionMode = action.executionMode === "connection_sync" ? "connection_sync" : "agent";
40019
- if (!id) return { ...action, executionMode, connections: [] };
41979
+ const actions = await Promise.all(r.actions.map(async (action2) => {
41980
+ const id = typeof action2.id === "string" ? action2.id : "";
41981
+ const executionMode = action2.executionMode === "connection_sync" ? "connection_sync" : "agent";
41982
+ if (!id) return { ...action2, executionMode, connections: [] };
40020
41983
  const bindings = await getScheduleConnectionBindings(user.email, id);
40021
- return { ...action, executionMode, connections: bindingsForSchedule(bindings, id) };
41984
+ return { ...action2, executionMode, connections: bindingsForSchedule(bindings, id) };
40022
41985
  }));
40023
41986
  return c.json({
40024
41987
  ...r,
@@ -40028,12 +41991,12 @@ app.get("/schedule-actions", auth3, async (c) => {
40028
41991
  console.warn("[schedule-actions] connection bindings unavailable:", err instanceof Error ? err.message : String(err));
40029
41992
  return c.json({
40030
41993
  ...r,
40031
- actions: r.actions.map((action) => ({ ...action, executionMode: action.executionMode === "connection_sync" ? "connection_sync" : "agent", connections: [] })),
41994
+ actions: r.actions.map((action2) => ({ ...action2, executionMode: action2.executionMode === "connection_sync" ? "connection_sync" : "agent", connections: [] })),
40032
41995
  connectionBindingsUnavailable: true
40033
41996
  });
40034
41997
  }
40035
41998
  });
40036
- app.post("/schedule-actions", auth3, requirePaidSchedulingTier, async (c) => {
41999
+ app.post("/schedule-actions", auth4, requirePaidSchedulingTier, async (c) => {
40037
42000
  const user = c.get("user");
40038
42001
  const credentials = await syncScheduledActionCredentials(user);
40039
42002
  if (!credentials.ok) {
@@ -40078,7 +42041,7 @@ app.post("/schedule-actions", auth3, requirePaidSchedulingTier, async (c) => {
40078
42041
  return response;
40079
42042
  }
40080
42043
  });
40081
- app.post("/schedule-actions/propose", auth3, async (c) => {
42044
+ app.post("/schedule-actions/propose", auth4, async (c) => {
40082
42045
  const user = c.get("user");
40083
42046
  const { key, error } = await getOrCreateUserMemoryKey(user);
40084
42047
  if (!key) return c.json({ ok: false, error: error ?? "memory unavailable" }, 503);
@@ -40086,7 +42049,7 @@ app.post("/schedule-actions/propose", auth3, async (c) => {
40086
42049
  const r = await memoryCall("proposeScheduledActionTool", body, key);
40087
42050
  return c.json(r);
40088
42051
  });
40089
- app.get("/schedule-actions/:id/connections", auth3, async (c) => {
42052
+ app.get("/schedule-actions/:id/connections", auth4, async (c) => {
40090
42053
  try {
40091
42054
  const user = c.get("user");
40092
42055
  const scheduleActionId = c.req.param("id");
@@ -40096,7 +42059,7 @@ app.get("/schedule-actions/:id/connections", auth3, async (c) => {
40096
42059
  return scheduleConnectionError(c, err, "Unable to load scheduled action service connections.");
40097
42060
  }
40098
42061
  });
40099
- app.put("/schedule-actions/:id/connections", auth3, requireIntegrationsTier, async (c) => {
42062
+ app.put("/schedule-actions/:id/connections", auth4, requireIntegrationsTier, async (c) => {
40100
42063
  const user = c.get("user");
40101
42064
  const body = await c.req.json().catch(() => ({}));
40102
42065
  let connections;
@@ -40111,7 +42074,7 @@ app.put("/schedule-actions/:id/connections", auth3, requireIntegrationsTier, asy
40111
42074
  if (!key) return c.json({ ok: false, error: error ?? "memory unavailable" }, 503);
40112
42075
  const schedules = await memoryCall("listScheduledActionsTool", {}, key);
40113
42076
  if (!schedules.ok || !Array.isArray(schedules.actions)) return c.json(schedules, 502);
40114
- const schedule = schedules.actions.find((action) => action.id === scheduleActionId);
42077
+ const schedule = schedules.actions.find((action2) => action2.id === scheduleActionId);
40115
42078
  if (!schedule) return c.json({ ok: false, error: "scheduled action not found" }, 404);
40116
42079
  if (schedule.executionMode === "connection_sync") {
40117
42080
  const policyError = connectionSyncSelectionError(connections);
@@ -40123,7 +42086,7 @@ app.put("/schedule-actions/:id/connections", auth3, requireIntegrationsTier, asy
40123
42086
  return scheduleConnectionError(c, err, "Unable to update scheduled action service connections.");
40124
42087
  }
40125
42088
  });
40126
- app.get("/schedule-actions/:id/runs", auth3, async (c) => {
42089
+ app.get("/schedule-actions/:id/runs", auth4, async (c) => {
40127
42090
  const user = c.get("user");
40128
42091
  const { key, error } = await getOrCreateUserMemoryKey(user);
40129
42092
  if (!key) return c.json({ ok: false, error: error ?? "memory unavailable" }, 503);
@@ -40132,7 +42095,7 @@ app.get("/schedule-actions/:id/runs", auth3, async (c) => {
40132
42095
  const r = await memoryCall("listScheduledActionRunsTool", { id: c.req.param("id"), limit }, key);
40133
42096
  return c.json(r);
40134
42097
  });
40135
- app.put("/schedule-actions/:id", auth3, requirePaidSchedulingTier, async (c) => {
42098
+ app.put("/schedule-actions/:id", auth4, requirePaidSchedulingTier, async (c) => {
40136
42099
  const user = c.get("user");
40137
42100
  const { key, error } = await getOrCreateUserMemoryKey(user);
40138
42101
  if (!key) return c.json({ ok: false, error: error ?? "memory unavailable" }, 503);
@@ -40147,21 +42110,21 @@ app.put("/schedule-actions/:id", auth3, requirePaidSchedulingTier, async (c) =>
40147
42110
  const r = await memoryCall("updateScheduledActionTool", { id: c.req.param("id"), ...updates }, key);
40148
42111
  return c.json(r);
40149
42112
  });
40150
- app.post("/schedule-actions/:id/pause", auth3, async (c) => {
42113
+ app.post("/schedule-actions/:id/pause", auth4, async (c) => {
40151
42114
  const user = c.get("user");
40152
42115
  const { key, error } = await getOrCreateUserMemoryKey(user);
40153
42116
  if (!key) return c.json({ ok: false, error: error ?? "memory unavailable" }, 503);
40154
42117
  const r = await memoryCall("pauseScheduledActionTool", { id: c.req.param("id") }, key);
40155
42118
  return c.json(r);
40156
42119
  });
40157
- app.post("/schedule-actions/:id/resume", auth3, requirePaidSchedulingTier, async (c) => {
42120
+ app.post("/schedule-actions/:id/resume", auth4, requirePaidSchedulingTier, async (c) => {
40158
42121
  const user = c.get("user");
40159
42122
  const { key, error } = await getOrCreateUserMemoryKey(user);
40160
42123
  if (!key) return c.json({ ok: false, error: error ?? "memory unavailable" }, 503);
40161
42124
  const r = await memoryCall("resumeScheduledActionTool", { id: c.req.param("id") }, key);
40162
42125
  return c.json(r);
40163
42126
  });
40164
- app.delete("/schedule-actions/:id", auth3, async (c) => {
42127
+ app.delete("/schedule-actions/:id", auth4, async (c) => {
40165
42128
  const user = c.get("user");
40166
42129
  const { key, error } = await getOrCreateUserMemoryKey(user);
40167
42130
  if (!key) return c.json({ ok: false, error: error ?? "memory unavailable" }, 503);
@@ -40176,7 +42139,7 @@ app.delete("/schedule-actions/:id", auth3, async (c) => {
40176
42139
  return c.json({ ...r, connectionBindingCleanupPending: true });
40177
42140
  }
40178
42141
  });
40179
- app.post("/schedule-link/revoke", auth3, async (c) => {
42142
+ app.post("/schedule-link/revoke", auth4, async (c) => {
40180
42143
  try {
40181
42144
  const user = c.get("user");
40182
42145
  const { key, error } = await getOrCreateUserMemoryKey(user);
@@ -40189,7 +42152,7 @@ app.post("/schedule-link/revoke", auth3, async (c) => {
40189
42152
  return c.json({ error: message }, 500);
40190
42153
  }
40191
42154
  });
40192
- app.post("/support/submit", auth3, async (c) => {
42155
+ app.post("/support/submit", auth4, async (c) => {
40193
42156
  try {
40194
42157
  const user = c.get("user");
40195
42158
  const body = await c.req.json().catch(() => ({}));
@@ -40217,7 +42180,7 @@ app.post("/support/submit", auth3, async (c) => {
40217
42180
  return c.json({ error: message }, 500);
40218
42181
  }
40219
42182
  });
40220
- app.get("/memory/usage", auth3, async (c) => {
42183
+ app.get("/memory/usage", auth4, async (c) => {
40221
42184
  const user = c.get("user");
40222
42185
  try {
40223
42186
  const result = await dbUsage(resolveMemoryIdentity(user), memoryPlanForReads(user));
@@ -40270,7 +42233,7 @@ async function checkHarvestLimits(user, reuseLockId) {
40270
42233
  }
40271
42234
  return null;
40272
42235
  }
40273
- app.post("/harvest", auth3, async (c) => {
42236
+ app.post("/harvest", auth4, async (c) => {
40274
42237
  const user = c.get("user");
40275
42238
  const raw = await c.req.json().catch(() => ({}));
40276
42239
  const bodyResult = HarvestBodySchema.safeParse(raw);
@@ -40353,7 +42316,7 @@ app.post("/harvest", auth3, async (c) => {
40353
42316
  }
40354
42317
  return c.json({ job_id: jobId2, status: "pending" }, 202);
40355
42318
  });
40356
- app.post("/harvest/sync", auth3, async (c) => {
42319
+ app.post("/harvest/sync", auth4, async (c) => {
40357
42320
  const user = c.get("user");
40358
42321
  const raw = await c.req.json().catch(() => ({}));
40359
42322
  const bodyResult = HarvestBodySchema.safeParse(raw);
@@ -40434,17 +42397,17 @@ app.post("/harvest/sync", auth3, async (c) => {
40434
42397
  return c.json({ job_id: jobId2, status: "failed", ...response, attempts: sanitizeAttempts(attempts) }, problem.httpStatus);
40435
42398
  }
40436
42399
  });
40437
- app.get("/jobs/:id", auth3, async (c) => {
42400
+ app.get("/jobs/:id", auth4, async (c) => {
40438
42401
  const job = await getJob(c.req.param("id"), c.get("user").id);
40439
42402
  if (!job) return c.json({ error: "Job not found" }, 404);
40440
42403
  const attempts = await listHarvestAttempts(job.id, c.get("user").id);
40441
42404
  const safeResult = job.result && typeof job.result === "object" ? sanitizeHarvestResult(job.result) : job.result;
40442
42405
  return c.json({ ...job, result: safeResult, attempts: sanitizeAttempts(attempts) });
40443
42406
  });
40444
- app.get("/jobs", auth3, async (c) => {
42407
+ app.get("/jobs", auth4, async (c) => {
40445
42408
  return c.json(await listJobs(c.get("user").id));
40446
42409
  });
40447
- app.get("/history", auth3, async (c) => {
42410
+ app.get("/history", auth4, async (c) => {
40448
42411
  const userId = c.get("user").id;
40449
42412
  const [jobs, events] = await Promise.all([
40450
42413
  listJobs(userId),
@@ -40472,7 +42435,7 @@ app.get("/history", auth3, async (c) => {
40472
42435
  const rows = [...jobRows, ...eventRows].sort((a, b) => String(b.ts).localeCompare(String(a.ts)));
40473
42436
  return c.json(rows.slice(0, 100));
40474
42437
  });
40475
- app.get("/ledger", auth3, async (c) => {
42438
+ app.get("/ledger", auth4, async (c) => {
40476
42439
  return c.json(await getLedger(c.get("user").id, 100));
40477
42440
  });
40478
42441
  app.post("/admin/users", adminAuth, async (c) => {
@@ -40501,7 +42464,7 @@ app.post("/admin/backfill-signup-credits", adminAuth, async (c) => {
40501
42464
  retired: true
40502
42465
  });
40503
42466
  });
40504
- app.post("/extract-url", auth3, async (c) => {
42467
+ app.post("/extract-url", auth4, async (c) => {
40505
42468
  const raw = await c.req.json().catch(() => ({}));
40506
42469
  const allowLocalRequest = permitsLocalApiRequest(c.req.raw);
40507
42470
  const bodyResult = (allowLocalRequest ? ExtractUrlLocalBodySchema : ExtractUrlBodySchema).safeParse(raw);
@@ -40637,7 +42600,7 @@ app.post("/extract-url", auth3, async (c) => {
40637
42600
  await releaseConcurrencyGate(gate.lockId);
40638
42601
  }
40639
42602
  });
40640
- app.post("/diff-page", auth3, async (c) => {
42603
+ app.post("/diff-page", auth4, async (c) => {
40641
42604
  const raw = await c.req.json().catch(() => ({}));
40642
42605
  const allowLocalRequest = permitsLocalApiRequest(c.req.raw);
40643
42606
  const bodyResult = (allowLocalRequest ? DiffPageLocalBodySchema : DiffPageBodySchema).safeParse(raw);
@@ -40724,7 +42687,7 @@ app.post("/diff-page", auth3, async (c) => {
40724
42687
  await releaseConcurrencyGate(gate.lockId);
40725
42688
  }
40726
42689
  });
40727
- app.post("/archive/read", auth3, async (c) => {
42690
+ app.post("/archive/read", auth4, async (c) => {
40728
42691
  const raw = await c.req.json().catch(() => ({}));
40729
42692
  const bodyResult = ArchiveReadBodySchema.safeParse(raw);
40730
42693
  if (!bodyResult.success) {
@@ -40799,7 +42762,7 @@ app.post("/archive/read", auth3, async (c) => {
40799
42762
  await releaseConcurrencyGate(gate.lockId);
40800
42763
  }
40801
42764
  });
40802
- app.post("/map-urls", auth3, async (c) => {
42765
+ app.post("/map-urls", auth4, async (c) => {
40803
42766
  const raw = await c.req.json().catch(() => ({}));
40804
42767
  const bodyResult = MapUrlsBodySchema.safeParse(raw);
40805
42768
  if (!bodyResult.success) return c.json({ error: bodyResult.error.issues[0]?.message ?? "Invalid request" }, 400);
@@ -40848,7 +42811,7 @@ app.post("/map-urls", auth3, async (c) => {
40848
42811
  await releaseConcurrencyGate(gate.lockId);
40849
42812
  }
40850
42813
  });
40851
- app.post("/wayback/snapshots", auth3, async (c) => {
42814
+ app.post("/wayback/snapshots", auth4, async (c) => {
40852
42815
  const raw = await c.req.json().catch(() => ({}));
40853
42816
  const bodyResult = WaybackInventoryBodySchema.safeParse(raw);
40854
42817
  if (!bodyResult.success) return c.json({ error: bodyResult.error.issues[0]?.message ?? "Invalid request" }, 400);
@@ -40918,7 +42881,7 @@ app.post("/wayback/snapshots", auth3, async (c) => {
40918
42881
  await releaseConcurrencyGate(gate.lockId);
40919
42882
  }
40920
42883
  });
40921
- app.post("/extract-site", auth3, async (c) => {
42884
+ app.post("/extract-site", auth4, async (c) => {
40922
42885
  const raw = await c.req.json().catch(() => ({}));
40923
42886
  const bodyResult = ExtractSiteBodySchema.safeParse(raw);
40924
42887
  if (!bodyResult.success) return c.json({ error: bodyResult.error.issues[0]?.message ?? "Invalid request" }, 400);
@@ -41141,7 +43104,7 @@ app.post("/extract-site", auth3, async (c) => {
41141
43104
  await releaseConcurrencyGate(gate.lockId);
41142
43105
  }
41143
43106
  });
41144
- app.get("/extract-site/status/:id", auth3, async (c) => {
43107
+ app.get("/extract-site/status/:id", auth4, async (c) => {
41145
43108
  const user = c.get("user");
41146
43109
  const job = await getExtractJob(c.req.param("id"));
41147
43110
  if (!job || job.userId !== user.id) return c.json({ error: "Job not found" }, 404);
@@ -41175,9 +43138,9 @@ app.post("/billing/concurrency/checkout", requireAllowedOrigin, sessionAuth, asy
41175
43138
  try {
41176
43139
  const user = c.get("sessionUser");
41177
43140
  if (user.concurrency_stripe_sub_id) return c.json({ error: "Already subscribed \u2014 cancel existing slot first to change." }, 409);
41178
- const secret2 = process.env.STRIPE_SECRET_KEY?.trim();
41179
- if (!secret2) return c.json({ error: "Stripe is not configured." }, 503);
41180
- const stripeClient = new Stripe2(secret2, { apiVersion: STRIPE_API_VERSION });
43141
+ const secret = process.env.STRIPE_SECRET_KEY?.trim();
43142
+ if (!secret) return c.json({ error: "Stripe is not configured." }, 503);
43143
+ const stripeClient = new Stripe2(secret, { apiVersion: STRIPE_API_VERSION });
41181
43144
  let customerId = user.stripe_customer_id;
41182
43145
  if (!customerId) {
41183
43146
  const customer = await stripeClient.customers.create({ email: user.email });
@@ -41239,9 +43202,9 @@ app.post("/billing/subscribe", requireAllowedOrigin, sessionAuth, async (c) => {
41239
43202
  requested_plan: getSubscriptionPlanView(tier.tier)
41240
43203
  }, 409);
41241
43204
  }
41242
- const secret2 = process.env.STRIPE_SECRET_KEY?.trim();
41243
- if (!secret2) return c.json({ error: "Stripe is not configured." }, 503);
41244
- const stripeClient = new Stripe2(secret2, { apiVersion: STRIPE_API_VERSION });
43205
+ const secret = process.env.STRIPE_SECRET_KEY?.trim();
43206
+ if (!secret) return c.json({ error: "Stripe is not configured." }, 503);
43207
+ const stripeClient = new Stripe2(secret, { apiVersion: STRIPE_API_VERSION });
41245
43208
  let customerId = user.stripe_customer_id;
41246
43209
  if (!customerId) {
41247
43210
  const customer = await stripeClient.customers.create({ email: user.email });
@@ -41302,9 +43265,9 @@ app.post("/billing/portal", requireAllowedOrigin, sessionAuth, async (c) => {
41302
43265
  try {
41303
43266
  const user = c.get("sessionUser");
41304
43267
  if (!user.stripe_customer_id) return c.json({ error: "No billing account yet \u2014 subscribe first." }, 409);
41305
- const secret2 = requireStripeSecret();
41306
- if (!secret2) return c.json({ error: "Stripe is not configured." }, 503);
41307
- const stripeClient = new Stripe2(secret2, { apiVersion: STRIPE_API_VERSION });
43268
+ const secret = requireStripeSecret();
43269
+ if (!secret) return c.json({ error: "Stripe is not configured." }, 503);
43270
+ const stripeClient = new Stripe2(secret, { apiVersion: STRIPE_API_VERSION });
41308
43271
  const session = await stripeClient.billingPortal.sessions.create({
41309
43272
  customer: user.stripe_customer_id,
41310
43273
  return_url: `${appOrigin()}/billing`
@@ -41316,7 +43279,7 @@ app.post("/billing/portal", requireAllowedOrigin, sessionAuth, async (c) => {
41316
43279
  return c.json({ error: message }, 500);
41317
43280
  }
41318
43281
  });
41319
- app.post("/billing/concurrency/terminal-checkout", auth3, async (c) => {
43282
+ app.post("/billing/concurrency/terminal-checkout", auth4, async (c) => {
41320
43283
  try {
41321
43284
  const user = c.get("user");
41322
43285
  const upgrade = concurrencySlotBillingInfo();
@@ -41332,9 +43295,9 @@ app.post("/billing/concurrency/terminal-checkout", auth3, async (c) => {
41332
43295
  }
41333
43296
  }, 409);
41334
43297
  }
41335
- const secret2 = process.env.STRIPE_SECRET_KEY?.trim();
41336
- if (!secret2) return c.json({ error: "Stripe is not configured." }, 503);
41337
- const stripeClient = new Stripe2(secret2, { apiVersion: STRIPE_API_VERSION });
43298
+ const secret = process.env.STRIPE_SECRET_KEY?.trim();
43299
+ if (!secret) return c.json({ error: "Stripe is not configured." }, 503);
43300
+ const stripeClient = new Stripe2(secret, { apiVersion: STRIPE_API_VERSION });
41338
43301
  let customerId = user.stripe_customer_id;
41339
43302
  if (!customerId) {
41340
43303
  const customer = await stripeClient.customers.create({ email: user.email });
@@ -41370,7 +43333,7 @@ app.post("/billing/concurrency/terminal-checkout", auth3, async (c) => {
41370
43333
  return c.json({ error: message }, 500);
41371
43334
  }
41372
43335
  });
41373
- app.post("/billing/subscribe/terminal-checkout", auth3, async (c) => {
43336
+ app.post("/billing/subscribe/terminal-checkout", auth4, async (c) => {
41374
43337
  try {
41375
43338
  const user = c.get("user");
41376
43339
  const raw = await c.req.json().catch(() => ({}));
@@ -41386,9 +43349,9 @@ app.post("/billing/subscribe/terminal-checkout", auth3, async (c) => {
41386
43349
  requested_plan: getSubscriptionPlanView(tier.tier)
41387
43350
  }, 409);
41388
43351
  }
41389
- const secret2 = process.env.STRIPE_SECRET_KEY?.trim();
41390
- if (!secret2) return c.json({ error: "Stripe is not configured." }, 503);
41391
- const stripeClient = new Stripe2(secret2, { apiVersion: STRIPE_API_VERSION });
43352
+ const secret = process.env.STRIPE_SECRET_KEY?.trim();
43353
+ if (!secret) return c.json({ error: "Stripe is not configured." }, 503);
43354
+ const stripeClient = new Stripe2(secret, { apiVersion: STRIPE_API_VERSION });
41392
43355
  let customerId = user.stripe_customer_id;
41393
43356
  if (!customerId) {
41394
43357
  const customer = await stripeClient.customers.create({ email: user.email });
@@ -41472,7 +43435,7 @@ app.post("/billing/concurrency/cancel", requireAllowedOrigin, sessionAuth, async
41472
43435
  await setConcurrencySubId(user.id, null);
41473
43436
  return c.json({ ok: true, extra_concurrency_slots: nextSlots, concurrency_limit: 1 + nextSlots });
41474
43437
  });
41475
- app.get("/billing/balance", auth3, async (c) => {
43438
+ app.get("/billing/balance", auth4, async (c) => {
41476
43439
  const user = c.get("user");
41477
43440
  const balanceMc = user.balance_mc;
41478
43441
  const ledger = await getLedger(user.id, 20);
@@ -41484,15 +43447,15 @@ app.get("/billing/balance", auth3, async (c) => {
41484
43447
  ledger
41485
43448
  });
41486
43449
  });
41487
- app.get("/billing/summary", auth3, billingSummaryHandler);
41488
- app.get("/billing/connected-usage/history", auth3, async (c) => {
43450
+ app.get("/billing/summary", auth4, billingSummaryHandler);
43451
+ app.get("/billing/connected-usage/history", auth4, async (c) => {
41489
43452
  return c.json({
41490
43453
  ok: true,
41491
43454
  ratePolicy: CONNECTED_USAGE_RATE_POLICY,
41492
43455
  receipts: await listConnectedUsageHistory(c.get("user").id, 100)
41493
43456
  });
41494
43457
  });
41495
- app.post("/billing/credits", auth3, async (c) => {
43458
+ app.post("/billing/credits", auth4, async (c) => {
41496
43459
  const user = c.get("user");
41497
43460
  const balanceMc = user.balance_mc;
41498
43461
  const body = await c.req.json().catch(() => ({}));
@@ -41524,14 +43487,14 @@ app.post("/billing/credits", auth3, async (c) => {
41524
43487
  });
41525
43488
  });
41526
43489
  app.get("/cron/tick", async (c) => {
41527
- const secret2 = c.req.header("authorization");
41528
- if (!process.env.CRON_SECRET || secret2 !== `Bearer ${process.env.CRON_SECRET}`) {
43490
+ const secret = c.req.header("authorization");
43491
+ if (!process.env.CRON_SECRET || secret !== `Bearer ${process.env.CRON_SECRET}`) {
41529
43492
  return c.json({ error: "Unauthorized" }, 401);
41530
43493
  }
41531
- const { drainQueue } = await import("./worker-4LZGRHDA.js");
43494
+ const { drainQueue } = await import("./worker-4MHZS3OB.js");
41532
43495
  const budget = { maxJobs: 10, deadlineMs: Date.now() + 28e4 };
41533
43496
  const workflowDispatchResult = await dispatchDueWorkflowSchedules(`${new URL(c.req.url).protocol}//${new URL(c.req.url).host}`);
41534
- const [results, sweepResult, reapResult, expiredResult, blobCleanup, connectedDataArtifactCleanup, connectedAccountBilling, directoryWorkflowDispatch, directoryWorkflowReconciliation, directoryArtifactCleanup, siteExtractArtifactCleanup, siteExtractRedispatch, siteExtractReconciliation, paaHarvestRedispatch, paaHarvestReconciliation, localSourcebookRedispatch] = await Promise.all([
43497
+ const [results, sweepResult, reapResult, expiredResult, blobCleanup, connectedDataArtifactCleanup, connectedAccountBilling, directoryWorkflowDispatch, directoryWorkflowReconciliation, directoryArtifactCleanup, siteExtractArtifactCleanup, siteExtractRedispatch, siteExtractReconciliation, paaHarvestRedispatch, paaHarvestReconciliation, localSourcebookRedispatch, analyticsRollup, analyticsFormDelivery] = await Promise.all([
41535
43498
  drainQueue(budget),
41536
43499
  runMonthlyRefreshSweep(),
41537
43500
  reapIdleBrowserSessions(120).catch(() => ({ reaped: 0 })),
@@ -41547,7 +43510,9 @@ app.get("/cron/tick", async (c) => {
41547
43510
  reconcileSiteExtractSettlements(25).catch(() => ({ checked: 0, settled: 0, failed: 0 })),
41548
43511
  redispatchPendingPaaHarvests(25).catch(() => ({ checked: 0, dispatched: 0, failed: 0 })),
41549
43512
  reconcilePaaHarvestSettlements(25).catch(() => ({ checked: 0, staleFailed: 0, settled: 0, failed: 0 })),
41550
- redispatchQueuedLocalSourcebookAcquisitions(25).catch(() => ({ checked: 0, dispatched: 0, failed: 0 }))
43513
+ redispatchQueuedLocalSourcebookAcquisitions(25).catch(() => ({ checked: 0, dispatched: 0, failed: 0 })),
43514
+ refreshAnalyticsDailyRollupsIfDue().catch(() => ({ status: "failed", rows: 0 })),
43515
+ deliverPendingAnalyticsForms(10).catch(() => ({ claimed: 0, delivered: 0, deferred: 0 }))
41551
43516
  ]);
41552
43517
  return c.json({
41553
43518
  drained: results.length,
@@ -41567,12 +43532,14 @@ app.get("/cron/tick", async (c) => {
41567
43532
  paaHarvestRedispatch,
41568
43533
  paaHarvestReconciliation,
41569
43534
  localSourcebookRedispatch,
43535
+ analyticsRollup,
43536
+ analyticsFormDelivery,
41570
43537
  workflowDispatch: workflowDispatchResult
41571
43538
  });
41572
43539
  });
41573
43540
  app.post("/api/internal/extract-refinalize/:id", async (c) => {
41574
- const secret2 = c.req.header("authorization");
41575
- if (!process.env.CRON_SECRET || secret2 !== `Bearer ${process.env.CRON_SECRET}`) {
43541
+ const secret = c.req.header("authorization");
43542
+ if (!process.env.CRON_SECRET || secret !== `Bearer ${process.env.CRON_SECRET}`) {
41576
43543
  return c.json({ error: "Unauthorized" }, 401);
41577
43544
  }
41578
43545
  const jobId2 = c.req.param("id");
@@ -41583,8 +43550,8 @@ app.post("/api/internal/extract-refinalize/:id", async (c) => {
41583
43550
  finishExtractJob: finishExtractJob2,
41584
43551
  terminalExtractJobStatus: terminalExtractJobStatus2,
41585
43552
  extractJobLimitInfo: extractJobLimitInfo2
41586
- } = await import("./site-extract-repository-G723PZIL.js");
41587
- const { assembleExtractArtifacts } = await import("./extract-bundle-DYMQRQWG.js");
43553
+ } = await import("./site-extract-repository-STNOI5OH.js");
43554
+ const { assembleExtractArtifacts } = await import("./extract-bundle-2HONVC5T.js");
41588
43555
  const existing = await getExtractJob2(jobId2);
41589
43556
  if (!existing) return c.json({ error: "job not found" }, 404);
41590
43557
  if (existing.status !== "failed") {
@@ -41611,7 +43578,7 @@ app.post("/api/internal/extract-refinalize/:id", async (c) => {
41611
43578
  if (!updated) return c.json({ error: "job state changed during re-finalization" }, 409);
41612
43579
  let settlement = "already_settled_or_refunded";
41613
43580
  if (job.billedMc == null && job.userId != null) {
41614
- const { settleExtractJob: settleExtractJob3, countSuccessfulPages: countSuccessfulPages2 } = await import("./site-extract-repository-G723PZIL.js");
43581
+ const { settleExtractJob: settleExtractJob3, countSuccessfulPages: countSuccessfulPages2 } = await import("./site-extract-repository-STNOI5OH.js");
41615
43582
  const heldMc = Number(job.options.heldMc ?? 0);
41616
43583
  const successful = await countSuccessfulPages2(jobId2);
41617
43584
  const usedMc = Math.min(successful * MC_COSTS.page_scrape, heldMc);
@@ -41662,6 +43629,7 @@ app.route("/resend", resendInboundApp);
41662
43629
  app.route("/workshops", workshopRegistrationApp);
41663
43630
  app.route("/editorial-reading-room", editorialReadingRoomApp);
41664
43631
  app.route("/commons", commonsApp);
43632
+ app.route("/analytics", analyticsApp);
41665
43633
  app.route("/", scheduledResultApp);
41666
43634
  app.route("/public", publicScheduledResultApp);
41667
43635
  app.route("/api/internal/scheduled-artifacts", scheduledArtifactInternalApp);
@@ -41795,4 +43763,4 @@ app.get("/blog/:slug/", (c) => {
41795
43763
  export {
41796
43764
  app
41797
43765
  };
41798
- //# sourceMappingURL=server-SMEHT3OU.js.map
43766
+ //# sourceMappingURL=server-EVTCL3KB.js.map