@ainyc/canonry 4.21.4 → 4.23.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/assets/{index-CYfF3BeK.js → index-qGDEhNJu.js} +1 -1
- package/assets/index.html +1 -1
- package/dist/{chunk-3UGJUNQX.js → chunk-CLVZF3X7.js} +428 -51
- package/dist/{chunk-EY63PENL.js → chunk-EUGCQSFC.js} +12 -0
- package/dist/{chunk-GVQYROIK.js → chunk-OYYFXKRK.js} +1 -1
- package/dist/{chunk-VFKGHXVJ.js → chunk-VOSBGXXG.js} +24 -1
- package/dist/cli.js +104 -6
- package/dist/index.d.ts +17 -0
- package/dist/index.js +4 -4
- package/dist/{intelligence-service-5COCQKXG.js → intelligence-service-NVN2PAR7.js} +2 -2
- package/dist/mcp.js +2 -2
- package/package.json +6 -6
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
loadConfig,
|
|
6
6
|
loadConfigRaw,
|
|
7
7
|
saveConfigPatch
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-VOSBGXXG.js";
|
|
9
9
|
import {
|
|
10
10
|
DEFAULT_RUN_HISTORY_LIMIT,
|
|
11
11
|
IntelligenceService,
|
|
@@ -66,7 +66,7 @@ import {
|
|
|
66
66
|
schedules,
|
|
67
67
|
trafficSources,
|
|
68
68
|
usageCounters
|
|
69
|
-
} from "./chunk-
|
|
69
|
+
} from "./chunk-OYYFXKRK.js";
|
|
70
70
|
import {
|
|
71
71
|
AGENT_MEMORY_VALUE_MAX_BYTES,
|
|
72
72
|
AGENT_PROVIDER_IDS,
|
|
@@ -154,12 +154,13 @@ import {
|
|
|
154
154
|
serializeRunError,
|
|
155
155
|
snapshotRequestSchema,
|
|
156
156
|
summarizeCheckResults,
|
|
157
|
+
trafficConnectWordpressRequestSchema,
|
|
157
158
|
unsupportedKind,
|
|
158
159
|
validationError,
|
|
159
160
|
visibilityStateFromAnswerMentioned,
|
|
160
161
|
windowCutoff,
|
|
161
162
|
wordpressEnvSchema
|
|
162
|
-
} from "./chunk-
|
|
163
|
+
} from "./chunk-EUGCQSFC.js";
|
|
163
164
|
|
|
164
165
|
// src/telemetry.ts
|
|
165
166
|
import crypto from "crypto";
|
|
@@ -10007,6 +10008,37 @@ var routeCatalog = [
|
|
|
10007
10008
|
404: { description: "Project not found." }
|
|
10008
10009
|
}
|
|
10009
10010
|
},
|
|
10011
|
+
{
|
|
10012
|
+
method: "post",
|
|
10013
|
+
path: "/api/v1/projects/{name}/traffic/connect/wordpress",
|
|
10014
|
+
summary: "Connect a WordPress traffic-logger source",
|
|
10015
|
+
description: "Probes the WordPress traffic-logger plugin endpoint with the supplied Application Password (single page, `limit=1`) before persisting. On success, stores the credential in `~/.canonry/config.yaml` and creates / updates the project's active WordPress `traffic_sources` row. A probe failure (HTTP 4xx/5xx, network error) surfaces as 502 with the upstream status in the message so the caller learns about a bad credential up front instead of at the first sync.",
|
|
10016
|
+
tags: ["traffic"],
|
|
10017
|
+
parameters: [nameParameter],
|
|
10018
|
+
requestBody: {
|
|
10019
|
+
required: true,
|
|
10020
|
+
content: {
|
|
10021
|
+
"application/json": {
|
|
10022
|
+
schema: {
|
|
10023
|
+
type: "object",
|
|
10024
|
+
required: ["baseUrl", "username", "applicationPassword"],
|
|
10025
|
+
properties: {
|
|
10026
|
+
baseUrl: { ...stringSchema, description: "Absolute base URL of the WordPress site (e.g. `https://example.com`)." },
|
|
10027
|
+
username: { ...stringSchema, description: "WordPress username paired with the Application Password." },
|
|
10028
|
+
applicationPassword: { ...stringSchema, description: "WordPress Application Password (raw; the server base64-encodes it for Basic auth)." },
|
|
10029
|
+
displayName: stringSchema
|
|
10030
|
+
}
|
|
10031
|
+
}
|
|
10032
|
+
}
|
|
10033
|
+
}
|
|
10034
|
+
},
|
|
10035
|
+
responses: {
|
|
10036
|
+
200: { description: "Traffic source DTO returned." },
|
|
10037
|
+
400: { description: "Invalid WordPress connection request." },
|
|
10038
|
+
404: { description: "Project not found." },
|
|
10039
|
+
502: { description: "WordPress plugin endpoint probe failed (bad credentials, unreachable host, etc.)." }
|
|
10040
|
+
}
|
|
10041
|
+
},
|
|
10010
10042
|
{
|
|
10011
10043
|
method: "post",
|
|
10012
10044
|
path: "/api/v1/projects/{name}/traffic/sources/{id}/sync",
|
|
@@ -16779,6 +16811,12 @@ function hostMatches(host, domain) {
|
|
|
16779
16811
|
const normalizedDomain = normalizeHost(domain);
|
|
16780
16812
|
return normalizedHost === normalizedDomain || normalizedHost.endsWith(`.${normalizedDomain}`);
|
|
16781
16813
|
}
|
|
16814
|
+
function utmTokenMatchesDomain(utmSource, domain) {
|
|
16815
|
+
if (hostMatches(utmSource, domain)) return true;
|
|
16816
|
+
const normalizedUtm = normalizeHost(utmSource);
|
|
16817
|
+
const firstLabel = normalizeHost(domain).split(".")[0];
|
|
16818
|
+
return Boolean(firstLabel) && normalizedUtm === firstLabel;
|
|
16819
|
+
}
|
|
16782
16820
|
function hostFromUrl(value) {
|
|
16783
16821
|
if (!value) return null;
|
|
16784
16822
|
try {
|
|
@@ -16833,7 +16871,7 @@ function classifyAiReferral(event) {
|
|
|
16833
16871
|
}
|
|
16834
16872
|
const utmSource = utmSourceFromQuery(event.queryString);
|
|
16835
16873
|
if (utmSource) {
|
|
16836
|
-
const rule = DEFAULT_AI_REFERRER_RULES.find((candidate) =>
|
|
16874
|
+
const rule = DEFAULT_AI_REFERRER_RULES.find((candidate) => utmTokenMatchesDomain(utmSource, candidate.domain));
|
|
16837
16875
|
if (rule) {
|
|
16838
16876
|
return {
|
|
16839
16877
|
operator: rule.operator,
|
|
@@ -16845,7 +16883,7 @@ function classifyAiReferral(event) {
|
|
|
16845
16883
|
}
|
|
16846
16884
|
const refererUtmSource = utmSourceFromUrl(event.referer);
|
|
16847
16885
|
if (refererUtmSource) {
|
|
16848
|
-
const rule = DEFAULT_AI_REFERRER_RULES.find((candidate) =>
|
|
16886
|
+
const rule = DEFAULT_AI_REFERRER_RULES.find((candidate) => utmTokenMatchesDomain(refererUtmSource, candidate.domain));
|
|
16849
16887
|
if (rule) {
|
|
16850
16888
|
return {
|
|
16851
16889
|
operator: rule.operator,
|
|
@@ -16971,20 +17009,19 @@ function buildTrafficProbeReport(events, options = {}) {
|
|
|
16971
17009
|
incrementBucket(topAiReferralLandingPaths, pathNormalized, { landingPathNormalized: pathNormalized });
|
|
16972
17010
|
}
|
|
16973
17011
|
if (!crawler && !aiReferral) unknownHits += 1;
|
|
16974
|
-
|
|
16975
|
-
|
|
16976
|
-
|
|
16977
|
-
|
|
16978
|
-
|
|
16979
|
-
|
|
16980
|
-
|
|
16981
|
-
|
|
16982
|
-
|
|
16983
|
-
|
|
16984
|
-
|
|
16985
|
-
|
|
16986
|
-
|
|
16987
|
-
}
|
|
17012
|
+
samples.push({
|
|
17013
|
+
eventId: event.eventId,
|
|
17014
|
+
observedAt: event.observedAt,
|
|
17015
|
+
sourceType: event.sourceType,
|
|
17016
|
+
path: event.path,
|
|
17017
|
+
pathNormalized,
|
|
17018
|
+
status: event.status,
|
|
17019
|
+
userAgent: event.userAgent,
|
|
17020
|
+
referer: event.referer,
|
|
17021
|
+
crawler,
|
|
17022
|
+
aiReferral
|
|
17023
|
+
});
|
|
17024
|
+
if (samples.length > sampleLimit) samples.shift();
|
|
16988
17025
|
}
|
|
16989
17026
|
return {
|
|
16990
17027
|
generatedAt: options.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -17009,10 +17046,155 @@ function incrementBucket(map, key, fields) {
|
|
|
17009
17046
|
else map.set(key, { fields, hits: 1 });
|
|
17010
17047
|
}
|
|
17011
17048
|
|
|
17049
|
+
// ../integration-wordpress-traffic/src/normalize.ts
|
|
17050
|
+
function trimOrNull(value) {
|
|
17051
|
+
if (value === null || value === void 0) return null;
|
|
17052
|
+
const trimmed = value.trim();
|
|
17053
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
17054
|
+
}
|
|
17055
|
+
function buildEventId2(event) {
|
|
17056
|
+
return `wordpress:${event.observed_at}:${event.id}`;
|
|
17057
|
+
}
|
|
17058
|
+
function normalizeWordpressTrafficEvent(event) {
|
|
17059
|
+
if (!event.observed_at) return null;
|
|
17060
|
+
if (typeof event.id !== "number" || !Number.isFinite(event.id)) return null;
|
|
17061
|
+
const path15 = event.path?.trim();
|
|
17062
|
+
if (!path15) return null;
|
|
17063
|
+
const queryString = trimOrNull(event.query_string);
|
|
17064
|
+
const host = trimOrNull(event.host);
|
|
17065
|
+
const requestUrl = host ? `https://${host}${path15}${queryString ? `?${queryString}` : ""}` : `${path15}${queryString ? `?${queryString}` : ""}`;
|
|
17066
|
+
return {
|
|
17067
|
+
sourceType: TrafficSourceTypes.wordpress,
|
|
17068
|
+
evidenceKind: TrafficEvidenceKinds["raw-request"],
|
|
17069
|
+
confidence: TrafficEventConfidences.observed,
|
|
17070
|
+
eventId: buildEventId2(event),
|
|
17071
|
+
observedAt: event.observed_at,
|
|
17072
|
+
method: trimOrNull(event.method),
|
|
17073
|
+
requestUrl,
|
|
17074
|
+
host,
|
|
17075
|
+
path: path15,
|
|
17076
|
+
queryString,
|
|
17077
|
+
status: typeof event.status === "number" && Number.isFinite(event.status) ? event.status : null,
|
|
17078
|
+
userAgent: trimOrNull(event.user_agent),
|
|
17079
|
+
remoteIp: trimOrNull(event.remote_ip_hash),
|
|
17080
|
+
referer: trimOrNull(event.referer),
|
|
17081
|
+
latencyMs: null,
|
|
17082
|
+
requestSizeBytes: null,
|
|
17083
|
+
responseSizeBytes: null,
|
|
17084
|
+
providerResource: {
|
|
17085
|
+
type: "wordpress_site",
|
|
17086
|
+
labels: host ? { host } : {}
|
|
17087
|
+
},
|
|
17088
|
+
providerLabels: {}
|
|
17089
|
+
};
|
|
17090
|
+
}
|
|
17091
|
+
|
|
17092
|
+
// ../integration-wordpress-traffic/src/client.ts
|
|
17093
|
+
var WORDPRESS_TRAFFIC_ENDPOINT_PATH = "/wp-json/canonry/v1/events";
|
|
17094
|
+
var DEFAULT_PAGE_SIZE2 = 500;
|
|
17095
|
+
var DEFAULT_MAX_PAGES2 = 1;
|
|
17096
|
+
var DEFAULT_TIMEOUT_MS2 = 3e4;
|
|
17097
|
+
var WordpressTrafficApiError = class extends Error {
|
|
17098
|
+
constructor(message, status, body) {
|
|
17099
|
+
super(message);
|
|
17100
|
+
this.status = status;
|
|
17101
|
+
this.body = body;
|
|
17102
|
+
this.name = "WordpressTrafficApiError";
|
|
17103
|
+
}
|
|
17104
|
+
};
|
|
17105
|
+
function trimRequired(name, value) {
|
|
17106
|
+
const trimmed = value.trim();
|
|
17107
|
+
if (!trimmed) {
|
|
17108
|
+
throw new WordpressTrafficApiError(`${name} is required`, 400);
|
|
17109
|
+
}
|
|
17110
|
+
return trimmed;
|
|
17111
|
+
}
|
|
17112
|
+
function normalizePageSize2(pageSize) {
|
|
17113
|
+
if (pageSize === void 0) return DEFAULT_PAGE_SIZE2;
|
|
17114
|
+
if (!Number.isInteger(pageSize) || pageSize < 1) {
|
|
17115
|
+
throw new WordpressTrafficApiError("pageSize must be a positive integer", 400);
|
|
17116
|
+
}
|
|
17117
|
+
return pageSize;
|
|
17118
|
+
}
|
|
17119
|
+
function normalizeMaxPages2(maxPages) {
|
|
17120
|
+
if (maxPages === void 0) return DEFAULT_MAX_PAGES2;
|
|
17121
|
+
if (!Number.isInteger(maxPages) || maxPages < 1) {
|
|
17122
|
+
throw new WordpressTrafficApiError("maxPages must be a positive integer", 400);
|
|
17123
|
+
}
|
|
17124
|
+
return maxPages;
|
|
17125
|
+
}
|
|
17126
|
+
function resolveEndpoint(baseUrl) {
|
|
17127
|
+
const trimmed = trimRequired("baseUrl", baseUrl).replace(/\/+$/, "");
|
|
17128
|
+
return `${trimmed}${WORDPRESS_TRAFFIC_ENDPOINT_PATH}`;
|
|
17129
|
+
}
|
|
17130
|
+
function buildBasicAuthHeader(username, applicationPassword) {
|
|
17131
|
+
const credentials = `${trimRequired("username", username)}:${trimRequired("applicationPassword", applicationPassword)}`;
|
|
17132
|
+
return `Basic ${Buffer.from(credentials, "utf8").toString("base64")}`;
|
|
17133
|
+
}
|
|
17134
|
+
async function readErrorBody2(response) {
|
|
17135
|
+
const text = await response.text().catch(() => "");
|
|
17136
|
+
if (!text) return void 0;
|
|
17137
|
+
return text.length <= 500 ? text : `${text.slice(0, 500)}... [truncated]`;
|
|
17138
|
+
}
|
|
17139
|
+
async function listWordpressTrafficEvents(options) {
|
|
17140
|
+
const endpoint = resolveEndpoint(options.baseUrl);
|
|
17141
|
+
const authHeader = buildBasicAuthHeader(options.username, options.applicationPassword);
|
|
17142
|
+
const pageSize = normalizePageSize2(options.pageSize);
|
|
17143
|
+
const maxPages = normalizeMaxPages2(options.maxPages);
|
|
17144
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
|
|
17145
|
+
let cursor = options.cursor;
|
|
17146
|
+
let rawEntryCount = 0;
|
|
17147
|
+
let skippedEntryCount = 0;
|
|
17148
|
+
const events = [];
|
|
17149
|
+
for (let page = 0; page < maxPages; page += 1) {
|
|
17150
|
+
const url = new URL(endpoint);
|
|
17151
|
+
url.searchParams.set("limit", String(pageSize));
|
|
17152
|
+
if (cursor !== void 0 && cursor !== "") {
|
|
17153
|
+
url.searchParams.set("cursor", cursor);
|
|
17154
|
+
}
|
|
17155
|
+
const response = await fetch(url, {
|
|
17156
|
+
method: "GET",
|
|
17157
|
+
headers: {
|
|
17158
|
+
Authorization: authHeader,
|
|
17159
|
+
Accept: "application/json"
|
|
17160
|
+
},
|
|
17161
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
17162
|
+
});
|
|
17163
|
+
if (!response.ok) {
|
|
17164
|
+
const body2 = await readErrorBody2(response);
|
|
17165
|
+
throw new WordpressTrafficApiError(
|
|
17166
|
+
`WordPress traffic endpoint returned HTTP ${response.status}`,
|
|
17167
|
+
response.status,
|
|
17168
|
+
body2
|
|
17169
|
+
);
|
|
17170
|
+
}
|
|
17171
|
+
const body = await response.json();
|
|
17172
|
+
const entries = body.events ?? [];
|
|
17173
|
+
rawEntryCount += entries.length;
|
|
17174
|
+
for (const entry of entries) {
|
|
17175
|
+
const normalized = normalizeWordpressTrafficEvent(entry);
|
|
17176
|
+
if (normalized) {
|
|
17177
|
+
events.push(normalized);
|
|
17178
|
+
} else {
|
|
17179
|
+
skippedEntryCount += 1;
|
|
17180
|
+
}
|
|
17181
|
+
}
|
|
17182
|
+
cursor = body.next_cursor ?? void 0;
|
|
17183
|
+
if (!body.has_more || !cursor) break;
|
|
17184
|
+
}
|
|
17185
|
+
return {
|
|
17186
|
+
events,
|
|
17187
|
+
rawEntryCount,
|
|
17188
|
+
skippedEntryCount,
|
|
17189
|
+
nextCursor: cursor,
|
|
17190
|
+
endpoint
|
|
17191
|
+
};
|
|
17192
|
+
}
|
|
17193
|
+
|
|
17012
17194
|
// ../api-routes/src/traffic.ts
|
|
17013
17195
|
var DEFAULT_SYNC_WINDOW_MINUTES = 43200;
|
|
17014
|
-
var
|
|
17015
|
-
var
|
|
17196
|
+
var DEFAULT_PAGE_SIZE3 = 1e3;
|
|
17197
|
+
var DEFAULT_MAX_PAGES3 = 5;
|
|
17016
17198
|
var DEFAULT_SAMPLE_LIMIT2 = 100;
|
|
17017
17199
|
var MAX_TRACKED_EVENT_IDS = 1e3;
|
|
17018
17200
|
var DEFAULT_BACKFILL_DAYS = 30;
|
|
@@ -17089,7 +17271,7 @@ async function runBackfillTask(options) {
|
|
|
17089
17271
|
location,
|
|
17090
17272
|
startTime: windowStart.toISOString(),
|
|
17091
17273
|
endTime: windowEnd.toISOString(),
|
|
17092
|
-
pageSize:
|
|
17274
|
+
pageSize: DEFAULT_PAGE_SIZE3,
|
|
17093
17275
|
maxPages: BACKFILL_MAX_PAGES,
|
|
17094
17276
|
// Backfill is intentionally `firstSync: false`. We don't want desc
|
|
17095
17277
|
// ordering — the in-memory rollup builder handles any order, and the
|
|
@@ -17118,7 +17300,7 @@ async function runBackfillTask(options) {
|
|
|
17118
17300
|
const newSorted = allEvents.slice().sort((a, b) => a.observedAt < b.observedAt ? 1 : a.observedAt > b.observedAt ? -1 : 0).map((e) => e.eventId);
|
|
17119
17301
|
const newRingBuffer = newSorted.slice(0, MAX_TRACKED_EVENT_IDS);
|
|
17120
17302
|
const currentLastSyncedMs = sourceRow.lastSyncedAt ? new Date(sourceRow.lastSyncedAt).getTime() : Number.NEGATIVE_INFINITY;
|
|
17121
|
-
const nextLastSyncedAt = Math.max(currentLastSyncedMs, windowEnd.getTime()) === windowEnd.getTime() ?
|
|
17303
|
+
const nextLastSyncedAt = Math.max(currentLastSyncedMs, windowEnd.getTime()) === windowEnd.getTime() ? windowEndIso : sourceRow.lastSyncedAt;
|
|
17122
17304
|
try {
|
|
17123
17305
|
app.db.transaction((tx) => {
|
|
17124
17306
|
tx.delete(crawlerEventsHourly).where(
|
|
@@ -17219,9 +17401,10 @@ async function runBackfillTask(options) {
|
|
|
17219
17401
|
async function trafficRoutes(app, opts) {
|
|
17220
17402
|
const pullEvents = opts.pullCloudRunEvents ?? listCloudRunTrafficEvents;
|
|
17221
17403
|
const resolveAccessToken2 = opts.resolveCloudRunAccessToken ?? defaultResolveAccessToken;
|
|
17404
|
+
const pullWordpressEvents = opts.pullWordpressTrafficEvents ?? listWordpressTrafficEvents;
|
|
17222
17405
|
const syncWindowMinutes = opts.defaultSyncWindowMinutes ?? DEFAULT_SYNC_WINDOW_MINUTES;
|
|
17223
|
-
const pageSize = opts.defaultPageSize ??
|
|
17224
|
-
const maxPages = opts.defaultMaxPages ??
|
|
17406
|
+
const pageSize = opts.defaultPageSize ?? DEFAULT_PAGE_SIZE3;
|
|
17407
|
+
const maxPages = opts.defaultMaxPages ?? DEFAULT_MAX_PAGES3;
|
|
17225
17408
|
const sampleLimit = opts.defaultSampleLimit ?? DEFAULT_SAMPLE_LIMIT2;
|
|
17226
17409
|
app.post("/projects/:name/traffic/connect/cloud-run", async (request) => {
|
|
17227
17410
|
const project = resolveProject(app.db, request.params.name);
|
|
@@ -17305,6 +17488,84 @@ async function trafficRoutes(app, opts) {
|
|
|
17305
17488
|
});
|
|
17306
17489
|
return rowToDto(sourceRow);
|
|
17307
17490
|
});
|
|
17491
|
+
app.post("/projects/:name/traffic/connect/wordpress", async (request) => {
|
|
17492
|
+
const project = resolveProject(app.db, request.params.name);
|
|
17493
|
+
if (!opts.wordpressTrafficCredentialStore) {
|
|
17494
|
+
throw validationError("WordPress traffic credential storage is not configured for this deployment");
|
|
17495
|
+
}
|
|
17496
|
+
const credentialStore = opts.wordpressTrafficCredentialStore;
|
|
17497
|
+
const parsed = trafficConnectWordpressRequestSchema.safeParse(request.body ?? {});
|
|
17498
|
+
if (!parsed.success) {
|
|
17499
|
+
throw validationError(parsed.error.issues.map((i) => i.message).join("; "));
|
|
17500
|
+
}
|
|
17501
|
+
const { baseUrl, username, applicationPassword, displayName } = parsed.data;
|
|
17502
|
+
try {
|
|
17503
|
+
await pullWordpressEvents({
|
|
17504
|
+
baseUrl,
|
|
17505
|
+
username,
|
|
17506
|
+
applicationPassword,
|
|
17507
|
+
pageSize: 1,
|
|
17508
|
+
maxPages: 1
|
|
17509
|
+
});
|
|
17510
|
+
} catch (e) {
|
|
17511
|
+
if (e instanceof WordpressTrafficApiError) {
|
|
17512
|
+
throw providerError(
|
|
17513
|
+
`WordPress traffic probe failed (HTTP ${e.status}): ${e.message}${e.body ? ` \u2014 ${e.body}` : ""}`
|
|
17514
|
+
);
|
|
17515
|
+
}
|
|
17516
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
17517
|
+
throw providerError(`WordPress traffic probe failed: ${msg}`);
|
|
17518
|
+
}
|
|
17519
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
17520
|
+
const existing = credentialStore.getConnection(project.name);
|
|
17521
|
+
credentialStore.upsertConnection({
|
|
17522
|
+
projectName: project.name,
|
|
17523
|
+
baseUrl,
|
|
17524
|
+
username,
|
|
17525
|
+
applicationPassword,
|
|
17526
|
+
createdAt: existing?.createdAt ?? now,
|
|
17527
|
+
updatedAt: now
|
|
17528
|
+
});
|
|
17529
|
+
const activeSource = app.db.select().from(trafficSources).where(eq23(trafficSources.projectId, project.id)).all().find((row) => row.sourceType === TrafficSourceTypes.wordpress && row.status !== TrafficSourceStatuses.archived);
|
|
17530
|
+
const config = { baseUrl, username };
|
|
17531
|
+
const fallbackName = displayName ?? `WordPress \xB7 ${new URL(baseUrl).host}`;
|
|
17532
|
+
let sourceRow;
|
|
17533
|
+
if (activeSource) {
|
|
17534
|
+
app.db.update(trafficSources).set({
|
|
17535
|
+
displayName: fallbackName,
|
|
17536
|
+
status: TrafficSourceStatuses.connected,
|
|
17537
|
+
lastError: null,
|
|
17538
|
+
configJson: JSON.stringify(config),
|
|
17539
|
+
updatedAt: now
|
|
17540
|
+
}).where(eq23(trafficSources.id, activeSource.id)).run();
|
|
17541
|
+
sourceRow = app.db.select().from(trafficSources).where(eq23(trafficSources.id, activeSource.id)).get();
|
|
17542
|
+
} else {
|
|
17543
|
+
const newId = crypto20.randomUUID();
|
|
17544
|
+
app.db.insert(trafficSources).values({
|
|
17545
|
+
id: newId,
|
|
17546
|
+
projectId: project.id,
|
|
17547
|
+
sourceType: TrafficSourceTypes.wordpress,
|
|
17548
|
+
displayName: fallbackName,
|
|
17549
|
+
status: TrafficSourceStatuses.connected,
|
|
17550
|
+
lastSyncedAt: null,
|
|
17551
|
+
lastCursor: null,
|
|
17552
|
+
lastError: null,
|
|
17553
|
+
archivedAt: null,
|
|
17554
|
+
configJson: JSON.stringify(config),
|
|
17555
|
+
createdAt: now,
|
|
17556
|
+
updatedAt: now
|
|
17557
|
+
}).run();
|
|
17558
|
+
sourceRow = app.db.select().from(trafficSources).where(eq23(trafficSources.id, newId)).get();
|
|
17559
|
+
}
|
|
17560
|
+
writeAuditLog(app.db, {
|
|
17561
|
+
projectId: project.id,
|
|
17562
|
+
actor: "api",
|
|
17563
|
+
action: "traffic.wordpress.connected",
|
|
17564
|
+
entityType: "traffic_source",
|
|
17565
|
+
entityId: sourceRow.id
|
|
17566
|
+
});
|
|
17567
|
+
return rowToDto(sourceRow);
|
|
17568
|
+
});
|
|
17308
17569
|
app.post("/projects/:name/traffic/sources/:id/sync", async (request) => {
|
|
17309
17570
|
const project = resolveProject(app.db, request.params.name);
|
|
17310
17571
|
const sourceRow = app.db.select().from(trafficSources).where(eq23(trafficSources.id, request.params.id)).get();
|
|
@@ -17398,25 +17659,35 @@ async function trafficRoutes(app, opts) {
|
|
|
17398
17659
|
markFailed(msg, "PROVIDER_PULL");
|
|
17399
17660
|
throw providerError(`Cloud Run pull failed: ${msg}`);
|
|
17400
17661
|
}
|
|
17401
|
-
const seenEventIds = new Set(parseJsonColumn(sourceRow.lastEventIds, []));
|
|
17402
|
-
const dedupedEvents = seenEventIds.size === 0 ? allEvents : allEvents.filter((e) => !seenEventIds.has(e.eventId));
|
|
17403
|
-
const newSorted = dedupedEvents.slice().sort((a, b) => a.observedAt < b.observedAt ? 1 : a.observedAt > b.observedAt ? -1 : 0).map((e) => e.eventId);
|
|
17404
|
-
const previousIds = parseJsonColumn(sourceRow.lastEventIds, []);
|
|
17405
|
-
const merged = [];
|
|
17406
|
-
const mergedSet = /* @__PURE__ */ new Set();
|
|
17407
|
-
for (const id of [...newSorted, ...previousIds]) {
|
|
17408
|
-
if (mergedSet.has(id)) continue;
|
|
17409
|
-
mergedSet.add(id);
|
|
17410
|
-
merged.push(id);
|
|
17411
|
-
if (merged.length >= MAX_TRACKED_EVENT_IDS) break;
|
|
17412
|
-
}
|
|
17413
|
-
const nextEventIds = merged;
|
|
17414
|
-
const report = buildTrafficProbeReport(dedupedEvents, { sampleLimit });
|
|
17415
|
-
const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
17416
17662
|
let crawlerBucketRows = 0;
|
|
17417
17663
|
let aiReferralBucketRows = 0;
|
|
17418
17664
|
let sampleRows = 0;
|
|
17665
|
+
let finishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
17666
|
+
let pulledEventsCount = 0;
|
|
17667
|
+
let crawlerHitsCount = 0;
|
|
17668
|
+
let aiReferralHitsCount = 0;
|
|
17669
|
+
let unknownHitsCount = 0;
|
|
17419
17670
|
app.db.transaction((tx) => {
|
|
17671
|
+
const latestRow = tx.select().from(trafficSources).where(eq23(trafficSources.id, sourceRow.id)).get();
|
|
17672
|
+
const previousIds = parseJsonColumn(latestRow.lastEventIds, []);
|
|
17673
|
+
const seenEventIds = new Set(previousIds);
|
|
17674
|
+
const dedupedEvents = seenEventIds.size === 0 ? allEvents : allEvents.filter((e) => !seenEventIds.has(e.eventId));
|
|
17675
|
+
const newSorted = dedupedEvents.slice().sort((a, b) => a.observedAt < b.observedAt ? 1 : a.observedAt > b.observedAt ? -1 : 0).map((e) => e.eventId);
|
|
17676
|
+
const merged = [];
|
|
17677
|
+
const mergedSet = /* @__PURE__ */ new Set();
|
|
17678
|
+
for (const id of [...newSorted, ...previousIds]) {
|
|
17679
|
+
if (mergedSet.has(id)) continue;
|
|
17680
|
+
mergedSet.add(id);
|
|
17681
|
+
merged.push(id);
|
|
17682
|
+
if (merged.length >= MAX_TRACKED_EVENT_IDS) break;
|
|
17683
|
+
}
|
|
17684
|
+
const nextEventIds = merged;
|
|
17685
|
+
const report = buildTrafficProbeReport(dedupedEvents, { sampleLimit });
|
|
17686
|
+
finishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
17687
|
+
pulledEventsCount = report.totals.normalizedEvents;
|
|
17688
|
+
crawlerHitsCount = report.totals.crawlerHits;
|
|
17689
|
+
aiReferralHitsCount = report.totals.aiReferralHits;
|
|
17690
|
+
unknownHitsCount = report.totals.unknownHits;
|
|
17420
17691
|
for (const bucket of report.crawlerEventsHourly) {
|
|
17421
17692
|
const status = bucket.status ?? 0;
|
|
17422
17693
|
tx.insert(crawlerEventsHourly).values({
|
|
@@ -17515,7 +17786,11 @@ async function trafficRoutes(app, opts) {
|
|
|
17515
17786
|
}
|
|
17516
17787
|
tx.update(trafficSources).set({
|
|
17517
17788
|
status: TrafficSourceStatuses.connected,
|
|
17518
|
-
|
|
17789
|
+
// Advance to windowEnd, not finishedAt — events arriving at the
|
|
17790
|
+
// source between windowEnd and finishedAt aren't in this pull's
|
|
17791
|
+
// range. If we stored finishedAt, the next sync's clamp would skip
|
|
17792
|
+
// past them and they'd be lost.
|
|
17793
|
+
lastSyncedAt: windowEnd.toISOString(),
|
|
17519
17794
|
lastError: null,
|
|
17520
17795
|
lastEventIds: JSON.stringify(nextEventIds),
|
|
17521
17796
|
updatedAt: finishedAt
|
|
@@ -17534,9 +17809,9 @@ async function trafficRoutes(app, opts) {
|
|
|
17534
17809
|
status: "completed",
|
|
17535
17810
|
sourceType: sourceRow.sourceType,
|
|
17536
17811
|
sourceId: sourceRow.id,
|
|
17537
|
-
pulledEvents:
|
|
17538
|
-
crawlerHits:
|
|
17539
|
-
aiReferralHits:
|
|
17812
|
+
pulledEvents: pulledEventsCount,
|
|
17813
|
+
crawlerHits: crawlerHitsCount,
|
|
17814
|
+
aiReferralHits: aiReferralHitsCount,
|
|
17540
17815
|
durationMs: Date.now() - syncStartedAtMs
|
|
17541
17816
|
});
|
|
17542
17817
|
} catch {
|
|
@@ -17545,10 +17820,10 @@ async function trafficRoutes(app, opts) {
|
|
|
17545
17820
|
sourceId: sourceRow.id,
|
|
17546
17821
|
runId,
|
|
17547
17822
|
syncedAt: finishedAt,
|
|
17548
|
-
pulledEvents:
|
|
17549
|
-
crawlerHits:
|
|
17550
|
-
aiReferralHits:
|
|
17551
|
-
unknownHits:
|
|
17823
|
+
pulledEvents: pulledEventsCount,
|
|
17824
|
+
crawlerHits: crawlerHitsCount,
|
|
17825
|
+
aiReferralHits: aiReferralHitsCount,
|
|
17826
|
+
unknownHits: unknownHitsCount,
|
|
17552
17827
|
crawlerBucketRows,
|
|
17553
17828
|
aiReferralBucketRows,
|
|
17554
17829
|
sampleRows,
|
|
@@ -18548,8 +18823,16 @@ var recentDataCheck = {
|
|
|
18548
18823
|
)
|
|
18549
18824
|
).get()?.total ?? 0
|
|
18550
18825
|
);
|
|
18826
|
+
const olderReferrals = Number(
|
|
18827
|
+
ctx.db.select({ total: sql9`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
|
|
18828
|
+
and15(
|
|
18829
|
+
eq24(aiReferralEventsHourly.projectId, ctx.project.id),
|
|
18830
|
+
gte3(aiReferralEventsHourly.tsHour, failCutoff)
|
|
18831
|
+
)
|
|
18832
|
+
).get()?.total ?? 0
|
|
18833
|
+
);
|
|
18551
18834
|
const lastSyncedAt = sources.map((s) => s.lastSyncedAt).filter(Boolean).sort().at(-1) ?? null;
|
|
18552
|
-
if (olderCrawlers > 0 || lastSyncedAt) {
|
|
18835
|
+
if (olderCrawlers > 0 || olderReferrals > 0 || lastSyncedAt) {
|
|
18553
18836
|
return {
|
|
18554
18837
|
status: CheckStatuses.warn,
|
|
18555
18838
|
code: "traffic.recent-data.stale",
|
|
@@ -18955,6 +19238,8 @@ async function apiRoutes(app, opts) {
|
|
|
18955
19238
|
cloudRunCredentialStore: opts.cloudRunCredentialStore,
|
|
18956
19239
|
pullCloudRunEvents: opts.pullCloudRunEvents,
|
|
18957
19240
|
resolveCloudRunAccessToken: opts.resolveCloudRunAccessToken,
|
|
19241
|
+
wordpressTrafficCredentialStore: opts.wordpressTrafficCredentialStore,
|
|
19242
|
+
pullWordpressTrafficEvents: opts.pullWordpressTrafficEvents,
|
|
18958
19243
|
onTrafficSynced: opts.onTrafficSynced
|
|
18959
19244
|
});
|
|
18960
19245
|
await api.register(backlinksRoutes, {
|
|
@@ -19021,6 +19306,50 @@ function buildTrafficSourceValidators(opts) {
|
|
|
19021
19306
|
validateScopes: () => null
|
|
19022
19307
|
};
|
|
19023
19308
|
}
|
|
19309
|
+
if (opts.wordpressTrafficCredentialStore) {
|
|
19310
|
+
const store = opts.wordpressTrafficCredentialStore;
|
|
19311
|
+
const pullEvents = opts.pullWordpressTrafficEvents ?? listWordpressTrafficEvents;
|
|
19312
|
+
validators[TrafficSourceTypes.wordpress] = {
|
|
19313
|
+
validateCredentials: async (source) => {
|
|
19314
|
+
const record = store.getConnection(source.projectName);
|
|
19315
|
+
if (!record) {
|
|
19316
|
+
return {
|
|
19317
|
+
status: CheckStatuses.fail,
|
|
19318
|
+
code: "traffic.credentials.missing",
|
|
19319
|
+
summary: `No WordPress traffic credential found in ~/.canonry/config.yaml for project "${source.projectName}".`,
|
|
19320
|
+
remediation: "Re-run `canonry traffic connect wordpress <project> --url <site> --username <user> --app-password <password>`."
|
|
19321
|
+
};
|
|
19322
|
+
}
|
|
19323
|
+
try {
|
|
19324
|
+
await pullEvents({
|
|
19325
|
+
baseUrl: record.baseUrl,
|
|
19326
|
+
username: record.username,
|
|
19327
|
+
applicationPassword: record.applicationPassword,
|
|
19328
|
+
pageSize: 1,
|
|
19329
|
+
maxPages: 1
|
|
19330
|
+
});
|
|
19331
|
+
return {
|
|
19332
|
+
status: CheckStatuses.ok,
|
|
19333
|
+
code: "traffic.credentials.resolved",
|
|
19334
|
+
summary: `WordPress endpoint responds for "${source.displayName}" (${new URL(record.baseUrl).host}).`
|
|
19335
|
+
};
|
|
19336
|
+
} catch (e) {
|
|
19337
|
+
const httpStatus = e instanceof WordpressTrafficApiError ? e.status : null;
|
|
19338
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
19339
|
+
return {
|
|
19340
|
+
status: CheckStatuses.fail,
|
|
19341
|
+
code: httpStatus === 401 || httpStatus === 403 ? "traffic.credentials.unauthorized" : "traffic.credentials.resolve-failed",
|
|
19342
|
+
summary: httpStatus ? `WordPress endpoint returned HTTP ${httpStatus}: ${msg}.` : `WordPress endpoint probe failed: ${msg}.`,
|
|
19343
|
+
remediation: "Verify the site URL is reachable and the Application Password is valid. Re-connect the source if needed."
|
|
19344
|
+
};
|
|
19345
|
+
}
|
|
19346
|
+
},
|
|
19347
|
+
// WordPress Application Passwords have no scope concept — auth is
|
|
19348
|
+
// strictly "valid credential or not". Surface a skipped result so the
|
|
19349
|
+
// framework is uniform without producing a false signal.
|
|
19350
|
+
validateScopes: () => null
|
|
19351
|
+
};
|
|
19352
|
+
}
|
|
19024
19353
|
return Object.keys(validators).length > 0 ? validators : void 0;
|
|
19025
19354
|
}
|
|
19026
19355
|
|
|
@@ -21458,8 +21787,40 @@ function removeCloudRunConnection(config, projectName) {
|
|
|
21458
21787
|
return true;
|
|
21459
21788
|
}
|
|
21460
21789
|
|
|
21461
|
-
// src/wordpress-config.ts
|
|
21790
|
+
// src/wordpress-traffic-config.ts
|
|
21462
21791
|
function ensureConnections4(config) {
|
|
21792
|
+
if (!config.wordpressTraffic) config.wordpressTraffic = {};
|
|
21793
|
+
if (!config.wordpressTraffic.connections) config.wordpressTraffic.connections = [];
|
|
21794
|
+
return config.wordpressTraffic.connections;
|
|
21795
|
+
}
|
|
21796
|
+
function getWordpressTrafficConnection(config, projectName) {
|
|
21797
|
+
return (config.wordpressTraffic?.connections ?? []).find((c) => c.projectName === projectName);
|
|
21798
|
+
}
|
|
21799
|
+
function upsertWordpressTrafficConnection(config, connection) {
|
|
21800
|
+
const connections = ensureConnections4(config);
|
|
21801
|
+
const index = connections.findIndex((c) => c.projectName === connection.projectName);
|
|
21802
|
+
if (index === -1) {
|
|
21803
|
+
connections.push(connection);
|
|
21804
|
+
return connection;
|
|
21805
|
+
}
|
|
21806
|
+
connections[index] = connection;
|
|
21807
|
+
return connection;
|
|
21808
|
+
}
|
|
21809
|
+
function removeWordpressTrafficConnection(config, projectName) {
|
|
21810
|
+
const connections = config.wordpressTraffic?.connections;
|
|
21811
|
+
if (!connections?.length) return false;
|
|
21812
|
+
const next = connections.filter((c) => c.projectName !== projectName);
|
|
21813
|
+
if (next.length === connections.length) return false;
|
|
21814
|
+
if (!config.wordpressTraffic) return false;
|
|
21815
|
+
config.wordpressTraffic.connections = next;
|
|
21816
|
+
if (next.length === 0) {
|
|
21817
|
+
delete config.wordpressTraffic;
|
|
21818
|
+
}
|
|
21819
|
+
return true;
|
|
21820
|
+
}
|
|
21821
|
+
|
|
21822
|
+
// src/wordpress-config.ts
|
|
21823
|
+
function ensureConnections5(config) {
|
|
21463
21824
|
if (!config.wordpress) config.wordpress = {};
|
|
21464
21825
|
if (!config.wordpress.connections) config.wordpress.connections = [];
|
|
21465
21826
|
return config.wordpress.connections;
|
|
@@ -21476,7 +21837,7 @@ function getWordpressConnection(config, projectName) {
|
|
|
21476
21837
|
return (config.wordpress?.connections ?? []).find((connection) => connection.projectName === projectName);
|
|
21477
21838
|
}
|
|
21478
21839
|
function upsertWordpressConnection(config, connection) {
|
|
21479
|
-
const connections =
|
|
21840
|
+
const connections = ensureConnections5(config);
|
|
21480
21841
|
const normalized = normalizeConnection(connection);
|
|
21481
21842
|
const index = connections.findIndex((entry) => entry.projectName === connection.projectName);
|
|
21482
21843
|
if (index === -1) {
|
|
@@ -25808,6 +26169,21 @@ async function createServer(opts) {
|
|
|
25808
26169
|
return removed;
|
|
25809
26170
|
}
|
|
25810
26171
|
};
|
|
26172
|
+
const wordpressTrafficCredentialStore = {
|
|
26173
|
+
getConnection: (projectName) => {
|
|
26174
|
+
return getWordpressTrafficConnection(opts.config, projectName);
|
|
26175
|
+
},
|
|
26176
|
+
upsertConnection: (record) => {
|
|
26177
|
+
const updated = upsertWordpressTrafficConnection(opts.config, record);
|
|
26178
|
+
saveConfigPatch(opts.config);
|
|
26179
|
+
return updated;
|
|
26180
|
+
},
|
|
26181
|
+
deleteConnection: (projectName) => {
|
|
26182
|
+
const removed = removeWordpressTrafficConnection(opts.config, projectName);
|
|
26183
|
+
if (removed) saveConfigPatch(opts.config);
|
|
26184
|
+
return removed;
|
|
26185
|
+
}
|
|
26186
|
+
};
|
|
25811
26187
|
const googleStateSecret = process.env.GOOGLE_STATE_SECRET ?? crypto31.randomBytes(32).toString("hex");
|
|
25812
26188
|
const googleConnectionStore = {
|
|
25813
26189
|
listConnections: (domain) => listGoogleConnections(opts.config, domain),
|
|
@@ -26152,6 +26528,7 @@ async function createServer(opts) {
|
|
|
26152
26528
|
wordpressConnectionStore,
|
|
26153
26529
|
ga4CredentialStore,
|
|
26154
26530
|
cloudRunCredentialStore,
|
|
26531
|
+
wordpressTrafficCredentialStore,
|
|
26155
26532
|
onTrafficSynced: (event) => {
|
|
26156
26533
|
trackEvent("traffic.synced", {
|
|
26157
26534
|
status: event.status,
|
|
@@ -2227,6 +2227,10 @@ var cloudRunSourceConfigSchema = z20.object({
|
|
|
2227
2227
|
location: z20.string().nullable().optional(),
|
|
2228
2228
|
authMode: trafficSourceAuthModeSchema
|
|
2229
2229
|
});
|
|
2230
|
+
var wordpressTrafficSourceConfigSchema = z20.object({
|
|
2231
|
+
baseUrl: z20.string().url(),
|
|
2232
|
+
username: z20.string().min(1)
|
|
2233
|
+
});
|
|
2230
2234
|
var trafficSourceDtoSchema = z20.object({
|
|
2231
2235
|
id: z20.string(),
|
|
2232
2236
|
projectId: z20.string(),
|
|
@@ -2249,6 +2253,13 @@ var trafficConnectCloudRunRequestSchema = z20.object({
|
|
|
2249
2253
|
/** Service-account JSON content (string). When omitted, defaults to OAuth via `canonry google connect <project> --type ga4` flow. */
|
|
2250
2254
|
keyJson: z20.string().optional()
|
|
2251
2255
|
});
|
|
2256
|
+
var trafficConnectWordpressRequestSchema = z20.object({
|
|
2257
|
+
baseUrl: z20.string().url(),
|
|
2258
|
+
username: z20.string().min(1),
|
|
2259
|
+
/** WordPress Application Password (the same auth used by the content client). */
|
|
2260
|
+
applicationPassword: z20.string().min(1),
|
|
2261
|
+
displayName: z20.string().min(1).optional()
|
|
2262
|
+
});
|
|
2252
2263
|
var trafficSyncResponseSchema = z20.object({
|
|
2253
2264
|
sourceId: z20.string(),
|
|
2254
2265
|
runId: z20.string(),
|
|
@@ -2494,6 +2505,7 @@ export {
|
|
|
2494
2505
|
TrafficSourceAuthModes,
|
|
2495
2506
|
VerificationStatuses,
|
|
2496
2507
|
trafficConnectCloudRunRequestSchema,
|
|
2508
|
+
trafficConnectWordpressRequestSchema,
|
|
2497
2509
|
trafficEventKindSchema,
|
|
2498
2510
|
TrafficEventKinds,
|
|
2499
2511
|
formatRatio,
|