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