mcp-scraper 0.47.0 → 0.48.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/analytics-repository-WWXVU3VW.js +119 -0
- package/dist/bin/api-server.cjs +7429 -1887
- package/dist/bin/api-server.cjs.map +1 -1
- package/dist/bin/api-server.js +8 -4
- package/dist/bin/api-server.js.map +1 -1
- package/dist/bin/mcp-scraper-cli.cjs +1 -1
- package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
- package/dist/bin/mcp-scraper-cli.js +1 -1
- package/dist/bin/mcp-scraper-install.cjs +2 -2
- package/dist/bin/mcp-scraper-install.cjs.map +1 -1
- package/dist/bin/mcp-scraper-install.js +2 -2
- package/dist/bin/mcp-stdio-server.cjs +1812 -1028
- package/dist/bin/mcp-stdio-server.cjs.map +1 -1
- package/dist/bin/mcp-stdio-server.js +5 -5
- package/dist/bin/paa-harvest.cjs.map +1 -1
- package/dist/bin/paa-harvest.js +3 -3
- package/dist/{chunk-MW3ET532.js → chunk-6DPIE262.js} +2 -2
- package/dist/chunk-IAXQIHNS.js +7 -0
- package/dist/chunk-IAXQIHNS.js.map +1 -0
- package/dist/{chunk-JH7A4HA7.js → chunk-MFE6RMIA.js} +2 -2
- package/dist/{chunk-5S6LYQQO.js → chunk-MFNGUM4L.js} +2 -2
- package/dist/chunk-MFNGUM4L.js.map +1 -0
- package/dist/{chunk-IWYYZXBR.js → chunk-PODASLGT.js} +2 -2
- package/dist/{chunk-XCGQBGDA.js → chunk-PTABZABJ.js} +2081 -1262
- package/dist/chunk-PTABZABJ.js.map +1 -0
- package/dist/{chunk-QSUGPYGQ.js → chunk-QAH7WMYC.js} +2 -2
- package/dist/{chunk-QSUGPYGQ.js.map → chunk-QAH7WMYC.js.map} +1 -1
- package/dist/{chunk-TV6KPZ6J.js → chunk-R66PJOZW.js} +2 -2
- package/dist/chunk-YW2LXLDU.js +2865 -0
- package/dist/chunk-YW2LXLDU.js.map +1 -0
- package/dist/{chunk-GV5EGE5M.js → chunk-ZEMHL36J.js} +2 -2
- package/dist/{db-W3RPRILW.js → db-A4YUPV4Z.js} +2 -2
- package/dist/{extract-bundle-DXWWQER6.js → extract-bundle-2HONVC5T.js} +3 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +3 -3
- package/dist/{location-data-repository-2FJVMTZ2.js → location-data-repository-YWFJGNIP.js} +3 -3
- package/dist/{server-OHMZJRSB.js → server-EVTCL3KB.js} +2124 -269
- package/dist/server-EVTCL3KB.js.map +1 -0
- package/dist/{site-extract-repository-UULQAGO4.js → site-extract-repository-STNOI5OH.js} +3 -3
- package/dist/site-extract-repository-STNOI5OH.js.map +1 -0
- package/dist/{worker-3QX7LG62.js → worker-4MHZS3OB.js} +5 -5
- package/package.json +3 -1
- package/dist/chunk-5S6LYQQO.js.map +0 -1
- package/dist/chunk-H4NWAF5B.js +0 -7
- package/dist/chunk-H4NWAF5B.js.map +0 -1
- package/dist/chunk-XCGQBGDA.js.map +0 -1
- package/dist/server-OHMZJRSB.js.map +0 -1
- /package/dist/{db-W3RPRILW.js.map → analytics-repository-WWXVU3VW.js.map} +0 -0
- /package/dist/{chunk-MW3ET532.js.map → chunk-6DPIE262.js.map} +0 -0
- /package/dist/{chunk-JH7A4HA7.js.map → chunk-MFE6RMIA.js.map} +0 -0
- /package/dist/{chunk-IWYYZXBR.js.map → chunk-PODASLGT.js.map} +0 -0
- /package/dist/{chunk-TV6KPZ6J.js.map → chunk-R66PJOZW.js.map} +0 -0
- /package/dist/{chunk-GV5EGE5M.js.map → chunk-ZEMHL36J.js.map} +0 -0
- /package/dist/{location-data-repository-2FJVMTZ2.js.map → db-A4YUPV4Z.js.map} +0 -0
- /package/dist/{extract-bundle-DXWWQER6.js.map → extract-bundle-2HONVC5T.js.map} +0 -0
- /package/dist/{site-extract-repository-UULQAGO4.js.map → location-data-repository-YWFJGNIP.js.map} +0 -0
- /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-
|
|
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-
|
|
74
|
+
} from "./chunk-ZEMHL36J.js";
|
|
21
75
|
import {
|
|
22
76
|
AIModeSelectors,
|
|
23
77
|
AIOverviewSelectors,
|
|
@@ -44,9 +98,10 @@ import {
|
|
|
44
98
|
resolveKernelProxyId,
|
|
45
99
|
serpArrivalUrl,
|
|
46
100
|
withRuntimeUserAgent
|
|
47
|
-
} from "./chunk-
|
|
101
|
+
} from "./chunk-MFE6RMIA.js";
|
|
48
102
|
import {
|
|
49
103
|
ApiErrorResponseSchema,
|
|
104
|
+
ArtifactTemplatePresetKeySchema,
|
|
50
105
|
CommonsClaimPublicationInputSchema,
|
|
51
106
|
CommonsPreparePublicationInputSchema,
|
|
52
107
|
CommonsPublishEditorialInputSchema,
|
|
@@ -62,6 +117,9 @@ import {
|
|
|
62
117
|
InternalScheduledArtifactRenderRequestSchema,
|
|
63
118
|
ListScheduledRunsQuerySchema,
|
|
64
119
|
LocalSourcebookSchemaTypeSchema,
|
|
120
|
+
NEWSROOM_PUBLISHER_TEMPLATE_PRESET,
|
|
121
|
+
NewsroomPublisherTemplateConfigSchema,
|
|
122
|
+
NewsroomPublisherV1InputSchema,
|
|
65
123
|
PERSONAL_AUTHORITY_TEMPLATE_PRESET,
|
|
66
124
|
PersonalAuthorityTemplateConfigSchema,
|
|
67
125
|
PersonalAuthorityV1InputSchema,
|
|
@@ -97,7 +155,7 @@ import {
|
|
|
97
155
|
resolveDeploymentProfile,
|
|
98
156
|
resolveLocalSourcebookSchemaType,
|
|
99
157
|
transcribeMediaUrl
|
|
100
|
-
} from "./chunk-
|
|
158
|
+
} from "./chunk-PTABZABJ.js";
|
|
101
159
|
import {
|
|
102
160
|
auditImageUrls,
|
|
103
161
|
auditImages,
|
|
@@ -125,7 +183,7 @@ import {
|
|
|
125
183
|
recordKernelSession,
|
|
126
184
|
recordVendorUsage,
|
|
127
185
|
runWithCostContext
|
|
128
|
-
} from "./chunk-
|
|
186
|
+
} from "./chunk-PODASLGT.js";
|
|
129
187
|
import {
|
|
130
188
|
listWorkflowDefinitions,
|
|
131
189
|
runWorkflow,
|
|
@@ -153,7 +211,7 @@ import {
|
|
|
153
211
|
} from "./chunk-3ZUBQQPQ.js";
|
|
154
212
|
import {
|
|
155
213
|
PACKAGE_VERSION
|
|
156
|
-
} from "./chunk-
|
|
214
|
+
} from "./chunk-IAXQIHNS.js";
|
|
157
215
|
import {
|
|
158
216
|
abandonExtractSettlement,
|
|
159
217
|
countSuccessfulPages,
|
|
@@ -176,7 +234,7 @@ import {
|
|
|
176
234
|
setExtractJobTotal,
|
|
177
235
|
settleExtractJob,
|
|
178
236
|
terminalExtractJobStatus
|
|
179
|
-
} from "./chunk-
|
|
237
|
+
} from "./chunk-6DPIE262.js";
|
|
180
238
|
import {
|
|
181
239
|
BROWSER_OPEN_MIN_BALANCE_MC,
|
|
182
240
|
CONCURRENCY_PRICE_ID,
|
|
@@ -333,7 +391,7 @@ import {
|
|
|
333
391
|
startHarvestAttempt,
|
|
334
392
|
upsertPageSnapshot,
|
|
335
393
|
verifyPassword
|
|
336
|
-
} from "./chunk-
|
|
394
|
+
} from "./chunk-MFNGUM4L.js";
|
|
337
395
|
|
|
338
396
|
// src/blog/registry.ts
|
|
339
397
|
var posts = [
|
|
@@ -6271,7 +6329,7 @@ async function extractSite(opts) {
|
|
|
6271
6329
|
}
|
|
6272
6330
|
|
|
6273
6331
|
// src/api/server.ts
|
|
6274
|
-
import { Hono as
|
|
6332
|
+
import { Hono as Hono36 } from "hono";
|
|
6275
6333
|
import { serve as serveInngest } from "inngest/hono";
|
|
6276
6334
|
|
|
6277
6335
|
// src/inngest/client.ts
|
|
@@ -8679,7 +8737,7 @@ var siteExtractFn = inngest.createFunction(
|
|
|
8679
8737
|
});
|
|
8680
8738
|
}) : null;
|
|
8681
8739
|
const finalized = await step.run("finalize", async () => {
|
|
8682
|
-
const { assembleExtractArtifacts } = await import("./extract-bundle-
|
|
8740
|
+
const { assembleExtractArtifacts } = await import("./extract-bundle-2HONVC5T.js");
|
|
8683
8741
|
const current = await getExtractJob(jobId2);
|
|
8684
8742
|
if (!current) throw new Error("extract job disappeared before finalization");
|
|
8685
8743
|
const stored = await assembleExtractArtifacts(current, { branding, imageAudit });
|
|
@@ -9621,7 +9679,7 @@ async function resolveUsZipsPath(requestedPath) {
|
|
|
9621
9679
|
async function loadZipGroups(stateAbbr, requestedPath, warnings, sourceMode) {
|
|
9622
9680
|
const mode = resolvedLocationDataMode({ zipSource: sourceMode });
|
|
9623
9681
|
if (mode === "hosted") {
|
|
9624
|
-
const { getHostedZipGroups } = await import("./location-data-repository-
|
|
9682
|
+
const { getHostedZipGroups } = await import("./location-data-repository-YWFJGNIP.js");
|
|
9625
9683
|
const hosted = await getHostedZipGroups(stateAbbr);
|
|
9626
9684
|
if (!hosted.dataset) {
|
|
9627
9685
|
throw new DirectoryLocationError(
|
|
@@ -9684,7 +9742,7 @@ async function resolveDirectoryMarkets(options, config = {}) {
|
|
|
9684
9742
|
const sourceUrl = censusStateUrl(state.fips);
|
|
9685
9743
|
const warnings = [];
|
|
9686
9744
|
if (resolvedLocationDataMode(config) === "hosted") {
|
|
9687
|
-
const { queryHostedLocationMarkets } = await import("./location-data-repository-
|
|
9745
|
+
const { queryHostedLocationMarkets } = await import("./location-data-repository-YWFJGNIP.js");
|
|
9688
9746
|
const hosted = await queryHostedLocationMarkets({
|
|
9689
9747
|
state: state.abbr,
|
|
9690
9748
|
minPopulation: options.minPopulation,
|
|
@@ -14250,8 +14308,8 @@ siteAuditApp.get("/jobs", async (c) => {
|
|
|
14250
14308
|
import { Hono as Hono2 } from "hono";
|
|
14251
14309
|
var internalMemoryApp = new Hono2();
|
|
14252
14310
|
internalMemoryApp.post("/ai-debit", async (c) => {
|
|
14253
|
-
const
|
|
14254
|
-
if (!
|
|
14311
|
+
const secret = c.req.header("x-internal-secret");
|
|
14312
|
+
if (!secret || secret !== process.env.INTERNAL_MEMORY_DEBIT_SECRET) {
|
|
14255
14313
|
return c.json({ error: "unauthorized" }, 401);
|
|
14256
14314
|
}
|
|
14257
14315
|
const body = await c.req.json().catch(() => ({}));
|
|
@@ -14342,9 +14400,9 @@ var UnifiedBillingError = class extends Error {
|
|
|
14342
14400
|
details;
|
|
14343
14401
|
};
|
|
14344
14402
|
function billingSecret() {
|
|
14345
|
-
const
|
|
14346
|
-
if (!
|
|
14347
|
-
return
|
|
14403
|
+
const secret = process.env.INTERNAL_BILLING_SECRET?.trim() || process.env.INTERNAL_MEMORY_DEBIT_SECRET?.trim();
|
|
14404
|
+
if (!secret) throw new UnifiedBillingError("unauthorized", "internal billing secret is not configured", 503);
|
|
14405
|
+
return secret;
|
|
14348
14406
|
}
|
|
14349
14407
|
function normalizeIdentity(identity) {
|
|
14350
14408
|
const normalized = identity.trim().toLowerCase();
|
|
@@ -14764,42 +14822,6 @@ async function voidScheduledRunAuthorization(args) {
|
|
|
14764
14822
|
|
|
14765
14823
|
// src/api/memory.ts
|
|
14766
14824
|
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
14825
|
import { provisionDefaultsTool } from "mcpscraper-memory-tools/tools/vaults/provision-defaults";
|
|
14804
14826
|
import { setScheduleEntitlementTool } from "mcpscraper-memory-tools/tools/schedule/set-schedule-entitlement";
|
|
14805
14827
|
var MEMORY_BASE_URL = () => (process.env.MCP_MEMORY_BASE_URL ?? "https://memory.mcpscraper.dev").replace(/\/$/, "");
|
|
@@ -14817,10 +14839,10 @@ function isMemoryOperator(email) {
|
|
|
14817
14839
|
function encKey() {
|
|
14818
14840
|
return scryptSync(getSessionSecret(), "mcp-memory-key-v1", 32);
|
|
14819
14841
|
}
|
|
14820
|
-
function encryptMemoryKey(
|
|
14842
|
+
function encryptMemoryKey(secret) {
|
|
14821
14843
|
const iv = randomBytes(12);
|
|
14822
14844
|
const cipher = createCipheriv("aes-256-gcm", encKey(), iv);
|
|
14823
|
-
const enc = Buffer.concat([cipher.update(
|
|
14845
|
+
const enc = Buffer.concat([cipher.update(secret, "utf8"), cipher.final()]);
|
|
14824
14846
|
const tag = cipher.getAuthTag();
|
|
14825
14847
|
return `${iv.toString("base64")}:${tag.toString("base64")}:${enc.toString("base64")}`;
|
|
14826
14848
|
}
|
|
@@ -16437,15 +16459,15 @@ function mediaResolverConfigured() {
|
|
|
16437
16459
|
var RESOLVE_TIMEOUT_MS = 24e4;
|
|
16438
16460
|
async function resolveYouTubeAudio(videoId) {
|
|
16439
16461
|
const base = mediaResolverUrl();
|
|
16440
|
-
const
|
|
16441
|
-
if (!base || !
|
|
16462
|
+
const secret = mediaResolverSecret();
|
|
16463
|
+
if (!base || !secret) return null;
|
|
16442
16464
|
const controller = new AbortController();
|
|
16443
16465
|
const timer = setTimeout(() => controller.abort(), RESOLVE_TIMEOUT_MS);
|
|
16444
16466
|
const startedMs = Date.now();
|
|
16445
16467
|
try {
|
|
16446
16468
|
const resp = await fetch(`${base.replace(/\/+$/, "")}/resolve-audio`, {
|
|
16447
16469
|
method: "POST",
|
|
16448
|
-
headers: { "content-type": "application/json", "x-frames-secret":
|
|
16470
|
+
headers: { "content-type": "application/json", "x-frames-secret": secret },
|
|
16449
16471
|
body: JSON.stringify({ videoUrl: `https://www.youtube.com/watch?v=${videoId}` }),
|
|
16450
16472
|
signal: controller.signal
|
|
16451
16473
|
});
|
|
@@ -22227,7 +22249,7 @@ async function reconcileDirectoryWorkflowSettlements(limit = 25) {
|
|
|
22227
22249
|
}
|
|
22228
22250
|
|
|
22229
22251
|
// src/api/workflow-routes.ts
|
|
22230
|
-
import { createHmac as
|
|
22252
|
+
import { createHmac as createHmac2 } from "crypto";
|
|
22231
22253
|
import { readFile as readFile2 } from "fs/promises";
|
|
22232
22254
|
import { Hono as Hono19 } from "hono";
|
|
22233
22255
|
import { z as z28 } from "zod";
|
|
@@ -22313,9 +22335,9 @@ async function readManifestFromSummary(summary) {
|
|
|
22313
22335
|
}
|
|
22314
22336
|
}
|
|
22315
22337
|
function webhookSignature(body, timestamp) {
|
|
22316
|
-
const
|
|
22317
|
-
if (!
|
|
22318
|
-
return
|
|
22338
|
+
const secret = process.env.MCP_SCRAPER_WEBHOOK_SECRET?.trim();
|
|
22339
|
+
if (!secret) return null;
|
|
22340
|
+
return createHmac2("sha256", secret).update(`${timestamp}.${body}`).digest("hex");
|
|
22319
22341
|
}
|
|
22320
22342
|
async function deliverWorkflowWebhook(input) {
|
|
22321
22343
|
if (!input.webhookUrl) return;
|
|
@@ -22753,8 +22775,8 @@ async function dispatchDueWorkflowSchedules(apiUrl, limit = 3) {
|
|
|
22753
22775
|
return { dispatched: results.length, results };
|
|
22754
22776
|
}
|
|
22755
22777
|
workflowApp.post("/cron/dispatch", async (c) => {
|
|
22756
|
-
const
|
|
22757
|
-
if (!process.env.CRON_SECRET ||
|
|
22778
|
+
const secret = c.req.header("authorization");
|
|
22779
|
+
if (!process.env.CRON_SECRET || secret !== `Bearer ${process.env.CRON_SECRET}`) {
|
|
22758
22780
|
return c.json({ error: "Unauthorized" }, 401);
|
|
22759
22781
|
}
|
|
22760
22782
|
return c.json(await dispatchDueWorkflowSchedules(originFromUrl(c.req.url)));
|
|
@@ -33561,18 +33583,1700 @@ function xmlEscape(value) {
|
|
|
33561
33583
|
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
33562
33584
|
}
|
|
33563
33585
|
|
|
33564
|
-
// src/api/
|
|
33586
|
+
// src/api/analytics-routes.ts
|
|
33565
33587
|
import { Hono as Hono32 } from "hono";
|
|
33588
|
+
import { createMiddleware as createMiddleware4 } from "hono/factory";
|
|
33589
|
+
import { z as z34 } from "zod";
|
|
33590
|
+
import { createHash as createHash15 } from "crypto";
|
|
33591
|
+
import Papa2 from "papaparse";
|
|
33592
|
+
|
|
33593
|
+
// src/api/analytics-entitlement.ts
|
|
33594
|
+
import {
|
|
33595
|
+
createHash as createHash14,
|
|
33596
|
+
createHmac as createHmac3,
|
|
33597
|
+
randomBytes as randomBytes3,
|
|
33598
|
+
timingSafeEqual as timingSafeEqual2
|
|
33599
|
+
} from "crypto";
|
|
33566
33600
|
import { z as z33 } from "zod";
|
|
33601
|
+
var THORBIT_PRODUCT_URL = "https://thorbit.ai";
|
|
33602
|
+
var REFRESH_INTERVAL_MS = 15 * 60 * 1e3;
|
|
33603
|
+
var ELIGIBLE_GRACE_MS = 24 * 60 * 60 * 1e3;
|
|
33604
|
+
var TRIAL_LENGTH_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
33605
|
+
var ThorbitEntitlementSchema = z33.object({
|
|
33606
|
+
product: z33.literal("x_ray_pixel"),
|
|
33607
|
+
eligible: z33.boolean(),
|
|
33608
|
+
reason: z33.string().min(1).max(120),
|
|
33609
|
+
organizationId: z33.string().min(1).max(64),
|
|
33610
|
+
planId: z33.string().max(32).nullable(),
|
|
33611
|
+
subscriptionStatus: z33.string().max(20).nullable(),
|
|
33612
|
+
monthlyRecurringRevenueCents: z33.number().int().min(0),
|
|
33613
|
+
minimumMonthlyRecurringRevenueCents: z33.number().int().min(5e3),
|
|
33614
|
+
checkedAt: z33.string().datetime()
|
|
33615
|
+
}).strict();
|
|
33616
|
+
var AssertionSchema = z33.object({
|
|
33617
|
+
version: z33.literal(1),
|
|
33618
|
+
issuer: z33.literal("thorbit"),
|
|
33619
|
+
audience: z33.literal("mcp-scraper:x-ray"),
|
|
33620
|
+
issuedAt: z33.number().int().positive(),
|
|
33621
|
+
expiresAt: z33.number().int().positive(),
|
|
33622
|
+
entitlement: ThorbitEntitlementSchema
|
|
33623
|
+
}).strict();
|
|
33624
|
+
function thorbitBaseUrl() {
|
|
33625
|
+
return (process.env.THORBIT_X_RAY_BASE_URL?.trim() || THORBIT_PRODUCT_URL).replace(/\/$/, "");
|
|
33626
|
+
}
|
|
33627
|
+
function bridgeSecret() {
|
|
33628
|
+
return process.env.THORBIT_X_RAY_BRIDGE_SECRET?.trim() ?? "";
|
|
33629
|
+
}
|
|
33630
|
+
function bridgeConfigured() {
|
|
33631
|
+
return bridgeSecret().length >= 32;
|
|
33632
|
+
}
|
|
33633
|
+
function hint(value) {
|
|
33634
|
+
return value.length <= 8 ? value : `${value.slice(0, 4)}\u2026${value.slice(-4)}`;
|
|
33635
|
+
}
|
|
33636
|
+
function viewFromRow(row, options = {}) {
|
|
33637
|
+
const now = Date.now();
|
|
33638
|
+
const withinGrace = row ? Date.parse(row.access_expires_at) > now : false;
|
|
33639
|
+
const trialActive = Boolean(row && Date.parse(row.trial_ends_at) > now);
|
|
33640
|
+
const paidAccess = Boolean(row?.eligible && withinGrace);
|
|
33641
|
+
const access2 = paidAccess || trialActive;
|
|
33642
|
+
const status = paidAccess ? options.stale ? "stale" : "eligible" : trialActive ? "trial" : !row || row.connection_source === "trial" ? "trial_expired" : options.bridgeError ? "bridge_unavailable" : "ineligible";
|
|
33643
|
+
const trialStartedAt = row?.trial_started_at ?? new Date(now).toISOString();
|
|
33644
|
+
const trialEndsAt = row?.trial_ends_at ?? new Date(now).toISOString();
|
|
33645
|
+
return {
|
|
33646
|
+
product: "x_ray_pixel",
|
|
33647
|
+
productName: "Thorbit X-Ray Pixel",
|
|
33648
|
+
access: access2,
|
|
33649
|
+
status,
|
|
33650
|
+
reason: trialActive ? "trial_active" : row?.reason ?? "trial_expired",
|
|
33651
|
+
planId: row?.plan_id ?? null,
|
|
33652
|
+
subscriptionStatus: row?.subscription_status ?? null,
|
|
33653
|
+
monthlyRecurringRevenueCents: Number(row?.monthly_recurring_revenue_cents ?? 0),
|
|
33654
|
+
minimumMonthlyRecurringRevenueCents: Number(row?.minimum_monthly_recurring_revenue_cents ?? 5e3),
|
|
33655
|
+
verifiedAt: row?.verified_at ?? null,
|
|
33656
|
+
connectionSource: row?.connection_source === "trial" ? null : row?.connection_source ?? null,
|
|
33657
|
+
thorbitOrganizationHint: row?.thorbit_org_id ? hint(row.thorbit_org_id) : null,
|
|
33658
|
+
trialStartedAt,
|
|
33659
|
+
trialEndsAt,
|
|
33660
|
+
trialDaysRemaining: Math.max(0, Math.ceil((Date.parse(trialEndsAt) - now) / (24 * 60 * 60 * 1e3))),
|
|
33661
|
+
bridgeConfigured: bridgeConfigured(),
|
|
33662
|
+
billing: {
|
|
33663
|
+
standalonePriceUsdMonthly: 50,
|
|
33664
|
+
includedWithThorbitSubscriptionAtOrAboveUsdMonthly: 50,
|
|
33665
|
+
consumesMcpScraperCredits: false
|
|
33666
|
+
},
|
|
33667
|
+
owner: {
|
|
33668
|
+
name: "Thorbit",
|
|
33669
|
+
productUrl: THORBIT_PRODUCT_URL,
|
|
33670
|
+
host: "MCP Scraper"
|
|
33671
|
+
}
|
|
33672
|
+
};
|
|
33673
|
+
}
|
|
33674
|
+
async function storedEntitlement(userId) {
|
|
33675
|
+
const result = await getAnalyticsPool().query(
|
|
33676
|
+
"SELECT * FROM analytics_thorbit_entitlements WHERE user_id = $1",
|
|
33677
|
+
[userId]
|
|
33678
|
+
);
|
|
33679
|
+
return result.rows[0] ?? null;
|
|
33680
|
+
}
|
|
33681
|
+
async function ensureTrial(userId) {
|
|
33682
|
+
const startedAt = /* @__PURE__ */ new Date();
|
|
33683
|
+
const endsAt = new Date(startedAt.getTime() + TRIAL_LENGTH_MS);
|
|
33684
|
+
const result = await getAnalyticsPool().query(
|
|
33685
|
+
`INSERT INTO analytics_thorbit_entitlements(
|
|
33686
|
+
user_id, thorbit_org_id, connection_source, eligible, reason, plan_id,
|
|
33687
|
+
subscription_status, monthly_recurring_revenue_cents,
|
|
33688
|
+
minimum_monthly_recurring_revenue_cents, verified_at, refresh_after,
|
|
33689
|
+
access_expires_at, trial_started_at, trial_ends_at, last_refresh_error, updated_at
|
|
33690
|
+
) VALUES ($1,NULL,'trial',false,'trial_active',NULL,NULL,0,5000,$2,$3,$3,$2,$3,NULL,now())
|
|
33691
|
+
ON CONFLICT(user_id) DO UPDATE SET user_id=excluded.user_id
|
|
33692
|
+
RETURNING *`,
|
|
33693
|
+
[userId, startedAt.toISOString(), endsAt.toISOString()]
|
|
33694
|
+
);
|
|
33695
|
+
return result.rows[0];
|
|
33696
|
+
}
|
|
33697
|
+
async function persistEntitlement(userId, source, entitlement) {
|
|
33698
|
+
const checked = new Date(entitlement.checkedAt);
|
|
33699
|
+
const refreshAfter = new Date(checked.getTime() + REFRESH_INTERVAL_MS);
|
|
33700
|
+
const accessExpiresAt = new Date(
|
|
33701
|
+
checked.getTime() + (entitlement.eligible ? ELIGIBLE_GRACE_MS : REFRESH_INTERVAL_MS)
|
|
33702
|
+
);
|
|
33703
|
+
const result = await getAnalyticsPool().query(
|
|
33704
|
+
`INSERT INTO analytics_thorbit_entitlements(
|
|
33705
|
+
user_id, thorbit_org_id, connection_source, eligible, reason, plan_id,
|
|
33706
|
+
subscription_status, monthly_recurring_revenue_cents,
|
|
33707
|
+
minimum_monthly_recurring_revenue_cents, verified_at, refresh_after,
|
|
33708
|
+
access_expires_at, last_refresh_error, updated_at
|
|
33709
|
+
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,NULL,now())
|
|
33710
|
+
ON CONFLICT(user_id) DO UPDATE SET
|
|
33711
|
+
thorbit_org_id=excluded.thorbit_org_id,
|
|
33712
|
+
connection_source=excluded.connection_source,
|
|
33713
|
+
eligible=excluded.eligible,
|
|
33714
|
+
reason=excluded.reason,
|
|
33715
|
+
plan_id=excluded.plan_id,
|
|
33716
|
+
subscription_status=excluded.subscription_status,
|
|
33717
|
+
monthly_recurring_revenue_cents=excluded.monthly_recurring_revenue_cents,
|
|
33718
|
+
minimum_monthly_recurring_revenue_cents=excluded.minimum_monthly_recurring_revenue_cents,
|
|
33719
|
+
verified_at=excluded.verified_at,
|
|
33720
|
+
refresh_after=excluded.refresh_after,
|
|
33721
|
+
access_expires_at=excluded.access_expires_at,
|
|
33722
|
+
last_refresh_error=NULL,
|
|
33723
|
+
updated_at=now()
|
|
33724
|
+
RETURNING *`,
|
|
33725
|
+
[
|
|
33726
|
+
userId,
|
|
33727
|
+
entitlement.organizationId,
|
|
33728
|
+
source,
|
|
33729
|
+
entitlement.eligible,
|
|
33730
|
+
entitlement.reason,
|
|
33731
|
+
entitlement.planId,
|
|
33732
|
+
entitlement.subscriptionStatus,
|
|
33733
|
+
entitlement.monthlyRecurringRevenueCents,
|
|
33734
|
+
entitlement.minimumMonthlyRecurringRevenueCents,
|
|
33735
|
+
checked.toISOString(),
|
|
33736
|
+
refreshAfter.toISOString(),
|
|
33737
|
+
accessExpiresAt.toISOString()
|
|
33738
|
+
]
|
|
33739
|
+
);
|
|
33740
|
+
return result.rows[0];
|
|
33741
|
+
}
|
|
33742
|
+
async function requestEntitlement(method, options) {
|
|
33743
|
+
const headers = { accept: "application/json" };
|
|
33744
|
+
let body;
|
|
33745
|
+
if (method === "POST") {
|
|
33746
|
+
headers.authorization = `Bearer ${options.apiKey ?? ""}`;
|
|
33747
|
+
} else {
|
|
33748
|
+
headers.authorization = `Bearer ${bridgeSecret()}`;
|
|
33749
|
+
headers["content-type"] = "application/json";
|
|
33750
|
+
body = JSON.stringify({ organizationId: options.organizationId });
|
|
33751
|
+
}
|
|
33752
|
+
const response = await fetch(`${thorbitBaseUrl()}/api/v1/x-ray/entitlement`, {
|
|
33753
|
+
method,
|
|
33754
|
+
headers,
|
|
33755
|
+
body,
|
|
33756
|
+
signal: AbortSignal.timeout(8e3)
|
|
33757
|
+
});
|
|
33758
|
+
const payload = await response.json().catch(() => null);
|
|
33759
|
+
if (!response.ok) {
|
|
33760
|
+
const message = payload && typeof payload === "object" && "error" in payload ? String(payload.error) : `Thorbit entitlement check failed (${response.status})`;
|
|
33761
|
+
throw new Error(message);
|
|
33762
|
+
}
|
|
33763
|
+
const parsed = z33.object({ entitlement: ThorbitEntitlementSchema }).safeParse(payload);
|
|
33764
|
+
if (!parsed.success) throw new Error("Thorbit returned an invalid entitlement assertion");
|
|
33765
|
+
return parsed.data.entitlement;
|
|
33766
|
+
}
|
|
33767
|
+
async function resolveAnalyticsEntitlement(userId) {
|
|
33768
|
+
const row = await storedEntitlement(userId) ?? await ensureTrial(userId);
|
|
33769
|
+
if (row.connection_source === "trial" || !row.thorbit_org_id) return viewFromRow(row);
|
|
33770
|
+
if (Date.parse(row.refresh_after) > Date.now()) return viewFromRow(row);
|
|
33771
|
+
if (!bridgeConfigured()) return viewFromRow(row, { stale: true, bridgeError: true });
|
|
33772
|
+
try {
|
|
33773
|
+
const entitlement = await requestEntitlement("PUT", {
|
|
33774
|
+
organizationId: row.thorbit_org_id
|
|
33775
|
+
});
|
|
33776
|
+
return viewFromRow(
|
|
33777
|
+
await persistEntitlement(userId, row.connection_source, entitlement)
|
|
33778
|
+
);
|
|
33779
|
+
} catch (error) {
|
|
33780
|
+
await getAnalyticsPool().query(
|
|
33781
|
+
"UPDATE analytics_thorbit_entitlements SET last_refresh_error=$2, updated_at=now() WHERE user_id=$1",
|
|
33782
|
+
[userId, error instanceof Error ? error.message.slice(0, 240) : "refresh_failed"]
|
|
33783
|
+
);
|
|
33784
|
+
return viewFromRow(row, { stale: true, bridgeError: true });
|
|
33785
|
+
}
|
|
33786
|
+
}
|
|
33787
|
+
async function analyticsOwnerHasCurrentEntitlement(userId) {
|
|
33788
|
+
const result = await getAnalyticsPool().query(
|
|
33789
|
+
`SELECT EXISTS(
|
|
33790
|
+
SELECT 1 FROM analytics_thorbit_entitlements
|
|
33791
|
+
WHERE user_id=$1 AND (
|
|
33792
|
+
(eligible=true AND access_expires_at > now()) OR trial_ends_at > now()
|
|
33793
|
+
)
|
|
33794
|
+
) AS entitled`,
|
|
33795
|
+
[userId]
|
|
33796
|
+
);
|
|
33797
|
+
return Boolean(result.rows[0]?.entitled);
|
|
33798
|
+
}
|
|
33799
|
+
async function connectAnalyticsWithThorbitApiKey(userId, apiKey) {
|
|
33800
|
+
const entitlement = await requestEntitlement("POST", { apiKey });
|
|
33801
|
+
return viewFromRow(await persistEntitlement(userId, "api_key", entitlement));
|
|
33802
|
+
}
|
|
33803
|
+
function dashboardCallbackUrl() {
|
|
33804
|
+
const value = process.env.ANALYTICS_DASHBOARD_URL?.trim() || (process.env.NODE_ENV === "production" ? "https://mcpscraper.dev/dashboard/analytics" : "http://localhost:3000/dashboard/analytics");
|
|
33805
|
+
return new URL(value).toString();
|
|
33806
|
+
}
|
|
33807
|
+
async function createThorbitConnectUrl(userId) {
|
|
33808
|
+
if (!bridgeConfigured()) throw new Error("Thorbit X-Ray account bridge is not configured");
|
|
33809
|
+
const state = randomBytes3(32).toString("base64url");
|
|
33810
|
+
const stateHash = createHash14("sha256").update(state).digest("hex");
|
|
33811
|
+
await getAnalyticsPool().query(
|
|
33812
|
+
`INSERT INTO analytics_thorbit_connect_states(state_hash,user_id,expires_at)
|
|
33813
|
+
VALUES ($1,$2,now()+interval '10 minutes')
|
|
33814
|
+
ON CONFLICT(state_hash) DO NOTHING`,
|
|
33815
|
+
[stateHash, userId]
|
|
33816
|
+
);
|
|
33817
|
+
const url = new URL(`${thorbitBaseUrl()}/api/v1/x-ray/entitlement`);
|
|
33818
|
+
url.searchParams.set("redirect_uri", dashboardCallbackUrl());
|
|
33819
|
+
url.searchParams.set("state", state);
|
|
33820
|
+
return url.toString();
|
|
33821
|
+
}
|
|
33822
|
+
function verifyThorbitAssertion(token4) {
|
|
33823
|
+
const [encoded, signature] = token4.split(".");
|
|
33824
|
+
if (!encoded || !signature || !bridgeConfigured()) throw new Error("Invalid Thorbit assertion");
|
|
33825
|
+
const expected = createHmac3("sha256", bridgeSecret()).update(encoded).digest();
|
|
33826
|
+
const actual = Buffer.from(signature, "base64url");
|
|
33827
|
+
if (actual.length !== expected.length || !timingSafeEqual2(actual, expected)) {
|
|
33828
|
+
throw new Error("Invalid Thorbit assertion signature");
|
|
33829
|
+
}
|
|
33830
|
+
const parsed = AssertionSchema.safeParse(
|
|
33831
|
+
JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"))
|
|
33832
|
+
);
|
|
33833
|
+
if (!parsed.success || parsed.data.expiresAt <= Math.floor(Date.now() / 1e3)) {
|
|
33834
|
+
throw new Error("Expired or invalid Thorbit assertion");
|
|
33835
|
+
}
|
|
33836
|
+
return parsed.data.entitlement;
|
|
33837
|
+
}
|
|
33838
|
+
async function consumeThorbitConnectCallback(input) {
|
|
33839
|
+
const stateHash = createHash14("sha256").update(input.state).digest("hex");
|
|
33840
|
+
const client2 = await getAnalyticsPool().connect();
|
|
33841
|
+
try {
|
|
33842
|
+
await client2.query("BEGIN");
|
|
33843
|
+
const state = await client2.query(
|
|
33844
|
+
`UPDATE analytics_thorbit_connect_states
|
|
33845
|
+
SET consumed_at=now()
|
|
33846
|
+
WHERE state_hash=$1 AND user_id=$2 AND consumed_at IS NULL AND expires_at > now()
|
|
33847
|
+
RETURNING user_id`,
|
|
33848
|
+
[stateHash, input.userId]
|
|
33849
|
+
);
|
|
33850
|
+
if (!state.rowCount) throw new Error("Thorbit connection state is invalid or expired");
|
|
33851
|
+
const entitlement = verifyThorbitAssertion(input.assertion);
|
|
33852
|
+
await client2.query("COMMIT");
|
|
33853
|
+
return viewFromRow(
|
|
33854
|
+
await persistEntitlement(input.userId, "thorbit_session", entitlement)
|
|
33855
|
+
);
|
|
33856
|
+
} catch (error) {
|
|
33857
|
+
await client2.query("ROLLBACK");
|
|
33858
|
+
throw error;
|
|
33859
|
+
} finally {
|
|
33860
|
+
client2.release();
|
|
33861
|
+
}
|
|
33862
|
+
}
|
|
33863
|
+
async function disconnectAnalyticsEntitlement(userId) {
|
|
33864
|
+
await getAnalyticsPool().query(
|
|
33865
|
+
`UPDATE analytics_thorbit_entitlements
|
|
33866
|
+
SET thorbit_org_id=NULL,
|
|
33867
|
+
connection_source='trial',
|
|
33868
|
+
eligible=false,
|
|
33869
|
+
reason=CASE WHEN trial_ends_at > now() THEN 'trial_active' ELSE 'trial_expired' END,
|
|
33870
|
+
plan_id=NULL,
|
|
33871
|
+
subscription_status=NULL,
|
|
33872
|
+
monthly_recurring_revenue_cents=0,
|
|
33873
|
+
last_refresh_error=NULL,
|
|
33874
|
+
updated_at=now()
|
|
33875
|
+
WHERE user_id=$1`,
|
|
33876
|
+
[userId]
|
|
33877
|
+
);
|
|
33878
|
+
}
|
|
33879
|
+
|
|
33880
|
+
// src/api/analytics-routes.ts
|
|
33881
|
+
var analyticsApp = new Hono32();
|
|
33882
|
+
var auth3 = createApiKeyAuth();
|
|
33883
|
+
var entitlementGuard = createMiddleware4(async (c, next) => {
|
|
33884
|
+
const entitlement = await resolveAnalyticsEntitlement(Number(c.get("user").id));
|
|
33885
|
+
if (!entitlement.access) {
|
|
33886
|
+
return c.json(
|
|
33887
|
+
{
|
|
33888
|
+
ok: false,
|
|
33889
|
+
code: "thorbit_entitlement_required",
|
|
33890
|
+
error: "Thorbit X-Ray Pixel requires an active $50+ Thorbit subscription.",
|
|
33891
|
+
entitlement
|
|
33892
|
+
},
|
|
33893
|
+
402
|
|
33894
|
+
);
|
|
33895
|
+
}
|
|
33896
|
+
await next();
|
|
33897
|
+
});
|
|
33898
|
+
var ThorbitApiKeySchema = z34.object({ apiKey: z34.string().trim().min(20).max(512) }).strict();
|
|
33899
|
+
var ThorbitCallbackSchema = z34.object({
|
|
33900
|
+
state: z34.string().min(20).max(300),
|
|
33901
|
+
assertion: z34.string().min(40).max(8e3)
|
|
33902
|
+
}).strict();
|
|
33903
|
+
var SiteInputSchema = z34.object({
|
|
33904
|
+
name: z34.string().trim().min(1).max(120),
|
|
33905
|
+
slug: z34.string().trim().min(1).max(80).optional(),
|
|
33906
|
+
timezone: z34.string().trim().min(1).max(80).optional()
|
|
33907
|
+
}).strict();
|
|
33908
|
+
var PixelInputSchema = z34.object({
|
|
33909
|
+
name: z34.string().trim().min(1).max(120),
|
|
33910
|
+
environment: z34.string().trim().min(1).max(60).optional()
|
|
33911
|
+
}).strict();
|
|
33912
|
+
var PixelUpdateSchema = z34.object({
|
|
33913
|
+
name: z34.string().trim().min(1).max(120).optional(),
|
|
33914
|
+
environment: z34.string().trim().min(1).max(60).optional(),
|
|
33915
|
+
status: z34.enum(["active", "disabled"]).optional()
|
|
33916
|
+
}).strict().refine(
|
|
33917
|
+
(value) => Object.keys(value).length > 0,
|
|
33918
|
+
"Provide at least one change."
|
|
33919
|
+
);
|
|
33920
|
+
var EventSchema = z34.object({
|
|
33921
|
+
eventId: z34.string().trim().min(8).max(160),
|
|
33922
|
+
eventName: z34.string().trim().regex(/^[a-z][a-z0-9_]{1,79}$/),
|
|
33923
|
+
occurredAt: z34.string().datetime(),
|
|
33924
|
+
visitorId: z34.string().trim().max(160).optional(),
|
|
33925
|
+
sessionId: z34.string().trim().max(160).optional(),
|
|
33926
|
+
path: z34.string().trim().max(2e3).optional(),
|
|
33927
|
+
canonicalUrl: z34.string().url().max(3e3).optional(),
|
|
33928
|
+
title: z34.string().trim().max(500).optional(),
|
|
33929
|
+
referrer: z34.string().trim().max(3e3).optional(),
|
|
33930
|
+
source: z34.string().trim().max(180).optional(),
|
|
33931
|
+
medium: z34.string().trim().max(180).optional(),
|
|
33932
|
+
campaign: z34.string().trim().max(240).optional(),
|
|
33933
|
+
clickIds: z34.record(z34.string(), z34.string().trim().max(500)).optional(),
|
|
33934
|
+
deviceClass: z34.enum(["desktop", "tablet", "mobile", "unknown"]).optional(),
|
|
33935
|
+
properties: z34.record(z34.string(), z34.unknown()).optional()
|
|
33936
|
+
}).strict();
|
|
33937
|
+
var IngestionSchema = z34.object({
|
|
33938
|
+
pixelId: z34.string().trim().min(8).max(160),
|
|
33939
|
+
events: z34.array(EventSchema).min(1).max(20)
|
|
33940
|
+
}).strict();
|
|
33941
|
+
var ConversionSchema = z34.object({
|
|
33942
|
+
siteId: z34.string().uuid(),
|
|
33943
|
+
pixelId: z34.string().uuid().optional(),
|
|
33944
|
+
sessionId: z34.string().trim().max(160).optional(),
|
|
33945
|
+
sourceEventId: z34.string().trim().min(8).max(200),
|
|
33946
|
+
conversionKind: z34.string().trim().regex(/^[a-z][a-z0-9_]{1,79}$/),
|
|
33947
|
+
valueMinor: z34.number().int().min(0).max(2147483647).optional(),
|
|
33948
|
+
currency: z34.string().trim().regex(/^[A-Za-z]{3}$/).optional(),
|
|
33949
|
+
occurredAt: z34.string().datetime()
|
|
33950
|
+
}).strict();
|
|
33951
|
+
var ExportSchema = z34.object({
|
|
33952
|
+
report: z34.enum(["overview", "acquisition", "content", "conversions"]).default("overview"),
|
|
33953
|
+
format: z34.enum(["csv", "json", "markdown"]).default("csv"),
|
|
33954
|
+
idempotencyKey: z34.string().trim().min(8).max(160).optional()
|
|
33955
|
+
}).strict();
|
|
33956
|
+
var CampaignLinkSchema = z34.object({
|
|
33957
|
+
pixelId: z34.string().uuid().optional(),
|
|
33958
|
+
name: z34.string().trim().min(1).max(120),
|
|
33959
|
+
destinationUrl: z34.string().url().max(3e3),
|
|
33960
|
+
shortCode: z34.string().trim().min(3).max(48).optional(),
|
|
33961
|
+
source: z34.string().trim().min(1).max(180),
|
|
33962
|
+
medium: z34.string().trim().min(1).max(180),
|
|
33963
|
+
campaign: z34.string().trim().min(1).max(240),
|
|
33964
|
+
term: z34.string().trim().max(240).optional(),
|
|
33965
|
+
content: z34.string().trim().max(240).optional(),
|
|
33966
|
+
adGroup: z34.string().trim().max(240).optional(),
|
|
33967
|
+
adName: z34.string().trim().max(240).optional(),
|
|
33968
|
+
creativeId: z34.string().trim().max(240).optional()
|
|
33969
|
+
}).strict();
|
|
33970
|
+
var FormFieldSchema = z34.object({
|
|
33971
|
+
id: z34.string().trim().regex(/^[a-z][a-z0-9_]{0,47}$/),
|
|
33972
|
+
type: z34.enum([
|
|
33973
|
+
"text",
|
|
33974
|
+
"email",
|
|
33975
|
+
"tel",
|
|
33976
|
+
"textarea",
|
|
33977
|
+
"select",
|
|
33978
|
+
"checkbox",
|
|
33979
|
+
"radio",
|
|
33980
|
+
"number"
|
|
33981
|
+
]),
|
|
33982
|
+
label: z34.string().trim().min(1).max(120),
|
|
33983
|
+
required: z34.boolean().default(false),
|
|
33984
|
+
options: z34.array(z34.string().trim().min(1).max(120)).max(30).optional()
|
|
33985
|
+
}).strict();
|
|
33986
|
+
var FormInputSchema = z34.object({
|
|
33987
|
+
pixelId: z34.string().uuid(),
|
|
33988
|
+
name: z34.string().trim().min(1).max(120),
|
|
33989
|
+
fields: z34.array(FormFieldSchema).min(1).max(30).refine(
|
|
33990
|
+
(fields) => new Set(fields.map((field) => field.id)).size === fields.length,
|
|
33991
|
+
"Field IDs must be unique."
|
|
33992
|
+
),
|
|
33993
|
+
brand: z34.object({
|
|
33994
|
+
primaryColor: z34.string().regex(/^#[0-9a-f]{6}$/i),
|
|
33995
|
+
backgroundColor: z34.string().regex(/^#[0-9a-f]{6}$/i),
|
|
33996
|
+
textColor: z34.string().regex(/^#[0-9a-f]{6}$/i),
|
|
33997
|
+
radius: z34.number().int().min(0).max(32)
|
|
33998
|
+
}).strict(),
|
|
33999
|
+
submitLabel: z34.string().trim().min(1).max(80).optional(),
|
|
34000
|
+
successMessage: z34.string().trim().min(1).max(500).optional(),
|
|
34001
|
+
redirectUrl: z34.string().url().max(3e3).optional(),
|
|
34002
|
+
consentText: z34.string().trim().max(1e3).optional(),
|
|
34003
|
+
publish: z34.boolean().optional()
|
|
34004
|
+
}).strict();
|
|
34005
|
+
var FormSubmissionSchema = z34.object({
|
|
34006
|
+
data: z34.record(
|
|
34007
|
+
z34.string(),
|
|
34008
|
+
z34.union([z34.string().max(5e3), z34.number(), z34.boolean()])
|
|
34009
|
+
).refine((value) => Object.keys(value).length <= 30),
|
|
34010
|
+
visitorId: z34.string().trim().max(160).optional(),
|
|
34011
|
+
sessionId: z34.string().trim().max(160).optional(),
|
|
34012
|
+
source: z34.string().trim().max(180).optional(),
|
|
34013
|
+
medium: z34.string().trim().max(180).optional(),
|
|
34014
|
+
campaign: z34.string().trim().max(240).optional(),
|
|
34015
|
+
clickIds: z34.record(z34.string(), z34.string().trim().max(500)).optional(),
|
|
34016
|
+
placementUrl: z34.string().url().max(3e3),
|
|
34017
|
+
website: z34.string().max(0).optional()
|
|
34018
|
+
}).strict();
|
|
34019
|
+
var CrmImportSchema = z34.object({
|
|
34020
|
+
sourceSystem: z34.enum([
|
|
34021
|
+
"hubspot",
|
|
34022
|
+
"salesforce",
|
|
34023
|
+
"gohighlevel",
|
|
34024
|
+
"zoho",
|
|
34025
|
+
"pipedrive",
|
|
34026
|
+
"keap",
|
|
34027
|
+
"other"
|
|
34028
|
+
]),
|
|
34029
|
+
filename: z34.string().trim().min(1).max(240),
|
|
34030
|
+
csv: z34.string().min(1).max(8e6),
|
|
34031
|
+
mapping: z34.object({
|
|
34032
|
+
email: z34.string().trim().min(1).max(240).optional(),
|
|
34033
|
+
firstName: z34.string().trim().min(1).max(240).optional(),
|
|
34034
|
+
lastName: z34.string().trim().min(1).max(240).optional(),
|
|
34035
|
+
name: z34.string().trim().min(1).max(240).optional(),
|
|
34036
|
+
phone: z34.string().trim().min(1).max(240).optional(),
|
|
34037
|
+
company: z34.string().trim().min(1).max(240).optional(),
|
|
34038
|
+
externalId: z34.string().trim().min(1).max(240).optional()
|
|
34039
|
+
}).strict()
|
|
34040
|
+
}).strict();
|
|
34041
|
+
var ActivationDestinationSchema = z34.object({
|
|
34042
|
+
platform: z34.enum(["meta", "google", "tiktok", "reddit"]),
|
|
34043
|
+
name: z34.string().trim().min(1).max(120),
|
|
34044
|
+
connectionRef: z34.string().trim().max(240).optional(),
|
|
34045
|
+
externalDatasetId: z34.string().trim().max(240).optional(),
|
|
34046
|
+
eventMapping: z34.record(z34.string(), z34.string().trim().max(120)).optional()
|
|
34047
|
+
}).strict();
|
|
34048
|
+
var BusinessModelSchema = z34.object({ businessModel: z34.enum(["leadgen", "saas", "ecommerce"]) }).strict();
|
|
34049
|
+
var AdSpendSchema = z34.object({
|
|
34050
|
+
rows: z34.array(
|
|
34051
|
+
z34.object({
|
|
34052
|
+
date: z34.string().date(),
|
|
34053
|
+
platform: z34.string().trim().min(1).max(80),
|
|
34054
|
+
campaign: z34.string().trim().max(240).optional(),
|
|
34055
|
+
spendMinor: z34.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
|
|
34056
|
+
currency: z34.string().regex(/^[A-Za-z]{3}$/).optional(),
|
|
34057
|
+
sourceRef: z34.string().max(500).optional()
|
|
34058
|
+
}).strict()
|
|
34059
|
+
).min(1).max(5e3)
|
|
34060
|
+
}).strict();
|
|
34061
|
+
function safeOrigin(origin) {
|
|
34062
|
+
if (!origin || origin === "null") return null;
|
|
34063
|
+
try {
|
|
34064
|
+
const url = new URL(origin);
|
|
34065
|
+
return url.protocol === "http:" || url.protocol === "https:" ? url.origin : null;
|
|
34066
|
+
} catch {
|
|
34067
|
+
return null;
|
|
34068
|
+
}
|
|
34069
|
+
}
|
|
34070
|
+
function dashboardOriginAllowed(origin) {
|
|
34071
|
+
if (origin === "https://mcpscraper.dev" || origin === "https://www.mcpscraper.dev")
|
|
34072
|
+
return true;
|
|
34073
|
+
if (process.env.NODE_ENV !== "production" && /^http:\/\/(localhost|127\.0\.0\.1):\d+$/.test(origin))
|
|
34074
|
+
return true;
|
|
34075
|
+
return (process.env.ALLOWED_ORIGINS ?? process.env.APP_ORIGIN ?? "").split(",").map((value) => value.trim()).includes(origin);
|
|
34076
|
+
}
|
|
34077
|
+
analyticsApp.use("*", async (c, next) => {
|
|
34078
|
+
const origin = safeOrigin(c.req.header("origin"));
|
|
34079
|
+
const publicIngestion = c.req.path === "/v1/events";
|
|
34080
|
+
if (origin && (publicIngestion || dashboardOriginAllowed(origin))) {
|
|
34081
|
+
c.header("Access-Control-Allow-Origin", origin);
|
|
34082
|
+
c.header("Vary", "Origin");
|
|
34083
|
+
c.header(
|
|
34084
|
+
"Access-Control-Allow-Methods",
|
|
34085
|
+
"GET, POST, PATCH, DELETE, OPTIONS"
|
|
34086
|
+
);
|
|
34087
|
+
c.header("Access-Control-Allow-Headers", "content-type, x-api-key");
|
|
34088
|
+
c.header("Access-Control-Max-Age", "600");
|
|
34089
|
+
}
|
|
34090
|
+
if (c.req.method === "OPTIONS") return c.body(null, 204);
|
|
34091
|
+
await next();
|
|
34092
|
+
});
|
|
34093
|
+
function analyticsPixelSdk() {
|
|
34094
|
+
return `(()=>{const s=document.currentScript;if(!s)return;const pixel=s.dataset.pixel;if(!pixel)return;const endpoint=new URL('/analytics/v1/events',s.src).toString(),ttl=18e5;const key='mcp_analytics_visitor',sessionKey='mcp_analytics_session',touchKey='mcp_analytics_touch';const id=()=>crypto.randomUUID?crypto.randomUUID():'e_'+Date.now().toString(36)+Math.random().toString(36).slice(2);let consent=s.dataset.consent==='granted'&&!navigator.globalPrivacyControl,visitor=null,session=null;const read=k=>{try{return localStorage.getItem(k)}catch{return null}},write=(k,v)=>{try{localStorage.setItem(k,v)}catch{}};const ensure=()=>{if(!consent)return;visitor=read(key)||id();const now=Date.now();let saved={};try{saved=JSON.parse(read(sessionKey)||'{}')}catch{}session=saved.id&&now-Number(saved.seen||0)<ttl?saved.id:id();write(key,visitor);write(sessionKey,JSON.stringify({id:session,seen:now}))};const attribution=()=>{const q=new URLSearchParams(location.search),clickIds={},properties={},map={utm_term:'utm_term',utm_content:'utm_content',utm_id:'campaign_id',campaign_id:'campaign_id',adset_id:'ad_set_id',ad_set_id:'ad_set_id',adgroup_id:'ad_group_id',ad_group_id:'ad_group_id',ad_id:'ad_id',creative_id:'creative_id',placement:'placement'};for(const k of ['fbclid','gclid','gbraid','wbraid','ttclid','rdt_cid','msclkid']){const v=q.get(k);if(v)clickIds[k]=v}for(const [from,to] of Object.entries(map)){const v=q.get(from);if(v)properties[to]=v}const current={source:q.get('utm_source')||undefined,medium:q.get('utm_medium')||undefined,campaign:q.get('utm_campaign')||undefined,clickIds,properties};if(current.source||Object.keys(clickIds).length)write(touchKey,JSON.stringify(current));else{try{return JSON.parse(read(touchKey)||'null')||current}catch{}}return current};const send=(name,properties={})=>{if(!consent||navigator.globalPrivacyControl)return;ensure();const a=attribution();const body=JSON.stringify({pixelId:pixel,events:[{eventId:id(),eventName:name,occurredAt:new Date().toISOString(),visitorId:visitor,sessionId:session,path:location.pathname,canonicalUrl:location.origin+location.pathname,title:document.title,referrer:document.referrer||undefined,...a,deviceClass:innerWidth<640?'mobile':innerWidth<1024?'tablet':'desktop',properties:{...a.properties,...properties}}]});if(navigator.sendBeacon&&navigator.sendBeacon(endpoint,new Blob([body],{type:'application/json'})))return;fetch(endpoint,{method:'POST',headers:{'content-type':'application/json'},body,keepalive:true,credentials:'omit'}).catch(()=>{})};const api={page:()=>send('page_view'),track:send,consent:state=>{consent=(state==='granted'||state===true)&&!navigator.globalPrivacyControl;if(consent){ensure();api.page()}},debug:()=>({pixel,endpoint,consent,gpc:Boolean(navigator.globalPrivacyControl),visitorId:visitor,sessionId:session,hostname:location.hostname,path:location.pathname,...attribution()})};window.mcpAnalytics=api;ensure();if(consent)api.page();let current=location.href;const changed=()=>{if(location.href!==current){current=location.href;api.page()}};for(const method of ['pushState','replaceState']){const original=history[method];history[method]=function(...args){const result=original.apply(this,args);queueMicrotask(changed);return result}}addEventListener('popstate',changed)})();`;
|
|
34095
|
+
}
|
|
34096
|
+
function reportFilters(url) {
|
|
34097
|
+
const end = url.searchParams.get("end") || (/* @__PURE__ */ new Date()).toISOString();
|
|
34098
|
+
const defaultStart = new Date(
|
|
34099
|
+
Date.parse(end) - 30 * 24 * 60 * 60 * 1e3
|
|
34100
|
+
).toISOString();
|
|
34101
|
+
const list = (name) => url.searchParams.getAll(name).flatMap((value) => value.split(",")).map((value) => value.trim()).filter(Boolean);
|
|
34102
|
+
return {
|
|
34103
|
+
start: url.searchParams.get("start") || defaultStart,
|
|
34104
|
+
end,
|
|
34105
|
+
timezone: validTimezone(url.searchParams.get("timezone")),
|
|
34106
|
+
pixelIds: list("pixelId"),
|
|
34107
|
+
hostnames: list("hostname"),
|
|
34108
|
+
source: url.searchParams.get("source") || void 0,
|
|
34109
|
+
medium: url.searchParams.get("medium") || void 0,
|
|
34110
|
+
campaign: url.searchParams.get("campaign") || void 0,
|
|
34111
|
+
eventName: url.searchParams.get("eventName") || void 0,
|
|
34112
|
+
attributionModel: url.searchParams.get("attributionModel") === "last_touch" ? "last_touch" : "first_touch",
|
|
34113
|
+
channelFamily: url.searchParams.get("channelFamily") || void 0,
|
|
34114
|
+
platform: url.searchParams.get("platform") || void 0,
|
|
34115
|
+
referrer: url.searchParams.get("referrer") || void 0,
|
|
34116
|
+
path: url.searchParams.get("path") || void 0,
|
|
34117
|
+
deviceClass: url.searchParams.get("deviceClass") || void 0,
|
|
34118
|
+
countryCode: url.searchParams.get("countryCode") || void 0,
|
|
34119
|
+
regionCode: url.searchParams.get("regionCode") || void 0,
|
|
34120
|
+
conversionKind: url.searchParams.get("conversionKind") || void 0
|
|
34121
|
+
};
|
|
34122
|
+
}
|
|
34123
|
+
function validTimezone(value) {
|
|
34124
|
+
if (!value) return "UTC";
|
|
34125
|
+
try {
|
|
34126
|
+
new Intl.DateTimeFormat("en-US", { timeZone: value }).format(/* @__PURE__ */ new Date());
|
|
34127
|
+
return value;
|
|
34128
|
+
} catch {
|
|
34129
|
+
throw new AnalyticsRepositoryError(
|
|
34130
|
+
"invalid_timezone",
|
|
34131
|
+
"Choose a valid IANA timezone."
|
|
34132
|
+
);
|
|
34133
|
+
}
|
|
34134
|
+
}
|
|
34135
|
+
function validateReportWindow(filters) {
|
|
34136
|
+
const start = Date.parse(filters.start);
|
|
34137
|
+
const end = Date.parse(filters.end);
|
|
34138
|
+
if (!Number.isFinite(start) || !Number.isFinite(end) || start >= end) {
|
|
34139
|
+
throw new AnalyticsRepositoryError(
|
|
34140
|
+
"invalid_date_range",
|
|
34141
|
+
"Choose a valid date range."
|
|
34142
|
+
);
|
|
34143
|
+
}
|
|
34144
|
+
if (end - start > 366 * 24 * 60 * 60 * 1e3) {
|
|
34145
|
+
throw new AnalyticsRepositoryError(
|
|
34146
|
+
"date_range_too_large",
|
|
34147
|
+
"Date ranges are limited to 366 days."
|
|
34148
|
+
);
|
|
34149
|
+
}
|
|
34150
|
+
}
|
|
34151
|
+
function reportPage(url) {
|
|
34152
|
+
const rawLimit = Number(url.searchParams.get("limit") || 25);
|
|
34153
|
+
return {
|
|
34154
|
+
limit: Number.isFinite(rawLimit) ? Math.min(100, Math.max(1, Math.trunc(rawLimit))) : 25,
|
|
34155
|
+
cursor: url.searchParams.get("cursor") || void 0
|
|
34156
|
+
};
|
|
34157
|
+
}
|
|
34158
|
+
function pixelSnippet(publicId) {
|
|
34159
|
+
return `<script async src="https://mcpscraper.dev/analytics/v1/pixel.js" data-pixel="${publicId}" data-consent="required"></script>`;
|
|
34160
|
+
}
|
|
34161
|
+
function formSnippet(publicId) {
|
|
34162
|
+
return `<div data-mcp-form="${publicId}"></div><script async src="https://mcpscraper.dev/analytics/v1/form.js?form=${publicId}"></script>`;
|
|
34163
|
+
}
|
|
34164
|
+
function escapeHtml6(value) {
|
|
34165
|
+
return String(value ?? "").replace(
|
|
34166
|
+
/[&<>"']/g,
|
|
34167
|
+
(character) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[character]
|
|
34168
|
+
);
|
|
34169
|
+
}
|
|
34170
|
+
function renderPublicForm(form, placementUrl) {
|
|
34171
|
+
if (!form)
|
|
34172
|
+
return "<!doctype html><title>Form unavailable</title><p>Form unavailable.</p>";
|
|
34173
|
+
const fields = form.fields.map((field) => {
|
|
34174
|
+
const required = field.required ? " required" : "";
|
|
34175
|
+
const options = (field.options ?? []).map(
|
|
34176
|
+
(option) => `<option value="${escapeHtml6(option)}">${escapeHtml6(option)}</option>`
|
|
34177
|
+
).join("");
|
|
34178
|
+
const control = field.type === "textarea" ? `<textarea id="${escapeHtml6(field.id)}" name="${escapeHtml6(field.id)}"${required}></textarea>` : field.type === "select" ? `<select id="${escapeHtml6(field.id)}" name="${escapeHtml6(field.id)}"${required}><option value="">Choose\u2026</option>${options}</select>` : field.type === "checkbox" ? `<input id="${escapeHtml6(field.id)}" name="${escapeHtml6(field.id)}" type="checkbox" value="true"${required}>` : `<input id="${escapeHtml6(field.id)}" name="${escapeHtml6(field.id)}" type="${escapeHtml6(field.type)}"${required}>`;
|
|
34179
|
+
return `<label${field.type === "checkbox" ? ' class="check"' : ""}><span>${escapeHtml6(field.label)}${field.required ? " *" : ""}</span>${control}</label>`;
|
|
34180
|
+
}).join("");
|
|
34181
|
+
const brand = form.brand;
|
|
34182
|
+
return `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><style>:root{font-family:ui-sans-serif,system-ui;color:${escapeHtml6(brand.textColor)};background:${escapeHtml6(brand.backgroundColor)}}*{box-sizing:border-box}body{margin:0;padding:18px}form{display:grid;gap:14px}label{display:grid;gap:6px;font-size:13px;font-weight:650}.check{grid-template-columns:auto 1fr;align-items:center}.check span{grid-column:2}.check input{grid-column:1;grid-row:1}input,textarea,select{width:100%;min-height:44px;padding:10px 12px;border:1px solid #ccd3df;border-radius:${brand.radius}px;background:white;color:inherit;font:inherit}textarea{min-height:110px;resize:vertical}button{min-height:46px;border:0;border-radius:${brand.radius}px;color:white;background:${escapeHtml6(brand.primaryColor)};font:inherit;font-weight:750;cursor:pointer}.consent{margin:0;color:#667085;font-size:11px;line-height:1.45}.notice{display:none;padding:12px;border-radius:${brand.radius}px;background:#edf8f2;color:#17613c}.hp{position:absolute!important;left:-9999px!important}</style></head><body><form id="mcp-form">${fields}<label class="hp">Website<input name="website" autocomplete="off" tabindex="-1"></label>${form.consent_text ? `<p class="consent">${escapeHtml6(form.consent_text)}</p>` : ""}<button>${escapeHtml6(form.submit_label)}</button><div class="notice" role="status"></div></form><script>(()=>{const form=document.querySelector('#mcp-form'),notice=form.querySelector('.notice'),q=new URLSearchParams(location.search);form.addEventListener('submit',async event=>{event.preventDefault();const button=form.querySelector('button');button.disabled=true;const raw=Object.fromEntries(new FormData(form).entries()),website=String(raw.website||''),clickIds={};delete raw.website;for(const k of ['fbclid','gclid','gbraid','wbraid','ttclid','rdt_cid','msclkid']){const v=q.get(k);if(v)clickIds[k]=v}try{const response=await fetch('/analytics/forms/${escapeHtml6(form.public_id)}/submissions',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({data:raw,website,placementUrl:${JSON.stringify(placementUrl)},visitorId:q.get('visitor')||undefined,sessionId:q.get('session')||undefined,source:q.get('source')||undefined,medium:q.get('medium')||undefined,campaign:q.get('campaign')||undefined,clickIds})});if(!response.ok)throw new Error('submit');notice.textContent=${JSON.stringify(form.success_message)};notice.style.display='block';form.reset();${form.redirect_url ? `setTimeout(()=>{top.location.href=${JSON.stringify(form.redirect_url)}},700);` : ""}}catch{notice.textContent='Your response could not be submitted. Please try again.';notice.style.display='block'}finally{button.disabled=false}})})()</script></body></html>`;
|
|
34183
|
+
}
|
|
34184
|
+
analyticsApp.get(
|
|
34185
|
+
"/health/live",
|
|
34186
|
+
(c) => c.json({ status: "live", checkedAt: (/* @__PURE__ */ new Date()).toISOString() }, 200, {
|
|
34187
|
+
"Cache-Control": "no-store"
|
|
34188
|
+
})
|
|
34189
|
+
);
|
|
34190
|
+
analyticsApp.get("/health/ready", async (c) => {
|
|
34191
|
+
const startedAt = Date.now();
|
|
34192
|
+
try {
|
|
34193
|
+
await getAnalyticsPool().query("SELECT 1");
|
|
34194
|
+
return c.json(
|
|
34195
|
+
{
|
|
34196
|
+
status: "ready",
|
|
34197
|
+
database: true,
|
|
34198
|
+
latencyMs: Date.now() - startedAt,
|
|
34199
|
+
checkedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
34200
|
+
},
|
|
34201
|
+
200,
|
|
34202
|
+
{ "Cache-Control": "no-store" }
|
|
34203
|
+
);
|
|
34204
|
+
} catch (error) {
|
|
34205
|
+
return c.json(
|
|
34206
|
+
{
|
|
34207
|
+
status: "not_ready",
|
|
34208
|
+
database: false,
|
|
34209
|
+
error: error instanceof Error ? error.message : String(error),
|
|
34210
|
+
checkedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
34211
|
+
},
|
|
34212
|
+
503,
|
|
34213
|
+
{ "Cache-Control": "no-store" }
|
|
34214
|
+
);
|
|
34215
|
+
}
|
|
34216
|
+
});
|
|
34217
|
+
analyticsApp.get("/v1/pixel.js", (c) => {
|
|
34218
|
+
c.header("Content-Type", "application/javascript; charset=utf-8");
|
|
34219
|
+
c.header("Cache-Control", "public, max-age=300");
|
|
34220
|
+
return c.body(analyticsPixelSdk());
|
|
34221
|
+
});
|
|
34222
|
+
analyticsApp.get("/v1/form.js", async (c) => {
|
|
34223
|
+
const form = await getPublicAnalyticsForm(c.req.query("form") || "");
|
|
34224
|
+
if (!form)
|
|
34225
|
+
return c.body("/* Form unavailable. */", 404, {
|
|
34226
|
+
"Content-Type": "application/javascript; charset=utf-8"
|
|
34227
|
+
});
|
|
34228
|
+
if (!await analyticsOwnerHasCurrentEntitlement(Number(form.owner_user_id)))
|
|
34229
|
+
return c.body("/* Thorbit X-Ray Pixel subscription required. */", 402, {
|
|
34230
|
+
"Content-Type": "application/javascript; charset=utf-8",
|
|
34231
|
+
"Cache-Control": "no-store"
|
|
34232
|
+
});
|
|
34233
|
+
const script = `(()=>{const s=document.currentScript,form=${JSON.stringify(form.public_id)},pixel=${JSON.stringify(form.pixel_public_id)};if(!document.querySelector('script[data-pixel="'+pixel+'"]')){const p=document.createElement('script');p.async=true;p.src=new URL('/analytics/v1/pixel.js',s.src);p.dataset.pixel=pixel;p.dataset.consent='required';document.head.appendChild(p)}const target=document.querySelector('[data-mcp-form="'+form+'"]');if(!target)return;const frame=document.createElement('iframe'),params=new URLSearchParams({placement:location.href}),q=new URLSearchParams(location.search);try{const visitor=localStorage.getItem('mcp_analytics_visitor'),session=JSON.parse(localStorage.getItem('mcp_analytics_session')||'{}').id;if(visitor)params.set('visitor',visitor);if(session)params.set('session',session)}catch{}for(const [from,to] of [['utm_source','source'],['utm_medium','medium'],['utm_campaign','campaign'],['fbclid','fbclid'],['gclid','gclid'],['gbraid','gbraid'],['wbraid','wbraid'],['ttclid','ttclid'],['rdt_cid','rdt_cid'],['msclkid','msclkid']]){const value=q.get(from);if(value)params.set(to,value)}frame.title=${JSON.stringify(form.name)};frame.src=new URL('/analytics/forms/'+form+'/embed?'+params,s.src);frame.style.cssText='width:100%;min-height:520px;border:0;display:block';frame.loading='lazy';target.replaceChildren(frame)})();`;
|
|
34234
|
+
return c.body(script, 200, {
|
|
34235
|
+
"Content-Type": "application/javascript; charset=utf-8",
|
|
34236
|
+
"Cache-Control": "public, max-age=300"
|
|
34237
|
+
});
|
|
34238
|
+
});
|
|
34239
|
+
analyticsApp.get("/forms/:publicId/embed", async (c) => {
|
|
34240
|
+
const form = await getPublicAnalyticsForm(c.req.param("publicId"));
|
|
34241
|
+
if (!form)
|
|
34242
|
+
return c.html(
|
|
34243
|
+
"<!doctype html><title>Form unavailable</title><p>Form unavailable.</p>",
|
|
34244
|
+
404
|
|
34245
|
+
);
|
|
34246
|
+
if (!await analyticsOwnerHasCurrentEntitlement(Number(form.owner_user_id)))
|
|
34247
|
+
return c.html(
|
|
34248
|
+
'<!doctype html><meta name=viewport content="width=device-width"><style>body{font:15px system-ui;padding:24px;color:#475467}strong{display:block;color:#101828;margin-bottom:6px}</style><strong>Form unavailable</strong>This Thorbit X-Ray Pixel account needs an active subscription.',
|
|
34249
|
+
402
|
|
34250
|
+
);
|
|
34251
|
+
const placementUrl = c.req.query("placement") || "";
|
|
34252
|
+
if (!await isAnalyticsFormPlacementApproved(form.pixel_id, placementUrl))
|
|
34253
|
+
return c.html(
|
|
34254
|
+
"<!doctype html><title>Form unavailable</title><p>This form is not approved for this hostname.</p>",
|
|
34255
|
+
403
|
|
34256
|
+
);
|
|
34257
|
+
c.header(
|
|
34258
|
+
"Content-Security-Policy",
|
|
34259
|
+
"default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; connect-src 'self'; form-action 'none'; base-uri 'none'"
|
|
34260
|
+
);
|
|
34261
|
+
c.header("Cache-Control", "no-store");
|
|
34262
|
+
return c.html(renderPublicForm(form, placementUrl));
|
|
34263
|
+
});
|
|
34264
|
+
analyticsApp.post("/forms/:publicId/submissions", async (c) => {
|
|
34265
|
+
const parsed = FormSubmissionSchema.safeParse(
|
|
34266
|
+
await c.req.json().catch(() => null)
|
|
34267
|
+
);
|
|
34268
|
+
if (!parsed.success)
|
|
34269
|
+
return c.json(
|
|
34270
|
+
{
|
|
34271
|
+
ok: false,
|
|
34272
|
+
error: "Invalid form submission.",
|
|
34273
|
+
issues: parsed.error.issues
|
|
34274
|
+
},
|
|
34275
|
+
400
|
|
34276
|
+
);
|
|
34277
|
+
const form = await getPublicAnalyticsForm(c.req.param("publicId"));
|
|
34278
|
+
if (!form) return c.json({ ok: false, error: "Form unavailable." }, 404);
|
|
34279
|
+
if (!await analyticsOwnerHasCurrentEntitlement(Number(form.owner_user_id)))
|
|
34280
|
+
return c.json(
|
|
34281
|
+
{ ok: false, error: "This Thorbit X-Ray Pixel form is not currently active." },
|
|
34282
|
+
402
|
|
34283
|
+
);
|
|
34284
|
+
if (!await isAnalyticsFormPlacementApproved(
|
|
34285
|
+
form.pixel_id,
|
|
34286
|
+
parsed.data.placementUrl
|
|
34287
|
+
))
|
|
34288
|
+
return c.json({ ok: false, error: "Form hostname is not approved." }, 403);
|
|
34289
|
+
const allowed = new Map(
|
|
34290
|
+
form.fields.map((field) => [field.id, field])
|
|
34291
|
+
);
|
|
34292
|
+
for (const key of Object.keys(parsed.data.data))
|
|
34293
|
+
if (!allowed.has(key))
|
|
34294
|
+
return c.json(
|
|
34295
|
+
{ ok: false, error: "Submission contains an unknown field." },
|
|
34296
|
+
400
|
|
34297
|
+
);
|
|
34298
|
+
for (const field of allowed.values())
|
|
34299
|
+
if (field.required && (parsed.data.data[field.id] === void 0 || parsed.data.data[field.id] === ""))
|
|
34300
|
+
return c.json({ ok: false, error: `${field.label} is required.` }, 400);
|
|
34301
|
+
const user = await getUserById(form.owner_user_id);
|
|
34302
|
+
if (!user)
|
|
34303
|
+
return c.json({ ok: false, error: "Form owner unavailable." }, 503);
|
|
34304
|
+
const data = parsed.data.data;
|
|
34305
|
+
const email = typeof data.email === "string" ? data.email.trim().toLowerCase() : "";
|
|
34306
|
+
const firstName = typeof data.first_name === "string" ? data.first_name.trim() : "";
|
|
34307
|
+
const lastName = typeof data.last_name === "string" ? data.last_name.trim() : "";
|
|
34308
|
+
const fullName = [firstName, lastName].filter(Boolean).join(" ") || (typeof data.name === "string" ? data.name.trim() : "") || email || "Website lead";
|
|
34309
|
+
const identitySeed = email || `${fullName}:${Date.now()}`;
|
|
34310
|
+
const suffix2 = createHash15("sha256").update(identitySeed).digest("hex").slice(0, 12);
|
|
34311
|
+
const path5 = `Leads/person-${suffix2}`;
|
|
34312
|
+
const { key: memoryKey, error: memoryKeyError } = await getOrCreateUserMemoryKey(user);
|
|
34313
|
+
const existing = memoryKey ? await memoryCall("getTool", { vault: "People", path: path5 }, memoryKey) : { ok: false, error: memoryKeyError || "memory_credential_unavailable" };
|
|
34314
|
+
const props = {
|
|
34315
|
+
...existing.note?.props ?? {},
|
|
34316
|
+
type: "person",
|
|
34317
|
+
status: "active",
|
|
34318
|
+
person_name: fullName,
|
|
34319
|
+
...firstName ? { first_name: firstName } : {},
|
|
34320
|
+
...lastName ? { last_name: lastName } : {},
|
|
34321
|
+
...email ? { email } : {},
|
|
34322
|
+
...typeof data.phone === "string" && data.phone.trim() ? { phone: data.phone.trim(), text_phone: data.phone.trim() } : {},
|
|
34323
|
+
...typeof data.company === "string" && data.company.trim() ? { company: data.company.trim() } : {},
|
|
34324
|
+
attribution_site_id: form.site_id,
|
|
34325
|
+
attribution_form_id: form.id,
|
|
34326
|
+
attribution_form_name: form.name,
|
|
34327
|
+
attributed_source: parsed.data.source || "(direct)",
|
|
34328
|
+
attributed_medium: parsed.data.medium || "(none)",
|
|
34329
|
+
attributed_campaign: parsed.data.campaign || "(none)",
|
|
34330
|
+
form_fields: data,
|
|
34331
|
+
source_type: "analytics_form",
|
|
34332
|
+
source_ref: `form:${form.public_id}`,
|
|
34333
|
+
summary: `Website lead from ${form.name}`,
|
|
34334
|
+
embedding_summary: `${fullName} submitted ${form.name}`,
|
|
34335
|
+
related: [],
|
|
34336
|
+
related_vault_notes: [],
|
|
34337
|
+
embed: true,
|
|
34338
|
+
embed_priority: "normal"
|
|
34339
|
+
};
|
|
34340
|
+
const crmPayload = {
|
|
34341
|
+
vault: "People",
|
|
34342
|
+
path: path5,
|
|
34343
|
+
title: fullName,
|
|
34344
|
+
content: existing.note?.content || `# ${fullName}
|
|
34345
|
+
|
|
34346
|
+
## Identity
|
|
34347
|
+
Website lead captured through ${form.name}.
|
|
34348
|
+
|
|
34349
|
+
## Relationship
|
|
34350
|
+
Attribution is linked to Site Analytics.
|
|
34351
|
+
|
|
34352
|
+
## Known Context
|
|
34353
|
+
Submitted the published form.
|
|
34354
|
+
|
|
34355
|
+
## Interaction History
|
|
34356
|
+
- ${(/* @__PURE__ */ new Date()).toISOString()}: Form submitted.
|
|
34357
|
+
|
|
34358
|
+
## Open Loops
|
|
34359
|
+
- Review and follow up.`,
|
|
34360
|
+
props,
|
|
34361
|
+
...existing.note ? { baseRevision: existing.note.revision } : {}
|
|
34362
|
+
};
|
|
34363
|
+
const saved = memoryKey ? await memoryCall(
|
|
34364
|
+
"putTool",
|
|
34365
|
+
crmPayload,
|
|
34366
|
+
memoryKey
|
|
34367
|
+
) : { ok: false, error: memoryKeyError || "memory_credential_unavailable" };
|
|
34368
|
+
const crmDeliveryStatus = saved.ok ? "delivered" : "pending";
|
|
34369
|
+
const receipt = await recordAnalyticsFormSubmission({
|
|
34370
|
+
form,
|
|
34371
|
+
visitorId: parsed.data.visitorId,
|
|
34372
|
+
sessionId: parsed.data.sessionId,
|
|
34373
|
+
crmPersonRef: `People::${path5}`,
|
|
34374
|
+
crmDeliveryStatus,
|
|
34375
|
+
source: parsed.data.source,
|
|
34376
|
+
medium: parsed.data.medium,
|
|
34377
|
+
campaign: parsed.data.campaign,
|
|
34378
|
+
clickIds: parsed.data.clickIds
|
|
34379
|
+
});
|
|
34380
|
+
const phone = typeof data.phone === "string" ? data.phone.trim() : "";
|
|
34381
|
+
const identityGraph = await linkAnalyticsFormIdentity({
|
|
34382
|
+
submissionId: receipt.submissionId,
|
|
34383
|
+
siteId: form.site_id,
|
|
34384
|
+
crmPersonRef: `People::${path5}`,
|
|
34385
|
+
visitorId: parsed.data.visitorId,
|
|
34386
|
+
sessionId: parsed.data.sessionId,
|
|
34387
|
+
email: email || void 0,
|
|
34388
|
+
phone: phone || void 0,
|
|
34389
|
+
clickIds: parsed.data.clickIds
|
|
34390
|
+
});
|
|
34391
|
+
if (!saved.ok) {
|
|
34392
|
+
await queueAnalyticsFormDelivery({
|
|
34393
|
+
submissionId: receipt.submissionId,
|
|
34394
|
+
ownerUserId: form.owner_user_id,
|
|
34395
|
+
payloadCiphertext: encryptMemoryKey(JSON.stringify(crmPayload)),
|
|
34396
|
+
errorCode: memoryKey ? "memory_write_failed" : "memory_credential_unavailable"
|
|
34397
|
+
});
|
|
34398
|
+
}
|
|
34399
|
+
const conversion = await createAnalyticsConversion({
|
|
34400
|
+
userId: form.owner_user_id,
|
|
34401
|
+
siteId: form.site_id,
|
|
34402
|
+
pixelId: form.pixel_id,
|
|
34403
|
+
sessionId: parsed.data.sessionId,
|
|
34404
|
+
sourceEventId: `form:${receipt.submissionId}`,
|
|
34405
|
+
conversionKind: "form_submitted",
|
|
34406
|
+
occurredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
34407
|
+
});
|
|
34408
|
+
const match = {
|
|
34409
|
+
...email ? { emailSha256: createHash15("sha256").update(email).digest("hex") } : {},
|
|
34410
|
+
...phone ? {
|
|
34411
|
+
phoneSha256: createHash15("sha256").update(phone.replace(/\D/g, "")).digest("hex")
|
|
34412
|
+
} : {}
|
|
34413
|
+
};
|
|
34414
|
+
const activationQueued = await queueAnalyticsActivation({
|
|
34415
|
+
siteId: form.site_id,
|
|
34416
|
+
conversionId: conversion.id,
|
|
34417
|
+
personId: identityGraph.personId,
|
|
34418
|
+
payloadCiphertext: encryptMemoryKey(
|
|
34419
|
+
JSON.stringify({
|
|
34420
|
+
eventId: conversion.id,
|
|
34421
|
+
eventName: "form_submitted",
|
|
34422
|
+
eventTime: (/* @__PURE__ */ new Date()).toISOString(),
|
|
34423
|
+
actionSource: "website",
|
|
34424
|
+
sourceUrl: parsed.data.placementUrl,
|
|
34425
|
+
match,
|
|
34426
|
+
clickIds: parsed.data.clickIds ?? {},
|
|
34427
|
+
attribution: {
|
|
34428
|
+
source: parsed.data.source,
|
|
34429
|
+
medium: parsed.data.medium,
|
|
34430
|
+
campaign: parsed.data.campaign
|
|
34431
|
+
}
|
|
34432
|
+
})
|
|
34433
|
+
)
|
|
34434
|
+
});
|
|
34435
|
+
return c.json(
|
|
34436
|
+
{
|
|
34437
|
+
ok: true,
|
|
34438
|
+
submissionId: receipt.submissionId,
|
|
34439
|
+
crmDeliveryStatus,
|
|
34440
|
+
attribution: {
|
|
34441
|
+
personId: identityGraph.personId,
|
|
34442
|
+
linkedSignals: identityGraph.linkedSignals
|
|
34443
|
+
},
|
|
34444
|
+
activation: { queuedDestinations: activationQueued },
|
|
34445
|
+
message: form.success_message,
|
|
34446
|
+
redirectUrl: form.redirect_url
|
|
34447
|
+
},
|
|
34448
|
+
saved.ok ? 201 : 202
|
|
34449
|
+
);
|
|
34450
|
+
});
|
|
34451
|
+
analyticsApp.get("/l/:shortCode", async (c) => {
|
|
34452
|
+
const destination = await resolveAnalyticsCampaignLink(
|
|
34453
|
+
c.req.param("shortCode"),
|
|
34454
|
+
c.req.header("referer")
|
|
34455
|
+
);
|
|
34456
|
+
if (!destination)
|
|
34457
|
+
return c.json(
|
|
34458
|
+
{
|
|
34459
|
+
ok: false,
|
|
34460
|
+
code: "campaign_link_not_found",
|
|
34461
|
+
error: "Campaign link not found."
|
|
34462
|
+
},
|
|
34463
|
+
404
|
|
34464
|
+
);
|
|
34465
|
+
c.header("Cache-Control", "no-store");
|
|
34466
|
+
return c.redirect(destination, 302);
|
|
34467
|
+
});
|
|
34468
|
+
analyticsApp.post("/v1/events", async (c) => {
|
|
34469
|
+
const contentLength = Number(c.req.header("content-length") || 0);
|
|
34470
|
+
if (contentLength > 32 * 1024)
|
|
34471
|
+
return c.json(
|
|
34472
|
+
{ accepted: 0, rejected: 1, reasonCodes: ["payload_too_large"] },
|
|
34473
|
+
413
|
|
34474
|
+
);
|
|
34475
|
+
const parsed = IngestionSchema.safeParse(
|
|
34476
|
+
await c.req.json().catch(() => null)
|
|
34477
|
+
);
|
|
34478
|
+
if (!parsed.success) {
|
|
34479
|
+
return c.json(
|
|
34480
|
+
{
|
|
34481
|
+
accepted: 0,
|
|
34482
|
+
rejected: 1,
|
|
34483
|
+
reasonCodes: ["invalid_payload"],
|
|
34484
|
+
issues: parsed.error.issues
|
|
34485
|
+
},
|
|
34486
|
+
400
|
|
34487
|
+
);
|
|
34488
|
+
}
|
|
34489
|
+
const result = await ingestAnalyticsEvents({
|
|
34490
|
+
publicPixelId: parsed.data.pixelId,
|
|
34491
|
+
origin: c.req.header("origin") ?? null,
|
|
34492
|
+
events: parsed.data.events,
|
|
34493
|
+
geography: {
|
|
34494
|
+
countryCode: c.req.header("x-vercel-ip-country") || c.req.header("cf-ipcountry") || null,
|
|
34495
|
+
regionCode: c.req.header("x-vercel-ip-country-region") || null
|
|
34496
|
+
}
|
|
34497
|
+
});
|
|
34498
|
+
return c.json(result, 202);
|
|
34499
|
+
});
|
|
34500
|
+
analyticsApp.get("/entitlement", auth3, async (c) => {
|
|
34501
|
+
c.header("Cache-Control", "private, no-store");
|
|
34502
|
+
return c.json({
|
|
34503
|
+
entitlement: await resolveAnalyticsEntitlement(Number(c.get("user").id))
|
|
34504
|
+
});
|
|
34505
|
+
});
|
|
34506
|
+
analyticsApp.post("/entitlement/connect", auth3, async (c) => {
|
|
34507
|
+
c.header("Cache-Control", "private, no-store");
|
|
34508
|
+
try {
|
|
34509
|
+
return c.json({
|
|
34510
|
+
connectUrl: await createThorbitConnectUrl(Number(c.get("user").id))
|
|
34511
|
+
});
|
|
34512
|
+
} catch (error) {
|
|
34513
|
+
return c.json(
|
|
34514
|
+
{
|
|
34515
|
+
error: error instanceof Error ? error.message : "Could not start Thorbit sign-in."
|
|
34516
|
+
},
|
|
34517
|
+
503
|
|
34518
|
+
);
|
|
34519
|
+
}
|
|
34520
|
+
});
|
|
34521
|
+
analyticsApp.post("/entitlement/api-key", auth3, async (c) => {
|
|
34522
|
+
c.header("Cache-Control", "private, no-store");
|
|
34523
|
+
const parsed = ThorbitApiKeySchema.safeParse(
|
|
34524
|
+
await c.req.json().catch(() => null)
|
|
34525
|
+
);
|
|
34526
|
+
if (!parsed.success)
|
|
34527
|
+
return c.json({ error: "Enter a valid Thorbit API key." }, 400);
|
|
34528
|
+
try {
|
|
34529
|
+
return c.json({
|
|
34530
|
+
entitlement: await connectAnalyticsWithThorbitApiKey(
|
|
34531
|
+
Number(c.get("user").id),
|
|
34532
|
+
parsed.data.apiKey
|
|
34533
|
+
)
|
|
34534
|
+
});
|
|
34535
|
+
} catch (error) {
|
|
34536
|
+
return c.json(
|
|
34537
|
+
{
|
|
34538
|
+
error: error instanceof Error ? error.message : "Thorbit could not verify this API key."
|
|
34539
|
+
},
|
|
34540
|
+
401
|
|
34541
|
+
);
|
|
34542
|
+
}
|
|
34543
|
+
});
|
|
34544
|
+
analyticsApp.post("/entitlement/callback", auth3, async (c) => {
|
|
34545
|
+
c.header("Cache-Control", "private, no-store");
|
|
34546
|
+
const parsed = ThorbitCallbackSchema.safeParse(
|
|
34547
|
+
await c.req.json().catch(() => null)
|
|
34548
|
+
);
|
|
34549
|
+
if (!parsed.success)
|
|
34550
|
+
return c.json({ error: "Invalid Thorbit connection response." }, 400);
|
|
34551
|
+
try {
|
|
34552
|
+
return c.json({
|
|
34553
|
+
entitlement: await consumeThorbitConnectCallback({
|
|
34554
|
+
userId: Number(c.get("user").id),
|
|
34555
|
+
state: parsed.data.state,
|
|
34556
|
+
assertion: parsed.data.assertion
|
|
34557
|
+
})
|
|
34558
|
+
});
|
|
34559
|
+
} catch (error) {
|
|
34560
|
+
return c.json(
|
|
34561
|
+
{
|
|
34562
|
+
error: error instanceof Error ? error.message : "Could not connect the Thorbit account."
|
|
34563
|
+
},
|
|
34564
|
+
400
|
|
34565
|
+
);
|
|
34566
|
+
}
|
|
34567
|
+
});
|
|
34568
|
+
analyticsApp.delete("/entitlement", auth3, async (c) => {
|
|
34569
|
+
await disconnectAnalyticsEntitlement(Number(c.get("user").id));
|
|
34570
|
+
return c.json({ ok: true });
|
|
34571
|
+
});
|
|
34572
|
+
analyticsApp.use("/v1/conversions", auth3, entitlementGuard);
|
|
34573
|
+
analyticsApp.use("/sites", auth3, entitlementGuard);
|
|
34574
|
+
analyticsApp.use("/sites/*", auth3, entitlementGuard);
|
|
34575
|
+
analyticsApp.post("/v1/conversions", auth3, async (c) => {
|
|
34576
|
+
const parsed = ConversionSchema.safeParse(
|
|
34577
|
+
await c.req.json().catch(() => null)
|
|
34578
|
+
);
|
|
34579
|
+
if (!parsed.success)
|
|
34580
|
+
return c.json(
|
|
34581
|
+
{ error: "Invalid conversion.", issues: parsed.error.issues },
|
|
34582
|
+
400
|
|
34583
|
+
);
|
|
34584
|
+
const conversion = await createAnalyticsConversion({
|
|
34585
|
+
userId: Number(c.get("user").id),
|
|
34586
|
+
...parsed.data
|
|
34587
|
+
});
|
|
34588
|
+
return c.json({ ok: true, conversion }, conversion.created ? 201 : 200);
|
|
34589
|
+
});
|
|
34590
|
+
analyticsApp.get("/sites", auth3, async (c) => {
|
|
34591
|
+
const sites = await listAnalyticsSites(Number(c.get("user").id));
|
|
34592
|
+
return c.json({ ok: true, sites });
|
|
34593
|
+
});
|
|
34594
|
+
analyticsApp.post("/sites", auth3, async (c) => {
|
|
34595
|
+
const parsed = SiteInputSchema.safeParse(
|
|
34596
|
+
await c.req.json().catch(() => null)
|
|
34597
|
+
);
|
|
34598
|
+
if (!parsed.success)
|
|
34599
|
+
return c.json(
|
|
34600
|
+
{ error: "Invalid Site details.", issues: parsed.error.issues },
|
|
34601
|
+
400
|
|
34602
|
+
);
|
|
34603
|
+
const site = await createAnalyticsSite({
|
|
34604
|
+
userId: Number(c.get("user").id),
|
|
34605
|
+
...parsed.data
|
|
34606
|
+
});
|
|
34607
|
+
return c.json({ ok: true, site }, 201);
|
|
34608
|
+
});
|
|
34609
|
+
analyticsApp.delete("/sites/:siteId", auth3, async (c) => {
|
|
34610
|
+
await deleteAnalyticsSite({
|
|
34611
|
+
siteId: c.req.param("siteId"),
|
|
34612
|
+
userId: Number(c.get("user").id)
|
|
34613
|
+
});
|
|
34614
|
+
return c.json({ ok: true });
|
|
34615
|
+
});
|
|
34616
|
+
analyticsApp.patch("/sites/:siteId/business-model", auth3, async (c) => {
|
|
34617
|
+
const parsed = BusinessModelSchema.safeParse(
|
|
34618
|
+
await c.req.json().catch(() => null)
|
|
34619
|
+
);
|
|
34620
|
+
if (!parsed.success)
|
|
34621
|
+
return c.json(
|
|
34622
|
+
{ ok: false, error: "Choose a supported business model." },
|
|
34623
|
+
400
|
|
34624
|
+
);
|
|
34625
|
+
await updateAnalyticsBusinessModel({
|
|
34626
|
+
siteId: c.req.param("siteId"),
|
|
34627
|
+
userId: Number(c.get("user").id),
|
|
34628
|
+
...parsed.data
|
|
34629
|
+
});
|
|
34630
|
+
return c.json({ ok: true, businessModel: parsed.data.businessModel });
|
|
34631
|
+
});
|
|
34632
|
+
analyticsApp.post("/sites/:siteId/ad-spend", auth3, async (c) => {
|
|
34633
|
+
const parsed = AdSpendSchema.safeParse(await c.req.json().catch(() => null));
|
|
34634
|
+
if (!parsed.success)
|
|
34635
|
+
return c.json(
|
|
34636
|
+
{
|
|
34637
|
+
ok: false,
|
|
34638
|
+
error: "Invalid ad-spend rows.",
|
|
34639
|
+
issues: parsed.error.issues
|
|
34640
|
+
},
|
|
34641
|
+
400
|
|
34642
|
+
);
|
|
34643
|
+
const result = await upsertAnalyticsAdSpend({
|
|
34644
|
+
siteId: c.req.param("siteId"),
|
|
34645
|
+
userId: Number(c.get("user").id),
|
|
34646
|
+
...parsed.data
|
|
34647
|
+
});
|
|
34648
|
+
return c.json({ ok: true, result }, 202);
|
|
34649
|
+
});
|
|
34650
|
+
analyticsApp.get("/sites/:siteId/business-metrics", auth3, async (c) => {
|
|
34651
|
+
const url = new URL(c.req.url);
|
|
34652
|
+
const filters = reportFilters(url);
|
|
34653
|
+
validateReportWindow(filters);
|
|
34654
|
+
const data = await analyticsBusinessMetrics(
|
|
34655
|
+
c.req.param("siteId"),
|
|
34656
|
+
Number(c.get("user").id),
|
|
34657
|
+
filters.start,
|
|
34658
|
+
filters.end
|
|
34659
|
+
);
|
|
34660
|
+
return c.json({ ok: true, data, filters });
|
|
34661
|
+
});
|
|
34662
|
+
analyticsApp.get("/sites/:siteId/forecast", auth3, async (c) => {
|
|
34663
|
+
const horizon = Math.min(
|
|
34664
|
+
Math.max(Number(c.req.query("horizonMonths") || 12), 1),
|
|
34665
|
+
36
|
|
34666
|
+
);
|
|
34667
|
+
const data = await analyticsForecast(
|
|
34668
|
+
c.req.param("siteId"),
|
|
34669
|
+
Number(c.get("user").id),
|
|
34670
|
+
horizon
|
|
34671
|
+
);
|
|
34672
|
+
return c.json({ ok: true, data });
|
|
34673
|
+
});
|
|
34674
|
+
analyticsApp.get("/sites/:siteId/pixels", auth3, async (c) => {
|
|
34675
|
+
const pixels = await listAnalyticsPixels(
|
|
34676
|
+
c.req.param("siteId"),
|
|
34677
|
+
Number(c.get("user").id)
|
|
34678
|
+
);
|
|
34679
|
+
return c.json({
|
|
34680
|
+
ok: true,
|
|
34681
|
+
pixels: pixels.map((pixel) => ({
|
|
34682
|
+
...pixel,
|
|
34683
|
+
snippet: pixelSnippet(pixel.public_id)
|
|
34684
|
+
}))
|
|
34685
|
+
});
|
|
34686
|
+
});
|
|
34687
|
+
analyticsApp.post("/sites/:siteId/pixels", auth3, async (c) => {
|
|
34688
|
+
const parsed = PixelInputSchema.safeParse(
|
|
34689
|
+
await c.req.json().catch(() => null)
|
|
34690
|
+
);
|
|
34691
|
+
if (!parsed.success)
|
|
34692
|
+
return c.json(
|
|
34693
|
+
{ error: "Invalid Pixel details.", issues: parsed.error.issues },
|
|
34694
|
+
400
|
|
34695
|
+
);
|
|
34696
|
+
const pixel = await createAnalyticsPixel({
|
|
34697
|
+
siteId: c.req.param("siteId"),
|
|
34698
|
+
userId: Number(c.get("user").id),
|
|
34699
|
+
...parsed.data
|
|
34700
|
+
});
|
|
34701
|
+
return c.json(
|
|
34702
|
+
{ ok: true, pixel: { ...pixel, snippet: pixelSnippet(pixel.public_id) } },
|
|
34703
|
+
201
|
|
34704
|
+
);
|
|
34705
|
+
});
|
|
34706
|
+
analyticsApp.get("/sites/:siteId/campaign-links", auth3, async (c) => {
|
|
34707
|
+
const url = new URL(c.req.url);
|
|
34708
|
+
const data = await listAnalyticsCampaignLinks(
|
|
34709
|
+
c.req.param("siteId"),
|
|
34710
|
+
Number(c.get("user").id),
|
|
34711
|
+
reportPage(url)
|
|
34712
|
+
);
|
|
34713
|
+
return c.json({
|
|
34714
|
+
ok: true,
|
|
34715
|
+
data: {
|
|
34716
|
+
...data,
|
|
34717
|
+
links: data.links.map((link) => ({
|
|
34718
|
+
...link,
|
|
34719
|
+
short_url: `${url.origin}/analytics/l/${link.short_code}`
|
|
34720
|
+
}))
|
|
34721
|
+
}
|
|
34722
|
+
});
|
|
34723
|
+
});
|
|
34724
|
+
analyticsApp.get("/sites/:siteId/forms", auth3, async (c) => {
|
|
34725
|
+
const url = new URL(c.req.url);
|
|
34726
|
+
const data = await listAnalyticsForms(
|
|
34727
|
+
c.req.param("siteId"),
|
|
34728
|
+
Number(c.get("user").id),
|
|
34729
|
+
reportPage(url)
|
|
34730
|
+
);
|
|
34731
|
+
return c.json({
|
|
34732
|
+
ok: true,
|
|
34733
|
+
data: {
|
|
34734
|
+
...data,
|
|
34735
|
+
forms: data.forms.map((form) => ({
|
|
34736
|
+
...form,
|
|
34737
|
+
snippet: formSnippet(form.public_id),
|
|
34738
|
+
embed_url: `${url.origin}/analytics/forms/${form.public_id}/embed`
|
|
34739
|
+
}))
|
|
34740
|
+
}
|
|
34741
|
+
});
|
|
34742
|
+
});
|
|
34743
|
+
analyticsApp.post("/sites/:siteId/forms", auth3, async (c) => {
|
|
34744
|
+
const parsed = FormInputSchema.safeParse(
|
|
34745
|
+
await c.req.json().catch(() => null)
|
|
34746
|
+
);
|
|
34747
|
+
if (!parsed.success)
|
|
34748
|
+
return c.json(
|
|
34749
|
+
{ ok: false, error: "Invalid form.", issues: parsed.error.issues },
|
|
34750
|
+
400
|
|
34751
|
+
);
|
|
34752
|
+
const form = await createAnalyticsForm({
|
|
34753
|
+
siteId: c.req.param("siteId"),
|
|
34754
|
+
userId: Number(c.get("user").id),
|
|
34755
|
+
...parsed.data
|
|
34756
|
+
});
|
|
34757
|
+
return c.json(
|
|
34758
|
+
{
|
|
34759
|
+
ok: true,
|
|
34760
|
+
form: {
|
|
34761
|
+
...form,
|
|
34762
|
+
snippet: formSnippet(form.public_id),
|
|
34763
|
+
embed_url: `${new URL(c.req.url).origin}/analytics/forms/${form.public_id}/embed`
|
|
34764
|
+
}
|
|
34765
|
+
},
|
|
34766
|
+
201
|
|
34767
|
+
);
|
|
34768
|
+
});
|
|
34769
|
+
analyticsApp.get("/sites/:siteId/crm-imports", auth3, async (c) => {
|
|
34770
|
+
const data = await listAnalyticsCrmImports(
|
|
34771
|
+
c.req.param("siteId"),
|
|
34772
|
+
Number(c.get("user").id),
|
|
34773
|
+
reportPage(new URL(c.req.url))
|
|
34774
|
+
);
|
|
34775
|
+
return c.json({ ok: true, data });
|
|
34776
|
+
});
|
|
34777
|
+
analyticsApp.get("/sites/:siteId/people", auth3, async (c) => {
|
|
34778
|
+
const data = await listAnalyticsPeople(
|
|
34779
|
+
c.req.param("siteId"),
|
|
34780
|
+
Number(c.get("user").id),
|
|
34781
|
+
reportPage(new URL(c.req.url))
|
|
34782
|
+
);
|
|
34783
|
+
return c.json({ ok: true, data });
|
|
34784
|
+
});
|
|
34785
|
+
analyticsApp.get("/sites/:siteId/people/:personId/journey", auth3, async (c) => {
|
|
34786
|
+
const data = await getAnalyticsPersonJourney(
|
|
34787
|
+
c.req.param("siteId"),
|
|
34788
|
+
c.req.param("personId"),
|
|
34789
|
+
Number(c.get("user").id),
|
|
34790
|
+
reportPage(new URL(c.req.url))
|
|
34791
|
+
);
|
|
34792
|
+
return c.json({ ok: true, data });
|
|
34793
|
+
});
|
|
34794
|
+
analyticsApp.post("/sites/:siteId/crm-imports", auth3, async (c) => {
|
|
34795
|
+
const parsed = CrmImportSchema.safeParse(
|
|
34796
|
+
await c.req.json().catch(() => null)
|
|
34797
|
+
);
|
|
34798
|
+
if (!parsed.success)
|
|
34799
|
+
return c.json(
|
|
34800
|
+
{ ok: false, error: "Invalid CRM import.", issues: parsed.error.issues },
|
|
34801
|
+
400
|
|
34802
|
+
);
|
|
34803
|
+
const csv = Papa2.parse(parsed.data.csv, {
|
|
34804
|
+
header: true,
|
|
34805
|
+
skipEmptyLines: "greedy",
|
|
34806
|
+
transformHeader: (header) => header.trim()
|
|
34807
|
+
});
|
|
34808
|
+
if (csv.errors.length)
|
|
34809
|
+
return c.json(
|
|
34810
|
+
{
|
|
34811
|
+
ok: false,
|
|
34812
|
+
error: "CSV could not be parsed.",
|
|
34813
|
+
issues: csv.errors.slice(0, 10)
|
|
34814
|
+
},
|
|
34815
|
+
400
|
|
34816
|
+
);
|
|
34817
|
+
if (csv.data.length > 2500)
|
|
34818
|
+
return c.json(
|
|
34819
|
+
{ ok: false, error: "Import files are limited to 2,500 rows per batch." },
|
|
34820
|
+
400
|
|
34821
|
+
);
|
|
34822
|
+
const pick = (row, header) => header ? String(row[header] ?? "").trim() : "";
|
|
34823
|
+
const queued = [];
|
|
34824
|
+
const identities = [];
|
|
34825
|
+
let rejectedCount = 0;
|
|
34826
|
+
for (const [index, row] of csv.data.entries()) {
|
|
34827
|
+
const email = pick(row, parsed.data.mapping.email).toLowerCase();
|
|
34828
|
+
const firstName = pick(row, parsed.data.mapping.firstName);
|
|
34829
|
+
const lastName = pick(row, parsed.data.mapping.lastName);
|
|
34830
|
+
const explicitName = pick(row, parsed.data.mapping.name);
|
|
34831
|
+
const fullName = explicitName || [firstName, lastName].filter(Boolean).join(" ");
|
|
34832
|
+
const phone = pick(row, parsed.data.mapping.phone);
|
|
34833
|
+
const company = pick(row, parsed.data.mapping.company);
|
|
34834
|
+
const externalId = pick(row, parsed.data.mapping.externalId);
|
|
34835
|
+
if (!email && !phone && !externalId && !fullName) {
|
|
34836
|
+
rejectedCount += 1;
|
|
34837
|
+
continue;
|
|
34838
|
+
}
|
|
34839
|
+
const digest2 = createHash15("sha256").update(
|
|
34840
|
+
`${parsed.data.sourceSystem}:${externalId || email || phone || `${fullName}:${index}`}`
|
|
34841
|
+
).digest("hex").slice(0, 16);
|
|
34842
|
+
const path5 = `Leads/person-${digest2}`;
|
|
34843
|
+
const crmPersonRef = `People::${path5}`;
|
|
34844
|
+
const props = {
|
|
34845
|
+
type: "person",
|
|
34846
|
+
status: "active",
|
|
34847
|
+
person_name: fullName || email || phone || "Imported contact",
|
|
34848
|
+
...firstName ? { first_name: firstName } : {},
|
|
34849
|
+
...lastName ? { last_name: lastName } : {},
|
|
34850
|
+
...email ? { email } : {},
|
|
34851
|
+
...phone ? { phone, text_phone: phone } : {},
|
|
34852
|
+
...company ? { company } : {},
|
|
34853
|
+
...externalId ? { external_id: externalId } : {},
|
|
34854
|
+
import_fields: row,
|
|
34855
|
+
source_type: "crm_csv_import",
|
|
34856
|
+
source_ref: `${parsed.data.sourceSystem}:${digest2}`,
|
|
34857
|
+
summary: `Imported from ${parsed.data.sourceSystem}`,
|
|
34858
|
+
embedding_summary: `${fullName || "Contact"} imported from ${parsed.data.sourceSystem}`,
|
|
34859
|
+
related: [],
|
|
34860
|
+
related_vault_notes: [],
|
|
34861
|
+
embed: true,
|
|
34862
|
+
embed_priority: "normal"
|
|
34863
|
+
};
|
|
34864
|
+
const title = fullName || email || phone || "Imported contact";
|
|
34865
|
+
const payload = {
|
|
34866
|
+
vault: "People",
|
|
34867
|
+
path: path5,
|
|
34868
|
+
title,
|
|
34869
|
+
content: `# ${title}
|
|
34870
|
+
|
|
34871
|
+
## Identity
|
|
34872
|
+
Imported from ${parsed.data.sourceSystem}.
|
|
34873
|
+
|
|
34874
|
+
## Relationship
|
|
34875
|
+
Review and qualify this contact.
|
|
34876
|
+
|
|
34877
|
+
## Known Context
|
|
34878
|
+
Imported through the Site Analytics CRM workflow.
|
|
34879
|
+
|
|
34880
|
+
## Interaction History
|
|
34881
|
+
- ${(/* @__PURE__ */ new Date()).toISOString()}: Imported from ${parsed.data.filename}.
|
|
34882
|
+
|
|
34883
|
+
## Open Loops
|
|
34884
|
+
- Verify attribution and follow-up status.`,
|
|
34885
|
+
props
|
|
34886
|
+
};
|
|
34887
|
+
queued.push({
|
|
34888
|
+
crmPersonRef,
|
|
34889
|
+
payloadCiphertext: encryptMemoryKey(JSON.stringify(payload))
|
|
34890
|
+
});
|
|
34891
|
+
identities.push({
|
|
34892
|
+
crmPersonRef,
|
|
34893
|
+
email: email || void 0,
|
|
34894
|
+
phone: phone || void 0
|
|
34895
|
+
});
|
|
34896
|
+
}
|
|
34897
|
+
const imported = await createAnalyticsCrmImport({
|
|
34898
|
+
siteId: c.req.param("siteId"),
|
|
34899
|
+
userId: Number(c.get("user").id),
|
|
34900
|
+
sourceSystem: parsed.data.sourceSystem,
|
|
34901
|
+
filename: parsed.data.filename,
|
|
34902
|
+
rows: queued,
|
|
34903
|
+
rejectedCount
|
|
34904
|
+
});
|
|
34905
|
+
for (const identity of identities) {
|
|
34906
|
+
await linkAnalyticsFormIdentity({
|
|
34907
|
+
siteId: c.req.param("siteId"),
|
|
34908
|
+
...identity
|
|
34909
|
+
});
|
|
34910
|
+
}
|
|
34911
|
+
return c.json({ ok: true, import: imported }, 202);
|
|
34912
|
+
});
|
|
34913
|
+
analyticsApp.get("/sites/:siteId/activation-destinations", auth3, async (c) => {
|
|
34914
|
+
const destinations = await listAnalyticsActivationDestinations(
|
|
34915
|
+
c.req.param("siteId"),
|
|
34916
|
+
Number(c.get("user").id)
|
|
34917
|
+
);
|
|
34918
|
+
return c.json({ ok: true, destinations });
|
|
34919
|
+
});
|
|
34920
|
+
analyticsApp.post("/sites/:siteId/activation-destinations", auth3, async (c) => {
|
|
34921
|
+
const parsed = ActivationDestinationSchema.safeParse(
|
|
34922
|
+
await c.req.json().catch(() => null)
|
|
34923
|
+
);
|
|
34924
|
+
if (!parsed.success)
|
|
34925
|
+
return c.json(
|
|
34926
|
+
{
|
|
34927
|
+
ok: false,
|
|
34928
|
+
error: "Invalid activation destination.",
|
|
34929
|
+
issues: parsed.error.issues
|
|
34930
|
+
},
|
|
34931
|
+
400
|
|
34932
|
+
);
|
|
34933
|
+
const destination = await createAnalyticsActivationDestination({
|
|
34934
|
+
siteId: c.req.param("siteId"),
|
|
34935
|
+
userId: Number(c.get("user").id),
|
|
34936
|
+
...parsed.data
|
|
34937
|
+
});
|
|
34938
|
+
return c.json({ ok: true, destination }, 201);
|
|
34939
|
+
});
|
|
34940
|
+
analyticsApp.delete(
|
|
34941
|
+
"/sites/:siteId/activation-destinations/:destinationId",
|
|
34942
|
+
auth3,
|
|
34943
|
+
async (c) => {
|
|
34944
|
+
await archiveAnalyticsActivationDestination({
|
|
34945
|
+
siteId: c.req.param("siteId"),
|
|
34946
|
+
destinationId: c.req.param("destinationId"),
|
|
34947
|
+
userId: Number(c.get("user").id)
|
|
34948
|
+
});
|
|
34949
|
+
return c.json({ ok: true });
|
|
34950
|
+
}
|
|
34951
|
+
);
|
|
34952
|
+
analyticsApp.post("/sites/:siteId/campaign-links", auth3, async (c) => {
|
|
34953
|
+
const parsed = CampaignLinkSchema.safeParse(
|
|
34954
|
+
await c.req.json().catch(() => null)
|
|
34955
|
+
);
|
|
34956
|
+
if (!parsed.success)
|
|
34957
|
+
return c.json(
|
|
34958
|
+
{
|
|
34959
|
+
ok: false,
|
|
34960
|
+
error: "Invalid campaign link.",
|
|
34961
|
+
issues: parsed.error.issues
|
|
34962
|
+
},
|
|
34963
|
+
400
|
|
34964
|
+
);
|
|
34965
|
+
const link = await createAnalyticsCampaignLink({
|
|
34966
|
+
siteId: c.req.param("siteId"),
|
|
34967
|
+
userId: Number(c.get("user").id),
|
|
34968
|
+
...parsed.data
|
|
34969
|
+
});
|
|
34970
|
+
return c.json(
|
|
34971
|
+
{
|
|
34972
|
+
ok: true,
|
|
34973
|
+
link: {
|
|
34974
|
+
...link,
|
|
34975
|
+
short_url: `${new URL(c.req.url).origin}/analytics/l/${link.short_code}`
|
|
34976
|
+
}
|
|
34977
|
+
},
|
|
34978
|
+
201
|
|
34979
|
+
);
|
|
34980
|
+
});
|
|
34981
|
+
analyticsApp.delete(
|
|
34982
|
+
"/sites/:siteId/campaign-links/:linkId",
|
|
34983
|
+
auth3,
|
|
34984
|
+
async (c) => {
|
|
34985
|
+
await archiveAnalyticsCampaignLink({
|
|
34986
|
+
siteId: c.req.param("siteId"),
|
|
34987
|
+
linkId: c.req.param("linkId"),
|
|
34988
|
+
userId: Number(c.get("user").id)
|
|
34989
|
+
});
|
|
34990
|
+
return c.json({ ok: true });
|
|
34991
|
+
}
|
|
34992
|
+
);
|
|
34993
|
+
analyticsApp.patch("/sites/:siteId/pixels/:pixelId", auth3, async (c) => {
|
|
34994
|
+
const parsed = PixelUpdateSchema.safeParse(
|
|
34995
|
+
await c.req.json().catch(() => null)
|
|
34996
|
+
);
|
|
34997
|
+
if (!parsed.success)
|
|
34998
|
+
return c.json(
|
|
34999
|
+
{ error: "Invalid Pixel changes.", issues: parsed.error.issues },
|
|
35000
|
+
400
|
|
35001
|
+
);
|
|
35002
|
+
await updateAnalyticsPixel({
|
|
35003
|
+
siteId: c.req.param("siteId"),
|
|
35004
|
+
pixelId: c.req.param("pixelId"),
|
|
35005
|
+
userId: Number(c.get("user").id),
|
|
35006
|
+
...parsed.data
|
|
35007
|
+
});
|
|
35008
|
+
return c.json({ ok: true });
|
|
35009
|
+
});
|
|
35010
|
+
analyticsApp.post(
|
|
35011
|
+
"/sites/:siteId/pixels/:pixelId/domains/:hostname/approve",
|
|
35012
|
+
auth3,
|
|
35013
|
+
async (c) => {
|
|
35014
|
+
await setAnalyticsPixelDomainState({
|
|
35015
|
+
siteId: c.req.param("siteId"),
|
|
35016
|
+
pixelId: c.req.param("pixelId"),
|
|
35017
|
+
userId: Number(c.get("user").id),
|
|
35018
|
+
hostname: c.req.param("hostname"),
|
|
35019
|
+
state: "approved"
|
|
35020
|
+
});
|
|
35021
|
+
return c.json({ ok: true });
|
|
35022
|
+
}
|
|
35023
|
+
);
|
|
35024
|
+
analyticsApp.post(
|
|
35025
|
+
"/sites/:siteId/pixels/:pixelId/domains/:hostname/reject",
|
|
35026
|
+
auth3,
|
|
35027
|
+
async (c) => {
|
|
35028
|
+
await setAnalyticsPixelDomainState({
|
|
35029
|
+
siteId: c.req.param("siteId"),
|
|
35030
|
+
pixelId: c.req.param("pixelId"),
|
|
35031
|
+
userId: Number(c.get("user").id),
|
|
35032
|
+
hostname: c.req.param("hostname"),
|
|
35033
|
+
state: "rejected"
|
|
35034
|
+
});
|
|
35035
|
+
return c.json({ ok: true });
|
|
35036
|
+
}
|
|
35037
|
+
);
|
|
35038
|
+
for (const [path5, load] of [
|
|
35039
|
+
["/sites/:siteId/overview", analyticsOverview],
|
|
35040
|
+
["/sites/:siteId/timeseries", analyticsTimeseries],
|
|
35041
|
+
["/sites/:siteId/acquisition", analyticsAcquisition],
|
|
35042
|
+
["/sites/:siteId/content", analyticsContent],
|
|
35043
|
+
["/sites/:siteId/events", analyticsEventCounts]
|
|
35044
|
+
]) {
|
|
35045
|
+
analyticsApp.get(path5, auth3, async (c) => {
|
|
35046
|
+
const url = new URL(c.req.url);
|
|
35047
|
+
const filters = reportFilters(url);
|
|
35048
|
+
validateReportWindow(filters);
|
|
35049
|
+
const paginated = path5.endsWith("/acquisition") || path5.endsWith("/content") || path5.endsWith("/events");
|
|
35050
|
+
const data = await load(
|
|
35051
|
+
c.req.param("siteId"),
|
|
35052
|
+
Number(c.get("user").id),
|
|
35053
|
+
filters,
|
|
35054
|
+
paginated ? reportPage(url) : void 0
|
|
35055
|
+
);
|
|
35056
|
+
return c.json({ ok: true, data, filters });
|
|
35057
|
+
});
|
|
35058
|
+
}
|
|
35059
|
+
analyticsApp.get("/sites/:siteId/dashboard", auth3, async (c) => {
|
|
35060
|
+
const url = new URL(c.req.url);
|
|
35061
|
+
const filters = reportFilters(url);
|
|
35062
|
+
validateReportWindow(filters);
|
|
35063
|
+
const siteId = c.req.param("siteId");
|
|
35064
|
+
const userId = Number(c.get("user").id);
|
|
35065
|
+
const [
|
|
35066
|
+
overview,
|
|
35067
|
+
timeseries,
|
|
35068
|
+
acquisition,
|
|
35069
|
+
devices,
|
|
35070
|
+
activity,
|
|
35071
|
+
geography,
|
|
35072
|
+
content,
|
|
35073
|
+
conversions
|
|
35074
|
+
] = await Promise.all([
|
|
35075
|
+
analyticsOverview(siteId, userId, filters),
|
|
35076
|
+
analyticsTimeseries(siteId, userId, filters),
|
|
35077
|
+
analyticsAcquisition(siteId, userId, filters, { limit: 25 }),
|
|
35078
|
+
analyticsDimensions(siteId, userId, filters, "device", { limit: 10 }),
|
|
35079
|
+
analyticsDimensions(siteId, userId, filters, "weekday_hour", {
|
|
35080
|
+
limit: 100
|
|
35081
|
+
}),
|
|
35082
|
+
analyticsDimensions(siteId, userId, filters, "region", { limit: 60 }),
|
|
35083
|
+
analyticsContent(siteId, userId, filters, { limit: 8 }),
|
|
35084
|
+
analyticsConversions(siteId, userId, filters, { limit: 8 })
|
|
35085
|
+
]);
|
|
35086
|
+
let comparison = null;
|
|
35087
|
+
if (url.searchParams.get("compare") === "1") {
|
|
35088
|
+
const start = Date.parse(filters.start);
|
|
35089
|
+
const end = Date.parse(filters.end);
|
|
35090
|
+
const duration = end - start;
|
|
35091
|
+
const previousFilters = {
|
|
35092
|
+
...filters,
|
|
35093
|
+
start: new Date(start - duration).toISOString(),
|
|
35094
|
+
end: new Date(start).toISOString()
|
|
35095
|
+
};
|
|
35096
|
+
const [previousOverview, previousTimeseries] = await Promise.all([
|
|
35097
|
+
analyticsOverview(siteId, userId, previousFilters),
|
|
35098
|
+
analyticsTimeseries(siteId, userId, previousFilters)
|
|
35099
|
+
]);
|
|
35100
|
+
comparison = { overview: previousOverview, timeseries: previousTimeseries };
|
|
35101
|
+
}
|
|
35102
|
+
return c.json({
|
|
35103
|
+
ok: true,
|
|
35104
|
+
data: {
|
|
35105
|
+
overview,
|
|
35106
|
+
timeseries,
|
|
35107
|
+
acquisition,
|
|
35108
|
+
devices,
|
|
35109
|
+
activity,
|
|
35110
|
+
geography,
|
|
35111
|
+
content,
|
|
35112
|
+
conversions,
|
|
35113
|
+
comparison
|
|
35114
|
+
},
|
|
35115
|
+
filters
|
|
35116
|
+
});
|
|
35117
|
+
});
|
|
35118
|
+
analyticsApp.get("/sites/:siteId/dimensions", auth3, async (c) => {
|
|
35119
|
+
const url = new URL(c.req.url);
|
|
35120
|
+
const filters = reportFilters(url);
|
|
35121
|
+
validateReportWindow(filters);
|
|
35122
|
+
const dimension = url.searchParams.get("dimension") || "device";
|
|
35123
|
+
if (!["device", "source", "country", "region", "weekday_hour"].includes(
|
|
35124
|
+
dimension
|
|
35125
|
+
)) {
|
|
35126
|
+
return c.json(
|
|
35127
|
+
{
|
|
35128
|
+
ok: false,
|
|
35129
|
+
code: "invalid_dimension",
|
|
35130
|
+
error: "Choose a supported dimension."
|
|
35131
|
+
},
|
|
35132
|
+
400
|
|
35133
|
+
);
|
|
35134
|
+
}
|
|
35135
|
+
const data = await analyticsDimensions(
|
|
35136
|
+
c.req.param("siteId"),
|
|
35137
|
+
Number(c.get("user").id),
|
|
35138
|
+
filters,
|
|
35139
|
+
dimension,
|
|
35140
|
+
reportPage(url)
|
|
35141
|
+
);
|
|
35142
|
+
return c.json({ ok: true, data, filters });
|
|
35143
|
+
});
|
|
35144
|
+
analyticsApp.get("/sites/:siteId/conversions", auth3, async (c) => {
|
|
35145
|
+
const url = new URL(c.req.url);
|
|
35146
|
+
const filters = reportFilters(url);
|
|
35147
|
+
validateReportWindow(filters);
|
|
35148
|
+
const data = await analyticsConversions(
|
|
35149
|
+
c.req.param("siteId"),
|
|
35150
|
+
Number(c.get("user").id),
|
|
35151
|
+
filters,
|
|
35152
|
+
reportPage(url)
|
|
35153
|
+
);
|
|
35154
|
+
return c.json({ ok: true, data, filters });
|
|
35155
|
+
});
|
|
35156
|
+
analyticsApp.get("/sites/:siteId/paths", auth3, async (c) => {
|
|
35157
|
+
const url = new URL(c.req.url);
|
|
35158
|
+
const filters = reportFilters(url);
|
|
35159
|
+
validateReportWindow(filters);
|
|
35160
|
+
const data = await analyticsPaths(
|
|
35161
|
+
c.req.param("siteId"),
|
|
35162
|
+
Number(c.get("user").id),
|
|
35163
|
+
filters,
|
|
35164
|
+
reportPage(url)
|
|
35165
|
+
);
|
|
35166
|
+
return c.json({ ok: true, data, filters });
|
|
35167
|
+
});
|
|
35168
|
+
analyticsApp.get("/sites/:siteId/channel-breakdown", auth3, async (c) => {
|
|
35169
|
+
const filters = reportFilters(new URL(c.req.url));
|
|
35170
|
+
const data = await analyticsChannelBreakdown(
|
|
35171
|
+
c.req.param("siteId"),
|
|
35172
|
+
Number(c.get("user").id),
|
|
35173
|
+
filters
|
|
35174
|
+
);
|
|
35175
|
+
return c.json({ ok: true, data });
|
|
35176
|
+
});
|
|
35177
|
+
analyticsApp.get("/sites/:siteId/health", auth3, async (c) => {
|
|
35178
|
+
const health = await analyticsHealth(
|
|
35179
|
+
c.req.param("siteId"),
|
|
35180
|
+
Number(c.get("user").id)
|
|
35181
|
+
);
|
|
35182
|
+
return c.json({ ok: true, health }, 200, { "Cache-Control": "no-store" });
|
|
35183
|
+
});
|
|
35184
|
+
analyticsApp.post("/sites/:siteId/exports", auth3, async (c) => {
|
|
35185
|
+
const parsed = ExportSchema.safeParse(await c.req.json().catch(() => ({})));
|
|
35186
|
+
if (!parsed.success)
|
|
35187
|
+
return c.json(
|
|
35188
|
+
{
|
|
35189
|
+
ok: false,
|
|
35190
|
+
error: "Invalid export request.",
|
|
35191
|
+
issues: parsed.error.issues
|
|
35192
|
+
},
|
|
35193
|
+
400
|
|
35194
|
+
);
|
|
35195
|
+
const filters = reportFilters(new URL(c.req.url));
|
|
35196
|
+
validateReportWindow(filters);
|
|
35197
|
+
const artifact = await createAnalyticsExport({
|
|
35198
|
+
siteId: c.req.param("siteId"),
|
|
35199
|
+
userId: Number(c.get("user").id),
|
|
35200
|
+
filters,
|
|
35201
|
+
...parsed.data,
|
|
35202
|
+
idempotencyKey: c.req.header("idempotency-key") || parsed.data.idempotencyKey
|
|
35203
|
+
});
|
|
35204
|
+
return c.json({ ok: true, artifact }, 201);
|
|
35205
|
+
});
|
|
35206
|
+
analyticsApp.onError((error, c) => {
|
|
35207
|
+
if (error instanceof AnalyticsRepositoryError) {
|
|
35208
|
+
return c.json(
|
|
35209
|
+
{ ok: false, code: error.code, error: error.message },
|
|
35210
|
+
error.status
|
|
35211
|
+
);
|
|
35212
|
+
}
|
|
35213
|
+
console.error(
|
|
35214
|
+
"[analytics]",
|
|
35215
|
+
error instanceof Error ? error.message : String(error)
|
|
35216
|
+
);
|
|
35217
|
+
return c.json(
|
|
35218
|
+
{
|
|
35219
|
+
ok: false,
|
|
35220
|
+
code: "analytics_internal_error",
|
|
35221
|
+
error: "Site Analytics could not complete the request."
|
|
35222
|
+
},
|
|
35223
|
+
500
|
|
35224
|
+
);
|
|
35225
|
+
});
|
|
35226
|
+
|
|
35227
|
+
// src/api/analytics-delivery.ts
|
|
35228
|
+
function safeDeliveryError(error) {
|
|
35229
|
+
if (!error) return "memory_write_failed";
|
|
35230
|
+
if (error.includes("configured") || error.includes("credential")) return "memory_credential_unavailable";
|
|
35231
|
+
if (error.includes("401") || error.includes("403")) return "memory_authorization_failed";
|
|
35232
|
+
if (error.includes("429")) return "memory_rate_limited";
|
|
35233
|
+
return "memory_write_failed";
|
|
35234
|
+
}
|
|
35235
|
+
async function deliverPendingAnalyticsForms(limit = 10) {
|
|
35236
|
+
const jobs = await claimAnalyticsFormDeliveryJobs(limit);
|
|
35237
|
+
let delivered = 0;
|
|
35238
|
+
let deferred = 0;
|
|
35239
|
+
for (const job of jobs) {
|
|
35240
|
+
const payloadText = decryptMemoryKey(job.payloadCiphertext);
|
|
35241
|
+
const user = await getUserById(job.ownerUserId);
|
|
35242
|
+
if (!payloadText || !user) {
|
|
35243
|
+
await deferAnalyticsFormDelivery(job.id, job.attempts, payloadText ? "owner_not_found" : "payload_unreadable");
|
|
35244
|
+
deferred += 1;
|
|
35245
|
+
continue;
|
|
35246
|
+
}
|
|
35247
|
+
let payload;
|
|
35248
|
+
try {
|
|
35249
|
+
payload = JSON.parse(payloadText);
|
|
35250
|
+
} catch {
|
|
35251
|
+
await deferAnalyticsFormDelivery(job.id, job.attempts, "payload_invalid");
|
|
35252
|
+
deferred += 1;
|
|
35253
|
+
continue;
|
|
35254
|
+
}
|
|
35255
|
+
const { key, error } = await getOrCreateUserMemoryKey(user);
|
|
35256
|
+
const result = key ? await memoryCall("putTool", payload, key) : { ok: false, error };
|
|
35257
|
+
if (result.ok) {
|
|
35258
|
+
await completeAnalyticsFormDelivery(job.id, job.submissionId);
|
|
35259
|
+
delivered += 1;
|
|
35260
|
+
} else {
|
|
35261
|
+
await deferAnalyticsFormDelivery(job.id, job.attempts, safeDeliveryError(result.error));
|
|
35262
|
+
deferred += 1;
|
|
35263
|
+
}
|
|
35264
|
+
}
|
|
35265
|
+
return { claimed: jobs.length, delivered, deferred };
|
|
35266
|
+
}
|
|
35267
|
+
|
|
35268
|
+
// src/api/scheduled-result-routes.ts
|
|
35269
|
+
import { Hono as Hono33 } from "hono";
|
|
35270
|
+
import { z as z35 } from "zod";
|
|
33567
35271
|
|
|
33568
35272
|
// src/api/scheduled-artifact-owner.ts
|
|
33569
|
-
import { createHash as
|
|
35273
|
+
import { createHash as createHash16 } from "crypto";
|
|
33570
35274
|
function scheduledArtifactOwnerIdForApiKey(apiKey) {
|
|
33571
|
-
return
|
|
35275
|
+
return createHash16("sha256").update(apiKey).digest("hex").slice(0, 24);
|
|
33572
35276
|
}
|
|
33573
35277
|
|
|
33574
35278
|
// src/api/scheduled-run-view-links.ts
|
|
33575
|
-
import { createHash as
|
|
35279
|
+
import { createHash as createHash17, randomBytes as randomBytes4, randomUUID as randomUUID17 } from "crypto";
|
|
33576
35280
|
var schemaReady3 = false;
|
|
33577
35281
|
async function ensureScheduledRunViewLinksSchema() {
|
|
33578
35282
|
if (schemaReady3) return;
|
|
@@ -33585,7 +35289,7 @@ async function ensureScheduledRunViewLinksSchema() {
|
|
|
33585
35289
|
artifact_id TEXT NOT NULL,
|
|
33586
35290
|
title TEXT NOT NULL,
|
|
33587
35291
|
summary TEXT NOT NULL,
|
|
33588
|
-
renderer_key TEXT NOT NULL CHECK (renderer_key IN ('editorial_reading_room_v1','personal_authority_v1')),
|
|
35292
|
+
renderer_key TEXT NOT NULL CHECK (renderer_key IN ('editorial_reading_room_v1','personal_authority_v1','newsroom_publisher_v1')),
|
|
33589
35293
|
token_hash TEXT NOT NULL UNIQUE,
|
|
33590
35294
|
expires_at TEXT NOT NULL,
|
|
33591
35295
|
revoked_at TEXT,
|
|
@@ -33597,22 +35301,22 @@ async function ensureScheduledRunViewLinksSchema() {
|
|
|
33597
35301
|
args: []
|
|
33598
35302
|
});
|
|
33599
35303
|
const createSql = String(schema.rows[0]?.sql ?? "");
|
|
33600
|
-
if (createSql.includes("
|
|
35304
|
+
if (!createSql.includes("newsroom_publisher_v1")) {
|
|
33601
35305
|
await db.batch([
|
|
33602
|
-
`DROP TABLE IF EXISTS
|
|
33603
|
-
`CREATE TABLE
|
|
35306
|
+
`DROP TABLE IF EXISTS scheduled_run_view_links_v3`,
|
|
35307
|
+
`CREATE TABLE scheduled_run_view_links_v3 (
|
|
33604
35308
|
share_id TEXT PRIMARY KEY, owner_id TEXT NOT NULL, run_id TEXT NOT NULL,
|
|
33605
35309
|
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')),
|
|
35310
|
+
renderer_key TEXT NOT NULL CHECK (renderer_key IN ('editorial_reading_room_v1','personal_authority_v1','newsroom_publisher_v1')),
|
|
33607
35311
|
token_hash TEXT NOT NULL UNIQUE, expires_at TEXT NOT NULL, revoked_at TEXT,
|
|
33608
35312
|
created_at TEXT NOT NULL
|
|
33609
35313
|
)`,
|
|
33610
|
-
`INSERT INTO
|
|
35314
|
+
`INSERT INTO scheduled_run_view_links_v3
|
|
33611
35315
|
SELECT share_id, owner_id, run_id, artifact_id, title, summary, renderer_key,
|
|
33612
35316
|
token_hash, expires_at, revoked_at, created_at
|
|
33613
35317
|
FROM scheduled_run_view_links`,
|
|
33614
35318
|
`DROP TABLE scheduled_run_view_links`,
|
|
33615
|
-
`ALTER TABLE
|
|
35319
|
+
`ALTER TABLE scheduled_run_view_links_v3 RENAME TO scheduled_run_view_links`
|
|
33616
35320
|
], "write");
|
|
33617
35321
|
}
|
|
33618
35322
|
await db.execute(`
|
|
@@ -33626,7 +35330,7 @@ async function ensureScheduledRunViewLinksSchema() {
|
|
|
33626
35330
|
schemaReady3 = true;
|
|
33627
35331
|
}
|
|
33628
35332
|
function tokenHash2(token4) {
|
|
33629
|
-
return
|
|
35333
|
+
return createHash17("sha256").update(token4).digest("hex");
|
|
33630
35334
|
}
|
|
33631
35335
|
function mapRow(row) {
|
|
33632
35336
|
return {
|
|
@@ -33636,7 +35340,7 @@ function mapRow(row) {
|
|
|
33636
35340
|
artifactId: String(row.artifact_id),
|
|
33637
35341
|
title: String(row.title),
|
|
33638
35342
|
summary: String(row.summary),
|
|
33639
|
-
rendererKey: row.renderer_key === "personal_authority_v1" ? "personal_authority_v1" : "editorial_reading_room_v1",
|
|
35343
|
+
rendererKey: row.renderer_key === "newsroom_publisher_v1" ? "newsroom_publisher_v1" : row.renderer_key === "personal_authority_v1" ? "personal_authority_v1" : "editorial_reading_room_v1",
|
|
33640
35344
|
tokenHash: String(row.token_hash),
|
|
33641
35345
|
expiresAt: String(row.expires_at),
|
|
33642
35346
|
revokedAt: row.revoked_at == null ? null : String(row.revoked_at),
|
|
@@ -33646,7 +35350,7 @@ function mapRow(row) {
|
|
|
33646
35350
|
async function createScheduledRunViewLink(input) {
|
|
33647
35351
|
await ensureScheduledRunViewLinksSchema();
|
|
33648
35352
|
const now = input.now ?? /* @__PURE__ */ new Date();
|
|
33649
|
-
const token4 =
|
|
35353
|
+
const token4 = randomBytes4(32).toString("base64url");
|
|
33650
35354
|
const record = {
|
|
33651
35355
|
shareId: randomUUID17(),
|
|
33652
35356
|
ownerId: input.ownerId,
|
|
@@ -33713,8 +35417,8 @@ async function revokeScheduledRunViewLink(ownerId, runId, shareId, now = /* @__P
|
|
|
33713
35417
|
}
|
|
33714
35418
|
|
|
33715
35419
|
// src/personal-authority/render.ts
|
|
33716
|
-
import { createHash as
|
|
33717
|
-
function
|
|
35420
|
+
import { createHash as createHash18 } from "crypto";
|
|
35421
|
+
function escapeHtml7(value) {
|
|
33718
35422
|
return value.replace(/[&<>"']/g, (character) => ({
|
|
33719
35423
|
"&": "&",
|
|
33720
35424
|
"<": "<",
|
|
@@ -33725,13 +35429,13 @@ function escapeHtml6(value) {
|
|
|
33725
35429
|
}
|
|
33726
35430
|
function safeHref(value) {
|
|
33727
35431
|
const trimmed = value.trim();
|
|
33728
|
-
return /^(?:https?:|mailto:|tel:|#|\/)/i.test(trimmed) ?
|
|
35432
|
+
return /^(?:https?:|mailto:|tel:|#|\/)/i.test(trimmed) ? escapeHtml7(trimmed) : "#";
|
|
33729
35433
|
}
|
|
33730
35434
|
function initials(name) {
|
|
33731
35435
|
return name.split(/\s+/).filter(Boolean).slice(0, 2).map((part) => part[0]?.toUpperCase()).join("") || "PA";
|
|
33732
35436
|
}
|
|
33733
35437
|
function action(action2, secondary = false) {
|
|
33734
|
-
return `<a class="button${secondary ? " button--secondary" : ""}" href="${safeHref(action2.href)}">${
|
|
35438
|
+
return `<a class="button${secondary ? " button--secondary" : ""}" href="${safeHref(action2.href)}">${escapeHtml7(action2.label)} <span aria-hidden="true">\u2197</span></a>`;
|
|
33735
35439
|
}
|
|
33736
35440
|
function section(enabled, html) {
|
|
33737
35441
|
return enabled ? html : "";
|
|
@@ -33740,7 +35444,7 @@ function renderPersonalAuthorityV1(input, config, generatedAt) {
|
|
|
33740
35444
|
const flags = config.featureFlags;
|
|
33741
35445
|
const brand = config.brandName || input.name;
|
|
33742
35446
|
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)}">${
|
|
35447
|
+
const social = flags.socialLinks && input.socialLinks.length > 0 ? `<nav class="social" aria-label="Professional profiles">${input.socialLinks.map((link) => `<a href="${safeHref(link.href)}">${escapeHtml7(link.label)}</a>`).join("")}</nav>` : "";
|
|
33744
35448
|
const navigation = flags.navigation ? `<button class="menu" type="button" aria-expanded="false" aria-controls="nav">Menu</button><nav id="nav" class="nav" aria-label="Primary navigation"><a href="#about">About</a><a href="#services">Work</a><a href="#proof">Proof</a><a href="#contact">Contact</a></nav>` : "";
|
|
33745
35449
|
const authority = input.authority;
|
|
33746
35450
|
const consultation = input.consultation;
|
|
@@ -33752,32 +35456,144 @@ function renderPersonalAuthorityV1(input, config, generatedAt) {
|
|
|
33752
35456
|
<html lang="en" data-theme="${config.theme}">
|
|
33753
35457
|
<head>
|
|
33754
35458
|
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
33755
|
-
<meta name="description" content="${
|
|
35459
|
+
<meta name="description" content="${escapeHtml7(input.summary)}"><title>${escapeHtml7(input.name)} \u2014 ${escapeHtml7(input.professionalTitle)}</title>
|
|
33756
35460
|
<style>
|
|
33757
35461
|
:root{--blue:#2f66eb;--blue-soft:#dbe6ff;--ink:#050708;--paper:#f7f8fb;--white:#fff;--gray:#9ca5b5;--line:#27303b;--serif:Georgia,'Times New Roman',serif;--sans:Arial,Helvetica,sans-serif}*{box-sizing:border-box}html{scroll-behavior:smooth}body{margin:0;background:var(--ink);color:var(--white);font-family:var(--sans);line-height:1.55}a{color:inherit}.skip{position:absolute;left:-9999px}.skip:focus{left:16px;top:16px;z-index:9;background:#fff;color:#000;padding:10px}.header{height:88px;display:flex;align-items:center;justify-content:space-between;padding:0 clamp(20px,4vw,72px);border-bottom:1px solid var(--line);position:relative;z-index:3;background:rgba(5,7,8,.96)}.brand{display:flex;align-items:center;gap:12px;text-decoration:none;font-family:var(--serif);font-size:24px}.mark{width:40px;height:40px;border-radius:50%;display:grid;place-items:center;background:var(--blue);font-family:var(--sans);font-weight:700}.nav{display:flex;align-items:center;gap:28px}.nav a{text-decoration:none;color:#c8ced8;font-size:14px}.nav a:hover{color:#fff}.menu{display:none;background:transparent;color:#fff;border:1px solid #5b6471;padding:10px 14px}.hero{display:grid;grid-template-columns:minmax(0,.9fr) minmax(0,1.25fr);min-height:760px;border-bottom:3px solid var(--blue)}.portrait{display:grid;place-items:end center;background:radial-gradient(circle at 50% 38%,#273243 0,#11161d 38%,#060708 70%);overflow:hidden}.portrait-card{width:min(82%,560px);aspect-ratio:4/5;display:grid;place-items:center;align-self:end;border:1px solid #263142;border-bottom:0;background:linear-gradient(145deg,#252d38,#0b0d10 58%);font-family:var(--serif);font-size:clamp(80px,13vw,180px);color:#d9e4ff;text-shadow:0 0 42px rgba(47,102,235,.5)}.hero-copy{padding:clamp(44px,7vw,110px) clamp(24px,6vw,100px);display:flex;flex-direction:column;justify-content:center;background:linear-gradient(135deg,#080a0d,#101318)}h1,h2,h3,p{margin-top:0}.hero h1{font-family:var(--serif);font-size:clamp(58px,8vw,116px);line-height:.92;font-weight:400;color:var(--blue);margin-bottom:24px}.role{font-family:var(--serif);font-size:clamp(28px,3.8vw,52px);line-height:1.05;border-left:5px solid var(--blue);padding-left:22px;max-width:820px}.summary{color:#c6cbd3;font-size:clamp(17px,1.5vw,22px);max-width:760px}.actions{display:flex;gap:12px;flex-wrap:wrap;margin:24px 0}.button{display:inline-flex;align-items:center;justify-content:space-between;gap:28px;min-height:58px;padding:0 26px;background:var(--blue);border:1px solid var(--blue);text-decoration:none;font-weight:700}.button--secondary{background:transparent;border-color:#5c6572}.social{display:flex;gap:10px;flex-wrap:wrap}.social a{border:1px solid #39414d;border-radius:999px;padding:8px 13px;text-decoration:none;color:#d8dce4;font-size:13px}.light{background:var(--paper);color:#111722;padding:clamp(70px,10vw,150px) clamp(20px,6vw,90px)}.light h2,.section h2{font-family:var(--serif);font-size:clamp(42px,6vw,82px);line-height:1.02;font-weight:400;max-width:1080px}.light p{max-width:920px;color:#4e596a;font-size:18px}.section{padding:clamp(70px,9vw,130px) clamp(20px,6vw,90px);border-bottom:1px solid var(--line)}.section-head{max-width:920px;margin-bottom:46px}.eyebrow{text-transform:uppercase;letter-spacing:.18em;font-size:12px;color:#aab2bf}.consult{display:grid;grid-template-columns:1.3fr .7fr;gap:50px;background:linear-gradient(130deg,#060708,#111721)}.accent-card{min-height:360px;border:1px solid #2a3545;background:linear-gradient(145deg,rgba(47,102,235,.22),transparent 55%);display:grid;place-items:center;font-family:var(--serif);font-size:96px;color:var(--blue)}.grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px}.card{border:1px solid #323a46;padding:26px;min-height:220px;background:#0b0e12}.card h3{font-family:var(--serif);font-size:30px}.card p{color:#b9c0ca}.learning{background:#020303}.topic-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:1px;background:#29313c;border:1px solid #29313c}.topic{background:#050708;padding:30px}.topic strong{display:block;color:var(--blue);text-transform:uppercase;font-size:11px;letter-spacing:.15em}.topic h3{font-family:var(--serif);font-size:30px;margin:8px 0}.topic p{color:#aeb6c1}.proof{background:linear-gradient(180deg,#0a0d11,#050607)}.quotes{display:flex;gap:14px;overflow:auto;scroll-snap-type:x mandatory;padding-bottom:12px}.quote{min-width:min(420px,86vw);scroll-snap-align:start;border:1px solid #343d49;padding:30px;background:#0c1015}.quote q{font-family:var(--serif);font-size:30px;line-height:1.25}.quote span{display:block;margin-top:24px;color:#aeb5c0;font-size:12px;text-transform:uppercase;letter-spacing:.12em}.cta{text-align:center;border-top:3px solid var(--blue);background:radial-gradient(circle at 50% 0,rgba(47,102,235,.25),transparent 50%)}.cta h2{margin-left:auto;margin-right:auto}.footer{display:grid;grid-template-columns:1fr auto;gap:40px;padding:46px clamp(20px,6vw,90px);color:#abb3bf}.footer strong{color:#fff;font-family:var(--serif);font-size:26px}.generated{font-size:11px;color:#737d8b;margin-top:10px}html[data-theme='slate']{--blue:#7f95b8;--blue-soft:#dfe6ef}html[data-theme='monochrome']{--blue:#fff;--blue-soft:#e7e7e7}.sr-only{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0)}
|
|
33758
35462
|
@media(max-width:760px){.header{height:78px}.menu{display:block}.nav{display:none;position:absolute;left:0;right:0;top:77px;padding:18px 20px;background:#090b0e;border-bottom:1px solid #303844;flex-direction:column;align-items:stretch}.nav.open{display:flex}.hero{grid-template-columns:1fr;min-height:0}.portrait{min-height:390px}.portrait-card{width:72%;max-height:360px}.hero-copy{padding:48px 20px 66px}.hero h1{font-size:64px}.role{font-size:34px}.actions{display:grid}.button{width:100%}.consult{grid-template-columns:1fr}.accent-card{min-height:220px}.grid,.topic-grid{grid-template-columns:1fr}.light,.section{padding:64px 20px}.light h2,.section h2{font-size:46px}.footer{grid-template-columns:1fr}}
|
|
33759
35463
|
</style></head>
|
|
33760
35464
|
<body><a class="skip" href="#main">Skip to main content</a>
|
|
33761
|
-
<header class="header"><a class="brand" href="#top"><span class="mark">${
|
|
33762
|
-
<main id="main"><section class="hero" id="top"><div class="portrait" aria-label="Portrait placeholder"><div class="portrait-card" aria-hidden="true">${
|
|
33763
|
-
${section(flags.authority && Boolean(authority), `<section class="light" id="about"><h2>${
|
|
33764
|
-
${section(flags.consultation && Boolean(consultation), `<section class="section consult"><div><p class="eyebrow">${
|
|
33765
|
-
${section(flags.learning && Boolean(learning), `<section class="section learning"><div class="section-head"><h2>${
|
|
33766
|
-
${section(flags.services && Boolean(services), `<section class="section" id="services"><div class="section-head"><h2>${
|
|
33767
|
-
${section(flags.proof && Boolean(proof), `<section class="section proof" id="proof"><div class="section-head"><h2>${
|
|
33768
|
-
${section(flags.finalCta && Boolean(finalCta), `<section class="section cta" id="contact"><p class="eyebrow">${
|
|
33769
|
-
</main><footer class="footer"><div><strong>${
|
|
35465
|
+
<header class="header"><a class="brand" href="#top"><span class="mark">${escapeHtml7(initials(input.name).slice(0, 1))}</span><strong>${escapeHtml7(brand)}</strong></a>${navigation}</header>
|
|
35466
|
+
<main id="main"><section class="hero" id="top"><div class="portrait" aria-label="Portrait placeholder"><div class="portrait-card" aria-hidden="true">${escapeHtml7(initials(input.name))}</div></div><div class="hero-copy"><h1>${escapeHtml7(input.name)}</h1><p class="role">${escapeHtml7(input.professionalTitle)}</p><p class="summary">${escapeHtml7(input.summary)}</p><div class="actions">${action(input.primaryAction)}${input.secondaryAction ? action(input.secondaryAction, true) : ""}</div>${social}</div></section>
|
|
35467
|
+
${section(flags.authority && Boolean(authority), `<section class="light" id="about"><h2>${escapeHtml7(authority?.heading ?? "")}</h2>${authority?.paragraphs.map((paragraph) => `<p>${escapeHtml7(paragraph)}</p>`).join("") ?? ""}</section>`)}
|
|
35468
|
+
${section(flags.consultation && Boolean(consultation), `<section class="section consult"><div><p class="eyebrow">${escapeHtml7(consultation?.eyebrow ?? "Start a conversation")}</p><h2>${escapeHtml7(consultation?.heading ?? "")}</h2><p class="summary">${escapeHtml7(consultation?.summary ?? "")}</p>${consultation ? action(consultation.action) : ""}</div><div class="accent-card" aria-label="Image placeholder">${escapeHtml7(initials(input.name))}</div></section>`)}
|
|
35469
|
+
${section(flags.learning && Boolean(learning), `<section class="section learning"><div class="section-head"><h2>${escapeHtml7(learning?.heading ?? "")}</h2><p class="summary">${escapeHtml7(learning?.summary ?? "")}</p></div><div class="topic-grid">${learning?.topics.map((topic) => `<article class="topic"><strong>${escapeHtml7(topic.title)}</strong><h3>${escapeHtml7(topic.headline)}</h3><p>${escapeHtml7(topic.summary)}</p>${topic.href ? `<a href="${safeHref(topic.href)}">Explore \u2192</a>` : ""}</article>`).join("") ?? ""}</div></section>`)}
|
|
35470
|
+
${section(flags.services && Boolean(services), `<section class="section" id="services"><div class="section-head"><h2>${escapeHtml7(services?.heading ?? "")}</h2><p class="summary">${escapeHtml7(services?.summary ?? "")}</p></div><div class="grid">${services?.offers.map((offer) => `<article class="card"><h3>${escapeHtml7(offer.title)}</h3><p>${escapeHtml7(offer.description)}</p>${offer.action ? action(offer.action, true) : ""}</article>`).join("") ?? ""}</div></section>`)}
|
|
35471
|
+
${section(flags.proof && Boolean(proof), `<section class="section proof" id="proof"><div class="section-head"><h2>${escapeHtml7(proof?.heading ?? "")}</h2><p class="summary">${escapeHtml7(proof?.summary ?? "")}</p></div><div class="quotes">${proof?.testimonials.map((item) => `<article class="quote"><q>${escapeHtml7(item.quote)}</q><span>${escapeHtml7(item.person)}${item.role ? ` / ${escapeHtml7(item.role)}` : ""}</span></article>`).join("") ?? ""}</div></section>`)}
|
|
35472
|
+
${section(flags.finalCta && Boolean(finalCta), `<section class="section cta" id="contact"><p class="eyebrow">${escapeHtml7(finalCta?.eyebrow ?? "Ready when you are")}</p><h2>${escapeHtml7(finalCta?.heading ?? "")}</h2><p class="summary">${escapeHtml7(finalCta?.summary ?? "")}</p>${finalCta ? action(finalCta.action) : ""}</section>`)}
|
|
35473
|
+
</main><footer class="footer"><div><strong>${escapeHtml7(input.name)}</strong><div>${escapeHtml7(input.professionalTitle)}</div>${config.showGeneratedAt ? `<div class="generated">Generated ${escapeHtml7(generatedAtIso)}</div>` : ""}</div><div>${input.location ? `<div>${escapeHtml7(input.location)}</div>` : ""}${input.email ? `<a href="mailto:${escapeHtml7(input.email)}">${escapeHtml7(input.email)}</a>` : ""}${input.phone ? `<div>${escapeHtml7(input.phone)}</div>` : ""}</div></footer>
|
|
33770
35474
|
<script>document.querySelector('.menu')?.addEventListener('click',function(){var nav=document.querySelector('.nav');var open=this.getAttribute('aria-expanded')!=='true';this.setAttribute('aria-expanded',String(open));nav?.classList.toggle('open',open)});document.querySelectorAll('.nav a').forEach(function(link){link.addEventListener('click',function(){document.querySelector('.nav')?.classList.remove('open');document.querySelector('.menu')?.setAttribute('aria-expanded','false')})});</script></body></html>`;
|
|
33771
35475
|
const bytes = Buffer.byteLength(html, "utf8");
|
|
33772
35476
|
return {
|
|
33773
35477
|
html,
|
|
33774
35478
|
filename: `${input.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "personal-authority"}.html`,
|
|
33775
35479
|
bytes,
|
|
33776
|
-
sha256:
|
|
35480
|
+
sha256: createHash18("sha256").update(html).digest("hex"),
|
|
33777
35481
|
generatedAt: generatedAtIso
|
|
33778
35482
|
};
|
|
33779
35483
|
}
|
|
33780
35484
|
|
|
35485
|
+
// src/newsroom-publisher/render.ts
|
|
35486
|
+
import { createHash as createHash19 } from "crypto";
|
|
35487
|
+
function escapeHtml8(value) {
|
|
35488
|
+
return value.replace(/[&<>"']/g, (character) => ({
|
|
35489
|
+
"&": "&",
|
|
35490
|
+
"<": "<",
|
|
35491
|
+
">": ">",
|
|
35492
|
+
'"': """,
|
|
35493
|
+
"'": "'"
|
|
35494
|
+
})[character] ?? character);
|
|
35495
|
+
}
|
|
35496
|
+
function safeHref2(value) {
|
|
35497
|
+
const trimmed = value.trim();
|
|
35498
|
+
return /^(?:https?:|mailto:|tel:|#|\/)/i.test(trimmed) ? escapeHtml8(trimmed) : "#";
|
|
35499
|
+
}
|
|
35500
|
+
function formatDate(value) {
|
|
35501
|
+
const date = new Date(value);
|
|
35502
|
+
return Number.isNaN(date.getTime()) ? escapeHtml8(value) : new Intl.DateTimeFormat("en", { month: "short", day: "numeric", year: "numeric" }).format(date);
|
|
35503
|
+
}
|
|
35504
|
+
function initials2(value) {
|
|
35505
|
+
return value.split(/\s+/).filter(Boolean).slice(0, 2).map((part) => part[0]).join("").toUpperCase();
|
|
35506
|
+
}
|
|
35507
|
+
function storyMedia(story, className = "") {
|
|
35508
|
+
if (story.imageUrl) {
|
|
35509
|
+
return `<div class="story-media ${className}"><img src="${safeHref2(story.imageUrl)}" alt="${escapeHtml8(story.imageAlt || story.title)}" loading="lazy"></div>`;
|
|
35510
|
+
}
|
|
35511
|
+
const tone = [...story.title].reduce((total, character) => total + character.charCodeAt(0), 0) % 5;
|
|
35512
|
+
return `<div class="story-media story-media--placeholder story-media--tone-${tone} ${className}" role="img" aria-label="Editorial image placeholder for ${escapeHtml8(story.title)}"><i></i><b></b><span>${escapeHtml8(story.category)}</span></div>`;
|
|
35513
|
+
}
|
|
35514
|
+
function byline(story, showBylines) {
|
|
35515
|
+
const author = story.author.href ? `<a href="${safeHref2(story.author.href)}">${escapeHtml8(story.author.name)}</a>` : escapeHtml8(story.author.name);
|
|
35516
|
+
return `<div class="byline">${showBylines ? `<span>By ${author}</span>` : ""}<time datetime="${escapeHtml8(story.publishedAt)}">${formatDate(story.publishedAt)}</time>${story.sponsored ? "<b>Sponsored</b>" : ""}</div>`;
|
|
35517
|
+
}
|
|
35518
|
+
function storyCard(story, showBylines, variant = "") {
|
|
35519
|
+
return `<article class="story-card ${variant}">
|
|
35520
|
+
<a class="story-card__media" href="${safeHref2(story.href)}">${storyMedia(story)}</a>
|
|
35521
|
+
<div class="story-card__body"><a class="kicker" href="${safeHref2(story.href)}">${escapeHtml8(story.category)}</a><h3><a href="${safeHref2(story.href)}">${escapeHtml8(story.title)}</a></h3>${story.dek ? `<p>${escapeHtml8(story.dek)}</p>` : ""}${byline(story, showBylines)}</div>
|
|
35522
|
+
</article>`;
|
|
35523
|
+
}
|
|
35524
|
+
function renderNewsroomPublisherV1(input, config, generatedAt) {
|
|
35525
|
+
const flags = config.featureFlags;
|
|
35526
|
+
const generatedAtIso = generatedAt.toISOString();
|
|
35527
|
+
const brand = config.brandName || input.publication.name;
|
|
35528
|
+
const [lead, ...supporting] = input.featured;
|
|
35529
|
+
const primaryPages = [
|
|
35530
|
+
{ label: "Home", href: input.publication.url },
|
|
35531
|
+
input.primaryPages.about,
|
|
35532
|
+
input.primaryPages.editor,
|
|
35533
|
+
input.primaryPages.press,
|
|
35534
|
+
input.primaryPages.news,
|
|
35535
|
+
input.primaryPages.contact
|
|
35536
|
+
];
|
|
35537
|
+
const trustPages = Object.values(input.trustPages);
|
|
35538
|
+
const ticker = flags.breakingTicker && input.ticker ? `<div class="ticker"><div><b>${escapeHtml8(input.ticker.label)}</b><a href="${safeHref2(input.ticker.href || "#latest")}">${escapeHtml8(input.ticker.headline)}</a><span>${formatDate(generatedAtIso)}</span></div></div>` : "";
|
|
35539
|
+
const masthead = `<nav class="utility-nav" aria-label="Publication pages"><div><span>${escapeHtml8(input.publication.location || "Independent newsroom")}</span>${primaryPages.slice(1).map((link) => `<a href="${safeHref2(link.href)}">${escapeHtml8(link.label)}</a>`).join("")}</div></nav><header class="masthead">
|
|
35540
|
+
<a class="wordmark" href="${safeHref2(input.publication.url)}"><span>${escapeHtml8(brand)}</span><small>${escapeHtml8(input.publication.tagline)}</small></a>
|
|
35541
|
+
<div class="masthead__date"><span>Independent journalism</span><time datetime="${generatedAtIso}">${formatDate(generatedAtIso)}</time></div>
|
|
35542
|
+
<div class="masthead__tools"><button type="button" aria-label="Search">Search</button><button type="button" aria-label="Open menu">Menu</button></div>
|
|
35543
|
+
</header>`;
|
|
35544
|
+
const navigation = flags.navigation ? `<nav class="desk-nav" aria-label="Publication categories"><div><a class="desk-nav__home" href="#top">Home</a>${input.categories.map((category) => `<a href="${safeHref2(category.href)}">${escapeHtml8(category.label)}</a>`).join("")}<a class="desk-nav__latest" href="#latest">Latest</a></div></nav>` : "";
|
|
35545
|
+
const leadGrid = flags.leadGrid ? `<section class="lead-grid" aria-label="Top stories">
|
|
35546
|
+
<div class="lead-grid__label"><span>Today\u2019s edition</span><b>Top stories</b></div>
|
|
35547
|
+
<article class="lead-story"><a class="lead-story__media" href="${safeHref2(lead.href)}">${storyMedia(lead, "story-media--lead")}</a><div class="lead-story__body"><a class="kicker" href="${safeHref2(lead.href)}">${escapeHtml8(lead.category)}</a><h1><a href="${safeHref2(lead.href)}">${escapeHtml8(lead.title)}</a></h1>${lead.dek ? `<p>${escapeHtml8(lead.dek)}</p>` : ""}${byline(lead, config.showBylines)}<a class="read-story" href="${safeHref2(lead.href)}">Read the full report <span>\u2197</span></a></div></article>
|
|
35548
|
+
<div class="lead-support">${supporting.slice(0, 4).map((story, index) => `<div class="lead-support__item"><span>${String(index + 1).padStart(2, "0")}</span>${storyCard(story, config.showBylines, "story-card--support")}</div>`).join("")}</div>
|
|
35549
|
+
</section>` : "";
|
|
35550
|
+
const latest = flags.latestNews && input.latest.length > 0 ? `<section class="latest" id="latest">
|
|
35551
|
+
<div class="section-title"><div><span>Live desk</span><h2>Latest news</h2></div><a href="#top">Back to top \u2191</a></div>
|
|
35552
|
+
<div class="latest__layout"><div class="latest__feed">${input.latest.map((story, index) => `<article class="feed-story"><span>${String(index + 1).padStart(2, "0")}</span><div><a class="kicker" href="${safeHref2(story.href)}">${escapeHtml8(story.category)}</a><h3><a href="${safeHref2(story.href)}">${escapeHtml8(story.title)}</a></h3>${story.dek ? `<p>${escapeHtml8(story.dek)}</p>` : ""}${byline(story, config.showBylines)}</div>${storyMedia(story, "story-media--feed")}</article>`).join("")}</div>
|
|
35553
|
+
<aside class="editor-note"><span>From the newsroom</span><h3>Reporting with context, sources, and accountable bylines.</h3><p>${escapeHtml8(input.editorial.disclosure)}</p>${trustPages.slice(0, 4).map((link) => `<a href="${safeHref2(link.href)}">${escapeHtml8(link.label)} \u2197</a>`).join("")}</aside></div>
|
|
35554
|
+
</section>` : "";
|
|
35555
|
+
const sectionLayouts = ["panorama", "mosaic", "tiles"];
|
|
35556
|
+
const sections = flags.categorySections ? input.sections.map((section2, sectionIndex) => {
|
|
35557
|
+
const layout = sectionLayouts[sectionIndex % sectionLayouts.length];
|
|
35558
|
+
const cards = section2.stories.map((story, index) => storyCard(
|
|
35559
|
+
story,
|
|
35560
|
+
config.showBylines,
|
|
35561
|
+
layout === "panorama" && index === 0 ? "story-card--panorama" : layout === "mosaic" && index === 0 ? "story-card--mosaic-lead" : ""
|
|
35562
|
+
)).join("");
|
|
35563
|
+
return `<section class="desk-section desk-section--${sectionIndex % 2 ? "tint" : "plain"} desk-section--layout-${layout}">
|
|
35564
|
+
<div class="section-title"><div><span>Coverage desk \xB7 ${String(sectionIndex + 1).padStart(2, "0")}</span><h2>${escapeHtml8(section2.name)}</h2></div>${section2.href ? `<a href="${safeHref2(section2.href)}">View all ${escapeHtml8(section2.name)} \u2192</a>` : ""}</div>
|
|
35565
|
+
<div class="desk-section__grid desk-section__grid--${layout}">${cards}</div>
|
|
35566
|
+
</section>`;
|
|
35567
|
+
}).join("") : "";
|
|
35568
|
+
const newsletter = flags.newsletter && input.newsletter ? `<section class="newsletter"><div><span>${escapeHtml8(input.newsletter.eyebrow || "Reader briefing")}</span><h2>${escapeHtml8(input.newsletter.heading)}</h2><p>${escapeHtml8(input.newsletter.summary)}</p></div><a href="${safeHref2(input.newsletter.action.href)}">${escapeHtml8(input.newsletter.action.label)} \u2192</a></section>` : "";
|
|
35569
|
+
const pressRoom = flags.pressRoom && input.pressRoom ? `<section class="press-room" id="press-room"><div class="press-room__intro"><span>Separate from editorial coverage</span><h2>${escapeHtml8(input.pressRoom.heading)}</h2><p>${escapeHtml8(input.pressRoom.summary)}</p><a href="${safeHref2(input.pressRoom.action.href)}">${escapeHtml8(input.pressRoom.action.label)} \u2192</a></div><div class="release-list">${input.pressRoom.releases.map((release) => `<article><time datetime="${escapeHtml8(release.publishedAt)}">${formatDate(release.publishedAt)}</time><span>${escapeHtml8(release.originatingCity)}</span><h3><a href="${safeHref2(release.href)}">${escapeHtml8(release.headline)}</a></h3></article>`).join("") || "<p>No releases are currently published.</p>"}</div></section>` : "";
|
|
35570
|
+
const trustCenter = `<section class="trust-center" id="trust"><div class="trust-center__intro"><span>Public record</span><h2>How this newsroom works.</h2><p>Ownership, editorial practice, technology use, commercial relationships, and reader rights should be easy to find\u2014and written to be understood.</p></div><div class="trust-center__grid">${trustPages.map((link, index) => `<a href="${safeHref2(link.href)}"><span>${String(index + 1).padStart(2, "0")}</span><strong>${escapeHtml8(link.label)}</strong><small>Read \u2192</small></a>`).join("")}</div></section>`;
|
|
35571
|
+
const trustFooter = flags.trustFooter ? `<footer class="site-footer"><div class="editor"><span class="editor__avatar">${escapeHtml8(initials2(input.editorial.managingEditor.name))}</span><div><small>${escapeHtml8(input.editorial.managingEditor.role)}</small><h2>${input.editorial.managingEditor.href ? `<a href="${safeHref2(input.editorial.managingEditor.href)}">${escapeHtml8(input.editorial.managingEditor.name)}</a>` : escapeHtml8(input.editorial.managingEditor.name)}</h2><p>${escapeHtml8(input.editorial.managingEditor.bio)}</p></div></div><div class="footer-links"><h3>${escapeHtml8(brand)}</h3><p>${escapeHtml8(input.publication.tagline)}</p><nav>${primaryPages.map((link) => `<a href="${safeHref2(link.href)}">${escapeHtml8(link.label)}</a>`).join("")}</nav>${config.showGeneratedAt ? `<time datetime="${generatedAtIso}">Updated ${formatDate(generatedAtIso)}</time>` : ""}</div></footer>` : "";
|
|
35572
|
+
const html = `<!doctype html>
|
|
35573
|
+
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="robots" content="noindex,nofollow"><title>${escapeHtml8(brand)} \xB7 News Publisher</title><link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link href="https://fonts.googleapis.com/css2?family=Archivo:wdth,wght@75..125,300..900&family=Instrument+Serif:ital@0;1&family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">
|
|
35574
|
+
<style>
|
|
35575
|
+
:root{--ink:#0b0b0c;--soft-ink:#222326;--muted:#696b70;--paper:#fff;--cream:#f4f0e8;--wash:#f5f5f3;--line:#d8d8d4;--signal:#275cf3;--acid:#d9ff43;--dark:#0b0b0c;--serif:"Instrument Serif",Georgia,serif;--sans:"Archivo",Arial,sans-serif;--mono:"JetBrains Mono",monospace;font-family:var(--sans);color:var(--ink);background:var(--paper)}*{box-sizing:border-box}html{scroll-behavior:smooth}body{margin:0;background:var(--paper);line-height:1.45}body[data-theme=journal]{--cream:#eee7da;--signal:#9d2d25;--acid:#f0c84f}body[data-theme=midnight]{--paper:#e8e9ec;--cream:#dfe1e6;--wash:#d8dae0;--line:#b9bdc6;--signal:#224bcc}a{color:inherit;text-decoration:none}a,button{transition:color .18s ease,background .18s ease,opacity .18s ease,transform .18s ease}button{font:inherit}.utility-nav{color:#d2d2d2;background:var(--dark);border-bottom:1px solid #292929}.utility-nav>div{max-width:1180px;height:35px;margin:auto;display:flex;align-items:center;justify-content:flex-end;gap:20px;padding:0 18px;overflow:auto}.utility-nav span{margin-right:auto;color:#8d8d8d}.utility-nav a,.utility-nav span{font:600 8px/1 var(--mono);letter-spacing:.08em;text-transform:uppercase;white-space:nowrap}.utility-nav a:hover{color:#fff}.ticker{color:var(--ink);background:var(--acid);padding:0 18px}.ticker>div{max-width:1180px;height:36px;margin:auto;display:flex;align-items:center;gap:16px;font:600 10px/1 var(--mono)}.ticker b{padding:5px 7px;color:#fff;background:var(--ink);font-size:8px;letter-spacing:.1em;text-transform:uppercase}.ticker a{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.ticker a:hover{text-decoration:underline}.ticker span{margin-left:auto;opacity:.58;font-size:8px;text-transform:uppercase}.masthead{max-width:1180px;height:126px;margin:auto;display:grid;grid-template-columns:minmax(0,1fr) auto auto;align-items:center;gap:42px;padding:0 18px}.wordmark{display:grid;justify-self:start}.wordmark span{font:400 clamp(40px,5vw,68px)/.82 var(--serif);letter-spacing:-.055em}.wordmark small{max-width:430px;margin-top:11px;color:var(--muted);font:600 8px/1 var(--mono);letter-spacing:.1em;text-transform:uppercase}.masthead__date{display:grid;gap:3px;text-align:right}.masthead__date span,.masthead__date time{font:600 8px/1.4 var(--mono);letter-spacing:.08em;text-transform:uppercase}.masthead__date span{color:var(--signal)}.masthead__date time{color:var(--muted)}.masthead__tools{display:flex;border:1px solid var(--ink)}.masthead__tools button{height:38px;padding:0 13px;border:0;background:transparent;font:700 9px/1 var(--mono);text-transform:uppercase;cursor:pointer}.masthead__tools button+button{border-left:1px solid var(--ink)}.masthead__tools button:hover{color:#fff;background:var(--ink)}.desk-nav{position:sticky;top:0;z-index:20;color:var(--ink);background:rgba(255,255,255,.96);border-top:1px solid var(--ink);border-bottom:1px solid var(--ink);backdrop-filter:blur(10px)}.desk-nav>div{max-width:1180px;height:43px;margin:auto;display:flex;align-items:center;overflow-x:auto;padding:0 18px}.desk-nav a{height:42px;display:grid;place-items:center;padding:0 17px;border-right:1px solid var(--line);font:650 9px/1 var(--mono);letter-spacing:.04em;text-transform:uppercase;white-space:nowrap}.desk-nav a:first-child{border-left:1px solid var(--line)}.desk-nav a:hover,.desk-nav__home{color:#fff!important;background:var(--ink)}.desk-nav__latest{margin-left:auto;color:var(--signal);border-left:1px solid var(--line)}main{max-width:1180px;margin:auto;padding:30px 18px 90px}.lead-grid{margin:0 -18px;padding:0 18px 42px;color:#fff;background:var(--dark);border-bottom:1px solid var(--ink)}.lead-grid__label{height:50px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid #303030}.lead-grid__label span,.lead-grid__label b{font:600 8px/1 var(--mono);letter-spacing:.12em;text-transform:uppercase}.lead-grid__label span{color:var(--acid)}.lead-grid__label b{color:#8f8f8f}.lead-story{display:grid;grid-template-columns:minmax(0,1.28fr) minmax(340px,.72fr);min-height:500px;border-bottom:1px solid #303030}.lead-story__media{display:block;margin-left:calc((1180px - 100vw)/2);min-width:0}.story-media{position:relative;aspect-ratio:16/10;overflow:hidden;background:#e2e2df}.story-media img{width:100%;height:100%;display:block;object-fit:cover;filter:saturate(.88) contrast(1.03);transition:transform .55s cubic-bezier(.2,.7,.2,1)}a:hover .story-media img{transform:scale(1.025)}.story-media--lead{height:100%;min-height:500px;aspect-ratio:auto}.story-media--placeholder{isolation:isolate;display:block;background:#d9d7d1}.story-media--placeholder:after{position:absolute;z-index:1;inset:0;background:linear-gradient(145deg,transparent 35%,rgba(0,0,0,.14));content:""}.story-media--placeholder i,.story-media--placeholder b{position:absolute;display:block}.story-media--placeholder i{width:72%;aspect-ratio:1;right:-20%;top:-32%;border:clamp(22px,5vw,72px) solid currentColor;border-radius:50%;opacity:.78}.story-media--placeholder b{width:42%;height:115%;left:15%;bottom:-45%;background:currentColor;transform:rotate(32deg);opacity:.88}.story-media--placeholder span{position:absolute;z-index:2;left:18px;bottom:15px;padding:6px 8px;color:#fff;background:var(--ink);font:600 8px/1 var(--mono);letter-spacing:.12em;text-transform:uppercase}.story-media--tone-0{color:#123fce;background:#a5c8ff}.story-media--tone-1{color:#ef4d25;background:#ffd9bd}.story-media--tone-2{color:#907000;background:#f3df36}.story-media--tone-3{color:#1a7857;background:#b8e8cc}.story-media--tone-4{color:#7d3de0;background:#dfc5ff}.lead-story__body{display:flex;flex-direction:column;justify-content:center;padding:45px 0 45px 40px}.kicker{display:inline-flex;width:max-content;color:var(--signal);font:700 8px/1 var(--mono);letter-spacing:.11em;text-transform:uppercase}.lead-grid .kicker{color:var(--acid)}.lead-story h1{max-width:540px;margin:13px 0 18px;font:400 clamp(48px,5vw,76px)/.92 var(--serif);letter-spacing:-.052em}.lead-story p{max-width:490px;margin:0;color:#bdbdbd;font-size:15px;line-height:1.55}.byline{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-top:16px;color:var(--muted);font:600 8px/1.4 var(--mono);letter-spacing:.05em;text-transform:uppercase}.byline span{color:var(--ink);font-weight:700}.lead-grid .byline{color:#848484}.lead-grid .byline span{color:#e7e7e7}.byline b{padding:4px 6px;color:var(--ink);background:var(--acid)}.read-story{width:max-content;margin-top:34px;padding-bottom:6px;border-bottom:1px solid #777;font:650 10px/1 var(--mono);text-transform:uppercase}.read-story span{color:var(--acid)}.read-story:hover{border-color:#fff}.lead-support{display:grid;grid-template-columns:repeat(4,1fr);gap:0}.lead-support__item{display:grid;grid-template-columns:25px minmax(0,1fr);gap:12px;padding:22px 18px 0 0;border-right:1px solid #303030}.lead-support__item+.lead-support__item{padding-left:18px}.lead-support__item:last-child{border-right:0}.lead-support__item>span{color:#666;font:600 9px/1 var(--mono)}.story-card{min-width:0}.story-card h3{margin:7px 0;font:400 26px/1.02 var(--serif);letter-spacing:-.025em}.story-card p{margin:9px 0;color:var(--muted);font-size:12px}.story-card__body{padding-top:11px}.story-card--support .story-card__media{display:none}.story-card--support .story-card__body{padding:0}.story-card--support h3{margin-top:8px;color:#fff;font-size:21px}.story-card--support p,.story-card--support .byline span{display:none}.story-card--support .byline{color:#777}.section-title{display:flex;align-items:end;justify-content:space-between;gap:30px;margin-bottom:24px;padding-bottom:11px;border-bottom:2px solid var(--ink)}.section-title span,.newsletter span,.press-room__intro>span,.trust-center__intro>span{display:block;color:var(--signal);font:700 8px/1 var(--mono);letter-spacing:.12em;text-transform:uppercase}.section-title h2,.newsletter h2,.press-room h2,.trust-center h2,.site-footer h2{margin:7px 0 0;font:400 clamp(38px,5vw,58px)/.95 var(--serif);letter-spacing:-.04em}.section-title>a{padding-bottom:4px;border-bottom:1px solid var(--ink);font:650 9px/1 var(--mono);text-transform:uppercase}.latest{padding:64px 0}.latest__layout{display:grid;grid-template-columns:minmax(0,1fr) 300px;gap:55px}.latest__feed{border-top:1px solid var(--ink)}.feed-story{display:grid;grid-template-columns:34px minmax(0,1fr) 180px;gap:18px;align-items:start;padding:19px 0;border-bottom:1px solid var(--line)}.feed-story>span{color:#a0a0a0;font:600 9px/1 var(--mono)}.feed-story h3{margin:7px 0;font:400 29px/1.02 var(--serif);letter-spacing:-.025em}.feed-story p{margin:8px 0;color:var(--muted);font-size:12px}.story-media--feed{aspect-ratio:4/3}.editor-note{position:sticky;top:68px;align-self:start;padding:25px;color:#fff;background:var(--dark);border-top:5px solid var(--acid)}.editor-note>span{color:var(--acid);font:600 8px/1 var(--mono);letter-spacing:.12em;text-transform:uppercase}.editor-note h3{margin:13px 0;font:400 31px/.98 var(--serif)}.editor-note p{color:#aaa;font-size:12px}.editor-note a{display:block;padding:11px 0;border-top:1px solid #333;font:600 9px/1 var(--mono);text-transform:uppercase}.editor-note a:hover{color:var(--acid)}.desk-section{margin:0 -18px;padding:58px 18px;border-top:1px solid var(--ink)}.desk-section--tint{background:var(--cream)}.desk-section__grid{display:grid;grid-template-columns:repeat(4,1fr);gap:26px 14px}.story-card--wide{grid-column:span 2;grid-row:span 2}.story-card--wide h3{max-width:530px;font-size:40px}.story-card--wide p{max-width:540px;font-size:13px}.desk-section__grid>.story-card:not(.story-card--wide) .story-media{aspect-ratio:1}.newsletter{position:relative;margin:64px 0;display:grid;grid-template-columns:1fr auto;align-items:end;gap:50px;overflow:hidden;padding:clamp(38px,6vw,72px);color:var(--ink);background:var(--acid)}.newsletter:after{position:absolute;width:260px;height:260px;right:18%;top:-160px;border:55px solid var(--ink);border-radius:50%;content:"";opacity:.08}.newsletter span{color:var(--ink)}.newsletter h2{max-width:760px;font-size:clamp(44px,6vw,72px)}.newsletter p{max-width:680px;margin-bottom:0;color:#303030}.newsletter>a,.press-room__intro>a{position:relative;z-index:1;display:inline-flex;padding:14px 18px;color:#fff;background:var(--ink);border:1px solid var(--ink);font:700 9px/1 var(--mono);text-transform:uppercase}.newsletter>a:hover,.press-room__intro>a:hover{color:var(--ink);background:transparent}.press-room{display:grid;grid-template-columns:.7fr 1.3fr;gap:56px;padding:62px 0;border-top:6px solid var(--ink)}.press-room__intro p{max-width:390px;color:var(--muted)}.release-list{border-top:1px solid var(--ink)}.release-list article{display:grid;grid-template-columns:105px 105px 1fr;gap:15px;align-items:start;padding:20px 0;border-bottom:1px solid var(--line)}.release-list time,.release-list span{color:var(--muted);font:500 8px/1.4 var(--mono);text-transform:uppercase}.release-list h3{margin:0;font:400 22px/1.04 var(--serif)}.trust-center{margin:20px -18px -90px;padding:70px max(18px,calc((100vw - 1144px)/2)) 82px;color:#fff;background:var(--dark)}.trust-center__intro{max-width:810px;margin-bottom:38px}.trust-center__intro>span{color:var(--acid)}.trust-center__intro p{max-width:670px;color:#aaa}.trust-center__grid{display:grid;grid-template-columns:repeat(2,1fr);border-top:1px solid #444}.trust-center__grid a{display:grid;grid-template-columns:32px minmax(0,1fr) auto;align-items:center;gap:15px;min-height:70px;padding:0 18px 0 0;border-bottom:1px solid #333}.trust-center__grid a:nth-child(odd){margin-right:28px}.trust-center__grid a:hover{color:var(--acid)}.trust-center__grid span,.trust-center__grid small{color:#696969;font:600 8px/1 var(--mono)}.trust-center__grid strong{font:400 20px/1 var(--serif)}.site-footer{display:grid;grid-template-columns:1.15fr .85fr;gap:80px;padding:70px max(24px,calc((100vw - 1144px)/2));color:var(--ink);background:var(--cream);border-top:1px solid var(--ink)}.editor{display:grid;grid-template-columns:78px 1fr;gap:22px}.editor__avatar{width:78px;height:78px;display:grid;place-items:center;color:#fff;background:var(--signal);font:400 27px/1 var(--serif)}.editor small{color:var(--signal);font:700 8px/1 var(--mono);letter-spacing:.08em;text-transform:uppercase}.editor h2{font-size:38px}.editor p,.footer-links p{max-width:620px;color:var(--muted)}.footer-links h3{margin-top:0;font:400 29px/1 var(--serif)}.footer-links nav{display:grid;grid-template-columns:1fr 1fr;border-top:1px solid var(--ink)}.footer-links nav a{padding:9px 0;border-bottom:1px solid var(--line);font:600 8px/1 var(--mono);text-transform:uppercase}.footer-links time{display:block;margin-top:24px;color:var(--muted);font:500 8px/1 var(--mono)}@media(max-width:900px){.masthead{grid-template-columns:1fr auto;height:110px}.masthead__date{display:none}.lead-story{grid-template-columns:1fr}.lead-story__media{margin:0}.lead-story__body{padding:35px 0}.lead-support{grid-template-columns:1fr 1fr}.lead-support__item:nth-child(2){border-right:0}.lead-support__item:nth-child(n+3){border-top:1px solid #303030}.latest__layout,.press-room,.site-footer{grid-template-columns:1fr}.editor-note{position:static}.desk-section__grid{grid-template-columns:1fr 1fr}.story-card--wide{grid-column:1/-1}.site-footer{gap:40px}.feed-story{grid-template-columns:30px 1fr 150px}}@media(max-width:620px){.utility-nav span,.utility-nav a:nth-of-type(-n+2),.ticker span{display:none}.ticker>div{height:34px}.masthead{height:92px;gap:14px}.wordmark span{font-size:38px}.wordmark small{max-width:220px;font-size:6px}.masthead__tools button:first-child{display:none}.desk-nav>div{padding:0}.desk-nav a{padding:0 13px}.lead-grid{padding-bottom:20px}.lead-grid__label{height:40px}.story-media--lead{min-height:320px}.lead-story{min-height:0}.lead-story h1{font-size:48px}.lead-story__body{padding:27px 0 34px}.lead-support{grid-template-columns:1fr}.lead-support__item,.lead-support__item+.lead-support__item{padding:17px 0;border-right:0;border-top:1px solid #303030}.story-card--support h3{font-size:24px}.section-title{align-items:start}.section-title>a{display:none}.latest{padding:48px 0}.feed-story{grid-template-columns:25px 1fr}.story-media--feed{grid-column:2}.feed-story h3{font-size:26px}.desk-section__grid{grid-template-columns:1fr}.story-card--wide{grid-column:auto;grid-row:auto}.story-card--wide h3{font-size:34px}.newsletter{grid-template-columns:1fr}.newsletter h2{font-size:48px}.release-list article{grid-template-columns:1fr}.trust-center__grid{grid-template-columns:1fr}.trust-center__grid a:nth-child(odd){margin-right:0}.site-footer{padding:50px 20px}.editor{grid-template-columns:1fr}.footer-links nav{grid-template-columns:1fr}}
|
|
35576
|
+
/* Tampa-inspired publication palette */
|
|
35577
|
+
:root{--ink:#12312f;--soft-ink:#294744;--muted:#697b77;--paper:#fffdf8;--cream:#f8f2e8;--wash:#f3eee5;--line:#d8d3c8;--signal:#ee7057;--acid:#f0b45d;--dark:#082f2c}
|
|
35578
|
+
body[data-theme=journal]{--dark:#102d2a;--ink:#172f2d;--cream:#eee7da;--signal:#bd5b47;--acid:#e4aa52}
|
|
35579
|
+
body[data-theme=midnight]{--paper:#e8e9e4;--cream:#dfe3dc;--wash:#d8ddd6;--line:#b9c2bb;--dark:#072a28;--signal:#df6c56}
|
|
35580
|
+
.utility-nav{color:#d7e0dc;border-bottom-color:rgba(255,255,255,.14)}
|
|
35581
|
+
.lead-grid__label,.lead-story{border-bottom-color:rgba(255,255,255,.17)}
|
|
35582
|
+
.lead-grid__label b{color:#90aaa4}
|
|
35583
|
+
.lead-story__media .story-media:not(.story-media--placeholder):after{position:absolute;inset:0;background:linear-gradient(90deg,rgba(8,47,44,.38),transparent 64%),linear-gradient(0deg,rgba(8,47,44,.26),transparent 58%);content:""}
|
|
35584
|
+
.story-media--tone-0{color:#0b514b;background:#8fb8ac}.story-media--tone-1{color:#d65f49;background:#f5c2a9}.story-media--tone-2{color:#a97022;background:#f1d59b}.story-media--tone-3{color:#255f52;background:#b8d9c9}.story-media--tone-4{color:#9a5f48;background:#dfc2ad}
|
|
35585
|
+
body{overflow-x:hidden}.lead-story__body,.lead-support__item,.story-card__body{min-width:0}.lead-story h1,.story-card h3,.feed-story h3{overflow-wrap:anywhere}
|
|
35586
|
+
.desk-section__grid--panorama{grid-template-columns:repeat(4,1fr)}.story-card--panorama{grid-column:1/-1;display:grid;grid-template-columns:minmax(0,1.65fr) minmax(250px,.65fr);gap:28px;margin-bottom:12px;padding-bottom:28px;border-bottom:1px solid var(--line)}.story-card--panorama .story-media{aspect-ratio:2.2/1}.story-card--panorama .story-card__body{display:flex;flex-direction:column;justify-content:center;padding:0}.story-card--panorama h3{font-size:48px}.story-card--panorama p{font-size:14px}.desk-section__grid--mosaic{grid-template-columns:repeat(4,1fr);grid-auto-flow:dense}.story-card--mosaic-lead{grid-column:span 2;grid-row:span 2}.story-card--mosaic-lead .story-media{aspect-ratio:1}.story-card--mosaic-lead h3{font-size:42px}.desk-section__grid--mosaic>.story-card:not(.story-card--mosaic-lead) .story-media{aspect-ratio:1.35}.desk-section__grid--tiles{grid-template-columns:repeat(4,1fr)}.desk-section__grid--tiles .story-media{aspect-ratio:1.15}.desk-section__grid--tiles>.story-card:nth-child(3n+2){margin-top:38px}
|
|
35587
|
+
@media(max-width:620px){main{width:100%}.story-media--lead{height:360px;min-height:0}.lead-story__body,.lead-support,.latest,.feed-story{width:100%;max-width:calc(100vw - 36px)}.feed-story>div{min-width:0}.lead-story h1,.story-card h3,.feed-story h3,.story-card h3 a,.feed-story h3 a{white-space:normal;word-break:break-word;overflow-wrap:anywhere}.lead-story h1{max-width:100%;font-size:40px;line-height:.94}.lead-support__item{width:100%;max-width:100%;grid-template-columns:24px minmax(0,1fr);overflow:hidden}.story-card--support,.story-card--support .story-card__body,.story-card--support h3{width:100%;max-width:calc(100vw - 92px)}.feed-story>div,.feed-story h3,.feed-story p{max-width:calc(100vw - 100px)}}
|
|
35588
|
+
@media(max-width:900px){.story-card--panorama{grid-template-columns:1fr}.desk-section__grid--mosaic{grid-template-columns:repeat(2,1fr)}.desk-section__grid--tiles{grid-template-columns:repeat(2,1fr)}}
|
|
35589
|
+
@media(max-width:620px){.desk-section__grid--panorama,.desk-section__grid--mosaic,.desk-section__grid--tiles{grid-template-columns:1fr}.story-card--panorama{display:block}.story-card--panorama h3,.story-card--mosaic-lead h3{font-size:34px}.story-card--mosaic-lead{grid-column:auto;grid-row:auto}.desk-section__grid--tiles>.story-card:nth-child(3n+2){margin-top:0}}
|
|
35590
|
+
</style></head><body data-theme="${escapeHtml8(config.theme)}" id="top" data-scheduled-renderer="newsroom_publisher_v1">${ticker}${masthead}${navigation}<main>${leadGrid}${latest}${sections}${newsletter}${pressRoom}${trustCenter}</main>${trustFooter}</body></html>`;
|
|
35591
|
+
const bytes = Buffer.byteLength(html);
|
|
35592
|
+
const sha2563 = createHash19("sha256").update(html).digest("hex");
|
|
35593
|
+
const filename = `${brand.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "newsroom"}-news-site.html`;
|
|
35594
|
+
return { html, filename, bytes, sha256: sha2563, generatedAt: generatedAtIso };
|
|
35595
|
+
}
|
|
35596
|
+
|
|
33781
35597
|
// src/scheduled-artifacts/scheduled-artifact-renderer-registry.ts
|
|
33782
35598
|
var editorialReadingRoomV1 = {
|
|
33783
35599
|
rendererKey: "editorial_reading_room_v1",
|
|
@@ -33815,24 +35631,47 @@ var personalAuthorityV1 = {
|
|
|
33815
35631
|
};
|
|
33816
35632
|
}
|
|
33817
35633
|
};
|
|
35634
|
+
var newsroomPublisherV1 = {
|
|
35635
|
+
rendererKey: "newsroom_publisher_v1",
|
|
35636
|
+
rendererVersion: 1,
|
|
35637
|
+
async render(rawInput, rawConfig, generatedAt) {
|
|
35638
|
+
const input = NewsroomPublisherV1InputSchema.parse(rawInput);
|
|
35639
|
+
const config = NewsroomPublisherTemplateConfigSchema.parse(rawConfig);
|
|
35640
|
+
const rendered = renderNewsroomPublisherV1(input, config, generatedAt);
|
|
35641
|
+
if (rendered.bytes > 2e6) {
|
|
35642
|
+
throw new Error("Scheduled artifact HTML must be 2,000,000 bytes or fewer.");
|
|
35643
|
+
}
|
|
35644
|
+
return {
|
|
35645
|
+
...rendered,
|
|
35646
|
+
title: `${input.publication.name} News Publisher`
|
|
35647
|
+
};
|
|
35648
|
+
}
|
|
35649
|
+
};
|
|
33818
35650
|
var scheduledArtifactRendererRegistry = Object.freeze({
|
|
33819
35651
|
"editorial_reading_room_v1:1": editorialReadingRoomV1,
|
|
33820
|
-
"personal_authority_v1:1": personalAuthorityV1
|
|
35652
|
+
"personal_authority_v1:1": personalAuthorityV1,
|
|
35653
|
+
"newsroom_publisher_v1:1": newsroomPublisherV1
|
|
33821
35654
|
});
|
|
33822
35655
|
function getScheduledArtifactRenderer(rendererKey, rendererVersion) {
|
|
33823
35656
|
return scheduledArtifactRendererRegistry[`${rendererKey}:${rendererVersion}`] ?? null;
|
|
33824
35657
|
}
|
|
33825
35658
|
var scheduledArtifactTemplatePresets = [
|
|
33826
35659
|
EDITORIAL_READING_ROOM_TEMPLATE_PRESET,
|
|
33827
|
-
PERSONAL_AUTHORITY_TEMPLATE_PRESET
|
|
35660
|
+
PERSONAL_AUTHORITY_TEMPLATE_PRESET,
|
|
35661
|
+
NEWSROOM_PUBLISHER_TEMPLATE_PRESET
|
|
33828
35662
|
];
|
|
33829
35663
|
|
|
33830
35664
|
// src/api/scheduled-result-routes.ts
|
|
33831
|
-
var TemplateParamsSchema =
|
|
33832
|
-
var RunParamsSchema =
|
|
33833
|
-
var ViewLinkParamsSchema =
|
|
33834
|
-
runId:
|
|
33835
|
-
shareId:
|
|
35665
|
+
var TemplateParamsSchema = z35.object({ templateId: z35.string().uuid() }).strict();
|
|
35666
|
+
var RunParamsSchema = z35.object({ runId: z35.string().min(1).max(200) }).strict();
|
|
35667
|
+
var ViewLinkParamsSchema = z35.object({
|
|
35668
|
+
runId: z35.string().min(1).max(200),
|
|
35669
|
+
shareId: z35.string().uuid()
|
|
35670
|
+
}).strict();
|
|
35671
|
+
var ArtifactTemplateListQuerySchema = z35.object({
|
|
35672
|
+
status: z35.enum(["active", "archived", "all"]).default("active"),
|
|
35673
|
+
query: z35.string().trim().min(1).max(120).optional(),
|
|
35674
|
+
presetKey: ArtifactTemplatePresetKeySchema.optional()
|
|
33836
35675
|
}).strict();
|
|
33837
35676
|
function apiError(error, errorCode = "invalid_request") {
|
|
33838
35677
|
return { ok: false, error, errorCode };
|
|
@@ -33861,25 +35700,36 @@ function safeMemoryFailure(result) {
|
|
|
33861
35700
|
errorCode in messages ? errorCode : "internal_error"
|
|
33862
35701
|
);
|
|
33863
35702
|
}
|
|
33864
|
-
var scheduledResultApp = new
|
|
35703
|
+
var scheduledResultApp = new Hono33();
|
|
33865
35704
|
scheduledResultApp.use("/artifact-templates", createApiKeyAuth());
|
|
33866
35705
|
scheduledResultApp.use("/artifact-templates/*", createApiKeyAuth());
|
|
33867
35706
|
scheduledResultApp.use("/schedule-runs", createApiKeyAuth());
|
|
33868
35707
|
scheduledResultApp.use("/schedule-runs/*", createApiKeyAuth());
|
|
33869
35708
|
scheduledResultApp.get("/artifact-templates", async (c) => {
|
|
33870
|
-
const
|
|
33871
|
-
if (!
|
|
33872
|
-
|
|
33873
|
-
}
|
|
35709
|
+
const parsed = ArtifactTemplateListQuerySchema.safeParse(c.req.query());
|
|
35710
|
+
if (!parsed.success) return c.json(apiError("The template search filters are invalid."), 400);
|
|
35711
|
+
const { status, query: searchText, presetKey } = parsed.data;
|
|
33874
35712
|
const { key, error } = await memoryContext(c);
|
|
33875
35713
|
if (!key) return c.json(apiError(error ?? "Memory is unavailable.", "internal_error"), 503);
|
|
33876
35714
|
const result = await memoryCall("list_artifact_templates", { status }, key);
|
|
33877
35715
|
if (!result.ok) return c.json(safeMemoryFailure(result), memoryStatus(result));
|
|
33878
35716
|
c.header("Cache-Control", "private, no-store");
|
|
33879
|
-
|
|
33880
|
-
|
|
33881
|
-
|
|
33882
|
-
|
|
35717
|
+
const normalizedQuery = searchText?.toLocaleLowerCase();
|
|
35718
|
+
const presets = scheduledArtifactTemplatePresets.filter((preset) => {
|
|
35719
|
+
if (presetKey && preset.presetKey !== presetKey) return false;
|
|
35720
|
+
if (!normalizedQuery) return true;
|
|
35721
|
+
return [preset.name, preset.description, preset.presetKey].some((value) => value.toLocaleLowerCase().includes(normalizedQuery));
|
|
35722
|
+
});
|
|
35723
|
+
const templates = (Array.isArray(result.templates) ? result.templates : []).filter((template) => {
|
|
35724
|
+
if (!template || typeof template !== "object") return false;
|
|
35725
|
+
const record = template;
|
|
35726
|
+
const currentVersion = record.currentVersion && typeof record.currentVersion === "object" ? record.currentVersion : null;
|
|
35727
|
+
const rendererKey = typeof currentVersion?.rendererKey === "string" ? currentVersion.rendererKey : "";
|
|
35728
|
+
if (presetKey && rendererKey !== presetKey) return false;
|
|
35729
|
+
if (!normalizedQuery) return true;
|
|
35730
|
+
return [record.name, record.description, rendererKey, currentVersion?.authoringInstructions].filter((value) => typeof value === "string").some((value) => value.toLocaleLowerCase().includes(normalizedQuery));
|
|
35731
|
+
});
|
|
35732
|
+
return c.json({ ok: true, presets, templates });
|
|
33883
35733
|
});
|
|
33884
35734
|
scheduledResultApp.post("/artifact-templates", async (c) => {
|
|
33885
35735
|
const parsed = CreateArtifactTemplateInputSchema.safeParse(await c.req.json().catch(() => null));
|
|
@@ -33997,10 +35847,10 @@ scheduledResultApp.delete("/schedule-runs/:runId/view-links/:shareId", async (c)
|
|
|
33997
35847
|
});
|
|
33998
35848
|
|
|
33999
35849
|
// src/api/public-scheduled-result-routes.ts
|
|
34000
|
-
import { Hono as
|
|
35850
|
+
import { Hono as Hono34 } from "hono";
|
|
34001
35851
|
|
|
34002
35852
|
// src/scheduled-artifacts/scheduled-run-artifact-store.ts
|
|
34003
|
-
import { createHash as
|
|
35853
|
+
import { createHash as createHash20 } from "crypto";
|
|
34004
35854
|
var SCHEDULED_RUN_ARTIFACT_PREFIX = "scheduled-run-artifacts/";
|
|
34005
35855
|
var SCHEDULED_RUN_ARTIFACT_DOWNLOAD_TTL_MS = 15 * 60 * 1e3;
|
|
34006
35856
|
var SCHEDULED_RUN_ARTIFACT_MAX_BYTES = 2e6;
|
|
@@ -34016,7 +35866,7 @@ function policy3() {
|
|
|
34016
35866
|
};
|
|
34017
35867
|
}
|
|
34018
35868
|
function runStorageSegment(runId) {
|
|
34019
|
-
return /^[a-zA-Z0-9_-]{1,160}$/.test(runId) ? runId : `run-${
|
|
35869
|
+
return /^[a-zA-Z0-9_-]{1,160}$/.test(runId) ? runId : `run-${createHash20("sha256").update(runId).digest("hex").slice(0, 32)}`;
|
|
34020
35870
|
}
|
|
34021
35871
|
async function createScheduledRunArtifact(args) {
|
|
34022
35872
|
if (args.rendered.bytes > SCHEDULED_RUN_ARTIFACT_MAX_BYTES) {
|
|
@@ -34059,7 +35909,7 @@ async function readScheduledRunArtifact(args) {
|
|
|
34059
35909
|
}
|
|
34060
35910
|
|
|
34061
35911
|
// src/api/public-scheduled-result-routes.ts
|
|
34062
|
-
var publicScheduledResultApp = new
|
|
35912
|
+
var publicScheduledResultApp = new Hono34();
|
|
34063
35913
|
function allowedFrameAncestors() {
|
|
34064
35914
|
const configured = (process.env.SCHEDULED_ARTIFACT_FRAME_ANCESTORS ?? "").split(",").map((value) => value.trim()).filter((value) => /^https?:\/\/[^/\s]+$/.test(value));
|
|
34065
35915
|
const origins = configured.length ? configured : ["https://mcpscraper.dev", "https://www.mcpscraper.dev"];
|
|
@@ -34120,10 +35970,10 @@ publicScheduledResultApp.get("/scheduled-run-view-links/:token/content", async (
|
|
|
34120
35970
|
});
|
|
34121
35971
|
|
|
34122
35972
|
// src/api/scheduled-artifact-internal-routes.ts
|
|
34123
|
-
import { Hono as
|
|
35973
|
+
import { Hono as Hono35 } from "hono";
|
|
34124
35974
|
|
|
34125
35975
|
// src/api/scheduler-integration-auth.ts
|
|
34126
|
-
import { createHash as
|
|
35976
|
+
import { createHash as createHash21, createHmac as createHmac4, timingSafeEqual as timingSafeEqual3 } from "crypto";
|
|
34127
35977
|
|
|
34128
35978
|
// src/api/service-connections.ts
|
|
34129
35979
|
import { randomUUID as randomUUID18 } from "crypto";
|
|
@@ -34416,12 +36266,12 @@ var SchedulerIntegrationAuthError = class extends Error {
|
|
|
34416
36266
|
}
|
|
34417
36267
|
};
|
|
34418
36268
|
function signingSecret() {
|
|
34419
|
-
const
|
|
34420
|
-
if (!
|
|
34421
|
-
return
|
|
36269
|
+
const secret = process.env.SCHEDULER_INTEGRATION_SIGNING_SECRET?.trim();
|
|
36270
|
+
if (!secret) throw new SchedulerIntegrationAuthError("not_configured");
|
|
36271
|
+
return secret;
|
|
34422
36272
|
}
|
|
34423
36273
|
function schedulerIntegrationSignature(args) {
|
|
34424
|
-
const bodyHash =
|
|
36274
|
+
const bodyHash = createHash21("sha256").update(args.body).digest("hex");
|
|
34425
36275
|
return createHmac4("sha256", args.secret).update(`${args.method.toUpperCase()}
|
|
34426
36276
|
${args.path}
|
|
34427
36277
|
${args.timestamp}
|
|
@@ -34477,7 +36327,7 @@ async function verifySchedulerIntegrationRequest(request, rawBody) {
|
|
|
34477
36327
|
}
|
|
34478
36328
|
|
|
34479
36329
|
// src/api/scheduled-artifact-internal-routes.ts
|
|
34480
|
-
var scheduledArtifactInternalApp = new
|
|
36330
|
+
var scheduledArtifactInternalApp = new Hono35();
|
|
34481
36331
|
function errorPayload(errorCode, error, requestId) {
|
|
34482
36332
|
return ApiErrorResponseSchema.parse({
|
|
34483
36333
|
ok: false,
|
|
@@ -34597,7 +36447,7 @@ function startSiteAuditWorker() {
|
|
|
34597
36447
|
}
|
|
34598
36448
|
|
|
34599
36449
|
// src/api/server.ts
|
|
34600
|
-
import { createMiddleware as
|
|
36450
|
+
import { createMiddleware as createMiddleware5 } from "hono/factory";
|
|
34601
36451
|
import { getCookie as getCookie2, setCookie as setCookie2, deleteCookie } from "hono/cookie";
|
|
34602
36452
|
import Stripe2 from "stripe";
|
|
34603
36453
|
|
|
@@ -34677,13 +36527,13 @@ async function reconcileSiteExtractSettlements(limit = 25) {
|
|
|
34677
36527
|
}
|
|
34678
36528
|
|
|
34679
36529
|
// src/api/page-diff.ts
|
|
34680
|
-
import { createHash as
|
|
36530
|
+
import { createHash as createHash22 } from "crypto";
|
|
34681
36531
|
import { diffLines } from "diff";
|
|
34682
36532
|
var MAX_SNAPSHOT_CONTENT_CHARS = 25e4;
|
|
34683
36533
|
var MAX_DIFF_HUNKS = 200;
|
|
34684
36534
|
var MAX_DIFF_LINES_PER_RESPONSE = 2e3;
|
|
34685
36535
|
function sha256Hex(value) {
|
|
34686
|
-
return
|
|
36536
|
+
return createHash22("sha256").update(value).digest("hex");
|
|
34687
36537
|
}
|
|
34688
36538
|
function truncateForStorage(value, maxChars = MAX_SNAPSHOT_CONTENT_CHARS) {
|
|
34689
36539
|
if (value.length <= maxChars) return { value, truncated: false };
|
|
@@ -34797,10 +36647,10 @@ async function persistScrapeBody(user, opts) {
|
|
|
34797
36647
|
}
|
|
34798
36648
|
|
|
34799
36649
|
// src/api/scrape-image-sink.ts
|
|
34800
|
-
import { createHash as
|
|
36650
|
+
import { createHash as createHash23 } from "crypto";
|
|
34801
36651
|
var MAX_IMAGES_PER_SCRAPE = 25;
|
|
34802
36652
|
function idempotencyKey2(userId, vault, input) {
|
|
34803
|
-
return `scrape-image-${
|
|
36653
|
+
return `scrape-image-${createHash23("sha256").update(`${userId}\0${vault}\0${input.sourceKind}\0${input.sourceUrl}\0${input.imageUrl ?? ""}\0${input.imageBase64 ?? ""}`).digest("hex")}`;
|
|
34804
36654
|
}
|
|
34805
36655
|
async function persistScrapeImagesToMemory(user, inputs, vault) {
|
|
34806
36656
|
const selected = inputs.slice(0, MAX_IMAGES_PER_SCRAPE);
|
|
@@ -35272,7 +37122,7 @@ function inferredWaybackCapturedAt(content) {
|
|
|
35272
37122
|
}
|
|
35273
37123
|
|
|
35274
37124
|
// src/api/connection-memory-import.ts
|
|
35275
|
-
import { createHash as
|
|
37125
|
+
import { createHash as createHash24 } from "crypto";
|
|
35276
37126
|
var CONNECTION_MEMORY_IMPORT_MAX_ARGS_BYTES = 64 * 1024;
|
|
35277
37127
|
var CONNECTION_MEMORY_IMPORT_MAX_RESULT_BYTES = 1e6;
|
|
35278
37128
|
var CONNECTION_MEMORY_IMPORT_MAX_STRING_CHARS = 256e3;
|
|
@@ -35302,7 +37152,7 @@ function canonicalJson(value) {
|
|
|
35302
37152
|
return JSON.stringify(value);
|
|
35303
37153
|
}
|
|
35304
37154
|
function sha2562(value) {
|
|
35305
|
-
return
|
|
37155
|
+
return createHash24("sha256").update(value).digest("hex");
|
|
35306
37156
|
}
|
|
35307
37157
|
function sensitiveKey(key) {
|
|
35308
37158
|
const normalized = key.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
@@ -36313,7 +38163,7 @@ async function dbUsage(identity, plan) {
|
|
|
36313
38163
|
}
|
|
36314
38164
|
|
|
36315
38165
|
// src/api/nango-control.ts
|
|
36316
|
-
import { createHash as
|
|
38166
|
+
import { createHash as createHash26, randomUUID as randomUUID22 } from "crypto";
|
|
36317
38167
|
|
|
36318
38168
|
// src/api/slack-archive-analysis.ts
|
|
36319
38169
|
function isRecord2(value) {
|
|
@@ -36687,7 +38537,7 @@ async function exportSlackChannelPage(input, dependencies) {
|
|
|
36687
38537
|
}
|
|
36688
38538
|
|
|
36689
38539
|
// src/api/main-nango-transport.ts
|
|
36690
|
-
import { createHash as
|
|
38540
|
+
import { createHash as createHash25, randomUUID as randomUUID21 } from "crypto";
|
|
36691
38541
|
import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client";
|
|
36692
38542
|
|
|
36693
38543
|
// src/api/connected-tool-policy.ts
|
|
@@ -36820,9 +38670,9 @@ function arrayAt(payload, key) {
|
|
|
36820
38670
|
return Array.isArray(data?.[key]) ? data[key] : [];
|
|
36821
38671
|
}
|
|
36822
38672
|
function nangoSecret() {
|
|
36823
|
-
const
|
|
36824
|
-
if (!
|
|
36825
|
-
return
|
|
38673
|
+
const secret = process.env.NANGO_SECRET_KEY?.trim();
|
|
38674
|
+
if (!secret) throw new MainNangoTransportError("Nango is not configured.", 503, "nango_not_configured");
|
|
38675
|
+
return secret;
|
|
36826
38676
|
}
|
|
36827
38677
|
function nangoMcpUrl() {
|
|
36828
38678
|
try {
|
|
@@ -37080,7 +38930,7 @@ async function listNangoToolsDirect(identity, connectionId) {
|
|
|
37080
38930
|
});
|
|
37081
38931
|
const readTools = [...policies.values()].filter((policy4) => policy4.classification === "read").map((policy4) => policy4.name);
|
|
37082
38932
|
const actionTools = [...policies.values()].filter((policy4) => policy4.classification === "action").map((policy4) => policy4.name);
|
|
37083
|
-
const revision =
|
|
38933
|
+
const revision = createHash25("sha256").update(JSON.stringify(tools.map((tool) => ({ name: tool.name, inputSchema: tool.inputSchema })))).digest("hex");
|
|
37084
38934
|
await updateServiceConnectionTools(connection.id, readTools, actionTools, revision);
|
|
37085
38935
|
const refreshed = await getOwnedServiceConnection(identity, connection.id);
|
|
37086
38936
|
return { connection: refreshed ?? { ...connection, readTools, actionTools, toolRevision: revision }, tools };
|
|
@@ -37108,7 +38958,7 @@ async function callNangoToolDirect(args) {
|
|
|
37108
38958
|
ratePolicyVersion: CONNECTED_USAGE_RATE_POLICY_VERSION
|
|
37109
38959
|
});
|
|
37110
38960
|
const requestId = args.requestId?.trim() || randomUUID21();
|
|
37111
|
-
const idempotencyKey3 = `main-nango:${
|
|
38961
|
+
const idempotencyKey3 = `main-nango:${createHash25("sha256").update(args.identity.toLowerCase()).update("\0").update(connection.id).update("\0").update(args.tool).update("\0").update(requestId).digest("hex")}`;
|
|
37112
38962
|
const startedAt = /* @__PURE__ */ new Date();
|
|
37113
38963
|
const started = performance.now();
|
|
37114
38964
|
let result;
|
|
@@ -37133,7 +38983,7 @@ async function callNangoToolDirect(args) {
|
|
|
37133
38983
|
toolName: args.tool,
|
|
37134
38984
|
operationKind: args.operationKind ?? args.classification,
|
|
37135
38985
|
outcome: providerError ? "error" : "partial",
|
|
37136
|
-
requestId: requestId.length <= 200 ? requestId :
|
|
38986
|
+
requestId: requestId.length <= 200 ? requestId : createHash25("sha256").update(requestId).digest("hex"),
|
|
37137
38987
|
startedAt: startedAt.toISOString(),
|
|
37138
38988
|
completedAt: completedAt.toISOString()
|
|
37139
38989
|
}
|
|
@@ -37175,7 +39025,7 @@ async function describeNangoToolDirect(identity, connectionId, toolName) {
|
|
|
37175
39025
|
providerContractHash: MAIN_INTEGRATION_CONTRACT_HASH,
|
|
37176
39026
|
protocolVersion: null,
|
|
37177
39027
|
schemaSource: "live_tools_list",
|
|
37178
|
-
schemaHash:
|
|
39028
|
+
schemaHash: createHash25("sha256").update(JSON.stringify(projected)).digest("hex"),
|
|
37179
39029
|
fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
37180
39030
|
};
|
|
37181
39031
|
}
|
|
@@ -37321,9 +39171,9 @@ function controlBaseUrl() {
|
|
|
37321
39171
|
return (process.env.NANGO_CONTROL_URL?.trim() || DEFAULT_NANGO_CONTROL_URL).replace(/\/$/, "");
|
|
37322
39172
|
}
|
|
37323
39173
|
function controlSecret() {
|
|
37324
|
-
const
|
|
37325
|
-
if (!
|
|
37326
|
-
return
|
|
39174
|
+
const secret = process.env.SCHEDULE_INTEGRATIONS_SECRET?.trim();
|
|
39175
|
+
if (!secret) throw new NangoControlError("Scheduled service connections are not configured.", 503);
|
|
39176
|
+
return secret;
|
|
37327
39177
|
}
|
|
37328
39178
|
function isRecord4(value) {
|
|
37329
39179
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
@@ -37899,8 +39749,8 @@ async function setScheduleConnectionActionsEnabled(identity, connectionId, enabl
|
|
|
37899
39749
|
return data.connection.actionsEnabled === true;
|
|
37900
39750
|
}
|
|
37901
39751
|
async function callScheduleConnectionAction(identity, connectionId, input, tool, idempotencyKey3) {
|
|
37902
|
-
const requestId = `main-connected-action:${
|
|
37903
|
-
const requestDigest =
|
|
39752
|
+
const requestId = `main-connected-action:${createHash26("sha256").update(identity).update("\0").update(idempotencyKey3?.trim() || randomUUID22()).digest("hex")}`;
|
|
39753
|
+
const requestDigest = createHash26("sha256").update(connectionId).update("\0").update(tool?.trim() ?? "").update("\0").update(canonicalJson2(input)).digest("hex");
|
|
37904
39754
|
if (mainOwnsIntegrations()) {
|
|
37905
39755
|
const selectedTool = tool?.trim();
|
|
37906
39756
|
if (!selectedTool) throw new NangoControlError("An action tool is required.", 400, "invalid_request", false);
|
|
@@ -38051,7 +39901,7 @@ function canonicalJson2(value) {
|
|
|
38051
39901
|
return JSON.stringify(value);
|
|
38052
39902
|
}
|
|
38053
39903
|
function projectedToolSchemaHash(tool) {
|
|
38054
|
-
return
|
|
39904
|
+
return createHash26("sha256").update(canonicalJson2(tool)).digest("hex");
|
|
38055
39905
|
}
|
|
38056
39906
|
async function describeNangoTool(identity, connectionId, tool, fresh) {
|
|
38057
39907
|
if (mainOwnsIntegrations()) {
|
|
@@ -38330,7 +40180,7 @@ async function callMainOwnedExportPage(identity, input) {
|
|
|
38330
40180
|
}
|
|
38331
40181
|
|
|
38332
40182
|
// src/api/resend-control.ts
|
|
38333
|
-
import { createHash as
|
|
40183
|
+
import { createHash as createHash27 } from "crypto";
|
|
38334
40184
|
var DEFAULT_CONNECTION_CONTROL_URL = "https://mcp-scraper-scheduler.vercel.app";
|
|
38335
40185
|
var RESEND_PROVIDER_CONFIG_KEY = "resend";
|
|
38336
40186
|
var RESEND_LOGO_URL = "https://cdn.resend.com/brand/resend-icon-black.svg";
|
|
@@ -38370,9 +40220,9 @@ function controlBaseUrl2() {
|
|
|
38370
40220
|
return (process.env.SCHEDULE_INTEGRATIONS_CONTROL_URL?.trim() || process.env.NANGO_CONTROL_URL?.trim() || DEFAULT_CONNECTION_CONTROL_URL).replace(/\/$/, "");
|
|
38371
40221
|
}
|
|
38372
40222
|
function controlSecret2() {
|
|
38373
|
-
const
|
|
38374
|
-
if (!
|
|
38375
|
-
return
|
|
40223
|
+
const secret = process.env.SCHEDULE_INTEGRATIONS_SECRET?.trim();
|
|
40224
|
+
if (!secret) throw new ResendControlError("Resend connections are not configured.", 503);
|
|
40225
|
+
return secret;
|
|
38376
40226
|
}
|
|
38377
40227
|
function isRecord5(value) {
|
|
38378
40228
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
@@ -38589,7 +40439,7 @@ async function callResendRead(identity, connectionId, tool, args) {
|
|
|
38589
40439
|
return isRecord5(data) ? data.result ?? data : data;
|
|
38590
40440
|
}
|
|
38591
40441
|
async function callResendAction(identity, connectionId, tool, input, idempotencyKey3) {
|
|
38592
|
-
const requestId = `main-resend-action:${
|
|
40442
|
+
const requestId = `main-resend-action:${createHash27("sha256").update(identity).update("\0").update(idempotencyKey3.trim()).digest("hex")}`;
|
|
38593
40443
|
const body = await controlRequest2("/api/internal/resend/actions/call", {
|
|
38594
40444
|
method: "POST",
|
|
38595
40445
|
headers: { "x-request-id": requestId },
|
|
@@ -38657,7 +40507,7 @@ async function callResendExportPage(identity, input) {
|
|
|
38657
40507
|
|
|
38658
40508
|
// src/api/server.ts
|
|
38659
40509
|
var secureCookies2 = process.env.NODE_ENV === "production" || process.env.VERCEL === "1";
|
|
38660
|
-
var
|
|
40510
|
+
var isProduction = secureCookies2;
|
|
38661
40511
|
var sessionCookieOptions2 = {
|
|
38662
40512
|
httpOnly: true,
|
|
38663
40513
|
secure: secureCookies2,
|
|
@@ -38671,7 +40521,7 @@ function configuredOrigins() {
|
|
|
38671
40521
|
const trimmed = raw.trim();
|
|
38672
40522
|
if (trimmed) origins.add(trimmed);
|
|
38673
40523
|
}
|
|
38674
|
-
if (!
|
|
40524
|
+
if (!isProduction) {
|
|
38675
40525
|
origins.add("http://localhost:5173");
|
|
38676
40526
|
origins.add("http://localhost:3000");
|
|
38677
40527
|
origins.add("http://localhost:63316");
|
|
@@ -38697,13 +40547,13 @@ async function enforceRateLimit(c, scope, key, limit, windowSeconds) {
|
|
|
38697
40547
|
{ "Retry-After": String(result.resetSeconds) }
|
|
38698
40548
|
);
|
|
38699
40549
|
}
|
|
38700
|
-
var requireAllowedOrigin =
|
|
40550
|
+
var requireAllowedOrigin = createMiddleware5(async (c, next) => {
|
|
38701
40551
|
const origin = c.req.header("origin");
|
|
38702
40552
|
if (!origin) return next();
|
|
38703
40553
|
if (!configuredOrigins().has(origin)) return c.json({ error: "Origin not allowed" }, 403);
|
|
38704
40554
|
return next();
|
|
38705
40555
|
});
|
|
38706
|
-
var
|
|
40556
|
+
var auth4 = createMiddleware5(async (c, next) => {
|
|
38707
40557
|
const key = c.req.header("x-api-key");
|
|
38708
40558
|
if (!key) return c.json({ error: "Missing API key" }, 401);
|
|
38709
40559
|
const user = await getUserByApiKey(key);
|
|
@@ -38715,7 +40565,7 @@ var auth3 = createMiddleware4(async (c, next) => {
|
|
|
38715
40565
|
if (cost && cost.userId == null) cost.userId = Number(refreshed.id);
|
|
38716
40566
|
return next();
|
|
38717
40567
|
});
|
|
38718
|
-
var sessionAuth =
|
|
40568
|
+
var sessionAuth = createMiddleware5(async (c, next) => {
|
|
38719
40569
|
const token4 = getCookie2(c, "session");
|
|
38720
40570
|
if (!token4) return c.json({ error: "Not authenticated" }, 401);
|
|
38721
40571
|
const userId = verifySession(token4);
|
|
@@ -38727,7 +40577,7 @@ var sessionAuth = createMiddleware4(async (c, next) => {
|
|
|
38727
40577
|
c.set("sessionUser", { ...refreshed, balance_mc: balanceMc });
|
|
38728
40578
|
return next();
|
|
38729
40579
|
});
|
|
38730
|
-
var requireIntegrationsTier =
|
|
40580
|
+
var requireIntegrationsTier = createMiddleware5(async (c, next) => {
|
|
38731
40581
|
const user = c.get("user");
|
|
38732
40582
|
if (!user.subscription_tier) {
|
|
38733
40583
|
return c.json({
|
|
@@ -38738,27 +40588,27 @@ var requireIntegrationsTier = createMiddleware4(async (c, next) => {
|
|
|
38738
40588
|
}
|
|
38739
40589
|
return next();
|
|
38740
40590
|
});
|
|
38741
|
-
var requirePaidSchedulingTier =
|
|
40591
|
+
var requirePaidSchedulingTier = createMiddleware5(async (c, next) => {
|
|
38742
40592
|
const user = c.get("user");
|
|
38743
40593
|
if (!hasPaidSchedulingAccess(user)) return c.json(paidPlanRequiredPayload(), 403);
|
|
38744
40594
|
return next();
|
|
38745
40595
|
});
|
|
38746
|
-
var app = new
|
|
40596
|
+
var app = new Hono36();
|
|
38747
40597
|
var deploymentProfile = resolveDeploymentProfile();
|
|
38748
40598
|
var STRIPE_API_VERSION = "2026-02-25.clover";
|
|
38749
40599
|
function permitsLocalApiRequest(request) {
|
|
38750
40600
|
return (deploymentProfile === "development" || deploymentProfile === "test" || process.env.MCP_SCRAPER_ALLOW_PRIVATE_NETWORK === "1") && request.headers.get("x-mcp-scraper-local-network") === "1" && isLoopbackBaseUrl(request.url);
|
|
38751
40601
|
}
|
|
38752
40602
|
function requireStripeSecret() {
|
|
38753
|
-
const
|
|
38754
|
-
if (
|
|
40603
|
+
const secret = process.env.STRIPE_SECRET_KEY?.trim();
|
|
40604
|
+
if (secret) return secret;
|
|
38755
40605
|
if (process.env.NODE_ENV === "production" || process.env.VERCEL === "1") return null;
|
|
38756
40606
|
return null;
|
|
38757
40607
|
}
|
|
38758
40608
|
async function createSignupStripeCustomer(email) {
|
|
38759
|
-
const
|
|
38760
|
-
if (!
|
|
38761
|
-
const stripeClient = new Stripe2(
|
|
40609
|
+
const secret = requireStripeSecret();
|
|
40610
|
+
if (!secret) return null;
|
|
40611
|
+
const stripeClient = new Stripe2(secret, { apiVersion: STRIPE_API_VERSION });
|
|
38762
40612
|
const customer = await stripeClient.customers.create({
|
|
38763
40613
|
email,
|
|
38764
40614
|
description: "MCP Scraper free account",
|
|
@@ -38909,9 +40759,9 @@ app.post("/account/delete", requireAllowedOrigin, sessionAuth, async (c) => {
|
|
|
38909
40759
|
return c.json({ error: "Could not remove every connected account \u2014 please retry before deleting the account." }, 502);
|
|
38910
40760
|
}
|
|
38911
40761
|
}
|
|
38912
|
-
const
|
|
38913
|
-
if (
|
|
38914
|
-
const stripeClient = new Stripe2(
|
|
40762
|
+
const secret = requireStripeSecret();
|
|
40763
|
+
if (secret && user.stripe_customer_id) {
|
|
40764
|
+
const stripeClient = new Stripe2(secret, { apiVersion: STRIPE_API_VERSION });
|
|
38915
40765
|
const subs = await stripeClient.subscriptions.list({ customer: user.stripe_customer_id, status: "all", limit: 100 });
|
|
38916
40766
|
for (const sub of subs.data) {
|
|
38917
40767
|
if (sub.status === "active" || sub.status === "trialing" || sub.status === "past_due") {
|
|
@@ -39048,7 +40898,7 @@ function maybeProvisionInBackground(user, vaultCount) {
|
|
|
39048
40898
|
}
|
|
39049
40899
|
})();
|
|
39050
40900
|
}
|
|
39051
|
-
app.get("/memory/overview",
|
|
40901
|
+
app.get("/memory/overview", auth4, async (c) => {
|
|
39052
40902
|
const user = c.get("user");
|
|
39053
40903
|
const identity = resolveMemoryIdentity(user);
|
|
39054
40904
|
const plan = memoryPlanForReads(user);
|
|
@@ -39076,7 +40926,7 @@ app.get("/memory/overview", auth3, async (c) => {
|
|
|
39076
40926
|
return c.json({ ok: false, error: message }, 502);
|
|
39077
40927
|
}
|
|
39078
40928
|
});
|
|
39079
|
-
app.get("/memory/vaults",
|
|
40929
|
+
app.get("/memory/vaults", auth4, async (c) => {
|
|
39080
40930
|
const user = c.get("user");
|
|
39081
40931
|
try {
|
|
39082
40932
|
const vaults = await dbListVaults(resolveMemoryIdentity(user));
|
|
@@ -39086,7 +40936,7 @@ app.get("/memory/vaults", auth3, async (c) => {
|
|
|
39086
40936
|
return c.json({ ok: false, error: err instanceof Error ? err.message : "could not load vaults" }, 502);
|
|
39087
40937
|
}
|
|
39088
40938
|
});
|
|
39089
|
-
app.post("/memory/mcp-call",
|
|
40939
|
+
app.post("/memory/mcp-call", auth4, async (c) => {
|
|
39090
40940
|
const user = c.get("user");
|
|
39091
40941
|
const body = await c.req.json().catch(() => ({}));
|
|
39092
40942
|
if (!body.toolName) return c.json({ ok: false, error: "toolName is required" }, 400);
|
|
@@ -39107,13 +40957,13 @@ app.post("/memory/mcp-call", auth3, async (c) => {
|
|
|
39107
40957
|
const result = await memoryCall(body.toolName, body.args ?? {}, key);
|
|
39108
40958
|
return c.json(result);
|
|
39109
40959
|
});
|
|
39110
|
-
app.get("/memory/approved-senders",
|
|
40960
|
+
app.get("/memory/approved-senders", auth4, async (c) => {
|
|
39111
40961
|
const { key, error } = await getOrCreateUserMemoryKey(c.get("user"));
|
|
39112
40962
|
if (!key) return c.json({ ok: false, error: error ?? "memory unavailable" }, 502);
|
|
39113
40963
|
const r = await memoryCall("listApprovedSendersTool", {}, key);
|
|
39114
40964
|
return c.json(r);
|
|
39115
40965
|
});
|
|
39116
|
-
app.post("/memory/approved-senders",
|
|
40966
|
+
app.post("/memory/approved-senders", auth4, async (c) => {
|
|
39117
40967
|
const { key, error } = await getOrCreateUserMemoryKey(c.get("user"));
|
|
39118
40968
|
if (!key) return c.json({ ok: false, error: error ?? "memory unavailable" }, 502);
|
|
39119
40969
|
const body = await c.req.json().catch(() => ({}));
|
|
@@ -39122,14 +40972,14 @@ app.post("/memory/approved-senders", auth3, async (c) => {
|
|
|
39122
40972
|
const r = await memoryCall("approveSenderTool", { senderIdentity }, key);
|
|
39123
40973
|
return c.json(r);
|
|
39124
40974
|
});
|
|
39125
|
-
app.delete("/memory/approved-senders/:identity",
|
|
40975
|
+
app.delete("/memory/approved-senders/:identity", auth4, async (c) => {
|
|
39126
40976
|
const { key, error } = await getOrCreateUserMemoryKey(c.get("user"));
|
|
39127
40977
|
if (!key) return c.json({ ok: false, error: error ?? "memory unavailable" }, 502);
|
|
39128
40978
|
const senderIdentity = c.req.param("identity");
|
|
39129
40979
|
const r = await memoryCall("removeApprovedSenderTool", { senderIdentity }, key);
|
|
39130
40980
|
return c.json(r);
|
|
39131
40981
|
});
|
|
39132
|
-
app.get("/memory/connect",
|
|
40982
|
+
app.get("/memory/connect", auth4, async (c) => {
|
|
39133
40983
|
const { key, error } = await getOrCreateUserMemoryKey(c.get("user"));
|
|
39134
40984
|
if (!key) return c.json({ error: error ?? "memory unavailable" }, 502);
|
|
39135
40985
|
const url = "https://memory.mcpscraper.dev/mcp";
|
|
@@ -39144,7 +40994,7 @@ app.get("/memory/connect", auth3, async (c) => {
|
|
|
39144
40994
|
const codeCommand = `claude mcp add --transport http memory ${url} --header "Authorization: Bearer ${key}"`;
|
|
39145
40995
|
return c.json({ ok: true, url, key, config, codeCommand });
|
|
39146
40996
|
});
|
|
39147
|
-
app.post("/memory/vault",
|
|
40997
|
+
app.post("/memory/vault", auth4, async (c) => {
|
|
39148
40998
|
const { key, error } = await getOrCreateUserMemoryKey(c.get("user"));
|
|
39149
40999
|
if (!key) return c.json({ error: error ?? "memory unavailable" }, 502);
|
|
39150
41000
|
const body = await c.req.json().catch(() => ({}));
|
|
@@ -39153,7 +41003,7 @@ app.post("/memory/vault", auth3, async (c) => {
|
|
|
39153
41003
|
const result = await memoryCall("addVaultTool", { vault }, key);
|
|
39154
41004
|
return c.json(result, result.ok ? 200 : 502);
|
|
39155
41005
|
});
|
|
39156
|
-
app.get("/memory/notes",
|
|
41006
|
+
app.get("/memory/notes", auth4, async (c) => {
|
|
39157
41007
|
const user = c.get("user");
|
|
39158
41008
|
const identity = resolveMemoryIdentity(user);
|
|
39159
41009
|
try {
|
|
@@ -39169,7 +41019,7 @@ app.get("/memory/notes", auth3, async (c) => {
|
|
|
39169
41019
|
return c.json({ ok: false, error: err instanceof Error ? err.message : "could not load notes" }, 502);
|
|
39170
41020
|
}
|
|
39171
41021
|
});
|
|
39172
|
-
app.get("/memory/note",
|
|
41022
|
+
app.get("/memory/note", auth4, async (c) => {
|
|
39173
41023
|
const user = c.get("user");
|
|
39174
41024
|
const vault = c.req.query("vault")?.trim();
|
|
39175
41025
|
const path5 = c.req.query("path");
|
|
@@ -39183,7 +41033,7 @@ app.get("/memory/note", auth3, async (c) => {
|
|
|
39183
41033
|
return c.json({ ok: false, error: err instanceof Error ? err.message : "could not load note" }, 502);
|
|
39184
41034
|
}
|
|
39185
41035
|
});
|
|
39186
|
-
app.get("/memory/universe",
|
|
41036
|
+
app.get("/memory/universe", auth4, async (c) => {
|
|
39187
41037
|
const user = c.get("user");
|
|
39188
41038
|
const includeConnections = c.req.query("includeConnections") === "1";
|
|
39189
41039
|
try {
|
|
@@ -39193,7 +41043,7 @@ app.get("/memory/universe", auth3, async (c) => {
|
|
|
39193
41043
|
return c.json({ ok: false, error: err instanceof Error ? err.message : "could not build universe" }, 502);
|
|
39194
41044
|
}
|
|
39195
41045
|
});
|
|
39196
|
-
app.put("/memory/note",
|
|
41046
|
+
app.put("/memory/note", auth4, async (c) => {
|
|
39197
41047
|
const { key, error } = await getOrCreateUserMemoryKey(c.get("user"));
|
|
39198
41048
|
if (!key) return c.json({ error: error ?? "memory unavailable" }, 502);
|
|
39199
41049
|
const body = await c.req.json().catch(() => ({}));
|
|
@@ -39201,7 +41051,7 @@ app.put("/memory/note", auth3, async (c) => {
|
|
|
39201
41051
|
const result = await memoryCall("putTool", { vault: body.vault, path: body.path, title: body.title, content: body.content }, key);
|
|
39202
41052
|
return c.json(result, result.ok ? 200 : 502);
|
|
39203
41053
|
});
|
|
39204
|
-
app.delete("/memory/note",
|
|
41054
|
+
app.delete("/memory/note", auth4, async (c) => {
|
|
39205
41055
|
const { key, error } = await getOrCreateUserMemoryKey(c.get("user"));
|
|
39206
41056
|
if (!key) return c.json({ error: error ?? "memory unavailable" }, 502);
|
|
39207
41057
|
const vault = c.req.query("vault");
|
|
@@ -39210,7 +41060,7 @@ app.delete("/memory/note", auth3, async (c) => {
|
|
|
39210
41060
|
const result = await memoryCall("deleteNoteTool", { vault, path: path5 }, key);
|
|
39211
41061
|
return c.json(result, result.ok ? 200 : 502);
|
|
39212
41062
|
});
|
|
39213
|
-
app.get("/memory/note/download",
|
|
41063
|
+
app.get("/memory/note/download", auth4, async (c) => {
|
|
39214
41064
|
const user = c.get("user");
|
|
39215
41065
|
const vault = c.req.query("vault")?.trim();
|
|
39216
41066
|
const path5 = c.req.query("path");
|
|
@@ -39227,7 +41077,7 @@ app.get("/memory/note/download", auth3, async (c) => {
|
|
|
39227
41077
|
}
|
|
39228
41078
|
});
|
|
39229
41079
|
});
|
|
39230
|
-
app.get("/memory/storage",
|
|
41080
|
+
app.get("/memory/storage", auth4, async (c) => {
|
|
39231
41081
|
const user = c.get("user");
|
|
39232
41082
|
try {
|
|
39233
41083
|
const result = await dbStorage(resolveMemoryIdentity(user), memoryPlanForReads(user));
|
|
@@ -39236,7 +41086,7 @@ app.get("/memory/storage", auth3, async (c) => {
|
|
|
39236
41086
|
return c.json({ ok: false, error: err instanceof Error ? err.message : "could not load storage" }, 502);
|
|
39237
41087
|
}
|
|
39238
41088
|
});
|
|
39239
|
-
app.get("/memory/billing",
|
|
41089
|
+
app.get("/memory/billing", auth4, async (c) => {
|
|
39240
41090
|
const user = c.get("user");
|
|
39241
41091
|
const plans = Object.values(MEMORY_PLANS).map((p) => ({ plan: p.plan, label: p.label, price_id: p.price_id, interval: p.interval, monthly_usd: p.monthly_usd, quota_gb: p.quota_bytes / 1e9 }));
|
|
39242
41092
|
return c.json({
|
|
@@ -39247,7 +41097,7 @@ app.get("/memory/billing", auth3, async (c) => {
|
|
|
39247
41097
|
plans
|
|
39248
41098
|
});
|
|
39249
41099
|
});
|
|
39250
|
-
app.post("/memory/checkout",
|
|
41100
|
+
app.post("/memory/checkout", auth4, async (c) => {
|
|
39251
41101
|
try {
|
|
39252
41102
|
const user = c.get("user");
|
|
39253
41103
|
const body = await c.req.json().catch(() => ({}));
|
|
@@ -39259,9 +41109,9 @@ app.post("/memory/checkout", auth3, async (c) => {
|
|
|
39259
41109
|
error_code: "memory_already_included"
|
|
39260
41110
|
}, 409);
|
|
39261
41111
|
}
|
|
39262
|
-
const
|
|
39263
|
-
if (!
|
|
39264
|
-
const stripeClient = new Stripe2(
|
|
41112
|
+
const secret = process.env.STRIPE_SECRET_KEY?.trim();
|
|
41113
|
+
if (!secret) return c.json({ error: "Stripe is not configured." }, 503);
|
|
41114
|
+
const stripeClient = new Stripe2(secret, { apiVersion: STRIPE_API_VERSION });
|
|
39265
41115
|
let customerId = user.stripe_customer_id;
|
|
39266
41116
|
if (!customerId) {
|
|
39267
41117
|
const customer = await stripeClient.customers.create({ email: user.email });
|
|
@@ -39290,13 +41140,13 @@ app.post("/memory/checkout", auth3, async (c) => {
|
|
|
39290
41140
|
return c.json({ error: message }, 500);
|
|
39291
41141
|
}
|
|
39292
41142
|
});
|
|
39293
|
-
app.post("/memory/portal",
|
|
41143
|
+
app.post("/memory/portal", auth4, async (c) => {
|
|
39294
41144
|
try {
|
|
39295
41145
|
const user = c.get("user");
|
|
39296
41146
|
if (!user.stripe_customer_id) return c.json({ error: "No billing account yet \u2014 subscribe first." }, 409);
|
|
39297
|
-
const
|
|
39298
|
-
if (!
|
|
39299
|
-
const stripeClient = new Stripe2(
|
|
41147
|
+
const secret = process.env.STRIPE_SECRET_KEY?.trim();
|
|
41148
|
+
if (!secret) return c.json({ error: "Stripe is not configured." }, 503);
|
|
41149
|
+
const stripeClient = new Stripe2(secret, { apiVersion: STRIPE_API_VERSION });
|
|
39300
41150
|
const session = await stripeClient.billingPortal.sessions.create({
|
|
39301
41151
|
customer: user.stripe_customer_id,
|
|
39302
41152
|
return_url: `${appOrigin()}/memory`
|
|
@@ -39308,7 +41158,7 @@ app.post("/memory/portal", auth3, async (c) => {
|
|
|
39308
41158
|
return c.json({ error: message }, 500);
|
|
39309
41159
|
}
|
|
39310
41160
|
});
|
|
39311
|
-
app.post("/schedule-checkout",
|
|
41161
|
+
app.post("/schedule-checkout", auth4, async (c) => {
|
|
39312
41162
|
return c.json({
|
|
39313
41163
|
error_code: "scheduled_actions_now_credit_metered",
|
|
39314
41164
|
error: "Scheduled Actions is included and no longer requires a separate subscription.",
|
|
@@ -39468,7 +41318,7 @@ function renderResendOAuthResult(ok) {
|
|
|
39468
41318
|
const message = ok ? "Your Resend account is connected privately to MCP Scraper. You can close this tab." : "The connection could not be completed. Close this tab and try again from Integrations.";
|
|
39469
41319
|
return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="referrer" content="no-referrer"><title>${title}</title><style>:root{color-scheme:light dark}*{box-sizing:border-box}body{margin:0;min-height:100vh;display:grid;place-items:center;padding:24px;background:#f5f2ee;color:#26211d;font-family:ui-sans-serif,system-ui,-apple-system,sans-serif}.card{width:min(100%,440px);padding:32px;border:1px solid #d8d0c8;border-radius:16px;background:#fff;box-shadow:0 18px 55px rgba(31,24,18,.1)}.mark{width:38px;height:38px;display:grid;place-items:center;border-radius:10px;background:#26211d;color:#fff;font-size:20px;font-weight:700}h1{margin:18px 0 8px;font-size:24px;line-height:1.15}p{margin:0;color:#71675f;line-height:1.55}a{display:inline-flex;margin-top:20px;color:#b44d36;font-weight:650;text-underline-offset:3px}@media(prefers-color-scheme:dark){body{background:#171411;color:#f4eee8}.card{background:#211d19;border-color:#3b342d}.mark{background:#f4eee8;color:#211d19}p{color:#b9aea5}}</style></head><body><main class="card"><div class="mark" aria-hidden="true">R</div><h1>${title}</h1><p>${message}</p><a href="/dashboard/integrations">Return to Integrations</a></main><script>if(window.opener){try{window.opener.postMessage({type:'mcp-scraper:resend-oauth',ok:${ok}},window.location.origin)}catch(e){}}setTimeout(function(){window.close()},1200)</script></body></html>`;
|
|
39470
41320
|
}
|
|
39471
|
-
app.get("/schedule-status",
|
|
41321
|
+
app.get("/schedule-status", auth4, requirePaidSchedulingTier, async (c) => {
|
|
39472
41322
|
try {
|
|
39473
41323
|
const user = c.get("user");
|
|
39474
41324
|
const { key, error } = await getOrCreateUserMemoryKey(user);
|
|
@@ -39488,14 +41338,14 @@ app.get("/schedule-status", auth3, requirePaidSchedulingTier, async (c) => {
|
|
|
39488
41338
|
return c.json({ error: message }, 500);
|
|
39489
41339
|
}
|
|
39490
41340
|
});
|
|
39491
|
-
app.get("/schedule-connection-catalog",
|
|
41341
|
+
app.get("/schedule-connection-catalog", auth4, async (c) => {
|
|
39492
41342
|
try {
|
|
39493
41343
|
return c.json({ ok: true, catalog: await combinedConnectionCatalog() });
|
|
39494
41344
|
} catch (err) {
|
|
39495
41345
|
return scheduleConnectionError(c, err, "Unable to load the service connection catalog.");
|
|
39496
41346
|
}
|
|
39497
41347
|
});
|
|
39498
|
-
app.get("/schedule-connections",
|
|
41348
|
+
app.get("/schedule-connections", auth4, async (c) => {
|
|
39499
41349
|
try {
|
|
39500
41350
|
const user = c.get("user");
|
|
39501
41351
|
const connections = await combinedConnections(user.email);
|
|
@@ -39511,7 +41361,7 @@ app.get("/schedule-connections", auth3, async (c) => {
|
|
|
39511
41361
|
return scheduleConnectionError(c, err, "Unable to load service connections.");
|
|
39512
41362
|
}
|
|
39513
41363
|
});
|
|
39514
|
-
app.post("/schedule-connections/billing/reconcile",
|
|
41364
|
+
app.post("/schedule-connections/billing/reconcile", auth4, requireIntegrationsTier, async (c) => {
|
|
39515
41365
|
const user = c.get("user");
|
|
39516
41366
|
try {
|
|
39517
41367
|
return c.json({
|
|
@@ -39522,7 +41372,7 @@ app.post("/schedule-connections/billing/reconcile", auth3, requireIntegrationsTi
|
|
|
39522
41372
|
return scheduleConnectionError(c, err, "Unable to synchronize connected-account billing.");
|
|
39523
41373
|
}
|
|
39524
41374
|
});
|
|
39525
|
-
app.post("/schedule-connections/session",
|
|
41375
|
+
app.post("/schedule-connections/session", auth4, requireIntegrationsTier, async (c) => {
|
|
39526
41376
|
const user = c.get("user");
|
|
39527
41377
|
const body = await c.req.json().catch(() => ({}));
|
|
39528
41378
|
const providerConfigKey = providerConfigKeyFrom(body.providerConfigKey);
|
|
@@ -39537,7 +41387,7 @@ app.post("/schedule-connections/session", auth3, requireIntegrationsTier, async
|
|
|
39537
41387
|
return scheduleConnectionError(c, err, "Unable to start the service connection flow.");
|
|
39538
41388
|
}
|
|
39539
41389
|
});
|
|
39540
|
-
app.post("/schedule-connections/:id/reconnect-session",
|
|
41390
|
+
app.post("/schedule-connections/:id/reconnect-session", auth4, requireIntegrationsTier, async (c) => {
|
|
39541
41391
|
const user = c.get("user");
|
|
39542
41392
|
const body = await c.req.json().catch(() => ({}));
|
|
39543
41393
|
const providerConfigKey = providerConfigKeyFrom(body.providerConfigKey);
|
|
@@ -39548,7 +41398,7 @@ app.post("/schedule-connections/:id/reconnect-session", auth3, requireIntegratio
|
|
|
39548
41398
|
return scheduleConnectionError(c, err, "Unable to start the service reconnection flow.");
|
|
39549
41399
|
}
|
|
39550
41400
|
});
|
|
39551
|
-
app.delete("/schedule-connections/:id",
|
|
41401
|
+
app.delete("/schedule-connections/:id", auth4, async (c) => {
|
|
39552
41402
|
const user = c.get("user");
|
|
39553
41403
|
const body = await c.req.json().catch(() => ({}));
|
|
39554
41404
|
const providerConfigKey = providerConfigKeyFrom(body.providerConfigKey);
|
|
@@ -39574,7 +41424,7 @@ app.delete("/schedule-connections/:id", auth3, async (c) => {
|
|
|
39574
41424
|
return scheduleConnectionError(c, err, "Unable to disconnect this service connection.");
|
|
39575
41425
|
}
|
|
39576
41426
|
});
|
|
39577
|
-
app.post("/schedule-connections/:id/enable-actions",
|
|
41427
|
+
app.post("/schedule-connections/:id/enable-actions", auth4, requireIntegrationsTier, async (c) => {
|
|
39578
41428
|
const user = c.get("user");
|
|
39579
41429
|
const body = await c.req.json().catch(() => ({}));
|
|
39580
41430
|
if (typeof body.enabled !== "boolean") return c.json({ ok: false, error: "enabled must be a boolean." }, 400);
|
|
@@ -39586,7 +41436,7 @@ app.post("/schedule-connections/:id/enable-actions", auth3, requireIntegrationsT
|
|
|
39586
41436
|
return scheduleConnectionError(c, err, "Unable to update this connection's action setting.");
|
|
39587
41437
|
}
|
|
39588
41438
|
});
|
|
39589
|
-
app.post("/schedule-connections/:id/test",
|
|
41439
|
+
app.post("/schedule-connections/:id/test", auth4, requireIntegrationsTier, async (c) => {
|
|
39590
41440
|
const user = c.get("user");
|
|
39591
41441
|
const body = await c.req.json().catch(() => ({}));
|
|
39592
41442
|
const providerConfigKey = providerConfigKeyFrom(body.providerConfigKey);
|
|
@@ -39676,7 +41526,7 @@ app.get("/oauth/resend/callback", async (c) => {
|
|
|
39676
41526
|
return c.html(renderResendOAuthResult(false), 502);
|
|
39677
41527
|
}
|
|
39678
41528
|
});
|
|
39679
|
-
app.post("/schedule-connections/actions/slack/send-message",
|
|
41529
|
+
app.post("/schedule-connections/actions/slack/send-message", auth4, requireIntegrationsTier, async (c) => {
|
|
39680
41530
|
const user = c.get("user");
|
|
39681
41531
|
const body = await c.req.json().catch(() => ({}));
|
|
39682
41532
|
const connectionId = providerConfigKeyFrom(body.connectionId);
|
|
@@ -39696,7 +41546,7 @@ app.post("/schedule-connections/actions/slack/send-message", auth3, requireInteg
|
|
|
39696
41546
|
return scheduleConnectionError(c, err, "Unable to send the Slack message.");
|
|
39697
41547
|
}
|
|
39698
41548
|
});
|
|
39699
|
-
app.post("/schedule-connections/actions/gmail/send-message",
|
|
41549
|
+
app.post("/schedule-connections/actions/gmail/send-message", auth4, requireIntegrationsTier, async (c) => {
|
|
39700
41550
|
const user = c.get("user");
|
|
39701
41551
|
const body = await c.req.json().catch(() => ({}));
|
|
39702
41552
|
const connectionId = providerConfigKeyFrom(body.connectionId);
|
|
@@ -39732,7 +41582,7 @@ function parseEmailAddress(headerValue) {
|
|
|
39732
41582
|
const bare = value.match(/^([^\s<>]+@[^\s<>]+)$/);
|
|
39733
41583
|
return bare ? { email: bare[1].trim().toLowerCase(), name: null } : null;
|
|
39734
41584
|
}
|
|
39735
|
-
app.post("/schedule-connections/actions/gmail/search-contacts",
|
|
41585
|
+
app.post("/schedule-connections/actions/gmail/search-contacts", auth4, requireIntegrationsTier, async (c) => {
|
|
39736
41586
|
const user = c.get("user");
|
|
39737
41587
|
const body = await c.req.json().catch(() => ({}));
|
|
39738
41588
|
const connectionId = providerConfigKeyFrom(body.connectionId);
|
|
@@ -39786,7 +41636,7 @@ app.post("/schedule-connections/actions/gmail/search-contacts", auth3, requireIn
|
|
|
39786
41636
|
return scheduleConnectionError(c, err, "Unable to search Gmail contacts.");
|
|
39787
41637
|
}
|
|
39788
41638
|
});
|
|
39789
|
-
app.post("/schedule-connections/actions/google-calendar/create-event",
|
|
41639
|
+
app.post("/schedule-connections/actions/google-calendar/create-event", auth4, requireIntegrationsTier, async (c) => {
|
|
39790
41640
|
const user = c.get("user");
|
|
39791
41641
|
const body = await c.req.json().catch(() => ({}));
|
|
39792
41642
|
const connectionId = providerConfigKeyFrom(body.connectionId);
|
|
@@ -39816,7 +41666,7 @@ app.post("/schedule-connections/actions/google-calendar/create-event", auth3, re
|
|
|
39816
41666
|
return scheduleConnectionError(c, err, "Unable to create the calendar event.");
|
|
39817
41667
|
}
|
|
39818
41668
|
});
|
|
39819
|
-
app.post("/schedule-connections/actions/zoom/create-meeting",
|
|
41669
|
+
app.post("/schedule-connections/actions/zoom/create-meeting", auth4, requireIntegrationsTier, async (c) => {
|
|
39820
41670
|
const user = c.get("user");
|
|
39821
41671
|
const body = await c.req.json().catch(() => ({}));
|
|
39822
41672
|
const connectionId = providerConfigKeyFrom(body.connectionId);
|
|
@@ -39842,7 +41692,7 @@ app.post("/schedule-connections/actions/zoom/create-meeting", auth3, requireInte
|
|
|
39842
41692
|
return scheduleConnectionError(c, err, "Unable to create the Zoom meeting.");
|
|
39843
41693
|
}
|
|
39844
41694
|
});
|
|
39845
|
-
app.post("/schedule-connections/actions/read",
|
|
41695
|
+
app.post("/schedule-connections/actions/read", auth4, requireIntegrationsTier, async (c) => {
|
|
39846
41696
|
const user = c.get("user");
|
|
39847
41697
|
const body = await c.req.json().catch(() => ({}));
|
|
39848
41698
|
const connectionId = providerConfigKeyFrom(body.connectionId);
|
|
@@ -39859,7 +41709,7 @@ app.post("/schedule-connections/actions/read", auth3, requireIntegrationsTier, a
|
|
|
39859
41709
|
return scheduleConnectionError(c, err, "Unable to read from this connection.");
|
|
39860
41710
|
}
|
|
39861
41711
|
});
|
|
39862
|
-
app.post("/schedule-connections/actions/import-memory",
|
|
41712
|
+
app.post("/schedule-connections/actions/import-memory", auth4, requireIntegrationsTier, async (c) => {
|
|
39863
41713
|
const user = c.get("user");
|
|
39864
41714
|
const body = await c.req.json().catch(() => ({}));
|
|
39865
41715
|
const connectionId = providerConfigKeyFrom(body.connectionId);
|
|
@@ -39916,7 +41766,7 @@ app.post("/schedule-connections/actions/import-memory", auth3, requireIntegratio
|
|
|
39916
41766
|
return scheduleConnectionError(c, err, "Unable to import this connected-service snapshot into Memory.");
|
|
39917
41767
|
}
|
|
39918
41768
|
});
|
|
39919
|
-
app.post("/schedule-connections/actions/describe",
|
|
41769
|
+
app.post("/schedule-connections/actions/describe", auth4, requireIntegrationsTier, async (c) => {
|
|
39920
41770
|
const user = c.get("user");
|
|
39921
41771
|
const body = await c.req.json().catch(() => ({}));
|
|
39922
41772
|
const connectionId = providerConfigKeyFrom(body.connectionId);
|
|
@@ -39933,7 +41783,7 @@ app.post("/schedule-connections/actions/describe", auth3, requireIntegrationsTie
|
|
|
39933
41783
|
return scheduleConnectionError(c, err, "Unable to describe this connection tool.");
|
|
39934
41784
|
}
|
|
39935
41785
|
});
|
|
39936
|
-
app.post("/schedule-connections/actions/export",
|
|
41786
|
+
app.post("/schedule-connections/actions/export", auth4, requireIntegrationsTier, async (c) => {
|
|
39937
41787
|
const user = c.get("user");
|
|
39938
41788
|
const body = await c.req.json().catch(() => ({}));
|
|
39939
41789
|
const connectionId = providerConfigKeyFrom(body.connectionId);
|
|
@@ -40020,7 +41870,7 @@ app.post("/schedule-connections/actions/export", auth3, requireIntegrationsTier,
|
|
|
40020
41870
|
return scheduleConnectionError(c, err, "Unable to export this connected service.");
|
|
40021
41871
|
}
|
|
40022
41872
|
});
|
|
40023
|
-
app.post("/schedule-connections/actions/export-search-console-table",
|
|
41873
|
+
app.post("/schedule-connections/actions/export-search-console-table", auth4, requireIntegrationsTier, async (c) => {
|
|
40024
41874
|
const user = c.get("user");
|
|
40025
41875
|
const body = await c.req.json().catch(() => ({}));
|
|
40026
41876
|
try {
|
|
@@ -40045,7 +41895,7 @@ app.post("/schedule-connections/actions/export-search-console-table", auth3, req
|
|
|
40045
41895
|
return c.json({ ok: false, error: "Unable to export the persisted Search Console table." }, 502);
|
|
40046
41896
|
}
|
|
40047
41897
|
});
|
|
40048
|
-
app.post("/schedule-connections/actions/export-download",
|
|
41898
|
+
app.post("/schedule-connections/actions/export-download", auth4, requireIntegrationsTier, async (c) => {
|
|
40049
41899
|
const user = c.get("user");
|
|
40050
41900
|
const body = await c.req.json().catch(() => ({}));
|
|
40051
41901
|
const artifactId = typeof body.artifactId === "string" ? body.artifactId.trim() : "";
|
|
@@ -40062,7 +41912,7 @@ app.post("/schedule-connections/actions/export-download", auth3, requireIntegrat
|
|
|
40062
41912
|
return c.json({ ok: false, error: "Unable to renew this artifact download." }, 502);
|
|
40063
41913
|
}
|
|
40064
41914
|
});
|
|
40065
|
-
app.post("/schedule-connections/actions/call",
|
|
41915
|
+
app.post("/schedule-connections/actions/call", auth4, requireIntegrationsTier, async (c) => {
|
|
40066
41916
|
const user = c.get("user");
|
|
40067
41917
|
const body = await c.req.json().catch(() => ({}));
|
|
40068
41918
|
const connectionId = providerConfigKeyFrom(body.connectionId);
|
|
@@ -40102,7 +41952,7 @@ app.post("/integrations/actions/read", (c) => forwardIntegrationAlias(c, "/sched
|
|
|
40102
41952
|
app.post("/integrations/actions/describe", (c) => forwardIntegrationAlias(c, "/schedule-connections/actions/describe"));
|
|
40103
41953
|
app.post("/integrations/actions/call", (c) => forwardIntegrationAlias(c, "/schedule-connections/actions/call"));
|
|
40104
41954
|
app.post("/integrations/actions/export", (c) => forwardIntegrationAlias(c, "/schedule-connections/actions/export"));
|
|
40105
|
-
app.post("/schedule-link",
|
|
41955
|
+
app.post("/schedule-link", auth4, requirePaidSchedulingTier, async (c) => {
|
|
40106
41956
|
try {
|
|
40107
41957
|
const user = c.get("user");
|
|
40108
41958
|
const credentials = await syncScheduledActionCredentials(user);
|
|
@@ -40119,7 +41969,7 @@ app.post("/schedule-link", auth3, requirePaidSchedulingTier, async (c) => {
|
|
|
40119
41969
|
return c.json({ error: message }, 500);
|
|
40120
41970
|
}
|
|
40121
41971
|
});
|
|
40122
|
-
app.get("/schedule-actions",
|
|
41972
|
+
app.get("/schedule-actions", auth4, async (c) => {
|
|
40123
41973
|
const user = c.get("user");
|
|
40124
41974
|
const { key, error } = await getOrCreateUserMemoryKey(user);
|
|
40125
41975
|
if (!key) return c.json({ ok: false, error: error ?? "memory unavailable" }, 503);
|
|
@@ -40146,7 +41996,7 @@ app.get("/schedule-actions", auth3, async (c) => {
|
|
|
40146
41996
|
});
|
|
40147
41997
|
}
|
|
40148
41998
|
});
|
|
40149
|
-
app.post("/schedule-actions",
|
|
41999
|
+
app.post("/schedule-actions", auth4, requirePaidSchedulingTier, async (c) => {
|
|
40150
42000
|
const user = c.get("user");
|
|
40151
42001
|
const credentials = await syncScheduledActionCredentials(user);
|
|
40152
42002
|
if (!credentials.ok) {
|
|
@@ -40191,7 +42041,7 @@ app.post("/schedule-actions", auth3, requirePaidSchedulingTier, async (c) => {
|
|
|
40191
42041
|
return response;
|
|
40192
42042
|
}
|
|
40193
42043
|
});
|
|
40194
|
-
app.post("/schedule-actions/propose",
|
|
42044
|
+
app.post("/schedule-actions/propose", auth4, async (c) => {
|
|
40195
42045
|
const user = c.get("user");
|
|
40196
42046
|
const { key, error } = await getOrCreateUserMemoryKey(user);
|
|
40197
42047
|
if (!key) return c.json({ ok: false, error: error ?? "memory unavailable" }, 503);
|
|
@@ -40199,7 +42049,7 @@ app.post("/schedule-actions/propose", auth3, async (c) => {
|
|
|
40199
42049
|
const r = await memoryCall("proposeScheduledActionTool", body, key);
|
|
40200
42050
|
return c.json(r);
|
|
40201
42051
|
});
|
|
40202
|
-
app.get("/schedule-actions/:id/connections",
|
|
42052
|
+
app.get("/schedule-actions/:id/connections", auth4, async (c) => {
|
|
40203
42053
|
try {
|
|
40204
42054
|
const user = c.get("user");
|
|
40205
42055
|
const scheduleActionId = c.req.param("id");
|
|
@@ -40209,7 +42059,7 @@ app.get("/schedule-actions/:id/connections", auth3, async (c) => {
|
|
|
40209
42059
|
return scheduleConnectionError(c, err, "Unable to load scheduled action service connections.");
|
|
40210
42060
|
}
|
|
40211
42061
|
});
|
|
40212
|
-
app.put("/schedule-actions/:id/connections",
|
|
42062
|
+
app.put("/schedule-actions/:id/connections", auth4, requireIntegrationsTier, async (c) => {
|
|
40213
42063
|
const user = c.get("user");
|
|
40214
42064
|
const body = await c.req.json().catch(() => ({}));
|
|
40215
42065
|
let connections;
|
|
@@ -40236,7 +42086,7 @@ app.put("/schedule-actions/:id/connections", auth3, requireIntegrationsTier, asy
|
|
|
40236
42086
|
return scheduleConnectionError(c, err, "Unable to update scheduled action service connections.");
|
|
40237
42087
|
}
|
|
40238
42088
|
});
|
|
40239
|
-
app.get("/schedule-actions/:id/runs",
|
|
42089
|
+
app.get("/schedule-actions/:id/runs", auth4, async (c) => {
|
|
40240
42090
|
const user = c.get("user");
|
|
40241
42091
|
const { key, error } = await getOrCreateUserMemoryKey(user);
|
|
40242
42092
|
if (!key) return c.json({ ok: false, error: error ?? "memory unavailable" }, 503);
|
|
@@ -40245,7 +42095,7 @@ app.get("/schedule-actions/:id/runs", auth3, async (c) => {
|
|
|
40245
42095
|
const r = await memoryCall("listScheduledActionRunsTool", { id: c.req.param("id"), limit }, key);
|
|
40246
42096
|
return c.json(r);
|
|
40247
42097
|
});
|
|
40248
|
-
app.put("/schedule-actions/:id",
|
|
42098
|
+
app.put("/schedule-actions/:id", auth4, requirePaidSchedulingTier, async (c) => {
|
|
40249
42099
|
const user = c.get("user");
|
|
40250
42100
|
const { key, error } = await getOrCreateUserMemoryKey(user);
|
|
40251
42101
|
if (!key) return c.json({ ok: false, error: error ?? "memory unavailable" }, 503);
|
|
@@ -40260,21 +42110,21 @@ app.put("/schedule-actions/:id", auth3, requirePaidSchedulingTier, async (c) =>
|
|
|
40260
42110
|
const r = await memoryCall("updateScheduledActionTool", { id: c.req.param("id"), ...updates }, key);
|
|
40261
42111
|
return c.json(r);
|
|
40262
42112
|
});
|
|
40263
|
-
app.post("/schedule-actions/:id/pause",
|
|
42113
|
+
app.post("/schedule-actions/:id/pause", auth4, async (c) => {
|
|
40264
42114
|
const user = c.get("user");
|
|
40265
42115
|
const { key, error } = await getOrCreateUserMemoryKey(user);
|
|
40266
42116
|
if (!key) return c.json({ ok: false, error: error ?? "memory unavailable" }, 503);
|
|
40267
42117
|
const r = await memoryCall("pauseScheduledActionTool", { id: c.req.param("id") }, key);
|
|
40268
42118
|
return c.json(r);
|
|
40269
42119
|
});
|
|
40270
|
-
app.post("/schedule-actions/:id/resume",
|
|
42120
|
+
app.post("/schedule-actions/:id/resume", auth4, requirePaidSchedulingTier, async (c) => {
|
|
40271
42121
|
const user = c.get("user");
|
|
40272
42122
|
const { key, error } = await getOrCreateUserMemoryKey(user);
|
|
40273
42123
|
if (!key) return c.json({ ok: false, error: error ?? "memory unavailable" }, 503);
|
|
40274
42124
|
const r = await memoryCall("resumeScheduledActionTool", { id: c.req.param("id") }, key);
|
|
40275
42125
|
return c.json(r);
|
|
40276
42126
|
});
|
|
40277
|
-
app.delete("/schedule-actions/:id",
|
|
42127
|
+
app.delete("/schedule-actions/:id", auth4, async (c) => {
|
|
40278
42128
|
const user = c.get("user");
|
|
40279
42129
|
const { key, error } = await getOrCreateUserMemoryKey(user);
|
|
40280
42130
|
if (!key) return c.json({ ok: false, error: error ?? "memory unavailable" }, 503);
|
|
@@ -40289,7 +42139,7 @@ app.delete("/schedule-actions/:id", auth3, async (c) => {
|
|
|
40289
42139
|
return c.json({ ...r, connectionBindingCleanupPending: true });
|
|
40290
42140
|
}
|
|
40291
42141
|
});
|
|
40292
|
-
app.post("/schedule-link/revoke",
|
|
42142
|
+
app.post("/schedule-link/revoke", auth4, async (c) => {
|
|
40293
42143
|
try {
|
|
40294
42144
|
const user = c.get("user");
|
|
40295
42145
|
const { key, error } = await getOrCreateUserMemoryKey(user);
|
|
@@ -40302,7 +42152,7 @@ app.post("/schedule-link/revoke", auth3, async (c) => {
|
|
|
40302
42152
|
return c.json({ error: message }, 500);
|
|
40303
42153
|
}
|
|
40304
42154
|
});
|
|
40305
|
-
app.post("/support/submit",
|
|
42155
|
+
app.post("/support/submit", auth4, async (c) => {
|
|
40306
42156
|
try {
|
|
40307
42157
|
const user = c.get("user");
|
|
40308
42158
|
const body = await c.req.json().catch(() => ({}));
|
|
@@ -40330,7 +42180,7 @@ app.post("/support/submit", auth3, async (c) => {
|
|
|
40330
42180
|
return c.json({ error: message }, 500);
|
|
40331
42181
|
}
|
|
40332
42182
|
});
|
|
40333
|
-
app.get("/memory/usage",
|
|
42183
|
+
app.get("/memory/usage", auth4, async (c) => {
|
|
40334
42184
|
const user = c.get("user");
|
|
40335
42185
|
try {
|
|
40336
42186
|
const result = await dbUsage(resolveMemoryIdentity(user), memoryPlanForReads(user));
|
|
@@ -40383,7 +42233,7 @@ async function checkHarvestLimits(user, reuseLockId) {
|
|
|
40383
42233
|
}
|
|
40384
42234
|
return null;
|
|
40385
42235
|
}
|
|
40386
|
-
app.post("/harvest",
|
|
42236
|
+
app.post("/harvest", auth4, async (c) => {
|
|
40387
42237
|
const user = c.get("user");
|
|
40388
42238
|
const raw = await c.req.json().catch(() => ({}));
|
|
40389
42239
|
const bodyResult = HarvestBodySchema.safeParse(raw);
|
|
@@ -40466,7 +42316,7 @@ app.post("/harvest", auth3, async (c) => {
|
|
|
40466
42316
|
}
|
|
40467
42317
|
return c.json({ job_id: jobId2, status: "pending" }, 202);
|
|
40468
42318
|
});
|
|
40469
|
-
app.post("/harvest/sync",
|
|
42319
|
+
app.post("/harvest/sync", auth4, async (c) => {
|
|
40470
42320
|
const user = c.get("user");
|
|
40471
42321
|
const raw = await c.req.json().catch(() => ({}));
|
|
40472
42322
|
const bodyResult = HarvestBodySchema.safeParse(raw);
|
|
@@ -40547,17 +42397,17 @@ app.post("/harvest/sync", auth3, async (c) => {
|
|
|
40547
42397
|
return c.json({ job_id: jobId2, status: "failed", ...response, attempts: sanitizeAttempts(attempts) }, problem.httpStatus);
|
|
40548
42398
|
}
|
|
40549
42399
|
});
|
|
40550
|
-
app.get("/jobs/:id",
|
|
42400
|
+
app.get("/jobs/:id", auth4, async (c) => {
|
|
40551
42401
|
const job = await getJob(c.req.param("id"), c.get("user").id);
|
|
40552
42402
|
if (!job) return c.json({ error: "Job not found" }, 404);
|
|
40553
42403
|
const attempts = await listHarvestAttempts(job.id, c.get("user").id);
|
|
40554
42404
|
const safeResult = job.result && typeof job.result === "object" ? sanitizeHarvestResult(job.result) : job.result;
|
|
40555
42405
|
return c.json({ ...job, result: safeResult, attempts: sanitizeAttempts(attempts) });
|
|
40556
42406
|
});
|
|
40557
|
-
app.get("/jobs",
|
|
42407
|
+
app.get("/jobs", auth4, async (c) => {
|
|
40558
42408
|
return c.json(await listJobs(c.get("user").id));
|
|
40559
42409
|
});
|
|
40560
|
-
app.get("/history",
|
|
42410
|
+
app.get("/history", auth4, async (c) => {
|
|
40561
42411
|
const userId = c.get("user").id;
|
|
40562
42412
|
const [jobs, events] = await Promise.all([
|
|
40563
42413
|
listJobs(userId),
|
|
@@ -40585,7 +42435,7 @@ app.get("/history", auth3, async (c) => {
|
|
|
40585
42435
|
const rows = [...jobRows, ...eventRows].sort((a, b) => String(b.ts).localeCompare(String(a.ts)));
|
|
40586
42436
|
return c.json(rows.slice(0, 100));
|
|
40587
42437
|
});
|
|
40588
|
-
app.get("/ledger",
|
|
42438
|
+
app.get("/ledger", auth4, async (c) => {
|
|
40589
42439
|
return c.json(await getLedger(c.get("user").id, 100));
|
|
40590
42440
|
});
|
|
40591
42441
|
app.post("/admin/users", adminAuth, async (c) => {
|
|
@@ -40614,7 +42464,7 @@ app.post("/admin/backfill-signup-credits", adminAuth, async (c) => {
|
|
|
40614
42464
|
retired: true
|
|
40615
42465
|
});
|
|
40616
42466
|
});
|
|
40617
|
-
app.post("/extract-url",
|
|
42467
|
+
app.post("/extract-url", auth4, async (c) => {
|
|
40618
42468
|
const raw = await c.req.json().catch(() => ({}));
|
|
40619
42469
|
const allowLocalRequest = permitsLocalApiRequest(c.req.raw);
|
|
40620
42470
|
const bodyResult = (allowLocalRequest ? ExtractUrlLocalBodySchema : ExtractUrlBodySchema).safeParse(raw);
|
|
@@ -40750,7 +42600,7 @@ app.post("/extract-url", auth3, async (c) => {
|
|
|
40750
42600
|
await releaseConcurrencyGate(gate.lockId);
|
|
40751
42601
|
}
|
|
40752
42602
|
});
|
|
40753
|
-
app.post("/diff-page",
|
|
42603
|
+
app.post("/diff-page", auth4, async (c) => {
|
|
40754
42604
|
const raw = await c.req.json().catch(() => ({}));
|
|
40755
42605
|
const allowLocalRequest = permitsLocalApiRequest(c.req.raw);
|
|
40756
42606
|
const bodyResult = (allowLocalRequest ? DiffPageLocalBodySchema : DiffPageBodySchema).safeParse(raw);
|
|
@@ -40837,7 +42687,7 @@ app.post("/diff-page", auth3, async (c) => {
|
|
|
40837
42687
|
await releaseConcurrencyGate(gate.lockId);
|
|
40838
42688
|
}
|
|
40839
42689
|
});
|
|
40840
|
-
app.post("/archive/read",
|
|
42690
|
+
app.post("/archive/read", auth4, async (c) => {
|
|
40841
42691
|
const raw = await c.req.json().catch(() => ({}));
|
|
40842
42692
|
const bodyResult = ArchiveReadBodySchema.safeParse(raw);
|
|
40843
42693
|
if (!bodyResult.success) {
|
|
@@ -40912,7 +42762,7 @@ app.post("/archive/read", auth3, async (c) => {
|
|
|
40912
42762
|
await releaseConcurrencyGate(gate.lockId);
|
|
40913
42763
|
}
|
|
40914
42764
|
});
|
|
40915
|
-
app.post("/map-urls",
|
|
42765
|
+
app.post("/map-urls", auth4, async (c) => {
|
|
40916
42766
|
const raw = await c.req.json().catch(() => ({}));
|
|
40917
42767
|
const bodyResult = MapUrlsBodySchema.safeParse(raw);
|
|
40918
42768
|
if (!bodyResult.success) return c.json({ error: bodyResult.error.issues[0]?.message ?? "Invalid request" }, 400);
|
|
@@ -40961,7 +42811,7 @@ app.post("/map-urls", auth3, async (c) => {
|
|
|
40961
42811
|
await releaseConcurrencyGate(gate.lockId);
|
|
40962
42812
|
}
|
|
40963
42813
|
});
|
|
40964
|
-
app.post("/wayback/snapshots",
|
|
42814
|
+
app.post("/wayback/snapshots", auth4, async (c) => {
|
|
40965
42815
|
const raw = await c.req.json().catch(() => ({}));
|
|
40966
42816
|
const bodyResult = WaybackInventoryBodySchema.safeParse(raw);
|
|
40967
42817
|
if (!bodyResult.success) return c.json({ error: bodyResult.error.issues[0]?.message ?? "Invalid request" }, 400);
|
|
@@ -41031,7 +42881,7 @@ app.post("/wayback/snapshots", auth3, async (c) => {
|
|
|
41031
42881
|
await releaseConcurrencyGate(gate.lockId);
|
|
41032
42882
|
}
|
|
41033
42883
|
});
|
|
41034
|
-
app.post("/extract-site",
|
|
42884
|
+
app.post("/extract-site", auth4, async (c) => {
|
|
41035
42885
|
const raw = await c.req.json().catch(() => ({}));
|
|
41036
42886
|
const bodyResult = ExtractSiteBodySchema.safeParse(raw);
|
|
41037
42887
|
if (!bodyResult.success) return c.json({ error: bodyResult.error.issues[0]?.message ?? "Invalid request" }, 400);
|
|
@@ -41254,7 +43104,7 @@ app.post("/extract-site", auth3, async (c) => {
|
|
|
41254
43104
|
await releaseConcurrencyGate(gate.lockId);
|
|
41255
43105
|
}
|
|
41256
43106
|
});
|
|
41257
|
-
app.get("/extract-site/status/:id",
|
|
43107
|
+
app.get("/extract-site/status/:id", auth4, async (c) => {
|
|
41258
43108
|
const user = c.get("user");
|
|
41259
43109
|
const job = await getExtractJob(c.req.param("id"));
|
|
41260
43110
|
if (!job || job.userId !== user.id) return c.json({ error: "Job not found" }, 404);
|
|
@@ -41288,9 +43138,9 @@ app.post("/billing/concurrency/checkout", requireAllowedOrigin, sessionAuth, asy
|
|
|
41288
43138
|
try {
|
|
41289
43139
|
const user = c.get("sessionUser");
|
|
41290
43140
|
if (user.concurrency_stripe_sub_id) return c.json({ error: "Already subscribed \u2014 cancel existing slot first to change." }, 409);
|
|
41291
|
-
const
|
|
41292
|
-
if (!
|
|
41293
|
-
const stripeClient = new Stripe2(
|
|
43141
|
+
const secret = process.env.STRIPE_SECRET_KEY?.trim();
|
|
43142
|
+
if (!secret) return c.json({ error: "Stripe is not configured." }, 503);
|
|
43143
|
+
const stripeClient = new Stripe2(secret, { apiVersion: STRIPE_API_VERSION });
|
|
41294
43144
|
let customerId = user.stripe_customer_id;
|
|
41295
43145
|
if (!customerId) {
|
|
41296
43146
|
const customer = await stripeClient.customers.create({ email: user.email });
|
|
@@ -41352,9 +43202,9 @@ app.post("/billing/subscribe", requireAllowedOrigin, sessionAuth, async (c) => {
|
|
|
41352
43202
|
requested_plan: getSubscriptionPlanView(tier.tier)
|
|
41353
43203
|
}, 409);
|
|
41354
43204
|
}
|
|
41355
|
-
const
|
|
41356
|
-
if (!
|
|
41357
|
-
const stripeClient = new Stripe2(
|
|
43205
|
+
const secret = process.env.STRIPE_SECRET_KEY?.trim();
|
|
43206
|
+
if (!secret) return c.json({ error: "Stripe is not configured." }, 503);
|
|
43207
|
+
const stripeClient = new Stripe2(secret, { apiVersion: STRIPE_API_VERSION });
|
|
41358
43208
|
let customerId = user.stripe_customer_id;
|
|
41359
43209
|
if (!customerId) {
|
|
41360
43210
|
const customer = await stripeClient.customers.create({ email: user.email });
|
|
@@ -41415,9 +43265,9 @@ app.post("/billing/portal", requireAllowedOrigin, sessionAuth, async (c) => {
|
|
|
41415
43265
|
try {
|
|
41416
43266
|
const user = c.get("sessionUser");
|
|
41417
43267
|
if (!user.stripe_customer_id) return c.json({ error: "No billing account yet \u2014 subscribe first." }, 409);
|
|
41418
|
-
const
|
|
41419
|
-
if (!
|
|
41420
|
-
const stripeClient = new Stripe2(
|
|
43268
|
+
const secret = requireStripeSecret();
|
|
43269
|
+
if (!secret) return c.json({ error: "Stripe is not configured." }, 503);
|
|
43270
|
+
const stripeClient = new Stripe2(secret, { apiVersion: STRIPE_API_VERSION });
|
|
41421
43271
|
const session = await stripeClient.billingPortal.sessions.create({
|
|
41422
43272
|
customer: user.stripe_customer_id,
|
|
41423
43273
|
return_url: `${appOrigin()}/billing`
|
|
@@ -41429,7 +43279,7 @@ app.post("/billing/portal", requireAllowedOrigin, sessionAuth, async (c) => {
|
|
|
41429
43279
|
return c.json({ error: message }, 500);
|
|
41430
43280
|
}
|
|
41431
43281
|
});
|
|
41432
|
-
app.post("/billing/concurrency/terminal-checkout",
|
|
43282
|
+
app.post("/billing/concurrency/terminal-checkout", auth4, async (c) => {
|
|
41433
43283
|
try {
|
|
41434
43284
|
const user = c.get("user");
|
|
41435
43285
|
const upgrade = concurrencySlotBillingInfo();
|
|
@@ -41445,9 +43295,9 @@ app.post("/billing/concurrency/terminal-checkout", auth3, async (c) => {
|
|
|
41445
43295
|
}
|
|
41446
43296
|
}, 409);
|
|
41447
43297
|
}
|
|
41448
|
-
const
|
|
41449
|
-
if (!
|
|
41450
|
-
const stripeClient = new Stripe2(
|
|
43298
|
+
const secret = process.env.STRIPE_SECRET_KEY?.trim();
|
|
43299
|
+
if (!secret) return c.json({ error: "Stripe is not configured." }, 503);
|
|
43300
|
+
const stripeClient = new Stripe2(secret, { apiVersion: STRIPE_API_VERSION });
|
|
41451
43301
|
let customerId = user.stripe_customer_id;
|
|
41452
43302
|
if (!customerId) {
|
|
41453
43303
|
const customer = await stripeClient.customers.create({ email: user.email });
|
|
@@ -41483,7 +43333,7 @@ app.post("/billing/concurrency/terminal-checkout", auth3, async (c) => {
|
|
|
41483
43333
|
return c.json({ error: message }, 500);
|
|
41484
43334
|
}
|
|
41485
43335
|
});
|
|
41486
|
-
app.post("/billing/subscribe/terminal-checkout",
|
|
43336
|
+
app.post("/billing/subscribe/terminal-checkout", auth4, async (c) => {
|
|
41487
43337
|
try {
|
|
41488
43338
|
const user = c.get("user");
|
|
41489
43339
|
const raw = await c.req.json().catch(() => ({}));
|
|
@@ -41499,9 +43349,9 @@ app.post("/billing/subscribe/terminal-checkout", auth3, async (c) => {
|
|
|
41499
43349
|
requested_plan: getSubscriptionPlanView(tier.tier)
|
|
41500
43350
|
}, 409);
|
|
41501
43351
|
}
|
|
41502
|
-
const
|
|
41503
|
-
if (!
|
|
41504
|
-
const stripeClient = new Stripe2(
|
|
43352
|
+
const secret = process.env.STRIPE_SECRET_KEY?.trim();
|
|
43353
|
+
if (!secret) return c.json({ error: "Stripe is not configured." }, 503);
|
|
43354
|
+
const stripeClient = new Stripe2(secret, { apiVersion: STRIPE_API_VERSION });
|
|
41505
43355
|
let customerId = user.stripe_customer_id;
|
|
41506
43356
|
if (!customerId) {
|
|
41507
43357
|
const customer = await stripeClient.customers.create({ email: user.email });
|
|
@@ -41585,7 +43435,7 @@ app.post("/billing/concurrency/cancel", requireAllowedOrigin, sessionAuth, async
|
|
|
41585
43435
|
await setConcurrencySubId(user.id, null);
|
|
41586
43436
|
return c.json({ ok: true, extra_concurrency_slots: nextSlots, concurrency_limit: 1 + nextSlots });
|
|
41587
43437
|
});
|
|
41588
|
-
app.get("/billing/balance",
|
|
43438
|
+
app.get("/billing/balance", auth4, async (c) => {
|
|
41589
43439
|
const user = c.get("user");
|
|
41590
43440
|
const balanceMc = user.balance_mc;
|
|
41591
43441
|
const ledger = await getLedger(user.id, 20);
|
|
@@ -41597,15 +43447,15 @@ app.get("/billing/balance", auth3, async (c) => {
|
|
|
41597
43447
|
ledger
|
|
41598
43448
|
});
|
|
41599
43449
|
});
|
|
41600
|
-
app.get("/billing/summary",
|
|
41601
|
-
app.get("/billing/connected-usage/history",
|
|
43450
|
+
app.get("/billing/summary", auth4, billingSummaryHandler);
|
|
43451
|
+
app.get("/billing/connected-usage/history", auth4, async (c) => {
|
|
41602
43452
|
return c.json({
|
|
41603
43453
|
ok: true,
|
|
41604
43454
|
ratePolicy: CONNECTED_USAGE_RATE_POLICY,
|
|
41605
43455
|
receipts: await listConnectedUsageHistory(c.get("user").id, 100)
|
|
41606
43456
|
});
|
|
41607
43457
|
});
|
|
41608
|
-
app.post("/billing/credits",
|
|
43458
|
+
app.post("/billing/credits", auth4, async (c) => {
|
|
41609
43459
|
const user = c.get("user");
|
|
41610
43460
|
const balanceMc = user.balance_mc;
|
|
41611
43461
|
const body = await c.req.json().catch(() => ({}));
|
|
@@ -41637,14 +43487,14 @@ app.post("/billing/credits", auth3, async (c) => {
|
|
|
41637
43487
|
});
|
|
41638
43488
|
});
|
|
41639
43489
|
app.get("/cron/tick", async (c) => {
|
|
41640
|
-
const
|
|
41641
|
-
if (!process.env.CRON_SECRET ||
|
|
43490
|
+
const secret = c.req.header("authorization");
|
|
43491
|
+
if (!process.env.CRON_SECRET || secret !== `Bearer ${process.env.CRON_SECRET}`) {
|
|
41642
43492
|
return c.json({ error: "Unauthorized" }, 401);
|
|
41643
43493
|
}
|
|
41644
|
-
const { drainQueue } = await import("./worker-
|
|
43494
|
+
const { drainQueue } = await import("./worker-4MHZS3OB.js");
|
|
41645
43495
|
const budget = { maxJobs: 10, deadlineMs: Date.now() + 28e4 };
|
|
41646
43496
|
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([
|
|
43497
|
+
const [results, sweepResult, reapResult, expiredResult, blobCleanup, connectedDataArtifactCleanup, connectedAccountBilling, directoryWorkflowDispatch, directoryWorkflowReconciliation, directoryArtifactCleanup, siteExtractArtifactCleanup, siteExtractRedispatch, siteExtractReconciliation, paaHarvestRedispatch, paaHarvestReconciliation, localSourcebookRedispatch, analyticsRollup, analyticsFormDelivery] = await Promise.all([
|
|
41648
43498
|
drainQueue(budget),
|
|
41649
43499
|
runMonthlyRefreshSweep(),
|
|
41650
43500
|
reapIdleBrowserSessions(120).catch(() => ({ reaped: 0 })),
|
|
@@ -41660,7 +43510,9 @@ app.get("/cron/tick", async (c) => {
|
|
|
41660
43510
|
reconcileSiteExtractSettlements(25).catch(() => ({ checked: 0, settled: 0, failed: 0 })),
|
|
41661
43511
|
redispatchPendingPaaHarvests(25).catch(() => ({ checked: 0, dispatched: 0, failed: 0 })),
|
|
41662
43512
|
reconcilePaaHarvestSettlements(25).catch(() => ({ checked: 0, staleFailed: 0, settled: 0, failed: 0 })),
|
|
41663
|
-
redispatchQueuedLocalSourcebookAcquisitions(25).catch(() => ({ checked: 0, dispatched: 0, failed: 0 }))
|
|
43513
|
+
redispatchQueuedLocalSourcebookAcquisitions(25).catch(() => ({ checked: 0, dispatched: 0, failed: 0 })),
|
|
43514
|
+
refreshAnalyticsDailyRollupsIfDue().catch(() => ({ status: "failed", rows: 0 })),
|
|
43515
|
+
deliverPendingAnalyticsForms(10).catch(() => ({ claimed: 0, delivered: 0, deferred: 0 }))
|
|
41664
43516
|
]);
|
|
41665
43517
|
return c.json({
|
|
41666
43518
|
drained: results.length,
|
|
@@ -41680,12 +43532,14 @@ app.get("/cron/tick", async (c) => {
|
|
|
41680
43532
|
paaHarvestRedispatch,
|
|
41681
43533
|
paaHarvestReconciliation,
|
|
41682
43534
|
localSourcebookRedispatch,
|
|
43535
|
+
analyticsRollup,
|
|
43536
|
+
analyticsFormDelivery,
|
|
41683
43537
|
workflowDispatch: workflowDispatchResult
|
|
41684
43538
|
});
|
|
41685
43539
|
});
|
|
41686
43540
|
app.post("/api/internal/extract-refinalize/:id", async (c) => {
|
|
41687
|
-
const
|
|
41688
|
-
if (!process.env.CRON_SECRET ||
|
|
43541
|
+
const secret = c.req.header("authorization");
|
|
43542
|
+
if (!process.env.CRON_SECRET || secret !== `Bearer ${process.env.CRON_SECRET}`) {
|
|
41689
43543
|
return c.json({ error: "Unauthorized" }, 401);
|
|
41690
43544
|
}
|
|
41691
43545
|
const jobId2 = c.req.param("id");
|
|
@@ -41696,8 +43550,8 @@ app.post("/api/internal/extract-refinalize/:id", async (c) => {
|
|
|
41696
43550
|
finishExtractJob: finishExtractJob2,
|
|
41697
43551
|
terminalExtractJobStatus: terminalExtractJobStatus2,
|
|
41698
43552
|
extractJobLimitInfo: extractJobLimitInfo2
|
|
41699
|
-
} = await import("./site-extract-repository-
|
|
41700
|
-
const { assembleExtractArtifacts } = await import("./extract-bundle-
|
|
43553
|
+
} = await import("./site-extract-repository-STNOI5OH.js");
|
|
43554
|
+
const { assembleExtractArtifacts } = await import("./extract-bundle-2HONVC5T.js");
|
|
41701
43555
|
const existing = await getExtractJob2(jobId2);
|
|
41702
43556
|
if (!existing) return c.json({ error: "job not found" }, 404);
|
|
41703
43557
|
if (existing.status !== "failed") {
|
|
@@ -41724,7 +43578,7 @@ app.post("/api/internal/extract-refinalize/:id", async (c) => {
|
|
|
41724
43578
|
if (!updated) return c.json({ error: "job state changed during re-finalization" }, 409);
|
|
41725
43579
|
let settlement = "already_settled_or_refunded";
|
|
41726
43580
|
if (job.billedMc == null && job.userId != null) {
|
|
41727
|
-
const { settleExtractJob: settleExtractJob3, countSuccessfulPages: countSuccessfulPages2 } = await import("./site-extract-repository-
|
|
43581
|
+
const { settleExtractJob: settleExtractJob3, countSuccessfulPages: countSuccessfulPages2 } = await import("./site-extract-repository-STNOI5OH.js");
|
|
41728
43582
|
const heldMc = Number(job.options.heldMc ?? 0);
|
|
41729
43583
|
const successful = await countSuccessfulPages2(jobId2);
|
|
41730
43584
|
const usedMc = Math.min(successful * MC_COSTS.page_scrape, heldMc);
|
|
@@ -41775,6 +43629,7 @@ app.route("/resend", resendInboundApp);
|
|
|
41775
43629
|
app.route("/workshops", workshopRegistrationApp);
|
|
41776
43630
|
app.route("/editorial-reading-room", editorialReadingRoomApp);
|
|
41777
43631
|
app.route("/commons", commonsApp);
|
|
43632
|
+
app.route("/analytics", analyticsApp);
|
|
41778
43633
|
app.route("/", scheduledResultApp);
|
|
41779
43634
|
app.route("/public", publicScheduledResultApp);
|
|
41780
43635
|
app.route("/api/internal/scheduled-artifacts", scheduledArtifactInternalApp);
|
|
@@ -41908,4 +43763,4 @@ app.get("/blog/:slug/", (c) => {
|
|
|
41908
43763
|
export {
|
|
41909
43764
|
app
|
|
41910
43765
|
};
|
|
41911
|
-
//# sourceMappingURL=server-
|
|
43766
|
+
//# sourceMappingURL=server-EVTCL3KB.js.map
|