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