mcp-scraper 0.47.0 → 0.49.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 (59) hide show
  1. package/README.md +2 -2
  2. package/dist/analytics-repository-WWXVU3VW.js +119 -0
  3. package/dist/bin/api-server.cjs +7905 -1911
  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 +3 -3
  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 +1856 -1029
  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-XCGQBGDA.js → chunk-64WBUDPC.js} +2125 -1262
  19. package/dist/chunk-64WBUDPC.js.map +1 -0
  20. package/dist/{chunk-MW3ET532.js → chunk-6DPIE262.js} +2 -2
  21. package/dist/{chunk-JH7A4HA7.js → chunk-MFE6RMIA.js} +2 -2
  22. package/dist/{chunk-5S6LYQQO.js → chunk-MFNGUM4L.js} +2 -2
  23. package/dist/chunk-MFNGUM4L.js.map +1 -0
  24. package/dist/{chunk-IWYYZXBR.js → chunk-PODASLGT.js} +2 -2
  25. package/dist/{chunk-QSUGPYGQ.js → chunk-QH63IZTX.js} +3 -3
  26. package/dist/chunk-QH63IZTX.js.map +1 -0
  27. package/dist/{chunk-TV6KPZ6J.js → chunk-R66PJOZW.js} +2 -2
  28. package/dist/chunk-XLEDHV3J.js +7 -0
  29. package/dist/chunk-XLEDHV3J.js.map +1 -0
  30. package/dist/chunk-YW2LXLDU.js +2865 -0
  31. package/dist/chunk-YW2LXLDU.js.map +1 -0
  32. package/dist/{chunk-GV5EGE5M.js → chunk-ZEMHL36J.js} +2 -2
  33. package/dist/{db-W3RPRILW.js → db-A4YUPV4Z.js} +2 -2
  34. package/dist/{extract-bundle-DXWWQER6.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-2FJVMTZ2.js → location-data-repository-YWFJGNIP.js} +3 -3
  38. package/dist/{server-OHMZJRSB.js → server-JE7SLZLJ.js} +2605 -346
  39. package/dist/server-JE7SLZLJ.js.map +1 -0
  40. package/dist/{site-extract-repository-UULQAGO4.js → site-extract-repository-STNOI5OH.js} +3 -3
  41. package/dist/site-extract-repository-STNOI5OH.js.map +1 -0
  42. package/dist/{worker-3QX7LG62.js → worker-4MHZS3OB.js} +5 -5
  43. package/package.json +3 -1
  44. package/dist/chunk-5S6LYQQO.js.map +0 -1
  45. package/dist/chunk-H4NWAF5B.js +0 -7
  46. package/dist/chunk-H4NWAF5B.js.map +0 -1
  47. package/dist/chunk-QSUGPYGQ.js.map +0 -1
  48. package/dist/chunk-XCGQBGDA.js.map +0 -1
  49. package/dist/server-OHMZJRSB.js.map +0 -1
  50. /package/dist/{db-W3RPRILW.js.map → analytics-repository-WWXVU3VW.js.map} +0 -0
  51. /package/dist/{chunk-MW3ET532.js.map → chunk-6DPIE262.js.map} +0 -0
  52. /package/dist/{chunk-JH7A4HA7.js.map → chunk-MFE6RMIA.js.map} +0 -0
  53. /package/dist/{chunk-IWYYZXBR.js.map → chunk-PODASLGT.js.map} +0 -0
  54. /package/dist/{chunk-TV6KPZ6J.js.map → chunk-R66PJOZW.js.map} +0 -0
  55. /package/dist/{chunk-GV5EGE5M.js.map → chunk-ZEMHL36J.js.map} +0 -0
  56. /package/dist/{location-data-repository-2FJVMTZ2.js.map → db-A4YUPV4Z.js.map} +0 -0
  57. /package/dist/{extract-bundle-DXWWQER6.js.map → extract-bundle-2HONVC5T.js.map} +0 -0
  58. /package/dist/{site-extract-repository-UULQAGO4.js.map → location-data-repository-YWFJGNIP.js.map} +0 -0
  59. /package/dist/{worker-3QX7LG62.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-TV6KPZ6J.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-GV5EGE5M.js";
74
+ } from "./chunk-ZEMHL36J.js";
21
75
  import {
22
76
  AIModeSelectors,
23
77
  AIOverviewSelectors,
@@ -44,9 +98,11 @@ import {
44
98
  resolveKernelProxyId,
45
99
  serpArrivalUrl,
46
100
  withRuntimeUserAgent
47
- } from "./chunk-JH7A4HA7.js";
101
+ } from "./chunk-MFE6RMIA.js";
48
102
  import {
49
103
  ApiErrorResponseSchema,
104
+ ArtifactTemplatePresetKeySchema,
105
+ CommonsClaimInputSchema,
50
106
  CommonsClaimPublicationInputSchema,
51
107
  CommonsPreparePublicationInputSchema,
52
108
  CommonsPublishEditorialInputSchema,
@@ -62,6 +118,9 @@ import {
62
118
  InternalScheduledArtifactRenderRequestSchema,
63
119
  ListScheduledRunsQuerySchema,
64
120
  LocalSourcebookSchemaTypeSchema,
121
+ NEWSROOM_PUBLISHER_TEMPLATE_PRESET,
122
+ NewsroomPublisherTemplateConfigSchema,
123
+ NewsroomPublisherV1InputSchema,
65
124
  PERSONAL_AUTHORITY_TEMPLATE_PRESET,
66
125
  PersonalAuthorityTemplateConfigSchema,
67
126
  PersonalAuthorityV1InputSchema,
@@ -97,7 +156,7 @@ import {
97
156
  resolveDeploymentProfile,
98
157
  resolveLocalSourcebookSchemaType,
99
158
  transcribeMediaUrl
100
- } from "./chunk-XCGQBGDA.js";
159
+ } from "./chunk-64WBUDPC.js";
101
160
  import {
102
161
  auditImageUrls,
103
162
  auditImages,
@@ -125,7 +184,7 @@ import {
125
184
  recordKernelSession,
126
185
  recordVendorUsage,
127
186
  runWithCostContext
128
- } from "./chunk-IWYYZXBR.js";
187
+ } from "./chunk-PODASLGT.js";
129
188
  import {
130
189
  listWorkflowDefinitions,
131
190
  runWorkflow,
@@ -153,7 +212,7 @@ import {
153
212
  } from "./chunk-3ZUBQQPQ.js";
154
213
  import {
155
214
  PACKAGE_VERSION
156
- } from "./chunk-H4NWAF5B.js";
215
+ } from "./chunk-XLEDHV3J.js";
157
216
  import {
158
217
  abandonExtractSettlement,
159
218
  countSuccessfulPages,
@@ -176,7 +235,7 @@ import {
176
235
  setExtractJobTotal,
177
236
  settleExtractJob,
178
237
  terminalExtractJobStatus
179
- } from "./chunk-MW3ET532.js";
238
+ } from "./chunk-6DPIE262.js";
180
239
  import {
181
240
  BROWSER_OPEN_MIN_BALANCE_MC,
182
241
  CONCURRENCY_PRICE_ID,
@@ -333,7 +392,7 @@ import {
333
392
  startHarvestAttempt,
334
393
  upsertPageSnapshot,
335
394
  verifyPassword
336
- } from "./chunk-5S6LYQQO.js";
395
+ } from "./chunk-MFNGUM4L.js";
337
396
 
338
397
  // src/blog/registry.ts
339
398
  var posts = [
@@ -6271,7 +6330,7 @@ async function extractSite(opts) {
6271
6330
  }
6272
6331
 
6273
6332
  // src/api/server.ts
6274
- import { Hono as Hono35 } from "hono";
6333
+ import { Hono as Hono36 } from "hono";
6275
6334
  import { serve as serveInngest } from "inngest/hono";
6276
6335
 
6277
6336
  // src/inngest/client.ts
@@ -8679,7 +8738,7 @@ var siteExtractFn = inngest.createFunction(
8679
8738
  });
8680
8739
  }) : null;
8681
8740
  const finalized = await step.run("finalize", async () => {
8682
- const { assembleExtractArtifacts } = await import("./extract-bundle-DXWWQER6.js");
8741
+ const { assembleExtractArtifacts } = await import("./extract-bundle-2HONVC5T.js");
8683
8742
  const current = await getExtractJob(jobId2);
8684
8743
  if (!current) throw new Error("extract job disappeared before finalization");
8685
8744
  const stored = await assembleExtractArtifacts(current, { branding, imageAudit });
@@ -9621,7 +9680,7 @@ async function resolveUsZipsPath(requestedPath) {
9621
9680
  async function loadZipGroups(stateAbbr, requestedPath, warnings, sourceMode) {
9622
9681
  const mode = resolvedLocationDataMode({ zipSource: sourceMode });
9623
9682
  if (mode === "hosted") {
9624
- const { getHostedZipGroups } = await import("./location-data-repository-2FJVMTZ2.js");
9683
+ const { getHostedZipGroups } = await import("./location-data-repository-YWFJGNIP.js");
9625
9684
  const hosted = await getHostedZipGroups(stateAbbr);
9626
9685
  if (!hosted.dataset) {
9627
9686
  throw new DirectoryLocationError(
@@ -9684,7 +9743,7 @@ async function resolveDirectoryMarkets(options, config = {}) {
9684
9743
  const sourceUrl = censusStateUrl(state.fips);
9685
9744
  const warnings = [];
9686
9745
  if (resolvedLocationDataMode(config) === "hosted") {
9687
- const { queryHostedLocationMarkets } = await import("./location-data-repository-2FJVMTZ2.js");
9746
+ const { queryHostedLocationMarkets } = await import("./location-data-repository-YWFJGNIP.js");
9688
9747
  const hosted = await queryHostedLocationMarkets({
9689
9748
  state: state.abbr,
9690
9749
  minPopulation: options.minPopulation,
@@ -14250,8 +14309,8 @@ siteAuditApp.get("/jobs", async (c) => {
14250
14309
  import { Hono as Hono2 } from "hono";
14251
14310
  var internalMemoryApp = new Hono2();
14252
14311
  internalMemoryApp.post("/ai-debit", async (c) => {
14253
- const secret2 = c.req.header("x-internal-secret");
14254
- if (!secret2 || secret2 !== process.env.INTERNAL_MEMORY_DEBIT_SECRET) {
14312
+ const secret = c.req.header("x-internal-secret");
14313
+ if (!secret || secret !== process.env.INTERNAL_MEMORY_DEBIT_SECRET) {
14255
14314
  return c.json({ error: "unauthorized" }, 401);
14256
14315
  }
14257
14316
  const body = await c.req.json().catch(() => ({}));
@@ -14342,9 +14401,9 @@ var UnifiedBillingError = class extends Error {
14342
14401
  details;
14343
14402
  };
14344
14403
  function billingSecret() {
14345
- const secret2 = process.env.INTERNAL_BILLING_SECRET?.trim() || process.env.INTERNAL_MEMORY_DEBIT_SECRET?.trim();
14346
- if (!secret2) throw new UnifiedBillingError("unauthorized", "internal billing secret is not configured", 503);
14347
- return secret2;
14404
+ const secret = process.env.INTERNAL_BILLING_SECRET?.trim() || process.env.INTERNAL_MEMORY_DEBIT_SECRET?.trim();
14405
+ if (!secret) throw new UnifiedBillingError("unauthorized", "internal billing secret is not configured", 503);
14406
+ return secret;
14348
14407
  }
14349
14408
  function normalizeIdentity(identity) {
14350
14409
  const normalized = identity.trim().toLowerCase();
@@ -14764,42 +14823,6 @@ async function voidScheduledRunAuthorization(args) {
14764
14823
 
14765
14824
  // src/api/memory.ts
14766
14825
  import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from "crypto";
14767
-
14768
- // src/api/session.ts
14769
- import { createHmac as createHmac2, timingSafeEqual as timingSafeEqual2 } from "crypto";
14770
- var isProduction = () => process.env.NODE_ENV === "production" || process.env.VERCEL === "1";
14771
- function getSessionSecret() {
14772
- const configured = process.env.SESSION_SECRET?.trim();
14773
- if (configured) return configured;
14774
- if (isProduction()) throw new Error("SESSION_SECRET is not set \u2014 add it to your Vercel env vars (see src/api/env.ts)");
14775
- return "dev-secret-change-me";
14776
- }
14777
- var secret = () => getSessionSecret();
14778
- function safeEqualHex(a, b) {
14779
- if (a.length !== b.length) return false;
14780
- try {
14781
- return timingSafeEqual2(Buffer.from(a, "hex"), Buffer.from(b, "hex"));
14782
- } catch {
14783
- return false;
14784
- }
14785
- }
14786
- function signSession(userId) {
14787
- const payload = String(userId);
14788
- const sig = createHmac2("sha256", secret()).update(payload).digest("hex");
14789
- return `${payload}.${sig}`;
14790
- }
14791
- function verifySession(token4) {
14792
- const dot = token4.lastIndexOf(".");
14793
- if (dot === -1) return null;
14794
- const payload = token4.slice(0, dot);
14795
- const sig = token4.slice(dot + 1);
14796
- const expected = createHmac2("sha256", secret()).update(payload).digest("hex");
14797
- if (!safeEqualHex(sig, expected)) return null;
14798
- const id = parseInt(payload);
14799
- return isNaN(id) ? null : id;
14800
- }
14801
-
14802
- // src/api/memory.ts
14803
14826
  import { provisionDefaultsTool } from "mcpscraper-memory-tools/tools/vaults/provision-defaults";
14804
14827
  import { setScheduleEntitlementTool } from "mcpscraper-memory-tools/tools/schedule/set-schedule-entitlement";
14805
14828
  var MEMORY_BASE_URL = () => (process.env.MCP_MEMORY_BASE_URL ?? "https://memory.mcpscraper.dev").replace(/\/$/, "");
@@ -14817,10 +14840,10 @@ function isMemoryOperator(email) {
14817
14840
  function encKey() {
14818
14841
  return scryptSync(getSessionSecret(), "mcp-memory-key-v1", 32);
14819
14842
  }
14820
- function encryptMemoryKey(secret2) {
14843
+ function encryptMemoryKey(secret) {
14821
14844
  const iv = randomBytes(12);
14822
14845
  const cipher = createCipheriv("aes-256-gcm", encKey(), iv);
14823
- const enc = Buffer.concat([cipher.update(secret2, "utf8"), cipher.final()]);
14846
+ const enc = Buffer.concat([cipher.update(secret, "utf8"), cipher.final()]);
14824
14847
  const tag = cipher.getAuthTag();
14825
14848
  return `${iv.toString("base64")}:${tag.toString("base64")}:${enc.toString("base64")}`;
14826
14849
  }
@@ -16437,15 +16460,15 @@ function mediaResolverConfigured() {
16437
16460
  var RESOLVE_TIMEOUT_MS = 24e4;
16438
16461
  async function resolveYouTubeAudio(videoId) {
16439
16462
  const base = mediaResolverUrl();
16440
- const secret2 = mediaResolverSecret();
16441
- if (!base || !secret2) return null;
16463
+ const secret = mediaResolverSecret();
16464
+ if (!base || !secret) return null;
16442
16465
  const controller = new AbortController();
16443
16466
  const timer = setTimeout(() => controller.abort(), RESOLVE_TIMEOUT_MS);
16444
16467
  const startedMs = Date.now();
16445
16468
  try {
16446
16469
  const resp = await fetch(`${base.replace(/\/+$/, "")}/resolve-audio`, {
16447
16470
  method: "POST",
16448
- headers: { "content-type": "application/json", "x-frames-secret": secret2 },
16471
+ headers: { "content-type": "application/json", "x-frames-secret": secret },
16449
16472
  body: JSON.stringify({ videoUrl: `https://www.youtube.com/watch?v=${videoId}` }),
16450
16473
  signal: controller.signal
16451
16474
  });
@@ -22227,7 +22250,7 @@ async function reconcileDirectoryWorkflowSettlements(limit = 25) {
22227
22250
  }
22228
22251
 
22229
22252
  // src/api/workflow-routes.ts
22230
- import { createHmac as createHmac3 } from "crypto";
22253
+ import { createHmac as createHmac2 } from "crypto";
22231
22254
  import { readFile as readFile2 } from "fs/promises";
22232
22255
  import { Hono as Hono19 } from "hono";
22233
22256
  import { z as z28 } from "zod";
@@ -22313,9 +22336,9 @@ async function readManifestFromSummary(summary) {
22313
22336
  }
22314
22337
  }
22315
22338
  function webhookSignature(body, timestamp) {
22316
- const secret2 = process.env.MCP_SCRAPER_WEBHOOK_SECRET?.trim();
22317
- if (!secret2) return null;
22318
- return createHmac3("sha256", secret2).update(`${timestamp}.${body}`).digest("hex");
22339
+ const secret = process.env.MCP_SCRAPER_WEBHOOK_SECRET?.trim();
22340
+ if (!secret) return null;
22341
+ return createHmac2("sha256", secret).update(`${timestamp}.${body}`).digest("hex");
22319
22342
  }
22320
22343
  async function deliverWorkflowWebhook(input) {
22321
22344
  if (!input.webhookUrl) return;
@@ -22753,8 +22776,8 @@ async function dispatchDueWorkflowSchedules(apiUrl, limit = 3) {
22753
22776
  return { dispatched: results.length, results };
22754
22777
  }
22755
22778
  workflowApp.post("/cron/dispatch", async (c) => {
22756
- const secret2 = c.req.header("authorization");
22757
- if (!process.env.CRON_SECRET || secret2 !== `Bearer ${process.env.CRON_SECRET}`) {
22779
+ const secret = c.req.header("authorization");
22780
+ if (!process.env.CRON_SECRET || secret !== `Bearer ${process.env.CRON_SECRET}`) {
22758
22781
  return c.json({ error: "Unauthorized" }, 401);
22759
22782
  }
22760
22783
  return c.json(await dispatchDueWorkflowSchedules(originFromUrl(c.req.url)));
@@ -30851,8 +30874,187 @@ import { Hono as Hono31 } from "hono";
30851
30874
  import { z as z32 } from "zod";
30852
30875
 
30853
30876
  // src/api/commons-repository.ts
30854
- import { createHash as createHash12, randomUUID as randomUUID15 } from "crypto";
30855
- var COMMONS_SCHEMA_VERSION = "2026-08-04.3";
30877
+ import { createHash as createHash13, randomUUID as randomUUID15 } from "crypto";
30878
+
30879
+ // src/api/commons-linksets.ts
30880
+ import { createHash as createHash12 } from "crypto";
30881
+ var COMMONS_LINKSET_MEDIA_TYPE = "application/linkset+json";
30882
+ var COMMONS_RELATIONSHIP_PROFILE = "https://mcpscraper.dev/commons/profiles/relationships/v1";
30883
+ var REGISTERED_RELATIONS = /* @__PURE__ */ new Set([
30884
+ "about",
30885
+ "alternate",
30886
+ "author",
30887
+ "canonical",
30888
+ "cite-as",
30889
+ "collection",
30890
+ "contents",
30891
+ "copyright",
30892
+ "describedby",
30893
+ "describes",
30894
+ "first",
30895
+ "glossary",
30896
+ "help",
30897
+ "icon",
30898
+ "index",
30899
+ "item",
30900
+ "last",
30901
+ "latest-version",
30902
+ "license",
30903
+ "memento",
30904
+ "next",
30905
+ "original",
30906
+ "predecessor-version",
30907
+ "prev",
30908
+ "related",
30909
+ "replies",
30910
+ "service-desc",
30911
+ "service-doc",
30912
+ "successor-version",
30913
+ "terms-of-service",
30914
+ "type",
30915
+ "via",
30916
+ "version-history"
30917
+ ]);
30918
+ function normalizeCommonsClaims(input) {
30919
+ if (input === void 0) return void 0;
30920
+ if (!Array.isArray(input)) throw new Error("claims must be an array.");
30921
+ if (input.length > 300) throw new Error("claims must contain no more than 300 assertions.");
30922
+ const seen = /* @__PURE__ */ new Set();
30923
+ return input.map((claim, index) => {
30924
+ const predicate = String(claim.predicate ?? "").trim();
30925
+ if (!isSupportedPredicate(predicate)) {
30926
+ throw new Error(`claims[${index}].predicate must be a supported registered relation or an absolute HTTPS relation URI.`);
30927
+ }
30928
+ const objectEntityId = optionalText(claim.objectEntityId, 80);
30929
+ const objectUrl = optionalHttpsUrl(claim.objectUrl, `claims[${index}].objectUrl`);
30930
+ if ((objectEntityId ? 1 : 0) + (objectUrl ? 1 : 0) !== 1) {
30931
+ throw new Error(`claims[${index}] must provide exactly one of objectEntityId or objectUrl.`);
30932
+ }
30933
+ if (objectEntityId && !/^TPW-Q[A-Za-z0-9._-]+$/.test(objectEntityId)) {
30934
+ throw new Error(`claims[${index}].objectEntityId must be a Transparent Commons TPW-Q identifier.`);
30935
+ }
30936
+ const confidence = claim.confidence ?? 1;
30937
+ if (!Number.isFinite(confidence) || confidence < 0 || confidence > 1) {
30938
+ throw new Error(`claims[${index}].confidence must be between 0 and 1.`);
30939
+ }
30940
+ const key = `${predicate}\0${objectEntityId ?? objectUrl}`;
30941
+ if (seen.has(key)) throw new Error(`claims[${index}] duplicates another predicate and object pair.`);
30942
+ seen.add(key);
30943
+ return {
30944
+ predicate,
30945
+ predicateLabel: optionalText(claim.predicateLabel, 160),
30946
+ objectEntityId,
30947
+ objectUrl,
30948
+ title: optionalText(claim.title, 500),
30949
+ mediaType: optionalText(claim.mediaType, 160),
30950
+ hreflang: normalizeHreflang(claim.hreflang),
30951
+ qualifiers: normalizeScalarRecord(claim.qualifiers),
30952
+ references: normalizeReferences(claim.references),
30953
+ rank: claim.rank ?? "normal",
30954
+ confidence,
30955
+ confidenceReason: optionalText(claim.confidenceReason, 1e3),
30956
+ disputeState: claim.disputeState ?? "undisputed",
30957
+ validFrom: optionalDate(claim.validFrom, `claims[${index}].validFrom`),
30958
+ validTo: optionalDate(claim.validTo, `claims[${index}].validTo`),
30959
+ asOf: optionalDate(claim.asOf, `claims[${index}].asOf`),
30960
+ sourceFamily: claim.sourceFamily ?? "unknown"
30961
+ };
30962
+ });
30963
+ }
30964
+ function claimIsPublic(claim, now = /* @__PURE__ */ new Date()) {
30965
+ if (claim.reviewState !== "approved" || claim.rank === "deprecated") return false;
30966
+ if (!["undisputed", "attributed"].includes(claim.disputeState ?? "undisputed")) return false;
30967
+ const timestamp = now.getTime();
30968
+ if (claim.validFrom && Date.parse(claim.validFrom) > timestamp) return false;
30969
+ if (claim.validTo && Date.parse(claim.validTo) < timestamp) return false;
30970
+ return true;
30971
+ }
30972
+ function buildCommonsLinkset(entity, claims) {
30973
+ const publicUrl = `${commonsPublicWikiBaseUrl()}/wiki/${encodeURIComponent(entity.slug)}`;
30974
+ const linksetUrl = commonsLinksetUrl(entity.id);
30975
+ const context = { anchor: publicUrl };
30976
+ const publicClaims = claims.filter((claim) => claimIsPublic(claim)).sort((a, b) => a.predicate.localeCompare(b.predicate) || a.href.localeCompare(b.href) || a.id.localeCompare(b.id));
30977
+ for (const claim of publicClaims) {
30978
+ const targets = context[claim.predicate] ?? [];
30979
+ const target = { href: claim.href };
30980
+ if (claim.mediaType) target.type = claim.mediaType;
30981
+ if (claim.hreflang) target.hreflang = claim.hreflang;
30982
+ if (claim.title) target.title = claim.title;
30983
+ targets.push(target);
30984
+ context[claim.predicate] = targets;
30985
+ }
30986
+ const document2 = { linkset: [context] };
30987
+ const etag = `"${createHash12("sha256").update(JSON.stringify(document2)).digest("hex")}"`;
30988
+ return { document: document2, etag, publicUrl, linksetUrl, profile: COMMONS_RELATIONSHIP_PROFILE };
30989
+ }
30990
+ function commonsLinksetDiscoveryHeader(idOrSlug) {
30991
+ const url = commonsLinksetUrl(idOrSlug);
30992
+ return `<${url}>; rel="linkset"; type="${COMMONS_LINKSET_MEDIA_TYPE}"; profile="${COMMONS_RELATIONSHIP_PROFILE}"`;
30993
+ }
30994
+ function commonsLinksetUrl(idOrSlug) {
30995
+ const base = (process.env.APP_URL?.trim() || "https://mcpscraper.dev").replace(/\/+$/, "");
30996
+ return `${base}/commons/entities/${encodeURIComponent(idOrSlug)}/linkset`;
30997
+ }
30998
+ function commonsClaimIdentityKey(claim) {
30999
+ return `${claim.predicate}\0${claim.objectEntityId ?? claim.objectUrl ?? ""}`;
31000
+ }
31001
+ function commonsPublicWikiBaseUrl() {
31002
+ return (process.env.TRANSPARENT_COMMONS_PUBLIC_BASE_URL?.trim() || "https://transparent-commons.cc").replace(/\/+$/, "");
31003
+ }
31004
+ function isSupportedPredicate(value) {
31005
+ if (REGISTERED_RELATIONS.has(value)) return true;
31006
+ try {
31007
+ const url = new URL(value);
31008
+ return url.protocol === "https:" && Boolean(url.hostname);
31009
+ } catch {
31010
+ return false;
31011
+ }
31012
+ }
31013
+ function optionalText(value, max) {
31014
+ const text2 = String(value ?? "").trim();
31015
+ return text2 ? text2.slice(0, max) : void 0;
31016
+ }
31017
+ function optionalHttpsUrl(value, field) {
31018
+ const text2 = optionalText(value, 2e3);
31019
+ if (!text2) return void 0;
31020
+ try {
31021
+ const url = new URL(text2);
31022
+ if (url.protocol !== "https:") throw new Error();
31023
+ url.hash = "";
31024
+ return url.href;
31025
+ } catch {
31026
+ throw new Error(`${field} must be an absolute HTTPS URL.`);
31027
+ }
31028
+ }
31029
+ function optionalDate(value, field) {
31030
+ const text2 = optionalText(value, 80);
31031
+ if (!text2) return void 0;
31032
+ const timestamp = Date.parse(text2);
31033
+ if (!Number.isFinite(timestamp)) throw new Error(`${field} must be an ISO-compatible date.`);
31034
+ return new Date(timestamp).toISOString();
31035
+ }
31036
+ function normalizeScalarRecord(value) {
31037
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
31038
+ return Object.fromEntries(Object.entries(value).slice(0, 50).map(([key, item]) => [key.slice(0, 120), item]));
31039
+ }
31040
+ function normalizeReferences(value) {
31041
+ if (!Array.isArray(value)) return void 0;
31042
+ return value.slice(0, 50).map((reference) => ({
31043
+ title: String(reference.title ?? "").trim().slice(0, 240),
31044
+ url: optionalHttpsUrl(reference.url, "claim reference URL"),
31045
+ source: optionalText(reference.source, 240),
31046
+ note: optionalText(reference.note, 1e3),
31047
+ accessedAt: optionalDate(reference.accessedAt, "claim reference accessedAt")
31048
+ })).filter((reference) => reference.title);
31049
+ }
31050
+ function normalizeHreflang(value) {
31051
+ if (!Array.isArray(value)) return void 0;
31052
+ const languages = [...new Set(value.map((item) => optionalText(item, 80)).filter((item) => Boolean(item)))];
31053
+ return languages.length ? languages : void 0;
31054
+ }
31055
+
31056
+ // src/api/commons-repository.ts
31057
+ var COMMONS_SCHEMA_VERSION = "2026-08-05.1";
30856
31058
  var DEFAULT_COMMONS_BASE_URL = "https://transparent-commons.cc";
30857
31059
  var DEFAULT_ENTITY_TYPE = "PublicArticle";
30858
31060
  var COMMONS_ENTITY_PROFILES = {
@@ -31049,6 +31251,42 @@ async function ensureCommonsSchema() {
31049
31251
  { sql: `CREATE INDEX IF NOT EXISTS commons_entities_status_updated ON commons_entities(status, updated_at DESC)`, args: [] },
31050
31252
  { sql: `CREATE INDEX IF NOT EXISTS commons_entities_type_updated ON commons_entities(entity_type, updated_at DESC)`, args: [] },
31051
31253
  { sql: `CREATE INDEX IF NOT EXISTS commons_entities_source_domain ON commons_entities(source_json)`, args: [] },
31254
+ {
31255
+ sql: `
31256
+ CREATE TABLE IF NOT EXISTS commons_claims (
31257
+ id TEXT PRIMARY KEY,
31258
+ subject_entity_id TEXT NOT NULL REFERENCES commons_entities(id),
31259
+ predicate TEXT NOT NULL,
31260
+ predicate_label TEXT,
31261
+ object_entity_id TEXT,
31262
+ object_url TEXT,
31263
+ title TEXT,
31264
+ media_type TEXT,
31265
+ hreflang TEXT,
31266
+ qualifiers_json TEXT NOT NULL DEFAULT '{}',
31267
+ references_json TEXT NOT NULL DEFAULT '[]',
31268
+ rank TEXT NOT NULL DEFAULT 'normal',
31269
+ confidence REAL NOT NULL DEFAULT 1,
31270
+ confidence_reason TEXT,
31271
+ dispute_state TEXT NOT NULL DEFAULT 'undisputed',
31272
+ valid_from TEXT,
31273
+ valid_to TEXT,
31274
+ as_of TEXT,
31275
+ source_family TEXT NOT NULL DEFAULT 'unknown',
31276
+ review_state TEXT NOT NULL DEFAULT 'approved',
31277
+ proposal_id TEXT NOT NULL REFERENCES commons_proposals(id),
31278
+ created_by_user_id INTEGER,
31279
+ updated_by_user_id INTEGER,
31280
+ created_at TEXT NOT NULL,
31281
+ updated_at TEXT NOT NULL,
31282
+ CHECK ((object_entity_id IS NOT NULL) != (object_url IS NOT NULL)),
31283
+ UNIQUE(subject_entity_id, predicate, object_entity_id, object_url)
31284
+ )
31285
+ `,
31286
+ args: []
31287
+ },
31288
+ { sql: `CREATE INDEX IF NOT EXISTS commons_claims_subject_predicate ON commons_claims(subject_entity_id, predicate)`, args: [] },
31289
+ { sql: `CREATE INDEX IF NOT EXISTS commons_claims_object_entity ON commons_claims(object_entity_id)`, args: [] },
31052
31290
  {
31053
31291
  sql: `
31054
31292
  CREATE TABLE IF NOT EXISTS commons_proposals (
@@ -31209,8 +31447,15 @@ async function searchCommonsEntities(filters = {}, userId) {
31209
31447
  args.push(jsonLikeValue(keyword));
31210
31448
  }
31211
31449
  if (mergedFilters.relatedEntityId) {
31212
- clauses.push("related_entities_json LIKE ?");
31213
- args.push(jsonLikeValue(mergedFilters.relatedEntityId));
31450
+ clauses.push(`(
31451
+ related_entities_json LIKE ?
31452
+ OR EXISTS (
31453
+ SELECT 1 FROM commons_claims
31454
+ WHERE commons_claims.subject_entity_id = commons_entities.id
31455
+ AND commons_claims.object_entity_id = ?
31456
+ )
31457
+ )`);
31458
+ args.push(jsonLikeValue(mergedFilters.relatedEntityId), mergedFilters.relatedEntityId);
31214
31459
  }
31215
31460
  if (mergedFilters.trailId) {
31216
31461
  clauses.push("related_entities_json LIKE ?");
@@ -31232,8 +31477,16 @@ async function searchCommonsEntities(filters = {}, userId) {
31232
31477
  }
31233
31478
  const terms = tokenize(mergedFilters.query);
31234
31479
  for (const term of terms) {
31235
- clauses.push("search_text LIKE ?");
31236
- args.push(`%${escapeLike(term)}%`);
31480
+ clauses.push(`(
31481
+ search_text LIKE ?
31482
+ OR EXISTS (
31483
+ SELECT 1 FROM commons_claims
31484
+ WHERE commons_claims.subject_entity_id = commons_entities.id
31485
+ AND (commons_claims.predicate LIKE ? OR commons_claims.predicate_label LIKE ? OR commons_claims.title LIKE ? OR commons_claims.object_url LIKE ?)
31486
+ )
31487
+ )`);
31488
+ const pattern = `%${escapeLike(term)}%`;
31489
+ args.push(pattern, pattern, pattern, pattern, pattern);
31237
31490
  }
31238
31491
  args.push(limit, offset);
31239
31492
  const result = await getDb().execute({
@@ -31255,9 +31508,9 @@ async function searchCommonsEntities(filters = {}, userId) {
31255
31508
  }
31256
31509
  async function prepareCommonsEntity(input, user) {
31257
31510
  await ensureCommonsSchema();
31258
- const title = optionalText(input.title, 180) ?? "";
31511
+ const title = optionalText2(input.title, 180) ?? "";
31259
31512
  const slug2 = normalizeSlug(input.slug || title);
31260
- const schemaOrgType = optionalText(input.schemaOrgType, 80);
31513
+ const schemaOrgType = optionalText2(input.schemaOrgType, 80);
31261
31514
  const contract = commonsEntityProfileFor(input.entityType, schemaOrgType);
31262
31515
  const entityType = contract.entityType;
31263
31516
  const bodyMarkdown = safeCandidateBody(input.bodyMarkdown, input.contentSections);
@@ -31315,8 +31568,8 @@ async function validateCommonsEntity(input, user) {
31315
31568
  await ensureCommonsSchema();
31316
31569
  const errors = [];
31317
31570
  const warnings = [];
31318
- const title = optionalText(input.title, 180);
31319
- const description = optionalText(input.description, 600);
31571
+ const title = optionalText2(input.title, 180);
31572
+ const description = optionalText2(input.description, 600);
31320
31573
  const slug2 = normalizeSlug(input.slug || title || "");
31321
31574
  const contract = commonsEntityProfileFor(input.entityType, input.seo?.schemaOrgType);
31322
31575
  const source = normalizeSource(input.source);
@@ -31331,6 +31584,11 @@ async function validateCommonsEntity(input, user) {
31331
31584
  maxCandidates: 8
31332
31585
  });
31333
31586
  const existing = slug2 || input.entityId ? await findEntityBySlugOrIdAnyStatus(slug2, input.entityId) : null;
31587
+ try {
31588
+ normalizeCommonsClaims(input.claims);
31589
+ } catch (error) {
31590
+ errors.push(error instanceof Error ? error.message : "Claims are invalid.");
31591
+ }
31334
31592
  if (!title || title.length < 3) errors.push("title must be at least 3 characters.");
31335
31593
  if (!description || description.length < 8) errors.push("description must be at least 8 characters.");
31336
31594
  if (!slug2) errors.push("A Commons entity needs a URL slug.");
@@ -31420,6 +31678,7 @@ async function submitCommonsEntity(input, user) {
31420
31678
  };
31421
31679
  }
31422
31680
  const existing = await findEntityBySlugOrIdAnyStatus(normalized.slug, normalized.entityId);
31681
+ const existingClaims = existing && normalized.claims !== void 0 ? await getCommonsClaimsByEntityId(existing.id) : [];
31423
31682
  const safety = evaluatePublishSafety(normalized, existing);
31424
31683
  const shouldApply = safety.safe && normalized.reviewPolicy !== "always_review";
31425
31684
  const proposalId = `commons-proposal-${randomUUID15()}`;
@@ -31464,6 +31723,10 @@ async function submitCommonsEntity(input, user) {
31464
31723
  ];
31465
31724
  if (nextEntity) {
31466
31725
  batch.push(entityUpsertStatement(nextEntity, existing));
31726
+ if (normalized.claims !== void 0) {
31727
+ batch.push({ sql: "DELETE FROM commons_claims WHERE subject_entity_id = ?", args: [nextEntity.id] });
31728
+ batch.push(...claimInsertStatements(normalized.claims, existingClaims, nextEntity.id, proposalId, Number(user.id), now));
31729
+ }
31467
31730
  batch.push(...commonsIndexStatements(nextEntity, now));
31468
31731
  batch.push({
31469
31732
  sql: `
@@ -31480,10 +31743,19 @@ async function submitCommonsEntity(input, user) {
31480
31743
  actorLabel(input, user),
31481
31744
  normalized.actorChannel,
31482
31745
  existing ? "entity_updated" : "entity_created",
31483
- JSON.stringify(changedFieldPaths(existing, nextEntity)),
31746
+ JSON.stringify([
31747
+ ...changedFieldPaths(existing, nextEntity),
31748
+ ...normalized.claims !== void 0 ? ["claims"] : []
31749
+ ]),
31484
31750
  normalized.changeSummary || (existing ? "Updated Commons entity." : "Created Commons entity."),
31485
- existing ? JSON.stringify(entityLedgerSnapshot(existing)) : null,
31486
- JSON.stringify(entityLedgerSnapshot(nextEntity)),
31751
+ existing ? JSON.stringify({
31752
+ ...entityLedgerSnapshot(existing),
31753
+ ...normalized.claims !== void 0 ? { claims: existingClaims } : {}
31754
+ }) : null,
31755
+ JSON.stringify({
31756
+ ...entityLedgerSnapshot(nextEntity),
31757
+ ...normalized.claims !== void 0 ? { claims: normalized.claims } : {}
31758
+ }),
31487
31759
  now
31488
31760
  ]
31489
31761
  });
@@ -31534,11 +31806,35 @@ async function getCommonsEntityLedger(idOrSlug) {
31534
31806
  ledger: result.rows.map((row) => rowToLedgerEntry(row))
31535
31807
  };
31536
31808
  }
31809
+ async function listCommonsClaims(idOrSlug) {
31810
+ await ensureCommonsSchema();
31811
+ const entity = await getCommonsEntity(idOrSlug);
31812
+ if (!entity) return null;
31813
+ return { entity, claims: await getCommonsClaimsByEntityId(entity.id) };
31814
+ }
31815
+ async function getCommonsEntityLinkset(idOrSlug) {
31816
+ const result = await listCommonsClaims(idOrSlug);
31817
+ if (!result) return null;
31818
+ const targetIds = [...new Set(result.claims.map((claim) => claim.objectEntityId).filter((id) => Boolean(id)))];
31819
+ const targetSlugs = /* @__PURE__ */ new Map();
31820
+ if (targetIds.length) {
31821
+ const targets = await getDb().execute({
31822
+ sql: `SELECT id, slug FROM commons_entities WHERE status = 'published' AND id IN (${targetIds.map(() => "?").join(", ")})`,
31823
+ args: targetIds
31824
+ });
31825
+ for (const row of targets.rows) targetSlugs.set(String(row.id), String(row.slug));
31826
+ }
31827
+ const resolved = result.claims.map((claim) => {
31828
+ const href = claim.objectUrl ?? (claim.objectEntityId && targetSlugs.has(claim.objectEntityId) ? publicUrlForSlug(targetSlugs.get(claim.objectEntityId)) : null);
31829
+ return href ? { ...claim, href } : null;
31830
+ }).filter((claim) => claim !== null);
31831
+ return { entity: result.entity, claims: result.claims, ...buildCommonsLinkset(result.entity, resolved) };
31832
+ }
31537
31833
  async function saveCommonsFilter(input, user) {
31538
31834
  await ensureCommonsSchema();
31539
31835
  const now = (/* @__PURE__ */ new Date()).toISOString();
31540
31836
  const name = requiredText(input.name, "name", 2, 80);
31541
- const description = optionalText(input.description, 500);
31837
+ const description = optionalText2(input.description, 500);
31542
31838
  const filter = sanitizeSearchFilters(input.filter);
31543
31839
  const existing = await getDb().execute({
31544
31840
  sql: "SELECT id FROM commons_saved_filters WHERE user_id = ? AND name = ? LIMIT 1",
@@ -31671,9 +31967,9 @@ function commonsDatabaseReport() {
31671
31967
  conflictBehavior: "Existing entity edits require baseRevision. Otherwise the proposal is pending_review.",
31672
31968
  billing: { validationCredits: 0, createCredits: LISTING_WRITE_RATE_POLICY.create.credits, editCredits: LISTING_WRITE_RATE_POLICY.edit.credits }
31673
31969
  },
31674
- searchCorpus: ["title", "disambiguationName", "description", "tags", "keywords", "relatedEntities", "bodyMarkdown", "sections", "media", "source metadata", "jsonLd"],
31970
+ searchCorpus: ["title", "disambiguationName", "description", "tags", "keywords", "relatedEntities", "relationship claims", "bodyMarkdown", "sections", "media", "source metadata", "jsonLd"],
31675
31971
  indexing: commonsIndexingPolicy(),
31676
- tables: ["commons_entities", "commons_proposals", "commons_ledger", "commons_saved_filters", "commons_index_documents"]
31972
+ tables: ["commons_entities", "commons_claims", "commons_proposals", "commons_ledger", "commons_saved_filters", "commons_index_documents"]
31677
31973
  };
31678
31974
  }
31679
31975
  function entityToWikiPage(entity) {
@@ -31693,6 +31989,13 @@ function entityToWikiPage(entity) {
31693
31989
  media: entity.media,
31694
31990
  sections: entity.articleSections,
31695
31991
  metadata: {
31992
+ linksetUrl: commonsLinksetUrl(entity.id),
31993
+ linksetDiscovery: {
31994
+ rel: "linkset",
31995
+ type: COMMONS_LINKSET_MEDIA_TYPE,
31996
+ profile: COMMONS_RELATIONSHIP_PROFILE,
31997
+ href: commonsLinksetUrl(entity.id)
31998
+ },
31696
31999
  entityType: friendlyEntityType(entity.entityType),
31697
32000
  disambiguationName: entity.disambiguationName,
31698
32001
  keywords: entity.keywords,
@@ -31849,7 +32152,7 @@ async function commonsTagVocabulary() {
31849
32152
  ...parseJson3(record.tags_json, []),
31850
32153
  ...parseJson3(record.keywords_json, [])
31851
32154
  ]) {
31852
- const cleaned = optionalText(tag, 120);
32155
+ const cleaned = optionalText2(tag, 120);
31853
32156
  if (!cleaned) continue;
31854
32157
  counts.set(cleaned, (counts.get(cleaned) ?? 0) + 1);
31855
32158
  }
@@ -31859,7 +32162,7 @@ async function commonsTagVocabulary() {
31859
32162
  function resolveCommonsTags(candidates, vocabulary) {
31860
32163
  const vocabularyByKey = new Map(vocabulary.map((entry) => [entry.tag.toLowerCase(), entry]));
31861
32164
  return candidates.map((candidate) => {
31862
- const cleaned = optionalText(candidate, 120) ?? "";
32165
+ const cleaned = optionalText2(candidate, 120) ?? "";
31863
32166
  const key = cleaned.toLowerCase();
31864
32167
  const exact = vocabularyByKey.get(key);
31865
32168
  if (!cleaned || cleaned.length < 2) {
@@ -31956,12 +32259,13 @@ function normalizeSubmitInput(input) {
31956
32259
  slug: slug2,
31957
32260
  entityId: normalizeEntityId(input.entityId),
31958
32261
  entityType: normalizeEntityType(input.entityType),
31959
- disambiguationName: optionalText(input.disambiguationName, 240) ?? void 0,
32262
+ disambiguationName: optionalText2(input.disambiguationName, 240) ?? void 0,
31960
32263
  featuredImage: normalizeFeaturedImage(input.featuredImage),
31961
32264
  source: normalizeSource(input.source),
31962
32265
  tags: uniqueStrings(input.tags ?? []).slice(0, 24),
31963
32266
  keywords: uniqueStrings(input.keywords ?? []).slice(0, 50),
31964
32267
  relatedEntities: normalizeRelatedEntities(input.relatedEntities),
32268
+ claims: normalizeClaimsForRepository(input.claims),
31965
32269
  bodyMarkdown: normalizeBody(input.bodyMarkdown, input.contentSections),
31966
32270
  contentSections: normalizeContentSections(input.contentSections),
31967
32271
  articleSections: normalizeArticleSections(input.articleSections),
@@ -31970,11 +32274,18 @@ function normalizeSubmitInput(input) {
31970
32274
  seo: normalizeSeo(input.seo),
31971
32275
  actorChannel: input.actorChannel ?? "mcp",
31972
32276
  actorLabel: input.actorLabel ?? "",
31973
- changeSummary: optionalText(input.changeSummary, 500) ?? "",
32277
+ changeSummary: optionalText2(input.changeSummary, 500) ?? "",
31974
32278
  reviewPolicy: input.reviewPolicy ?? "auto_publish_if_safe",
31975
32279
  baseRevision: input.baseRevision
31976
32280
  };
31977
32281
  }
32282
+ function normalizeClaimsForRepository(input) {
32283
+ try {
32284
+ return normalizeCommonsClaims(input);
32285
+ } catch (error) {
32286
+ throw new CommonsRepositoryError("invalid_claims", error instanceof Error ? error.message : "Claims are invalid.");
32287
+ }
32288
+ }
31978
32289
  function evaluatePublishSafety(input, existing) {
31979
32290
  if (!input.featuredImage?.url) return { safe: false, reason: "A featured image is required before a Commons entity can publish." };
31980
32291
  if (!input.bodyMarkdown.trim() && !input.source.originalUrl && !input.source.resolvedUrl) {
@@ -32120,6 +32431,84 @@ function entityStorageValues(entity) {
32120
32431
  entity.revision
32121
32432
  ];
32122
32433
  }
32434
+ function claimInsertStatements(claims, existingClaims, subjectEntityId, proposalId, userId, now) {
32435
+ const existingByIdentity = new Map(existingClaims.map((claim) => [commonsClaimIdentityKey(claim), claim]));
32436
+ return claims.map((claim) => {
32437
+ const existing = existingByIdentity.get(commonsClaimIdentityKey(claim));
32438
+ return {
32439
+ sql: `
32440
+ INSERT INTO commons_claims (
32441
+ id, subject_entity_id, predicate, predicate_label, object_entity_id, object_url,
32442
+ title, media_type, hreflang, qualifiers_json, references_json, rank, confidence,
32443
+ confidence_reason, dispute_state, valid_from, valid_to, as_of, source_family,
32444
+ review_state, proposal_id, created_by_user_id, updated_by_user_id, created_at, updated_at
32445
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'approved', ?, ?, ?, ?, ?)
32446
+ `,
32447
+ args: [
32448
+ existing?.id ?? allocateClaimId(),
32449
+ subjectEntityId,
32450
+ claim.predicate,
32451
+ claim.predicateLabel ?? null,
32452
+ claim.objectEntityId ?? null,
32453
+ claim.objectUrl ?? null,
32454
+ claim.title ?? null,
32455
+ claim.mediaType ?? null,
32456
+ claim.hreflang ? JSON.stringify(claim.hreflang) : null,
32457
+ JSON.stringify(claim.qualifiers ?? {}),
32458
+ JSON.stringify(claim.references ?? []),
32459
+ claim.rank ?? "normal",
32460
+ claim.confidence ?? 1,
32461
+ claim.confidenceReason ?? null,
32462
+ claim.disputeState ?? "undisputed",
32463
+ claim.validFrom ?? null,
32464
+ claim.validTo ?? null,
32465
+ claim.asOf ?? null,
32466
+ claim.sourceFamily ?? "unknown",
32467
+ proposalId,
32468
+ existing?.createdByUserId ?? userId,
32469
+ userId,
32470
+ existing?.createdAt ?? now,
32471
+ now
32472
+ ]
32473
+ };
32474
+ });
32475
+ }
32476
+ async function getCommonsClaimsByEntityId(entityId) {
32477
+ const result = await getDb().execute({
32478
+ sql: "SELECT * FROM commons_claims WHERE subject_entity_id = ? ORDER BY predicate ASC, object_url ASC, object_entity_id ASC, id ASC",
32479
+ args: [entityId]
32480
+ });
32481
+ return result.rows.map((row) => rowToClaim(row));
32482
+ }
32483
+ function rowToClaim(row) {
32484
+ return {
32485
+ id: String(row.id),
32486
+ subjectEntityId: String(row.subject_entity_id),
32487
+ predicate: String(row.predicate),
32488
+ predicateLabel: row.predicate_label == null ? void 0 : String(row.predicate_label),
32489
+ objectEntityId: row.object_entity_id == null ? void 0 : String(row.object_entity_id),
32490
+ objectUrl: row.object_url == null ? void 0 : String(row.object_url),
32491
+ title: row.title == null ? void 0 : String(row.title),
32492
+ mediaType: row.media_type == null ? void 0 : String(row.media_type),
32493
+ hreflang: row.hreflang == null ? void 0 : parseJson3(row.hreflang, []),
32494
+ qualifiers: parseJson3(row.qualifiers_json, {}),
32495
+ references: parseJson3(row.references_json, []),
32496
+ rank: String(row.rank),
32497
+ confidence: Number(row.confidence),
32498
+ confidenceReason: row.confidence_reason == null ? void 0 : String(row.confidence_reason),
32499
+ disputeState: String(row.dispute_state),
32500
+ validFrom: row.valid_from == null ? void 0 : String(row.valid_from),
32501
+ validTo: row.valid_to == null ? void 0 : String(row.valid_to),
32502
+ asOf: row.as_of == null ? void 0 : String(row.as_of),
32503
+ sourceFamily: String(row.source_family),
32504
+ reviewState: String(row.review_state),
32505
+ proposalId: String(row.proposal_id),
32506
+ createdByUserId: row.created_by_user_id == null ? null : Number(row.created_by_user_id),
32507
+ updatedByUserId: row.updated_by_user_id == null ? null : Number(row.updated_by_user_id),
32508
+ createdAt: String(row.created_at),
32509
+ updatedAt: String(row.updated_at)
32510
+ };
32511
+ }
32123
32512
  function commonsIndexStatements(entity, now) {
32124
32513
  return commonsIndexDocuments(entity, now).map((document2) => ({
32125
32514
  sql: `
@@ -32224,7 +32613,7 @@ function commonsIndexDocuments(entity, now) {
32224
32613
  return documents;
32225
32614
  }
32226
32615
  function commonsIndexDocumentId(entityId, documentType, documentKey) {
32227
- return `commons-index-${createHash12("sha256").update(`${entityId}
32616
+ return `commons-index-${createHash13("sha256").update(`${entityId}
32228
32617
  ${documentType}
32229
32618
  ${documentKey}`).digest("hex").slice(0, 32)}`;
32230
32619
  }
@@ -32399,14 +32788,14 @@ function normalizeArticleSections(sections) {
32399
32788
  return {
32400
32789
  relatedLinks: normalizeRelatedLinks(sections?.relatedLinks),
32401
32790
  notes: Array.isArray(sections?.notes) ? sections.notes.map((note) => ({
32402
- marker: optionalText(note.marker, 20) ?? void 0,
32791
+ marker: optionalText2(note.marker, 20) ?? void 0,
32403
32792
  body: requiredText(note.body, "notes.body", 1, 2e3)
32404
32793
  })).slice(0, 100) : [],
32405
32794
  citations: normalizeCitations(sections?.citations),
32406
32795
  externalLinks: Array.isArray(sections?.externalLinks) ? sections.externalLinks.map((link) => ({
32407
32796
  title: requiredText(link.title, "externalLinks.title", 1, 180),
32408
32797
  url: requiredText(link.url, "externalLinks.url", 1, 2e3),
32409
- summary: optionalText(link.summary, 300) ?? void 0
32798
+ summary: optionalText2(link.summary, 300) ?? void 0
32410
32799
  })).slice(0, 100) : [],
32411
32800
  categories: uniqueStrings(sections?.categories ?? []).slice(0, 50)
32412
32801
  };
@@ -32415,7 +32804,7 @@ function normalizeRelatedLinks(relatedLinks) {
32415
32804
  if (!Array.isArray(relatedLinks)) return [];
32416
32805
  return relatedLinks.map((link) => {
32417
32806
  const title = requiredText(link.title, "relatedLinks.title", 1, 180);
32418
- const url = optionalText(link.url, 2e3);
32807
+ const url = optionalText2(link.url, 2e3);
32419
32808
  const slug2 = link.slug ? normalizeSlug(link.slug) : void 0;
32420
32809
  const entityId = normalizeEntityId(link.entityId);
32421
32810
  const needsLink = link.needsLink === true || !url && !slug2 && !entityId;
@@ -32424,9 +32813,9 @@ function normalizeRelatedLinks(relatedLinks) {
32424
32813
  url,
32425
32814
  slug: slug2,
32426
32815
  entityId,
32427
- relationship: optionalText(link.relationship, 120) ?? void 0,
32428
- summary: optionalText(link.summary, 300) ?? void 0,
32429
- imageUrl: optionalText(link.imageUrl, 2e3) ?? void 0,
32816
+ relationship: optionalText2(link.relationship, 120) ?? void 0,
32817
+ summary: optionalText2(link.summary, 300) ?? void 0,
32818
+ imageUrl: optionalText2(link.imageUrl, 2e3) ?? void 0,
32430
32819
  needsLink
32431
32820
  };
32432
32821
  }).slice(0, 100);
@@ -32435,20 +32824,20 @@ function normalizeCitations(citations) {
32435
32824
  if (!Array.isArray(citations)) return [];
32436
32825
  return citations.map((citation) => ({
32437
32826
  title: requiredText(citation.title, "citation.title", 1, 240),
32438
- url: optionalText(citation.url, 2e3) ?? void 0,
32439
- source: optionalText(citation.source, 240) ?? void 0,
32440
- note: optionalText(citation.note, 1e3) ?? void 0,
32441
- accessedAt: optionalText(citation.accessedAt, 80) ?? void 0
32827
+ url: optionalText2(citation.url, 2e3) ?? void 0,
32828
+ source: optionalText2(citation.source, 240) ?? void 0,
32829
+ note: optionalText2(citation.note, 1e3) ?? void 0,
32830
+ accessedAt: optionalText2(citation.accessedAt, 80) ?? void 0
32442
32831
  })).slice(0, 300);
32443
32832
  }
32444
32833
  function normalizeFeaturedImage(image) {
32445
32834
  if (!image?.url) return void 0;
32446
32835
  return {
32447
32836
  url: requiredText(image.url, "featuredImage.url", 1, 2e3),
32448
- alt: optionalText(image.alt, 500) ?? void 0,
32449
- caption: optionalText(image.caption, 1e3) ?? void 0,
32450
- sourceUrl: optionalText(image.sourceUrl, 2e3) ?? void 0,
32451
- license: optionalText(image.license, 240) ?? void 0,
32837
+ alt: optionalText2(image.alt, 500) ?? void 0,
32838
+ caption: optionalText2(image.caption, 1e3) ?? void 0,
32839
+ sourceUrl: optionalText2(image.sourceUrl, 2e3) ?? void 0,
32840
+ license: optionalText2(image.license, 240) ?? void 0,
32452
32841
  width: positiveInt(image.width),
32453
32842
  height: positiveInt(image.height)
32454
32843
  };
@@ -32457,11 +32846,11 @@ function normalizeMedia(media, featuredImage) {
32457
32846
  const normalized = Array.isArray(media) ? media.map((item) => ({
32458
32847
  type: item.type,
32459
32848
  url: requiredText(item.url, "media.url", 1, 2e3),
32460
- alt: optionalText(item.alt, 500) ?? void 0,
32461
- caption: optionalText(item.caption, 1e3) ?? void 0,
32462
- posterUrl: optionalText(item.posterUrl, 2e3) ?? void 0,
32463
- sourceUrl: optionalText(item.sourceUrl, 2e3) ?? void 0,
32464
- license: optionalText(item.license, 240) ?? void 0,
32849
+ alt: optionalText2(item.alt, 500) ?? void 0,
32850
+ caption: optionalText2(item.caption, 1e3) ?? void 0,
32851
+ posterUrl: optionalText2(item.posterUrl, 2e3) ?? void 0,
32852
+ sourceUrl: optionalText2(item.sourceUrl, 2e3) ?? void 0,
32853
+ license: optionalText2(item.license, 240) ?? void 0,
32465
32854
  width: positiveInt(item.width),
32466
32855
  height: positiveInt(item.height),
32467
32856
  durationSeconds: positiveInt(item.durationSeconds)
@@ -32491,28 +32880,28 @@ function normalizeMedia(media, featuredImage) {
32491
32880
  }
32492
32881
  function normalizeSource(source) {
32493
32882
  return {
32494
- originalUrl: optionalText(source?.originalUrl, 2e3) ?? void 0,
32495
- resolvedUrl: optionalText(source?.resolvedUrl, 2e3) ?? void 0,
32496
- sourceCanonicalUrl: optionalText(source?.sourceCanonicalUrl, 2e3) ?? void 0,
32497
- relCanonicalHref: optionalText(source?.relCanonicalHref, 2e3) ?? void 0,
32498
- sourceByline: optionalText(source?.sourceByline, 240) ?? void 0,
32499
- publisher: optionalText(source?.publisher, 240) ?? void 0,
32883
+ originalUrl: optionalText2(source?.originalUrl, 2e3) ?? void 0,
32884
+ resolvedUrl: optionalText2(source?.resolvedUrl, 2e3) ?? void 0,
32885
+ sourceCanonicalUrl: optionalText2(source?.sourceCanonicalUrl, 2e3) ?? void 0,
32886
+ relCanonicalHref: optionalText2(source?.relCanonicalHref, 2e3) ?? void 0,
32887
+ sourceByline: optionalText2(source?.sourceByline, 240) ?? void 0,
32888
+ publisher: optionalText2(source?.publisher, 240) ?? void 0,
32500
32889
  authors: uniqueStrings(source?.authors ?? []).slice(0, 20),
32501
- publishedAt: optionalText(source?.publishedAt, 80) ?? void 0,
32502
- capturedAt: optionalText(source?.capturedAt, 80) ?? void 0,
32503
- license: optionalText(source?.license, 240) ?? void 0,
32504
- rightsSummary: optionalText(source?.rightsSummary, 1e3) ?? void 0,
32505
- contentHash: optionalText(source?.contentHash, 200) ?? void 0
32890
+ publishedAt: optionalText2(source?.publishedAt, 80) ?? void 0,
32891
+ capturedAt: optionalText2(source?.capturedAt, 80) ?? void 0,
32892
+ license: optionalText2(source?.license, 240) ?? void 0,
32893
+ rightsSummary: optionalText2(source?.rightsSummary, 1e3) ?? void 0,
32894
+ contentHash: optionalText2(source?.contentHash, 200) ?? void 0
32506
32895
  };
32507
32896
  }
32508
32897
  function normalizeSeo(seo) {
32509
32898
  return {
32510
- canonicalUrl: optionalText(seo?.canonicalUrl, 2e3) ?? void 0,
32511
- relCanonical: optionalText(seo?.relCanonical, 2e3) ?? void 0,
32512
- metaTitle: optionalText(seo?.metaTitle, 240) ?? void 0,
32513
- metaDescription: optionalText(seo?.metaDescription, 500) ?? void 0,
32514
- ogImage: optionalText(seo?.ogImage, 2e3) ?? void 0,
32515
- schemaOrgType: optionalText(seo?.schemaOrgType, 80) ?? void 0,
32899
+ canonicalUrl: optionalText2(seo?.canonicalUrl, 2e3) ?? void 0,
32900
+ relCanonical: optionalText2(seo?.relCanonical, 2e3) ?? void 0,
32901
+ metaTitle: optionalText2(seo?.metaTitle, 240) ?? void 0,
32902
+ metaDescription: optionalText2(seo?.metaDescription, 500) ?? void 0,
32903
+ ogImage: optionalText2(seo?.ogImage, 2e3) ?? void 0,
32904
+ schemaOrgType: optionalText2(seo?.schemaOrgType, 80) ?? void 0,
32516
32905
  noIndex: seo?.noIndex === true
32517
32906
  };
32518
32907
  }
@@ -32521,37 +32910,37 @@ function normalizeRelatedEntities(related) {
32521
32910
  return related.map((item) => ({
32522
32911
  entityId: normalizeEntityId(item.entityId),
32523
32912
  title: requiredText(item.title, "relatedEntities.title", 1, 180),
32524
- relationship: optionalText(item.relationship, 120) ?? void 0,
32913
+ relationship: optionalText2(item.relationship, 120) ?? void 0,
32525
32914
  slug: item.slug ? normalizeSlug(item.slug) : void 0,
32526
- url: optionalText(item.url, 2e3) ?? void 0,
32527
- description: optionalText(item.description, 300) ?? void 0
32915
+ url: optionalText2(item.url, 2e3) ?? void 0,
32916
+ description: optionalText2(item.description, 300) ?? void 0
32528
32917
  })).slice(0, 200);
32529
32918
  }
32530
32919
  function sanitizeSearchFilters(filters) {
32531
32920
  return {
32532
- query: optionalText(filters.query, 300) ?? void 0,
32921
+ query: optionalText2(filters.query, 300) ?? void 0,
32533
32922
  entityType: filters.entityType ? normalizeEntityType(filters.entityType) : void 0,
32534
- tag: optionalText(filters.tag, 80) ?? void 0,
32923
+ tag: optionalText2(filters.tag, 80) ?? void 0,
32535
32924
  tags: uniqueStrings(filters.tags ?? []).slice(0, 20),
32536
- keyword: optionalText(filters.keyword, 120) ?? void 0,
32925
+ keyword: optionalText2(filters.keyword, 120) ?? void 0,
32537
32926
  keywords: uniqueStrings(filters.keywords ?? []).slice(0, 20),
32538
32927
  relatedEntityId: normalizeEntityId(filters.relatedEntityId),
32539
32928
  trailId: normalizeEntityId(filters.trailId),
32540
- sourceDomain: optionalText(filters.sourceDomain, 240) ?? void 0,
32929
+ sourceDomain: optionalText2(filters.sourceDomain, 240) ?? void 0,
32541
32930
  hasMedia: filters.hasMedia === true ? true : void 0,
32542
32931
  hasVideo: filters.hasVideo === true ? true : void 0,
32543
- publishedAfter: optionalText(filters.publishedAfter, 80) ?? void 0,
32544
- updatedAfter: optionalText(filters.updatedAfter, 80) ?? void 0,
32545
- filterId: optionalText(filters.filterId, 200) ?? void 0,
32932
+ publishedAfter: optionalText2(filters.publishedAfter, 80) ?? void 0,
32933
+ updatedAfter: optionalText2(filters.updatedAfter, 80) ?? void 0,
32934
+ filterId: optionalText2(filters.filterId, 200) ?? void 0,
32546
32935
  limit: clampNumber(filters.limit, 1, 100, 20),
32547
32936
  offset: clampNumber(filters.offset, 0, 1e4, 0)
32548
32937
  };
32549
32938
  }
32550
32939
  function sanitizeNeedsLinkFilters(filters) {
32551
32940
  return {
32552
- query: optionalText(filters.query, 300) ?? void 0,
32941
+ query: optionalText2(filters.query, 300) ?? void 0,
32553
32942
  entityType: filters.entityType ? normalizeEntityType(filters.entityType) : void 0,
32554
- tag: optionalText(filters.tag, 80) ?? void 0,
32943
+ tag: optionalText2(filters.tag, 80) ?? void 0,
32555
32944
  tags: uniqueStrings(filters.tags ?? []).slice(0, 20),
32556
32945
  sourceEntityId: normalizeEntityId(filters.sourceEntityId),
32557
32946
  sourceSlug: filters.sourceSlug ? normalizeSlug(filters.sourceSlug) : void 0,
@@ -32572,7 +32961,7 @@ function commonsIndexingPolicy() {
32572
32961
  storageScope: "platform_commons_graph",
32573
32962
  countsAgainstUserStorage: false,
32574
32963
  embedsPrivateUserVaults: false,
32575
- corpus: ["published Commons article body", "tags", "keywords", "JSON-LD metadata", "related entities", "See Also concepts marked needsLink"],
32964
+ corpus: ["published Commons article body", "tags", "keywords", "JSON-LD metadata", "related entities", "governed relationship claims", "See Also concepts marked needsLink"],
32576
32965
  excluded: ["comments", "comment threads", "reader alerts", "private reading-room state"],
32577
32966
  retrievalPattern: {
32578
32967
  lexicalFirst: ["title", "tags", "keywords", "source metadata", "article body", "See Also concepts"],
@@ -32648,7 +33037,7 @@ function searchableText(entity) {
32648
33037
  ].filter(Boolean).join("\n").toLowerCase();
32649
33038
  }
32650
33039
  function actorLabel(input, user) {
32651
- return optionalText(input.actorLabel, 120) || optionalText(user.name, 120) || `mcp-user-${user.id}`;
33040
+ return optionalText2(input.actorLabel, 120) || optionalText2(user.name, 120) || `mcp-user-${user.id}`;
32652
33041
  }
32653
33042
  function publicBaseUrl() {
32654
33043
  return (process.env.TRANSPARENT_COMMONS_PUBLIC_BASE_URL?.trim() || DEFAULT_COMMONS_BASE_URL).replace(/\/+$/, "");
@@ -32659,13 +33048,16 @@ function publicUrlForSlug(slug2) {
32659
33048
  function allocateEntityId() {
32660
33049
  return `TPW-Q${Date.now()}${Math.floor(Math.random() * 1e3).toString().padStart(3, "0")}`;
32661
33050
  }
33051
+ function allocateClaimId() {
33052
+ return `TPW-S${Date.now()}${Math.floor(Math.random() * 1e6).toString().padStart(6, "0")}`;
33053
+ }
32662
33054
  function normalizeEntityId(value) {
32663
- const text2 = optionalText(value, 80);
33055
+ const text2 = optionalText2(value, 80);
32664
33056
  if (!text2) return void 0;
32665
33057
  return text2.replace(/\s+/g, "").toUpperCase();
32666
33058
  }
32667
33059
  function normalizeEntityType(value) {
32668
- const raw = optionalText(value, 120) || DEFAULT_ENTITY_TYPE;
33060
+ const raw = optionalText2(value, 120) || DEFAULT_ENTITY_TYPE;
32669
33061
  const squashed = raw.replace(/[\s_-]+/g, "").toLowerCase();
32670
33062
  if (squashed === "publicarticle" || squashed === "article" || squashed === "item") return DEFAULT_ENTITY_TYPE;
32671
33063
  if (squashed === "trail" || squashed === "readingtrail") return "Trail";
@@ -32693,7 +33085,7 @@ function requiredText(value, field, min, max) {
32693
33085
  if (text2.length < min) throw new CommonsRepositoryError(`${field.replaceAll(".", "_")}_required`, `${field} must be at least ${min} characters.`);
32694
33086
  return text2.slice(0, max);
32695
33087
  }
32696
- function optionalText(value, max) {
33088
+ function optionalText2(value, max) {
32697
33089
  const text2 = String(value ?? "").trim();
32698
33090
  return text2 ? text2.slice(0, max) : void 0;
32699
33091
  }
@@ -32713,7 +33105,7 @@ function uniqueStrings(values) {
32713
33105
  const seen = /* @__PURE__ */ new Set();
32714
33106
  const out = [];
32715
33107
  for (const value of values) {
32716
- const text2 = optionalText(value, 120);
33108
+ const text2 = optionalText2(value, 120);
32717
33109
  if (!text2) continue;
32718
33110
  const key = text2.toLowerCase();
32719
33111
  if (seen.has(key)) continue;
@@ -32760,7 +33152,7 @@ var CommonsRepositoryError = class extends Error {
32760
33152
  };
32761
33153
 
32762
33154
  // src/api/commons-publication-repository.ts
32763
- import { createHash as createHash13, randomUUID as randomUUID16 } from "crypto";
33155
+ import { createHash as createHash14, randomUUID as randomUUID16 } from "crypto";
32764
33156
  var PUBLICATION_ROOT_DOMAIN = process.env.COMMONS_PUBLICATION_ROOT_DOMAIN || "transparent-commons.cc";
32765
33157
  var RESERVED_SUBDOMAINS = /* @__PURE__ */ new Set([
32766
33158
  "admin",
@@ -32958,7 +33350,7 @@ async function publishCommonsEditorial(input, user) {
32958
33350
  JSON.stringify(editionInput.articles),
32959
33351
  html,
32960
33352
  rendered.filename,
32961
- createHash13("sha256").update(html).digest("hex"),
33353
+ createHash14("sha256").update(html).digest("hex"),
32962
33354
  rendered.articleCount,
32963
33355
  rendered.wordCount,
32964
33356
  Buffer.byteLength(html),
@@ -33237,6 +33629,7 @@ var SubmitEntitySchema = z32.object({
33237
33629
  url: z32.string().url().optional(),
33238
33630
  description: z32.string().max(300).optional()
33239
33631
  }).strict()).max(200).optional(),
33632
+ claims: z32.array(CommonsClaimInputSchema).max(300).optional(),
33240
33633
  bodyMarkdown: z32.string().max(5e5).optional(),
33241
33634
  contentSections: z32.array(z32.object({
33242
33635
  id: z32.string().max(180).optional(),
@@ -33314,6 +33707,17 @@ commonsApp.get("/health", async (c) => {
33314
33707
  await ensureCommonsSchema();
33315
33708
  return c.json({ ok: true, data: commonsDatabaseReport() });
33316
33709
  });
33710
+ commonsApp.get("/profiles/relationships/v1", (c) => c.json({
33711
+ id: COMMONS_RELATIONSHIP_PROFILE,
33712
+ title: "Transparent Commons governed relationship linksets",
33713
+ mediaType: COMMONS_LINKSET_MEDIA_TYPE,
33714
+ claimAuthority: "MCP Scraper authenticated proposal and contribution ledger",
33715
+ anchorPolicy: "Every link context is an absolute canonical Transparent Commons wiki URL.",
33716
+ predicatePolicy: "Predicates are curated registered link relations or absolute HTTPS extension relation URIs.",
33717
+ publicationPolicy: "Only approved, active, non-deprecated claims whose dispute state is undisputed or attributed are projected.",
33718
+ trustPolicy: "A third-party target is an asserted relationship, not an endorsement or proof that the target agrees.",
33719
+ source: "https://www.rfc-editor.org/rfc/rfc9264.html"
33720
+ }));
33317
33721
  commonsApp.post("/publications/prepare", auth2, async (c) => {
33318
33722
  const parsed = PreparePublicationSchema.safeParse(await c.req.json().catch(() => ({})));
33319
33723
  if (!parsed.success) return validationError(c, parsed.error);
@@ -33444,11 +33848,32 @@ commonsApp.post("/entities/propose", auth2, async (c) => {
33444
33848
  commonsApp.get("/entities/:idOrSlug", async (c) => {
33445
33849
  const entity = await getCommonsEntity(c.req.param("idOrSlug"));
33446
33850
  if (!entity) return c.json({ ok: false, error: "entity_not_found", message: "No published Commons entity matched that id or slug." }, 404);
33851
+ c.header("Link", commonsLinksetDiscoveryHeader(entity.id));
33447
33852
  return c.json({ ok: true, data: entity });
33448
33853
  });
33854
+ commonsApp.get("/entities/:idOrSlug/claims", async (c) => {
33855
+ const result = await getCommonsEntityLinkset(c.req.param("idOrSlug"));
33856
+ if (!result) return c.json({ ok: false, error: "entity_not_found", message: "No published Commons entity matched that id or slug." }, 404);
33857
+ c.header("Link", commonsLinksetDiscoveryHeader(result.entity.id));
33858
+ return c.json({ ok: true, data: { entity: result.entity, claims: result.claims, linksetUrl: result.linksetUrl } });
33859
+ });
33860
+ commonsApp.get("/entities/:idOrSlug/linkset", async (c) => {
33861
+ const result = await getCommonsEntityLinkset(c.req.param("idOrSlug"));
33862
+ if (!result) return c.json({ ok: false, error: "entity_not_found", message: "No published Commons entity matched that id or slug." }, 404);
33863
+ c.header("Content-Type", `${COMMONS_LINKSET_MEDIA_TYPE}; profile="${COMMONS_RELATIONSHIP_PROFILE}"`);
33864
+ c.header("ETag", result.etag);
33865
+ c.header("Link", `<${COMMONS_RELATIONSHIP_PROFILE}>; rel="profile"`);
33866
+ c.header("Cache-Control", "public, max-age=60, s-maxage=300");
33867
+ if (c.req.header("If-None-Match") === result.etag) return c.body(null, 304);
33868
+ if (c.req.query("envelope") === "true") {
33869
+ return c.json({ ok: true, data: { ...result, document: result.document } });
33870
+ }
33871
+ return c.body(JSON.stringify(result.document));
33872
+ });
33449
33873
  commonsApp.get("/entities/:idOrSlug/wiki-page", async (c) => {
33450
33874
  const entity = await getCommonsEntity(c.req.param("idOrSlug"));
33451
33875
  if (!entity) return c.json({ ok: false, error: "entity_not_found", message: "No published Commons entity matched that id or slug." }, 404);
33876
+ c.header("Link", commonsLinksetDiscoveryHeader(entity.id));
33452
33877
  return c.json({ ok: true, data: entityToWikiPage(entity) });
33453
33878
  });
33454
33879
  commonsApp.get("/entities/:idOrSlug/json-ld", async (c) => {
@@ -33464,6 +33889,7 @@ commonsApp.get("/entities/:idOrSlug/ledger", async (c) => {
33464
33889
  commonsApp.get("/wiki/:slug", async (c) => {
33465
33890
  const entity = await getCommonsEntity(c.req.param("slug"));
33466
33891
  if (!entity) return c.json({ ok: false, error: "page_not_found", message: "No published Commons wiki page matched that slug." }, 404);
33892
+ c.header("Link", commonsLinksetDiscoveryHeader(entity.id));
33467
33893
  return c.json({
33468
33894
  ok: true,
33469
33895
  data: {
@@ -33561,18 +33987,1700 @@ function xmlEscape(value) {
33561
33987
  return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
33562
33988
  }
33563
33989
 
33564
- // src/api/scheduled-result-routes.ts
33990
+ // src/api/analytics-routes.ts
33565
33991
  import { Hono as Hono32 } from "hono";
33992
+ import { createMiddleware as createMiddleware4 } from "hono/factory";
33993
+ import { z as z34 } from "zod";
33994
+ import { createHash as createHash16 } from "crypto";
33995
+ import Papa2 from "papaparse";
33996
+
33997
+ // src/api/analytics-entitlement.ts
33998
+ import {
33999
+ createHash as createHash15,
34000
+ createHmac as createHmac3,
34001
+ randomBytes as randomBytes3,
34002
+ timingSafeEqual as timingSafeEqual2
34003
+ } from "crypto";
33566
34004
  import { z as z33 } from "zod";
34005
+ var THORBIT_PRODUCT_URL = "https://thorbit.ai";
34006
+ var REFRESH_INTERVAL_MS = 15 * 60 * 1e3;
34007
+ var ELIGIBLE_GRACE_MS = 24 * 60 * 60 * 1e3;
34008
+ var TRIAL_LENGTH_MS = 30 * 24 * 60 * 60 * 1e3;
34009
+ var ThorbitEntitlementSchema = z33.object({
34010
+ product: z33.literal("x_ray_pixel"),
34011
+ eligible: z33.boolean(),
34012
+ reason: z33.string().min(1).max(120),
34013
+ organizationId: z33.string().min(1).max(64),
34014
+ planId: z33.string().max(32).nullable(),
34015
+ subscriptionStatus: z33.string().max(20).nullable(),
34016
+ monthlyRecurringRevenueCents: z33.number().int().min(0),
34017
+ minimumMonthlyRecurringRevenueCents: z33.number().int().min(5e3),
34018
+ checkedAt: z33.string().datetime()
34019
+ }).strict();
34020
+ var AssertionSchema = z33.object({
34021
+ version: z33.literal(1),
34022
+ issuer: z33.literal("thorbit"),
34023
+ audience: z33.literal("mcp-scraper:x-ray"),
34024
+ issuedAt: z33.number().int().positive(),
34025
+ expiresAt: z33.number().int().positive(),
34026
+ entitlement: ThorbitEntitlementSchema
34027
+ }).strict();
34028
+ function thorbitBaseUrl() {
34029
+ return (process.env.THORBIT_X_RAY_BASE_URL?.trim() || THORBIT_PRODUCT_URL).replace(/\/$/, "");
34030
+ }
34031
+ function bridgeSecret() {
34032
+ return process.env.THORBIT_X_RAY_BRIDGE_SECRET?.trim() ?? "";
34033
+ }
34034
+ function bridgeConfigured() {
34035
+ return bridgeSecret().length >= 32;
34036
+ }
34037
+ function hint(value) {
34038
+ return value.length <= 8 ? value : `${value.slice(0, 4)}\u2026${value.slice(-4)}`;
34039
+ }
34040
+ function viewFromRow(row, options = {}) {
34041
+ const now = Date.now();
34042
+ const withinGrace = row ? Date.parse(row.access_expires_at) > now : false;
34043
+ const trialActive = Boolean(row && Date.parse(row.trial_ends_at) > now);
34044
+ const paidAccess = Boolean(row?.eligible && withinGrace);
34045
+ const access2 = paidAccess || trialActive;
34046
+ const status = paidAccess ? options.stale ? "stale" : "eligible" : trialActive ? "trial" : !row || row.connection_source === "trial" ? "trial_expired" : options.bridgeError ? "bridge_unavailable" : "ineligible";
34047
+ const trialStartedAt = row?.trial_started_at ?? new Date(now).toISOString();
34048
+ const trialEndsAt = row?.trial_ends_at ?? new Date(now).toISOString();
34049
+ return {
34050
+ product: "x_ray_pixel",
34051
+ productName: "Thorbit X-Ray Pixel",
34052
+ access: access2,
34053
+ status,
34054
+ reason: trialActive ? "trial_active" : row?.reason ?? "trial_expired",
34055
+ planId: row?.plan_id ?? null,
34056
+ subscriptionStatus: row?.subscription_status ?? null,
34057
+ monthlyRecurringRevenueCents: Number(row?.monthly_recurring_revenue_cents ?? 0),
34058
+ minimumMonthlyRecurringRevenueCents: Number(row?.minimum_monthly_recurring_revenue_cents ?? 5e3),
34059
+ verifiedAt: row?.verified_at ?? null,
34060
+ connectionSource: row?.connection_source === "trial" ? null : row?.connection_source ?? null,
34061
+ thorbitOrganizationHint: row?.thorbit_org_id ? hint(row.thorbit_org_id) : null,
34062
+ trialStartedAt,
34063
+ trialEndsAt,
34064
+ trialDaysRemaining: Math.max(0, Math.ceil((Date.parse(trialEndsAt) - now) / (24 * 60 * 60 * 1e3))),
34065
+ bridgeConfigured: bridgeConfigured(),
34066
+ billing: {
34067
+ standalonePriceUsdMonthly: 50,
34068
+ includedWithThorbitSubscriptionAtOrAboveUsdMonthly: 50,
34069
+ consumesMcpScraperCredits: false
34070
+ },
34071
+ owner: {
34072
+ name: "Thorbit",
34073
+ productUrl: THORBIT_PRODUCT_URL,
34074
+ host: "MCP Scraper"
34075
+ }
34076
+ };
34077
+ }
34078
+ async function storedEntitlement(userId) {
34079
+ const result = await getAnalyticsPool().query(
34080
+ "SELECT * FROM analytics_thorbit_entitlements WHERE user_id = $1",
34081
+ [userId]
34082
+ );
34083
+ return result.rows[0] ?? null;
34084
+ }
34085
+ async function ensureTrial(userId) {
34086
+ const startedAt = /* @__PURE__ */ new Date();
34087
+ const endsAt = new Date(startedAt.getTime() + TRIAL_LENGTH_MS);
34088
+ const result = await getAnalyticsPool().query(
34089
+ `INSERT INTO analytics_thorbit_entitlements(
34090
+ user_id, thorbit_org_id, connection_source, eligible, reason, plan_id,
34091
+ subscription_status, monthly_recurring_revenue_cents,
34092
+ minimum_monthly_recurring_revenue_cents, verified_at, refresh_after,
34093
+ access_expires_at, trial_started_at, trial_ends_at, last_refresh_error, updated_at
34094
+ ) VALUES ($1,NULL,'trial',false,'trial_active',NULL,NULL,0,5000,$2,$3,$3,$2,$3,NULL,now())
34095
+ ON CONFLICT(user_id) DO UPDATE SET user_id=excluded.user_id
34096
+ RETURNING *`,
34097
+ [userId, startedAt.toISOString(), endsAt.toISOString()]
34098
+ );
34099
+ return result.rows[0];
34100
+ }
34101
+ async function persistEntitlement(userId, source, entitlement) {
34102
+ const checked = new Date(entitlement.checkedAt);
34103
+ const refreshAfter = new Date(checked.getTime() + REFRESH_INTERVAL_MS);
34104
+ const accessExpiresAt = new Date(
34105
+ checked.getTime() + (entitlement.eligible ? ELIGIBLE_GRACE_MS : REFRESH_INTERVAL_MS)
34106
+ );
34107
+ const result = await getAnalyticsPool().query(
34108
+ `INSERT INTO analytics_thorbit_entitlements(
34109
+ user_id, thorbit_org_id, connection_source, eligible, reason, plan_id,
34110
+ subscription_status, monthly_recurring_revenue_cents,
34111
+ minimum_monthly_recurring_revenue_cents, verified_at, refresh_after,
34112
+ access_expires_at, last_refresh_error, updated_at
34113
+ ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,NULL,now())
34114
+ ON CONFLICT(user_id) DO UPDATE SET
34115
+ thorbit_org_id=excluded.thorbit_org_id,
34116
+ connection_source=excluded.connection_source,
34117
+ eligible=excluded.eligible,
34118
+ reason=excluded.reason,
34119
+ plan_id=excluded.plan_id,
34120
+ subscription_status=excluded.subscription_status,
34121
+ monthly_recurring_revenue_cents=excluded.monthly_recurring_revenue_cents,
34122
+ minimum_monthly_recurring_revenue_cents=excluded.minimum_monthly_recurring_revenue_cents,
34123
+ verified_at=excluded.verified_at,
34124
+ refresh_after=excluded.refresh_after,
34125
+ access_expires_at=excluded.access_expires_at,
34126
+ last_refresh_error=NULL,
34127
+ updated_at=now()
34128
+ RETURNING *`,
34129
+ [
34130
+ userId,
34131
+ entitlement.organizationId,
34132
+ source,
34133
+ entitlement.eligible,
34134
+ entitlement.reason,
34135
+ entitlement.planId,
34136
+ entitlement.subscriptionStatus,
34137
+ entitlement.monthlyRecurringRevenueCents,
34138
+ entitlement.minimumMonthlyRecurringRevenueCents,
34139
+ checked.toISOString(),
34140
+ refreshAfter.toISOString(),
34141
+ accessExpiresAt.toISOString()
34142
+ ]
34143
+ );
34144
+ return result.rows[0];
34145
+ }
34146
+ async function requestEntitlement(method, options) {
34147
+ const headers = { accept: "application/json" };
34148
+ let body;
34149
+ if (method === "POST") {
34150
+ headers.authorization = `Bearer ${options.apiKey ?? ""}`;
34151
+ } else {
34152
+ headers.authorization = `Bearer ${bridgeSecret()}`;
34153
+ headers["content-type"] = "application/json";
34154
+ body = JSON.stringify({ organizationId: options.organizationId });
34155
+ }
34156
+ const response = await fetch(`${thorbitBaseUrl()}/api/v1/x-ray/entitlement`, {
34157
+ method,
34158
+ headers,
34159
+ body,
34160
+ signal: AbortSignal.timeout(8e3)
34161
+ });
34162
+ const payload = await response.json().catch(() => null);
34163
+ if (!response.ok) {
34164
+ const message = payload && typeof payload === "object" && "error" in payload ? String(payload.error) : `Thorbit entitlement check failed (${response.status})`;
34165
+ throw new Error(message);
34166
+ }
34167
+ const parsed = z33.object({ entitlement: ThorbitEntitlementSchema }).safeParse(payload);
34168
+ if (!parsed.success) throw new Error("Thorbit returned an invalid entitlement assertion");
34169
+ return parsed.data.entitlement;
34170
+ }
34171
+ async function resolveAnalyticsEntitlement(userId) {
34172
+ const row = await storedEntitlement(userId) ?? await ensureTrial(userId);
34173
+ if (row.connection_source === "trial" || !row.thorbit_org_id) return viewFromRow(row);
34174
+ if (Date.parse(row.refresh_after) > Date.now()) return viewFromRow(row);
34175
+ if (!bridgeConfigured()) return viewFromRow(row, { stale: true, bridgeError: true });
34176
+ try {
34177
+ const entitlement = await requestEntitlement("PUT", {
34178
+ organizationId: row.thorbit_org_id
34179
+ });
34180
+ return viewFromRow(
34181
+ await persistEntitlement(userId, row.connection_source, entitlement)
34182
+ );
34183
+ } catch (error) {
34184
+ await getAnalyticsPool().query(
34185
+ "UPDATE analytics_thorbit_entitlements SET last_refresh_error=$2, updated_at=now() WHERE user_id=$1",
34186
+ [userId, error instanceof Error ? error.message.slice(0, 240) : "refresh_failed"]
34187
+ );
34188
+ return viewFromRow(row, { stale: true, bridgeError: true });
34189
+ }
34190
+ }
34191
+ async function analyticsOwnerHasCurrentEntitlement(userId) {
34192
+ const result = await getAnalyticsPool().query(
34193
+ `SELECT EXISTS(
34194
+ SELECT 1 FROM analytics_thorbit_entitlements
34195
+ WHERE user_id=$1 AND (
34196
+ (eligible=true AND access_expires_at > now()) OR trial_ends_at > now()
34197
+ )
34198
+ ) AS entitled`,
34199
+ [userId]
34200
+ );
34201
+ return Boolean(result.rows[0]?.entitled);
34202
+ }
34203
+ async function connectAnalyticsWithThorbitApiKey(userId, apiKey) {
34204
+ const entitlement = await requestEntitlement("POST", { apiKey });
34205
+ return viewFromRow(await persistEntitlement(userId, "api_key", entitlement));
34206
+ }
34207
+ function dashboardCallbackUrl() {
34208
+ const value = process.env.ANALYTICS_DASHBOARD_URL?.trim() || (process.env.NODE_ENV === "production" ? "https://mcpscraper.dev/dashboard/analytics" : "http://localhost:3000/dashboard/analytics");
34209
+ return new URL(value).toString();
34210
+ }
34211
+ async function createThorbitConnectUrl(userId) {
34212
+ if (!bridgeConfigured()) throw new Error("Thorbit X-Ray account bridge is not configured");
34213
+ const state = randomBytes3(32).toString("base64url");
34214
+ const stateHash = createHash15("sha256").update(state).digest("hex");
34215
+ await getAnalyticsPool().query(
34216
+ `INSERT INTO analytics_thorbit_connect_states(state_hash,user_id,expires_at)
34217
+ VALUES ($1,$2,now()+interval '10 minutes')
34218
+ ON CONFLICT(state_hash) DO NOTHING`,
34219
+ [stateHash, userId]
34220
+ );
34221
+ const url = new URL(`${thorbitBaseUrl()}/api/v1/x-ray/entitlement`);
34222
+ url.searchParams.set("redirect_uri", dashboardCallbackUrl());
34223
+ url.searchParams.set("state", state);
34224
+ return url.toString();
34225
+ }
34226
+ function verifyThorbitAssertion(token4) {
34227
+ const [encoded, signature] = token4.split(".");
34228
+ if (!encoded || !signature || !bridgeConfigured()) throw new Error("Invalid Thorbit assertion");
34229
+ const expected = createHmac3("sha256", bridgeSecret()).update(encoded).digest();
34230
+ const actual = Buffer.from(signature, "base64url");
34231
+ if (actual.length !== expected.length || !timingSafeEqual2(actual, expected)) {
34232
+ throw new Error("Invalid Thorbit assertion signature");
34233
+ }
34234
+ const parsed = AssertionSchema.safeParse(
34235
+ JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"))
34236
+ );
34237
+ if (!parsed.success || parsed.data.expiresAt <= Math.floor(Date.now() / 1e3)) {
34238
+ throw new Error("Expired or invalid Thorbit assertion");
34239
+ }
34240
+ return parsed.data.entitlement;
34241
+ }
34242
+ async function consumeThorbitConnectCallback(input) {
34243
+ const stateHash = createHash15("sha256").update(input.state).digest("hex");
34244
+ const client2 = await getAnalyticsPool().connect();
34245
+ try {
34246
+ await client2.query("BEGIN");
34247
+ const state = await client2.query(
34248
+ `UPDATE analytics_thorbit_connect_states
34249
+ SET consumed_at=now()
34250
+ WHERE state_hash=$1 AND user_id=$2 AND consumed_at IS NULL AND expires_at > now()
34251
+ RETURNING user_id`,
34252
+ [stateHash, input.userId]
34253
+ );
34254
+ if (!state.rowCount) throw new Error("Thorbit connection state is invalid or expired");
34255
+ const entitlement = verifyThorbitAssertion(input.assertion);
34256
+ await client2.query("COMMIT");
34257
+ return viewFromRow(
34258
+ await persistEntitlement(input.userId, "thorbit_session", entitlement)
34259
+ );
34260
+ } catch (error) {
34261
+ await client2.query("ROLLBACK");
34262
+ throw error;
34263
+ } finally {
34264
+ client2.release();
34265
+ }
34266
+ }
34267
+ async function disconnectAnalyticsEntitlement(userId) {
34268
+ await getAnalyticsPool().query(
34269
+ `UPDATE analytics_thorbit_entitlements
34270
+ SET thorbit_org_id=NULL,
34271
+ connection_source='trial',
34272
+ eligible=false,
34273
+ reason=CASE WHEN trial_ends_at > now() THEN 'trial_active' ELSE 'trial_expired' END,
34274
+ plan_id=NULL,
34275
+ subscription_status=NULL,
34276
+ monthly_recurring_revenue_cents=0,
34277
+ last_refresh_error=NULL,
34278
+ updated_at=now()
34279
+ WHERE user_id=$1`,
34280
+ [userId]
34281
+ );
34282
+ }
34283
+
34284
+ // src/api/analytics-routes.ts
34285
+ var analyticsApp = new Hono32();
34286
+ var auth3 = createApiKeyAuth();
34287
+ var entitlementGuard = createMiddleware4(async (c, next) => {
34288
+ const entitlement = await resolveAnalyticsEntitlement(Number(c.get("user").id));
34289
+ if (!entitlement.access) {
34290
+ return c.json(
34291
+ {
34292
+ ok: false,
34293
+ code: "thorbit_entitlement_required",
34294
+ error: "Thorbit X-Ray Pixel requires an active $50+ Thorbit subscription.",
34295
+ entitlement
34296
+ },
34297
+ 402
34298
+ );
34299
+ }
34300
+ await next();
34301
+ });
34302
+ var ThorbitApiKeySchema = z34.object({ apiKey: z34.string().trim().min(20).max(512) }).strict();
34303
+ var ThorbitCallbackSchema = z34.object({
34304
+ state: z34.string().min(20).max(300),
34305
+ assertion: z34.string().min(40).max(8e3)
34306
+ }).strict();
34307
+ var SiteInputSchema = z34.object({
34308
+ name: z34.string().trim().min(1).max(120),
34309
+ slug: z34.string().trim().min(1).max(80).optional(),
34310
+ timezone: z34.string().trim().min(1).max(80).optional()
34311
+ }).strict();
34312
+ var PixelInputSchema = z34.object({
34313
+ name: z34.string().trim().min(1).max(120),
34314
+ environment: z34.string().trim().min(1).max(60).optional()
34315
+ }).strict();
34316
+ var PixelUpdateSchema = z34.object({
34317
+ name: z34.string().trim().min(1).max(120).optional(),
34318
+ environment: z34.string().trim().min(1).max(60).optional(),
34319
+ status: z34.enum(["active", "disabled"]).optional()
34320
+ }).strict().refine(
34321
+ (value) => Object.keys(value).length > 0,
34322
+ "Provide at least one change."
34323
+ );
34324
+ var EventSchema = z34.object({
34325
+ eventId: z34.string().trim().min(8).max(160),
34326
+ eventName: z34.string().trim().regex(/^[a-z][a-z0-9_]{1,79}$/),
34327
+ occurredAt: z34.string().datetime(),
34328
+ visitorId: z34.string().trim().max(160).optional(),
34329
+ sessionId: z34.string().trim().max(160).optional(),
34330
+ path: z34.string().trim().max(2e3).optional(),
34331
+ canonicalUrl: z34.string().url().max(3e3).optional(),
34332
+ title: z34.string().trim().max(500).optional(),
34333
+ referrer: z34.string().trim().max(3e3).optional(),
34334
+ source: z34.string().trim().max(180).optional(),
34335
+ medium: z34.string().trim().max(180).optional(),
34336
+ campaign: z34.string().trim().max(240).optional(),
34337
+ clickIds: z34.record(z34.string(), z34.string().trim().max(500)).optional(),
34338
+ deviceClass: z34.enum(["desktop", "tablet", "mobile", "unknown"]).optional(),
34339
+ properties: z34.record(z34.string(), z34.unknown()).optional()
34340
+ }).strict();
34341
+ var IngestionSchema = z34.object({
34342
+ pixelId: z34.string().trim().min(8).max(160),
34343
+ events: z34.array(EventSchema).min(1).max(20)
34344
+ }).strict();
34345
+ var ConversionSchema = z34.object({
34346
+ siteId: z34.string().uuid(),
34347
+ pixelId: z34.string().uuid().optional(),
34348
+ sessionId: z34.string().trim().max(160).optional(),
34349
+ sourceEventId: z34.string().trim().min(8).max(200),
34350
+ conversionKind: z34.string().trim().regex(/^[a-z][a-z0-9_]{1,79}$/),
34351
+ valueMinor: z34.number().int().min(0).max(2147483647).optional(),
34352
+ currency: z34.string().trim().regex(/^[A-Za-z]{3}$/).optional(),
34353
+ occurredAt: z34.string().datetime()
34354
+ }).strict();
34355
+ var ExportSchema = z34.object({
34356
+ report: z34.enum(["overview", "acquisition", "content", "conversions"]).default("overview"),
34357
+ format: z34.enum(["csv", "json", "markdown"]).default("csv"),
34358
+ idempotencyKey: z34.string().trim().min(8).max(160).optional()
34359
+ }).strict();
34360
+ var CampaignLinkSchema = z34.object({
34361
+ pixelId: z34.string().uuid().optional(),
34362
+ name: z34.string().trim().min(1).max(120),
34363
+ destinationUrl: z34.string().url().max(3e3),
34364
+ shortCode: z34.string().trim().min(3).max(48).optional(),
34365
+ source: z34.string().trim().min(1).max(180),
34366
+ medium: z34.string().trim().min(1).max(180),
34367
+ campaign: z34.string().trim().min(1).max(240),
34368
+ term: z34.string().trim().max(240).optional(),
34369
+ content: z34.string().trim().max(240).optional(),
34370
+ adGroup: z34.string().trim().max(240).optional(),
34371
+ adName: z34.string().trim().max(240).optional(),
34372
+ creativeId: z34.string().trim().max(240).optional()
34373
+ }).strict();
34374
+ var FormFieldSchema = z34.object({
34375
+ id: z34.string().trim().regex(/^[a-z][a-z0-9_]{0,47}$/),
34376
+ type: z34.enum([
34377
+ "text",
34378
+ "email",
34379
+ "tel",
34380
+ "textarea",
34381
+ "select",
34382
+ "checkbox",
34383
+ "radio",
34384
+ "number"
34385
+ ]),
34386
+ label: z34.string().trim().min(1).max(120),
34387
+ required: z34.boolean().default(false),
34388
+ options: z34.array(z34.string().trim().min(1).max(120)).max(30).optional()
34389
+ }).strict();
34390
+ var FormInputSchema = z34.object({
34391
+ pixelId: z34.string().uuid(),
34392
+ name: z34.string().trim().min(1).max(120),
34393
+ fields: z34.array(FormFieldSchema).min(1).max(30).refine(
34394
+ (fields) => new Set(fields.map((field) => field.id)).size === fields.length,
34395
+ "Field IDs must be unique."
34396
+ ),
34397
+ brand: z34.object({
34398
+ primaryColor: z34.string().regex(/^#[0-9a-f]{6}$/i),
34399
+ backgroundColor: z34.string().regex(/^#[0-9a-f]{6}$/i),
34400
+ textColor: z34.string().regex(/^#[0-9a-f]{6}$/i),
34401
+ radius: z34.number().int().min(0).max(32)
34402
+ }).strict(),
34403
+ submitLabel: z34.string().trim().min(1).max(80).optional(),
34404
+ successMessage: z34.string().trim().min(1).max(500).optional(),
34405
+ redirectUrl: z34.string().url().max(3e3).optional(),
34406
+ consentText: z34.string().trim().max(1e3).optional(),
34407
+ publish: z34.boolean().optional()
34408
+ }).strict();
34409
+ var FormSubmissionSchema = z34.object({
34410
+ data: z34.record(
34411
+ z34.string(),
34412
+ z34.union([z34.string().max(5e3), z34.number(), z34.boolean()])
34413
+ ).refine((value) => Object.keys(value).length <= 30),
34414
+ visitorId: z34.string().trim().max(160).optional(),
34415
+ sessionId: z34.string().trim().max(160).optional(),
34416
+ source: z34.string().trim().max(180).optional(),
34417
+ medium: z34.string().trim().max(180).optional(),
34418
+ campaign: z34.string().trim().max(240).optional(),
34419
+ clickIds: z34.record(z34.string(), z34.string().trim().max(500)).optional(),
34420
+ placementUrl: z34.string().url().max(3e3),
34421
+ website: z34.string().max(0).optional()
34422
+ }).strict();
34423
+ var CrmImportSchema = z34.object({
34424
+ sourceSystem: z34.enum([
34425
+ "hubspot",
34426
+ "salesforce",
34427
+ "gohighlevel",
34428
+ "zoho",
34429
+ "pipedrive",
34430
+ "keap",
34431
+ "other"
34432
+ ]),
34433
+ filename: z34.string().trim().min(1).max(240),
34434
+ csv: z34.string().min(1).max(8e6),
34435
+ mapping: z34.object({
34436
+ email: z34.string().trim().min(1).max(240).optional(),
34437
+ firstName: z34.string().trim().min(1).max(240).optional(),
34438
+ lastName: z34.string().trim().min(1).max(240).optional(),
34439
+ name: z34.string().trim().min(1).max(240).optional(),
34440
+ phone: z34.string().trim().min(1).max(240).optional(),
34441
+ company: z34.string().trim().min(1).max(240).optional(),
34442
+ externalId: z34.string().trim().min(1).max(240).optional()
34443
+ }).strict()
34444
+ }).strict();
34445
+ var ActivationDestinationSchema = z34.object({
34446
+ platform: z34.enum(["meta", "google", "tiktok", "reddit"]),
34447
+ name: z34.string().trim().min(1).max(120),
34448
+ connectionRef: z34.string().trim().max(240).optional(),
34449
+ externalDatasetId: z34.string().trim().max(240).optional(),
34450
+ eventMapping: z34.record(z34.string(), z34.string().trim().max(120)).optional()
34451
+ }).strict();
34452
+ var BusinessModelSchema = z34.object({ businessModel: z34.enum(["leadgen", "saas", "ecommerce"]) }).strict();
34453
+ var AdSpendSchema = z34.object({
34454
+ rows: z34.array(
34455
+ z34.object({
34456
+ date: z34.string().date(),
34457
+ platform: z34.string().trim().min(1).max(80),
34458
+ campaign: z34.string().trim().max(240).optional(),
34459
+ spendMinor: z34.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
34460
+ currency: z34.string().regex(/^[A-Za-z]{3}$/).optional(),
34461
+ sourceRef: z34.string().max(500).optional()
34462
+ }).strict()
34463
+ ).min(1).max(5e3)
34464
+ }).strict();
34465
+ function safeOrigin(origin) {
34466
+ if (!origin || origin === "null") return null;
34467
+ try {
34468
+ const url = new URL(origin);
34469
+ return url.protocol === "http:" || url.protocol === "https:" ? url.origin : null;
34470
+ } catch {
34471
+ return null;
34472
+ }
34473
+ }
34474
+ function dashboardOriginAllowed(origin) {
34475
+ if (origin === "https://mcpscraper.dev" || origin === "https://www.mcpscraper.dev")
34476
+ return true;
34477
+ if (process.env.NODE_ENV !== "production" && /^http:\/\/(localhost|127\.0\.0\.1):\d+$/.test(origin))
34478
+ return true;
34479
+ return (process.env.ALLOWED_ORIGINS ?? process.env.APP_ORIGIN ?? "").split(",").map((value) => value.trim()).includes(origin);
34480
+ }
34481
+ analyticsApp.use("*", async (c, next) => {
34482
+ const origin = safeOrigin(c.req.header("origin"));
34483
+ const publicIngestion = c.req.path === "/v1/events";
34484
+ if (origin && (publicIngestion || dashboardOriginAllowed(origin))) {
34485
+ c.header("Access-Control-Allow-Origin", origin);
34486
+ c.header("Vary", "Origin");
34487
+ c.header(
34488
+ "Access-Control-Allow-Methods",
34489
+ "GET, POST, PATCH, DELETE, OPTIONS"
34490
+ );
34491
+ c.header("Access-Control-Allow-Headers", "content-type, x-api-key");
34492
+ c.header("Access-Control-Max-Age", "600");
34493
+ }
34494
+ if (c.req.method === "OPTIONS") return c.body(null, 204);
34495
+ await next();
34496
+ });
34497
+ function analyticsPixelSdk() {
34498
+ 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)})();`;
34499
+ }
34500
+ function reportFilters(url) {
34501
+ const end = url.searchParams.get("end") || (/* @__PURE__ */ new Date()).toISOString();
34502
+ const defaultStart = new Date(
34503
+ Date.parse(end) - 30 * 24 * 60 * 60 * 1e3
34504
+ ).toISOString();
34505
+ const list = (name) => url.searchParams.getAll(name).flatMap((value) => value.split(",")).map((value) => value.trim()).filter(Boolean);
34506
+ return {
34507
+ start: url.searchParams.get("start") || defaultStart,
34508
+ end,
34509
+ timezone: validTimezone(url.searchParams.get("timezone")),
34510
+ pixelIds: list("pixelId"),
34511
+ hostnames: list("hostname"),
34512
+ source: url.searchParams.get("source") || void 0,
34513
+ medium: url.searchParams.get("medium") || void 0,
34514
+ campaign: url.searchParams.get("campaign") || void 0,
34515
+ eventName: url.searchParams.get("eventName") || void 0,
34516
+ attributionModel: url.searchParams.get("attributionModel") === "last_touch" ? "last_touch" : "first_touch",
34517
+ channelFamily: url.searchParams.get("channelFamily") || void 0,
34518
+ platform: url.searchParams.get("platform") || void 0,
34519
+ referrer: url.searchParams.get("referrer") || void 0,
34520
+ path: url.searchParams.get("path") || void 0,
34521
+ deviceClass: url.searchParams.get("deviceClass") || void 0,
34522
+ countryCode: url.searchParams.get("countryCode") || void 0,
34523
+ regionCode: url.searchParams.get("regionCode") || void 0,
34524
+ conversionKind: url.searchParams.get("conversionKind") || void 0
34525
+ };
34526
+ }
34527
+ function validTimezone(value) {
34528
+ if (!value) return "UTC";
34529
+ try {
34530
+ new Intl.DateTimeFormat("en-US", { timeZone: value }).format(/* @__PURE__ */ new Date());
34531
+ return value;
34532
+ } catch {
34533
+ throw new AnalyticsRepositoryError(
34534
+ "invalid_timezone",
34535
+ "Choose a valid IANA timezone."
34536
+ );
34537
+ }
34538
+ }
34539
+ function validateReportWindow(filters) {
34540
+ const start = Date.parse(filters.start);
34541
+ const end = Date.parse(filters.end);
34542
+ if (!Number.isFinite(start) || !Number.isFinite(end) || start >= end) {
34543
+ throw new AnalyticsRepositoryError(
34544
+ "invalid_date_range",
34545
+ "Choose a valid date range."
34546
+ );
34547
+ }
34548
+ if (end - start > 366 * 24 * 60 * 60 * 1e3) {
34549
+ throw new AnalyticsRepositoryError(
34550
+ "date_range_too_large",
34551
+ "Date ranges are limited to 366 days."
34552
+ );
34553
+ }
34554
+ }
34555
+ function reportPage(url) {
34556
+ const rawLimit = Number(url.searchParams.get("limit") || 25);
34557
+ return {
34558
+ limit: Number.isFinite(rawLimit) ? Math.min(100, Math.max(1, Math.trunc(rawLimit))) : 25,
34559
+ cursor: url.searchParams.get("cursor") || void 0
34560
+ };
34561
+ }
34562
+ function pixelSnippet(publicId) {
34563
+ return `<script async src="https://mcpscraper.dev/analytics/v1/pixel.js" data-pixel="${publicId}" data-consent="required"></script>`;
34564
+ }
34565
+ function formSnippet(publicId) {
34566
+ return `<div data-mcp-form="${publicId}"></div><script async src="https://mcpscraper.dev/analytics/v1/form.js?form=${publicId}"></script>`;
34567
+ }
34568
+ function escapeHtml6(value) {
34569
+ return String(value ?? "").replace(
34570
+ /[&<>"']/g,
34571
+ (character) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[character]
34572
+ );
34573
+ }
34574
+ function renderPublicForm(form, placementUrl) {
34575
+ if (!form)
34576
+ return "<!doctype html><title>Form unavailable</title><p>Form unavailable.</p>";
34577
+ const fields = form.fields.map((field) => {
34578
+ const required = field.required ? " required" : "";
34579
+ const options = (field.options ?? []).map(
34580
+ (option) => `<option value="${escapeHtml6(option)}">${escapeHtml6(option)}</option>`
34581
+ ).join("");
34582
+ 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}>`;
34583
+ return `<label${field.type === "checkbox" ? ' class="check"' : ""}><span>${escapeHtml6(field.label)}${field.required ? " *" : ""}</span>${control}</label>`;
34584
+ }).join("");
34585
+ const brand = form.brand;
34586
+ 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>`;
34587
+ }
34588
+ analyticsApp.get(
34589
+ "/health/live",
34590
+ (c) => c.json({ status: "live", checkedAt: (/* @__PURE__ */ new Date()).toISOString() }, 200, {
34591
+ "Cache-Control": "no-store"
34592
+ })
34593
+ );
34594
+ analyticsApp.get("/health/ready", async (c) => {
34595
+ const startedAt = Date.now();
34596
+ try {
34597
+ await getAnalyticsPool().query("SELECT 1");
34598
+ return c.json(
34599
+ {
34600
+ status: "ready",
34601
+ database: true,
34602
+ latencyMs: Date.now() - startedAt,
34603
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString()
34604
+ },
34605
+ 200,
34606
+ { "Cache-Control": "no-store" }
34607
+ );
34608
+ } catch (error) {
34609
+ return c.json(
34610
+ {
34611
+ status: "not_ready",
34612
+ database: false,
34613
+ error: error instanceof Error ? error.message : String(error),
34614
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString()
34615
+ },
34616
+ 503,
34617
+ { "Cache-Control": "no-store" }
34618
+ );
34619
+ }
34620
+ });
34621
+ analyticsApp.get("/v1/pixel.js", (c) => {
34622
+ c.header("Content-Type", "application/javascript; charset=utf-8");
34623
+ c.header("Cache-Control", "public, max-age=300");
34624
+ return c.body(analyticsPixelSdk());
34625
+ });
34626
+ analyticsApp.get("/v1/form.js", async (c) => {
34627
+ const form = await getPublicAnalyticsForm(c.req.query("form") || "");
34628
+ if (!form)
34629
+ return c.body("/* Form unavailable. */", 404, {
34630
+ "Content-Type": "application/javascript; charset=utf-8"
34631
+ });
34632
+ if (!await analyticsOwnerHasCurrentEntitlement(Number(form.owner_user_id)))
34633
+ return c.body("/* Thorbit X-Ray Pixel subscription required. */", 402, {
34634
+ "Content-Type": "application/javascript; charset=utf-8",
34635
+ "Cache-Control": "no-store"
34636
+ });
34637
+ 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)})();`;
34638
+ return c.body(script, 200, {
34639
+ "Content-Type": "application/javascript; charset=utf-8",
34640
+ "Cache-Control": "public, max-age=300"
34641
+ });
34642
+ });
34643
+ analyticsApp.get("/forms/:publicId/embed", async (c) => {
34644
+ const form = await getPublicAnalyticsForm(c.req.param("publicId"));
34645
+ if (!form)
34646
+ return c.html(
34647
+ "<!doctype html><title>Form unavailable</title><p>Form unavailable.</p>",
34648
+ 404
34649
+ );
34650
+ if (!await analyticsOwnerHasCurrentEntitlement(Number(form.owner_user_id)))
34651
+ return c.html(
34652
+ '<!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.',
34653
+ 402
34654
+ );
34655
+ const placementUrl = c.req.query("placement") || "";
34656
+ if (!await isAnalyticsFormPlacementApproved(form.pixel_id, placementUrl))
34657
+ return c.html(
34658
+ "<!doctype html><title>Form unavailable</title><p>This form is not approved for this hostname.</p>",
34659
+ 403
34660
+ );
34661
+ c.header(
34662
+ "Content-Security-Policy",
34663
+ "default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; connect-src 'self'; form-action 'none'; base-uri 'none'"
34664
+ );
34665
+ c.header("Cache-Control", "no-store");
34666
+ return c.html(renderPublicForm(form, placementUrl));
34667
+ });
34668
+ analyticsApp.post("/forms/:publicId/submissions", async (c) => {
34669
+ const parsed = FormSubmissionSchema.safeParse(
34670
+ await c.req.json().catch(() => null)
34671
+ );
34672
+ if (!parsed.success)
34673
+ return c.json(
34674
+ {
34675
+ ok: false,
34676
+ error: "Invalid form submission.",
34677
+ issues: parsed.error.issues
34678
+ },
34679
+ 400
34680
+ );
34681
+ const form = await getPublicAnalyticsForm(c.req.param("publicId"));
34682
+ if (!form) return c.json({ ok: false, error: "Form unavailable." }, 404);
34683
+ if (!await analyticsOwnerHasCurrentEntitlement(Number(form.owner_user_id)))
34684
+ return c.json(
34685
+ { ok: false, error: "This Thorbit X-Ray Pixel form is not currently active." },
34686
+ 402
34687
+ );
34688
+ if (!await isAnalyticsFormPlacementApproved(
34689
+ form.pixel_id,
34690
+ parsed.data.placementUrl
34691
+ ))
34692
+ return c.json({ ok: false, error: "Form hostname is not approved." }, 403);
34693
+ const allowed = new Map(
34694
+ form.fields.map((field) => [field.id, field])
34695
+ );
34696
+ for (const key of Object.keys(parsed.data.data))
34697
+ if (!allowed.has(key))
34698
+ return c.json(
34699
+ { ok: false, error: "Submission contains an unknown field." },
34700
+ 400
34701
+ );
34702
+ for (const field of allowed.values())
34703
+ if (field.required && (parsed.data.data[field.id] === void 0 || parsed.data.data[field.id] === ""))
34704
+ return c.json({ ok: false, error: `${field.label} is required.` }, 400);
34705
+ const user = await getUserById(form.owner_user_id);
34706
+ if (!user)
34707
+ return c.json({ ok: false, error: "Form owner unavailable." }, 503);
34708
+ const data = parsed.data.data;
34709
+ const email = typeof data.email === "string" ? data.email.trim().toLowerCase() : "";
34710
+ const firstName = typeof data.first_name === "string" ? data.first_name.trim() : "";
34711
+ const lastName = typeof data.last_name === "string" ? data.last_name.trim() : "";
34712
+ const fullName = [firstName, lastName].filter(Boolean).join(" ") || (typeof data.name === "string" ? data.name.trim() : "") || email || "Website lead";
34713
+ const identitySeed = email || `${fullName}:${Date.now()}`;
34714
+ const suffix2 = createHash16("sha256").update(identitySeed).digest("hex").slice(0, 12);
34715
+ const path5 = `Leads/person-${suffix2}`;
34716
+ const { key: memoryKey, error: memoryKeyError } = await getOrCreateUserMemoryKey(user);
34717
+ const existing = memoryKey ? await memoryCall("getTool", { vault: "People", path: path5 }, memoryKey) : { ok: false, error: memoryKeyError || "memory_credential_unavailable" };
34718
+ const props = {
34719
+ ...existing.note?.props ?? {},
34720
+ type: "person",
34721
+ status: "active",
34722
+ person_name: fullName,
34723
+ ...firstName ? { first_name: firstName } : {},
34724
+ ...lastName ? { last_name: lastName } : {},
34725
+ ...email ? { email } : {},
34726
+ ...typeof data.phone === "string" && data.phone.trim() ? { phone: data.phone.trim(), text_phone: data.phone.trim() } : {},
34727
+ ...typeof data.company === "string" && data.company.trim() ? { company: data.company.trim() } : {},
34728
+ attribution_site_id: form.site_id,
34729
+ attribution_form_id: form.id,
34730
+ attribution_form_name: form.name,
34731
+ attributed_source: parsed.data.source || "(direct)",
34732
+ attributed_medium: parsed.data.medium || "(none)",
34733
+ attributed_campaign: parsed.data.campaign || "(none)",
34734
+ form_fields: data,
34735
+ source_type: "analytics_form",
34736
+ source_ref: `form:${form.public_id}`,
34737
+ summary: `Website lead from ${form.name}`,
34738
+ embedding_summary: `${fullName} submitted ${form.name}`,
34739
+ related: [],
34740
+ related_vault_notes: [],
34741
+ embed: true,
34742
+ embed_priority: "normal"
34743
+ };
34744
+ const crmPayload = {
34745
+ vault: "People",
34746
+ path: path5,
34747
+ title: fullName,
34748
+ content: existing.note?.content || `# ${fullName}
34749
+
34750
+ ## Identity
34751
+ Website lead captured through ${form.name}.
34752
+
34753
+ ## Relationship
34754
+ Attribution is linked to Site Analytics.
34755
+
34756
+ ## Known Context
34757
+ Submitted the published form.
34758
+
34759
+ ## Interaction History
34760
+ - ${(/* @__PURE__ */ new Date()).toISOString()}: Form submitted.
34761
+
34762
+ ## Open Loops
34763
+ - Review and follow up.`,
34764
+ props,
34765
+ ...existing.note ? { baseRevision: existing.note.revision } : {}
34766
+ };
34767
+ const saved = memoryKey ? await memoryCall(
34768
+ "putTool",
34769
+ crmPayload,
34770
+ memoryKey
34771
+ ) : { ok: false, error: memoryKeyError || "memory_credential_unavailable" };
34772
+ const crmDeliveryStatus = saved.ok ? "delivered" : "pending";
34773
+ const receipt = await recordAnalyticsFormSubmission({
34774
+ form,
34775
+ visitorId: parsed.data.visitorId,
34776
+ sessionId: parsed.data.sessionId,
34777
+ crmPersonRef: `People::${path5}`,
34778
+ crmDeliveryStatus,
34779
+ source: parsed.data.source,
34780
+ medium: parsed.data.medium,
34781
+ campaign: parsed.data.campaign,
34782
+ clickIds: parsed.data.clickIds
34783
+ });
34784
+ const phone = typeof data.phone === "string" ? data.phone.trim() : "";
34785
+ const identityGraph = await linkAnalyticsFormIdentity({
34786
+ submissionId: receipt.submissionId,
34787
+ siteId: form.site_id,
34788
+ crmPersonRef: `People::${path5}`,
34789
+ visitorId: parsed.data.visitorId,
34790
+ sessionId: parsed.data.sessionId,
34791
+ email: email || void 0,
34792
+ phone: phone || void 0,
34793
+ clickIds: parsed.data.clickIds
34794
+ });
34795
+ if (!saved.ok) {
34796
+ await queueAnalyticsFormDelivery({
34797
+ submissionId: receipt.submissionId,
34798
+ ownerUserId: form.owner_user_id,
34799
+ payloadCiphertext: encryptMemoryKey(JSON.stringify(crmPayload)),
34800
+ errorCode: memoryKey ? "memory_write_failed" : "memory_credential_unavailable"
34801
+ });
34802
+ }
34803
+ const conversion = await createAnalyticsConversion({
34804
+ userId: form.owner_user_id,
34805
+ siteId: form.site_id,
34806
+ pixelId: form.pixel_id,
34807
+ sessionId: parsed.data.sessionId,
34808
+ sourceEventId: `form:${receipt.submissionId}`,
34809
+ conversionKind: "form_submitted",
34810
+ occurredAt: (/* @__PURE__ */ new Date()).toISOString()
34811
+ });
34812
+ const match = {
34813
+ ...email ? { emailSha256: createHash16("sha256").update(email).digest("hex") } : {},
34814
+ ...phone ? {
34815
+ phoneSha256: createHash16("sha256").update(phone.replace(/\D/g, "")).digest("hex")
34816
+ } : {}
34817
+ };
34818
+ const activationQueued = await queueAnalyticsActivation({
34819
+ siteId: form.site_id,
34820
+ conversionId: conversion.id,
34821
+ personId: identityGraph.personId,
34822
+ payloadCiphertext: encryptMemoryKey(
34823
+ JSON.stringify({
34824
+ eventId: conversion.id,
34825
+ eventName: "form_submitted",
34826
+ eventTime: (/* @__PURE__ */ new Date()).toISOString(),
34827
+ actionSource: "website",
34828
+ sourceUrl: parsed.data.placementUrl,
34829
+ match,
34830
+ clickIds: parsed.data.clickIds ?? {},
34831
+ attribution: {
34832
+ source: parsed.data.source,
34833
+ medium: parsed.data.medium,
34834
+ campaign: parsed.data.campaign
34835
+ }
34836
+ })
34837
+ )
34838
+ });
34839
+ return c.json(
34840
+ {
34841
+ ok: true,
34842
+ submissionId: receipt.submissionId,
34843
+ crmDeliveryStatus,
34844
+ attribution: {
34845
+ personId: identityGraph.personId,
34846
+ linkedSignals: identityGraph.linkedSignals
34847
+ },
34848
+ activation: { queuedDestinations: activationQueued },
34849
+ message: form.success_message,
34850
+ redirectUrl: form.redirect_url
34851
+ },
34852
+ saved.ok ? 201 : 202
34853
+ );
34854
+ });
34855
+ analyticsApp.get("/l/:shortCode", async (c) => {
34856
+ const destination = await resolveAnalyticsCampaignLink(
34857
+ c.req.param("shortCode"),
34858
+ c.req.header("referer")
34859
+ );
34860
+ if (!destination)
34861
+ return c.json(
34862
+ {
34863
+ ok: false,
34864
+ code: "campaign_link_not_found",
34865
+ error: "Campaign link not found."
34866
+ },
34867
+ 404
34868
+ );
34869
+ c.header("Cache-Control", "no-store");
34870
+ return c.redirect(destination, 302);
34871
+ });
34872
+ analyticsApp.post("/v1/events", async (c) => {
34873
+ const contentLength = Number(c.req.header("content-length") || 0);
34874
+ if (contentLength > 32 * 1024)
34875
+ return c.json(
34876
+ { accepted: 0, rejected: 1, reasonCodes: ["payload_too_large"] },
34877
+ 413
34878
+ );
34879
+ const parsed = IngestionSchema.safeParse(
34880
+ await c.req.json().catch(() => null)
34881
+ );
34882
+ if (!parsed.success) {
34883
+ return c.json(
34884
+ {
34885
+ accepted: 0,
34886
+ rejected: 1,
34887
+ reasonCodes: ["invalid_payload"],
34888
+ issues: parsed.error.issues
34889
+ },
34890
+ 400
34891
+ );
34892
+ }
34893
+ const result = await ingestAnalyticsEvents({
34894
+ publicPixelId: parsed.data.pixelId,
34895
+ origin: c.req.header("origin") ?? null,
34896
+ events: parsed.data.events,
34897
+ geography: {
34898
+ countryCode: c.req.header("x-vercel-ip-country") || c.req.header("cf-ipcountry") || null,
34899
+ regionCode: c.req.header("x-vercel-ip-country-region") || null
34900
+ }
34901
+ });
34902
+ return c.json(result, 202);
34903
+ });
34904
+ analyticsApp.get("/entitlement", auth3, async (c) => {
34905
+ c.header("Cache-Control", "private, no-store");
34906
+ return c.json({
34907
+ entitlement: await resolveAnalyticsEntitlement(Number(c.get("user").id))
34908
+ });
34909
+ });
34910
+ analyticsApp.post("/entitlement/connect", auth3, async (c) => {
34911
+ c.header("Cache-Control", "private, no-store");
34912
+ try {
34913
+ return c.json({
34914
+ connectUrl: await createThorbitConnectUrl(Number(c.get("user").id))
34915
+ });
34916
+ } catch (error) {
34917
+ return c.json(
34918
+ {
34919
+ error: error instanceof Error ? error.message : "Could not start Thorbit sign-in."
34920
+ },
34921
+ 503
34922
+ );
34923
+ }
34924
+ });
34925
+ analyticsApp.post("/entitlement/api-key", auth3, async (c) => {
34926
+ c.header("Cache-Control", "private, no-store");
34927
+ const parsed = ThorbitApiKeySchema.safeParse(
34928
+ await c.req.json().catch(() => null)
34929
+ );
34930
+ if (!parsed.success)
34931
+ return c.json({ error: "Enter a valid Thorbit API key." }, 400);
34932
+ try {
34933
+ return c.json({
34934
+ entitlement: await connectAnalyticsWithThorbitApiKey(
34935
+ Number(c.get("user").id),
34936
+ parsed.data.apiKey
34937
+ )
34938
+ });
34939
+ } catch (error) {
34940
+ return c.json(
34941
+ {
34942
+ error: error instanceof Error ? error.message : "Thorbit could not verify this API key."
34943
+ },
34944
+ 401
34945
+ );
34946
+ }
34947
+ });
34948
+ analyticsApp.post("/entitlement/callback", auth3, async (c) => {
34949
+ c.header("Cache-Control", "private, no-store");
34950
+ const parsed = ThorbitCallbackSchema.safeParse(
34951
+ await c.req.json().catch(() => null)
34952
+ );
34953
+ if (!parsed.success)
34954
+ return c.json({ error: "Invalid Thorbit connection response." }, 400);
34955
+ try {
34956
+ return c.json({
34957
+ entitlement: await consumeThorbitConnectCallback({
34958
+ userId: Number(c.get("user").id),
34959
+ state: parsed.data.state,
34960
+ assertion: parsed.data.assertion
34961
+ })
34962
+ });
34963
+ } catch (error) {
34964
+ return c.json(
34965
+ {
34966
+ error: error instanceof Error ? error.message : "Could not connect the Thorbit account."
34967
+ },
34968
+ 400
34969
+ );
34970
+ }
34971
+ });
34972
+ analyticsApp.delete("/entitlement", auth3, async (c) => {
34973
+ await disconnectAnalyticsEntitlement(Number(c.get("user").id));
34974
+ return c.json({ ok: true });
34975
+ });
34976
+ analyticsApp.use("/v1/conversions", auth3, entitlementGuard);
34977
+ analyticsApp.use("/sites", auth3, entitlementGuard);
34978
+ analyticsApp.use("/sites/*", auth3, entitlementGuard);
34979
+ analyticsApp.post("/v1/conversions", auth3, async (c) => {
34980
+ const parsed = ConversionSchema.safeParse(
34981
+ await c.req.json().catch(() => null)
34982
+ );
34983
+ if (!parsed.success)
34984
+ return c.json(
34985
+ { error: "Invalid conversion.", issues: parsed.error.issues },
34986
+ 400
34987
+ );
34988
+ const conversion = await createAnalyticsConversion({
34989
+ userId: Number(c.get("user").id),
34990
+ ...parsed.data
34991
+ });
34992
+ return c.json({ ok: true, conversion }, conversion.created ? 201 : 200);
34993
+ });
34994
+ analyticsApp.get("/sites", auth3, async (c) => {
34995
+ const sites = await listAnalyticsSites(Number(c.get("user").id));
34996
+ return c.json({ ok: true, sites });
34997
+ });
34998
+ analyticsApp.post("/sites", auth3, async (c) => {
34999
+ const parsed = SiteInputSchema.safeParse(
35000
+ await c.req.json().catch(() => null)
35001
+ );
35002
+ if (!parsed.success)
35003
+ return c.json(
35004
+ { error: "Invalid Site details.", issues: parsed.error.issues },
35005
+ 400
35006
+ );
35007
+ const site = await createAnalyticsSite({
35008
+ userId: Number(c.get("user").id),
35009
+ ...parsed.data
35010
+ });
35011
+ return c.json({ ok: true, site }, 201);
35012
+ });
35013
+ analyticsApp.delete("/sites/:siteId", auth3, async (c) => {
35014
+ await deleteAnalyticsSite({
35015
+ siteId: c.req.param("siteId"),
35016
+ userId: Number(c.get("user").id)
35017
+ });
35018
+ return c.json({ ok: true });
35019
+ });
35020
+ analyticsApp.patch("/sites/:siteId/business-model", auth3, async (c) => {
35021
+ const parsed = BusinessModelSchema.safeParse(
35022
+ await c.req.json().catch(() => null)
35023
+ );
35024
+ if (!parsed.success)
35025
+ return c.json(
35026
+ { ok: false, error: "Choose a supported business model." },
35027
+ 400
35028
+ );
35029
+ await updateAnalyticsBusinessModel({
35030
+ siteId: c.req.param("siteId"),
35031
+ userId: Number(c.get("user").id),
35032
+ ...parsed.data
35033
+ });
35034
+ return c.json({ ok: true, businessModel: parsed.data.businessModel });
35035
+ });
35036
+ analyticsApp.post("/sites/:siteId/ad-spend", auth3, async (c) => {
35037
+ const parsed = AdSpendSchema.safeParse(await c.req.json().catch(() => null));
35038
+ if (!parsed.success)
35039
+ return c.json(
35040
+ {
35041
+ ok: false,
35042
+ error: "Invalid ad-spend rows.",
35043
+ issues: parsed.error.issues
35044
+ },
35045
+ 400
35046
+ );
35047
+ const result = await upsertAnalyticsAdSpend({
35048
+ siteId: c.req.param("siteId"),
35049
+ userId: Number(c.get("user").id),
35050
+ ...parsed.data
35051
+ });
35052
+ return c.json({ ok: true, result }, 202);
35053
+ });
35054
+ analyticsApp.get("/sites/:siteId/business-metrics", auth3, async (c) => {
35055
+ const url = new URL(c.req.url);
35056
+ const filters = reportFilters(url);
35057
+ validateReportWindow(filters);
35058
+ const data = await analyticsBusinessMetrics(
35059
+ c.req.param("siteId"),
35060
+ Number(c.get("user").id),
35061
+ filters.start,
35062
+ filters.end
35063
+ );
35064
+ return c.json({ ok: true, data, filters });
35065
+ });
35066
+ analyticsApp.get("/sites/:siteId/forecast", auth3, async (c) => {
35067
+ const horizon = Math.min(
35068
+ Math.max(Number(c.req.query("horizonMonths") || 12), 1),
35069
+ 36
35070
+ );
35071
+ const data = await analyticsForecast(
35072
+ c.req.param("siteId"),
35073
+ Number(c.get("user").id),
35074
+ horizon
35075
+ );
35076
+ return c.json({ ok: true, data });
35077
+ });
35078
+ analyticsApp.get("/sites/:siteId/pixels", auth3, async (c) => {
35079
+ const pixels = await listAnalyticsPixels(
35080
+ c.req.param("siteId"),
35081
+ Number(c.get("user").id)
35082
+ );
35083
+ return c.json({
35084
+ ok: true,
35085
+ pixels: pixels.map((pixel) => ({
35086
+ ...pixel,
35087
+ snippet: pixelSnippet(pixel.public_id)
35088
+ }))
35089
+ });
35090
+ });
35091
+ analyticsApp.post("/sites/:siteId/pixels", auth3, async (c) => {
35092
+ const parsed = PixelInputSchema.safeParse(
35093
+ await c.req.json().catch(() => null)
35094
+ );
35095
+ if (!parsed.success)
35096
+ return c.json(
35097
+ { error: "Invalid Pixel details.", issues: parsed.error.issues },
35098
+ 400
35099
+ );
35100
+ const pixel = await createAnalyticsPixel({
35101
+ siteId: c.req.param("siteId"),
35102
+ userId: Number(c.get("user").id),
35103
+ ...parsed.data
35104
+ });
35105
+ return c.json(
35106
+ { ok: true, pixel: { ...pixel, snippet: pixelSnippet(pixel.public_id) } },
35107
+ 201
35108
+ );
35109
+ });
35110
+ analyticsApp.get("/sites/:siteId/campaign-links", auth3, async (c) => {
35111
+ const url = new URL(c.req.url);
35112
+ const data = await listAnalyticsCampaignLinks(
35113
+ c.req.param("siteId"),
35114
+ Number(c.get("user").id),
35115
+ reportPage(url)
35116
+ );
35117
+ return c.json({
35118
+ ok: true,
35119
+ data: {
35120
+ ...data,
35121
+ links: data.links.map((link) => ({
35122
+ ...link,
35123
+ short_url: `${url.origin}/analytics/l/${link.short_code}`
35124
+ }))
35125
+ }
35126
+ });
35127
+ });
35128
+ analyticsApp.get("/sites/:siteId/forms", auth3, async (c) => {
35129
+ const url = new URL(c.req.url);
35130
+ const data = await listAnalyticsForms(
35131
+ c.req.param("siteId"),
35132
+ Number(c.get("user").id),
35133
+ reportPage(url)
35134
+ );
35135
+ return c.json({
35136
+ ok: true,
35137
+ data: {
35138
+ ...data,
35139
+ forms: data.forms.map((form) => ({
35140
+ ...form,
35141
+ snippet: formSnippet(form.public_id),
35142
+ embed_url: `${url.origin}/analytics/forms/${form.public_id}/embed`
35143
+ }))
35144
+ }
35145
+ });
35146
+ });
35147
+ analyticsApp.post("/sites/:siteId/forms", auth3, async (c) => {
35148
+ const parsed = FormInputSchema.safeParse(
35149
+ await c.req.json().catch(() => null)
35150
+ );
35151
+ if (!parsed.success)
35152
+ return c.json(
35153
+ { ok: false, error: "Invalid form.", issues: parsed.error.issues },
35154
+ 400
35155
+ );
35156
+ const form = await createAnalyticsForm({
35157
+ siteId: c.req.param("siteId"),
35158
+ userId: Number(c.get("user").id),
35159
+ ...parsed.data
35160
+ });
35161
+ return c.json(
35162
+ {
35163
+ ok: true,
35164
+ form: {
35165
+ ...form,
35166
+ snippet: formSnippet(form.public_id),
35167
+ embed_url: `${new URL(c.req.url).origin}/analytics/forms/${form.public_id}/embed`
35168
+ }
35169
+ },
35170
+ 201
35171
+ );
35172
+ });
35173
+ analyticsApp.get("/sites/:siteId/crm-imports", auth3, async (c) => {
35174
+ const data = await listAnalyticsCrmImports(
35175
+ c.req.param("siteId"),
35176
+ Number(c.get("user").id),
35177
+ reportPage(new URL(c.req.url))
35178
+ );
35179
+ return c.json({ ok: true, data });
35180
+ });
35181
+ analyticsApp.get("/sites/:siteId/people", auth3, async (c) => {
35182
+ const data = await listAnalyticsPeople(
35183
+ c.req.param("siteId"),
35184
+ Number(c.get("user").id),
35185
+ reportPage(new URL(c.req.url))
35186
+ );
35187
+ return c.json({ ok: true, data });
35188
+ });
35189
+ analyticsApp.get("/sites/:siteId/people/:personId/journey", auth3, async (c) => {
35190
+ const data = await getAnalyticsPersonJourney(
35191
+ c.req.param("siteId"),
35192
+ c.req.param("personId"),
35193
+ Number(c.get("user").id),
35194
+ reportPage(new URL(c.req.url))
35195
+ );
35196
+ return c.json({ ok: true, data });
35197
+ });
35198
+ analyticsApp.post("/sites/:siteId/crm-imports", auth3, async (c) => {
35199
+ const parsed = CrmImportSchema.safeParse(
35200
+ await c.req.json().catch(() => null)
35201
+ );
35202
+ if (!parsed.success)
35203
+ return c.json(
35204
+ { ok: false, error: "Invalid CRM import.", issues: parsed.error.issues },
35205
+ 400
35206
+ );
35207
+ const csv = Papa2.parse(parsed.data.csv, {
35208
+ header: true,
35209
+ skipEmptyLines: "greedy",
35210
+ transformHeader: (header) => header.trim()
35211
+ });
35212
+ if (csv.errors.length)
35213
+ return c.json(
35214
+ {
35215
+ ok: false,
35216
+ error: "CSV could not be parsed.",
35217
+ issues: csv.errors.slice(0, 10)
35218
+ },
35219
+ 400
35220
+ );
35221
+ if (csv.data.length > 2500)
35222
+ return c.json(
35223
+ { ok: false, error: "Import files are limited to 2,500 rows per batch." },
35224
+ 400
35225
+ );
35226
+ const pick = (row, header) => header ? String(row[header] ?? "").trim() : "";
35227
+ const queued = [];
35228
+ const identities = [];
35229
+ let rejectedCount = 0;
35230
+ for (const [index, row] of csv.data.entries()) {
35231
+ const email = pick(row, parsed.data.mapping.email).toLowerCase();
35232
+ const firstName = pick(row, parsed.data.mapping.firstName);
35233
+ const lastName = pick(row, parsed.data.mapping.lastName);
35234
+ const explicitName = pick(row, parsed.data.mapping.name);
35235
+ const fullName = explicitName || [firstName, lastName].filter(Boolean).join(" ");
35236
+ const phone = pick(row, parsed.data.mapping.phone);
35237
+ const company = pick(row, parsed.data.mapping.company);
35238
+ const externalId = pick(row, parsed.data.mapping.externalId);
35239
+ if (!email && !phone && !externalId && !fullName) {
35240
+ rejectedCount += 1;
35241
+ continue;
35242
+ }
35243
+ const digest2 = createHash16("sha256").update(
35244
+ `${parsed.data.sourceSystem}:${externalId || email || phone || `${fullName}:${index}`}`
35245
+ ).digest("hex").slice(0, 16);
35246
+ const path5 = `Leads/person-${digest2}`;
35247
+ const crmPersonRef = `People::${path5}`;
35248
+ const props = {
35249
+ type: "person",
35250
+ status: "active",
35251
+ person_name: fullName || email || phone || "Imported contact",
35252
+ ...firstName ? { first_name: firstName } : {},
35253
+ ...lastName ? { last_name: lastName } : {},
35254
+ ...email ? { email } : {},
35255
+ ...phone ? { phone, text_phone: phone } : {},
35256
+ ...company ? { company } : {},
35257
+ ...externalId ? { external_id: externalId } : {},
35258
+ import_fields: row,
35259
+ source_type: "crm_csv_import",
35260
+ source_ref: `${parsed.data.sourceSystem}:${digest2}`,
35261
+ summary: `Imported from ${parsed.data.sourceSystem}`,
35262
+ embedding_summary: `${fullName || "Contact"} imported from ${parsed.data.sourceSystem}`,
35263
+ related: [],
35264
+ related_vault_notes: [],
35265
+ embed: true,
35266
+ embed_priority: "normal"
35267
+ };
35268
+ const title = fullName || email || phone || "Imported contact";
35269
+ const payload = {
35270
+ vault: "People",
35271
+ path: path5,
35272
+ title,
35273
+ content: `# ${title}
35274
+
35275
+ ## Identity
35276
+ Imported from ${parsed.data.sourceSystem}.
35277
+
35278
+ ## Relationship
35279
+ Review and qualify this contact.
35280
+
35281
+ ## Known Context
35282
+ Imported through the Site Analytics CRM workflow.
35283
+
35284
+ ## Interaction History
35285
+ - ${(/* @__PURE__ */ new Date()).toISOString()}: Imported from ${parsed.data.filename}.
35286
+
35287
+ ## Open Loops
35288
+ - Verify attribution and follow-up status.`,
35289
+ props
35290
+ };
35291
+ queued.push({
35292
+ crmPersonRef,
35293
+ payloadCiphertext: encryptMemoryKey(JSON.stringify(payload))
35294
+ });
35295
+ identities.push({
35296
+ crmPersonRef,
35297
+ email: email || void 0,
35298
+ phone: phone || void 0
35299
+ });
35300
+ }
35301
+ const imported = await createAnalyticsCrmImport({
35302
+ siteId: c.req.param("siteId"),
35303
+ userId: Number(c.get("user").id),
35304
+ sourceSystem: parsed.data.sourceSystem,
35305
+ filename: parsed.data.filename,
35306
+ rows: queued,
35307
+ rejectedCount
35308
+ });
35309
+ for (const identity of identities) {
35310
+ await linkAnalyticsFormIdentity({
35311
+ siteId: c.req.param("siteId"),
35312
+ ...identity
35313
+ });
35314
+ }
35315
+ return c.json({ ok: true, import: imported }, 202);
35316
+ });
35317
+ analyticsApp.get("/sites/:siteId/activation-destinations", auth3, async (c) => {
35318
+ const destinations = await listAnalyticsActivationDestinations(
35319
+ c.req.param("siteId"),
35320
+ Number(c.get("user").id)
35321
+ );
35322
+ return c.json({ ok: true, destinations });
35323
+ });
35324
+ analyticsApp.post("/sites/:siteId/activation-destinations", auth3, async (c) => {
35325
+ const parsed = ActivationDestinationSchema.safeParse(
35326
+ await c.req.json().catch(() => null)
35327
+ );
35328
+ if (!parsed.success)
35329
+ return c.json(
35330
+ {
35331
+ ok: false,
35332
+ error: "Invalid activation destination.",
35333
+ issues: parsed.error.issues
35334
+ },
35335
+ 400
35336
+ );
35337
+ const destination = await createAnalyticsActivationDestination({
35338
+ siteId: c.req.param("siteId"),
35339
+ userId: Number(c.get("user").id),
35340
+ ...parsed.data
35341
+ });
35342
+ return c.json({ ok: true, destination }, 201);
35343
+ });
35344
+ analyticsApp.delete(
35345
+ "/sites/:siteId/activation-destinations/:destinationId",
35346
+ auth3,
35347
+ async (c) => {
35348
+ await archiveAnalyticsActivationDestination({
35349
+ siteId: c.req.param("siteId"),
35350
+ destinationId: c.req.param("destinationId"),
35351
+ userId: Number(c.get("user").id)
35352
+ });
35353
+ return c.json({ ok: true });
35354
+ }
35355
+ );
35356
+ analyticsApp.post("/sites/:siteId/campaign-links", auth3, async (c) => {
35357
+ const parsed = CampaignLinkSchema.safeParse(
35358
+ await c.req.json().catch(() => null)
35359
+ );
35360
+ if (!parsed.success)
35361
+ return c.json(
35362
+ {
35363
+ ok: false,
35364
+ error: "Invalid campaign link.",
35365
+ issues: parsed.error.issues
35366
+ },
35367
+ 400
35368
+ );
35369
+ const link = await createAnalyticsCampaignLink({
35370
+ siteId: c.req.param("siteId"),
35371
+ userId: Number(c.get("user").id),
35372
+ ...parsed.data
35373
+ });
35374
+ return c.json(
35375
+ {
35376
+ ok: true,
35377
+ link: {
35378
+ ...link,
35379
+ short_url: `${new URL(c.req.url).origin}/analytics/l/${link.short_code}`
35380
+ }
35381
+ },
35382
+ 201
35383
+ );
35384
+ });
35385
+ analyticsApp.delete(
35386
+ "/sites/:siteId/campaign-links/:linkId",
35387
+ auth3,
35388
+ async (c) => {
35389
+ await archiveAnalyticsCampaignLink({
35390
+ siteId: c.req.param("siteId"),
35391
+ linkId: c.req.param("linkId"),
35392
+ userId: Number(c.get("user").id)
35393
+ });
35394
+ return c.json({ ok: true });
35395
+ }
35396
+ );
35397
+ analyticsApp.patch("/sites/:siteId/pixels/:pixelId", auth3, async (c) => {
35398
+ const parsed = PixelUpdateSchema.safeParse(
35399
+ await c.req.json().catch(() => null)
35400
+ );
35401
+ if (!parsed.success)
35402
+ return c.json(
35403
+ { error: "Invalid Pixel changes.", issues: parsed.error.issues },
35404
+ 400
35405
+ );
35406
+ await updateAnalyticsPixel({
35407
+ siteId: c.req.param("siteId"),
35408
+ pixelId: c.req.param("pixelId"),
35409
+ userId: Number(c.get("user").id),
35410
+ ...parsed.data
35411
+ });
35412
+ return c.json({ ok: true });
35413
+ });
35414
+ analyticsApp.post(
35415
+ "/sites/:siteId/pixels/:pixelId/domains/:hostname/approve",
35416
+ auth3,
35417
+ async (c) => {
35418
+ await setAnalyticsPixelDomainState({
35419
+ siteId: c.req.param("siteId"),
35420
+ pixelId: c.req.param("pixelId"),
35421
+ userId: Number(c.get("user").id),
35422
+ hostname: c.req.param("hostname"),
35423
+ state: "approved"
35424
+ });
35425
+ return c.json({ ok: true });
35426
+ }
35427
+ );
35428
+ analyticsApp.post(
35429
+ "/sites/:siteId/pixels/:pixelId/domains/:hostname/reject",
35430
+ auth3,
35431
+ async (c) => {
35432
+ await setAnalyticsPixelDomainState({
35433
+ siteId: c.req.param("siteId"),
35434
+ pixelId: c.req.param("pixelId"),
35435
+ userId: Number(c.get("user").id),
35436
+ hostname: c.req.param("hostname"),
35437
+ state: "rejected"
35438
+ });
35439
+ return c.json({ ok: true });
35440
+ }
35441
+ );
35442
+ for (const [path5, load] of [
35443
+ ["/sites/:siteId/overview", analyticsOverview],
35444
+ ["/sites/:siteId/timeseries", analyticsTimeseries],
35445
+ ["/sites/:siteId/acquisition", analyticsAcquisition],
35446
+ ["/sites/:siteId/content", analyticsContent],
35447
+ ["/sites/:siteId/events", analyticsEventCounts]
35448
+ ]) {
35449
+ analyticsApp.get(path5, auth3, async (c) => {
35450
+ const url = new URL(c.req.url);
35451
+ const filters = reportFilters(url);
35452
+ validateReportWindow(filters);
35453
+ const paginated = path5.endsWith("/acquisition") || path5.endsWith("/content") || path5.endsWith("/events");
35454
+ const data = await load(
35455
+ c.req.param("siteId"),
35456
+ Number(c.get("user").id),
35457
+ filters,
35458
+ paginated ? reportPage(url) : void 0
35459
+ );
35460
+ return c.json({ ok: true, data, filters });
35461
+ });
35462
+ }
35463
+ analyticsApp.get("/sites/:siteId/dashboard", auth3, async (c) => {
35464
+ const url = new URL(c.req.url);
35465
+ const filters = reportFilters(url);
35466
+ validateReportWindow(filters);
35467
+ const siteId = c.req.param("siteId");
35468
+ const userId = Number(c.get("user").id);
35469
+ const [
35470
+ overview,
35471
+ timeseries,
35472
+ acquisition,
35473
+ devices,
35474
+ activity,
35475
+ geography,
35476
+ content,
35477
+ conversions
35478
+ ] = await Promise.all([
35479
+ analyticsOverview(siteId, userId, filters),
35480
+ analyticsTimeseries(siteId, userId, filters),
35481
+ analyticsAcquisition(siteId, userId, filters, { limit: 25 }),
35482
+ analyticsDimensions(siteId, userId, filters, "device", { limit: 10 }),
35483
+ analyticsDimensions(siteId, userId, filters, "weekday_hour", {
35484
+ limit: 100
35485
+ }),
35486
+ analyticsDimensions(siteId, userId, filters, "region", { limit: 60 }),
35487
+ analyticsContent(siteId, userId, filters, { limit: 8 }),
35488
+ analyticsConversions(siteId, userId, filters, { limit: 8 })
35489
+ ]);
35490
+ let comparison = null;
35491
+ if (url.searchParams.get("compare") === "1") {
35492
+ const start = Date.parse(filters.start);
35493
+ const end = Date.parse(filters.end);
35494
+ const duration = end - start;
35495
+ const previousFilters = {
35496
+ ...filters,
35497
+ start: new Date(start - duration).toISOString(),
35498
+ end: new Date(start).toISOString()
35499
+ };
35500
+ const [previousOverview, previousTimeseries] = await Promise.all([
35501
+ analyticsOverview(siteId, userId, previousFilters),
35502
+ analyticsTimeseries(siteId, userId, previousFilters)
35503
+ ]);
35504
+ comparison = { overview: previousOverview, timeseries: previousTimeseries };
35505
+ }
35506
+ return c.json({
35507
+ ok: true,
35508
+ data: {
35509
+ overview,
35510
+ timeseries,
35511
+ acquisition,
35512
+ devices,
35513
+ activity,
35514
+ geography,
35515
+ content,
35516
+ conversions,
35517
+ comparison
35518
+ },
35519
+ filters
35520
+ });
35521
+ });
35522
+ analyticsApp.get("/sites/:siteId/dimensions", auth3, async (c) => {
35523
+ const url = new URL(c.req.url);
35524
+ const filters = reportFilters(url);
35525
+ validateReportWindow(filters);
35526
+ const dimension = url.searchParams.get("dimension") || "device";
35527
+ if (!["device", "source", "country", "region", "weekday_hour"].includes(
35528
+ dimension
35529
+ )) {
35530
+ return c.json(
35531
+ {
35532
+ ok: false,
35533
+ code: "invalid_dimension",
35534
+ error: "Choose a supported dimension."
35535
+ },
35536
+ 400
35537
+ );
35538
+ }
35539
+ const data = await analyticsDimensions(
35540
+ c.req.param("siteId"),
35541
+ Number(c.get("user").id),
35542
+ filters,
35543
+ dimension,
35544
+ reportPage(url)
35545
+ );
35546
+ return c.json({ ok: true, data, filters });
35547
+ });
35548
+ analyticsApp.get("/sites/:siteId/conversions", auth3, async (c) => {
35549
+ const url = new URL(c.req.url);
35550
+ const filters = reportFilters(url);
35551
+ validateReportWindow(filters);
35552
+ const data = await analyticsConversions(
35553
+ c.req.param("siteId"),
35554
+ Number(c.get("user").id),
35555
+ filters,
35556
+ reportPage(url)
35557
+ );
35558
+ return c.json({ ok: true, data, filters });
35559
+ });
35560
+ analyticsApp.get("/sites/:siteId/paths", auth3, async (c) => {
35561
+ const url = new URL(c.req.url);
35562
+ const filters = reportFilters(url);
35563
+ validateReportWindow(filters);
35564
+ const data = await analyticsPaths(
35565
+ c.req.param("siteId"),
35566
+ Number(c.get("user").id),
35567
+ filters,
35568
+ reportPage(url)
35569
+ );
35570
+ return c.json({ ok: true, data, filters });
35571
+ });
35572
+ analyticsApp.get("/sites/:siteId/channel-breakdown", auth3, async (c) => {
35573
+ const filters = reportFilters(new URL(c.req.url));
35574
+ const data = await analyticsChannelBreakdown(
35575
+ c.req.param("siteId"),
35576
+ Number(c.get("user").id),
35577
+ filters
35578
+ );
35579
+ return c.json({ ok: true, data });
35580
+ });
35581
+ analyticsApp.get("/sites/:siteId/health", auth3, async (c) => {
35582
+ const health = await analyticsHealth(
35583
+ c.req.param("siteId"),
35584
+ Number(c.get("user").id)
35585
+ );
35586
+ return c.json({ ok: true, health }, 200, { "Cache-Control": "no-store" });
35587
+ });
35588
+ analyticsApp.post("/sites/:siteId/exports", auth3, async (c) => {
35589
+ const parsed = ExportSchema.safeParse(await c.req.json().catch(() => ({})));
35590
+ if (!parsed.success)
35591
+ return c.json(
35592
+ {
35593
+ ok: false,
35594
+ error: "Invalid export request.",
35595
+ issues: parsed.error.issues
35596
+ },
35597
+ 400
35598
+ );
35599
+ const filters = reportFilters(new URL(c.req.url));
35600
+ validateReportWindow(filters);
35601
+ const artifact = await createAnalyticsExport({
35602
+ siteId: c.req.param("siteId"),
35603
+ userId: Number(c.get("user").id),
35604
+ filters,
35605
+ ...parsed.data,
35606
+ idempotencyKey: c.req.header("idempotency-key") || parsed.data.idempotencyKey
35607
+ });
35608
+ return c.json({ ok: true, artifact }, 201);
35609
+ });
35610
+ analyticsApp.onError((error, c) => {
35611
+ if (error instanceof AnalyticsRepositoryError) {
35612
+ return c.json(
35613
+ { ok: false, code: error.code, error: error.message },
35614
+ error.status
35615
+ );
35616
+ }
35617
+ console.error(
35618
+ "[analytics]",
35619
+ error instanceof Error ? error.message : String(error)
35620
+ );
35621
+ return c.json(
35622
+ {
35623
+ ok: false,
35624
+ code: "analytics_internal_error",
35625
+ error: "Site Analytics could not complete the request."
35626
+ },
35627
+ 500
35628
+ );
35629
+ });
35630
+
35631
+ // src/api/analytics-delivery.ts
35632
+ function safeDeliveryError(error) {
35633
+ if (!error) return "memory_write_failed";
35634
+ if (error.includes("configured") || error.includes("credential")) return "memory_credential_unavailable";
35635
+ if (error.includes("401") || error.includes("403")) return "memory_authorization_failed";
35636
+ if (error.includes("429")) return "memory_rate_limited";
35637
+ return "memory_write_failed";
35638
+ }
35639
+ async function deliverPendingAnalyticsForms(limit = 10) {
35640
+ const jobs = await claimAnalyticsFormDeliveryJobs(limit);
35641
+ let delivered = 0;
35642
+ let deferred = 0;
35643
+ for (const job of jobs) {
35644
+ const payloadText = decryptMemoryKey(job.payloadCiphertext);
35645
+ const user = await getUserById(job.ownerUserId);
35646
+ if (!payloadText || !user) {
35647
+ await deferAnalyticsFormDelivery(job.id, job.attempts, payloadText ? "owner_not_found" : "payload_unreadable");
35648
+ deferred += 1;
35649
+ continue;
35650
+ }
35651
+ let payload;
35652
+ try {
35653
+ payload = JSON.parse(payloadText);
35654
+ } catch {
35655
+ await deferAnalyticsFormDelivery(job.id, job.attempts, "payload_invalid");
35656
+ deferred += 1;
35657
+ continue;
35658
+ }
35659
+ const { key, error } = await getOrCreateUserMemoryKey(user);
35660
+ const result = key ? await memoryCall("putTool", payload, key) : { ok: false, error };
35661
+ if (result.ok) {
35662
+ await completeAnalyticsFormDelivery(job.id, job.submissionId);
35663
+ delivered += 1;
35664
+ } else {
35665
+ await deferAnalyticsFormDelivery(job.id, job.attempts, safeDeliveryError(result.error));
35666
+ deferred += 1;
35667
+ }
35668
+ }
35669
+ return { claimed: jobs.length, delivered, deferred };
35670
+ }
35671
+
35672
+ // src/api/scheduled-result-routes.ts
35673
+ import { Hono as Hono33 } from "hono";
35674
+ import { z as z35 } from "zod";
33567
35675
 
33568
35676
  // src/api/scheduled-artifact-owner.ts
33569
- import { createHash as createHash14 } from "crypto";
35677
+ import { createHash as createHash17 } from "crypto";
33570
35678
  function scheduledArtifactOwnerIdForApiKey(apiKey) {
33571
- return createHash14("sha256").update(apiKey).digest("hex").slice(0, 24);
35679
+ return createHash17("sha256").update(apiKey).digest("hex").slice(0, 24);
33572
35680
  }
33573
35681
 
33574
35682
  // src/api/scheduled-run-view-links.ts
33575
- import { createHash as createHash15, randomBytes as randomBytes3, randomUUID as randomUUID17 } from "crypto";
35683
+ import { createHash as createHash18, randomBytes as randomBytes4, randomUUID as randomUUID17 } from "crypto";
33576
35684
  var schemaReady3 = false;
33577
35685
  async function ensureScheduledRunViewLinksSchema() {
33578
35686
  if (schemaReady3) return;
@@ -33585,7 +35693,7 @@ async function ensureScheduledRunViewLinksSchema() {
33585
35693
  artifact_id TEXT NOT NULL,
33586
35694
  title TEXT NOT NULL,
33587
35695
  summary TEXT NOT NULL,
33588
- renderer_key TEXT NOT NULL CHECK (renderer_key IN ('editorial_reading_room_v1','personal_authority_v1')),
35696
+ renderer_key TEXT NOT NULL CHECK (renderer_key IN ('editorial_reading_room_v1','personal_authority_v1','newsroom_publisher_v1')),
33589
35697
  token_hash TEXT NOT NULL UNIQUE,
33590
35698
  expires_at TEXT NOT NULL,
33591
35699
  revoked_at TEXT,
@@ -33597,22 +35705,22 @@ async function ensureScheduledRunViewLinksSchema() {
33597
35705
  args: []
33598
35706
  });
33599
35707
  const createSql = String(schema.rows[0]?.sql ?? "");
33600
- if (createSql.includes("renderer_key = 'editorial_reading_room_v1'")) {
35708
+ if (!createSql.includes("newsroom_publisher_v1")) {
33601
35709
  await db.batch([
33602
- `DROP TABLE IF EXISTS scheduled_run_view_links_v2`,
33603
- `CREATE TABLE scheduled_run_view_links_v2 (
35710
+ `DROP TABLE IF EXISTS scheduled_run_view_links_v3`,
35711
+ `CREATE TABLE scheduled_run_view_links_v3 (
33604
35712
  share_id TEXT PRIMARY KEY, owner_id TEXT NOT NULL, run_id TEXT NOT NULL,
33605
35713
  artifact_id TEXT NOT NULL, title TEXT NOT NULL, summary TEXT NOT NULL,
33606
- renderer_key TEXT NOT NULL CHECK (renderer_key IN ('editorial_reading_room_v1','personal_authority_v1')),
35714
+ renderer_key TEXT NOT NULL CHECK (renderer_key IN ('editorial_reading_room_v1','personal_authority_v1','newsroom_publisher_v1')),
33607
35715
  token_hash TEXT NOT NULL UNIQUE, expires_at TEXT NOT NULL, revoked_at TEXT,
33608
35716
  created_at TEXT NOT NULL
33609
35717
  )`,
33610
- `INSERT INTO scheduled_run_view_links_v2
35718
+ `INSERT INTO scheduled_run_view_links_v3
33611
35719
  SELECT share_id, owner_id, run_id, artifact_id, title, summary, renderer_key,
33612
35720
  token_hash, expires_at, revoked_at, created_at
33613
35721
  FROM scheduled_run_view_links`,
33614
35722
  `DROP TABLE scheduled_run_view_links`,
33615
- `ALTER TABLE scheduled_run_view_links_v2 RENAME TO scheduled_run_view_links`
35723
+ `ALTER TABLE scheduled_run_view_links_v3 RENAME TO scheduled_run_view_links`
33616
35724
  ], "write");
33617
35725
  }
33618
35726
  await db.execute(`
@@ -33626,7 +35734,7 @@ async function ensureScheduledRunViewLinksSchema() {
33626
35734
  schemaReady3 = true;
33627
35735
  }
33628
35736
  function tokenHash2(token4) {
33629
- return createHash15("sha256").update(token4).digest("hex");
35737
+ return createHash18("sha256").update(token4).digest("hex");
33630
35738
  }
33631
35739
  function mapRow(row) {
33632
35740
  return {
@@ -33636,7 +35744,7 @@ function mapRow(row) {
33636
35744
  artifactId: String(row.artifact_id),
33637
35745
  title: String(row.title),
33638
35746
  summary: String(row.summary),
33639
- rendererKey: row.renderer_key === "personal_authority_v1" ? "personal_authority_v1" : "editorial_reading_room_v1",
35747
+ rendererKey: row.renderer_key === "newsroom_publisher_v1" ? "newsroom_publisher_v1" : row.renderer_key === "personal_authority_v1" ? "personal_authority_v1" : "editorial_reading_room_v1",
33640
35748
  tokenHash: String(row.token_hash),
33641
35749
  expiresAt: String(row.expires_at),
33642
35750
  revokedAt: row.revoked_at == null ? null : String(row.revoked_at),
@@ -33646,7 +35754,7 @@ function mapRow(row) {
33646
35754
  async function createScheduledRunViewLink(input) {
33647
35755
  await ensureScheduledRunViewLinksSchema();
33648
35756
  const now = input.now ?? /* @__PURE__ */ new Date();
33649
- const token4 = randomBytes3(32).toString("base64url");
35757
+ const token4 = randomBytes4(32).toString("base64url");
33650
35758
  const record = {
33651
35759
  shareId: randomUUID17(),
33652
35760
  ownerId: input.ownerId,
@@ -33713,8 +35821,8 @@ async function revokeScheduledRunViewLink(ownerId, runId, shareId, now = /* @__P
33713
35821
  }
33714
35822
 
33715
35823
  // src/personal-authority/render.ts
33716
- import { createHash as createHash16 } from "crypto";
33717
- function escapeHtml6(value) {
35824
+ import { createHash as createHash19 } from "crypto";
35825
+ function escapeHtml7(value) {
33718
35826
  return value.replace(/[&<>"']/g, (character) => ({
33719
35827
  "&": "&amp;",
33720
35828
  "<": "&lt;",
@@ -33725,13 +35833,13 @@ function escapeHtml6(value) {
33725
35833
  }
33726
35834
  function safeHref(value) {
33727
35835
  const trimmed = value.trim();
33728
- return /^(?:https?:|mailto:|tel:|#|\/)/i.test(trimmed) ? escapeHtml6(trimmed) : "#";
35836
+ return /^(?:https?:|mailto:|tel:|#|\/)/i.test(trimmed) ? escapeHtml7(trimmed) : "#";
33729
35837
  }
33730
35838
  function initials(name) {
33731
35839
  return name.split(/\s+/).filter(Boolean).slice(0, 2).map((part) => part[0]?.toUpperCase()).join("") || "PA";
33732
35840
  }
33733
35841
  function action(action2, secondary = false) {
33734
- return `<a class="button${secondary ? " button--secondary" : ""}" href="${safeHref(action2.href)}">${escapeHtml6(action2.label)} <span aria-hidden="true">\u2197</span></a>`;
35842
+ return `<a class="button${secondary ? " button--secondary" : ""}" href="${safeHref(action2.href)}">${escapeHtml7(action2.label)} <span aria-hidden="true">\u2197</span></a>`;
33735
35843
  }
33736
35844
  function section(enabled, html) {
33737
35845
  return enabled ? html : "";
@@ -33740,7 +35848,7 @@ function renderPersonalAuthorityV1(input, config, generatedAt) {
33740
35848
  const flags = config.featureFlags;
33741
35849
  const brand = config.brandName || input.name;
33742
35850
  const generatedAtIso = generatedAt.toISOString();
33743
- const social = flags.socialLinks && input.socialLinks.length > 0 ? `<nav class="social" aria-label="Professional profiles">${input.socialLinks.map((link) => `<a href="${safeHref(link.href)}">${escapeHtml6(link.label)}</a>`).join("")}</nav>` : "";
35851
+ 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>` : "";
33744
35852
  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>` : "";
33745
35853
  const authority = input.authority;
33746
35854
  const consultation = input.consultation;
@@ -33752,32 +35860,144 @@ function renderPersonalAuthorityV1(input, config, generatedAt) {
33752
35860
  <html lang="en" data-theme="${config.theme}">
33753
35861
  <head>
33754
35862
  <meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
33755
- <meta name="description" content="${escapeHtml6(input.summary)}"><title>${escapeHtml6(input.name)} \u2014 ${escapeHtml6(input.professionalTitle)}</title>
35863
+ <meta name="description" content="${escapeHtml7(input.summary)}"><title>${escapeHtml7(input.name)} \u2014 ${escapeHtml7(input.professionalTitle)}</title>
33756
35864
  <style>
33757
35865
  :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)}
33758
35866
  @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}}
33759
35867
  </style></head>
33760
35868
  <body><a class="skip" href="#main">Skip to main content</a>
33761
- <header class="header"><a class="brand" href="#top"><span class="mark">${escapeHtml6(initials(input.name).slice(0, 1))}</span><strong>${escapeHtml6(brand)}</strong></a>${navigation}</header>
33762
- <main id="main"><section class="hero" id="top"><div class="portrait" aria-label="Portrait placeholder"><div class="portrait-card" aria-hidden="true">${escapeHtml6(initials(input.name))}</div></div><div class="hero-copy"><h1>${escapeHtml6(input.name)}</h1><p class="role">${escapeHtml6(input.professionalTitle)}</p><p class="summary">${escapeHtml6(input.summary)}</p><div class="actions">${action(input.primaryAction)}${input.secondaryAction ? action(input.secondaryAction, true) : ""}</div>${social}</div></section>
33763
- ${section(flags.authority && Boolean(authority), `<section class="light" id="about"><h2>${escapeHtml6(authority?.heading ?? "")}</h2>${authority?.paragraphs.map((paragraph) => `<p>${escapeHtml6(paragraph)}</p>`).join("") ?? ""}</section>`)}
33764
- ${section(flags.consultation && Boolean(consultation), `<section class="section consult"><div><p class="eyebrow">${escapeHtml6(consultation?.eyebrow ?? "Start a conversation")}</p><h2>${escapeHtml6(consultation?.heading ?? "")}</h2><p class="summary">${escapeHtml6(consultation?.summary ?? "")}</p>${consultation ? action(consultation.action) : ""}</div><div class="accent-card" aria-label="Image placeholder">${escapeHtml6(initials(input.name))}</div></section>`)}
33765
- ${section(flags.learning && Boolean(learning), `<section class="section learning"><div class="section-head"><h2>${escapeHtml6(learning?.heading ?? "")}</h2><p class="summary">${escapeHtml6(learning?.summary ?? "")}</p></div><div class="topic-grid">${learning?.topics.map((topic) => `<article class="topic"><strong>${escapeHtml6(topic.title)}</strong><h3>${escapeHtml6(topic.headline)}</h3><p>${escapeHtml6(topic.summary)}</p>${topic.href ? `<a href="${safeHref(topic.href)}">Explore \u2192</a>` : ""}</article>`).join("") ?? ""}</div></section>`)}
33766
- ${section(flags.services && Boolean(services), `<section class="section" id="services"><div class="section-head"><h2>${escapeHtml6(services?.heading ?? "")}</h2><p class="summary">${escapeHtml6(services?.summary ?? "")}</p></div><div class="grid">${services?.offers.map((offer) => `<article class="card"><h3>${escapeHtml6(offer.title)}</h3><p>${escapeHtml6(offer.description)}</p>${offer.action ? action(offer.action, true) : ""}</article>`).join("") ?? ""}</div></section>`)}
33767
- ${section(flags.proof && Boolean(proof), `<section class="section proof" id="proof"><div class="section-head"><h2>${escapeHtml6(proof?.heading ?? "")}</h2><p class="summary">${escapeHtml6(proof?.summary ?? "")}</p></div><div class="quotes">${proof?.testimonials.map((item) => `<article class="quote"><q>${escapeHtml6(item.quote)}</q><span>${escapeHtml6(item.person)}${item.role ? ` / ${escapeHtml6(item.role)}` : ""}</span></article>`).join("") ?? ""}</div></section>`)}
33768
- ${section(flags.finalCta && Boolean(finalCta), `<section class="section cta" id="contact"><p class="eyebrow">${escapeHtml6(finalCta?.eyebrow ?? "Ready when you are")}</p><h2>${escapeHtml6(finalCta?.heading ?? "")}</h2><p class="summary">${escapeHtml6(finalCta?.summary ?? "")}</p>${finalCta ? action(finalCta.action) : ""}</section>`)}
33769
- </main><footer class="footer"><div><strong>${escapeHtml6(input.name)}</strong><div>${escapeHtml6(input.professionalTitle)}</div>${config.showGeneratedAt ? `<div class="generated">Generated ${escapeHtml6(generatedAtIso)}</div>` : ""}</div><div>${input.location ? `<div>${escapeHtml6(input.location)}</div>` : ""}${input.email ? `<a href="mailto:${escapeHtml6(input.email)}">${escapeHtml6(input.email)}</a>` : ""}${input.phone ? `<div>${escapeHtml6(input.phone)}</div>` : ""}</div></footer>
35869
+ <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>
35870
+ <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>
35871
+ ${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>`)}
35872
+ ${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>`)}
35873
+ ${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>`)}
35874
+ ${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>`)}
35875
+ ${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>`)}
35876
+ ${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>`)}
35877
+ </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>
33770
35878
  <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>`;
33771
35879
  const bytes = Buffer.byteLength(html, "utf8");
33772
35880
  return {
33773
35881
  html,
33774
35882
  filename: `${input.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "personal-authority"}.html`,
33775
35883
  bytes,
33776
- sha256: createHash16("sha256").update(html).digest("hex"),
35884
+ sha256: createHash19("sha256").update(html).digest("hex"),
33777
35885
  generatedAt: generatedAtIso
33778
35886
  };
33779
35887
  }
33780
35888
 
35889
+ // src/newsroom-publisher/render.ts
35890
+ import { createHash as createHash20 } from "crypto";
35891
+ function escapeHtml8(value) {
35892
+ return value.replace(/[&<>"']/g, (character) => ({
35893
+ "&": "&amp;",
35894
+ "<": "&lt;",
35895
+ ">": "&gt;",
35896
+ '"': "&quot;",
35897
+ "'": "&#39;"
35898
+ })[character] ?? character);
35899
+ }
35900
+ function safeHref2(value) {
35901
+ const trimmed = value.trim();
35902
+ return /^(?:https?:|mailto:|tel:|#|\/)/i.test(trimmed) ? escapeHtml8(trimmed) : "#";
35903
+ }
35904
+ function formatDate(value) {
35905
+ const date = new Date(value);
35906
+ return Number.isNaN(date.getTime()) ? escapeHtml8(value) : new Intl.DateTimeFormat("en", { month: "short", day: "numeric", year: "numeric" }).format(date);
35907
+ }
35908
+ function initials2(value) {
35909
+ return value.split(/\s+/).filter(Boolean).slice(0, 2).map((part) => part[0]).join("").toUpperCase();
35910
+ }
35911
+ function storyMedia(story, className = "") {
35912
+ if (story.imageUrl) {
35913
+ return `<div class="story-media ${className}"><img src="${safeHref2(story.imageUrl)}" alt="${escapeHtml8(story.imageAlt || story.title)}" loading="lazy"></div>`;
35914
+ }
35915
+ const tone = [...story.title].reduce((total, character) => total + character.charCodeAt(0), 0) % 5;
35916
+ 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>`;
35917
+ }
35918
+ function byline(story, showBylines) {
35919
+ const author = story.author.href ? `<a href="${safeHref2(story.author.href)}">${escapeHtml8(story.author.name)}</a>` : escapeHtml8(story.author.name);
35920
+ return `<div class="byline">${showBylines ? `<span>By ${author}</span>` : ""}<time datetime="${escapeHtml8(story.publishedAt)}">${formatDate(story.publishedAt)}</time>${story.sponsored ? "<b>Sponsored</b>" : ""}</div>`;
35921
+ }
35922
+ function storyCard(story, showBylines, variant = "") {
35923
+ return `<article class="story-card ${variant}">
35924
+ <a class="story-card__media" href="${safeHref2(story.href)}">${storyMedia(story)}</a>
35925
+ <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>
35926
+ </article>`;
35927
+ }
35928
+ function renderNewsroomPublisherV1(input, config, generatedAt) {
35929
+ const flags = config.featureFlags;
35930
+ const generatedAtIso = generatedAt.toISOString();
35931
+ const brand = config.brandName || input.publication.name;
35932
+ const [lead, ...supporting] = input.featured;
35933
+ const primaryPages = [
35934
+ { label: "Home", href: input.publication.url },
35935
+ input.primaryPages.about,
35936
+ input.primaryPages.editor,
35937
+ input.primaryPages.press,
35938
+ input.primaryPages.news,
35939
+ input.primaryPages.contact
35940
+ ];
35941
+ const trustPages = Object.values(input.trustPages);
35942
+ 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>` : "";
35943
+ 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">
35944
+ <a class="wordmark" href="${safeHref2(input.publication.url)}"><span>${escapeHtml8(brand)}</span><small>${escapeHtml8(input.publication.tagline)}</small></a>
35945
+ <div class="masthead__date"><span>Independent journalism</span><time datetime="${generatedAtIso}">${formatDate(generatedAtIso)}</time></div>
35946
+ <div class="masthead__tools"><button type="button" aria-label="Search">Search</button><button type="button" aria-label="Open menu">Menu</button></div>
35947
+ </header>`;
35948
+ 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>` : "";
35949
+ const leadGrid = flags.leadGrid ? `<section class="lead-grid" aria-label="Top stories">
35950
+ <div class="lead-grid__label"><span>Today\u2019s edition</span><b>Top stories</b></div>
35951
+ <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>
35952
+ <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>
35953
+ </section>` : "";
35954
+ const latest = flags.latestNews && input.latest.length > 0 ? `<section class="latest" id="latest">
35955
+ <div class="section-title"><div><span>Live desk</span><h2>Latest news</h2></div><a href="#top">Back to top \u2191</a></div>
35956
+ <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>
35957
+ <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>
35958
+ </section>` : "";
35959
+ const sectionLayouts = ["panorama", "mosaic", "tiles"];
35960
+ const sections = flags.categorySections ? input.sections.map((section2, sectionIndex) => {
35961
+ const layout = sectionLayouts[sectionIndex % sectionLayouts.length];
35962
+ const cards = section2.stories.map((story, index) => storyCard(
35963
+ story,
35964
+ config.showBylines,
35965
+ layout === "panorama" && index === 0 ? "story-card--panorama" : layout === "mosaic" && index === 0 ? "story-card--mosaic-lead" : ""
35966
+ )).join("");
35967
+ return `<section class="desk-section desk-section--${sectionIndex % 2 ? "tint" : "plain"} desk-section--layout-${layout}">
35968
+ <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>
35969
+ <div class="desk-section__grid desk-section__grid--${layout}">${cards}</div>
35970
+ </section>`;
35971
+ }).join("") : "";
35972
+ 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>` : "";
35973
+ 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>` : "";
35974
+ 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>`;
35975
+ 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>` : "";
35976
+ const html = `<!doctype html>
35977
+ <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">
35978
+ <style>
35979
+ :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}}
35980
+ /* Tampa-inspired publication palette */
35981
+ :root{--ink:#12312f;--soft-ink:#294744;--muted:#697b77;--paper:#fffdf8;--cream:#f8f2e8;--wash:#f3eee5;--line:#d8d3c8;--signal:#ee7057;--acid:#f0b45d;--dark:#082f2c}
35982
+ body[data-theme=journal]{--dark:#102d2a;--ink:#172f2d;--cream:#eee7da;--signal:#bd5b47;--acid:#e4aa52}
35983
+ body[data-theme=midnight]{--paper:#e8e9e4;--cream:#dfe3dc;--wash:#d8ddd6;--line:#b9c2bb;--dark:#072a28;--signal:#df6c56}
35984
+ .utility-nav{color:#d7e0dc;border-bottom-color:rgba(255,255,255,.14)}
35985
+ .lead-grid__label,.lead-story{border-bottom-color:rgba(255,255,255,.17)}
35986
+ .lead-grid__label b{color:#90aaa4}
35987
+ .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:""}
35988
+ .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}
35989
+ 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}
35990
+ .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}
35991
+ @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)}}
35992
+ @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)}}
35993
+ @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}}
35994
+ </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>`;
35995
+ const bytes = Buffer.byteLength(html);
35996
+ const sha2563 = createHash20("sha256").update(html).digest("hex");
35997
+ const filename = `${brand.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "newsroom"}-news-site.html`;
35998
+ return { html, filename, bytes, sha256: sha2563, generatedAt: generatedAtIso };
35999
+ }
36000
+
33781
36001
  // src/scheduled-artifacts/scheduled-artifact-renderer-registry.ts
33782
36002
  var editorialReadingRoomV1 = {
33783
36003
  rendererKey: "editorial_reading_room_v1",
@@ -33815,24 +36035,47 @@ var personalAuthorityV1 = {
33815
36035
  };
33816
36036
  }
33817
36037
  };
36038
+ var newsroomPublisherV1 = {
36039
+ rendererKey: "newsroom_publisher_v1",
36040
+ rendererVersion: 1,
36041
+ async render(rawInput, rawConfig, generatedAt) {
36042
+ const input = NewsroomPublisherV1InputSchema.parse(rawInput);
36043
+ const config = NewsroomPublisherTemplateConfigSchema.parse(rawConfig);
36044
+ const rendered = renderNewsroomPublisherV1(input, config, generatedAt);
36045
+ if (rendered.bytes > 2e6) {
36046
+ throw new Error("Scheduled artifact HTML must be 2,000,000 bytes or fewer.");
36047
+ }
36048
+ return {
36049
+ ...rendered,
36050
+ title: `${input.publication.name} News Publisher`
36051
+ };
36052
+ }
36053
+ };
33818
36054
  var scheduledArtifactRendererRegistry = Object.freeze({
33819
36055
  "editorial_reading_room_v1:1": editorialReadingRoomV1,
33820
- "personal_authority_v1:1": personalAuthorityV1
36056
+ "personal_authority_v1:1": personalAuthorityV1,
36057
+ "newsroom_publisher_v1:1": newsroomPublisherV1
33821
36058
  });
33822
36059
  function getScheduledArtifactRenderer(rendererKey, rendererVersion) {
33823
36060
  return scheduledArtifactRendererRegistry[`${rendererKey}:${rendererVersion}`] ?? null;
33824
36061
  }
33825
36062
  var scheduledArtifactTemplatePresets = [
33826
36063
  EDITORIAL_READING_ROOM_TEMPLATE_PRESET,
33827
- PERSONAL_AUTHORITY_TEMPLATE_PRESET
36064
+ PERSONAL_AUTHORITY_TEMPLATE_PRESET,
36065
+ NEWSROOM_PUBLISHER_TEMPLATE_PRESET
33828
36066
  ];
33829
36067
 
33830
36068
  // src/api/scheduled-result-routes.ts
33831
- var TemplateParamsSchema = z33.object({ templateId: z33.string().uuid() }).strict();
33832
- var RunParamsSchema = z33.object({ runId: z33.string().min(1).max(200) }).strict();
33833
- var ViewLinkParamsSchema = z33.object({
33834
- runId: z33.string().min(1).max(200),
33835
- shareId: z33.string().uuid()
36069
+ var TemplateParamsSchema = z35.object({ templateId: z35.string().uuid() }).strict();
36070
+ var RunParamsSchema = z35.object({ runId: z35.string().min(1).max(200) }).strict();
36071
+ var ViewLinkParamsSchema = z35.object({
36072
+ runId: z35.string().min(1).max(200),
36073
+ shareId: z35.string().uuid()
36074
+ }).strict();
36075
+ var ArtifactTemplateListQuerySchema = z35.object({
36076
+ status: z35.enum(["active", "archived", "all"]).default("active"),
36077
+ query: z35.string().trim().min(1).max(120).optional(),
36078
+ presetKey: ArtifactTemplatePresetKeySchema.optional()
33836
36079
  }).strict();
33837
36080
  function apiError(error, errorCode = "invalid_request") {
33838
36081
  return { ok: false, error, errorCode };
@@ -33861,25 +36104,36 @@ function safeMemoryFailure(result) {
33861
36104
  errorCode in messages ? errorCode : "internal_error"
33862
36105
  );
33863
36106
  }
33864
- var scheduledResultApp = new Hono32();
36107
+ var scheduledResultApp = new Hono33();
33865
36108
  scheduledResultApp.use("/artifact-templates", createApiKeyAuth());
33866
36109
  scheduledResultApp.use("/artifact-templates/*", createApiKeyAuth());
33867
36110
  scheduledResultApp.use("/schedule-runs", createApiKeyAuth());
33868
36111
  scheduledResultApp.use("/schedule-runs/*", createApiKeyAuth());
33869
36112
  scheduledResultApp.get("/artifact-templates", async (c) => {
33870
- const status = c.req.query("status") ?? "active";
33871
- if (!["active", "archived", "all"].includes(status)) {
33872
- return c.json(apiError("status must be active, archived, or all."), 400);
33873
- }
36113
+ const parsed = ArtifactTemplateListQuerySchema.safeParse(c.req.query());
36114
+ if (!parsed.success) return c.json(apiError("The template search filters are invalid."), 400);
36115
+ const { status, query: searchText, presetKey } = parsed.data;
33874
36116
  const { key, error } = await memoryContext(c);
33875
36117
  if (!key) return c.json(apiError(error ?? "Memory is unavailable.", "internal_error"), 503);
33876
36118
  const result = await memoryCall("list_artifact_templates", { status }, key);
33877
36119
  if (!result.ok) return c.json(safeMemoryFailure(result), memoryStatus(result));
33878
36120
  c.header("Cache-Control", "private, no-store");
33879
- return c.json({
33880
- ...result,
33881
- presets: Array.isArray(result.presets) ? result.presets : scheduledArtifactTemplatePresets
33882
- });
36121
+ const normalizedQuery = searchText?.toLocaleLowerCase();
36122
+ const presets = scheduledArtifactTemplatePresets.filter((preset) => {
36123
+ if (presetKey && preset.presetKey !== presetKey) return false;
36124
+ if (!normalizedQuery) return true;
36125
+ return [preset.name, preset.description, preset.presetKey].some((value) => value.toLocaleLowerCase().includes(normalizedQuery));
36126
+ });
36127
+ const templates = (Array.isArray(result.templates) ? result.templates : []).filter((template) => {
36128
+ if (!template || typeof template !== "object") return false;
36129
+ const record = template;
36130
+ const currentVersion = record.currentVersion && typeof record.currentVersion === "object" ? record.currentVersion : null;
36131
+ const rendererKey = typeof currentVersion?.rendererKey === "string" ? currentVersion.rendererKey : "";
36132
+ if (presetKey && rendererKey !== presetKey) return false;
36133
+ if (!normalizedQuery) return true;
36134
+ return [record.name, record.description, rendererKey, currentVersion?.authoringInstructions].filter((value) => typeof value === "string").some((value) => value.toLocaleLowerCase().includes(normalizedQuery));
36135
+ });
36136
+ return c.json({ ok: true, presets, templates });
33883
36137
  });
33884
36138
  scheduledResultApp.post("/artifact-templates", async (c) => {
33885
36139
  const parsed = CreateArtifactTemplateInputSchema.safeParse(await c.req.json().catch(() => null));
@@ -33997,10 +36251,10 @@ scheduledResultApp.delete("/schedule-runs/:runId/view-links/:shareId", async (c)
33997
36251
  });
33998
36252
 
33999
36253
  // src/api/public-scheduled-result-routes.ts
34000
- import { Hono as Hono33 } from "hono";
36254
+ import { Hono as Hono34 } from "hono";
34001
36255
 
34002
36256
  // src/scheduled-artifacts/scheduled-run-artifact-store.ts
34003
- import { createHash as createHash17 } from "crypto";
36257
+ import { createHash as createHash21 } from "crypto";
34004
36258
  var SCHEDULED_RUN_ARTIFACT_PREFIX = "scheduled-run-artifacts/";
34005
36259
  var SCHEDULED_RUN_ARTIFACT_DOWNLOAD_TTL_MS = 15 * 60 * 1e3;
34006
36260
  var SCHEDULED_RUN_ARTIFACT_MAX_BYTES = 2e6;
@@ -34016,7 +36270,7 @@ function policy3() {
34016
36270
  };
34017
36271
  }
34018
36272
  function runStorageSegment(runId) {
34019
- return /^[a-zA-Z0-9_-]{1,160}$/.test(runId) ? runId : `run-${createHash17("sha256").update(runId).digest("hex").slice(0, 32)}`;
36273
+ return /^[a-zA-Z0-9_-]{1,160}$/.test(runId) ? runId : `run-${createHash21("sha256").update(runId).digest("hex").slice(0, 32)}`;
34020
36274
  }
34021
36275
  async function createScheduledRunArtifact(args) {
34022
36276
  if (args.rendered.bytes > SCHEDULED_RUN_ARTIFACT_MAX_BYTES) {
@@ -34059,7 +36313,7 @@ async function readScheduledRunArtifact(args) {
34059
36313
  }
34060
36314
 
34061
36315
  // src/api/public-scheduled-result-routes.ts
34062
- var publicScheduledResultApp = new Hono33();
36316
+ var publicScheduledResultApp = new Hono34();
34063
36317
  function allowedFrameAncestors() {
34064
36318
  const configured = (process.env.SCHEDULED_ARTIFACT_FRAME_ANCESTORS ?? "").split(",").map((value) => value.trim()).filter((value) => /^https?:\/\/[^/\s]+$/.test(value));
34065
36319
  const origins = configured.length ? configured : ["https://mcpscraper.dev", "https://www.mcpscraper.dev"];
@@ -34120,10 +36374,10 @@ publicScheduledResultApp.get("/scheduled-run-view-links/:token/content", async (
34120
36374
  });
34121
36375
 
34122
36376
  // src/api/scheduled-artifact-internal-routes.ts
34123
- import { Hono as Hono34 } from "hono";
36377
+ import { Hono as Hono35 } from "hono";
34124
36378
 
34125
36379
  // src/api/scheduler-integration-auth.ts
34126
- import { createHash as createHash18, createHmac as createHmac4, timingSafeEqual as timingSafeEqual3 } from "crypto";
36380
+ import { createHash as createHash22, createHmac as createHmac4, timingSafeEqual as timingSafeEqual3 } from "crypto";
34127
36381
 
34128
36382
  // src/api/service-connections.ts
34129
36383
  import { randomUUID as randomUUID18 } from "crypto";
@@ -34416,12 +36670,12 @@ var SchedulerIntegrationAuthError = class extends Error {
34416
36670
  }
34417
36671
  };
34418
36672
  function signingSecret() {
34419
- const secret2 = process.env.SCHEDULER_INTEGRATION_SIGNING_SECRET?.trim();
34420
- if (!secret2) throw new SchedulerIntegrationAuthError("not_configured");
34421
- return secret2;
36673
+ const secret = process.env.SCHEDULER_INTEGRATION_SIGNING_SECRET?.trim();
36674
+ if (!secret) throw new SchedulerIntegrationAuthError("not_configured");
36675
+ return secret;
34422
36676
  }
34423
36677
  function schedulerIntegrationSignature(args) {
34424
- const bodyHash = createHash18("sha256").update(args.body).digest("hex");
36678
+ const bodyHash = createHash22("sha256").update(args.body).digest("hex");
34425
36679
  return createHmac4("sha256", args.secret).update(`${args.method.toUpperCase()}
34426
36680
  ${args.path}
34427
36681
  ${args.timestamp}
@@ -34477,7 +36731,7 @@ async function verifySchedulerIntegrationRequest(request, rawBody) {
34477
36731
  }
34478
36732
 
34479
36733
  // src/api/scheduled-artifact-internal-routes.ts
34480
- var scheduledArtifactInternalApp = new Hono34();
36734
+ var scheduledArtifactInternalApp = new Hono35();
34481
36735
  function errorPayload(errorCode, error, requestId) {
34482
36736
  return ApiErrorResponseSchema.parse({
34483
36737
  ok: false,
@@ -34597,7 +36851,7 @@ function startSiteAuditWorker() {
34597
36851
  }
34598
36852
 
34599
36853
  // src/api/server.ts
34600
- import { createMiddleware as createMiddleware4 } from "hono/factory";
36854
+ import { createMiddleware as createMiddleware5 } from "hono/factory";
34601
36855
  import { getCookie as getCookie2, setCookie as setCookie2, deleteCookie } from "hono/cookie";
34602
36856
  import Stripe2 from "stripe";
34603
36857
 
@@ -34677,13 +36931,13 @@ async function reconcileSiteExtractSettlements(limit = 25) {
34677
36931
  }
34678
36932
 
34679
36933
  // src/api/page-diff.ts
34680
- import { createHash as createHash19 } from "crypto";
36934
+ import { createHash as createHash23 } from "crypto";
34681
36935
  import { diffLines } from "diff";
34682
36936
  var MAX_SNAPSHOT_CONTENT_CHARS = 25e4;
34683
36937
  var MAX_DIFF_HUNKS = 200;
34684
36938
  var MAX_DIFF_LINES_PER_RESPONSE = 2e3;
34685
36939
  function sha256Hex(value) {
34686
- return createHash19("sha256").update(value).digest("hex");
36940
+ return createHash23("sha256").update(value).digest("hex");
34687
36941
  }
34688
36942
  function truncateForStorage(value, maxChars = MAX_SNAPSHOT_CONTENT_CHARS) {
34689
36943
  if (value.length <= maxChars) return { value, truncated: false };
@@ -34797,10 +37051,10 @@ async function persistScrapeBody(user, opts) {
34797
37051
  }
34798
37052
 
34799
37053
  // src/api/scrape-image-sink.ts
34800
- import { createHash as createHash20 } from "crypto";
37054
+ import { createHash as createHash24 } from "crypto";
34801
37055
  var MAX_IMAGES_PER_SCRAPE = 25;
34802
37056
  function idempotencyKey2(userId, vault, input) {
34803
- return `scrape-image-${createHash20("sha256").update(`${userId}\0${vault}\0${input.sourceKind}\0${input.sourceUrl}\0${input.imageUrl ?? ""}\0${input.imageBase64 ?? ""}`).digest("hex")}`;
37057
+ return `scrape-image-${createHash24("sha256").update(`${userId}\0${vault}\0${input.sourceKind}\0${input.sourceUrl}\0${input.imageUrl ?? ""}\0${input.imageBase64 ?? ""}`).digest("hex")}`;
34804
37058
  }
34805
37059
  async function persistScrapeImagesToMemory(user, inputs, vault) {
34806
37060
  const selected = inputs.slice(0, MAX_IMAGES_PER_SCRAPE);
@@ -35272,7 +37526,7 @@ function inferredWaybackCapturedAt(content) {
35272
37526
  }
35273
37527
 
35274
37528
  // src/api/connection-memory-import.ts
35275
- import { createHash as createHash21 } from "crypto";
37529
+ import { createHash as createHash25 } from "crypto";
35276
37530
  var CONNECTION_MEMORY_IMPORT_MAX_ARGS_BYTES = 64 * 1024;
35277
37531
  var CONNECTION_MEMORY_IMPORT_MAX_RESULT_BYTES = 1e6;
35278
37532
  var CONNECTION_MEMORY_IMPORT_MAX_STRING_CHARS = 256e3;
@@ -35302,7 +37556,7 @@ function canonicalJson(value) {
35302
37556
  return JSON.stringify(value);
35303
37557
  }
35304
37558
  function sha2562(value) {
35305
- return createHash21("sha256").update(value).digest("hex");
37559
+ return createHash25("sha256").update(value).digest("hex");
35306
37560
  }
35307
37561
  function sensitiveKey(key) {
35308
37562
  const normalized = key.toLowerCase().replace(/[^a-z0-9]/g, "");
@@ -36313,7 +38567,7 @@ async function dbUsage(identity, plan) {
36313
38567
  }
36314
38568
 
36315
38569
  // src/api/nango-control.ts
36316
- import { createHash as createHash23, randomUUID as randomUUID22 } from "crypto";
38570
+ import { createHash as createHash27, randomUUID as randomUUID22 } from "crypto";
36317
38571
 
36318
38572
  // src/api/slack-archive-analysis.ts
36319
38573
  function isRecord2(value) {
@@ -36687,7 +38941,7 @@ async function exportSlackChannelPage(input, dependencies) {
36687
38941
  }
36688
38942
 
36689
38943
  // src/api/main-nango-transport.ts
36690
- import { createHash as createHash22, randomUUID as randomUUID21 } from "crypto";
38944
+ import { createHash as createHash26, randomUUID as randomUUID21 } from "crypto";
36691
38945
  import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client";
36692
38946
 
36693
38947
  // src/api/connected-tool-policy.ts
@@ -36820,9 +39074,9 @@ function arrayAt(payload, key) {
36820
39074
  return Array.isArray(data?.[key]) ? data[key] : [];
36821
39075
  }
36822
39076
  function nangoSecret() {
36823
- const secret2 = process.env.NANGO_SECRET_KEY?.trim();
36824
- if (!secret2) throw new MainNangoTransportError("Nango is not configured.", 503, "nango_not_configured");
36825
- return secret2;
39077
+ const secret = process.env.NANGO_SECRET_KEY?.trim();
39078
+ if (!secret) throw new MainNangoTransportError("Nango is not configured.", 503, "nango_not_configured");
39079
+ return secret;
36826
39080
  }
36827
39081
  function nangoMcpUrl() {
36828
39082
  try {
@@ -37080,7 +39334,7 @@ async function listNangoToolsDirect(identity, connectionId) {
37080
39334
  });
37081
39335
  const readTools = [...policies.values()].filter((policy4) => policy4.classification === "read").map((policy4) => policy4.name);
37082
39336
  const actionTools = [...policies.values()].filter((policy4) => policy4.classification === "action").map((policy4) => policy4.name);
37083
- const revision = createHash22("sha256").update(JSON.stringify(tools.map((tool) => ({ name: tool.name, inputSchema: tool.inputSchema })))).digest("hex");
39337
+ const revision = createHash26("sha256").update(JSON.stringify(tools.map((tool) => ({ name: tool.name, inputSchema: tool.inputSchema })))).digest("hex");
37084
39338
  await updateServiceConnectionTools(connection.id, readTools, actionTools, revision);
37085
39339
  const refreshed = await getOwnedServiceConnection(identity, connection.id);
37086
39340
  return { connection: refreshed ?? { ...connection, readTools, actionTools, toolRevision: revision }, tools };
@@ -37108,7 +39362,7 @@ async function callNangoToolDirect(args) {
37108
39362
  ratePolicyVersion: CONNECTED_USAGE_RATE_POLICY_VERSION
37109
39363
  });
37110
39364
  const requestId = args.requestId?.trim() || randomUUID21();
37111
- const idempotencyKey3 = `main-nango:${createHash22("sha256").update(args.identity.toLowerCase()).update("\0").update(connection.id).update("\0").update(args.tool).update("\0").update(requestId).digest("hex")}`;
39365
+ const idempotencyKey3 = `main-nango:${createHash26("sha256").update(args.identity.toLowerCase()).update("\0").update(connection.id).update("\0").update(args.tool).update("\0").update(requestId).digest("hex")}`;
37112
39366
  const startedAt = /* @__PURE__ */ new Date();
37113
39367
  const started = performance.now();
37114
39368
  let result;
@@ -37133,7 +39387,7 @@ async function callNangoToolDirect(args) {
37133
39387
  toolName: args.tool,
37134
39388
  operationKind: args.operationKind ?? args.classification,
37135
39389
  outcome: providerError ? "error" : "partial",
37136
- requestId: requestId.length <= 200 ? requestId : createHash22("sha256").update(requestId).digest("hex"),
39390
+ requestId: requestId.length <= 200 ? requestId : createHash26("sha256").update(requestId).digest("hex"),
37137
39391
  startedAt: startedAt.toISOString(),
37138
39392
  completedAt: completedAt.toISOString()
37139
39393
  }
@@ -37175,7 +39429,7 @@ async function describeNangoToolDirect(identity, connectionId, toolName) {
37175
39429
  providerContractHash: MAIN_INTEGRATION_CONTRACT_HASH,
37176
39430
  protocolVersion: null,
37177
39431
  schemaSource: "live_tools_list",
37178
- schemaHash: createHash22("sha256").update(JSON.stringify(projected)).digest("hex"),
39432
+ schemaHash: createHash26("sha256").update(JSON.stringify(projected)).digest("hex"),
37179
39433
  fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
37180
39434
  };
37181
39435
  }
@@ -37321,9 +39575,9 @@ function controlBaseUrl() {
37321
39575
  return (process.env.NANGO_CONTROL_URL?.trim() || DEFAULT_NANGO_CONTROL_URL).replace(/\/$/, "");
37322
39576
  }
37323
39577
  function controlSecret() {
37324
- const secret2 = process.env.SCHEDULE_INTEGRATIONS_SECRET?.trim();
37325
- if (!secret2) throw new NangoControlError("Scheduled service connections are not configured.", 503);
37326
- return secret2;
39578
+ const secret = process.env.SCHEDULE_INTEGRATIONS_SECRET?.trim();
39579
+ if (!secret) throw new NangoControlError("Scheduled service connections are not configured.", 503);
39580
+ return secret;
37327
39581
  }
37328
39582
  function isRecord4(value) {
37329
39583
  return !!value && typeof value === "object" && !Array.isArray(value);
@@ -37899,8 +40153,8 @@ async function setScheduleConnectionActionsEnabled(identity, connectionId, enabl
37899
40153
  return data.connection.actionsEnabled === true;
37900
40154
  }
37901
40155
  async function callScheduleConnectionAction(identity, connectionId, input, tool, idempotencyKey3) {
37902
- const requestId = `main-connected-action:${createHash23("sha256").update(identity).update("\0").update(idempotencyKey3?.trim() || randomUUID22()).digest("hex")}`;
37903
- const requestDigest = createHash23("sha256").update(connectionId).update("\0").update(tool?.trim() ?? "").update("\0").update(canonicalJson2(input)).digest("hex");
40156
+ const requestId = `main-connected-action:${createHash27("sha256").update(identity).update("\0").update(idempotencyKey3?.trim() || randomUUID22()).digest("hex")}`;
40157
+ const requestDigest = createHash27("sha256").update(connectionId).update("\0").update(tool?.trim() ?? "").update("\0").update(canonicalJson2(input)).digest("hex");
37904
40158
  if (mainOwnsIntegrations()) {
37905
40159
  const selectedTool = tool?.trim();
37906
40160
  if (!selectedTool) throw new NangoControlError("An action tool is required.", 400, "invalid_request", false);
@@ -38051,7 +40305,7 @@ function canonicalJson2(value) {
38051
40305
  return JSON.stringify(value);
38052
40306
  }
38053
40307
  function projectedToolSchemaHash(tool) {
38054
- return createHash23("sha256").update(canonicalJson2(tool)).digest("hex");
40308
+ return createHash27("sha256").update(canonicalJson2(tool)).digest("hex");
38055
40309
  }
38056
40310
  async function describeNangoTool(identity, connectionId, tool, fresh) {
38057
40311
  if (mainOwnsIntegrations()) {
@@ -38330,7 +40584,7 @@ async function callMainOwnedExportPage(identity, input) {
38330
40584
  }
38331
40585
 
38332
40586
  // src/api/resend-control.ts
38333
- import { createHash as createHash24 } from "crypto";
40587
+ import { createHash as createHash28 } from "crypto";
38334
40588
  var DEFAULT_CONNECTION_CONTROL_URL = "https://mcp-scraper-scheduler.vercel.app";
38335
40589
  var RESEND_PROVIDER_CONFIG_KEY = "resend";
38336
40590
  var RESEND_LOGO_URL = "https://cdn.resend.com/brand/resend-icon-black.svg";
@@ -38370,9 +40624,9 @@ function controlBaseUrl2() {
38370
40624
  return (process.env.SCHEDULE_INTEGRATIONS_CONTROL_URL?.trim() || process.env.NANGO_CONTROL_URL?.trim() || DEFAULT_CONNECTION_CONTROL_URL).replace(/\/$/, "");
38371
40625
  }
38372
40626
  function controlSecret2() {
38373
- const secret2 = process.env.SCHEDULE_INTEGRATIONS_SECRET?.trim();
38374
- if (!secret2) throw new ResendControlError("Resend connections are not configured.", 503);
38375
- return secret2;
40627
+ const secret = process.env.SCHEDULE_INTEGRATIONS_SECRET?.trim();
40628
+ if (!secret) throw new ResendControlError("Resend connections are not configured.", 503);
40629
+ return secret;
38376
40630
  }
38377
40631
  function isRecord5(value) {
38378
40632
  return !!value && typeof value === "object" && !Array.isArray(value);
@@ -38589,7 +40843,7 @@ async function callResendRead(identity, connectionId, tool, args) {
38589
40843
  return isRecord5(data) ? data.result ?? data : data;
38590
40844
  }
38591
40845
  async function callResendAction(identity, connectionId, tool, input, idempotencyKey3) {
38592
- const requestId = `main-resend-action:${createHash24("sha256").update(identity).update("\0").update(idempotencyKey3.trim()).digest("hex")}`;
40846
+ const requestId = `main-resend-action:${createHash28("sha256").update(identity).update("\0").update(idempotencyKey3.trim()).digest("hex")}`;
38593
40847
  const body = await controlRequest2("/api/internal/resend/actions/call", {
38594
40848
  method: "POST",
38595
40849
  headers: { "x-request-id": requestId },
@@ -38657,7 +40911,7 @@ async function callResendExportPage(identity, input) {
38657
40911
 
38658
40912
  // src/api/server.ts
38659
40913
  var secureCookies2 = process.env.NODE_ENV === "production" || process.env.VERCEL === "1";
38660
- var isProduction2 = secureCookies2;
40914
+ var isProduction = secureCookies2;
38661
40915
  var sessionCookieOptions2 = {
38662
40916
  httpOnly: true,
38663
40917
  secure: secureCookies2,
@@ -38671,7 +40925,7 @@ function configuredOrigins() {
38671
40925
  const trimmed = raw.trim();
38672
40926
  if (trimmed) origins.add(trimmed);
38673
40927
  }
38674
- if (!isProduction2) {
40928
+ if (!isProduction) {
38675
40929
  origins.add("http://localhost:5173");
38676
40930
  origins.add("http://localhost:3000");
38677
40931
  origins.add("http://localhost:63316");
@@ -38697,13 +40951,13 @@ async function enforceRateLimit(c, scope, key, limit, windowSeconds) {
38697
40951
  { "Retry-After": String(result.resetSeconds) }
38698
40952
  );
38699
40953
  }
38700
- var requireAllowedOrigin = createMiddleware4(async (c, next) => {
40954
+ var requireAllowedOrigin = createMiddleware5(async (c, next) => {
38701
40955
  const origin = c.req.header("origin");
38702
40956
  if (!origin) return next();
38703
40957
  if (!configuredOrigins().has(origin)) return c.json({ error: "Origin not allowed" }, 403);
38704
40958
  return next();
38705
40959
  });
38706
- var auth3 = createMiddleware4(async (c, next) => {
40960
+ var auth4 = createMiddleware5(async (c, next) => {
38707
40961
  const key = c.req.header("x-api-key");
38708
40962
  if (!key) return c.json({ error: "Missing API key" }, 401);
38709
40963
  const user = await getUserByApiKey(key);
@@ -38715,7 +40969,7 @@ var auth3 = createMiddleware4(async (c, next) => {
38715
40969
  if (cost && cost.userId == null) cost.userId = Number(refreshed.id);
38716
40970
  return next();
38717
40971
  });
38718
- var sessionAuth = createMiddleware4(async (c, next) => {
40972
+ var sessionAuth = createMiddleware5(async (c, next) => {
38719
40973
  const token4 = getCookie2(c, "session");
38720
40974
  if (!token4) return c.json({ error: "Not authenticated" }, 401);
38721
40975
  const userId = verifySession(token4);
@@ -38727,7 +40981,7 @@ var sessionAuth = createMiddleware4(async (c, next) => {
38727
40981
  c.set("sessionUser", { ...refreshed, balance_mc: balanceMc });
38728
40982
  return next();
38729
40983
  });
38730
- var requireIntegrationsTier = createMiddleware4(async (c, next) => {
40984
+ var requireIntegrationsTier = createMiddleware5(async (c, next) => {
38731
40985
  const user = c.get("user");
38732
40986
  if (!user.subscription_tier) {
38733
40987
  return c.json({
@@ -38738,27 +40992,27 @@ var requireIntegrationsTier = createMiddleware4(async (c, next) => {
38738
40992
  }
38739
40993
  return next();
38740
40994
  });
38741
- var requirePaidSchedulingTier = createMiddleware4(async (c, next) => {
40995
+ var requirePaidSchedulingTier = createMiddleware5(async (c, next) => {
38742
40996
  const user = c.get("user");
38743
40997
  if (!hasPaidSchedulingAccess(user)) return c.json(paidPlanRequiredPayload(), 403);
38744
40998
  return next();
38745
40999
  });
38746
- var app = new Hono35();
41000
+ var app = new Hono36();
38747
41001
  var deploymentProfile = resolveDeploymentProfile();
38748
41002
  var STRIPE_API_VERSION = "2026-02-25.clover";
38749
41003
  function permitsLocalApiRequest(request) {
38750
41004
  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);
38751
41005
  }
38752
41006
  function requireStripeSecret() {
38753
- const secret2 = process.env.STRIPE_SECRET_KEY?.trim();
38754
- if (secret2) return secret2;
41007
+ const secret = process.env.STRIPE_SECRET_KEY?.trim();
41008
+ if (secret) return secret;
38755
41009
  if (process.env.NODE_ENV === "production" || process.env.VERCEL === "1") return null;
38756
41010
  return null;
38757
41011
  }
38758
41012
  async function createSignupStripeCustomer(email) {
38759
- const secret2 = requireStripeSecret();
38760
- if (!secret2) return null;
38761
- const stripeClient = new Stripe2(secret2, { apiVersion: STRIPE_API_VERSION });
41013
+ const secret = requireStripeSecret();
41014
+ if (!secret) return null;
41015
+ const stripeClient = new Stripe2(secret, { apiVersion: STRIPE_API_VERSION });
38762
41016
  const customer = await stripeClient.customers.create({
38763
41017
  email,
38764
41018
  description: "MCP Scraper free account",
@@ -38909,9 +41163,9 @@ app.post("/account/delete", requireAllowedOrigin, sessionAuth, async (c) => {
38909
41163
  return c.json({ error: "Could not remove every connected account \u2014 please retry before deleting the account." }, 502);
38910
41164
  }
38911
41165
  }
38912
- const secret2 = requireStripeSecret();
38913
- if (secret2 && user.stripe_customer_id) {
38914
- const stripeClient = new Stripe2(secret2, { apiVersion: STRIPE_API_VERSION });
41166
+ const secret = requireStripeSecret();
41167
+ if (secret && user.stripe_customer_id) {
41168
+ const stripeClient = new Stripe2(secret, { apiVersion: STRIPE_API_VERSION });
38915
41169
  const subs = await stripeClient.subscriptions.list({ customer: user.stripe_customer_id, status: "all", limit: 100 });
38916
41170
  for (const sub of subs.data) {
38917
41171
  if (sub.status === "active" || sub.status === "trialing" || sub.status === "past_due") {
@@ -39048,7 +41302,7 @@ function maybeProvisionInBackground(user, vaultCount) {
39048
41302
  }
39049
41303
  })();
39050
41304
  }
39051
- app.get("/memory/overview", auth3, async (c) => {
41305
+ app.get("/memory/overview", auth4, async (c) => {
39052
41306
  const user = c.get("user");
39053
41307
  const identity = resolveMemoryIdentity(user);
39054
41308
  const plan = memoryPlanForReads(user);
@@ -39076,7 +41330,7 @@ app.get("/memory/overview", auth3, async (c) => {
39076
41330
  return c.json({ ok: false, error: message }, 502);
39077
41331
  }
39078
41332
  });
39079
- app.get("/memory/vaults", auth3, async (c) => {
41333
+ app.get("/memory/vaults", auth4, async (c) => {
39080
41334
  const user = c.get("user");
39081
41335
  try {
39082
41336
  const vaults = await dbListVaults(resolveMemoryIdentity(user));
@@ -39086,7 +41340,7 @@ app.get("/memory/vaults", auth3, async (c) => {
39086
41340
  return c.json({ ok: false, error: err instanceof Error ? err.message : "could not load vaults" }, 502);
39087
41341
  }
39088
41342
  });
39089
- app.post("/memory/mcp-call", auth3, async (c) => {
41343
+ app.post("/memory/mcp-call", auth4, async (c) => {
39090
41344
  const user = c.get("user");
39091
41345
  const body = await c.req.json().catch(() => ({}));
39092
41346
  if (!body.toolName) return c.json({ ok: false, error: "toolName is required" }, 400);
@@ -39107,13 +41361,13 @@ app.post("/memory/mcp-call", auth3, async (c) => {
39107
41361
  const result = await memoryCall(body.toolName, body.args ?? {}, key);
39108
41362
  return c.json(result);
39109
41363
  });
39110
- app.get("/memory/approved-senders", auth3, async (c) => {
41364
+ app.get("/memory/approved-senders", auth4, async (c) => {
39111
41365
  const { key, error } = await getOrCreateUserMemoryKey(c.get("user"));
39112
41366
  if (!key) return c.json({ ok: false, error: error ?? "memory unavailable" }, 502);
39113
41367
  const r = await memoryCall("listApprovedSendersTool", {}, key);
39114
41368
  return c.json(r);
39115
41369
  });
39116
- app.post("/memory/approved-senders", auth3, async (c) => {
41370
+ app.post("/memory/approved-senders", auth4, async (c) => {
39117
41371
  const { key, error } = await getOrCreateUserMemoryKey(c.get("user"));
39118
41372
  if (!key) return c.json({ ok: false, error: error ?? "memory unavailable" }, 502);
39119
41373
  const body = await c.req.json().catch(() => ({}));
@@ -39122,14 +41376,14 @@ app.post("/memory/approved-senders", auth3, async (c) => {
39122
41376
  const r = await memoryCall("approveSenderTool", { senderIdentity }, key);
39123
41377
  return c.json(r);
39124
41378
  });
39125
- app.delete("/memory/approved-senders/:identity", auth3, async (c) => {
41379
+ app.delete("/memory/approved-senders/:identity", auth4, async (c) => {
39126
41380
  const { key, error } = await getOrCreateUserMemoryKey(c.get("user"));
39127
41381
  if (!key) return c.json({ ok: false, error: error ?? "memory unavailable" }, 502);
39128
41382
  const senderIdentity = c.req.param("identity");
39129
41383
  const r = await memoryCall("removeApprovedSenderTool", { senderIdentity }, key);
39130
41384
  return c.json(r);
39131
41385
  });
39132
- app.get("/memory/connect", auth3, async (c) => {
41386
+ app.get("/memory/connect", auth4, async (c) => {
39133
41387
  const { key, error } = await getOrCreateUserMemoryKey(c.get("user"));
39134
41388
  if (!key) return c.json({ error: error ?? "memory unavailable" }, 502);
39135
41389
  const url = "https://memory.mcpscraper.dev/mcp";
@@ -39144,7 +41398,7 @@ app.get("/memory/connect", auth3, async (c) => {
39144
41398
  const codeCommand = `claude mcp add --transport http memory ${url} --header "Authorization: Bearer ${key}"`;
39145
41399
  return c.json({ ok: true, url, key, config, codeCommand });
39146
41400
  });
39147
- app.post("/memory/vault", auth3, async (c) => {
41401
+ app.post("/memory/vault", auth4, async (c) => {
39148
41402
  const { key, error } = await getOrCreateUserMemoryKey(c.get("user"));
39149
41403
  if (!key) return c.json({ error: error ?? "memory unavailable" }, 502);
39150
41404
  const body = await c.req.json().catch(() => ({}));
@@ -39153,7 +41407,7 @@ app.post("/memory/vault", auth3, async (c) => {
39153
41407
  const result = await memoryCall("addVaultTool", { vault }, key);
39154
41408
  return c.json(result, result.ok ? 200 : 502);
39155
41409
  });
39156
- app.get("/memory/notes", auth3, async (c) => {
41410
+ app.get("/memory/notes", auth4, async (c) => {
39157
41411
  const user = c.get("user");
39158
41412
  const identity = resolveMemoryIdentity(user);
39159
41413
  try {
@@ -39169,7 +41423,7 @@ app.get("/memory/notes", auth3, async (c) => {
39169
41423
  return c.json({ ok: false, error: err instanceof Error ? err.message : "could not load notes" }, 502);
39170
41424
  }
39171
41425
  });
39172
- app.get("/memory/note", auth3, async (c) => {
41426
+ app.get("/memory/note", auth4, async (c) => {
39173
41427
  const user = c.get("user");
39174
41428
  const vault = c.req.query("vault")?.trim();
39175
41429
  const path5 = c.req.query("path");
@@ -39183,7 +41437,7 @@ app.get("/memory/note", auth3, async (c) => {
39183
41437
  return c.json({ ok: false, error: err instanceof Error ? err.message : "could not load note" }, 502);
39184
41438
  }
39185
41439
  });
39186
- app.get("/memory/universe", auth3, async (c) => {
41440
+ app.get("/memory/universe", auth4, async (c) => {
39187
41441
  const user = c.get("user");
39188
41442
  const includeConnections = c.req.query("includeConnections") === "1";
39189
41443
  try {
@@ -39193,7 +41447,7 @@ app.get("/memory/universe", auth3, async (c) => {
39193
41447
  return c.json({ ok: false, error: err instanceof Error ? err.message : "could not build universe" }, 502);
39194
41448
  }
39195
41449
  });
39196
- app.put("/memory/note", auth3, async (c) => {
41450
+ app.put("/memory/note", auth4, async (c) => {
39197
41451
  const { key, error } = await getOrCreateUserMemoryKey(c.get("user"));
39198
41452
  if (!key) return c.json({ error: error ?? "memory unavailable" }, 502);
39199
41453
  const body = await c.req.json().catch(() => ({}));
@@ -39201,7 +41455,7 @@ app.put("/memory/note", auth3, async (c) => {
39201
41455
  const result = await memoryCall("putTool", { vault: body.vault, path: body.path, title: body.title, content: body.content }, key);
39202
41456
  return c.json(result, result.ok ? 200 : 502);
39203
41457
  });
39204
- app.delete("/memory/note", auth3, async (c) => {
41458
+ app.delete("/memory/note", auth4, async (c) => {
39205
41459
  const { key, error } = await getOrCreateUserMemoryKey(c.get("user"));
39206
41460
  if (!key) return c.json({ error: error ?? "memory unavailable" }, 502);
39207
41461
  const vault = c.req.query("vault");
@@ -39210,7 +41464,7 @@ app.delete("/memory/note", auth3, async (c) => {
39210
41464
  const result = await memoryCall("deleteNoteTool", { vault, path: path5 }, key);
39211
41465
  return c.json(result, result.ok ? 200 : 502);
39212
41466
  });
39213
- app.get("/memory/note/download", auth3, async (c) => {
41467
+ app.get("/memory/note/download", auth4, async (c) => {
39214
41468
  const user = c.get("user");
39215
41469
  const vault = c.req.query("vault")?.trim();
39216
41470
  const path5 = c.req.query("path");
@@ -39227,7 +41481,7 @@ app.get("/memory/note/download", auth3, async (c) => {
39227
41481
  }
39228
41482
  });
39229
41483
  });
39230
- app.get("/memory/storage", auth3, async (c) => {
41484
+ app.get("/memory/storage", auth4, async (c) => {
39231
41485
  const user = c.get("user");
39232
41486
  try {
39233
41487
  const result = await dbStorage(resolveMemoryIdentity(user), memoryPlanForReads(user));
@@ -39236,7 +41490,7 @@ app.get("/memory/storage", auth3, async (c) => {
39236
41490
  return c.json({ ok: false, error: err instanceof Error ? err.message : "could not load storage" }, 502);
39237
41491
  }
39238
41492
  });
39239
- app.get("/memory/billing", auth3, async (c) => {
41493
+ app.get("/memory/billing", auth4, async (c) => {
39240
41494
  const user = c.get("user");
39241
41495
  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 }));
39242
41496
  return c.json({
@@ -39247,7 +41501,7 @@ app.get("/memory/billing", auth3, async (c) => {
39247
41501
  plans
39248
41502
  });
39249
41503
  });
39250
- app.post("/memory/checkout", auth3, async (c) => {
41504
+ app.post("/memory/checkout", auth4, async (c) => {
39251
41505
  try {
39252
41506
  const user = c.get("user");
39253
41507
  const body = await c.req.json().catch(() => ({}));
@@ -39259,9 +41513,9 @@ app.post("/memory/checkout", auth3, async (c) => {
39259
41513
  error_code: "memory_already_included"
39260
41514
  }, 409);
39261
41515
  }
39262
- const secret2 = process.env.STRIPE_SECRET_KEY?.trim();
39263
- if (!secret2) return c.json({ error: "Stripe is not configured." }, 503);
39264
- const stripeClient = new Stripe2(secret2, { apiVersion: STRIPE_API_VERSION });
41516
+ const secret = process.env.STRIPE_SECRET_KEY?.trim();
41517
+ if (!secret) return c.json({ error: "Stripe is not configured." }, 503);
41518
+ const stripeClient = new Stripe2(secret, { apiVersion: STRIPE_API_VERSION });
39265
41519
  let customerId = user.stripe_customer_id;
39266
41520
  if (!customerId) {
39267
41521
  const customer = await stripeClient.customers.create({ email: user.email });
@@ -39290,13 +41544,13 @@ app.post("/memory/checkout", auth3, async (c) => {
39290
41544
  return c.json({ error: message }, 500);
39291
41545
  }
39292
41546
  });
39293
- app.post("/memory/portal", auth3, async (c) => {
41547
+ app.post("/memory/portal", auth4, async (c) => {
39294
41548
  try {
39295
41549
  const user = c.get("user");
39296
41550
  if (!user.stripe_customer_id) return c.json({ error: "No billing account yet \u2014 subscribe first." }, 409);
39297
- const secret2 = process.env.STRIPE_SECRET_KEY?.trim();
39298
- if (!secret2) return c.json({ error: "Stripe is not configured." }, 503);
39299
- const stripeClient = new Stripe2(secret2, { apiVersion: STRIPE_API_VERSION });
41551
+ const secret = process.env.STRIPE_SECRET_KEY?.trim();
41552
+ if (!secret) return c.json({ error: "Stripe is not configured." }, 503);
41553
+ const stripeClient = new Stripe2(secret, { apiVersion: STRIPE_API_VERSION });
39300
41554
  const session = await stripeClient.billingPortal.sessions.create({
39301
41555
  customer: user.stripe_customer_id,
39302
41556
  return_url: `${appOrigin()}/memory`
@@ -39308,7 +41562,7 @@ app.post("/memory/portal", auth3, async (c) => {
39308
41562
  return c.json({ error: message }, 500);
39309
41563
  }
39310
41564
  });
39311
- app.post("/schedule-checkout", auth3, async (c) => {
41565
+ app.post("/schedule-checkout", auth4, async (c) => {
39312
41566
  return c.json({
39313
41567
  error_code: "scheduled_actions_now_credit_metered",
39314
41568
  error: "Scheduled Actions is included and no longer requires a separate subscription.",
@@ -39468,7 +41722,7 @@ function renderResendOAuthResult(ok) {
39468
41722
  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.";
39469
41723
  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>`;
39470
41724
  }
39471
- app.get("/schedule-status", auth3, requirePaidSchedulingTier, async (c) => {
41725
+ app.get("/schedule-status", auth4, requirePaidSchedulingTier, async (c) => {
39472
41726
  try {
39473
41727
  const user = c.get("user");
39474
41728
  const { key, error } = await getOrCreateUserMemoryKey(user);
@@ -39488,14 +41742,14 @@ app.get("/schedule-status", auth3, requirePaidSchedulingTier, async (c) => {
39488
41742
  return c.json({ error: message }, 500);
39489
41743
  }
39490
41744
  });
39491
- app.get("/schedule-connection-catalog", auth3, async (c) => {
41745
+ app.get("/schedule-connection-catalog", auth4, async (c) => {
39492
41746
  try {
39493
41747
  return c.json({ ok: true, catalog: await combinedConnectionCatalog() });
39494
41748
  } catch (err) {
39495
41749
  return scheduleConnectionError(c, err, "Unable to load the service connection catalog.");
39496
41750
  }
39497
41751
  });
39498
- app.get("/schedule-connections", auth3, async (c) => {
41752
+ app.get("/schedule-connections", auth4, async (c) => {
39499
41753
  try {
39500
41754
  const user = c.get("user");
39501
41755
  const connections = await combinedConnections(user.email);
@@ -39511,7 +41765,7 @@ app.get("/schedule-connections", auth3, async (c) => {
39511
41765
  return scheduleConnectionError(c, err, "Unable to load service connections.");
39512
41766
  }
39513
41767
  });
39514
- app.post("/schedule-connections/billing/reconcile", auth3, requireIntegrationsTier, async (c) => {
41768
+ app.post("/schedule-connections/billing/reconcile", auth4, requireIntegrationsTier, async (c) => {
39515
41769
  const user = c.get("user");
39516
41770
  try {
39517
41771
  return c.json({
@@ -39522,7 +41776,7 @@ app.post("/schedule-connections/billing/reconcile", auth3, requireIntegrationsTi
39522
41776
  return scheduleConnectionError(c, err, "Unable to synchronize connected-account billing.");
39523
41777
  }
39524
41778
  });
39525
- app.post("/schedule-connections/session", auth3, requireIntegrationsTier, async (c) => {
41779
+ app.post("/schedule-connections/session", auth4, requireIntegrationsTier, async (c) => {
39526
41780
  const user = c.get("user");
39527
41781
  const body = await c.req.json().catch(() => ({}));
39528
41782
  const providerConfigKey = providerConfigKeyFrom(body.providerConfigKey);
@@ -39537,7 +41791,7 @@ app.post("/schedule-connections/session", auth3, requireIntegrationsTier, async
39537
41791
  return scheduleConnectionError(c, err, "Unable to start the service connection flow.");
39538
41792
  }
39539
41793
  });
39540
- app.post("/schedule-connections/:id/reconnect-session", auth3, requireIntegrationsTier, async (c) => {
41794
+ app.post("/schedule-connections/:id/reconnect-session", auth4, requireIntegrationsTier, async (c) => {
39541
41795
  const user = c.get("user");
39542
41796
  const body = await c.req.json().catch(() => ({}));
39543
41797
  const providerConfigKey = providerConfigKeyFrom(body.providerConfigKey);
@@ -39548,7 +41802,7 @@ app.post("/schedule-connections/:id/reconnect-session", auth3, requireIntegratio
39548
41802
  return scheduleConnectionError(c, err, "Unable to start the service reconnection flow.");
39549
41803
  }
39550
41804
  });
39551
- app.delete("/schedule-connections/:id", auth3, async (c) => {
41805
+ app.delete("/schedule-connections/:id", auth4, async (c) => {
39552
41806
  const user = c.get("user");
39553
41807
  const body = await c.req.json().catch(() => ({}));
39554
41808
  const providerConfigKey = providerConfigKeyFrom(body.providerConfigKey);
@@ -39574,7 +41828,7 @@ app.delete("/schedule-connections/:id", auth3, async (c) => {
39574
41828
  return scheduleConnectionError(c, err, "Unable to disconnect this service connection.");
39575
41829
  }
39576
41830
  });
39577
- app.post("/schedule-connections/:id/enable-actions", auth3, requireIntegrationsTier, async (c) => {
41831
+ app.post("/schedule-connections/:id/enable-actions", auth4, requireIntegrationsTier, async (c) => {
39578
41832
  const user = c.get("user");
39579
41833
  const body = await c.req.json().catch(() => ({}));
39580
41834
  if (typeof body.enabled !== "boolean") return c.json({ ok: false, error: "enabled must be a boolean." }, 400);
@@ -39586,7 +41840,7 @@ app.post("/schedule-connections/:id/enable-actions", auth3, requireIntegrationsT
39586
41840
  return scheduleConnectionError(c, err, "Unable to update this connection's action setting.");
39587
41841
  }
39588
41842
  });
39589
- app.post("/schedule-connections/:id/test", auth3, requireIntegrationsTier, async (c) => {
41843
+ app.post("/schedule-connections/:id/test", auth4, requireIntegrationsTier, async (c) => {
39590
41844
  const user = c.get("user");
39591
41845
  const body = await c.req.json().catch(() => ({}));
39592
41846
  const providerConfigKey = providerConfigKeyFrom(body.providerConfigKey);
@@ -39676,7 +41930,7 @@ app.get("/oauth/resend/callback", async (c) => {
39676
41930
  return c.html(renderResendOAuthResult(false), 502);
39677
41931
  }
39678
41932
  });
39679
- app.post("/schedule-connections/actions/slack/send-message", auth3, requireIntegrationsTier, async (c) => {
41933
+ app.post("/schedule-connections/actions/slack/send-message", auth4, requireIntegrationsTier, async (c) => {
39680
41934
  const user = c.get("user");
39681
41935
  const body = await c.req.json().catch(() => ({}));
39682
41936
  const connectionId = providerConfigKeyFrom(body.connectionId);
@@ -39696,7 +41950,7 @@ app.post("/schedule-connections/actions/slack/send-message", auth3, requireInteg
39696
41950
  return scheduleConnectionError(c, err, "Unable to send the Slack message.");
39697
41951
  }
39698
41952
  });
39699
- app.post("/schedule-connections/actions/gmail/send-message", auth3, requireIntegrationsTier, async (c) => {
41953
+ app.post("/schedule-connections/actions/gmail/send-message", auth4, requireIntegrationsTier, async (c) => {
39700
41954
  const user = c.get("user");
39701
41955
  const body = await c.req.json().catch(() => ({}));
39702
41956
  const connectionId = providerConfigKeyFrom(body.connectionId);
@@ -39732,7 +41986,7 @@ function parseEmailAddress(headerValue) {
39732
41986
  const bare = value.match(/^([^\s<>]+@[^\s<>]+)$/);
39733
41987
  return bare ? { email: bare[1].trim().toLowerCase(), name: null } : null;
39734
41988
  }
39735
- app.post("/schedule-connections/actions/gmail/search-contacts", auth3, requireIntegrationsTier, async (c) => {
41989
+ app.post("/schedule-connections/actions/gmail/search-contacts", auth4, requireIntegrationsTier, async (c) => {
39736
41990
  const user = c.get("user");
39737
41991
  const body = await c.req.json().catch(() => ({}));
39738
41992
  const connectionId = providerConfigKeyFrom(body.connectionId);
@@ -39786,7 +42040,7 @@ app.post("/schedule-connections/actions/gmail/search-contacts", auth3, requireIn
39786
42040
  return scheduleConnectionError(c, err, "Unable to search Gmail contacts.");
39787
42041
  }
39788
42042
  });
39789
- app.post("/schedule-connections/actions/google-calendar/create-event", auth3, requireIntegrationsTier, async (c) => {
42043
+ app.post("/schedule-connections/actions/google-calendar/create-event", auth4, requireIntegrationsTier, async (c) => {
39790
42044
  const user = c.get("user");
39791
42045
  const body = await c.req.json().catch(() => ({}));
39792
42046
  const connectionId = providerConfigKeyFrom(body.connectionId);
@@ -39816,7 +42070,7 @@ app.post("/schedule-connections/actions/google-calendar/create-event", auth3, re
39816
42070
  return scheduleConnectionError(c, err, "Unable to create the calendar event.");
39817
42071
  }
39818
42072
  });
39819
- app.post("/schedule-connections/actions/zoom/create-meeting", auth3, requireIntegrationsTier, async (c) => {
42073
+ app.post("/schedule-connections/actions/zoom/create-meeting", auth4, requireIntegrationsTier, async (c) => {
39820
42074
  const user = c.get("user");
39821
42075
  const body = await c.req.json().catch(() => ({}));
39822
42076
  const connectionId = providerConfigKeyFrom(body.connectionId);
@@ -39842,7 +42096,7 @@ app.post("/schedule-connections/actions/zoom/create-meeting", auth3, requireInte
39842
42096
  return scheduleConnectionError(c, err, "Unable to create the Zoom meeting.");
39843
42097
  }
39844
42098
  });
39845
- app.post("/schedule-connections/actions/read", auth3, requireIntegrationsTier, async (c) => {
42099
+ app.post("/schedule-connections/actions/read", auth4, requireIntegrationsTier, async (c) => {
39846
42100
  const user = c.get("user");
39847
42101
  const body = await c.req.json().catch(() => ({}));
39848
42102
  const connectionId = providerConfigKeyFrom(body.connectionId);
@@ -39859,7 +42113,7 @@ app.post("/schedule-connections/actions/read", auth3, requireIntegrationsTier, a
39859
42113
  return scheduleConnectionError(c, err, "Unable to read from this connection.");
39860
42114
  }
39861
42115
  });
39862
- app.post("/schedule-connections/actions/import-memory", auth3, requireIntegrationsTier, async (c) => {
42116
+ app.post("/schedule-connections/actions/import-memory", auth4, requireIntegrationsTier, async (c) => {
39863
42117
  const user = c.get("user");
39864
42118
  const body = await c.req.json().catch(() => ({}));
39865
42119
  const connectionId = providerConfigKeyFrom(body.connectionId);
@@ -39916,7 +42170,7 @@ app.post("/schedule-connections/actions/import-memory", auth3, requireIntegratio
39916
42170
  return scheduleConnectionError(c, err, "Unable to import this connected-service snapshot into Memory.");
39917
42171
  }
39918
42172
  });
39919
- app.post("/schedule-connections/actions/describe", auth3, requireIntegrationsTier, async (c) => {
42173
+ app.post("/schedule-connections/actions/describe", auth4, requireIntegrationsTier, async (c) => {
39920
42174
  const user = c.get("user");
39921
42175
  const body = await c.req.json().catch(() => ({}));
39922
42176
  const connectionId = providerConfigKeyFrom(body.connectionId);
@@ -39933,7 +42187,7 @@ app.post("/schedule-connections/actions/describe", auth3, requireIntegrationsTie
39933
42187
  return scheduleConnectionError(c, err, "Unable to describe this connection tool.");
39934
42188
  }
39935
42189
  });
39936
- app.post("/schedule-connections/actions/export", auth3, requireIntegrationsTier, async (c) => {
42190
+ app.post("/schedule-connections/actions/export", auth4, requireIntegrationsTier, async (c) => {
39937
42191
  const user = c.get("user");
39938
42192
  const body = await c.req.json().catch(() => ({}));
39939
42193
  const connectionId = providerConfigKeyFrom(body.connectionId);
@@ -40020,7 +42274,7 @@ app.post("/schedule-connections/actions/export", auth3, requireIntegrationsTier,
40020
42274
  return scheduleConnectionError(c, err, "Unable to export this connected service.");
40021
42275
  }
40022
42276
  });
40023
- app.post("/schedule-connections/actions/export-search-console-table", auth3, requireIntegrationsTier, async (c) => {
42277
+ app.post("/schedule-connections/actions/export-search-console-table", auth4, requireIntegrationsTier, async (c) => {
40024
42278
  const user = c.get("user");
40025
42279
  const body = await c.req.json().catch(() => ({}));
40026
42280
  try {
@@ -40045,7 +42299,7 @@ app.post("/schedule-connections/actions/export-search-console-table", auth3, req
40045
42299
  return c.json({ ok: false, error: "Unable to export the persisted Search Console table." }, 502);
40046
42300
  }
40047
42301
  });
40048
- app.post("/schedule-connections/actions/export-download", auth3, requireIntegrationsTier, async (c) => {
42302
+ app.post("/schedule-connections/actions/export-download", auth4, requireIntegrationsTier, async (c) => {
40049
42303
  const user = c.get("user");
40050
42304
  const body = await c.req.json().catch(() => ({}));
40051
42305
  const artifactId = typeof body.artifactId === "string" ? body.artifactId.trim() : "";
@@ -40062,7 +42316,7 @@ app.post("/schedule-connections/actions/export-download", auth3, requireIntegrat
40062
42316
  return c.json({ ok: false, error: "Unable to renew this artifact download." }, 502);
40063
42317
  }
40064
42318
  });
40065
- app.post("/schedule-connections/actions/call", auth3, requireIntegrationsTier, async (c) => {
42319
+ app.post("/schedule-connections/actions/call", auth4, requireIntegrationsTier, async (c) => {
40066
42320
  const user = c.get("user");
40067
42321
  const body = await c.req.json().catch(() => ({}));
40068
42322
  const connectionId = providerConfigKeyFrom(body.connectionId);
@@ -40102,7 +42356,7 @@ app.post("/integrations/actions/read", (c) => forwardIntegrationAlias(c, "/sched
40102
42356
  app.post("/integrations/actions/describe", (c) => forwardIntegrationAlias(c, "/schedule-connections/actions/describe"));
40103
42357
  app.post("/integrations/actions/call", (c) => forwardIntegrationAlias(c, "/schedule-connections/actions/call"));
40104
42358
  app.post("/integrations/actions/export", (c) => forwardIntegrationAlias(c, "/schedule-connections/actions/export"));
40105
- app.post("/schedule-link", auth3, requirePaidSchedulingTier, async (c) => {
42359
+ app.post("/schedule-link", auth4, requirePaidSchedulingTier, async (c) => {
40106
42360
  try {
40107
42361
  const user = c.get("user");
40108
42362
  const credentials = await syncScheduledActionCredentials(user);
@@ -40119,7 +42373,7 @@ app.post("/schedule-link", auth3, requirePaidSchedulingTier, async (c) => {
40119
42373
  return c.json({ error: message }, 500);
40120
42374
  }
40121
42375
  });
40122
- app.get("/schedule-actions", auth3, async (c) => {
42376
+ app.get("/schedule-actions", auth4, async (c) => {
40123
42377
  const user = c.get("user");
40124
42378
  const { key, error } = await getOrCreateUserMemoryKey(user);
40125
42379
  if (!key) return c.json({ ok: false, error: error ?? "memory unavailable" }, 503);
@@ -40146,7 +42400,7 @@ app.get("/schedule-actions", auth3, async (c) => {
40146
42400
  });
40147
42401
  }
40148
42402
  });
40149
- app.post("/schedule-actions", auth3, requirePaidSchedulingTier, async (c) => {
42403
+ app.post("/schedule-actions", auth4, requirePaidSchedulingTier, async (c) => {
40150
42404
  const user = c.get("user");
40151
42405
  const credentials = await syncScheduledActionCredentials(user);
40152
42406
  if (!credentials.ok) {
@@ -40191,7 +42445,7 @@ app.post("/schedule-actions", auth3, requirePaidSchedulingTier, async (c) => {
40191
42445
  return response;
40192
42446
  }
40193
42447
  });
40194
- app.post("/schedule-actions/propose", auth3, async (c) => {
42448
+ app.post("/schedule-actions/propose", auth4, async (c) => {
40195
42449
  const user = c.get("user");
40196
42450
  const { key, error } = await getOrCreateUserMemoryKey(user);
40197
42451
  if (!key) return c.json({ ok: false, error: error ?? "memory unavailable" }, 503);
@@ -40199,7 +42453,7 @@ app.post("/schedule-actions/propose", auth3, async (c) => {
40199
42453
  const r = await memoryCall("proposeScheduledActionTool", body, key);
40200
42454
  return c.json(r);
40201
42455
  });
40202
- app.get("/schedule-actions/:id/connections", auth3, async (c) => {
42456
+ app.get("/schedule-actions/:id/connections", auth4, async (c) => {
40203
42457
  try {
40204
42458
  const user = c.get("user");
40205
42459
  const scheduleActionId = c.req.param("id");
@@ -40209,7 +42463,7 @@ app.get("/schedule-actions/:id/connections", auth3, async (c) => {
40209
42463
  return scheduleConnectionError(c, err, "Unable to load scheduled action service connections.");
40210
42464
  }
40211
42465
  });
40212
- app.put("/schedule-actions/:id/connections", auth3, requireIntegrationsTier, async (c) => {
42466
+ app.put("/schedule-actions/:id/connections", auth4, requireIntegrationsTier, async (c) => {
40213
42467
  const user = c.get("user");
40214
42468
  const body = await c.req.json().catch(() => ({}));
40215
42469
  let connections;
@@ -40236,7 +42490,7 @@ app.put("/schedule-actions/:id/connections", auth3, requireIntegrationsTier, asy
40236
42490
  return scheduleConnectionError(c, err, "Unable to update scheduled action service connections.");
40237
42491
  }
40238
42492
  });
40239
- app.get("/schedule-actions/:id/runs", auth3, async (c) => {
42493
+ app.get("/schedule-actions/:id/runs", auth4, async (c) => {
40240
42494
  const user = c.get("user");
40241
42495
  const { key, error } = await getOrCreateUserMemoryKey(user);
40242
42496
  if (!key) return c.json({ ok: false, error: error ?? "memory unavailable" }, 503);
@@ -40245,7 +42499,7 @@ app.get("/schedule-actions/:id/runs", auth3, async (c) => {
40245
42499
  const r = await memoryCall("listScheduledActionRunsTool", { id: c.req.param("id"), limit }, key);
40246
42500
  return c.json(r);
40247
42501
  });
40248
- app.put("/schedule-actions/:id", auth3, requirePaidSchedulingTier, async (c) => {
42502
+ app.put("/schedule-actions/:id", auth4, requirePaidSchedulingTier, async (c) => {
40249
42503
  const user = c.get("user");
40250
42504
  const { key, error } = await getOrCreateUserMemoryKey(user);
40251
42505
  if (!key) return c.json({ ok: false, error: error ?? "memory unavailable" }, 503);
@@ -40260,21 +42514,21 @@ app.put("/schedule-actions/:id", auth3, requirePaidSchedulingTier, async (c) =>
40260
42514
  const r = await memoryCall("updateScheduledActionTool", { id: c.req.param("id"), ...updates }, key);
40261
42515
  return c.json(r);
40262
42516
  });
40263
- app.post("/schedule-actions/:id/pause", auth3, async (c) => {
42517
+ app.post("/schedule-actions/:id/pause", auth4, async (c) => {
40264
42518
  const user = c.get("user");
40265
42519
  const { key, error } = await getOrCreateUserMemoryKey(user);
40266
42520
  if (!key) return c.json({ ok: false, error: error ?? "memory unavailable" }, 503);
40267
42521
  const r = await memoryCall("pauseScheduledActionTool", { id: c.req.param("id") }, key);
40268
42522
  return c.json(r);
40269
42523
  });
40270
- app.post("/schedule-actions/:id/resume", auth3, requirePaidSchedulingTier, async (c) => {
42524
+ app.post("/schedule-actions/:id/resume", auth4, requirePaidSchedulingTier, async (c) => {
40271
42525
  const user = c.get("user");
40272
42526
  const { key, error } = await getOrCreateUserMemoryKey(user);
40273
42527
  if (!key) return c.json({ ok: false, error: error ?? "memory unavailable" }, 503);
40274
42528
  const r = await memoryCall("resumeScheduledActionTool", { id: c.req.param("id") }, key);
40275
42529
  return c.json(r);
40276
42530
  });
40277
- app.delete("/schedule-actions/:id", auth3, async (c) => {
42531
+ app.delete("/schedule-actions/:id", auth4, async (c) => {
40278
42532
  const user = c.get("user");
40279
42533
  const { key, error } = await getOrCreateUserMemoryKey(user);
40280
42534
  if (!key) return c.json({ ok: false, error: error ?? "memory unavailable" }, 503);
@@ -40289,7 +42543,7 @@ app.delete("/schedule-actions/:id", auth3, async (c) => {
40289
42543
  return c.json({ ...r, connectionBindingCleanupPending: true });
40290
42544
  }
40291
42545
  });
40292
- app.post("/schedule-link/revoke", auth3, async (c) => {
42546
+ app.post("/schedule-link/revoke", auth4, async (c) => {
40293
42547
  try {
40294
42548
  const user = c.get("user");
40295
42549
  const { key, error } = await getOrCreateUserMemoryKey(user);
@@ -40302,7 +42556,7 @@ app.post("/schedule-link/revoke", auth3, async (c) => {
40302
42556
  return c.json({ error: message }, 500);
40303
42557
  }
40304
42558
  });
40305
- app.post("/support/submit", auth3, async (c) => {
42559
+ app.post("/support/submit", auth4, async (c) => {
40306
42560
  try {
40307
42561
  const user = c.get("user");
40308
42562
  const body = await c.req.json().catch(() => ({}));
@@ -40330,7 +42584,7 @@ app.post("/support/submit", auth3, async (c) => {
40330
42584
  return c.json({ error: message }, 500);
40331
42585
  }
40332
42586
  });
40333
- app.get("/memory/usage", auth3, async (c) => {
42587
+ app.get("/memory/usage", auth4, async (c) => {
40334
42588
  const user = c.get("user");
40335
42589
  try {
40336
42590
  const result = await dbUsage(resolveMemoryIdentity(user), memoryPlanForReads(user));
@@ -40383,7 +42637,7 @@ async function checkHarvestLimits(user, reuseLockId) {
40383
42637
  }
40384
42638
  return null;
40385
42639
  }
40386
- app.post("/harvest", auth3, async (c) => {
42640
+ app.post("/harvest", auth4, async (c) => {
40387
42641
  const user = c.get("user");
40388
42642
  const raw = await c.req.json().catch(() => ({}));
40389
42643
  const bodyResult = HarvestBodySchema.safeParse(raw);
@@ -40466,7 +42720,7 @@ app.post("/harvest", auth3, async (c) => {
40466
42720
  }
40467
42721
  return c.json({ job_id: jobId2, status: "pending" }, 202);
40468
42722
  });
40469
- app.post("/harvest/sync", auth3, async (c) => {
42723
+ app.post("/harvest/sync", auth4, async (c) => {
40470
42724
  const user = c.get("user");
40471
42725
  const raw = await c.req.json().catch(() => ({}));
40472
42726
  const bodyResult = HarvestBodySchema.safeParse(raw);
@@ -40547,17 +42801,17 @@ app.post("/harvest/sync", auth3, async (c) => {
40547
42801
  return c.json({ job_id: jobId2, status: "failed", ...response, attempts: sanitizeAttempts(attempts) }, problem.httpStatus);
40548
42802
  }
40549
42803
  });
40550
- app.get("/jobs/:id", auth3, async (c) => {
42804
+ app.get("/jobs/:id", auth4, async (c) => {
40551
42805
  const job = await getJob(c.req.param("id"), c.get("user").id);
40552
42806
  if (!job) return c.json({ error: "Job not found" }, 404);
40553
42807
  const attempts = await listHarvestAttempts(job.id, c.get("user").id);
40554
42808
  const safeResult = job.result && typeof job.result === "object" ? sanitizeHarvestResult(job.result) : job.result;
40555
42809
  return c.json({ ...job, result: safeResult, attempts: sanitizeAttempts(attempts) });
40556
42810
  });
40557
- app.get("/jobs", auth3, async (c) => {
42811
+ app.get("/jobs", auth4, async (c) => {
40558
42812
  return c.json(await listJobs(c.get("user").id));
40559
42813
  });
40560
- app.get("/history", auth3, async (c) => {
42814
+ app.get("/history", auth4, async (c) => {
40561
42815
  const userId = c.get("user").id;
40562
42816
  const [jobs, events] = await Promise.all([
40563
42817
  listJobs(userId),
@@ -40585,7 +42839,7 @@ app.get("/history", auth3, async (c) => {
40585
42839
  const rows = [...jobRows, ...eventRows].sort((a, b) => String(b.ts).localeCompare(String(a.ts)));
40586
42840
  return c.json(rows.slice(0, 100));
40587
42841
  });
40588
- app.get("/ledger", auth3, async (c) => {
42842
+ app.get("/ledger", auth4, async (c) => {
40589
42843
  return c.json(await getLedger(c.get("user").id, 100));
40590
42844
  });
40591
42845
  app.post("/admin/users", adminAuth, async (c) => {
@@ -40614,7 +42868,7 @@ app.post("/admin/backfill-signup-credits", adminAuth, async (c) => {
40614
42868
  retired: true
40615
42869
  });
40616
42870
  });
40617
- app.post("/extract-url", auth3, async (c) => {
42871
+ app.post("/extract-url", auth4, async (c) => {
40618
42872
  const raw = await c.req.json().catch(() => ({}));
40619
42873
  const allowLocalRequest = permitsLocalApiRequest(c.req.raw);
40620
42874
  const bodyResult = (allowLocalRequest ? ExtractUrlLocalBodySchema : ExtractUrlBodySchema).safeParse(raw);
@@ -40750,7 +43004,7 @@ app.post("/extract-url", auth3, async (c) => {
40750
43004
  await releaseConcurrencyGate(gate.lockId);
40751
43005
  }
40752
43006
  });
40753
- app.post("/diff-page", auth3, async (c) => {
43007
+ app.post("/diff-page", auth4, async (c) => {
40754
43008
  const raw = await c.req.json().catch(() => ({}));
40755
43009
  const allowLocalRequest = permitsLocalApiRequest(c.req.raw);
40756
43010
  const bodyResult = (allowLocalRequest ? DiffPageLocalBodySchema : DiffPageBodySchema).safeParse(raw);
@@ -40837,7 +43091,7 @@ app.post("/diff-page", auth3, async (c) => {
40837
43091
  await releaseConcurrencyGate(gate.lockId);
40838
43092
  }
40839
43093
  });
40840
- app.post("/archive/read", auth3, async (c) => {
43094
+ app.post("/archive/read", auth4, async (c) => {
40841
43095
  const raw = await c.req.json().catch(() => ({}));
40842
43096
  const bodyResult = ArchiveReadBodySchema.safeParse(raw);
40843
43097
  if (!bodyResult.success) {
@@ -40912,7 +43166,7 @@ app.post("/archive/read", auth3, async (c) => {
40912
43166
  await releaseConcurrencyGate(gate.lockId);
40913
43167
  }
40914
43168
  });
40915
- app.post("/map-urls", auth3, async (c) => {
43169
+ app.post("/map-urls", auth4, async (c) => {
40916
43170
  const raw = await c.req.json().catch(() => ({}));
40917
43171
  const bodyResult = MapUrlsBodySchema.safeParse(raw);
40918
43172
  if (!bodyResult.success) return c.json({ error: bodyResult.error.issues[0]?.message ?? "Invalid request" }, 400);
@@ -40961,7 +43215,7 @@ app.post("/map-urls", auth3, async (c) => {
40961
43215
  await releaseConcurrencyGate(gate.lockId);
40962
43216
  }
40963
43217
  });
40964
- app.post("/wayback/snapshots", auth3, async (c) => {
43218
+ app.post("/wayback/snapshots", auth4, async (c) => {
40965
43219
  const raw = await c.req.json().catch(() => ({}));
40966
43220
  const bodyResult = WaybackInventoryBodySchema.safeParse(raw);
40967
43221
  if (!bodyResult.success) return c.json({ error: bodyResult.error.issues[0]?.message ?? "Invalid request" }, 400);
@@ -41031,7 +43285,7 @@ app.post("/wayback/snapshots", auth3, async (c) => {
41031
43285
  await releaseConcurrencyGate(gate.lockId);
41032
43286
  }
41033
43287
  });
41034
- app.post("/extract-site", auth3, async (c) => {
43288
+ app.post("/extract-site", auth4, async (c) => {
41035
43289
  const raw = await c.req.json().catch(() => ({}));
41036
43290
  const bodyResult = ExtractSiteBodySchema.safeParse(raw);
41037
43291
  if (!bodyResult.success) return c.json({ error: bodyResult.error.issues[0]?.message ?? "Invalid request" }, 400);
@@ -41254,7 +43508,7 @@ app.post("/extract-site", auth3, async (c) => {
41254
43508
  await releaseConcurrencyGate(gate.lockId);
41255
43509
  }
41256
43510
  });
41257
- app.get("/extract-site/status/:id", auth3, async (c) => {
43511
+ app.get("/extract-site/status/:id", auth4, async (c) => {
41258
43512
  const user = c.get("user");
41259
43513
  const job = await getExtractJob(c.req.param("id"));
41260
43514
  if (!job || job.userId !== user.id) return c.json({ error: "Job not found" }, 404);
@@ -41288,9 +43542,9 @@ app.post("/billing/concurrency/checkout", requireAllowedOrigin, sessionAuth, asy
41288
43542
  try {
41289
43543
  const user = c.get("sessionUser");
41290
43544
  if (user.concurrency_stripe_sub_id) return c.json({ error: "Already subscribed \u2014 cancel existing slot first to change." }, 409);
41291
- const secret2 = process.env.STRIPE_SECRET_KEY?.trim();
41292
- if (!secret2) return c.json({ error: "Stripe is not configured." }, 503);
41293
- const stripeClient = new Stripe2(secret2, { apiVersion: STRIPE_API_VERSION });
43545
+ const secret = process.env.STRIPE_SECRET_KEY?.trim();
43546
+ if (!secret) return c.json({ error: "Stripe is not configured." }, 503);
43547
+ const stripeClient = new Stripe2(secret, { apiVersion: STRIPE_API_VERSION });
41294
43548
  let customerId = user.stripe_customer_id;
41295
43549
  if (!customerId) {
41296
43550
  const customer = await stripeClient.customers.create({ email: user.email });
@@ -41352,9 +43606,9 @@ app.post("/billing/subscribe", requireAllowedOrigin, sessionAuth, async (c) => {
41352
43606
  requested_plan: getSubscriptionPlanView(tier.tier)
41353
43607
  }, 409);
41354
43608
  }
41355
- const secret2 = process.env.STRIPE_SECRET_KEY?.trim();
41356
- if (!secret2) return c.json({ error: "Stripe is not configured." }, 503);
41357
- const stripeClient = new Stripe2(secret2, { apiVersion: STRIPE_API_VERSION });
43609
+ const secret = process.env.STRIPE_SECRET_KEY?.trim();
43610
+ if (!secret) return c.json({ error: "Stripe is not configured." }, 503);
43611
+ const stripeClient = new Stripe2(secret, { apiVersion: STRIPE_API_VERSION });
41358
43612
  let customerId = user.stripe_customer_id;
41359
43613
  if (!customerId) {
41360
43614
  const customer = await stripeClient.customers.create({ email: user.email });
@@ -41415,9 +43669,9 @@ app.post("/billing/portal", requireAllowedOrigin, sessionAuth, async (c) => {
41415
43669
  try {
41416
43670
  const user = c.get("sessionUser");
41417
43671
  if (!user.stripe_customer_id) return c.json({ error: "No billing account yet \u2014 subscribe first." }, 409);
41418
- const secret2 = requireStripeSecret();
41419
- if (!secret2) return c.json({ error: "Stripe is not configured." }, 503);
41420
- const stripeClient = new Stripe2(secret2, { apiVersion: STRIPE_API_VERSION });
43672
+ const secret = requireStripeSecret();
43673
+ if (!secret) return c.json({ error: "Stripe is not configured." }, 503);
43674
+ const stripeClient = new Stripe2(secret, { apiVersion: STRIPE_API_VERSION });
41421
43675
  const session = await stripeClient.billingPortal.sessions.create({
41422
43676
  customer: user.stripe_customer_id,
41423
43677
  return_url: `${appOrigin()}/billing`
@@ -41429,7 +43683,7 @@ app.post("/billing/portal", requireAllowedOrigin, sessionAuth, async (c) => {
41429
43683
  return c.json({ error: message }, 500);
41430
43684
  }
41431
43685
  });
41432
- app.post("/billing/concurrency/terminal-checkout", auth3, async (c) => {
43686
+ app.post("/billing/concurrency/terminal-checkout", auth4, async (c) => {
41433
43687
  try {
41434
43688
  const user = c.get("user");
41435
43689
  const upgrade = concurrencySlotBillingInfo();
@@ -41445,9 +43699,9 @@ app.post("/billing/concurrency/terminal-checkout", auth3, async (c) => {
41445
43699
  }
41446
43700
  }, 409);
41447
43701
  }
41448
- const secret2 = process.env.STRIPE_SECRET_KEY?.trim();
41449
- if (!secret2) return c.json({ error: "Stripe is not configured." }, 503);
41450
- const stripeClient = new Stripe2(secret2, { apiVersion: STRIPE_API_VERSION });
43702
+ const secret = process.env.STRIPE_SECRET_KEY?.trim();
43703
+ if (!secret) return c.json({ error: "Stripe is not configured." }, 503);
43704
+ const stripeClient = new Stripe2(secret, { apiVersion: STRIPE_API_VERSION });
41451
43705
  let customerId = user.stripe_customer_id;
41452
43706
  if (!customerId) {
41453
43707
  const customer = await stripeClient.customers.create({ email: user.email });
@@ -41483,7 +43737,7 @@ app.post("/billing/concurrency/terminal-checkout", auth3, async (c) => {
41483
43737
  return c.json({ error: message }, 500);
41484
43738
  }
41485
43739
  });
41486
- app.post("/billing/subscribe/terminal-checkout", auth3, async (c) => {
43740
+ app.post("/billing/subscribe/terminal-checkout", auth4, async (c) => {
41487
43741
  try {
41488
43742
  const user = c.get("user");
41489
43743
  const raw = await c.req.json().catch(() => ({}));
@@ -41499,9 +43753,9 @@ app.post("/billing/subscribe/terminal-checkout", auth3, async (c) => {
41499
43753
  requested_plan: getSubscriptionPlanView(tier.tier)
41500
43754
  }, 409);
41501
43755
  }
41502
- const secret2 = process.env.STRIPE_SECRET_KEY?.trim();
41503
- if (!secret2) return c.json({ error: "Stripe is not configured." }, 503);
41504
- const stripeClient = new Stripe2(secret2, { apiVersion: STRIPE_API_VERSION });
43756
+ const secret = process.env.STRIPE_SECRET_KEY?.trim();
43757
+ if (!secret) return c.json({ error: "Stripe is not configured." }, 503);
43758
+ const stripeClient = new Stripe2(secret, { apiVersion: STRIPE_API_VERSION });
41505
43759
  let customerId = user.stripe_customer_id;
41506
43760
  if (!customerId) {
41507
43761
  const customer = await stripeClient.customers.create({ email: user.email });
@@ -41585,7 +43839,7 @@ app.post("/billing/concurrency/cancel", requireAllowedOrigin, sessionAuth, async
41585
43839
  await setConcurrencySubId(user.id, null);
41586
43840
  return c.json({ ok: true, extra_concurrency_slots: nextSlots, concurrency_limit: 1 + nextSlots });
41587
43841
  });
41588
- app.get("/billing/balance", auth3, async (c) => {
43842
+ app.get("/billing/balance", auth4, async (c) => {
41589
43843
  const user = c.get("user");
41590
43844
  const balanceMc = user.balance_mc;
41591
43845
  const ledger = await getLedger(user.id, 20);
@@ -41597,15 +43851,15 @@ app.get("/billing/balance", auth3, async (c) => {
41597
43851
  ledger
41598
43852
  });
41599
43853
  });
41600
- app.get("/billing/summary", auth3, billingSummaryHandler);
41601
- app.get("/billing/connected-usage/history", auth3, async (c) => {
43854
+ app.get("/billing/summary", auth4, billingSummaryHandler);
43855
+ app.get("/billing/connected-usage/history", auth4, async (c) => {
41602
43856
  return c.json({
41603
43857
  ok: true,
41604
43858
  ratePolicy: CONNECTED_USAGE_RATE_POLICY,
41605
43859
  receipts: await listConnectedUsageHistory(c.get("user").id, 100)
41606
43860
  });
41607
43861
  });
41608
- app.post("/billing/credits", auth3, async (c) => {
43862
+ app.post("/billing/credits", auth4, async (c) => {
41609
43863
  const user = c.get("user");
41610
43864
  const balanceMc = user.balance_mc;
41611
43865
  const body = await c.req.json().catch(() => ({}));
@@ -41637,14 +43891,14 @@ app.post("/billing/credits", auth3, async (c) => {
41637
43891
  });
41638
43892
  });
41639
43893
  app.get("/cron/tick", async (c) => {
41640
- const secret2 = c.req.header("authorization");
41641
- if (!process.env.CRON_SECRET || secret2 !== `Bearer ${process.env.CRON_SECRET}`) {
43894
+ const secret = c.req.header("authorization");
43895
+ if (!process.env.CRON_SECRET || secret !== `Bearer ${process.env.CRON_SECRET}`) {
41642
43896
  return c.json({ error: "Unauthorized" }, 401);
41643
43897
  }
41644
- const { drainQueue } = await import("./worker-3QX7LG62.js");
43898
+ const { drainQueue } = await import("./worker-4MHZS3OB.js");
41645
43899
  const budget = { maxJobs: 10, deadlineMs: Date.now() + 28e4 };
41646
43900
  const workflowDispatchResult = await dispatchDueWorkflowSchedules(`${new URL(c.req.url).protocol}//${new URL(c.req.url).host}`);
41647
- const [results, sweepResult, reapResult, expiredResult, blobCleanup, connectedDataArtifactCleanup, connectedAccountBilling, directoryWorkflowDispatch, directoryWorkflowReconciliation, directoryArtifactCleanup, siteExtractArtifactCleanup, siteExtractRedispatch, siteExtractReconciliation, paaHarvestRedispatch, paaHarvestReconciliation, localSourcebookRedispatch] = await Promise.all([
43901
+ const [results, sweepResult, reapResult, expiredResult, blobCleanup, connectedDataArtifactCleanup, connectedAccountBilling, directoryWorkflowDispatch, directoryWorkflowReconciliation, directoryArtifactCleanup, siteExtractArtifactCleanup, siteExtractRedispatch, siteExtractReconciliation, paaHarvestRedispatch, paaHarvestReconciliation, localSourcebookRedispatch, analyticsRollup, analyticsFormDelivery] = await Promise.all([
41648
43902
  drainQueue(budget),
41649
43903
  runMonthlyRefreshSweep(),
41650
43904
  reapIdleBrowserSessions(120).catch(() => ({ reaped: 0 })),
@@ -41660,7 +43914,9 @@ app.get("/cron/tick", async (c) => {
41660
43914
  reconcileSiteExtractSettlements(25).catch(() => ({ checked: 0, settled: 0, failed: 0 })),
41661
43915
  redispatchPendingPaaHarvests(25).catch(() => ({ checked: 0, dispatched: 0, failed: 0 })),
41662
43916
  reconcilePaaHarvestSettlements(25).catch(() => ({ checked: 0, staleFailed: 0, settled: 0, failed: 0 })),
41663
- redispatchQueuedLocalSourcebookAcquisitions(25).catch(() => ({ checked: 0, dispatched: 0, failed: 0 }))
43917
+ redispatchQueuedLocalSourcebookAcquisitions(25).catch(() => ({ checked: 0, dispatched: 0, failed: 0 })),
43918
+ refreshAnalyticsDailyRollupsIfDue().catch(() => ({ status: "failed", rows: 0 })),
43919
+ deliverPendingAnalyticsForms(10).catch(() => ({ claimed: 0, delivered: 0, deferred: 0 }))
41664
43920
  ]);
41665
43921
  return c.json({
41666
43922
  drained: results.length,
@@ -41680,12 +43936,14 @@ app.get("/cron/tick", async (c) => {
41680
43936
  paaHarvestRedispatch,
41681
43937
  paaHarvestReconciliation,
41682
43938
  localSourcebookRedispatch,
43939
+ analyticsRollup,
43940
+ analyticsFormDelivery,
41683
43941
  workflowDispatch: workflowDispatchResult
41684
43942
  });
41685
43943
  });
41686
43944
  app.post("/api/internal/extract-refinalize/:id", async (c) => {
41687
- const secret2 = c.req.header("authorization");
41688
- if (!process.env.CRON_SECRET || secret2 !== `Bearer ${process.env.CRON_SECRET}`) {
43945
+ const secret = c.req.header("authorization");
43946
+ if (!process.env.CRON_SECRET || secret !== `Bearer ${process.env.CRON_SECRET}`) {
41689
43947
  return c.json({ error: "Unauthorized" }, 401);
41690
43948
  }
41691
43949
  const jobId2 = c.req.param("id");
@@ -41696,8 +43954,8 @@ app.post("/api/internal/extract-refinalize/:id", async (c) => {
41696
43954
  finishExtractJob: finishExtractJob2,
41697
43955
  terminalExtractJobStatus: terminalExtractJobStatus2,
41698
43956
  extractJobLimitInfo: extractJobLimitInfo2
41699
- } = await import("./site-extract-repository-UULQAGO4.js");
41700
- const { assembleExtractArtifacts } = await import("./extract-bundle-DXWWQER6.js");
43957
+ } = await import("./site-extract-repository-STNOI5OH.js");
43958
+ const { assembleExtractArtifacts } = await import("./extract-bundle-2HONVC5T.js");
41701
43959
  const existing = await getExtractJob2(jobId2);
41702
43960
  if (!existing) return c.json({ error: "job not found" }, 404);
41703
43961
  if (existing.status !== "failed") {
@@ -41724,7 +43982,7 @@ app.post("/api/internal/extract-refinalize/:id", async (c) => {
41724
43982
  if (!updated) return c.json({ error: "job state changed during re-finalization" }, 409);
41725
43983
  let settlement = "already_settled_or_refunded";
41726
43984
  if (job.billedMc == null && job.userId != null) {
41727
- const { settleExtractJob: settleExtractJob3, countSuccessfulPages: countSuccessfulPages2 } = await import("./site-extract-repository-UULQAGO4.js");
43985
+ const { settleExtractJob: settleExtractJob3, countSuccessfulPages: countSuccessfulPages2 } = await import("./site-extract-repository-STNOI5OH.js");
41728
43986
  const heldMc = Number(job.options.heldMc ?? 0);
41729
43987
  const successful = await countSuccessfulPages2(jobId2);
41730
43988
  const usedMc = Math.min(successful * MC_COSTS.page_scrape, heldMc);
@@ -41775,6 +44033,7 @@ app.route("/resend", resendInboundApp);
41775
44033
  app.route("/workshops", workshopRegistrationApp);
41776
44034
  app.route("/editorial-reading-room", editorialReadingRoomApp);
41777
44035
  app.route("/commons", commonsApp);
44036
+ app.route("/analytics", analyticsApp);
41778
44037
  app.route("/", scheduledResultApp);
41779
44038
  app.route("/public", publicScheduledResultApp);
41780
44039
  app.route("/api/internal/scheduled-artifacts", scheduledArtifactInternalApp);
@@ -41908,4 +44167,4 @@ app.get("/blog/:slug/", (c) => {
41908
44167
  export {
41909
44168
  app
41910
44169
  };
41911
- //# sourceMappingURL=server-OHMZJRSB.js.map
44170
+ //# sourceMappingURL=server-JE7SLZLJ.js.map