@ainyc/canonry 4.31.0 → 4.33.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/assets/agent-workspace/skills/canonry/references/canonry-cli.md +2 -1
- package/assets/assets/{index-C4UBTDDS.js → index-47V0U52s.js} +105 -105
- package/assets/index.html +1 -1
- package/dist/{chunk-5STLZRGB.js → chunk-5EBN7736.js} +24 -1
- package/dist/{chunk-U3YKRV47.js → chunk-BJXHETQW.js} +1 -1
- package/dist/{chunk-PUTJHEVR.js → chunk-JJHBPITI.js} +528 -17
- package/dist/{chunk-HTNC6AWN.js → chunk-XW3F5EEW.js} +113 -81
- package/dist/cli.js +131 -10
- package/dist/index.d.ts +20 -0
- package/dist/index.js +4 -4
- package/dist/{intelligence-service-CJONZ7ST.js → intelligence-service-XKOUBRCE.js} +2 -2
- package/dist/mcp.js +2 -2
- package/package.json +8 -8
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
loadConfig,
|
|
6
6
|
loadConfigRaw,
|
|
7
7
|
saveConfigPatch
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-5EBN7736.js";
|
|
9
9
|
import {
|
|
10
10
|
DEFAULT_RUN_HISTORY_LIMIT,
|
|
11
11
|
IntelligenceService,
|
|
@@ -70,7 +70,7 @@ import {
|
|
|
70
70
|
schedules,
|
|
71
71
|
trafficSources,
|
|
72
72
|
usageCounters
|
|
73
|
-
} from "./chunk-
|
|
73
|
+
} from "./chunk-BJXHETQW.js";
|
|
74
74
|
import {
|
|
75
75
|
AGENT_MEMORY_VALUE_MAX_BYTES,
|
|
76
76
|
AGENT_PROVIDER_IDS,
|
|
@@ -162,6 +162,7 @@ import {
|
|
|
162
162
|
reportHorizonLabel,
|
|
163
163
|
reportSeverityLabel,
|
|
164
164
|
resolveConfigSpecQueries,
|
|
165
|
+
resolveLocations,
|
|
165
166
|
resolveSnapshotRequestQueries,
|
|
166
167
|
runInProgress,
|
|
167
168
|
runNotCancellable,
|
|
@@ -171,13 +172,14 @@ import {
|
|
|
171
172
|
serializeRunError,
|
|
172
173
|
snapshotRequestSchema,
|
|
173
174
|
summarizeCheckResults,
|
|
175
|
+
trafficConnectVercelRequestSchema,
|
|
174
176
|
trafficConnectWordpressRequestSchema,
|
|
175
177
|
unsupportedKind,
|
|
176
178
|
validationError,
|
|
177
179
|
visibilityStateFromAnswerMentioned,
|
|
178
180
|
windowCutoff,
|
|
179
181
|
wordpressEnvSchema
|
|
180
|
-
} from "./chunk-
|
|
182
|
+
} from "./chunk-XW3F5EEW.js";
|
|
181
183
|
|
|
182
184
|
// src/telemetry.ts
|
|
183
185
|
import crypto from "crypto";
|
|
@@ -10234,6 +10236,38 @@ var routeCatalog = [
|
|
|
10234
10236
|
502: { description: "WordPress plugin endpoint probe failed (bad credentials, unreachable host, etc.)." }
|
|
10235
10237
|
}
|
|
10236
10238
|
},
|
|
10239
|
+
{
|
|
10240
|
+
method: "post",
|
|
10241
|
+
path: "/api/v1/projects/{name}/traffic/connect/vercel",
|
|
10242
|
+
summary: "Connect a Vercel traffic source",
|
|
10243
|
+
description: "Probes Vercel's internal `request-logs` endpoint with the supplied API 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 API token lives in the credential file.",
|
|
10244
|
+
tags: ["traffic"],
|
|
10245
|
+
parameters: [nameParameter],
|
|
10246
|
+
requestBody: {
|
|
10247
|
+
required: true,
|
|
10248
|
+
content: {
|
|
10249
|
+
"application/json": {
|
|
10250
|
+
schema: {
|
|
10251
|
+
type: "object",
|
|
10252
|
+
required: ["projectId", "teamId", "token"],
|
|
10253
|
+
properties: {
|
|
10254
|
+
projectId: { ...stringSchema, description: "Vercel project id (e.g. `prj_...`) \u2014 from the Vercel dashboard or `.vercel/project.json`." },
|
|
10255
|
+
teamId: { ...stringSchema, description: "Vercel team / owner id (e.g. `team_...`)." },
|
|
10256
|
+
token: { ...stringSchema, description: "Vercel API token (personal access token). Stored in `~/.canonry/config.yaml`, never the DB or response." },
|
|
10257
|
+
environment: { type: "string", enum: ["production", "preview"], description: "Which deployment environment's request logs to pull. Default: `production`." },
|
|
10258
|
+
displayName: stringSchema
|
|
10259
|
+
}
|
|
10260
|
+
}
|
|
10261
|
+
}
|
|
10262
|
+
}
|
|
10263
|
+
},
|
|
10264
|
+
responses: {
|
|
10265
|
+
200: { description: "Traffic source DTO returned." },
|
|
10266
|
+
400: { description: "Invalid Vercel connection request." },
|
|
10267
|
+
404: { description: "Project not found." },
|
|
10268
|
+
502: { description: "Vercel request-logs endpoint probe failed (bad token, wrong project / team id, unreachable host, etc.)." }
|
|
10269
|
+
}
|
|
10270
|
+
},
|
|
10237
10271
|
{
|
|
10238
10272
|
method: "post",
|
|
10239
10273
|
path: "/api/v1/projects/{name}/traffic/sources/{id}/sync",
|
|
@@ -10366,7 +10400,12 @@ var routeCatalog = [
|
|
|
10366
10400
|
properties: {
|
|
10367
10401
|
icpDescription: { type: "string", description: "Free-text ICP. Required if the project does not have spec.icpDescription stored." },
|
|
10368
10402
|
dedupThreshold: { type: "number", description: "Cosine similarity threshold for clustering. Defaults to 0.85." },
|
|
10369
|
-
maxProbes: { type: "integer", description: "Max canonical queries to probe in this session. Default 100, hard cap 500." }
|
|
10403
|
+
maxProbes: { type: "integer", description: "Max canonical queries to probe in this session. Default 100, hard cap 500." },
|
|
10404
|
+
locations: {
|
|
10405
|
+
type: "array",
|
|
10406
|
+
items: { type: "string" },
|
|
10407
|
+
description: "Optional override of the project location labels used to geo-constrain seed generation. Each label must match a configured project location; an unknown label is a 400. Omit to use every project location."
|
|
10408
|
+
}
|
|
10370
10409
|
}
|
|
10371
10410
|
}
|
|
10372
10411
|
}
|
|
@@ -17607,13 +17646,194 @@ async function listWordpressTrafficEvents(options) {
|
|
|
17607
17646
|
};
|
|
17608
17647
|
}
|
|
17609
17648
|
|
|
17649
|
+
// ../integration-vercel/src/normalize.ts
|
|
17650
|
+
function numberOrNull2(value) {
|
|
17651
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return null;
|
|
17652
|
+
return value;
|
|
17653
|
+
}
|
|
17654
|
+
function resolveStatus(row) {
|
|
17655
|
+
if (typeof row.statusCode === "number" && row.statusCode >= 100) {
|
|
17656
|
+
return row.statusCode;
|
|
17657
|
+
}
|
|
17658
|
+
const events = row.events ?? [];
|
|
17659
|
+
for (let i = events.length - 1; i >= 0; i -= 1) {
|
|
17660
|
+
const status = events[i]?.httpStatus;
|
|
17661
|
+
if (typeof status === "number" && status >= 100) return status;
|
|
17662
|
+
}
|
|
17663
|
+
return null;
|
|
17664
|
+
}
|
|
17665
|
+
function serializeSearchParams(params) {
|
|
17666
|
+
if (!params) return null;
|
|
17667
|
+
const entries = Object.entries(params).filter(
|
|
17668
|
+
(entry) => typeof entry[1] === "string"
|
|
17669
|
+
);
|
|
17670
|
+
if (entries.length === 0) return null;
|
|
17671
|
+
return new URLSearchParams(entries).toString();
|
|
17672
|
+
}
|
|
17673
|
+
function emptyToNull(value) {
|
|
17674
|
+
if (typeof value !== "string" || value.trim() === "") return null;
|
|
17675
|
+
return value;
|
|
17676
|
+
}
|
|
17677
|
+
function stringLabels(input) {
|
|
17678
|
+
return Object.fromEntries(
|
|
17679
|
+
Object.entries(input).filter(
|
|
17680
|
+
(entry) => typeof entry[1] === "string" && entry[1] !== ""
|
|
17681
|
+
)
|
|
17682
|
+
);
|
|
17683
|
+
}
|
|
17684
|
+
function normalizeVercelLogRow(row) {
|
|
17685
|
+
const path15 = row.requestPath;
|
|
17686
|
+
if (!path15) return null;
|
|
17687
|
+
const observedAt = row.timestamp;
|
|
17688
|
+
if (!observedAt) return null;
|
|
17689
|
+
const requestId = row.requestId;
|
|
17690
|
+
if (!requestId) return null;
|
|
17691
|
+
const host = emptyToNull(row.domain);
|
|
17692
|
+
const queryString = serializeSearchParams(row.requestSearchParams);
|
|
17693
|
+
const requestUrl = host ? `https://${host}${path15}${queryString ? `?${queryString}` : ""}` : null;
|
|
17694
|
+
return {
|
|
17695
|
+
sourceType: TrafficSourceTypes.vercel,
|
|
17696
|
+
evidenceKind: TrafficEvidenceKinds["raw-request"],
|
|
17697
|
+
confidence: TrafficEventConfidences.observed,
|
|
17698
|
+
eventId: `vercel:${observedAt}:${requestId}`,
|
|
17699
|
+
observedAt,
|
|
17700
|
+
method: row.requestMethod ?? null,
|
|
17701
|
+
requestUrl,
|
|
17702
|
+
host,
|
|
17703
|
+
path: path15,
|
|
17704
|
+
queryString,
|
|
17705
|
+
status: resolveStatus(row),
|
|
17706
|
+
userAgent: emptyToNull(row.clientUserAgent),
|
|
17707
|
+
// The request-logs endpoint does not expose a client IP; UA-only matches
|
|
17708
|
+
// stay `claimed_unverified` in the classifier.
|
|
17709
|
+
remoteIp: null,
|
|
17710
|
+
referer: emptyToNull(row.requestReferer),
|
|
17711
|
+
latencyMs: numberOrNull2(row.requestDurationMs),
|
|
17712
|
+
requestSizeBytes: null,
|
|
17713
|
+
responseSizeBytes: null,
|
|
17714
|
+
providerResource: {
|
|
17715
|
+
type: "vercel_deployment",
|
|
17716
|
+
labels: stringLabels({
|
|
17717
|
+
deploymentId: row.deploymentId,
|
|
17718
|
+
environment: row.environment,
|
|
17719
|
+
region: row.clientRegion
|
|
17720
|
+
})
|
|
17721
|
+
},
|
|
17722
|
+
providerLabels: stringLabels({
|
|
17723
|
+
branch: row.branch,
|
|
17724
|
+
cache: row.cache
|
|
17725
|
+
})
|
|
17726
|
+
};
|
|
17727
|
+
}
|
|
17728
|
+
|
|
17729
|
+
// ../integration-vercel/src/client.ts
|
|
17730
|
+
var VERCEL_REQUEST_LOGS_URL = "https://vercel.com/api/logs/request-logs";
|
|
17731
|
+
var DEFAULT_ENVIRONMENT = "production";
|
|
17732
|
+
var DEFAULT_MAX_PAGES3 = 1;
|
|
17733
|
+
var DEFAULT_TIMEOUT_MS3 = 3e4;
|
|
17734
|
+
var VercelLogsApiError = class extends Error {
|
|
17735
|
+
constructor(message, status, body) {
|
|
17736
|
+
super(message);
|
|
17737
|
+
this.status = status;
|
|
17738
|
+
this.body = body;
|
|
17739
|
+
this.name = "VercelLogsApiError";
|
|
17740
|
+
}
|
|
17741
|
+
};
|
|
17742
|
+
function trimRequired2(name, value) {
|
|
17743
|
+
const trimmed = value.trim();
|
|
17744
|
+
if (!trimmed) {
|
|
17745
|
+
throw new VercelLogsApiError(`${name} is required`, 400);
|
|
17746
|
+
}
|
|
17747
|
+
return trimmed;
|
|
17748
|
+
}
|
|
17749
|
+
function normalizeMaxPages3(maxPages) {
|
|
17750
|
+
if (maxPages === void 0) return DEFAULT_MAX_PAGES3;
|
|
17751
|
+
if (!Number.isInteger(maxPages) || maxPages < 1) {
|
|
17752
|
+
throw new VercelLogsApiError("maxPages must be a positive integer", 400);
|
|
17753
|
+
}
|
|
17754
|
+
return maxPages;
|
|
17755
|
+
}
|
|
17756
|
+
function toEpochMs(label, value) {
|
|
17757
|
+
const ms = value instanceof Date ? value.getTime() : typeof value === "number" ? value : new Date(value).getTime();
|
|
17758
|
+
if (!Number.isFinite(ms)) {
|
|
17759
|
+
throw new VercelLogsApiError(`${label} must be a valid date`, 400);
|
|
17760
|
+
}
|
|
17761
|
+
return String(Math.trunc(ms));
|
|
17762
|
+
}
|
|
17763
|
+
async function readErrorBody3(response) {
|
|
17764
|
+
const text = await response.text().catch(() => "");
|
|
17765
|
+
if (!text) return void 0;
|
|
17766
|
+
return text.length <= 500 ? text : `${text.slice(0, 500)}... [truncated]`;
|
|
17767
|
+
}
|
|
17768
|
+
async function listVercelTrafficEvents(options) {
|
|
17769
|
+
const token = trimRequired2("token", options.token);
|
|
17770
|
+
const projectId = trimRequired2("projectId", options.projectId);
|
|
17771
|
+
const teamId = trimRequired2("teamId", options.teamId);
|
|
17772
|
+
const environment = options.environment ?? DEFAULT_ENVIRONMENT;
|
|
17773
|
+
const startDate = toEpochMs("startDate", options.startDate);
|
|
17774
|
+
const endDate = toEpochMs("endDate", options.endDate);
|
|
17775
|
+
const maxPages = normalizeMaxPages3(options.maxPages);
|
|
17776
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS3;
|
|
17777
|
+
let rawEntryCount = 0;
|
|
17778
|
+
let skippedEntryCount = 0;
|
|
17779
|
+
let hasMore = false;
|
|
17780
|
+
const events = [];
|
|
17781
|
+
for (let page = 0; page < maxPages; page += 1) {
|
|
17782
|
+
const url = new URL(VERCEL_REQUEST_LOGS_URL);
|
|
17783
|
+
url.searchParams.set("projectId", projectId);
|
|
17784
|
+
url.searchParams.set("ownerId", teamId);
|
|
17785
|
+
url.searchParams.set("teamId", teamId);
|
|
17786
|
+
url.searchParams.set("page", String(page));
|
|
17787
|
+
url.searchParams.set("startDate", startDate);
|
|
17788
|
+
url.searchParams.set("endDate", endDate);
|
|
17789
|
+
url.searchParams.set("environment", environment);
|
|
17790
|
+
const response = await fetch(url, {
|
|
17791
|
+
method: "GET",
|
|
17792
|
+
headers: {
|
|
17793
|
+
Authorization: `Bearer ${token}`,
|
|
17794
|
+
Accept: "application/json"
|
|
17795
|
+
},
|
|
17796
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
17797
|
+
});
|
|
17798
|
+
if (!response.ok) {
|
|
17799
|
+
const body2 = await readErrorBody3(response);
|
|
17800
|
+
throw new VercelLogsApiError(
|
|
17801
|
+
`Vercel request-logs endpoint returned HTTP ${response.status}`,
|
|
17802
|
+
response.status,
|
|
17803
|
+
body2
|
|
17804
|
+
);
|
|
17805
|
+
}
|
|
17806
|
+
const body = await response.json();
|
|
17807
|
+
const rows = body.rows ?? [];
|
|
17808
|
+
rawEntryCount += rows.length;
|
|
17809
|
+
for (const row of rows) {
|
|
17810
|
+
const event = normalizeVercelLogRow(row);
|
|
17811
|
+
if (event) {
|
|
17812
|
+
events.push(event);
|
|
17813
|
+
} else {
|
|
17814
|
+
skippedEntryCount += 1;
|
|
17815
|
+
}
|
|
17816
|
+
}
|
|
17817
|
+
hasMore = Boolean(body.hasMoreRows);
|
|
17818
|
+
if (!hasMore) break;
|
|
17819
|
+
}
|
|
17820
|
+
return {
|
|
17821
|
+
events,
|
|
17822
|
+
rawEntryCount,
|
|
17823
|
+
skippedEntryCount,
|
|
17824
|
+
hasMore,
|
|
17825
|
+
endpoint: VERCEL_REQUEST_LOGS_URL
|
|
17826
|
+
};
|
|
17827
|
+
}
|
|
17828
|
+
|
|
17610
17829
|
// ../api-routes/src/traffic.ts
|
|
17611
17830
|
var DEFAULT_SYNC_WINDOW_MINUTES = 43200;
|
|
17612
17831
|
var DEFAULT_PAGE_SIZE3 = 1e3;
|
|
17613
|
-
var
|
|
17832
|
+
var DEFAULT_MAX_PAGES4 = 5;
|
|
17614
17833
|
var DEFAULT_SAMPLE_LIMIT2 = 100;
|
|
17615
17834
|
var DEFAULT_WP_PAGE_SIZE = 500;
|
|
17616
17835
|
var DEFAULT_WP_MAX_PAGES = 20;
|
|
17836
|
+
var DEFAULT_VERCEL_MAX_PAGES = 50;
|
|
17617
17837
|
var MAX_TRACKED_EVENT_IDS = 1e3;
|
|
17618
17838
|
var DEFAULT_BACKFILL_DAYS = 30;
|
|
17619
17839
|
var MAX_BACKFILL_DAYS = 30;
|
|
@@ -17794,9 +18014,11 @@ async function trafficRoutes(app, opts) {
|
|
|
17794
18014
|
const pullEvents = opts.pullCloudRunEvents ?? listCloudRunTrafficEvents;
|
|
17795
18015
|
const resolveAccessToken2 = opts.resolveCloudRunAccessToken ?? defaultResolveAccessToken;
|
|
17796
18016
|
const pullWordpressEvents = opts.pullWordpressTrafficEvents ?? listWordpressTrafficEvents;
|
|
18017
|
+
const pullVercelEvents = opts.pullVercelTrafficEvents ?? listVercelTrafficEvents;
|
|
18018
|
+
const vercelMaxPages = opts.defaultVercelMaxPages ?? DEFAULT_VERCEL_MAX_PAGES;
|
|
17797
18019
|
const syncWindowMinutes = opts.defaultSyncWindowMinutes ?? DEFAULT_SYNC_WINDOW_MINUTES;
|
|
17798
18020
|
const pageSize = opts.defaultPageSize ?? DEFAULT_PAGE_SIZE3;
|
|
17799
|
-
const maxPages = opts.defaultMaxPages ??
|
|
18021
|
+
const maxPages = opts.defaultMaxPages ?? DEFAULT_MAX_PAGES4;
|
|
17800
18022
|
const sampleLimit = opts.defaultSampleLimit ?? DEFAULT_SAMPLE_LIMIT2;
|
|
17801
18023
|
app.post("/projects/:name/traffic/connect/cloud-run", async (request) => {
|
|
17802
18024
|
const project = resolveProject(app.db, request.params.name);
|
|
@@ -17958,15 +18180,98 @@ async function trafficRoutes(app, opts) {
|
|
|
17958
18180
|
});
|
|
17959
18181
|
return rowToDto(sourceRow);
|
|
17960
18182
|
});
|
|
18183
|
+
app.post("/projects/:name/traffic/connect/vercel", async (request) => {
|
|
18184
|
+
const project = resolveProject(app.db, request.params.name);
|
|
18185
|
+
if (!opts.vercelTrafficCredentialStore) {
|
|
18186
|
+
throw validationError("Vercel traffic credential storage is not configured for this deployment");
|
|
18187
|
+
}
|
|
18188
|
+
const credentialStore = opts.vercelTrafficCredentialStore;
|
|
18189
|
+
const parsed = trafficConnectVercelRequestSchema.safeParse(request.body ?? {});
|
|
18190
|
+
if (!parsed.success) {
|
|
18191
|
+
throw validationError(parsed.error.issues.map((i) => i.message).join("; "));
|
|
18192
|
+
}
|
|
18193
|
+
const { projectId, teamId, token, displayName } = parsed.data;
|
|
18194
|
+
const environment = parsed.data.environment ?? "production";
|
|
18195
|
+
const probeEnd = Date.now();
|
|
18196
|
+
try {
|
|
18197
|
+
await pullVercelEvents({
|
|
18198
|
+
token,
|
|
18199
|
+
projectId,
|
|
18200
|
+
teamId,
|
|
18201
|
+
environment,
|
|
18202
|
+
startDate: probeEnd - 60 * 6e4,
|
|
18203
|
+
endDate: probeEnd,
|
|
18204
|
+
maxPages: 1
|
|
18205
|
+
});
|
|
18206
|
+
} catch (e) {
|
|
18207
|
+
if (e instanceof VercelLogsApiError) {
|
|
18208
|
+
throw providerError(
|
|
18209
|
+
`Vercel traffic probe failed (HTTP ${e.status}): ${e.message}${e.body ? ` \u2014 ${e.body}` : ""}`
|
|
18210
|
+
);
|
|
18211
|
+
}
|
|
18212
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
18213
|
+
throw providerError(`Vercel traffic probe failed: ${msg}`);
|
|
18214
|
+
}
|
|
18215
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
18216
|
+
const existing = credentialStore.getConnection(project.name);
|
|
18217
|
+
credentialStore.upsertConnection({
|
|
18218
|
+
projectName: project.name,
|
|
18219
|
+
projectId,
|
|
18220
|
+
teamId,
|
|
18221
|
+
token,
|
|
18222
|
+
environment,
|
|
18223
|
+
createdAt: existing?.createdAt ?? now,
|
|
18224
|
+
updatedAt: now
|
|
18225
|
+
});
|
|
18226
|
+
const activeSource = app.db.select().from(trafficSources).where(eq23(trafficSources.projectId, project.id)).all().find((row) => row.sourceType === TrafficSourceTypes.vercel && row.status !== TrafficSourceStatuses.archived);
|
|
18227
|
+
const config = { projectId, teamId, environment };
|
|
18228
|
+
const fallbackName = displayName ?? `Vercel \xB7 ${projectId}`;
|
|
18229
|
+
let sourceRow;
|
|
18230
|
+
if (activeSource) {
|
|
18231
|
+
app.db.update(trafficSources).set({
|
|
18232
|
+
displayName: fallbackName,
|
|
18233
|
+
status: TrafficSourceStatuses.connected,
|
|
18234
|
+
lastError: null,
|
|
18235
|
+
configJson: JSON.stringify(config),
|
|
18236
|
+
updatedAt: now
|
|
18237
|
+
}).where(eq23(trafficSources.id, activeSource.id)).run();
|
|
18238
|
+
sourceRow = app.db.select().from(trafficSources).where(eq23(trafficSources.id, activeSource.id)).get();
|
|
18239
|
+
} else {
|
|
18240
|
+
const newId = crypto20.randomUUID();
|
|
18241
|
+
app.db.insert(trafficSources).values({
|
|
18242
|
+
id: newId,
|
|
18243
|
+
projectId: project.id,
|
|
18244
|
+
sourceType: TrafficSourceTypes.vercel,
|
|
18245
|
+
displayName: fallbackName,
|
|
18246
|
+
status: TrafficSourceStatuses.connected,
|
|
18247
|
+
lastSyncedAt: null,
|
|
18248
|
+
lastCursor: null,
|
|
18249
|
+
lastError: null,
|
|
18250
|
+
archivedAt: null,
|
|
18251
|
+
configJson: JSON.stringify(config),
|
|
18252
|
+
createdAt: now,
|
|
18253
|
+
updatedAt: now
|
|
18254
|
+
}).run();
|
|
18255
|
+
sourceRow = app.db.select().from(trafficSources).where(eq23(trafficSources.id, newId)).get();
|
|
18256
|
+
}
|
|
18257
|
+
writeAuditLog(app.db, {
|
|
18258
|
+
projectId: project.id,
|
|
18259
|
+
actor: "api",
|
|
18260
|
+
action: "traffic.vercel.connected",
|
|
18261
|
+
entityType: "traffic_source",
|
|
18262
|
+
entityId: sourceRow.id
|
|
18263
|
+
});
|
|
18264
|
+
return rowToDto(sourceRow);
|
|
18265
|
+
});
|
|
17961
18266
|
app.post("/projects/:name/traffic/sources/:id/sync", async (request) => {
|
|
17962
18267
|
const project = resolveProject(app.db, request.params.name);
|
|
17963
18268
|
const sourceRow = app.db.select().from(trafficSources).where(eq23(trafficSources.id, request.params.id)).get();
|
|
17964
18269
|
if (!sourceRow || sourceRow.projectId !== project.id) {
|
|
17965
18270
|
throw notFound("Traffic source", request.params.id);
|
|
17966
18271
|
}
|
|
17967
|
-
if (sourceRow.sourceType !== TrafficSourceTypes["cloud-run"] && sourceRow.sourceType !== TrafficSourceTypes.wordpress) {
|
|
18272
|
+
if (sourceRow.sourceType !== TrafficSourceTypes["cloud-run"] && sourceRow.sourceType !== TrafficSourceTypes.wordpress && sourceRow.sourceType !== TrafficSourceTypes.vercel) {
|
|
17968
18273
|
throw validationError(
|
|
17969
|
-
`Sync for source type "${sourceRow.sourceType}" is not implemented yet \u2014 only cloud-run and
|
|
18274
|
+
`Sync for source type "${sourceRow.sourceType}" is not implemented yet \u2014 only cloud-run, wordpress, and vercel are supported in v1.`
|
|
17970
18275
|
);
|
|
17971
18276
|
}
|
|
17972
18277
|
const windowEnd = /* @__PURE__ */ new Date();
|
|
@@ -18056,7 +18361,7 @@ async function trafficRoutes(app, opts) {
|
|
|
18056
18361
|
markFailed(msg, "PROVIDER_PULL");
|
|
18057
18362
|
throw providerError(`Cloud Run pull failed: ${msg}`);
|
|
18058
18363
|
}
|
|
18059
|
-
} else {
|
|
18364
|
+
} else if (sourceRow.sourceType === TrafficSourceTypes.wordpress) {
|
|
18060
18365
|
auditAction = "traffic.wordpress.synced";
|
|
18061
18366
|
const credentialStore = opts.wordpressTrafficCredentialStore;
|
|
18062
18367
|
if (!credentialStore) {
|
|
@@ -18097,6 +18402,53 @@ async function trafficRoutes(app, opts) {
|
|
|
18097
18402
|
markFailed(msg, "PROVIDER_PULL");
|
|
18098
18403
|
throw providerError(`WordPress pull failed: ${msg}`);
|
|
18099
18404
|
}
|
|
18405
|
+
} else {
|
|
18406
|
+
auditAction = "traffic.vercel.synced";
|
|
18407
|
+
const credentialStore = opts.vercelTrafficCredentialStore;
|
|
18408
|
+
if (!credentialStore) {
|
|
18409
|
+
app.db.delete(runs).where(eq23(runs.id, runId)).run();
|
|
18410
|
+
throw validationError("Vercel traffic credential storage is not configured for this deployment");
|
|
18411
|
+
}
|
|
18412
|
+
const credential = credentialStore.getConnection(project.name);
|
|
18413
|
+
if (!credential) {
|
|
18414
|
+
app.db.delete(runs).where(eq23(runs.id, runId)).run();
|
|
18415
|
+
throw validationError(
|
|
18416
|
+
`No Vercel credential found for project "${project.name}". Run "canonry traffic connect vercel" first.`
|
|
18417
|
+
);
|
|
18418
|
+
}
|
|
18419
|
+
const config = parseSourceConfig(sourceRow);
|
|
18420
|
+
const vercelProjectId = config.projectId ?? credential.projectId;
|
|
18421
|
+
const vercelTeamId = config.teamId ?? credential.teamId;
|
|
18422
|
+
const vercelEnvironment = config.environment ?? credential.environment;
|
|
18423
|
+
const requestedMinutes = request.body?.sinceMinutes;
|
|
18424
|
+
const windowMinutes = Number.isFinite(requestedMinutes) && requestedMinutes && requestedMinutes > 0 ? Math.floor(requestedMinutes) : syncWindowMinutes;
|
|
18425
|
+
const requestedStartMs = windowEnd.getTime() - windowMinutes * 6e4;
|
|
18426
|
+
const lastSyncedMs = sourceRow.lastSyncedAt ? new Date(sourceRow.lastSyncedAt).getTime() : Number.NEGATIVE_INFINITY;
|
|
18427
|
+
windowStart = new Date(
|
|
18428
|
+
Math.min(windowEnd.getTime(), Math.max(requestedStartMs, lastSyncedMs))
|
|
18429
|
+
);
|
|
18430
|
+
let page;
|
|
18431
|
+
try {
|
|
18432
|
+
page = await pullVercelEvents({
|
|
18433
|
+
token: credential.token,
|
|
18434
|
+
projectId: vercelProjectId,
|
|
18435
|
+
teamId: vercelTeamId,
|
|
18436
|
+
environment: vercelEnvironment,
|
|
18437
|
+
startDate: windowStart.getTime(),
|
|
18438
|
+
endDate: windowEnd.getTime(),
|
|
18439
|
+
maxPages: vercelMaxPages
|
|
18440
|
+
});
|
|
18441
|
+
} catch (e) {
|
|
18442
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
18443
|
+
markFailed(msg, "PROVIDER_PULL");
|
|
18444
|
+
throw providerError(`Vercel pull failed: ${msg}`);
|
|
18445
|
+
}
|
|
18446
|
+
if (page.hasMore) {
|
|
18447
|
+
const msg = `Vercel sync window exceeded the ${vercelMaxPages}-page budget \u2014 narrow the window with --since-minutes or run a backfill`;
|
|
18448
|
+
markFailed(msg, "PROVIDER_PULL");
|
|
18449
|
+
throw providerError(`Vercel pull failed: ${msg}`);
|
|
18450
|
+
}
|
|
18451
|
+
allEvents = page.events;
|
|
18100
18452
|
}
|
|
18101
18453
|
let crawlerBucketRows = 0;
|
|
18102
18454
|
let aiReferralBucketRows = 0;
|
|
@@ -18281,9 +18633,9 @@ async function trafficRoutes(app, opts) {
|
|
|
18281
18633
|
if (!sourceRow || sourceRow.projectId !== project.id) {
|
|
18282
18634
|
throw notFound("Traffic source", request.params.id);
|
|
18283
18635
|
}
|
|
18284
|
-
if (sourceRow.sourceType !== TrafficSourceTypes["cloud-run"] && sourceRow.sourceType !== TrafficSourceTypes.wordpress) {
|
|
18636
|
+
if (sourceRow.sourceType !== TrafficSourceTypes["cloud-run"] && sourceRow.sourceType !== TrafficSourceTypes.wordpress && sourceRow.sourceType !== TrafficSourceTypes.vercel) {
|
|
18285
18637
|
throw validationError(
|
|
18286
|
-
`Backfill for source type "${sourceRow.sourceType}" is not implemented yet \u2014 only cloud-run and
|
|
18638
|
+
`Backfill for source type "${sourceRow.sourceType}" is not implemented yet \u2014 only cloud-run, wordpress, and vercel are supported in v1.`
|
|
18287
18639
|
);
|
|
18288
18640
|
}
|
|
18289
18641
|
const requestedDays = request.body?.days ?? DEFAULT_BACKFILL_DAYS;
|
|
@@ -18331,7 +18683,7 @@ async function trafficRoutes(app, opts) {
|
|
|
18331
18683
|
});
|
|
18332
18684
|
return page.events;
|
|
18333
18685
|
};
|
|
18334
|
-
} else {
|
|
18686
|
+
} else if (sourceRow.sourceType === TrafficSourceTypes.wordpress) {
|
|
18335
18687
|
const credentialStore = opts.wordpressTrafficCredentialStore;
|
|
18336
18688
|
if (!credentialStore) {
|
|
18337
18689
|
throw validationError("WordPress traffic credential storage is not configured for this deployment");
|
|
@@ -18370,6 +18722,39 @@ async function trafficRoutes(app, opts) {
|
|
|
18370
18722
|
}
|
|
18371
18723
|
return collected;
|
|
18372
18724
|
};
|
|
18725
|
+
} else {
|
|
18726
|
+
const credentialStore = opts.vercelTrafficCredentialStore;
|
|
18727
|
+
if (!credentialStore) {
|
|
18728
|
+
throw validationError("Vercel traffic credential storage is not configured for this deployment");
|
|
18729
|
+
}
|
|
18730
|
+
const credential = credentialStore.getConnection(project.name);
|
|
18731
|
+
if (!credential) {
|
|
18732
|
+
throw validationError(
|
|
18733
|
+
`No Vercel credential found for project "${project.name}". Run "canonry traffic connect vercel" first.`
|
|
18734
|
+
);
|
|
18735
|
+
}
|
|
18736
|
+
const config = parseSourceConfig(sourceRow);
|
|
18737
|
+
const vercelProjectId = config.projectId ?? credential.projectId;
|
|
18738
|
+
const vercelTeamId = config.teamId ?? credential.teamId;
|
|
18739
|
+
const vercelEnvironment = config.environment ?? credential.environment;
|
|
18740
|
+
pullErrorPrefix = "Vercel pull failed";
|
|
18741
|
+
pullForBackfill = async () => {
|
|
18742
|
+
const page = await pullVercelEvents({
|
|
18743
|
+
token: credential.token,
|
|
18744
|
+
projectId: vercelProjectId,
|
|
18745
|
+
teamId: vercelTeamId,
|
|
18746
|
+
environment: vercelEnvironment,
|
|
18747
|
+
startDate: windowStart.getTime(),
|
|
18748
|
+
endDate: windowEnd.getTime(),
|
|
18749
|
+
maxPages: BACKFILL_MAX_PAGES
|
|
18750
|
+
});
|
|
18751
|
+
if (page.hasMore) {
|
|
18752
|
+
throw new Error(
|
|
18753
|
+
`backfill window exceeded the ${BACKFILL_MAX_PAGES}-page budget \u2014 narrow the window with --days`
|
|
18754
|
+
);
|
|
18755
|
+
}
|
|
18756
|
+
return page.events;
|
|
18757
|
+
};
|
|
18373
18758
|
}
|
|
18374
18759
|
const startedAt = windowEnd.toISOString();
|
|
18375
18760
|
const runId = crypto20.randomUUID();
|
|
@@ -19643,6 +20028,10 @@ async function discoveryRoutes(app, opts) {
|
|
|
19643
20028
|
"icpDescription is required. Pass it in the request body or store it on the project (spec.icpDescription)."
|
|
19644
20029
|
);
|
|
19645
20030
|
}
|
|
20031
|
+
const locations = resolveLocations(
|
|
20032
|
+
parseJsonColumn(project.locations, []),
|
|
20033
|
+
parsed.data.locations
|
|
20034
|
+
);
|
|
19646
20035
|
if (!opts.onDiscoveryRunRequested) {
|
|
19647
20036
|
throw validationError("Discovery is not available on this deployment.", {
|
|
19648
20037
|
reason: "no-discovery-handler"
|
|
@@ -19684,7 +20073,8 @@ async function discoveryRoutes(app, opts) {
|
|
|
19684
20073
|
projectId: project.id,
|
|
19685
20074
|
icpDescription,
|
|
19686
20075
|
dedupThreshold: parsed.data.dedupThreshold,
|
|
19687
|
-
maxProbes: parsed.data.maxProbes
|
|
20076
|
+
maxProbes: parsed.data.maxProbes,
|
|
20077
|
+
locations
|
|
19688
20078
|
});
|
|
19689
20079
|
return reply.status(201).send({ runId, sessionId, status: "running" });
|
|
19690
20080
|
});
|
|
@@ -19964,7 +20354,8 @@ async function executeDiscovery(opts) {
|
|
|
19964
20354
|
}).where(eq26(discoverySessions.id, opts.sessionId)).run();
|
|
19965
20355
|
const seedResult = await opts.deps.seed({
|
|
19966
20356
|
project: opts.project,
|
|
19967
|
-
icpDescription: opts.icpDescription
|
|
20357
|
+
icpDescription: opts.icpDescription,
|
|
20358
|
+
locations: opts.locations ?? []
|
|
19968
20359
|
});
|
|
19969
20360
|
const rawCandidates = dedupeStrings(seedResult.candidates);
|
|
19970
20361
|
const seedCountRaw = rawCandidates.length;
|
|
@@ -20169,6 +20560,8 @@ async function apiRoutes(app, opts) {
|
|
|
20169
20560
|
resolveCloudRunAccessToken: opts.resolveCloudRunAccessToken,
|
|
20170
20561
|
wordpressTrafficCredentialStore: opts.wordpressTrafficCredentialStore,
|
|
20171
20562
|
pullWordpressTrafficEvents: opts.pullWordpressTrafficEvents,
|
|
20563
|
+
vercelTrafficCredentialStore: opts.vercelTrafficCredentialStore,
|
|
20564
|
+
pullVercelTrafficEvents: opts.pullVercelTrafficEvents,
|
|
20172
20565
|
onTrafficSynced: opts.onTrafficSynced
|
|
20173
20566
|
});
|
|
20174
20567
|
await api.register(backlinksRoutes, {
|
|
@@ -20282,6 +20675,54 @@ function buildTrafficSourceValidators(opts) {
|
|
|
20282
20675
|
validateScopes: () => null
|
|
20283
20676
|
};
|
|
20284
20677
|
}
|
|
20678
|
+
if (opts.vercelTrafficCredentialStore) {
|
|
20679
|
+
const store = opts.vercelTrafficCredentialStore;
|
|
20680
|
+
const pullEvents = opts.pullVercelTrafficEvents ?? listVercelTrafficEvents;
|
|
20681
|
+
validators[TrafficSourceTypes.vercel] = {
|
|
20682
|
+
validateCredentials: async (source) => {
|
|
20683
|
+
const record = store.getConnection(source.projectName);
|
|
20684
|
+
if (!record) {
|
|
20685
|
+
return {
|
|
20686
|
+
status: CheckStatuses.fail,
|
|
20687
|
+
code: "traffic.credentials.missing",
|
|
20688
|
+
summary: `No Vercel credential found in ~/.canonry/config.yaml for project "${source.projectName}".`,
|
|
20689
|
+
remediation: "Re-run `canonry traffic connect vercel <project> --project-id <prj> --team-id <team> --token <token>`."
|
|
20690
|
+
};
|
|
20691
|
+
}
|
|
20692
|
+
try {
|
|
20693
|
+
const probeEnd = Date.now();
|
|
20694
|
+
await pullEvents({
|
|
20695
|
+
token: record.token,
|
|
20696
|
+
projectId: record.projectId,
|
|
20697
|
+
teamId: record.teamId,
|
|
20698
|
+
environment: record.environment,
|
|
20699
|
+
startDate: probeEnd - 60 * 6e4,
|
|
20700
|
+
endDate: probeEnd,
|
|
20701
|
+
maxPages: 1
|
|
20702
|
+
});
|
|
20703
|
+
return {
|
|
20704
|
+
status: CheckStatuses.ok,
|
|
20705
|
+
code: "traffic.credentials.resolved",
|
|
20706
|
+
summary: `Vercel request-logs responds for "${source.displayName}" (project ${record.projectId}).`
|
|
20707
|
+
};
|
|
20708
|
+
} catch (e) {
|
|
20709
|
+
const httpStatus = e instanceof VercelLogsApiError ? e.status : null;
|
|
20710
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
20711
|
+
return {
|
|
20712
|
+
status: CheckStatuses.fail,
|
|
20713
|
+
code: httpStatus === 401 || httpStatus === 403 ? "traffic.credentials.unauthorized" : "traffic.credentials.resolve-failed",
|
|
20714
|
+
summary: httpStatus ? `Vercel request-logs returned HTTP ${httpStatus}: ${msg}.` : `Vercel request-logs probe failed: ${msg}.`,
|
|
20715
|
+
remediation: "Verify the Vercel API token is unexpired and the project / team ids are correct. Vercel tokens can expire \u2014 re-connect the source with a fresh token if needed."
|
|
20716
|
+
};
|
|
20717
|
+
}
|
|
20718
|
+
},
|
|
20719
|
+
// Vercel API tokens have no granular per-resource scopes — a token
|
|
20720
|
+
// inherits the user's team access, so there is no "missing scope"
|
|
20721
|
+
// failure mode. Surface a skipped result so the framework stays
|
|
20722
|
+
// uniform without producing a false signal.
|
|
20723
|
+
validateScopes: () => null
|
|
20724
|
+
};
|
|
20725
|
+
}
|
|
20285
20726
|
return Object.keys(validators).length > 0 ? validators : void 0;
|
|
20286
20727
|
}
|
|
20287
20728
|
|
|
@@ -22799,8 +23240,40 @@ function removeWordpressTrafficConnection(config, projectName) {
|
|
|
22799
23240
|
return true;
|
|
22800
23241
|
}
|
|
22801
23242
|
|
|
22802
|
-
// src/
|
|
23243
|
+
// src/vercel-traffic-config.ts
|
|
22803
23244
|
function ensureConnections5(config) {
|
|
23245
|
+
if (!config.vercelTraffic) config.vercelTraffic = {};
|
|
23246
|
+
if (!config.vercelTraffic.connections) config.vercelTraffic.connections = [];
|
|
23247
|
+
return config.vercelTraffic.connections;
|
|
23248
|
+
}
|
|
23249
|
+
function getVercelTrafficConnection(config, projectName) {
|
|
23250
|
+
return (config.vercelTraffic?.connections ?? []).find((c) => c.projectName === projectName);
|
|
23251
|
+
}
|
|
23252
|
+
function upsertVercelTrafficConnection(config, connection) {
|
|
23253
|
+
const connections = ensureConnections5(config);
|
|
23254
|
+
const index = connections.findIndex((c) => c.projectName === connection.projectName);
|
|
23255
|
+
if (index === -1) {
|
|
23256
|
+
connections.push(connection);
|
|
23257
|
+
return connection;
|
|
23258
|
+
}
|
|
23259
|
+
connections[index] = connection;
|
|
23260
|
+
return connection;
|
|
23261
|
+
}
|
|
23262
|
+
function removeVercelTrafficConnection(config, projectName) {
|
|
23263
|
+
const connections = config.vercelTraffic?.connections;
|
|
23264
|
+
if (!connections?.length) return false;
|
|
23265
|
+
const next = connections.filter((c) => c.projectName !== projectName);
|
|
23266
|
+
if (next.length === connections.length) return false;
|
|
23267
|
+
if (!config.vercelTraffic) return false;
|
|
23268
|
+
config.vercelTraffic.connections = next;
|
|
23269
|
+
if (next.length === 0) {
|
|
23270
|
+
delete config.vercelTraffic;
|
|
23271
|
+
}
|
|
23272
|
+
return true;
|
|
23273
|
+
}
|
|
23274
|
+
|
|
23275
|
+
// src/wordpress-config.ts
|
|
23276
|
+
function ensureConnections6(config) {
|
|
22804
23277
|
if (!config.wordpress) config.wordpress = {};
|
|
22805
23278
|
if (!config.wordpress.connections) config.wordpress.connections = [];
|
|
22806
23279
|
return config.wordpress.connections;
|
|
@@ -22817,7 +23290,7 @@ function getWordpressConnection(config, projectName) {
|
|
|
22817
23290
|
return (config.wordpress?.connections ?? []).find((connection) => connection.projectName === projectName);
|
|
22818
23291
|
}
|
|
22819
23292
|
function upsertWordpressConnection(config, connection) {
|
|
22820
|
-
const connections =
|
|
23293
|
+
const connections = ensureConnections6(config);
|
|
22821
23294
|
const normalized = normalizeConnection(connection);
|
|
22822
23295
|
const index = connections.findIndex((entry) => entry.projectName === connection.projectName);
|
|
22823
23296
|
if (index === -1) {
|
|
@@ -24523,6 +24996,7 @@ async function executeDiscoveryRun(opts) {
|
|
|
24523
24996
|
icpDescription: opts.icpDescription,
|
|
24524
24997
|
dedupThreshold: opts.dedupThreshold,
|
|
24525
24998
|
maxProbes: opts.maxProbes,
|
|
24999
|
+
locations: opts.locations,
|
|
24526
25000
|
deps
|
|
24527
25001
|
});
|
|
24528
25002
|
writeDiscoveryInsight(opts.db, {
|
|
@@ -24680,12 +25154,32 @@ function extractClassificationCategory(line) {
|
|
|
24680
25154
|
}
|
|
24681
25155
|
return null;
|
|
24682
25156
|
}
|
|
25157
|
+
function formatLocationLine(location) {
|
|
25158
|
+
return [location.city, location.region, location.country].map((part) => part.trim()).filter(Boolean).join(", ");
|
|
25159
|
+
}
|
|
25160
|
+
function buildLocationConstraint(locations) {
|
|
25161
|
+
if (locations.length === 0) return [];
|
|
25162
|
+
const formatted = locations.map(formatLocationLine);
|
|
25163
|
+
if (locations.length === 1) {
|
|
25164
|
+
return [
|
|
25165
|
+
`The business serves ${formatted[0]}. Every query must be relevant to that service area \u2014 work the city or region into the query the way a real searcher would.`
|
|
25166
|
+
];
|
|
25167
|
+
}
|
|
25168
|
+
const perLocation = Math.max(1, Math.floor(DEFAULT_SEED_COUNT / locations.length));
|
|
25169
|
+
return [
|
|
25170
|
+
"The business serves these locations:",
|
|
25171
|
+
...formatted.map((line) => ` - ${line}`),
|
|
25172
|
+
`Generate at least ${perLocation} queries for EACH service area listed above so coverage stays balanced \u2014 do not let one area dominate. Every query must be relevant to at least one of these service areas, working the city or region into the query the way a real searcher would.`
|
|
25173
|
+
];
|
|
25174
|
+
}
|
|
24683
25175
|
function buildSeedPrompt(input) {
|
|
25176
|
+
const locationConstraint = buildLocationConstraint(input.locations ?? []);
|
|
24684
25177
|
return [
|
|
24685
25178
|
"You are an AEO (Answer Engine Optimization) analyst expanding a tracked-query basket for a customer.",
|
|
24686
25179
|
"",
|
|
24687
25180
|
`Customer: ${input.project.name} (domains: ${input.project.canonicalDomains.join(", ")})`,
|
|
24688
25181
|
`ICP: ${input.icpDescription}`,
|
|
25182
|
+
...locationConstraint.length > 0 ? ["", ...locationConstraint] : [],
|
|
24689
25183
|
"",
|
|
24690
25184
|
"Brainstorm a wide set of queries a member of this ICP would type into an AI answer engine (Gemini, ChatGPT, Perplexity) when they are about to make a decision in this space. Aim for 30+ candidates covering:",
|
|
24691
25185
|
' - Comparison queries ("best X for Y")',
|
|
@@ -27530,6 +28024,21 @@ async function createServer(opts) {
|
|
|
27530
28024
|
return removed;
|
|
27531
28025
|
}
|
|
27532
28026
|
};
|
|
28027
|
+
const vercelTrafficCredentialStore = {
|
|
28028
|
+
getConnection: (projectName) => {
|
|
28029
|
+
return getVercelTrafficConnection(opts.config, projectName);
|
|
28030
|
+
},
|
|
28031
|
+
upsertConnection: (record) => {
|
|
28032
|
+
const updated = upsertVercelTrafficConnection(opts.config, record);
|
|
28033
|
+
saveConfigPatch(opts.config);
|
|
28034
|
+
return updated;
|
|
28035
|
+
},
|
|
28036
|
+
deleteConnection: (projectName) => {
|
|
28037
|
+
const removed = removeVercelTrafficConnection(opts.config, projectName);
|
|
28038
|
+
if (removed) saveConfigPatch(opts.config);
|
|
28039
|
+
return removed;
|
|
28040
|
+
}
|
|
28041
|
+
};
|
|
27533
28042
|
const googleStateSecret = process.env.GOOGLE_STATE_SECRET ?? crypto34.randomBytes(32).toString("hex");
|
|
27534
28043
|
const googleConnectionStore = {
|
|
27535
28044
|
listConnections: (domain) => listGoogleConnections(opts.config, domain),
|
|
@@ -27832,7 +28341,8 @@ async function createServer(opts) {
|
|
|
27832
28341
|
projectId: input.projectId,
|
|
27833
28342
|
icpDescription: input.icpDescription,
|
|
27834
28343
|
dedupThreshold: input.dedupThreshold,
|
|
27835
|
-
maxProbes: input.maxProbes
|
|
28344
|
+
maxProbes: input.maxProbes,
|
|
28345
|
+
locations: input.locations
|
|
27836
28346
|
}).then(() => runCoordinator.onRunCompleted(input.runId, input.projectId)).catch((err) => {
|
|
27837
28347
|
app.log.error({ runId: input.runId, err }, "Discovery run failed");
|
|
27838
28348
|
});
|
|
@@ -27889,6 +28399,7 @@ async function createServer(opts) {
|
|
|
27889
28399
|
ga4CredentialStore,
|
|
27890
28400
|
cloudRunCredentialStore,
|
|
27891
28401
|
wordpressTrafficCredentialStore,
|
|
28402
|
+
vercelTrafficCredentialStore,
|
|
27892
28403
|
onTrafficSynced: (event) => {
|
|
27893
28404
|
trackEvent("traffic.synced", {
|
|
27894
28405
|
status: event.status,
|