@ainyc/canonry 4.54.0 → 4.55.1
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/assets/assets/{BacklinksPage-BXFT4pLI.js → BacklinksPage-DVmaM864.js} +1 -1
- package/assets/assets/{ProjectPage-DAtd9Vay.js → ProjectPage-DtL3LFne.js} +4 -4
- package/assets/assets/{RunRow-38dDceGl.js → RunRow-BRqiLxj2.js} +1 -1
- package/assets/assets/{RunsPage-AJnFLtaE.js → RunsPage-UxZ93-cg.js} +1 -1
- package/assets/assets/{SettingsPage-FT9ZAvFH.js → SettingsPage-Cr5_EGbk.js} +1 -1
- package/assets/assets/TrafficPage-CUC_lfTe.js +1 -0
- package/assets/assets/TrafficSourceDetailPage-DARPL2TU.js +1 -0
- package/assets/assets/extract-error-message-DD5MibWI.js +1 -0
- package/assets/assets/{index-DLPKqyhx.js → index-nnF1LnyK.js} +62 -62
- package/assets/assets/{server-traffic-GqiQYm6x.js → server-traffic-DjRISEZ-.js} +1 -1
- package/assets/assets/{trash-2-BwPzJ8NI.js → trash-2-CJ5M--Le.js} +1 -1
- package/assets/index.html +1 -1
- package/dist/{chunk-CRO6Q25G.js → chunk-2OI7HFAB.js} +315 -85
- package/dist/{chunk-VZPDBHBW.js → chunk-OFY3Z2F7.js} +8 -4
- package/dist/{chunk-JHAHNKSN.js → chunk-UTM3FPAJ.js} +80 -3
- package/dist/{chunk-J7MX3YOH.js → chunk-ZY3EDW3S.js} +1 -1
- package/dist/cli.js +6 -6
- package/dist/index.d.ts +13 -0
- package/dist/index.js +4 -4
- package/dist/{intelligence-service-OCREQUCQ.js → intelligence-service-NKAEHHJ5.js} +2 -2
- package/dist/mcp.js +2 -2
- package/package.json +14 -13
- package/assets/assets/TrafficPage-B4A3oO8M.js +0 -1
- package/assets/assets/TrafficSourceDetailPage-8NYU1TA6.js +0 -1
- package/assets/assets/arrow-left-DgI0X1Q1.js +0 -1
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
loadConfig,
|
|
7
7
|
loadConfigRaw,
|
|
8
8
|
saveConfigPatch
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-ZY3EDW3S.js";
|
|
10
10
|
import {
|
|
11
11
|
DEFAULT_RUN_HISTORY_LIMIT,
|
|
12
12
|
IntelligenceService,
|
|
@@ -84,7 +84,7 @@ import {
|
|
|
84
84
|
smoothedRunDelta,
|
|
85
85
|
trafficSources,
|
|
86
86
|
usageCounters
|
|
87
|
-
} from "./chunk-
|
|
87
|
+
} from "./chunk-UTM3FPAJ.js";
|
|
88
88
|
import {
|
|
89
89
|
AGENT_MEMORY_VALUE_MAX_BYTES,
|
|
90
90
|
AGENT_PROVIDER_IDS,
|
|
@@ -185,6 +185,7 @@ import {
|
|
|
185
185
|
emptyCitationVisibility,
|
|
186
186
|
extractAnswerMentions,
|
|
187
187
|
findDuplicateLocationLabels,
|
|
188
|
+
forbidden,
|
|
188
189
|
formatDate,
|
|
189
190
|
formatDateRange,
|
|
190
191
|
formatDeltaCopy,
|
|
@@ -285,7 +286,7 @@ import {
|
|
|
285
286
|
wordpressSchemaDeployResultDtoSchema,
|
|
286
287
|
wordpressSchemaStatusResultDtoSchema,
|
|
287
288
|
wordpressStatusDtoSchema
|
|
288
|
-
} from "./chunk-
|
|
289
|
+
} from "./chunk-OFY3Z2F7.js";
|
|
289
290
|
|
|
290
291
|
// src/telemetry.ts
|
|
291
292
|
import crypto from "crypto";
|
|
@@ -586,6 +587,12 @@ import fs8 from "fs";
|
|
|
586
587
|
// ../api-routes/src/auth.ts
|
|
587
588
|
import crypto2 from "crypto";
|
|
588
589
|
import { eq } from "drizzle-orm";
|
|
590
|
+
function requireScope(request, scope) {
|
|
591
|
+
const key = request.apiKey;
|
|
592
|
+
if (!key) return;
|
|
593
|
+
if (key.scopes.includes("*") || key.scopes.includes(scope)) return;
|
|
594
|
+
throw forbidden(`This action requires the "${scope}" scope on your API key.`);
|
|
595
|
+
}
|
|
589
596
|
function hashKey(key) {
|
|
590
597
|
return crypto2.createHash("sha256").update(key).digest("hex");
|
|
591
598
|
}
|
|
@@ -645,6 +652,8 @@ async function authPlugin(app, opts = {}) {
|
|
|
645
652
|
throw authRequired();
|
|
646
653
|
}
|
|
647
654
|
app.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq(apiKeys.id, key.id)).run();
|
|
655
|
+
const scopes = Array.isArray(key.scopes) ? key.scopes : [];
|
|
656
|
+
request.apiKey = { id: key.id, name: key.name, scopes };
|
|
648
657
|
});
|
|
649
658
|
}
|
|
650
659
|
|
|
@@ -1653,9 +1662,9 @@ async function runRoutes(app, opts) {
|
|
|
1653
1662
|
});
|
|
1654
1663
|
app.get("/projects/:name/runs/latest", async (request, reply) => {
|
|
1655
1664
|
const project = resolveProject(app.db, request.params.name);
|
|
1656
|
-
const countRow = app.db.select({ count: sql4`count(*)` }).from(runs).where(eq7(runs.projectId, project.id)).get();
|
|
1665
|
+
const countRow = app.db.select({ count: sql4`count(*)` }).from(runs).where(and2(eq7(runs.projectId, project.id), notProbeRun())).get();
|
|
1657
1666
|
const totalRuns = countRow?.count ?? 0;
|
|
1658
|
-
const latestRun = app.db.select().from(runs).where(eq7(runs.projectId, project.id)).orderBy(desc(runs.createdAt), desc(runs.id)).limit(1).get();
|
|
1667
|
+
const latestRun = app.db.select().from(runs).where(and2(eq7(runs.projectId, project.id), notProbeRun())).orderBy(desc(runs.createdAt), desc(runs.id)).limit(1).get();
|
|
1659
1668
|
if (!latestRun) {
|
|
1660
1669
|
return reply.send({ totalRuns: 0, run: null });
|
|
1661
1670
|
}
|
|
@@ -3816,6 +3825,14 @@ var COLORS = {
|
|
|
3816
3825
|
function escapeHtml(value) {
|
|
3817
3826
|
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
3818
3827
|
}
|
|
3828
|
+
function safeHref(value) {
|
|
3829
|
+
const trimmed = (value ?? "").trim();
|
|
3830
|
+
if (!trimmed) return "#";
|
|
3831
|
+
if (trimmed.startsWith("/")) return escapeHtml(trimmed);
|
|
3832
|
+
if (/^https?:\/\//i.test(trimmed)) return escapeHtml(trimmed);
|
|
3833
|
+
if (/^mailto:/i.test(trimmed)) return escapeHtml(trimmed);
|
|
3834
|
+
return "#";
|
|
3835
|
+
}
|
|
3819
3836
|
function summarizeQueryParams(params) {
|
|
3820
3837
|
const keys = Array.from(params.keys());
|
|
3821
3838
|
const total = keys.length;
|
|
@@ -5075,7 +5092,7 @@ function renderCompetitorLandscape(report) {
|
|
|
5075
5092
|
const mentionCount = mention?.mentionCount ?? 0;
|
|
5076
5093
|
const mentionTotal = mention?.totalCount ?? mentionLandscape.totalAnswerSnapshots;
|
|
5077
5094
|
const pagesDisclosure = c.theirCitedPages.length > 0 ? `<details class="cited-pages"><summary>${c.theirCitedPages.length} cited URL${c.theirCitedPages.length > 1 ? "s" : ""}</summary>
|
|
5078
|
-
<ul>${c.theirCitedPages.map((p) => `<li><a href="${
|
|
5095
|
+
<ul>${c.theirCitedPages.map((p) => `<li><a href="${safeHref(p.url)}" target="_blank" rel="noopener noreferrer">${escapeHtml(p.url)}</a> <span class="cited-for">${escapeHtml(p.citedFor.join(", "))}</span></li>`).join("")}</ul>
|
|
5079
5096
|
</details>` : "";
|
|
5080
5097
|
return `<tr>
|
|
5081
5098
|
<td>${escapeHtml(c.domain)}</td>
|
|
@@ -5490,7 +5507,7 @@ function renderServerActivity(report, audience) {
|
|
|
5490
5507
|
<thead><tr><th>AI tool</th><th class="numeric">Bot requests (7d)</th><th class="numeric">User fetches (7d)</th><th class="numeric">Referral sessions</th></tr></thead>
|
|
5491
5508
|
<tbody>${clientOperatorRows}</tbody>
|
|
5492
5509
|
</table>
|
|
5493
|
-
<p class="meta">Bot requests are bulk crawl (GPTBot, PerplexityBot, \u2026). User fetches are on-demand reads triggered by real users inside an AI surface (ChatGPT-User, Perplexity-User, \u2026). Verified
|
|
5510
|
+
<p class="meta">Bot requests are bulk crawl (GPTBot, PerplexityBot, \u2026). User fetches are on-demand reads triggered by real users inside an AI surface (ChatGPT-User, Perplexity-User, \u2026). Verified means the request came from an IP the operator publishes as its own; unverified means the user-agent matched but the IP is not in a published range. User-fetch totals count both, since many genuine user fetches come from outside any published range.</p>
|
|
5494
5511
|
</div>` : ""}`
|
|
5495
5512
|
);
|
|
5496
5513
|
}
|
|
@@ -5556,7 +5573,7 @@ function renderServerActivity(report, audience) {
|
|
|
5556
5573
|
</div>
|
|
5557
5574
|
${trendChart}
|
|
5558
5575
|
${operatorRows ? `<div class="chart-card"><h3>Per AI operator</h3>
|
|
5559
|
-
<p class="meta">Verified means
|
|
5576
|
+
<p class="meta">Verified means the request's source IP falls inside the operator's published range. Unverified bots claim the user-agent but the IP is not in a published range, so it could be the real bot or an imitator. User fetches are on-demand reads from an AI surface on behalf of a real user (ChatGPT-User, Perplexity-User, \u2026), disjoint from bulk crawl and counted whether or not the IP can be verified.</p>
|
|
5560
5577
|
<table class="report-table">
|
|
5561
5578
|
<thead><tr><th>Operator</th><th class="numeric">Verified hits</th><th class="numeric">Unverified</th><th class="numeric">User fetches</th><th class="numeric">Referral sessions</th><th class="numeric">7d delta</th></tr></thead>
|
|
5562
5579
|
<tbody>${operatorRows}</tbody>
|
|
@@ -5707,8 +5724,8 @@ function renderOpportunities(report) {
|
|
|
5707
5724
|
</article>`).join("")}
|
|
5708
5725
|
</div>`;
|
|
5709
5726
|
const rows = opps.slice(0, 10).map((o) => {
|
|
5710
|
-
const ourPage = o.ourBestPage ? `<a href="${
|
|
5711
|
-
const winning = o.winningCompetitor ? `<a href="${
|
|
5727
|
+
const ourPage = o.ourBestPage ? `<a href="${safeHref(absolutizeProjectUrl(o.ourBestPage.url, canonical))}" target="_blank" rel="noopener noreferrer">${escapeHtml(o.ourBestPage.url)}</a>` : '<span class="cell-not-cited">No page yet</span>';
|
|
5728
|
+
const winning = o.winningCompetitor ? `<a href="${safeHref(o.winningCompetitor.url)}" target="_blank" rel="noopener noreferrer">${escapeHtml(o.winningCompetitor.domain)}</a>` : '<span class="cell-not-cited">\u2014</span>';
|
|
5712
5729
|
const drivers = o.drivers.length > 0 ? `<ul class="driver-list">${o.drivers.map((d) => `<li>${escapeHtml(d)}</li>`).join("")}</ul>` : '<span class="cell-not-cited">No driver signal yet</span>';
|
|
5713
5730
|
return `<tr>
|
|
5714
5731
|
<td>${escapeHtml(o.query)}</td>
|
|
@@ -6026,10 +6043,12 @@ function renderReportHtml(report, opts = {}) {
|
|
|
6026
6043
|
renderRecommendedNextSteps(report)
|
|
6027
6044
|
].join("\n");
|
|
6028
6045
|
const json = escapeJsonForScript(JSON.stringify(report));
|
|
6046
|
+
const csp = "default-src 'none'; style-src 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data: https:; connect-src 'none'; script-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'";
|
|
6029
6047
|
return `<!DOCTYPE html>
|
|
6030
6048
|
<html lang="en">
|
|
6031
6049
|
<head>
|
|
6032
6050
|
<meta charset="utf-8" />
|
|
6051
|
+
<meta http-equiv="Content-Security-Policy" content="${csp}" />
|
|
6033
6052
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6034
6053
|
<title>${escapeHtml(title)}</title>
|
|
6035
6054
|
<style>${STYLE}</style>
|
|
@@ -11392,7 +11411,7 @@ var routeCatalog = [
|
|
|
11392
11411
|
method: "post",
|
|
11393
11412
|
path: "/api/v1/projects/{name}/traffic/connect/vercel",
|
|
11394
11413
|
summary: "Connect a Vercel traffic source",
|
|
11395
|
-
description: "Probes Vercel's internal `request-logs` endpoint with the supplied
|
|
11414
|
+
description: "Probes Vercel's internal `request-logs` endpoint with the supplied personal access token (single page, 60-minute window) before persisting. On success, stores the token in `~/.canonry/config.yaml` and creates / updates the project's active Vercel `traffic_sources` row. A probe failure (bad token, wrong project / team id, unreachable host) surfaces as 502 with the upstream status in the message so the caller learns about it up front instead of at the first sync. The project id, team id, and environment are stored as non-secret config on the row; only the personal access token lives in the credential file.",
|
|
11396
11415
|
tags: ["traffic"],
|
|
11397
11416
|
parameters: [nameParameter],
|
|
11398
11417
|
requestBody: {
|
|
@@ -11404,8 +11423,8 @@ var routeCatalog = [
|
|
|
11404
11423
|
required: ["projectId", "teamId", "token"],
|
|
11405
11424
|
properties: {
|
|
11406
11425
|
projectId: { ...stringSchema, description: "Vercel project id (e.g. `prj_...`) \u2014 from the Vercel dashboard or `.vercel/project.json`." },
|
|
11407
|
-
teamId: { ...stringSchema, description: "Vercel team
|
|
11408
|
-
token: { ...stringSchema, description: "Vercel
|
|
11426
|
+
teamId: { ...stringSchema, description: "Vercel team or account id: the org that owns the project (`orgId` in `.vercel/project.json`)." },
|
|
11427
|
+
token: { ...stringSchema, description: "Vercel personal access token. Stored in `~/.canonry/config.yaml`, never the DB or response." },
|
|
11409
11428
|
environment: { type: "string", enum: ["production", "preview"], description: "Which deployment environment's request logs to pull. Default: `production`." },
|
|
11410
11429
|
displayName: stringSchema
|
|
11411
11430
|
}
|
|
@@ -11454,7 +11473,7 @@ var routeCatalog = [
|
|
|
11454
11473
|
method: "post",
|
|
11455
11474
|
path: "/api/v1/projects/{name}/traffic/sources/{id}/backfill",
|
|
11456
11475
|
summary: "Reclassify historical traffic-source logs",
|
|
11457
|
-
description: 'Async one-shot backfill: pulls the last `days` of events (clamped server-side to the upstream retention ceiling \u2014 30d for Cloud Logging `_Default`; the WordPress plugin honours the same window via `since`/`until` query params), classifies them with the current rules, and replaces the hourly rollup buckets + sample slice in the window inside one transaction. Returns immediately with `{ runId, status: "running" }`; poll `GET /runs/{id}` for completion. lastSyncedAt only advances forward, so a backfill never undoes incremental sync progress that ran ahead of it. Supported source types: `cloud-run`, `wordpress`.',
|
|
11476
|
+
description: 'Async one-shot backfill: pulls the last `days` of events (clamped server-side to the upstream retention ceiling \u2014 30d for Cloud Logging `_Default`; the WordPress plugin honours the same window via `since`/`until` query params), classifies them with the current rules, and replaces the hourly rollup buckets + sample slice in the window inside one transaction. Returns immediately with `{ runId, status: "running" }`; poll `GET /runs/{id}` for completion. lastSyncedAt only advances forward, so a backfill never undoes incremental sync progress that ran ahead of it. Supported source types: `cloud-run`, `wordpress`, `vercel`.',
|
|
11458
11477
|
tags: ["traffic"],
|
|
11459
11478
|
parameters: [
|
|
11460
11479
|
nameParameter,
|
|
@@ -11885,6 +11904,7 @@ function buildOperationId(method, path16) {
|
|
|
11885
11904
|
}
|
|
11886
11905
|
|
|
11887
11906
|
// ../api-routes/src/settings.ts
|
|
11907
|
+
var SETTINGS_WRITE_SCOPE = "settings.write";
|
|
11888
11908
|
async function settingsRoutes(app, opts) {
|
|
11889
11909
|
app.get("/settings", async () => ({
|
|
11890
11910
|
providers: opts.providerSummary ?? [],
|
|
@@ -11892,6 +11912,7 @@ async function settingsRoutes(app, opts) {
|
|
|
11892
11912
|
bing: opts.bing ?? { configured: false }
|
|
11893
11913
|
}));
|
|
11894
11914
|
app.put("/settings/providers/:name", async (request) => {
|
|
11915
|
+
requireScope(request, SETTINGS_WRITE_SCOPE);
|
|
11895
11916
|
const { apiKey, baseUrl, model, quota } = request.body ?? {};
|
|
11896
11917
|
const name = request.params.name;
|
|
11897
11918
|
const adapters = opts.providerAdapters ?? [];
|
|
@@ -11950,6 +11971,7 @@ async function settingsRoutes(app, opts) {
|
|
|
11950
11971
|
return result;
|
|
11951
11972
|
});
|
|
11952
11973
|
app.put("/settings/google", async (request) => {
|
|
11974
|
+
requireScope(request, SETTINGS_WRITE_SCOPE);
|
|
11953
11975
|
const { clientId, clientSecret } = request.body ?? {};
|
|
11954
11976
|
if (!clientId || typeof clientId !== "string" || !clientSecret || typeof clientSecret !== "string") {
|
|
11955
11977
|
throw validationError("clientId and clientSecret are required");
|
|
@@ -11964,6 +11986,7 @@ async function settingsRoutes(app, opts) {
|
|
|
11964
11986
|
return result;
|
|
11965
11987
|
});
|
|
11966
11988
|
app.put("/settings/bing", async (request) => {
|
|
11989
|
+
requireScope(request, SETTINGS_WRITE_SCOPE);
|
|
11967
11990
|
const { apiKey } = request.body ?? {};
|
|
11968
11991
|
if (!apiKey || typeof apiKey !== "string") {
|
|
11969
11992
|
throw validationError("apiKey is required");
|
|
@@ -13482,7 +13505,14 @@ async function googleRoutes(app, opts) {
|
|
|
13482
13505
|
}
|
|
13483
13506
|
const scopes = type === "gsc" ? [GSC_SCOPE, INDEXING_SCOPE] : [GA4_SCOPE];
|
|
13484
13507
|
const stateEncoded = buildSignedState(
|
|
13485
|
-
{
|
|
13508
|
+
{
|
|
13509
|
+
projectId: project.id,
|
|
13510
|
+
projectName: project.name,
|
|
13511
|
+
domain: project.canonicalDomain,
|
|
13512
|
+
type,
|
|
13513
|
+
propertyId,
|
|
13514
|
+
redirectUri
|
|
13515
|
+
},
|
|
13486
13516
|
stateSecret
|
|
13487
13517
|
);
|
|
13488
13518
|
const authUrl = getAuthUrl(googleClientId, redirectUri, scopes, stateEncoded);
|
|
@@ -13529,7 +13559,19 @@ async function googleRoutes(app, opts) {
|
|
|
13529
13559
|
if (!stateData) {
|
|
13530
13560
|
return reply.status(400).send("Invalid or tampered state parameter");
|
|
13531
13561
|
}
|
|
13532
|
-
const { domain, type, propertyId, redirectUri } = stateData;
|
|
13562
|
+
const { domain, type, propertyId, redirectUri, projectId, projectName } = stateData;
|
|
13563
|
+
if (!projectId) {
|
|
13564
|
+
return reply.status(400).send("Stale OAuth state \u2014 restart the connect flow.");
|
|
13565
|
+
}
|
|
13566
|
+
const project = app.db.select().from(projects).where(eq19(projects.id, projectId)).get();
|
|
13567
|
+
if (!project) {
|
|
13568
|
+
return reply.status(400).send("Project no longer exists. Restart the connect flow.");
|
|
13569
|
+
}
|
|
13570
|
+
if (project.canonicalDomain.toLowerCase() !== domain.toLowerCase()) {
|
|
13571
|
+
return reply.status(400).send(
|
|
13572
|
+
`Project "${projectName ?? project.name}" canonical domain changed since this OAuth flow started. Expected "${domain}", got "${project.canonicalDomain}". Restart the connect flow.`
|
|
13573
|
+
);
|
|
13574
|
+
}
|
|
13533
13575
|
let tokens;
|
|
13534
13576
|
try {
|
|
13535
13577
|
tokens = await exchangeCode(googleClientId, googleClientSecret, code, redirectUri);
|
|
@@ -13550,6 +13592,11 @@ async function googleRoutes(app, opts) {
|
|
|
13550
13592
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
13551
13593
|
const expiresAt = new Date(Date.now() + tokens.expires_in * 1e3).toISOString();
|
|
13552
13594
|
const existing = store.getConnection(domain, type);
|
|
13595
|
+
if (existing && existing.createdByProjectId && existing.createdByProjectId !== projectId) {
|
|
13596
|
+
return reply.status(403).send(
|
|
13597
|
+
`This domain already has a Google ${String(type).toUpperCase()} connection owned by another project. Disconnect it from that project first (DELETE /api/v1/projects/<owner>/google/connections/${escapeHtml2(String(type))}) before re-connecting from "${escapeHtml2(projectName ?? "")}".`
|
|
13598
|
+
);
|
|
13599
|
+
}
|
|
13553
13600
|
store.upsertConnection({
|
|
13554
13601
|
domain,
|
|
13555
13602
|
connectionType: type,
|
|
@@ -13558,6 +13605,9 @@ async function googleRoutes(app, opts) {
|
|
|
13558
13605
|
refreshToken: tokens.refresh_token ?? existing?.refreshToken ?? null,
|
|
13559
13606
|
tokenExpiresAt: expiresAt,
|
|
13560
13607
|
scopes: tokens.scope?.split(" ") ?? [],
|
|
13608
|
+
// Stamp ownership on first write; subsequent same-project re-connects
|
|
13609
|
+
// preserve it.
|
|
13610
|
+
createdByProjectId: existing?.createdByProjectId ?? projectId,
|
|
13561
13611
|
createdAt: existing?.createdAt ?? now,
|
|
13562
13612
|
updatedAt: now
|
|
13563
13613
|
});
|
|
@@ -13586,16 +13636,26 @@ async function googleRoutes(app, opts) {
|
|
|
13586
13636
|
app.delete("/projects/:name/google/connections/:type", async (request, reply) => {
|
|
13587
13637
|
const store = requireConnectionStore();
|
|
13588
13638
|
const project = resolveProject(app.db, request.params.name);
|
|
13589
|
-
const
|
|
13639
|
+
const type = request.params.type;
|
|
13640
|
+
const existing = store.getConnection(project.canonicalDomain, type);
|
|
13641
|
+
if (!existing) {
|
|
13642
|
+
throw notFound("Google connection", type);
|
|
13643
|
+
}
|
|
13644
|
+
if (existing.createdByProjectId && existing.createdByProjectId !== project.id) {
|
|
13645
|
+
throw validationError(
|
|
13646
|
+
`This Google ${type.toUpperCase()} connection is owned by a different project. Disconnect from the owning project instead.`
|
|
13647
|
+
);
|
|
13648
|
+
}
|
|
13649
|
+
const deleted = store.deleteConnection(project.canonicalDomain, type);
|
|
13590
13650
|
if (!deleted) {
|
|
13591
|
-
throw notFound("Google connection",
|
|
13651
|
+
throw notFound("Google connection", type);
|
|
13592
13652
|
}
|
|
13593
13653
|
writeAuditLog(app.db, {
|
|
13594
13654
|
projectId: project.id,
|
|
13595
13655
|
actor: "api",
|
|
13596
13656
|
action: "google.disconnected",
|
|
13597
13657
|
entityType: "google_connection",
|
|
13598
|
-
entityId:
|
|
13658
|
+
entityId: type
|
|
13599
13659
|
});
|
|
13600
13660
|
return reply.status(204).send();
|
|
13601
13661
|
});
|
|
@@ -14342,10 +14402,16 @@ async function bingRoutes(app, opts) {
|
|
|
14342
14402
|
}
|
|
14343
14403
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
14344
14404
|
const existing = store.getConnection(project.canonicalDomain);
|
|
14405
|
+
if (existing && existing.createdByProjectId && existing.createdByProjectId !== project.id) {
|
|
14406
|
+
throw validationError(
|
|
14407
|
+
`This domain already has a Bing connection owned by another project. Disconnect it from that project first before re-connecting from "${project.name}".`
|
|
14408
|
+
);
|
|
14409
|
+
}
|
|
14345
14410
|
store.upsertConnection({
|
|
14346
14411
|
domain: project.canonicalDomain,
|
|
14347
14412
|
apiKey,
|
|
14348
14413
|
siteUrl: existing?.siteUrl ?? null,
|
|
14414
|
+
createdByProjectId: existing?.createdByProjectId ?? project.id,
|
|
14349
14415
|
createdAt: existing?.createdAt ?? now,
|
|
14350
14416
|
updatedAt: now
|
|
14351
14417
|
});
|
|
@@ -14366,6 +14432,15 @@ async function bingRoutes(app, opts) {
|
|
|
14366
14432
|
app.delete("/projects/:name/bing/disconnect", async (request, reply) => {
|
|
14367
14433
|
const store = requireConnectionStore();
|
|
14368
14434
|
const project = resolveProject(app.db, request.params.name);
|
|
14435
|
+
const existing = store.getConnection(project.canonicalDomain);
|
|
14436
|
+
if (!existing) {
|
|
14437
|
+
throw notFound("Bing connection", project.canonicalDomain);
|
|
14438
|
+
}
|
|
14439
|
+
if (existing.createdByProjectId && existing.createdByProjectId !== project.id) {
|
|
14440
|
+
throw validationError(
|
|
14441
|
+
`This Bing connection is owned by a different project. Disconnect from the owning project instead.`
|
|
14442
|
+
);
|
|
14443
|
+
}
|
|
14369
14444
|
const deleted = store.deleteConnection(project.canonicalDomain);
|
|
14370
14445
|
if (!deleted) {
|
|
14371
14446
|
throw notFound("Bing connection", project.canonicalDomain);
|
|
@@ -14775,6 +14850,7 @@ async function cdpRoutes(app, opts) {
|
|
|
14775
14850
|
return reply.type("image/png").send(stream);
|
|
14776
14851
|
});
|
|
14777
14852
|
app.put("/settings/cdp", async (request, reply) => {
|
|
14853
|
+
requireScope(request, SETTINGS_WRITE_SCOPE);
|
|
14778
14854
|
if (!opts.onCdpConfigure) {
|
|
14779
14855
|
const err = notImplemented("CDP configuration not supported in this deployment");
|
|
14780
14856
|
return reply.code(err.statusCode).send(err.toJSON());
|
|
@@ -18085,6 +18161,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
18085
18161
|
|
|
18086
18162
|
// ../api-routes/src/traffic.ts
|
|
18087
18163
|
import crypto21 from "crypto";
|
|
18164
|
+
import { Agent as UndiciAgent } from "undici";
|
|
18088
18165
|
import { and as and19, desc as desc13, eq as eq24, gte as gte3, lte as lte2, sql as sql10 } from "drizzle-orm";
|
|
18089
18166
|
|
|
18090
18167
|
// ../integration-cloud-run/src/auth.ts
|
|
@@ -21364,6 +21441,7 @@ async function listWordpressTrafficEvents(options) {
|
|
|
21364
21441
|
let skippedEntryCount = 0;
|
|
21365
21442
|
let hasMore = false;
|
|
21366
21443
|
const events = [];
|
|
21444
|
+
const dispatcher = options.dispatcher;
|
|
21367
21445
|
for (let page = 0; page < maxPages; page += 1) {
|
|
21368
21446
|
const url = new URL(endpoint);
|
|
21369
21447
|
url.searchParams.set("limit", String(pageSize));
|
|
@@ -21377,7 +21455,7 @@ async function listWordpressTrafficEvents(options) {
|
|
|
21377
21455
|
url.searchParams.set("until", options.until);
|
|
21378
21456
|
}
|
|
21379
21457
|
url.searchParams.set("_cb", randomUUID());
|
|
21380
|
-
const
|
|
21458
|
+
const fetchInit = {
|
|
21381
21459
|
method: "GET",
|
|
21382
21460
|
headers: {
|
|
21383
21461
|
Authorization: authHeader,
|
|
@@ -21385,7 +21463,11 @@ async function listWordpressTrafficEvents(options) {
|
|
|
21385
21463
|
"Cache-Control": "no-cache"
|
|
21386
21464
|
},
|
|
21387
21465
|
signal: AbortSignal.timeout(timeoutMs)
|
|
21388
|
-
}
|
|
21466
|
+
};
|
|
21467
|
+
if (dispatcher !== void 0) {
|
|
21468
|
+
fetchInit.dispatcher = dispatcher;
|
|
21469
|
+
}
|
|
21470
|
+
const response = await fetch(url, fetchInit);
|
|
21389
21471
|
if (!response.ok) {
|
|
21390
21472
|
const body2 = await readErrorBody2(response);
|
|
21391
21473
|
throw new WordpressTrafficApiError(
|
|
@@ -21599,6 +21681,62 @@ async function listVercelTrafficEvents(options) {
|
|
|
21599
21681
|
};
|
|
21600
21682
|
}
|
|
21601
21683
|
|
|
21684
|
+
// ../integration-vercel/src/drain.ts
|
|
21685
|
+
var MIN_SUB_WINDOW_MS = 6e4;
|
|
21686
|
+
function toMs(value) {
|
|
21687
|
+
return typeof value === "number" ? value : value.getTime();
|
|
21688
|
+
}
|
|
21689
|
+
async function drainVercelTrafficEvents(options) {
|
|
21690
|
+
const startMs = toMs(options.startDate);
|
|
21691
|
+
const endMs = toMs(options.endDate);
|
|
21692
|
+
const events = [];
|
|
21693
|
+
const seenEventIds = /* @__PURE__ */ new Set();
|
|
21694
|
+
if (endMs <= startMs) return { events, subWindowCount: 0 };
|
|
21695
|
+
let cursorMs = startMs;
|
|
21696
|
+
let spanMs = endMs - startMs;
|
|
21697
|
+
let subWindowCount = 0;
|
|
21698
|
+
while (cursorMs < endMs) {
|
|
21699
|
+
if (subWindowCount >= options.maxSubWindows) {
|
|
21700
|
+
throw new Error(
|
|
21701
|
+
`Vercel window not drained within ${options.maxSubWindows} sub-windows \u2014 narrow the time range`
|
|
21702
|
+
);
|
|
21703
|
+
}
|
|
21704
|
+
const subEndMs = Math.min(cursorMs + spanMs, endMs);
|
|
21705
|
+
const page = await options.pull({
|
|
21706
|
+
token: options.token,
|
|
21707
|
+
projectId: options.projectId,
|
|
21708
|
+
teamId: options.teamId,
|
|
21709
|
+
environment: options.environment,
|
|
21710
|
+
startDate: cursorMs,
|
|
21711
|
+
endDate: subEndMs,
|
|
21712
|
+
maxPages: options.pagesPerSubWindow
|
|
21713
|
+
});
|
|
21714
|
+
subWindowCount += 1;
|
|
21715
|
+
if (page.hasMore) {
|
|
21716
|
+
const subSpanMs = subEndMs - cursorMs;
|
|
21717
|
+
if (subSpanMs <= MIN_SUB_WINDOW_MS) {
|
|
21718
|
+
throw new Error(
|
|
21719
|
+
`Vercel window holds more than ${options.pagesPerSubWindow} pages within a ${MIN_SUB_WINDOW_MS / 6e4}-minute slice \u2014 cannot subdivide further`
|
|
21720
|
+
);
|
|
21721
|
+
}
|
|
21722
|
+
spanMs = Math.max(Math.floor(subSpanMs / 2), MIN_SUB_WINDOW_MS);
|
|
21723
|
+
continue;
|
|
21724
|
+
}
|
|
21725
|
+
for (const event of page.events) {
|
|
21726
|
+
if (!seenEventIds.has(event.eventId)) {
|
|
21727
|
+
seenEventIds.add(event.eventId);
|
|
21728
|
+
events.push(event);
|
|
21729
|
+
}
|
|
21730
|
+
}
|
|
21731
|
+
cursorMs = subEndMs;
|
|
21732
|
+
const remainingMs = endMs - cursorMs;
|
|
21733
|
+
if (remainingMs > 0) {
|
|
21734
|
+
spanMs = Math.min(spanMs * 2, remainingMs);
|
|
21735
|
+
}
|
|
21736
|
+
}
|
|
21737
|
+
return { events, subWindowCount };
|
|
21738
|
+
}
|
|
21739
|
+
|
|
21602
21740
|
// ../api-routes/src/traffic.ts
|
|
21603
21741
|
var DEFAULT_SYNC_WINDOW_MINUTES = 43200;
|
|
21604
21742
|
var DEFAULT_PAGE_SIZE3 = 1e3;
|
|
@@ -21607,6 +21745,7 @@ var DEFAULT_SAMPLE_LIMIT2 = 100;
|
|
|
21607
21745
|
var DEFAULT_WP_PAGE_SIZE = 500;
|
|
21608
21746
|
var DEFAULT_WP_MAX_PAGES = 20;
|
|
21609
21747
|
var DEFAULT_VERCEL_MAX_PAGES = 50;
|
|
21748
|
+
var VERCEL_MAX_SUB_WINDOWS = 5e3;
|
|
21610
21749
|
var MAX_TRACKED_EVENT_IDS = 1e3;
|
|
21611
21750
|
var DEFAULT_BACKFILL_DAYS = 30;
|
|
21612
21751
|
var MAX_BACKFILL_DAYS = 90;
|
|
@@ -21812,6 +21951,30 @@ async function trafficRoutes(app, opts) {
|
|
|
21812
21951
|
const resolveAccessToken2 = opts.resolveCloudRunAccessToken ?? defaultResolveAccessToken;
|
|
21813
21952
|
const pullWordpressEvents = opts.pullWordpressTrafficEvents ?? listWordpressTrafficEvents;
|
|
21814
21953
|
const pullVercelEvents = opts.pullVercelTrafficEvents ?? listVercelTrafficEvents;
|
|
21954
|
+
const allowLoopback = opts.allowLoopbackWebhooks === true;
|
|
21955
|
+
async function assertWordpressTargetAllowed(baseUrl) {
|
|
21956
|
+
const check = await resolveWebhookTarget(baseUrl, { allowLoopback });
|
|
21957
|
+
if (!check.ok) {
|
|
21958
|
+
throw validationError(`WordPress baseUrl rejected: ${check.message}`);
|
|
21959
|
+
}
|
|
21960
|
+
const { address, family } = check.target;
|
|
21961
|
+
return new UndiciAgent({
|
|
21962
|
+
connect: {
|
|
21963
|
+
lookup: (_hostname, _options, cb) => {
|
|
21964
|
+
cb(null, address, family === 6 ? 6 : 4);
|
|
21965
|
+
}
|
|
21966
|
+
}
|
|
21967
|
+
});
|
|
21968
|
+
}
|
|
21969
|
+
async function withPinnedWordpressDispatcher(baseUrl, fn) {
|
|
21970
|
+
const dispatcher = await assertWordpressTargetAllowed(baseUrl);
|
|
21971
|
+
try {
|
|
21972
|
+
return await fn(dispatcher);
|
|
21973
|
+
} finally {
|
|
21974
|
+
await dispatcher.close().catch(() => {
|
|
21975
|
+
});
|
|
21976
|
+
}
|
|
21977
|
+
}
|
|
21815
21978
|
const vercelMaxPages = opts.defaultVercelMaxPages ?? DEFAULT_VERCEL_MAX_PAGES;
|
|
21816
21979
|
const syncWindowMinutes = opts.defaultSyncWindowMinutes ?? DEFAULT_SYNC_WINDOW_MINUTES;
|
|
21817
21980
|
const pageSize = opts.defaultPageSize ?? DEFAULT_PAGE_SIZE3;
|
|
@@ -21910,23 +22073,26 @@ async function trafficRoutes(app, opts) {
|
|
|
21910
22073
|
throw validationError(parsed.error.issues.map((i) => i.message).join("; "));
|
|
21911
22074
|
}
|
|
21912
22075
|
const { baseUrl, username, applicationPassword, displayName } = parsed.data;
|
|
21913
|
-
|
|
21914
|
-
|
|
21915
|
-
|
|
21916
|
-
|
|
21917
|
-
|
|
21918
|
-
|
|
21919
|
-
|
|
21920
|
-
|
|
21921
|
-
|
|
21922
|
-
|
|
21923
|
-
|
|
21924
|
-
|
|
21925
|
-
|
|
22076
|
+
await withPinnedWordpressDispatcher(baseUrl, async (dispatcher) => {
|
|
22077
|
+
try {
|
|
22078
|
+
await pullWordpressEvents({
|
|
22079
|
+
baseUrl,
|
|
22080
|
+
username,
|
|
22081
|
+
applicationPassword,
|
|
22082
|
+
pageSize: 1,
|
|
22083
|
+
maxPages: 1,
|
|
22084
|
+
dispatcher
|
|
22085
|
+
});
|
|
22086
|
+
} catch (e) {
|
|
22087
|
+
if (e instanceof WordpressTrafficApiError) {
|
|
22088
|
+
throw providerError(
|
|
22089
|
+
`WordPress traffic probe failed (HTTP ${e.status}): ${e.message}${e.body ? ` \u2014 ${e.body}` : ""}`
|
|
22090
|
+
);
|
|
22091
|
+
}
|
|
22092
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
22093
|
+
throw providerError(`WordPress traffic probe failed: ${msg}`);
|
|
21926
22094
|
}
|
|
21927
|
-
|
|
21928
|
-
throw providerError(`WordPress traffic probe failed: ${msg}`);
|
|
21929
|
-
}
|
|
22095
|
+
});
|
|
21930
22096
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
21931
22097
|
const existing = credentialStore.getConnection(project.name);
|
|
21932
22098
|
credentialStore.upsertConnection({
|
|
@@ -22176,6 +22342,14 @@ async function trafficRoutes(app, opts) {
|
|
|
22176
22342
|
windowStart = sourceRow.lastSyncedAt ? new Date(sourceRow.lastSyncedAt) : windowEnd;
|
|
22177
22343
|
const wpPageSize = opts.defaultWordpressPageSize ?? DEFAULT_WP_PAGE_SIZE;
|
|
22178
22344
|
const wpMaxPages = opts.defaultWordpressMaxPages ?? DEFAULT_WP_MAX_PAGES;
|
|
22345
|
+
let pinnedDispatcher;
|
|
22346
|
+
try {
|
|
22347
|
+
pinnedDispatcher = await assertWordpressTargetAllowed(credential.baseUrl);
|
|
22348
|
+
} catch (e) {
|
|
22349
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
22350
|
+
markFailed(msg, "PROVIDER_PULL");
|
|
22351
|
+
throw e;
|
|
22352
|
+
}
|
|
22179
22353
|
const collected = [];
|
|
22180
22354
|
let cursor = sourceRow.lastCursor ?? void 0;
|
|
22181
22355
|
try {
|
|
@@ -22186,7 +22360,8 @@ async function trafficRoutes(app, opts) {
|
|
|
22186
22360
|
applicationPassword: credential.applicationPassword,
|
|
22187
22361
|
cursor,
|
|
22188
22362
|
pageSize: wpPageSize,
|
|
22189
|
-
maxPages: 1
|
|
22363
|
+
maxPages: 1,
|
|
22364
|
+
dispatcher: pinnedDispatcher
|
|
22190
22365
|
});
|
|
22191
22366
|
collected.push(...pageResult.events);
|
|
22192
22367
|
const previousCursor = cursor;
|
|
@@ -22200,6 +22375,9 @@ async function trafficRoutes(app, opts) {
|
|
|
22200
22375
|
const msg = e instanceof Error ? e.message : String(e);
|
|
22201
22376
|
markFailed(msg, "PROVIDER_PULL");
|
|
22202
22377
|
throw providerError(`WordPress pull failed: ${msg}`);
|
|
22378
|
+
} finally {
|
|
22379
|
+
await pinnedDispatcher.close().catch(() => {
|
|
22380
|
+
});
|
|
22203
22381
|
}
|
|
22204
22382
|
} else {
|
|
22205
22383
|
auditAction = "traffic.vercel.synced";
|
|
@@ -22226,28 +22404,24 @@ async function trafficRoutes(app, opts) {
|
|
|
22226
22404
|
windowStart = new Date(
|
|
22227
22405
|
Math.min(windowEnd.getTime(), Math.max(requestedStartMs, lastSyncedMs))
|
|
22228
22406
|
);
|
|
22229
|
-
let page;
|
|
22230
22407
|
try {
|
|
22231
|
-
|
|
22408
|
+
const drained = await drainVercelTrafficEvents({
|
|
22409
|
+
pull: pullVercelEvents,
|
|
22232
22410
|
token: credential.token,
|
|
22233
22411
|
projectId: vercelProjectId,
|
|
22234
22412
|
teamId: vercelTeamId,
|
|
22235
22413
|
environment: vercelEnvironment,
|
|
22236
22414
|
startDate: windowStart.getTime(),
|
|
22237
22415
|
endDate: windowEnd.getTime(),
|
|
22238
|
-
|
|
22416
|
+
pagesPerSubWindow: vercelMaxPages,
|
|
22417
|
+
maxSubWindows: VERCEL_MAX_SUB_WINDOWS
|
|
22239
22418
|
});
|
|
22419
|
+
allEvents = drained.events;
|
|
22240
22420
|
} catch (e) {
|
|
22241
22421
|
const msg = e instanceof Error ? e.message : String(e);
|
|
22242
22422
|
markFailed(msg, "PROVIDER_PULL");
|
|
22243
22423
|
throw providerError(`Vercel pull failed: ${msg}`);
|
|
22244
22424
|
}
|
|
22245
|
-
if (page.hasMore) {
|
|
22246
|
-
const msg = `Vercel sync window exceeded the ${vercelMaxPages}-page budget \u2014 narrow the window with --since-minutes or run a backfill`;
|
|
22247
|
-
markFailed(msg, "PROVIDER_PULL");
|
|
22248
|
-
throw providerError(`Vercel pull failed: ${msg}`);
|
|
22249
|
-
}
|
|
22250
|
-
allEvents = page.events;
|
|
22251
22425
|
}
|
|
22252
22426
|
let crawlerBucketRows = 0;
|
|
22253
22427
|
let aiUserFetchBucketRows = 0;
|
|
@@ -22534,33 +22708,42 @@ async function trafficRoutes(app, opts) {
|
|
|
22534
22708
|
`No WordPress credential found for project "${project.name}". Run "canonry traffic connect wordpress" first.`
|
|
22535
22709
|
);
|
|
22536
22710
|
}
|
|
22711
|
+
await (await assertWordpressTargetAllowed(credential.baseUrl)).close().catch(() => {
|
|
22712
|
+
});
|
|
22537
22713
|
const wpPageSize = opts.defaultWordpressPageSize ?? DEFAULT_WP_PAGE_SIZE;
|
|
22538
22714
|
pullErrorPrefix = "WordPress pull failed";
|
|
22539
22715
|
pullForBackfill = async () => {
|
|
22540
|
-
const
|
|
22541
|
-
|
|
22542
|
-
|
|
22543
|
-
|
|
22544
|
-
|
|
22545
|
-
|
|
22546
|
-
|
|
22547
|
-
|
|
22548
|
-
|
|
22549
|
-
|
|
22550
|
-
|
|
22551
|
-
|
|
22552
|
-
|
|
22553
|
-
|
|
22554
|
-
|
|
22555
|
-
|
|
22716
|
+
const pinnedDispatcher = await assertWordpressTargetAllowed(credential.baseUrl);
|
|
22717
|
+
try {
|
|
22718
|
+
const collected = [];
|
|
22719
|
+
const windowStartIso = windowStart.toISOString();
|
|
22720
|
+
const windowEndIso = windowEnd.toISOString();
|
|
22721
|
+
let cursor = void 0;
|
|
22722
|
+
for (let page = 0; page < BACKFILL_MAX_PAGES; page += 1) {
|
|
22723
|
+
const pageResult = await pullWordpressEvents({
|
|
22724
|
+
baseUrl: credential.baseUrl,
|
|
22725
|
+
username: credential.username,
|
|
22726
|
+
applicationPassword: credential.applicationPassword,
|
|
22727
|
+
cursor,
|
|
22728
|
+
pageSize: wpPageSize,
|
|
22729
|
+
// Each call fetches a single page; the for-loop drives
|
|
22730
|
+
// continuation. Matches the WP sync path's pattern.
|
|
22731
|
+
maxPages: 1,
|
|
22732
|
+
since: windowStartIso,
|
|
22733
|
+
until: windowEndIso,
|
|
22734
|
+
dispatcher: pinnedDispatcher
|
|
22735
|
+
});
|
|
22736
|
+
collected.push(...pageResult.events);
|
|
22737
|
+
const previousCursor = cursor;
|
|
22738
|
+
cursor = pageResult.nextCursor;
|
|
22739
|
+
if (!pageResult.hasMore) break;
|
|
22740
|
+
if (!cursor || cursor === previousCursor) break;
|
|
22741
|
+
}
|
|
22742
|
+
return collected;
|
|
22743
|
+
} finally {
|
|
22744
|
+
await pinnedDispatcher.close().catch(() => {
|
|
22556
22745
|
});
|
|
22557
|
-
collected.push(...pageResult.events);
|
|
22558
|
-
const previousCursor = cursor;
|
|
22559
|
-
cursor = pageResult.nextCursor;
|
|
22560
|
-
if (!pageResult.hasMore) break;
|
|
22561
|
-
if (!cursor || cursor === previousCursor) break;
|
|
22562
22746
|
}
|
|
22563
|
-
return collected;
|
|
22564
22747
|
};
|
|
22565
22748
|
} else {
|
|
22566
22749
|
const credentialStore = opts.vercelTrafficCredentialStore;
|
|
@@ -22579,21 +22762,18 @@ async function trafficRoutes(app, opts) {
|
|
|
22579
22762
|
const vercelEnvironment = config.environment ?? credential.environment;
|
|
22580
22763
|
pullErrorPrefix = "Vercel pull failed";
|
|
22581
22764
|
pullForBackfill = async () => {
|
|
22582
|
-
const
|
|
22765
|
+
const drained = await drainVercelTrafficEvents({
|
|
22766
|
+
pull: pullVercelEvents,
|
|
22583
22767
|
token: credential.token,
|
|
22584
22768
|
projectId: vercelProjectId,
|
|
22585
22769
|
teamId: vercelTeamId,
|
|
22586
22770
|
environment: vercelEnvironment,
|
|
22587
22771
|
startDate: windowStart.getTime(),
|
|
22588
22772
|
endDate: windowEnd.getTime(),
|
|
22589
|
-
|
|
22773
|
+
pagesPerSubWindow: vercelMaxPages,
|
|
22774
|
+
maxSubWindows: VERCEL_MAX_SUB_WINDOWS
|
|
22590
22775
|
});
|
|
22591
|
-
|
|
22592
|
-
throw new Error(
|
|
22593
|
-
`backfill window exceeded the ${BACKFILL_MAX_PAGES}-page budget \u2014 narrow the window with --days`
|
|
22594
|
-
);
|
|
22595
|
-
}
|
|
22596
|
-
return page.events;
|
|
22776
|
+
return drained.events;
|
|
22597
22777
|
};
|
|
22598
22778
|
}
|
|
22599
22779
|
const startedAt = windowEnd.toISOString();
|
|
@@ -24701,7 +24881,8 @@ async function apiRoutes(app, opts) {
|
|
|
24701
24881
|
pullWordpressTrafficEvents: opts.pullWordpressTrafficEvents,
|
|
24702
24882
|
vercelTrafficCredentialStore: opts.vercelTrafficCredentialStore,
|
|
24703
24883
|
pullVercelTrafficEvents: opts.pullVercelTrafficEvents,
|
|
24704
|
-
onTrafficSynced: opts.onTrafficSynced
|
|
24884
|
+
onTrafficSynced: opts.onTrafficSynced,
|
|
24885
|
+
allowLoopbackWebhooks: opts.allowLoopbackWebhooks
|
|
24705
24886
|
});
|
|
24706
24887
|
await api.register(backlinksRoutes, {
|
|
24707
24888
|
getBacklinksStatus: opts.getBacklinksStatus,
|
|
@@ -27251,7 +27432,8 @@ function upsertGoogleConnection(config, connection) {
|
|
|
27251
27432
|
sitemapUrl: connection.sitemapUrl ?? null,
|
|
27252
27433
|
refreshToken: connection.refreshToken ?? null,
|
|
27253
27434
|
tokenExpiresAt: connection.tokenExpiresAt ?? null,
|
|
27254
|
-
scopes: connection.scopes ?? []
|
|
27435
|
+
scopes: connection.scopes ?? [],
|
|
27436
|
+
createdByProjectId: connection.createdByProjectId ?? null
|
|
27255
27437
|
};
|
|
27256
27438
|
if (index === -1) {
|
|
27257
27439
|
connections.push(normalized);
|
|
@@ -29881,7 +30063,7 @@ function readStoredGroundingSources(rawResponse) {
|
|
|
29881
30063
|
return result;
|
|
29882
30064
|
}
|
|
29883
30065
|
async function backfillInsightsCommand(project, opts) {
|
|
29884
|
-
const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-
|
|
30066
|
+
const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-NKAEHHJ5.js");
|
|
29885
30067
|
const config = loadConfig();
|
|
29886
30068
|
const db = createClient(config.database);
|
|
29887
30069
|
migrate(db);
|
|
@@ -33036,6 +33218,48 @@ function summarizeProviderConfig(provider, config) {
|
|
|
33036
33218
|
function hashApiKey(key) {
|
|
33037
33219
|
return crypto35.createHash("sha256").update(key).digest("hex");
|
|
33038
33220
|
}
|
|
33221
|
+
var DASHBOARD_SCRYPT_KEYLEN = 64;
|
|
33222
|
+
var DASHBOARD_SCRYPT_COST = 1 << 15;
|
|
33223
|
+
var DASHBOARD_SCRYPT_MAXMEM = 64 * 1024 * 1024;
|
|
33224
|
+
function hashDashboardPassword(password) {
|
|
33225
|
+
const salt = crypto35.randomBytes(16);
|
|
33226
|
+
const derived = crypto35.scryptSync(password, salt, DASHBOARD_SCRYPT_KEYLEN, {
|
|
33227
|
+
N: DASHBOARD_SCRYPT_COST,
|
|
33228
|
+
maxmem: DASHBOARD_SCRYPT_MAXMEM
|
|
33229
|
+
});
|
|
33230
|
+
return `scrypt$1$${salt.toString("base64")}$${derived.toString("base64")}`;
|
|
33231
|
+
}
|
|
33232
|
+
function verifyDashboardPassword(password, storedHash) {
|
|
33233
|
+
if (storedHash.startsWith("scrypt$1$")) {
|
|
33234
|
+
const parts = storedHash.split("$");
|
|
33235
|
+
if (parts.length !== 4) return { ok: false, needsRehash: false };
|
|
33236
|
+
const saltB64 = parts[2];
|
|
33237
|
+
const hashB64 = parts[3];
|
|
33238
|
+
if (!saltB64 || !hashB64) return { ok: false, needsRehash: false };
|
|
33239
|
+
let salt;
|
|
33240
|
+
let expected;
|
|
33241
|
+
try {
|
|
33242
|
+
salt = Buffer.from(saltB64, "base64");
|
|
33243
|
+
expected = Buffer.from(hashB64, "base64");
|
|
33244
|
+
} catch {
|
|
33245
|
+
return { ok: false, needsRehash: false };
|
|
33246
|
+
}
|
|
33247
|
+
const derived = crypto35.scryptSync(password, salt, expected.length, {
|
|
33248
|
+
N: DASHBOARD_SCRYPT_COST,
|
|
33249
|
+
maxmem: DASHBOARD_SCRYPT_MAXMEM
|
|
33250
|
+
});
|
|
33251
|
+
if (derived.length !== expected.length) return { ok: false, needsRehash: false };
|
|
33252
|
+
return { ok: crypto35.timingSafeEqual(derived, expected), needsRehash: false };
|
|
33253
|
+
}
|
|
33254
|
+
if (/^[a-f0-9]{64}$/i.test(storedHash)) {
|
|
33255
|
+
const candidate = Buffer.from(hashApiKey(password), "hex");
|
|
33256
|
+
const expected = Buffer.from(storedHash, "hex");
|
|
33257
|
+
if (candidate.length !== expected.length) return { ok: false, needsRehash: false };
|
|
33258
|
+
const ok = crypto35.timingSafeEqual(candidate, expected);
|
|
33259
|
+
return { ok, needsRehash: ok };
|
|
33260
|
+
}
|
|
33261
|
+
return { ok: false, needsRehash: false };
|
|
33262
|
+
}
|
|
33039
33263
|
function parseCookies2(header) {
|
|
33040
33264
|
if (!header) return {};
|
|
33041
33265
|
return header.split(";").map((part) => part.trim()).filter(Boolean).reduce((cookies, part) => {
|
|
@@ -33264,13 +33488,14 @@ async function createServer(opts) {
|
|
|
33264
33488
|
if (!opts.config.bing) opts.config.bing = {};
|
|
33265
33489
|
if (!opts.config.bing.connections) opts.config.bing.connections = [];
|
|
33266
33490
|
const idx = opts.config.bing.connections.findIndex((c) => c.domain === connection.domain);
|
|
33491
|
+
const normalized = { ...connection, createdByProjectId: connection.createdByProjectId ?? null };
|
|
33267
33492
|
if (idx >= 0) {
|
|
33268
|
-
opts.config.bing.connections[idx] =
|
|
33493
|
+
opts.config.bing.connections[idx] = normalized;
|
|
33269
33494
|
} else {
|
|
33270
|
-
opts.config.bing.connections.push(
|
|
33495
|
+
opts.config.bing.connections.push(normalized);
|
|
33271
33496
|
}
|
|
33272
33497
|
saveConfigPatch(opts.config);
|
|
33273
|
-
return
|
|
33498
|
+
return normalized;
|
|
33274
33499
|
},
|
|
33275
33500
|
updateConnection: (domain, patch) => {
|
|
33276
33501
|
const conn = opts.config.bing?.connections?.find((c) => c.domain === domain);
|
|
@@ -33478,7 +33703,7 @@ async function createServer(opts) {
|
|
|
33478
33703
|
const err = validationError("Password must be at least 8 characters");
|
|
33479
33704
|
return reply.status(err.statusCode).send(err.toJSON());
|
|
33480
33705
|
}
|
|
33481
|
-
opts.config.dashboardPasswordHash =
|
|
33706
|
+
opts.config.dashboardPasswordHash = hashDashboardPassword(password);
|
|
33482
33707
|
saveConfigPatch(opts.config);
|
|
33483
33708
|
if (!createPasswordSession(reply)) {
|
|
33484
33709
|
const err = authInvalid();
|
|
@@ -33494,9 +33719,14 @@ async function createServer(opts) {
|
|
|
33494
33719
|
const err2 = validationError("No dashboard password configured \u2014 use /session/setup first");
|
|
33495
33720
|
return reply.status(err2.statusCode).send(err2.toJSON());
|
|
33496
33721
|
}
|
|
33497
|
-
|
|
33722
|
+
const verification = verifyDashboardPassword(password, opts.config.dashboardPasswordHash);
|
|
33723
|
+
if (!verification.ok) {
|
|
33498
33724
|
return reply.status(401).send({ error: { code: "AUTH_INVALID", message: "Incorrect password" } });
|
|
33499
33725
|
}
|
|
33726
|
+
if (verification.needsRehash) {
|
|
33727
|
+
opts.config.dashboardPasswordHash = hashDashboardPassword(password);
|
|
33728
|
+
saveConfigPatch(opts.config);
|
|
33729
|
+
}
|
|
33500
33730
|
if (!createPasswordSession(reply)) {
|
|
33501
33731
|
return reply.status(401).send({ error: { code: "AUTH_INVALID", message: "Server API key not found \u2014 re-run canonry init" } });
|
|
33502
33732
|
}
|