@ainyc/canonry 4.60.2 → 4.61.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/aero/references/regression-playbook.md +2 -1
- package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +4 -0
- package/assets/agent-workspace/skills/canonry/references/google-business-profile.md +23 -2
- package/assets/assets/{BacklinksPage-Dj4AVTma.js → BacklinksPage-CsGOAPNN.js} +1 -1
- package/assets/assets/{ChartPrimitives-7SFwUlCh.js → ChartPrimitives-Bjow7aaC.js} +1 -1
- package/assets/assets/{ProjectPage-4tWuU1ZR.js → ProjectPage-BZoMD93_.js} +1 -1
- package/assets/assets/{RunRow-CgPJfmWX.js → RunRow-ve7H_XIu.js} +1 -1
- package/assets/assets/{RunsPage-Cdo4jBn4.js → RunsPage-mYmYevh0.js} +1 -1
- package/assets/assets/{SettingsPage-N5iccPoU.js → SettingsPage-DoRiIJK5.js} +1 -1
- package/assets/assets/{TrafficPage-pSlQPdDg.js → TrafficPage-1LFyX4OT.js} +1 -1
- package/assets/assets/{TrafficSourceDetailPage-BQx3Evf6.js → TrafficSourceDetailPage-BYDJtQdO.js} +1 -1
- package/assets/assets/{extract-error-message-BOembgFV.js → extract-error-message-Bt6jcL_M.js} +1 -1
- package/assets/assets/{index-DjKFsFsl.js → index-BQxaYi-t.js} +70 -70
- package/assets/assets/{server-traffic-DNgNJ4Ht.js → server-traffic-C5f87b84.js} +1 -1
- package/assets/assets/{trash-2-DOznxxMW.js → trash-2-BFSmyr_7.js} +1 -1
- package/assets/index.html +1 -1
- package/dist/{chunk-CKWHFAVB.js → chunk-PITZUUFV.js} +33 -1
- package/dist/{chunk-3G3GAT3E.js → chunk-QZN3J35I.js} +489 -156
- package/dist/{chunk-DXWUBWBD.js → chunk-URPUUKLC.js} +22 -0
- package/dist/{chunk-AUR7VMQF.js → chunk-ZTVBTGDW.js} +59 -4
- package/dist/cli.js +32 -4
- package/dist/index.d.ts +16 -0
- package/dist/index.js +4 -4
- package/dist/{intelligence-service-UYVVKQ2K.js → intelligence-service-RWVBWSB5.js} +2 -2
- package/dist/mcp.js +2 -2
- package/package.json +11 -10
|
@@ -113,6 +113,7 @@ import {
|
|
|
113
113
|
gbpLocationSelectionRequestSchema,
|
|
114
114
|
gbpLodgingListResponseSchema,
|
|
115
115
|
gbpPlaceActionListResponseSchema,
|
|
116
|
+
gbpPlaceDetailsListResponseSchema,
|
|
116
117
|
gbpSummaryDtoSchema,
|
|
117
118
|
gbpSyncRequestSchema,
|
|
118
119
|
gbpSyncResponseSchema,
|
|
@@ -206,10 +207,10 @@ import {
|
|
|
206
207
|
wordpressSchemaDeployResultDtoSchema,
|
|
207
208
|
wordpressSchemaStatusResultDtoSchema,
|
|
208
209
|
wordpressStatusDtoSchema
|
|
209
|
-
} from "./chunk-
|
|
210
|
+
} from "./chunk-URPUUKLC.js";
|
|
210
211
|
|
|
211
212
|
// src/intelligence-service.ts
|
|
212
|
-
import { eq as
|
|
213
|
+
import { eq as eq30, desc as desc15, asc as asc3, and as and23, ne as ne5, or as or5, inArray as inArray11, gte as gte6, lte as lte3 } from "drizzle-orm";
|
|
213
214
|
|
|
214
215
|
// ../db/src/client.ts
|
|
215
216
|
import { mkdirSync } from "fs";
|
|
@@ -250,6 +251,7 @@ __export(schema_exports, {
|
|
|
250
251
|
gbpLocations: () => gbpLocations,
|
|
251
252
|
gbpLodgingSnapshots: () => gbpLodgingSnapshots,
|
|
252
253
|
gbpPlaceActions: () => gbpPlaceActions,
|
|
254
|
+
gbpPlaceDetails: () => gbpPlaceDetails,
|
|
253
255
|
googleConnections: () => googleConnections,
|
|
254
256
|
gscCoverageSnapshots: () => gscCoverageSnapshots,
|
|
255
257
|
gscSearchData: () => gscSearchData,
|
|
@@ -1018,6 +1020,12 @@ var gbpLocations = sqliteTable("gbp_locations", {
|
|
|
1018
1020
|
primaryCategoryDisplayName: text("primary_category_display_name"),
|
|
1019
1021
|
storefrontAddress: text("storefront_address"),
|
|
1020
1022
|
websiteUri: text("website_uri"),
|
|
1023
|
+
// Google Maps Place ID + public Maps link, sourced from the location's
|
|
1024
|
+
// `metadata` (output-only; populated only when the location is on Maps).
|
|
1025
|
+
// `placeId` links a GBP location to the Places API for supplemental
|
|
1026
|
+
// rendered-listing data. Null when Google has not assigned a Place ID.
|
|
1027
|
+
placeId: text("place_id"),
|
|
1028
|
+
mapsUri: text("maps_uri"),
|
|
1021
1029
|
selected: integer("selected", { mode: "boolean" }).notNull().default(true),
|
|
1022
1030
|
syncedAt: text("synced_at"),
|
|
1023
1031
|
createdAt: text("created_at").notNull(),
|
|
@@ -1101,6 +1109,19 @@ var gbpLodgingSnapshots = sqliteTable("gbp_lodging_snapshots", {
|
|
|
1101
1109
|
}, (table) => [
|
|
1102
1110
|
index("idx_gbp_lodging_loc").on(table.projectId, table.locationName, table.syncedAt)
|
|
1103
1111
|
]);
|
|
1112
|
+
var gbpPlaceDetails = sqliteTable("gbp_place_details", {
|
|
1113
|
+
id: text("id").primaryKey(),
|
|
1114
|
+
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
1115
|
+
locationName: text("location_name").notNull(),
|
|
1116
|
+
placeId: text("place_id").notNull(),
|
|
1117
|
+
contentHash: text("content_hash").notNull(),
|
|
1118
|
+
tier: text("tier").notNull(),
|
|
1119
|
+
attributes: text("attributes", { mode: "json" }).$type().notNull().default({}),
|
|
1120
|
+
syncedAt: text("synced_at").notNull(),
|
|
1121
|
+
syncRunId: text("sync_run_id").references(() => runs.id, { onDelete: "set null" })
|
|
1122
|
+
}, (table) => [
|
|
1123
|
+
index("idx_gbp_place_details_loc").on(table.projectId, table.locationName, table.syncedAt)
|
|
1124
|
+
]);
|
|
1104
1125
|
|
|
1105
1126
|
// ../db/src/client.ts
|
|
1106
1127
|
function createClient(databasePath) {
|
|
@@ -2632,6 +2653,38 @@ var MIGRATION_VERSIONS = [
|
|
|
2632
2653
|
`CREATE INDEX IF NOT EXISTS idx_gbp_keyword_monthly_loc ON gbp_keyword_monthly(project_id, location_name, month)`,
|
|
2633
2654
|
`CREATE UNIQUE INDEX IF NOT EXISTS uniq_gbp_keyword_monthly ON gbp_keyword_monthly(project_id, location_name, month, keyword)`
|
|
2634
2655
|
]
|
|
2656
|
+
},
|
|
2657
|
+
{
|
|
2658
|
+
// Capture the Google Maps Place ID + Maps link on each location so we can
|
|
2659
|
+
// link it to the Places API for supplemental rendered-listing data (#648).
|
|
2660
|
+
// ALTER ADD COLUMN is idempotent here — the runner swallows the duplicate-
|
|
2661
|
+
// column error on re-apply.
|
|
2662
|
+
version: 71,
|
|
2663
|
+
name: "gbp-locations-place-id",
|
|
2664
|
+
statements: [
|
|
2665
|
+
`ALTER TABLE gbp_locations ADD COLUMN place_id TEXT`,
|
|
2666
|
+
`ALTER TABLE gbp_locations ADD COLUMN maps_uri TEXT`
|
|
2667
|
+
]
|
|
2668
|
+
},
|
|
2669
|
+
{
|
|
2670
|
+
// Places (New) Place Details snapshots for lodging locations (#648) —
|
|
2671
|
+
// snapshot-on-change, same shape as gbp_lodging_snapshots.
|
|
2672
|
+
version: 72,
|
|
2673
|
+
name: "gbp-place-details",
|
|
2674
|
+
statements: [
|
|
2675
|
+
`CREATE TABLE IF NOT EXISTS gbp_place_details (
|
|
2676
|
+
id TEXT PRIMARY KEY,
|
|
2677
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
2678
|
+
location_name TEXT NOT NULL,
|
|
2679
|
+
place_id TEXT NOT NULL,
|
|
2680
|
+
content_hash TEXT NOT NULL,
|
|
2681
|
+
tier TEXT NOT NULL,
|
|
2682
|
+
attributes TEXT NOT NULL DEFAULT '{}',
|
|
2683
|
+
synced_at TEXT NOT NULL,
|
|
2684
|
+
sync_run_id TEXT REFERENCES runs(id) ON DELETE SET NULL
|
|
2685
|
+
)`,
|
|
2686
|
+
`CREATE INDEX IF NOT EXISTS idx_gbp_place_details_loc ON gbp_place_details(project_id, location_name, synced_at)`
|
|
2687
|
+
]
|
|
2635
2688
|
}
|
|
2636
2689
|
];
|
|
2637
2690
|
function isDuplicateColumnError(err) {
|
|
@@ -4531,21 +4584,40 @@ var METRIC_LABELS = {
|
|
|
4531
4584
|
function metricLabel(metric) {
|
|
4532
4585
|
return METRIC_LABELS[metric] ?? metric;
|
|
4533
4586
|
}
|
|
4587
|
+
function formatAmenityList(amenities) {
|
|
4588
|
+
if (amenities.length === 1) return amenities[0];
|
|
4589
|
+
if (amenities.length === 2) return `${amenities[0]} and ${amenities[1]}`;
|
|
4590
|
+
return `${amenities.slice(0, -1).join(", ")}, and ${amenities[amenities.length - 1]}`;
|
|
4591
|
+
}
|
|
4534
4592
|
function analyzeGbp(signals) {
|
|
4535
4593
|
const drafts = [];
|
|
4536
4594
|
for (const loc of signals) {
|
|
4537
4595
|
const base = { locationName: loc.locationName, query: loc.displayName, provider: GBP_INSIGHT_PROVIDER };
|
|
4538
4596
|
if (loc.lodgingCapable && loc.lodgingEmpty) {
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
|
|
4547
|
-
|
|
4548
|
-
|
|
4597
|
+
if (loc.placesAmenities.length > 0) {
|
|
4598
|
+
const amenityList = formatAmenityList(loc.placesAmenities);
|
|
4599
|
+
drafts.push({
|
|
4600
|
+
...base,
|
|
4601
|
+
type: "gbp-listing-discrepancy",
|
|
4602
|
+
severity: "high",
|
|
4603
|
+
title: `${loc.displayName}: public listing shows ${loc.placesAmenities.length} amenit${loc.placesAmenities.length === 1 ? "y" : "ies"} your GBP profile doesn\u2019t`,
|
|
4604
|
+
recommendation: {
|
|
4605
|
+
action: "Populate the hotel\u2019s structured amenity attributes in Google Business Profile to match what its public listing already advertises \u2014 the amenity source you directly control",
|
|
4606
|
+
reason: `Google\u2019s rendered listing advertises ${amenityList} (synthesized from Hotel Center / OTAs / Places), but your GBP structured profile has zero populated attributes. The structured attributes are what AI answer engines cite and the only amenity data you control, so the public listing is making promises your profile can\u2019t back.`
|
|
4607
|
+
}
|
|
4608
|
+
});
|
|
4609
|
+
} else {
|
|
4610
|
+
drafts.push({
|
|
4611
|
+
...base,
|
|
4612
|
+
type: "gbp-lodging-gap",
|
|
4613
|
+
severity: "high",
|
|
4614
|
+
title: `${loc.displayName}: lodging profile has no structured attributes`,
|
|
4615
|
+
recommendation: {
|
|
4616
|
+
action: "Populate the hotel\u2019s structured amenity attributes in Google Business Profile \u2014 the amenity source you directly control",
|
|
4617
|
+
reason: "The GBP API exposes only owner-configured attributes, and this profile has none. Google\u2019s rendered listing may still show amenities it synthesizes from Hotel Center, OTAs, and user data \u2014 so the public listing can differ from this profile \u2014 but the structured attributes are what AI answer engines cite and the only amenity data you control."
|
|
4618
|
+
}
|
|
4619
|
+
});
|
|
4620
|
+
}
|
|
4549
4621
|
}
|
|
4550
4622
|
if (loc.placeActionCount > 0 && !loc.hasDirectMerchantCta) {
|
|
4551
4623
|
drafts.push({
|
|
@@ -4616,11 +4688,141 @@ function pickWorstKeywordDrop(loc) {
|
|
|
4616
4688
|
return worst;
|
|
4617
4689
|
}
|
|
4618
4690
|
|
|
4691
|
+
// ../integration-google-places/src/constants.ts
|
|
4692
|
+
var PLACES_API_BASE = "https://places.googleapis.com/v1";
|
|
4693
|
+
var PLACES_REQUEST_TIMEOUT_MS = 3e4;
|
|
4694
|
+
var PLACES_PRO_FIELDS = [
|
|
4695
|
+
"id",
|
|
4696
|
+
"types",
|
|
4697
|
+
"primaryType",
|
|
4698
|
+
"primaryTypeDisplayName",
|
|
4699
|
+
"googleMapsUri",
|
|
4700
|
+
"websiteUri",
|
|
4701
|
+
"businessStatus",
|
|
4702
|
+
"accessibilityOptions"
|
|
4703
|
+
];
|
|
4704
|
+
var PLACES_ATMOSPHERE_FIELDS = [
|
|
4705
|
+
...PLACES_PRO_FIELDS,
|
|
4706
|
+
"editorialSummary",
|
|
4707
|
+
"servesBreakfast",
|
|
4708
|
+
"servesLunch",
|
|
4709
|
+
"servesDinner",
|
|
4710
|
+
"servesBrunch",
|
|
4711
|
+
"restroom",
|
|
4712
|
+
"goodForChildren",
|
|
4713
|
+
"goodForGroups",
|
|
4714
|
+
"allowsDogs",
|
|
4715
|
+
"parkingOptions",
|
|
4716
|
+
"outdoorSeating",
|
|
4717
|
+
"reservable"
|
|
4718
|
+
];
|
|
4719
|
+
|
|
4720
|
+
// ../integration-google-places/src/types.ts
|
|
4721
|
+
var PlacesApiError = class extends Error {
|
|
4722
|
+
status;
|
|
4723
|
+
reason;
|
|
4724
|
+
body;
|
|
4725
|
+
constructor(message, status, reason, body) {
|
|
4726
|
+
super(message);
|
|
4727
|
+
this.name = "PlacesApiError";
|
|
4728
|
+
this.status = status;
|
|
4729
|
+
this.reason = reason;
|
|
4730
|
+
this.body = body;
|
|
4731
|
+
}
|
|
4732
|
+
};
|
|
4733
|
+
|
|
4734
|
+
// ../integration-google-places/src/http.ts
|
|
4735
|
+
function extractReason(body) {
|
|
4736
|
+
if (!body || typeof body !== "object") return null;
|
|
4737
|
+
const status = body.error?.status;
|
|
4738
|
+
return typeof status === "string" ? status : null;
|
|
4739
|
+
}
|
|
4740
|
+
function isRetryable(err) {
|
|
4741
|
+
if (!(err instanceof PlacesApiError)) return false;
|
|
4742
|
+
return err.status === 429 || err.status === 503;
|
|
4743
|
+
}
|
|
4744
|
+
async function placesFetchOnce(url, apiKey, fieldMask) {
|
|
4745
|
+
const controller = new AbortController();
|
|
4746
|
+
const timeout = setTimeout(() => controller.abort(), PLACES_REQUEST_TIMEOUT_MS);
|
|
4747
|
+
try {
|
|
4748
|
+
const headers = {
|
|
4749
|
+
"X-Goog-Api-Key": apiKey,
|
|
4750
|
+
"X-Goog-FieldMask": fieldMask,
|
|
4751
|
+
accept: "application/json"
|
|
4752
|
+
};
|
|
4753
|
+
const res = await fetch(url, { method: "GET", headers, signal: controller.signal });
|
|
4754
|
+
const text2 = await res.text();
|
|
4755
|
+
let body;
|
|
4756
|
+
try {
|
|
4757
|
+
body = text2 ? JSON.parse(text2) : void 0;
|
|
4758
|
+
} catch {
|
|
4759
|
+
body = text2;
|
|
4760
|
+
}
|
|
4761
|
+
if (!res.ok) {
|
|
4762
|
+
const payload = body;
|
|
4763
|
+
const message = typeof payload === "object" && payload?.error?.message ? payload.error.message : typeof payload === "string" ? payload : `HTTP ${res.status}`;
|
|
4764
|
+
throw new PlacesApiError(message, res.status, extractReason(body), body);
|
|
4765
|
+
}
|
|
4766
|
+
return body;
|
|
4767
|
+
} finally {
|
|
4768
|
+
clearTimeout(timeout);
|
|
4769
|
+
}
|
|
4770
|
+
}
|
|
4771
|
+
async function placesFetchGet(url, apiKey, fieldMask, opts = {}) {
|
|
4772
|
+
return withRetry(() => placesFetchOnce(url, apiKey, fieldMask), {
|
|
4773
|
+
maxRetries: opts.retry?.maxRetries ?? 3,
|
|
4774
|
+
baseDelayMs: opts.retry?.baseDelayMs ?? 500,
|
|
4775
|
+
jitter: true,
|
|
4776
|
+
isRetryable,
|
|
4777
|
+
sleep: opts.retry?.sleep
|
|
4778
|
+
});
|
|
4779
|
+
}
|
|
4780
|
+
|
|
4781
|
+
// ../integration-google-places/src/place-details-client.ts
|
|
4782
|
+
import crypto from "crypto";
|
|
4783
|
+
function buildPlaceDetailsFieldMask(tier) {
|
|
4784
|
+
const fields = tier === "atmosphere" ? PLACES_ATMOSPHERE_FIELDS : PLACES_PRO_FIELDS;
|
|
4785
|
+
return fields.join(",");
|
|
4786
|
+
}
|
|
4787
|
+
async function getPlaceDetails(placeId, apiKey, opts = {}) {
|
|
4788
|
+
const fieldMask = opts.fieldMask ?? buildPlaceDetailsFieldMask(opts.tier ?? "atmosphere");
|
|
4789
|
+
const url = new URL(`${PLACES_API_BASE}/places/${encodeURIComponent(placeId)}`);
|
|
4790
|
+
if (opts.languageCode) url.searchParams.set("languageCode", opts.languageCode);
|
|
4791
|
+
return placesFetchGet(url.toString(), apiKey, fieldMask, opts);
|
|
4792
|
+
}
|
|
4793
|
+
function stableStringify(value) {
|
|
4794
|
+
if (value === void 0) return "null";
|
|
4795
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
|
|
4796
|
+
if (value && typeof value === "object") {
|
|
4797
|
+
const entries = Object.keys(value).sort().map((k) => `${JSON.stringify(k)}:${stableStringify(value[k])}`);
|
|
4798
|
+
return `{${entries.join(",")}}`;
|
|
4799
|
+
}
|
|
4800
|
+
return JSON.stringify(value);
|
|
4801
|
+
}
|
|
4802
|
+
function hashPlaceDetails(place) {
|
|
4803
|
+
return crypto.createHash("sha256").update(stableStringify(place)).digest("hex");
|
|
4804
|
+
}
|
|
4805
|
+
|
|
4806
|
+
// ../integration-google-places/src/amenities.ts
|
|
4807
|
+
function extractPlaceAmenities(place) {
|
|
4808
|
+
const out = [];
|
|
4809
|
+
if (place.servesBreakfast) out.push("breakfast");
|
|
4810
|
+
if (place.servesLunch || place.servesDinner || place.servesBrunch) out.push("on-site dining");
|
|
4811
|
+
if (place.allowsDogs) out.push("pet-friendly");
|
|
4812
|
+
if (place.parkingOptions && Object.values(place.parkingOptions).some(Boolean)) out.push("parking");
|
|
4813
|
+
if (place.accessibilityOptions && Object.values(place.accessibilityOptions).some(Boolean)) out.push("wheelchair accessibility");
|
|
4814
|
+
if (place.restroom) out.push("restroom");
|
|
4815
|
+
if (place.goodForChildren) out.push("family-friendly");
|
|
4816
|
+
if (place.outdoorSeating) out.push("outdoor seating");
|
|
4817
|
+
if (place.reservable) out.push("reservations");
|
|
4818
|
+
return out;
|
|
4819
|
+
}
|
|
4820
|
+
|
|
4619
4821
|
// ../api-routes/src/index.ts
|
|
4620
4822
|
import fs8 from "fs";
|
|
4621
4823
|
|
|
4622
4824
|
// ../api-routes/src/auth.ts
|
|
4623
|
-
import
|
|
4825
|
+
import crypto2 from "crypto";
|
|
4624
4826
|
import { eq } from "drizzle-orm";
|
|
4625
4827
|
function requireScope(request, scope) {
|
|
4626
4828
|
const key = request.apiKey;
|
|
@@ -4629,7 +4831,7 @@ function requireScope(request, scope) {
|
|
|
4629
4831
|
throw forbidden(`This action requires the "${scope}" scope on your API key.`);
|
|
4630
4832
|
}
|
|
4631
4833
|
function hashKey(key) {
|
|
4632
|
-
return
|
|
4834
|
+
return crypto2.createHash("sha256").update(key).digest("hex");
|
|
4633
4835
|
}
|
|
4634
4836
|
var SKIP_PATHS = ["/health"];
|
|
4635
4837
|
function shouldSkipAuth(url) {
|
|
@@ -4693,11 +4895,11 @@ async function authPlugin(app, opts = {}) {
|
|
|
4693
4895
|
}
|
|
4694
4896
|
|
|
4695
4897
|
// ../api-routes/src/projects.ts
|
|
4696
|
-
import
|
|
4898
|
+
import crypto4 from "crypto";
|
|
4697
4899
|
import { eq as eq3, sql as sql3 } from "drizzle-orm";
|
|
4698
4900
|
|
|
4699
4901
|
// ../api-routes/src/helpers.ts
|
|
4700
|
-
import
|
|
4902
|
+
import crypto3 from "crypto";
|
|
4701
4903
|
import { eq as eq2, ne, sql as sql2 } from "drizzle-orm";
|
|
4702
4904
|
function notProbeRun() {
|
|
4703
4905
|
return ne(runs.trigger, RunTriggers.probe);
|
|
@@ -4712,7 +4914,7 @@ function resolveProject(db, name) {
|
|
|
4712
4914
|
function writeAuditLog(db, entry) {
|
|
4713
4915
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4714
4916
|
db.insert(auditLog).values({
|
|
4715
|
-
id:
|
|
4917
|
+
id: crypto3.randomUUID(),
|
|
4716
4918
|
projectId: entry.projectId ?? null,
|
|
4717
4919
|
actor: entry.actor,
|
|
4718
4920
|
action: entry.action,
|
|
@@ -4846,7 +5048,7 @@ async function projectRoutes(app, opts) {
|
|
|
4846
5048
|
const updated = app.db.select().from(projects).where(eq3(projects.id, existing.id)).get();
|
|
4847
5049
|
return reply.status(200).send(formatProject(updated));
|
|
4848
5050
|
}
|
|
4849
|
-
const id =
|
|
5051
|
+
const id = crypto4.randomUUID();
|
|
4850
5052
|
app.db.transaction((tx) => {
|
|
4851
5053
|
tx.insert(projects).values({
|
|
4852
5054
|
id,
|
|
@@ -5084,7 +5286,7 @@ function aliasArraysEqual(a, b) {
|
|
|
5084
5286
|
}
|
|
5085
5287
|
|
|
5086
5288
|
// ../api-routes/src/queries.ts
|
|
5087
|
-
import
|
|
5289
|
+
import crypto5 from "crypto";
|
|
5088
5290
|
import { eq as eq4, inArray, sql as sql4 } from "drizzle-orm";
|
|
5089
5291
|
function preserveSnapshotQueryText(tx, projectId, queryIds) {
|
|
5090
5292
|
const candidates = queryIds && queryIds.length > 0 ? tx.select({ id: queries.id, text: queries.query }).from(queries).where(inArray(queries.id, queryIds)).all() : tx.select({ id: queries.id, text: queries.query }).from(queries).where(eq4(queries.projectId, projectId)).all();
|
|
@@ -5110,7 +5312,7 @@ async function queryRoutes(app, opts) {
|
|
|
5110
5312
|
tx.delete(queries).where(eq4(queries.projectId, project.id)).run();
|
|
5111
5313
|
for (const q of body.queries) {
|
|
5112
5314
|
tx.insert(queries).values({
|
|
5113
|
-
id:
|
|
5315
|
+
id: crypto5.randomUUID(),
|
|
5114
5316
|
projectId: project.id,
|
|
5115
5317
|
query: q,
|
|
5116
5318
|
provenance: "cli",
|
|
@@ -5198,7 +5400,7 @@ async function queryRoutes(app, opts) {
|
|
|
5198
5400
|
for (const q of body.queries) {
|
|
5199
5401
|
if (!existingSet.has(q)) {
|
|
5200
5402
|
app.db.insert(queries).values({
|
|
5201
|
-
id:
|
|
5403
|
+
id: crypto5.randomUUID(),
|
|
5202
5404
|
projectId: project.id,
|
|
5203
5405
|
query: q,
|
|
5204
5406
|
provenance: "cli",
|
|
@@ -5277,7 +5479,7 @@ async function queryRoutes(app, opts) {
|
|
|
5277
5479
|
tx.delete(queries).where(eq4(queries.projectId, project.id)).run();
|
|
5278
5480
|
for (const keyword of body.keywords) {
|
|
5279
5481
|
tx.insert(queries).values({
|
|
5280
|
-
id:
|
|
5482
|
+
id: crypto5.randomUUID(),
|
|
5281
5483
|
projectId: project.id,
|
|
5282
5484
|
query: keyword,
|
|
5283
5485
|
provenance: "cli",
|
|
@@ -5335,7 +5537,7 @@ async function queryRoutes(app, opts) {
|
|
|
5335
5537
|
for (const keyword of body.keywords) {
|
|
5336
5538
|
if (!existingSet.has(keyword)) {
|
|
5337
5539
|
app.db.insert(queries).values({
|
|
5338
|
-
id:
|
|
5540
|
+
id: crypto5.randomUUID(),
|
|
5339
5541
|
projectId: project.id,
|
|
5340
5542
|
query: keyword,
|
|
5341
5543
|
provenance: "cli",
|
|
@@ -5400,7 +5602,7 @@ async function queryRoutes(app, opts) {
|
|
|
5400
5602
|
}
|
|
5401
5603
|
|
|
5402
5604
|
// ../api-routes/src/competitors.ts
|
|
5403
|
-
import
|
|
5605
|
+
import crypto6 from "crypto";
|
|
5404
5606
|
import { eq as eq5 } from "drizzle-orm";
|
|
5405
5607
|
function normalizeCompetitor(domain) {
|
|
5406
5608
|
const reg = registrableDomain(domain);
|
|
@@ -5438,7 +5640,7 @@ async function competitorRoutes(app) {
|
|
|
5438
5640
|
tx.delete(competitors).where(eq5(competitors.projectId, project.id)).run();
|
|
5439
5641
|
for (const domain of normalizedCompetitors) {
|
|
5440
5642
|
tx.insert(competitors).values({
|
|
5441
|
-
id:
|
|
5643
|
+
id: crypto6.randomUUID(),
|
|
5442
5644
|
projectId: project.id,
|
|
5443
5645
|
domain,
|
|
5444
5646
|
provenance: "cli",
|
|
@@ -5468,7 +5670,7 @@ async function competitorRoutes(app) {
|
|
|
5468
5670
|
if (added.length === 0) return;
|
|
5469
5671
|
for (const domain of added) {
|
|
5470
5672
|
tx.insert(competitors).values({
|
|
5471
|
-
id:
|
|
5673
|
+
id: crypto6.randomUUID(),
|
|
5472
5674
|
projectId: project.id,
|
|
5473
5675
|
domain,
|
|
5474
5676
|
provenance: "cli",
|
|
@@ -5523,18 +5725,18 @@ function parseCompetitorBatch(value) {
|
|
|
5523
5725
|
}
|
|
5524
5726
|
|
|
5525
5727
|
// ../api-routes/src/runs.ts
|
|
5526
|
-
import
|
|
5728
|
+
import crypto8 from "crypto";
|
|
5527
5729
|
import { and as and2, eq as eq7, asc, desc, or as or2, sql as sql5 } from "drizzle-orm";
|
|
5528
5730
|
import { gte } from "drizzle-orm";
|
|
5529
5731
|
|
|
5530
5732
|
// ../api-routes/src/run-queue.ts
|
|
5531
|
-
import
|
|
5733
|
+
import crypto7 from "crypto";
|
|
5532
5734
|
import { and, eq as eq6, or } from "drizzle-orm";
|
|
5533
5735
|
function queueRunIfProjectIdle(db, params) {
|
|
5534
5736
|
const createdAt = params.createdAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
5535
5737
|
const kind = params.kind ?? "answer-visibility";
|
|
5536
5738
|
const trigger = params.trigger ?? "manual";
|
|
5537
|
-
const runId =
|
|
5739
|
+
const runId = crypto7.randomUUID();
|
|
5538
5740
|
return db.transaction((tx) => {
|
|
5539
5741
|
const activeRun = tx.select().from(runs).where(
|
|
5540
5742
|
and(
|
|
@@ -5629,7 +5831,7 @@ async function runRoutes(app, opts) {
|
|
|
5629
5831
|
}
|
|
5630
5832
|
const inserted = [];
|
|
5631
5833
|
for (const loc of projectLocations) {
|
|
5632
|
-
const runId2 =
|
|
5834
|
+
const runId2 = crypto8.randomUUID();
|
|
5633
5835
|
tx.insert(runs).values({
|
|
5634
5836
|
id: runId2,
|
|
5635
5837
|
projectId: project.id,
|
|
@@ -5925,7 +6127,7 @@ function loadRunDetail(app, run) {
|
|
|
5925
6127
|
}
|
|
5926
6128
|
|
|
5927
6129
|
// ../api-routes/src/apply.ts
|
|
5928
|
-
import
|
|
6130
|
+
import crypto10 from "crypto";
|
|
5929
6131
|
import { and as and3, eq as eq8 } from "drizzle-orm";
|
|
5930
6132
|
|
|
5931
6133
|
// ../api-routes/src/schedule-utils.ts
|
|
@@ -6017,7 +6219,7 @@ function isValidTimezone(tz) {
|
|
|
6017
6219
|
}
|
|
6018
6220
|
|
|
6019
6221
|
// ../api-routes/src/webhooks.ts
|
|
6020
|
-
import
|
|
6222
|
+
import crypto9 from "crypto";
|
|
6021
6223
|
import dns from "dns/promises";
|
|
6022
6224
|
import http from "http";
|
|
6023
6225
|
import https from "https";
|
|
@@ -6069,7 +6271,7 @@ async function deliverWebhook(target, payload, webhookSecret) {
|
|
|
6069
6271
|
"User-Agent": "Canonry/0.1.0"
|
|
6070
6272
|
};
|
|
6071
6273
|
if (webhookSecret) {
|
|
6072
|
-
headers["X-Canonry-Signature"] = "sha256=" +
|
|
6274
|
+
headers["X-Canonry-Signature"] = "sha256=" + crypto9.createHmac("sha256", webhookSecret).update(body).digest("hex");
|
|
6073
6275
|
}
|
|
6074
6276
|
return await new Promise((resolve) => {
|
|
6075
6277
|
const requestOptions = {
|
|
@@ -6284,7 +6486,7 @@ async function applyRoutes(app, opts) {
|
|
|
6284
6486
|
entityId: projectId
|
|
6285
6487
|
});
|
|
6286
6488
|
} else {
|
|
6287
|
-
projectId =
|
|
6489
|
+
projectId = crypto10.randomUUID();
|
|
6288
6490
|
tx.insert(projects).values({
|
|
6289
6491
|
id: projectId,
|
|
6290
6492
|
name,
|
|
@@ -6316,7 +6518,7 @@ async function applyRoutes(app, opts) {
|
|
|
6316
6518
|
tx.delete(queries).where(eq8(queries.projectId, projectId)).run();
|
|
6317
6519
|
for (const q of configQueries) {
|
|
6318
6520
|
tx.insert(queries).values({
|
|
6319
|
-
id:
|
|
6521
|
+
id: crypto10.randomUUID(),
|
|
6320
6522
|
projectId,
|
|
6321
6523
|
query: q,
|
|
6322
6524
|
provenance: "cli",
|
|
@@ -6334,7 +6536,7 @@ async function applyRoutes(app, opts) {
|
|
|
6334
6536
|
const normalizedCompetitors = normalizeCompetitorList2(config.spec.competitors);
|
|
6335
6537
|
for (const domain of normalizedCompetitors) {
|
|
6336
6538
|
tx.insert(competitors).values({
|
|
6337
|
-
id:
|
|
6539
|
+
id: crypto10.randomUUID(),
|
|
6338
6540
|
projectId,
|
|
6339
6541
|
domain,
|
|
6340
6542
|
provenance: "cli",
|
|
@@ -6362,7 +6564,7 @@ async function applyRoutes(app, opts) {
|
|
|
6362
6564
|
}).where(eq8(schedules.id, existingSched.id)).run();
|
|
6363
6565
|
} else {
|
|
6364
6566
|
tx.insert(schedules).values({
|
|
6365
|
-
id:
|
|
6567
|
+
id: crypto10.randomUUID(),
|
|
6366
6568
|
projectId,
|
|
6367
6569
|
kind: AV_KIND,
|
|
6368
6570
|
cronExpr: resolvedSchedule.cronExpr,
|
|
@@ -6386,11 +6588,11 @@ async function applyRoutes(app, opts) {
|
|
|
6386
6588
|
tx.delete(notifications).where(eq8(notifications.projectId, projectId)).run();
|
|
6387
6589
|
for (const notif of config.spec.notifications) {
|
|
6388
6590
|
tx.insert(notifications).values({
|
|
6389
|
-
id:
|
|
6591
|
+
id: crypto10.randomUUID(),
|
|
6390
6592
|
projectId,
|
|
6391
6593
|
channel: notif.channel,
|
|
6392
6594
|
config: { url: notif.url, events: notif.events },
|
|
6393
|
-
webhookSecret:
|
|
6595
|
+
webhookSecret: crypto10.randomBytes(32).toString("hex"),
|
|
6394
6596
|
enabled: true,
|
|
6395
6597
|
createdAt: now,
|
|
6396
6598
|
updatedAt: now
|
|
@@ -7262,7 +7464,7 @@ async function intelligenceRoutes(app) {
|
|
|
7262
7464
|
import { and as and9, desc as desc7, eq as eq14, gte as gte2, inArray as inArray6, lt, lte, ne as ne2, or as or3, sql as sql6 } from "drizzle-orm";
|
|
7263
7465
|
|
|
7264
7466
|
// ../api-routes/src/content.ts
|
|
7265
|
-
import
|
|
7467
|
+
import crypto11 from "crypto";
|
|
7266
7468
|
import { and as and8, desc as desc6, eq as eq13 } from "drizzle-orm";
|
|
7267
7469
|
|
|
7268
7470
|
// ../api-routes/src/content-data.ts
|
|
@@ -7724,7 +7926,7 @@ async function contentRoutes(app, opts = {}) {
|
|
|
7724
7926
|
const { targetRef, addressedUrl, note } = parsed.data;
|
|
7725
7927
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
7726
7928
|
app.db.insert(contentTargetDismissals).values({
|
|
7727
|
-
id:
|
|
7929
|
+
id: crypto11.randomUUID(),
|
|
7728
7930
|
projectId: project.id,
|
|
7729
7931
|
targetRef,
|
|
7730
7932
|
addressedUrl: addressedUrl ?? null,
|
|
@@ -7803,7 +8005,7 @@ async function contentRoutes(app, opts = {}) {
|
|
|
7803
8005
|
});
|
|
7804
8006
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
7805
8007
|
app.db.insert(recommendationExplanations).values({
|
|
7806
|
-
id:
|
|
8008
|
+
id: crypto11.randomUUID(),
|
|
7807
8009
|
projectId: project.id,
|
|
7808
8010
|
targetRef,
|
|
7809
8011
|
promptVersion: result.promptVersion,
|
|
@@ -12371,6 +12573,7 @@ var SCHEMA_TABLE = {
|
|
|
12371
12573
|
GbpLocationListResponse: gbpLocationListResponseSchema,
|
|
12372
12574
|
GbpLodgingListResponse: gbpLodgingListResponseSchema,
|
|
12373
12575
|
GbpPlaceActionListResponse: gbpPlaceActionListResponseSchema,
|
|
12576
|
+
GbpPlaceDetailsListResponse: gbpPlaceDetailsListResponseSchema,
|
|
12374
12577
|
GbpSummaryDto: gbpSummaryDtoSchema,
|
|
12375
12578
|
GbpSyncResponse: gbpSyncResponseSchema,
|
|
12376
12579
|
GoogleConnectionDto: googleConnectionDtoSchema,
|
|
@@ -14269,6 +14472,20 @@ var routeCatalog = [
|
|
|
14269
14472
|
404: errorResponse("Project not found.")
|
|
14270
14473
|
}
|
|
14271
14474
|
},
|
|
14475
|
+
{
|
|
14476
|
+
method: "get",
|
|
14477
|
+
path: "/api/v1/projects/{name}/gbp/places",
|
|
14478
|
+
summary: "List latest Google Places rendered-listing snapshots per location",
|
|
14479
|
+
tags: ["gbp"],
|
|
14480
|
+
parameters: [
|
|
14481
|
+
nameParameter,
|
|
14482
|
+
{ in: "query", name: "locationName", required: false, description: "Filter to one location resource name", schema: stringSchema }
|
|
14483
|
+
],
|
|
14484
|
+
responses: {
|
|
14485
|
+
200: jsonResponse("Place Details snapshots returned.", "GbpPlaceDetailsListResponse"),
|
|
14486
|
+
404: errorResponse("Project not found.")
|
|
14487
|
+
}
|
|
14488
|
+
},
|
|
14272
14489
|
{
|
|
14273
14490
|
method: "get",
|
|
14274
14491
|
path: "/api/v1/projects/{name}/gbp/summary",
|
|
@@ -16328,7 +16545,7 @@ async function telemetryRoutes(app, opts) {
|
|
|
16328
16545
|
}
|
|
16329
16546
|
|
|
16330
16547
|
// ../api-routes/src/schedules.ts
|
|
16331
|
-
import
|
|
16548
|
+
import crypto12 from "crypto";
|
|
16332
16549
|
import { and as and12, eq as eq17 } from "drizzle-orm";
|
|
16333
16550
|
function parseKindParam(raw) {
|
|
16334
16551
|
if (raw === void 0 || raw === null || raw === "") return SchedulableRunKinds["answer-visibility"];
|
|
@@ -16408,7 +16625,7 @@ async function scheduleRoutes(app, opts) {
|
|
|
16408
16625
|
}).where(eq17(schedules.id, existing.id)).run();
|
|
16409
16626
|
} else {
|
|
16410
16627
|
app.db.insert(schedules).values({
|
|
16411
|
-
id:
|
|
16628
|
+
id: crypto12.randomUUID(),
|
|
16412
16629
|
projectId: project.id,
|
|
16413
16630
|
kind,
|
|
16414
16631
|
cronExpr,
|
|
@@ -16480,7 +16697,7 @@ function formatSchedule(row) {
|
|
|
16480
16697
|
}
|
|
16481
16698
|
|
|
16482
16699
|
// ../api-routes/src/notifications.ts
|
|
16483
|
-
import
|
|
16700
|
+
import crypto13 from "crypto";
|
|
16484
16701
|
import { eq as eq18 } from "drizzle-orm";
|
|
16485
16702
|
var VALID_EVENTS = ["citation.lost", "citation.gained", "run.completed", "run.failed", "insight.critical", "insight.high"];
|
|
16486
16703
|
async function notificationRoutes(app, opts = {}) {
|
|
@@ -16500,8 +16717,8 @@ async function notificationRoutes(app, opts = {}) {
|
|
|
16500
16717
|
throw validationError(`Invalid event(s): ${invalid.join(", ")}. Must be one of: ${VALID_EVENTS.join(", ")}`);
|
|
16501
16718
|
}
|
|
16502
16719
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
16503
|
-
const id =
|
|
16504
|
-
const webhookSecret =
|
|
16720
|
+
const id = crypto13.randomUUID();
|
|
16721
|
+
const webhookSecret = crypto13.randomBytes(32).toString("hex");
|
|
16505
16722
|
app.db.insert(notifications).values({
|
|
16506
16723
|
id,
|
|
16507
16724
|
projectId: project.id,
|
|
@@ -16600,7 +16817,7 @@ function formatNotification(row) {
|
|
|
16600
16817
|
}
|
|
16601
16818
|
|
|
16602
16819
|
// ../api-routes/src/google.ts
|
|
16603
|
-
import
|
|
16820
|
+
import crypto16 from "crypto";
|
|
16604
16821
|
import { eq as eq19, and as and13, desc as desc9, sql as sql8, inArray as inArray9 } from "drizzle-orm";
|
|
16605
16822
|
|
|
16606
16823
|
// ../api-routes/src/gbp-summary.ts
|
|
@@ -17056,7 +17273,7 @@ async function inspectUrl(accessToken, inspectionUrl, siteUrl) {
|
|
|
17056
17273
|
}
|
|
17057
17274
|
|
|
17058
17275
|
// ../integration-google-analytics/src/ga4-client.ts
|
|
17059
|
-
import
|
|
17276
|
+
import crypto14 from "crypto";
|
|
17060
17277
|
|
|
17061
17278
|
// ../integration-google-analytics/src/constants.ts
|
|
17062
17279
|
var GA4_DATA_API_BASE = "https://analyticsdata.googleapis.com/v1beta";
|
|
@@ -17163,7 +17380,7 @@ function createServiceAccountJwt(clientEmail, privateKey, scope) {
|
|
|
17163
17380
|
const headerB64 = encode(header);
|
|
17164
17381
|
const payloadB64 = encode(payload);
|
|
17165
17382
|
const signingInput = `${headerB64}.${payloadB64}`;
|
|
17166
|
-
const sign =
|
|
17383
|
+
const sign = crypto14.createSign("RSA-SHA256");
|
|
17167
17384
|
sign.update(signingInput);
|
|
17168
17385
|
const signature = sign.sign(privateKey, "base64url");
|
|
17169
17386
|
return `${signingInput}.${signature}`;
|
|
@@ -17777,7 +17994,9 @@ var GBP_LOCATIONS_DEFAULT_READ_MASK = [
|
|
|
17777
17994
|
"title",
|
|
17778
17995
|
"storefrontAddress",
|
|
17779
17996
|
"websiteUri",
|
|
17780
|
-
"categories.primaryCategory.displayName"
|
|
17997
|
+
"categories.primaryCategory.displayName",
|
|
17998
|
+
"metadata.placeId",
|
|
17999
|
+
"metadata.mapsUri"
|
|
17781
18000
|
].join(",");
|
|
17782
18001
|
|
|
17783
18002
|
// ../integration-google-business-profile/src/types.ts
|
|
@@ -17797,7 +18016,7 @@ var GbpApiError = class extends Error {
|
|
|
17797
18016
|
};
|
|
17798
18017
|
|
|
17799
18018
|
// ../integration-google-business-profile/src/http.ts
|
|
17800
|
-
function
|
|
18019
|
+
function extractReason2(body) {
|
|
17801
18020
|
if (!body || typeof body !== "object") return null;
|
|
17802
18021
|
const payload = body;
|
|
17803
18022
|
const details = payload.error?.details;
|
|
@@ -17821,7 +18040,7 @@ function extractQuotaLimitValue(body) {
|
|
|
17821
18040
|
}
|
|
17822
18041
|
return null;
|
|
17823
18042
|
}
|
|
17824
|
-
function
|
|
18043
|
+
function isRetryable2(err) {
|
|
17825
18044
|
if (!(err instanceof GbpApiError)) return false;
|
|
17826
18045
|
if (err.status === 429) {
|
|
17827
18046
|
return err.quotaLimitValue !== 0;
|
|
@@ -17849,7 +18068,7 @@ async function gbpFetchOnce(url, accessToken, opts) {
|
|
|
17849
18068
|
if (!res.ok) {
|
|
17850
18069
|
const payload = body;
|
|
17851
18070
|
const message = typeof payload === "object" && payload?.error?.message ? payload.error.message : typeof payload === "string" ? payload : `HTTP ${res.status}`;
|
|
17852
|
-
throw new GbpApiError(message, res.status,
|
|
18071
|
+
throw new GbpApiError(message, res.status, extractReason2(body), body, extractQuotaLimitValue(body));
|
|
17853
18072
|
}
|
|
17854
18073
|
return body;
|
|
17855
18074
|
} finally {
|
|
@@ -17861,7 +18080,7 @@ async function gbpFetchGet(url, accessToken, opts = {}) {
|
|
|
17861
18080
|
maxRetries: opts.retry?.maxRetries ?? 5,
|
|
17862
18081
|
baseDelayMs: opts.retry?.baseDelayMs ?? 1e3,
|
|
17863
18082
|
jitter: true,
|
|
17864
|
-
isRetryable,
|
|
18083
|
+
isRetryable: isRetryable2,
|
|
17865
18084
|
sleep: opts.retry?.sleep
|
|
17866
18085
|
});
|
|
17867
18086
|
}
|
|
@@ -18001,7 +18220,7 @@ async function listPlaceActionLinks(accessToken, locationName, opts = {}) {
|
|
|
18001
18220
|
}
|
|
18002
18221
|
|
|
18003
18222
|
// ../integration-google-business-profile/src/lodging-client.ts
|
|
18004
|
-
import
|
|
18223
|
+
import crypto15 from "crypto";
|
|
18005
18224
|
async function getLodging(accessToken, locationName, opts = {}) {
|
|
18006
18225
|
const url = `${GBP_LODGING_BASE}/${locationName}/lodging?readMask=*`;
|
|
18007
18226
|
try {
|
|
@@ -18028,14 +18247,14 @@ function isPopulated(value) {
|
|
|
18028
18247
|
return true;
|
|
18029
18248
|
}
|
|
18030
18249
|
function hashLodging(lodging) {
|
|
18031
|
-
return
|
|
18250
|
+
return crypto15.createHash("sha256").update(stableStringify2(lodging)).digest("hex");
|
|
18032
18251
|
}
|
|
18033
|
-
function
|
|
18252
|
+
function stableStringify2(value) {
|
|
18034
18253
|
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
18035
|
-
if (Array.isArray(value)) return `[${value.map(
|
|
18254
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify2).join(",")}]`;
|
|
18036
18255
|
const obj = value;
|
|
18037
18256
|
const keys = Object.keys(obj).sort();
|
|
18038
|
-
return `{${keys.map((k) => `${JSON.stringify(k)}:${
|
|
18257
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify2(obj[k])}`).join(",")}}`;
|
|
18039
18258
|
}
|
|
18040
18259
|
|
|
18041
18260
|
// ../api-routes/src/google.ts
|
|
@@ -18050,7 +18269,7 @@ function scopesForConnectionType(type) {
|
|
|
18050
18269
|
}
|
|
18051
18270
|
}
|
|
18052
18271
|
function signState(payload, secret) {
|
|
18053
|
-
return
|
|
18272
|
+
return crypto16.createHmac("sha256", secret).update(payload).digest("hex");
|
|
18054
18273
|
}
|
|
18055
18274
|
function buildSignedState(data, secret) {
|
|
18056
18275
|
const payload = JSON.stringify(data);
|
|
@@ -18061,7 +18280,7 @@ function verifySignedState(encoded, secret) {
|
|
|
18061
18280
|
try {
|
|
18062
18281
|
const { payload, sig } = JSON.parse(Buffer.from(encoded, "base64url").toString());
|
|
18063
18282
|
const expected = signState(payload, secret);
|
|
18064
|
-
if (!
|
|
18283
|
+
if (!crypto16.timingSafeEqual(Buffer.from(sig, "hex"), Buffer.from(expected, "hex"))) return null;
|
|
18065
18284
|
return JSON.parse(payload);
|
|
18066
18285
|
} catch {
|
|
18067
18286
|
return null;
|
|
@@ -18331,7 +18550,7 @@ async function googleRoutes(app, opts) {
|
|
|
18331
18550
|
throw validationError('No GSC connection found for this domain. Run "canonry google connect" first.');
|
|
18332
18551
|
}
|
|
18333
18552
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
18334
|
-
const runId =
|
|
18553
|
+
const runId = crypto16.randomUUID();
|
|
18335
18554
|
app.db.insert(runs).values({
|
|
18336
18555
|
id: runId,
|
|
18337
18556
|
projectId: project.id,
|
|
@@ -18424,7 +18643,7 @@ async function googleRoutes(app, opts) {
|
|
|
18424
18643
|
const mob = ir.mobileUsabilityResult;
|
|
18425
18644
|
const rich = ir.richResultsResult;
|
|
18426
18645
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
18427
|
-
const id =
|
|
18646
|
+
const id = crypto16.randomUUID();
|
|
18428
18647
|
app.db.insert(gscUrlInspections).values({
|
|
18429
18648
|
id,
|
|
18430
18649
|
projectId: project.id,
|
|
@@ -18659,7 +18878,7 @@ async function googleRoutes(app, opts) {
|
|
|
18659
18878
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
18660
18879
|
});
|
|
18661
18880
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
18662
|
-
const runId =
|
|
18881
|
+
const runId = crypto16.randomUUID();
|
|
18663
18882
|
app.db.insert(runs).values({
|
|
18664
18883
|
id: runId,
|
|
18665
18884
|
projectId: project.id,
|
|
@@ -18685,7 +18904,7 @@ async function googleRoutes(app, opts) {
|
|
|
18685
18904
|
throw validationError("No GSC property configured for this connection");
|
|
18686
18905
|
}
|
|
18687
18906
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
18688
|
-
const runId =
|
|
18907
|
+
const runId = crypto16.randomUUID();
|
|
18689
18908
|
app.db.insert(runs).values({
|
|
18690
18909
|
id: runId,
|
|
18691
18910
|
projectId: project.id,
|
|
@@ -18847,6 +19066,8 @@ async function googleRoutes(app, opts) {
|
|
|
18847
19066
|
primaryCategoryDisplayName: row.primaryCategoryDisplayName ?? null,
|
|
18848
19067
|
storefrontAddress: row.storefrontAddress ?? null,
|
|
18849
19068
|
websiteUri: row.websiteUri ?? null,
|
|
19069
|
+
placeId: row.placeId ?? null,
|
|
19070
|
+
mapsUri: row.mapsUri ?? null,
|
|
18850
19071
|
selected: Boolean(row.selected),
|
|
18851
19072
|
syncedAt: row.syncedAt ?? null,
|
|
18852
19073
|
createdAt: row.createdAt,
|
|
@@ -18949,11 +19170,13 @@ async function googleRoutes(app, opts) {
|
|
|
18949
19170
|
primaryCategoryDisplayName: remote.categories?.primaryCategory?.displayName ?? null,
|
|
18950
19171
|
storefrontAddress: formatStorefrontAddress(remote),
|
|
18951
19172
|
websiteUri: remote.websiteUri ?? null,
|
|
19173
|
+
placeId: remote.metadata?.placeId ?? null,
|
|
19174
|
+
mapsUri: remote.metadata?.mapsUri ?? null,
|
|
18952
19175
|
updatedAt: now
|
|
18953
19176
|
}).where(eq19(gbpLocations.id, existing.id)).run();
|
|
18954
19177
|
} else {
|
|
18955
19178
|
tx.insert(gbpLocations).values({
|
|
18956
|
-
id:
|
|
19179
|
+
id: crypto16.randomUUID(),
|
|
18957
19180
|
projectId: project.id,
|
|
18958
19181
|
accountName,
|
|
18959
19182
|
locationName: remote.name,
|
|
@@ -18961,6 +19184,8 @@ async function googleRoutes(app, opts) {
|
|
|
18961
19184
|
primaryCategoryDisplayName: remote.categories?.primaryCategory?.displayName ?? null,
|
|
18962
19185
|
storefrontAddress: formatStorefrontAddress(remote),
|
|
18963
19186
|
websiteUri: remote.websiteUri ?? null,
|
|
19187
|
+
placeId: remote.metadata?.placeId ?? null,
|
|
19188
|
+
mapsUri: remote.metadata?.mapsUri ?? null,
|
|
18964
19189
|
selected: selectAllNew,
|
|
18965
19190
|
createdAt: now,
|
|
18966
19191
|
updatedAt: now
|
|
@@ -19074,7 +19299,7 @@ async function googleRoutes(app, opts) {
|
|
|
19074
19299
|
throw validationError(parsed.error.issues[0]?.message ?? "Invalid sync request");
|
|
19075
19300
|
}
|
|
19076
19301
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
19077
|
-
const runId =
|
|
19302
|
+
const runId = crypto16.randomUUID();
|
|
19078
19303
|
app.db.insert(runs).values({
|
|
19079
19304
|
id: runId,
|
|
19080
19305
|
projectId: project.id,
|
|
@@ -19151,6 +19376,26 @@ async function googleRoutes(app, opts) {
|
|
|
19151
19376
|
}));
|
|
19152
19377
|
return { lodging, total: lodging.length };
|
|
19153
19378
|
});
|
|
19379
|
+
app.get("/projects/:name/gbp/places", async (request) => {
|
|
19380
|
+
const project = resolveProject(app.db, request.params.name);
|
|
19381
|
+
const conditions = [eq19(gbpPlaceDetails.projectId, project.id)];
|
|
19382
|
+
if (request.query.locationName) conditions.push(eq19(gbpPlaceDetails.locationName, request.query.locationName));
|
|
19383
|
+
const rows = app.db.select().from(gbpPlaceDetails).where(and13(...conditions)).orderBy(desc9(gbpPlaceDetails.syncedAt)).all();
|
|
19384
|
+
const latestByLocation = /* @__PURE__ */ new Map();
|
|
19385
|
+
for (const row of rows) {
|
|
19386
|
+
if (!latestByLocation.has(row.locationName)) latestByLocation.set(row.locationName, row);
|
|
19387
|
+
}
|
|
19388
|
+
const places = [...latestByLocation.values()].map((r) => ({
|
|
19389
|
+
locationName: r.locationName,
|
|
19390
|
+
placeId: r.placeId,
|
|
19391
|
+
tier: r.tier,
|
|
19392
|
+
// Derived server-side so agents/UI consume the same amenity list.
|
|
19393
|
+
amenities: extractPlaceAmenities(r.attributes),
|
|
19394
|
+
syncedAt: r.syncedAt,
|
|
19395
|
+
place: r.attributes
|
|
19396
|
+
}));
|
|
19397
|
+
return { places, total: places.length };
|
|
19398
|
+
});
|
|
19154
19399
|
app.get("/projects/:name/gbp/summary", async (request) => {
|
|
19155
19400
|
const project = resolveProject(app.db, request.params.name);
|
|
19156
19401
|
const locationName = request.query.locationName ?? null;
|
|
@@ -19191,7 +19436,7 @@ async function googleRoutes(app, opts) {
|
|
|
19191
19436
|
}
|
|
19192
19437
|
|
|
19193
19438
|
// ../api-routes/src/bing.ts
|
|
19194
|
-
import
|
|
19439
|
+
import crypto17 from "crypto";
|
|
19195
19440
|
import { eq as eq20, and as and14, desc as desc10 } from "drizzle-orm";
|
|
19196
19441
|
|
|
19197
19442
|
// ../integration-bing/src/constants.ts
|
|
@@ -19577,7 +19822,7 @@ async function bingRoutes(app, opts) {
|
|
|
19577
19822
|
const snapshotDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
19578
19823
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
19579
19824
|
app.db.insert(bingCoverageSnapshots).values({
|
|
19580
|
-
id:
|
|
19825
|
+
id: crypto17.randomUUID(),
|
|
19581
19826
|
projectId: project.id,
|
|
19582
19827
|
syncRunId: snapshotRunId,
|
|
19583
19828
|
date: snapshotDate,
|
|
@@ -19648,7 +19893,7 @@ async function bingRoutes(app, opts) {
|
|
|
19648
19893
|
throw validationError("url is required");
|
|
19649
19894
|
}
|
|
19650
19895
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
19651
|
-
const runId =
|
|
19896
|
+
const runId = crypto17.randomUUID();
|
|
19652
19897
|
app.db.insert(runs).values({
|
|
19653
19898
|
id: runId,
|
|
19654
19899
|
projectId: project.id,
|
|
@@ -19669,7 +19914,7 @@ async function bingRoutes(app, opts) {
|
|
|
19669
19914
|
discoveryDate: result.DiscoveryDate ?? null
|
|
19670
19915
|
});
|
|
19671
19916
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
19672
|
-
const id =
|
|
19917
|
+
const id = crypto17.randomUUID();
|
|
19673
19918
|
const httpCode = result.HttpStatus ?? result.HttpCode ?? null;
|
|
19674
19919
|
const lastCrawledDate = parseBingDate(result.LastCrawledDate);
|
|
19675
19920
|
const inIndexDate = parseBingDate(result.InIndexDate);
|
|
@@ -19739,7 +19984,7 @@ async function bingRoutes(app, opts) {
|
|
|
19739
19984
|
throw validationError('No Bing site configured. Run "canonry bing set-site <project> <url>" first.');
|
|
19740
19985
|
}
|
|
19741
19986
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
19742
|
-
const runId =
|
|
19987
|
+
const runId = crypto17.randomUUID();
|
|
19743
19988
|
app.db.insert(runs).values({
|
|
19744
19989
|
id: runId,
|
|
19745
19990
|
projectId: project.id,
|
|
@@ -20023,7 +20268,7 @@ async function cdpRoutes(app, opts) {
|
|
|
20023
20268
|
}
|
|
20024
20269
|
|
|
20025
20270
|
// ../api-routes/src/ga.ts
|
|
20026
|
-
import
|
|
20271
|
+
import crypto18 from "crypto";
|
|
20027
20272
|
import { eq as eq22, desc as desc11, and as and16, sql as sql9 } from "drizzle-orm";
|
|
20028
20273
|
function gaLog(level, action, ctx) {
|
|
20029
20274
|
const entry = { ts: (/* @__PURE__ */ new Date()).toISOString(), level, module: "GA4Routes", action, ...ctx };
|
|
@@ -20279,7 +20524,7 @@ async function ga4Routes(app, opts) {
|
|
|
20279
20524
|
const syncAi = !only || only === "ai";
|
|
20280
20525
|
const syncSocial = !only || only === "social";
|
|
20281
20526
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
20282
|
-
const runId =
|
|
20527
|
+
const runId = crypto18.randomUUID();
|
|
20283
20528
|
app.db.insert(runs).values({
|
|
20284
20529
|
id: runId,
|
|
20285
20530
|
projectId: project.id,
|
|
@@ -20327,7 +20572,7 @@ async function ga4Routes(app, opts) {
|
|
|
20327
20572
|
).run();
|
|
20328
20573
|
for (const row of rows) {
|
|
20329
20574
|
tx.insert(gaTrafficSnapshots).values({
|
|
20330
|
-
id:
|
|
20575
|
+
id: crypto18.randomUUID(),
|
|
20331
20576
|
projectId: project.id,
|
|
20332
20577
|
date: row.date,
|
|
20333
20578
|
landingPage: row.landingPage,
|
|
@@ -20351,7 +20596,7 @@ async function ga4Routes(app, opts) {
|
|
|
20351
20596
|
).run();
|
|
20352
20597
|
for (const row of aiReferrals) {
|
|
20353
20598
|
tx.insert(gaAiReferrals).values({
|
|
20354
|
-
id:
|
|
20599
|
+
id: crypto18.randomUUID(),
|
|
20355
20600
|
projectId: project.id,
|
|
20356
20601
|
date: row.date,
|
|
20357
20602
|
source: row.source,
|
|
@@ -20377,7 +20622,7 @@ async function ga4Routes(app, opts) {
|
|
|
20377
20622
|
).run();
|
|
20378
20623
|
for (const row of socialReferrals) {
|
|
20379
20624
|
tx.insert(gaSocialReferrals).values({
|
|
20380
|
-
id:
|
|
20625
|
+
id: crypto18.randomUUID(),
|
|
20381
20626
|
projectId: project.id,
|
|
20382
20627
|
date: row.date,
|
|
20383
20628
|
source: row.source,
|
|
@@ -20393,7 +20638,7 @@ async function ga4Routes(app, opts) {
|
|
|
20393
20638
|
if (syncSummary) {
|
|
20394
20639
|
tx.delete(gaTrafficSummaries).where(eq22(gaTrafficSummaries.projectId, project.id)).run();
|
|
20395
20640
|
tx.insert(gaTrafficSummaries).values({
|
|
20396
|
-
id:
|
|
20641
|
+
id: crypto18.randomUUID(),
|
|
20397
20642
|
projectId: project.id,
|
|
20398
20643
|
periodStart: summary.periodStart,
|
|
20399
20644
|
periodEnd: summary.periodEnd,
|
|
@@ -20406,7 +20651,7 @@ async function ga4Routes(app, opts) {
|
|
|
20406
20651
|
tx.delete(gaTrafficWindowSummaries).where(eq22(gaTrafficWindowSummaries.projectId, project.id)).run();
|
|
20407
20652
|
for (const ws of windowSummaries) {
|
|
20408
20653
|
tx.insert(gaTrafficWindowSummaries).values({
|
|
20409
|
-
id:
|
|
20654
|
+
id: crypto18.randomUUID(),
|
|
20410
20655
|
projectId: project.id,
|
|
20411
20656
|
windowKey: ws.windowKey,
|
|
20412
20657
|
periodStart: ws.periodStart,
|
|
@@ -20977,7 +21222,7 @@ function parseSchemaPageEntry(entry) {
|
|
|
20977
21222
|
}
|
|
20978
21223
|
|
|
20979
21224
|
// ../integration-wordpress/src/wordpress-client.ts
|
|
20980
|
-
import
|
|
21225
|
+
import crypto19 from "crypto";
|
|
20981
21226
|
function validateUsername(username) {
|
|
20982
21227
|
if (!username || typeof username !== "string" || username.trim().length === 0) {
|
|
20983
21228
|
throw new WordpressApiError("AUTH_INVALID", "Username is required and must be a non-empty string", 400);
|
|
@@ -21190,7 +21435,7 @@ function buildSnippet(content) {
|
|
|
21190
21435
|
return `${text2.slice(0, 157)}...`;
|
|
21191
21436
|
}
|
|
21192
21437
|
function contentHash(content) {
|
|
21193
|
-
return
|
|
21438
|
+
return crypto19.createHash("sha256").update(content).digest("hex");
|
|
21194
21439
|
}
|
|
21195
21440
|
function buildAmbiguousSlugMessage(slug, pages) {
|
|
21196
21441
|
const candidates = pages.map((page) => {
|
|
@@ -22479,7 +22724,7 @@ async function wordpressRoutes(app, opts) {
|
|
|
22479
22724
|
}
|
|
22480
22725
|
|
|
22481
22726
|
// ../api-routes/src/backlinks.ts
|
|
22482
|
-
import
|
|
22727
|
+
import crypto20 from "crypto";
|
|
22483
22728
|
import { and as and18, asc as asc2, desc as desc12, eq as eq23, sql as sql10 } from "drizzle-orm";
|
|
22484
22729
|
|
|
22485
22730
|
// ../integration-commoncrawl/src/constants.ts
|
|
@@ -23059,7 +23304,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
23059
23304
|
const refreshed = app.db.select().from(ccReleaseSyncs).where(eq23(ccReleaseSyncs.id, existing.id)).get();
|
|
23060
23305
|
return reply.status(200).send(mapSyncRow(refreshed));
|
|
23061
23306
|
}
|
|
23062
|
-
const id =
|
|
23307
|
+
const id = crypto20.randomUUID();
|
|
23063
23308
|
app.db.insert(ccReleaseSyncs).values({
|
|
23064
23309
|
id,
|
|
23065
23310
|
release,
|
|
@@ -23116,7 +23361,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
23116
23361
|
throw validationError("Invalid release id");
|
|
23117
23362
|
}
|
|
23118
23363
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
23119
|
-
const runId =
|
|
23364
|
+
const runId = crypto20.randomUUID();
|
|
23120
23365
|
app.db.insert(runs).values({
|
|
23121
23366
|
id: runId,
|
|
23122
23367
|
projectId: project.id,
|
|
@@ -23189,12 +23434,12 @@ async function backlinksRoutes(app, opts) {
|
|
|
23189
23434
|
}
|
|
23190
23435
|
|
|
23191
23436
|
// ../api-routes/src/traffic.ts
|
|
23192
|
-
import
|
|
23437
|
+
import crypto22 from "crypto";
|
|
23193
23438
|
import { Agent as UndiciAgent } from "undici";
|
|
23194
23439
|
import { and as and19, desc as desc13, eq as eq24, gte as gte3, lte as lte2, sql as sql11 } from "drizzle-orm";
|
|
23195
23440
|
|
|
23196
23441
|
// ../integration-cloud-run/src/auth.ts
|
|
23197
|
-
import
|
|
23442
|
+
import crypto21 from "crypto";
|
|
23198
23443
|
var GOOGLE_TOKEN_URL3 = "https://oauth2.googleapis.com/token";
|
|
23199
23444
|
var CLOUD_LOGGING_READ_SCOPE = "https://www.googleapis.com/auth/logging.read";
|
|
23200
23445
|
var TOKEN_REQUEST_TIMEOUT_MS = 3e4;
|
|
@@ -23223,7 +23468,7 @@ function createServiceAccountJwt2(clientEmail, privateKey, scope) {
|
|
|
23223
23468
|
const headerB64 = encode(header);
|
|
23224
23469
|
const payloadB64 = encode(payload);
|
|
23225
23470
|
const signingInput = `${headerB64}.${payloadB64}`;
|
|
23226
|
-
const sign =
|
|
23471
|
+
const sign = crypto21.createSign("RSA-SHA256");
|
|
23227
23472
|
sign.update(signingInput);
|
|
23228
23473
|
const signature = sign.sign(privateKey, "base64url");
|
|
23229
23474
|
return `${signingInput}.${signature}`;
|
|
@@ -27089,7 +27334,7 @@ async function runBackfillTask(options) {
|
|
|
27089
27334
|
}
|
|
27090
27335
|
})();
|
|
27091
27336
|
tx.insert(rawEventSamples).values({
|
|
27092
|
-
id:
|
|
27337
|
+
id: crypto22.randomUUID(),
|
|
27093
27338
|
projectId: project.id,
|
|
27094
27339
|
sourceId: sourceRow.id,
|
|
27095
27340
|
ts: sample.observedAt,
|
|
@@ -27214,7 +27459,7 @@ async function trafficRoutes(app, opts) {
|
|
|
27214
27459
|
}).where(eq24(trafficSources.id, activeSource.id)).run();
|
|
27215
27460
|
sourceRow = app.db.select().from(trafficSources).where(eq24(trafficSources.id, activeSource.id)).get();
|
|
27216
27461
|
} else {
|
|
27217
|
-
const newId =
|
|
27462
|
+
const newId = crypto22.randomUUID();
|
|
27218
27463
|
app.db.insert(trafficSources).values({
|
|
27219
27464
|
id: newId,
|
|
27220
27465
|
projectId: project.id,
|
|
@@ -27295,7 +27540,7 @@ async function trafficRoutes(app, opts) {
|
|
|
27295
27540
|
}).where(eq24(trafficSources.id, activeSource.id)).run();
|
|
27296
27541
|
sourceRow = app.db.select().from(trafficSources).where(eq24(trafficSources.id, activeSource.id)).get();
|
|
27297
27542
|
} else {
|
|
27298
|
-
const newId =
|
|
27543
|
+
const newId = crypto22.randomUUID();
|
|
27299
27544
|
app.db.insert(trafficSources).values({
|
|
27300
27545
|
id: newId,
|
|
27301
27546
|
projectId: project.id,
|
|
@@ -27378,7 +27623,7 @@ async function trafficRoutes(app, opts) {
|
|
|
27378
27623
|
}).where(eq24(trafficSources.id, activeSource.id)).run();
|
|
27379
27624
|
sourceRow = app.db.select().from(trafficSources).where(eq24(trafficSources.id, activeSource.id)).get();
|
|
27380
27625
|
} else {
|
|
27381
|
-
const newId =
|
|
27626
|
+
const newId = crypto22.randomUUID();
|
|
27382
27627
|
app.db.insert(trafficSources).values({
|
|
27383
27628
|
id: newId,
|
|
27384
27629
|
projectId: project.id,
|
|
@@ -27426,7 +27671,7 @@ async function trafficRoutes(app, opts) {
|
|
|
27426
27671
|
const windowEnd = /* @__PURE__ */ new Date();
|
|
27427
27672
|
const startedAt = windowEnd.toISOString();
|
|
27428
27673
|
const syncStartedAtMs = windowEnd.getTime();
|
|
27429
|
-
const runId =
|
|
27674
|
+
const runId = crypto22.randomUUID();
|
|
27430
27675
|
app.db.insert(runs).values({
|
|
27431
27676
|
id: runId,
|
|
27432
27677
|
projectId: project.id,
|
|
@@ -27755,7 +28000,7 @@ async function trafficRoutes(app, opts) {
|
|
|
27755
28000
|
}
|
|
27756
28001
|
})();
|
|
27757
28002
|
tx.insert(rawEventSamples).values({
|
|
27758
|
-
id:
|
|
28003
|
+
id: crypto22.randomUUID(),
|
|
27759
28004
|
projectId: project.id,
|
|
27760
28005
|
sourceId: sourceRow.id,
|
|
27761
28006
|
ts: sample.observedAt,
|
|
@@ -27980,7 +28225,7 @@ async function trafficRoutes(app, opts) {
|
|
|
27980
28225
|
};
|
|
27981
28226
|
}
|
|
27982
28227
|
const startedAt = windowEnd.toISOString();
|
|
27983
|
-
const runId =
|
|
28228
|
+
const runId = crypto22.randomUUID();
|
|
27984
28229
|
app.db.insert(runs).values({
|
|
27985
28230
|
id: runId,
|
|
27986
28231
|
projectId: project.id,
|
|
@@ -28260,7 +28505,7 @@ async function trafficRoutes(app, opts) {
|
|
|
28260
28505
|
}
|
|
28261
28506
|
|
|
28262
28507
|
// ../api-routes/src/doctor/checks/agent.ts
|
|
28263
|
-
import
|
|
28508
|
+
import crypto23 from "crypto";
|
|
28264
28509
|
import fs6 from "fs";
|
|
28265
28510
|
import path7 from "path";
|
|
28266
28511
|
var REQUIRED_SKILLS = ["canonry", "aero"];
|
|
@@ -28413,7 +28658,7 @@ function isInstalled(dir) {
|
|
|
28413
28658
|
}
|
|
28414
28659
|
function hashInstalledFile(filePath) {
|
|
28415
28660
|
try {
|
|
28416
|
-
return
|
|
28661
|
+
return crypto23.createHash("sha256").update(fs6.readFileSync(filePath)).digest("hex");
|
|
28417
28662
|
} catch {
|
|
28418
28663
|
return void 0;
|
|
28419
28664
|
}
|
|
@@ -29007,6 +29252,84 @@ var GBP_AUTH_CHECK_BY_ID = Object.fromEntries(
|
|
|
29007
29252
|
GBP_AUTH_CHECKS.map((check) => [check.id, check])
|
|
29008
29253
|
);
|
|
29009
29254
|
|
|
29255
|
+
// ../api-routes/src/doctor/checks/places.ts
|
|
29256
|
+
import { eq as eq26 } from "drizzle-orm";
|
|
29257
|
+
var apiKeyCheck = {
|
|
29258
|
+
id: "gbp.places.api-key",
|
|
29259
|
+
category: CheckCategories.auth,
|
|
29260
|
+
scope: CheckScopes.project,
|
|
29261
|
+
title: "Google Places API key",
|
|
29262
|
+
run: (ctx) => {
|
|
29263
|
+
if (!ctx.project) {
|
|
29264
|
+
return { status: CheckStatuses.skipped, code: "gbp.places.no-project", summary: "Project context required.", remediation: null };
|
|
29265
|
+
}
|
|
29266
|
+
const cfg = ctx.getPlacesConfig?.();
|
|
29267
|
+
if (!cfg) {
|
|
29268
|
+
return {
|
|
29269
|
+
status: CheckStatuses.skipped,
|
|
29270
|
+
code: "gbp.places.config-unavailable",
|
|
29271
|
+
summary: "Places config is not available in this deployment.",
|
|
29272
|
+
remediation: null
|
|
29273
|
+
};
|
|
29274
|
+
}
|
|
29275
|
+
if (cfg.tier === "off") {
|
|
29276
|
+
return {
|
|
29277
|
+
status: CheckStatuses.skipped,
|
|
29278
|
+
code: "gbp.places.disabled",
|
|
29279
|
+
summary: "Places enrichment is disabled (tier: off).",
|
|
29280
|
+
remediation: "Set `places.tier` to `atmosphere` or `pro` in ~/.canonry/config.yaml to enable it."
|
|
29281
|
+
};
|
|
29282
|
+
}
|
|
29283
|
+
const conn = ctx.googleConnectionStore?.getConnection(ctx.project.canonicalDomain, "gbp");
|
|
29284
|
+
if (!conn) {
|
|
29285
|
+
return {
|
|
29286
|
+
status: CheckStatuses.skipped,
|
|
29287
|
+
code: "gbp.places.no-gbp-connection",
|
|
29288
|
+
summary: "No Google Business Profile connection; Places enrichment does not apply.",
|
|
29289
|
+
remediation: `Connect GBP first: \`canonry gbp connect ${ctx.project.name}\`.`
|
|
29290
|
+
};
|
|
29291
|
+
}
|
|
29292
|
+
if (!cfg.apiKey) {
|
|
29293
|
+
return {
|
|
29294
|
+
status: CheckStatuses.warn,
|
|
29295
|
+
code: "gbp.places.api-key-missing",
|
|
29296
|
+
summary: "No Places API key configured \u2014 GBP-vs-rendered-listing discrepancies cannot be detected.",
|
|
29297
|
+
remediation: "Set `GOOGLE_PLACES_API_KEY` or `places.apiKey` in ~/.canonry/config.yaml. The amenity cross-reference fits the 1,000 free Atmosphere calls/month for a typical operator book.",
|
|
29298
|
+
details: { tier: cfg.tier }
|
|
29299
|
+
};
|
|
29300
|
+
}
|
|
29301
|
+
const rows = ctx.db.select({ placeId: gbpLocations.placeId, selected: gbpLocations.selected }).from(gbpLocations).where(eq26(gbpLocations.projectId, ctx.project.id)).all();
|
|
29302
|
+
const selected = rows.filter((r) => r.selected);
|
|
29303
|
+
const locationsWithPlaceId = selected.filter((r) => Boolean(r.placeId)).length;
|
|
29304
|
+
const details = {
|
|
29305
|
+
tier: cfg.tier,
|
|
29306
|
+
refreshIntervalDays: cfg.refreshIntervalDays,
|
|
29307
|
+
selectedLocations: selected.length,
|
|
29308
|
+
locationsWithPlaceId
|
|
29309
|
+
};
|
|
29310
|
+
if (selected.length > 0 && locationsWithPlaceId === 0) {
|
|
29311
|
+
return {
|
|
29312
|
+
status: CheckStatuses.warn,
|
|
29313
|
+
code: "gbp.places.no-place-ids",
|
|
29314
|
+
summary: `None of the ${selected.length} selected location(s) have a Maps place id, so Places enrichment can't run.`,
|
|
29315
|
+
remediation: `Run \`canonry gbp locations discover ${ctx.project.name}\` to capture place ids from location metadata.`,
|
|
29316
|
+
details
|
|
29317
|
+
};
|
|
29318
|
+
}
|
|
29319
|
+
return {
|
|
29320
|
+
status: CheckStatuses.ok,
|
|
29321
|
+
code: "gbp.places.ready",
|
|
29322
|
+
summary: `Places enrichment ready (tier: ${cfg.tier}). ${locationsWithPlaceId}/${selected.length} selected location(s) have a Maps place id.`,
|
|
29323
|
+
remediation: null,
|
|
29324
|
+
details
|
|
29325
|
+
};
|
|
29326
|
+
}
|
|
29327
|
+
};
|
|
29328
|
+
var PLACES_CHECKS = [apiKeyCheck];
|
|
29329
|
+
var PLACES_CHECK_BY_ID = Object.fromEntries(
|
|
29330
|
+
PLACES_CHECKS.map((check) => [check.id, check])
|
|
29331
|
+
);
|
|
29332
|
+
|
|
29010
29333
|
// ../api-routes/src/doctor/checks/google-auth.ts
|
|
29011
29334
|
var REQUIRED_GSC_SCOPES = [GSC_SCOPE, INDEXING_SCOPE];
|
|
29012
29335
|
async function resolveAccessToken(ctx) {
|
|
@@ -29414,7 +29737,7 @@ var RUNTIME_STATE_CHECKS = [
|
|
|
29414
29737
|
];
|
|
29415
29738
|
|
|
29416
29739
|
// ../api-routes/src/doctor/checks/traffic-source.ts
|
|
29417
|
-
import { and as and21, eq as
|
|
29740
|
+
import { and as and21, eq as eq27, gte as gte4, ne as ne4, sql as sql12 } from "drizzle-orm";
|
|
29418
29741
|
var RECENT_DATA_WARN_DAYS = 7;
|
|
29419
29742
|
var RECENT_DATA_FAIL_DAYS = 30;
|
|
29420
29743
|
function skippedNoProject3() {
|
|
@@ -29429,7 +29752,7 @@ function loadProbes(ctx) {
|
|
|
29429
29752
|
if (!ctx.project) return [];
|
|
29430
29753
|
const rows = ctx.db.select().from(trafficSources).where(
|
|
29431
29754
|
and21(
|
|
29432
|
-
|
|
29755
|
+
eq27(trafficSources.projectId, ctx.project.id),
|
|
29433
29756
|
ne4(trafficSources.status, TrafficSourceStatuses.archived)
|
|
29434
29757
|
)
|
|
29435
29758
|
).all();
|
|
@@ -29510,7 +29833,7 @@ var recentDataCheck = {
|
|
|
29510
29833
|
const recentCrawlers = Number(
|
|
29511
29834
|
ctx.db.select({ total: sql12`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
|
|
29512
29835
|
and21(
|
|
29513
|
-
|
|
29836
|
+
eq27(crawlerEventsHourly.projectId, ctx.project.id),
|
|
29514
29837
|
gte4(crawlerEventsHourly.tsHour, warnCutoff)
|
|
29515
29838
|
)
|
|
29516
29839
|
).get()?.total ?? 0
|
|
@@ -29518,7 +29841,7 @@ var recentDataCheck = {
|
|
|
29518
29841
|
const recentReferrals = Number(
|
|
29519
29842
|
ctx.db.select({ total: sql12`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
|
|
29520
29843
|
and21(
|
|
29521
|
-
|
|
29844
|
+
eq27(aiReferralEventsHourly.projectId, ctx.project.id),
|
|
29522
29845
|
gte4(aiReferralEventsHourly.tsHour, warnCutoff)
|
|
29523
29846
|
)
|
|
29524
29847
|
).get()?.total ?? 0
|
|
@@ -29534,7 +29857,7 @@ var recentDataCheck = {
|
|
|
29534
29857
|
const olderCrawlers = Number(
|
|
29535
29858
|
ctx.db.select({ total: sql12`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
|
|
29536
29859
|
and21(
|
|
29537
|
-
|
|
29860
|
+
eq27(crawlerEventsHourly.projectId, ctx.project.id),
|
|
29538
29861
|
gte4(crawlerEventsHourly.tsHour, failCutoff)
|
|
29539
29862
|
)
|
|
29540
29863
|
).get()?.total ?? 0
|
|
@@ -29542,7 +29865,7 @@ var recentDataCheck = {
|
|
|
29542
29865
|
const olderReferrals = Number(
|
|
29543
29866
|
ctx.db.select({ total: sql12`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
|
|
29544
29867
|
and21(
|
|
29545
|
-
|
|
29868
|
+
eq27(aiReferralEventsHourly.projectId, ctx.project.id),
|
|
29546
29869
|
gte4(aiReferralEventsHourly.tsHour, failCutoff)
|
|
29547
29870
|
)
|
|
29548
29871
|
).get()?.total ?? 0
|
|
@@ -29793,6 +30116,7 @@ var ALL_CHECKS = [
|
|
|
29793
30116
|
...RUNTIME_STATE_CHECKS,
|
|
29794
30117
|
...GOOGLE_AUTH_CHECKS,
|
|
29795
30118
|
...GBP_AUTH_CHECKS,
|
|
30119
|
+
...PLACES_CHECKS,
|
|
29796
30120
|
...BING_AUTH_CHECKS,
|
|
29797
30121
|
...WORDPRESS_PUBLISH_CHECKS,
|
|
29798
30122
|
...GA_AUTH_CHECKS,
|
|
@@ -29882,6 +30206,7 @@ async function doctorRoutes(app, opts) {
|
|
|
29882
30206
|
wordpressConnectionStore: opts.wordpressConnectionStore,
|
|
29883
30207
|
ga4CredentialStore: opts.ga4CredentialStore,
|
|
29884
30208
|
getGoogleAuthConfig: opts.getGoogleAuthConfig,
|
|
30209
|
+
getPlacesConfig: opts.getPlacesConfig,
|
|
29885
30210
|
redirectUri,
|
|
29886
30211
|
providerSummary: opts.providerSummary,
|
|
29887
30212
|
trafficSourceValidators: opts.trafficSourceValidators,
|
|
@@ -29906,6 +30231,7 @@ async function doctorRoutes(app, opts) {
|
|
|
29906
30231
|
wordpressConnectionStore: opts.wordpressConnectionStore,
|
|
29907
30232
|
ga4CredentialStore: opts.ga4CredentialStore,
|
|
29908
30233
|
getGoogleAuthConfig: opts.getGoogleAuthConfig,
|
|
30234
|
+
getPlacesConfig: opts.getPlacesConfig,
|
|
29909
30235
|
redirectUri,
|
|
29910
30236
|
providerSummary: opts.providerSummary,
|
|
29911
30237
|
trafficSourceValidators: opts.trafficSourceValidators,
|
|
@@ -29917,8 +30243,8 @@ async function doctorRoutes(app, opts) {
|
|
|
29917
30243
|
}
|
|
29918
30244
|
|
|
29919
30245
|
// ../api-routes/src/discovery/routes.ts
|
|
29920
|
-
import
|
|
29921
|
-
import { and as and22, desc as desc14, eq as
|
|
30246
|
+
import crypto24 from "crypto";
|
|
30247
|
+
import { and as and22, desc as desc14, eq as eq28, gte as gte5, inArray as inArray10 } from "drizzle-orm";
|
|
29922
30248
|
var MAX_INFLIGHT_DISCOVERY_AGE_MS = 2 * 60 * 60 * 1e3;
|
|
29923
30249
|
async function discoveryRoutes(app, opts) {
|
|
29924
30250
|
app.post("/projects/:name/discover/run", async (request, reply) => {
|
|
@@ -29951,8 +30277,8 @@ async function discoveryRoutes(app, opts) {
|
|
|
29951
30277
|
const ageFloorIso = new Date(Date.now() - MAX_INFLIGHT_DISCOVERY_AGE_MS).toISOString();
|
|
29952
30278
|
const decision = app.db.transaction((tx) => {
|
|
29953
30279
|
const existing = tx.select({ id: discoverySessions.id, runId: discoverySessions.runId }).from(discoverySessions).where(and22(
|
|
29954
|
-
|
|
29955
|
-
|
|
30280
|
+
eq28(discoverySessions.projectId, project.id),
|
|
30281
|
+
eq28(discoverySessions.icpDescription, icpDescription),
|
|
29956
30282
|
inArray10(discoverySessions.status, [
|
|
29957
30283
|
DiscoverySessionStatuses.queued,
|
|
29958
30284
|
DiscoverySessionStatuses.seeding,
|
|
@@ -29963,8 +30289,8 @@ async function discoveryRoutes(app, opts) {
|
|
|
29963
30289
|
if (existing && existing.runId) {
|
|
29964
30290
|
return { reused: true, sessionId: existing.id, runId: existing.runId };
|
|
29965
30291
|
}
|
|
29966
|
-
const sessionId =
|
|
29967
|
-
const runId =
|
|
30292
|
+
const sessionId = crypto24.randomUUID();
|
|
30293
|
+
const runId = crypto24.randomUUID();
|
|
29968
30294
|
tx.insert(discoverySessions).values({
|
|
29969
30295
|
id: sessionId,
|
|
29970
30296
|
projectId: project.id,
|
|
@@ -30022,7 +30348,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
30022
30348
|
const project = resolveProject(app.db, request.params.name);
|
|
30023
30349
|
const parsedLimit = parseInt(request.query.limit ?? "", 10);
|
|
30024
30350
|
const limit = Number.isNaN(parsedLimit) || parsedLimit <= 0 ? 50 : parsedLimit;
|
|
30025
|
-
const rows = app.db.select().from(discoverySessions).where(
|
|
30351
|
+
const rows = app.db.select().from(discoverySessions).where(eq28(discoverySessions.projectId, project.id)).orderBy(desc14(discoverySessions.createdAt)).limit(limit).all();
|
|
30026
30352
|
return reply.send(rows.map(serializeSession));
|
|
30027
30353
|
}
|
|
30028
30354
|
);
|
|
@@ -30030,11 +30356,11 @@ async function discoveryRoutes(app, opts) {
|
|
|
30030
30356
|
"/projects/:name/discover/sessions/:id",
|
|
30031
30357
|
async (request, reply) => {
|
|
30032
30358
|
const project = resolveProject(app.db, request.params.name);
|
|
30033
|
-
const session = app.db.select().from(discoverySessions).where(
|
|
30359
|
+
const session = app.db.select().from(discoverySessions).where(eq28(discoverySessions.id, request.params.id)).get();
|
|
30034
30360
|
if (!session || session.projectId !== project.id) {
|
|
30035
30361
|
throw notFound("Discovery session", request.params.id);
|
|
30036
30362
|
}
|
|
30037
|
-
const probeRows = app.db.select().from(discoveryProbes).where(
|
|
30363
|
+
const probeRows = app.db.select().from(discoveryProbes).where(eq28(discoveryProbes.sessionId, session.id)).all();
|
|
30038
30364
|
const detail = {
|
|
30039
30365
|
...serializeSession(session),
|
|
30040
30366
|
probes: probeRows.map(serializeProbe)
|
|
@@ -30046,12 +30372,12 @@ async function discoveryRoutes(app, opts) {
|
|
|
30046
30372
|
"/projects/:name/discover/sessions/:id/promote",
|
|
30047
30373
|
async (request, reply) => {
|
|
30048
30374
|
const project = resolveProject(app.db, request.params.name);
|
|
30049
|
-
const session = app.db.select().from(discoverySessions).where(
|
|
30375
|
+
const session = app.db.select().from(discoverySessions).where(eq28(discoverySessions.id, request.params.id)).get();
|
|
30050
30376
|
if (!session || session.projectId !== project.id) {
|
|
30051
30377
|
throw notFound("Discovery session", request.params.id);
|
|
30052
30378
|
}
|
|
30053
|
-
const probeRows = app.db.select().from(discoveryProbes).where(
|
|
30054
|
-
const existingCompetitors = app.db.select({ domain: competitors.domain }).from(competitors).where(
|
|
30379
|
+
const probeRows = app.db.select().from(discoveryProbes).where(eq28(discoveryProbes.sessionId, session.id)).all();
|
|
30380
|
+
const existingCompetitors = app.db.select({ domain: competitors.domain }).from(competitors).where(eq28(competitors.projectId, project.id)).all().map((r) => r.domain.toLowerCase());
|
|
30055
30381
|
const seenCompetitors = new Set(existingCompetitors);
|
|
30056
30382
|
const cited = /* @__PURE__ */ new Set();
|
|
30057
30383
|
const aspirational = /* @__PURE__ */ new Set();
|
|
@@ -30080,7 +30406,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
30080
30406
|
);
|
|
30081
30407
|
app.post("/projects/:name/discover/sessions/:id/promote", async (request, reply) => {
|
|
30082
30408
|
const project = resolveProject(app.db, request.params.name);
|
|
30083
|
-
const session = app.db.select().from(discoverySessions).where(
|
|
30409
|
+
const session = app.db.select().from(discoverySessions).where(eq28(discoverySessions.id, request.params.id)).get();
|
|
30084
30410
|
if (!session || session.projectId !== project.id) {
|
|
30085
30411
|
throw notFound("Discovery session", request.params.id);
|
|
30086
30412
|
}
|
|
@@ -30103,7 +30429,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
30103
30429
|
const bucketSet = new Set(buckets);
|
|
30104
30430
|
const includeCompetitors = parsed.data.includeCompetitors ?? true;
|
|
30105
30431
|
const competitorTypes = parsed.data.competitorTypes ?? DEFAULT_DISCOVERY_PROMOTE_COMPETITOR_TYPES;
|
|
30106
|
-
const probeRows = app.db.select().from(discoveryProbes).where(
|
|
30432
|
+
const probeRows = app.db.select().from(discoveryProbes).where(eq28(discoveryProbes.sessionId, session.id)).all();
|
|
30107
30433
|
const candidateQueries = /* @__PURE__ */ new Set();
|
|
30108
30434
|
for (const probe of probeRows) {
|
|
30109
30435
|
if (!probe.bucket) continue;
|
|
@@ -30111,7 +30437,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
30111
30437
|
if (bucket.success && bucketSet.has(bucket.data)) candidateQueries.add(probe.query);
|
|
30112
30438
|
}
|
|
30113
30439
|
const existingQueries = new Set(
|
|
30114
|
-
app.db.select({ query: queries.query }).from(queries).where(
|
|
30440
|
+
app.db.select({ query: queries.query }).from(queries).where(eq28(queries.projectId, project.id)).all().map((r) => r.query.toLowerCase())
|
|
30115
30441
|
);
|
|
30116
30442
|
const promotedQueries = [];
|
|
30117
30443
|
const skippedQueries = [];
|
|
@@ -30127,7 +30453,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
30127
30453
|
const skippedCompetitors = [];
|
|
30128
30454
|
if (includeCompetitors) {
|
|
30129
30455
|
const existingCompetitors = new Set(
|
|
30130
|
-
app.db.select({ domain: competitors.domain }).from(competitors).where(
|
|
30456
|
+
app.db.select({ domain: competitors.domain }).from(competitors).where(eq28(competitors.projectId, project.id)).all().map((r) => r.domain.toLowerCase())
|
|
30131
30457
|
);
|
|
30132
30458
|
const competitorMap = parseCompetitorMap(session.competitorMap);
|
|
30133
30459
|
for (const entry of selectEligibleCompetitors(competitorMap, competitorTypes)) {
|
|
@@ -30146,7 +30472,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
30146
30472
|
app.db.transaction((tx) => {
|
|
30147
30473
|
for (const query of promotedQueries) {
|
|
30148
30474
|
tx.insert(queries).values({
|
|
30149
|
-
id:
|
|
30475
|
+
id: crypto24.randomUUID(),
|
|
30150
30476
|
projectId: project.id,
|
|
30151
30477
|
query,
|
|
30152
30478
|
provenance,
|
|
@@ -30155,7 +30481,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
30155
30481
|
}
|
|
30156
30482
|
for (const domain of promotedCompetitors) {
|
|
30157
30483
|
tx.insert(competitors).values({
|
|
30158
|
-
id:
|
|
30484
|
+
id: crypto24.randomUUID(),
|
|
30159
30485
|
projectId: project.id,
|
|
30160
30486
|
domain,
|
|
30161
30487
|
provenance,
|
|
@@ -30229,8 +30555,8 @@ function selectEligibleCompetitors(competitorMap, competitorTypes) {
|
|
|
30229
30555
|
}
|
|
30230
30556
|
|
|
30231
30557
|
// ../api-routes/src/discovery/orchestrate.ts
|
|
30232
|
-
import
|
|
30233
|
-
import { eq as
|
|
30558
|
+
import crypto25 from "crypto";
|
|
30559
|
+
import { eq as eq29 } from "drizzle-orm";
|
|
30234
30560
|
var DEFAULT_DEDUP_THRESHOLD = 0.85;
|
|
30235
30561
|
var DEFAULT_MAX_PROBES = 100;
|
|
30236
30562
|
var ABSOLUTE_MAX_PROBES = 500;
|
|
@@ -30285,7 +30611,7 @@ async function executeDiscovery(opts) {
|
|
|
30285
30611
|
status: DiscoverySessionStatuses.seeding,
|
|
30286
30612
|
dedupThreshold,
|
|
30287
30613
|
startedAt
|
|
30288
|
-
}).where(
|
|
30614
|
+
}).where(eq29(discoverySessions.id, opts.sessionId)).run();
|
|
30289
30615
|
const seedResult = await opts.deps.seed({
|
|
30290
30616
|
project: opts.project,
|
|
30291
30617
|
icpDescription: opts.icpDescription,
|
|
@@ -30305,7 +30631,7 @@ async function executeDiscovery(opts) {
|
|
|
30305
30631
|
seedProvider: seedResult.provider,
|
|
30306
30632
|
seedCountRaw,
|
|
30307
30633
|
seedCount
|
|
30308
|
-
}).where(
|
|
30634
|
+
}).where(eq29(discoverySessions.id, opts.sessionId)).run();
|
|
30309
30635
|
const probeRows = [];
|
|
30310
30636
|
const buckets = { cited: 0, aspirational: 0, "wasted-surface": 0 };
|
|
30311
30637
|
for (const query of probedCanonicals) {
|
|
@@ -30318,7 +30644,7 @@ async function executeDiscovery(opts) {
|
|
|
30318
30644
|
probeRows.push({ citedDomains: probe.citedDomains, bucket });
|
|
30319
30645
|
buckets[bucket]++;
|
|
30320
30646
|
opts.db.insert(discoveryProbes).values({
|
|
30321
|
-
id:
|
|
30647
|
+
id: crypto25.randomUUID(),
|
|
30322
30648
|
sessionId: opts.sessionId,
|
|
30323
30649
|
projectId: opts.project.id,
|
|
30324
30650
|
query,
|
|
@@ -30345,7 +30671,7 @@ async function executeDiscovery(opts) {
|
|
|
30345
30671
|
wastedCount: buckets["wasted-surface"],
|
|
30346
30672
|
competitorMap,
|
|
30347
30673
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
30348
|
-
}).where(
|
|
30674
|
+
}).where(eq29(discoverySessions.id, opts.sessionId)).run();
|
|
30349
30675
|
return {
|
|
30350
30676
|
buckets,
|
|
30351
30677
|
competitorMap,
|
|
@@ -30359,7 +30685,7 @@ function markSessionFailed(db, sessionId, error) {
|
|
|
30359
30685
|
status: DiscoverySessionStatuses.failed,
|
|
30360
30686
|
error,
|
|
30361
30687
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
30362
|
-
}).where(
|
|
30688
|
+
}).where(eq29(discoverySessions.id, sessionId)).run();
|
|
30363
30689
|
}
|
|
30364
30690
|
function dedupeStrings(input) {
|
|
30365
30691
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -30538,6 +30864,7 @@ async function apiRoutes(app, opts) {
|
|
|
30538
30864
|
wordpressConnectionStore: opts.wordpressConnectionStore,
|
|
30539
30865
|
ga4CredentialStore: opts.ga4CredentialStore,
|
|
30540
30866
|
getGoogleAuthConfig: opts.getGoogleAuthConfig,
|
|
30867
|
+
getPlacesConfig: opts.getPlacesConfig,
|
|
30541
30868
|
publicUrl: opts.publicUrl,
|
|
30542
30869
|
providerSummary: opts.providerSummary,
|
|
30543
30870
|
trafficSourceValidators: buildTrafficSourceValidators(opts),
|
|
@@ -30686,7 +31013,7 @@ function buildTrafficSourceValidators(opts) {
|
|
|
30686
31013
|
}
|
|
30687
31014
|
|
|
30688
31015
|
// src/intelligence-service.ts
|
|
30689
|
-
import
|
|
31016
|
+
import crypto26 from "crypto";
|
|
30690
31017
|
|
|
30691
31018
|
// src/logger.ts
|
|
30692
31019
|
var IS_TTY = process.stdout.isTTY === true;
|
|
@@ -30907,8 +31234,8 @@ var IntelligenceService = class {
|
|
|
30907
31234
|
analyzeAndPersist(runId, projectId) {
|
|
30908
31235
|
const recentRuns = this.db.select().from(runs).where(
|
|
30909
31236
|
and23(
|
|
30910
|
-
|
|
30911
|
-
or5(
|
|
31237
|
+
eq30(runs.projectId, projectId),
|
|
31238
|
+
or5(eq30(runs.status, "completed"), eq30(runs.status, "partial")),
|
|
30912
31239
|
// Defensive: RunCoordinator already skips probes before this is
|
|
30913
31240
|
// called, but if a future call site invokes analyzeAndPersist
|
|
30914
31241
|
// directly for a probe, probes still must not pollute the
|
|
@@ -30990,7 +31317,7 @@ var IntelligenceService = class {
|
|
|
30990
31317
|
* Returns the persisted insights so the coordinator can count critical/high.
|
|
30991
31318
|
*/
|
|
30992
31319
|
analyzeAndPersistGbp(runId, projectId) {
|
|
30993
|
-
const runRow = this.db.select({ createdAt: runs.createdAt, startedAt: runs.startedAt, finishedAt: runs.finishedAt }).from(runs).where(
|
|
31320
|
+
const runRow = this.db.select({ createdAt: runs.createdAt, startedAt: runs.startedAt, finishedAt: runs.finishedAt }).from(runs).where(eq30(runs.id, runId)).get();
|
|
30994
31321
|
if (!runRow) {
|
|
30995
31322
|
log.info("gbp-intelligence.skip", { runId, reason: "run not found" });
|
|
30996
31323
|
this.persistGbpInsights(runId, projectId, []);
|
|
@@ -30999,8 +31326,8 @@ var IntelligenceService = class {
|
|
|
30999
31326
|
const windowStart = runRow.startedAt ?? runRow.createdAt;
|
|
31000
31327
|
const windowEnd = runRow.finishedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
31001
31328
|
const selected = this.db.select().from(gbpLocations).where(and23(
|
|
31002
|
-
|
|
31003
|
-
|
|
31329
|
+
eq30(gbpLocations.projectId, projectId),
|
|
31330
|
+
eq30(gbpLocations.selected, true),
|
|
31004
31331
|
gte6(gbpLocations.syncedAt, windowStart),
|
|
31005
31332
|
lte3(gbpLocations.syncedAt, windowEnd)
|
|
31006
31333
|
)).all();
|
|
@@ -31034,9 +31361,11 @@ var IntelligenceService = class {
|
|
|
31034
31361
|
}
|
|
31035
31362
|
/** Build the per-location signal bundle the GBP analyzer consumes. */
|
|
31036
31363
|
buildGbpLocationSignals(projectId, locationName, displayName, fallbackDate) {
|
|
31037
|
-
const metricRows = this.db.select({ metric: gbpDailyMetrics.metric, date: gbpDailyMetrics.date, value: gbpDailyMetrics.value }).from(gbpDailyMetrics).where(and23(
|
|
31038
|
-
const placeActionRows = this.db.select({ placeActionType: gbpPlaceActions.placeActionType, providerType: gbpPlaceActions.providerType }).from(gbpPlaceActions).where(and23(
|
|
31039
|
-
const lodgingRow = this.db.select({ populatedGroupCount: gbpLodgingSnapshots.populatedGroupCount }).from(gbpLodgingSnapshots).where(and23(
|
|
31364
|
+
const metricRows = this.db.select({ metric: gbpDailyMetrics.metric, date: gbpDailyMetrics.date, value: gbpDailyMetrics.value }).from(gbpDailyMetrics).where(and23(eq30(gbpDailyMetrics.projectId, projectId), eq30(gbpDailyMetrics.locationName, locationName))).all();
|
|
31365
|
+
const placeActionRows = this.db.select({ placeActionType: gbpPlaceActions.placeActionType, providerType: gbpPlaceActions.providerType }).from(gbpPlaceActions).where(and23(eq30(gbpPlaceActions.projectId, projectId), eq30(gbpPlaceActions.locationName, locationName))).all();
|
|
31366
|
+
const lodgingRow = this.db.select({ populatedGroupCount: gbpLodgingSnapshots.populatedGroupCount }).from(gbpLodgingSnapshots).where(and23(eq30(gbpLodgingSnapshots.projectId, projectId), eq30(gbpLodgingSnapshots.locationName, locationName))).orderBy(desc15(gbpLodgingSnapshots.syncedAt)).limit(1).get();
|
|
31367
|
+
const placeRow = this.db.select({ attributes: gbpPlaceDetails.attributes }).from(gbpPlaceDetails).where(and23(eq30(gbpPlaceDetails.projectId, projectId), eq30(gbpPlaceDetails.locationName, locationName))).orderBy(desc15(gbpPlaceDetails.syncedAt)).limit(1).get();
|
|
31368
|
+
const placesAmenities = placeRow ? extractPlaceAmenities(placeRow.attributes) : [];
|
|
31040
31369
|
const referenceDate = metricRows.reduce((max, r) => r.date > max ? r.date : max, "") || fallbackDate;
|
|
31041
31370
|
const summary = buildGbpSummary({
|
|
31042
31371
|
locationName,
|
|
@@ -31057,6 +31386,7 @@ var IntelligenceService = class {
|
|
|
31057
31386
|
metricDeltaPct: summary.performance.deltaPct,
|
|
31058
31387
|
lodgingCapable: summary.lodging.lodgingLocationCount > 0,
|
|
31059
31388
|
lodgingEmpty: summary.lodging.emptyLodgingCount > 0,
|
|
31389
|
+
placesAmenities,
|
|
31060
31390
|
placeActionCount: summary.placeActions.total,
|
|
31061
31391
|
hasDirectMerchantCta: summary.placeActions.hasDirectMerchantCta,
|
|
31062
31392
|
keywordRecentMonth: trend.recentMonth,
|
|
@@ -31067,7 +31397,7 @@ var IntelligenceService = class {
|
|
|
31067
31397
|
/** Build the month-over-month keyword series for a location from the
|
|
31068
31398
|
* accumulating gbp_keyword_monthly table (latest complete month vs prior). */
|
|
31069
31399
|
buildGbpKeywordTrend(projectId, locationName) {
|
|
31070
|
-
const rows = this.db.select({ month: gbpKeywordMonthly.month, keyword: gbpKeywordMonthly.keyword, valueCount: gbpKeywordMonthly.valueCount }).from(gbpKeywordMonthly).where(and23(
|
|
31400
|
+
const rows = this.db.select({ month: gbpKeywordMonthly.month, keyword: gbpKeywordMonthly.keyword, valueCount: gbpKeywordMonthly.valueCount }).from(gbpKeywordMonthly).where(and23(eq30(gbpKeywordMonthly.projectId, projectId), eq30(gbpKeywordMonthly.locationName, locationName))).all();
|
|
31071
31401
|
if (rows.length === 0) return { recentMonth: null, priorMonth: null, points: [] };
|
|
31072
31402
|
const months = [...new Set(rows.map((r) => r.month))].sort().reverse();
|
|
31073
31403
|
const recentMonth = months[0] ?? null;
|
|
@@ -31093,12 +31423,12 @@ var IntelligenceService = class {
|
|
|
31093
31423
|
*/
|
|
31094
31424
|
persistGbpInsights(runId, projectId, gbpInsights) {
|
|
31095
31425
|
const previouslyDismissed = /* @__PURE__ */ new Set();
|
|
31096
|
-
const existing = this.db.select({ id: insights.id, dismissed: insights.dismissed }).from(insights).where(
|
|
31426
|
+
const existing = this.db.select({ id: insights.id, dismissed: insights.dismissed }).from(insights).where(eq30(insights.runId, runId)).all();
|
|
31097
31427
|
for (const row of existing) {
|
|
31098
31428
|
if (row.dismissed) previouslyDismissed.add(row.id);
|
|
31099
31429
|
}
|
|
31100
31430
|
this.db.transaction((tx) => {
|
|
31101
|
-
tx.delete(insights).where(
|
|
31431
|
+
tx.delete(insights).where(eq30(insights.runId, runId)).run();
|
|
31102
31432
|
for (const insight of gbpInsights) {
|
|
31103
31433
|
tx.insert(insights).values({
|
|
31104
31434
|
id: insight.id,
|
|
@@ -31173,7 +31503,7 @@ var IntelligenceService = class {
|
|
|
31173
31503
|
* create per run + aggregate). DB is left untouched.
|
|
31174
31504
|
*/
|
|
31175
31505
|
backfill(projectName, opts, onProgress) {
|
|
31176
|
-
const project = this.db.select().from(projects).where(
|
|
31506
|
+
const project = this.db.select().from(projects).where(eq30(projects.name, projectName)).get();
|
|
31177
31507
|
if (!project) {
|
|
31178
31508
|
throw new Error(`Project "${projectName}" not found`);
|
|
31179
31509
|
}
|
|
@@ -31187,8 +31517,8 @@ var IntelligenceService = class {
|
|
|
31187
31517
|
}
|
|
31188
31518
|
const allRuns = this.db.select().from(runs).where(
|
|
31189
31519
|
and23(
|
|
31190
|
-
|
|
31191
|
-
or5(
|
|
31520
|
+
eq30(runs.projectId, project.id),
|
|
31521
|
+
or5(eq30(runs.status, "completed"), eq30(runs.status, "partial")),
|
|
31192
31522
|
// Backfill must not replay probe runs as if they were real sweeps.
|
|
31193
31523
|
ne5(runs.trigger, RunTriggers.probe)
|
|
31194
31524
|
)
|
|
@@ -31267,7 +31597,7 @@ var IntelligenceService = class {
|
|
|
31267
31597
|
return { processed, skipped, totalInsights };
|
|
31268
31598
|
}
|
|
31269
31599
|
loadTrackedCompetitors(projectId) {
|
|
31270
|
-
return this.db.select({ domain: competitors.domain }).from(competitors).where(
|
|
31600
|
+
return this.db.select({ domain: competitors.domain }).from(competitors).where(eq30(competitors.projectId, projectId)).all().map((r) => r.domain);
|
|
31271
31601
|
}
|
|
31272
31602
|
/**
|
|
31273
31603
|
* Wipe transition signals from an analysis result while keeping health.
|
|
@@ -31288,15 +31618,15 @@ var IntelligenceService = class {
|
|
|
31288
31618
|
}
|
|
31289
31619
|
persistResult(result, runId, projectId) {
|
|
31290
31620
|
const previouslyDismissed = /* @__PURE__ */ new Set();
|
|
31291
|
-
const existingInsights = this.db.select({ query: insights.query, provider: insights.provider, type: insights.type, dismissed: insights.dismissed }).from(insights).where(
|
|
31621
|
+
const existingInsights = this.db.select({ query: insights.query, provider: insights.provider, type: insights.type, dismissed: insights.dismissed }).from(insights).where(eq30(insights.runId, runId)).all();
|
|
31292
31622
|
for (const row of existingInsights) {
|
|
31293
31623
|
if (row.dismissed) {
|
|
31294
31624
|
previouslyDismissed.add(`${row.query}:${row.provider}:${row.type}`);
|
|
31295
31625
|
}
|
|
31296
31626
|
}
|
|
31297
31627
|
this.db.transaction((tx) => {
|
|
31298
|
-
tx.delete(insights).where(
|
|
31299
|
-
tx.delete(healthSnapshots).where(
|
|
31628
|
+
tx.delete(insights).where(eq30(insights.runId, runId)).run();
|
|
31629
|
+
tx.delete(healthSnapshots).where(eq30(healthSnapshots.runId, runId)).run();
|
|
31300
31630
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
31301
31631
|
for (const insight of result.insights) {
|
|
31302
31632
|
const wasDismissed = previouslyDismissed.has(`${insight.query}:${insight.provider}:${insight.type}`);
|
|
@@ -31316,7 +31646,7 @@ var IntelligenceService = class {
|
|
|
31316
31646
|
}).run();
|
|
31317
31647
|
}
|
|
31318
31648
|
tx.insert(healthSnapshots).values({
|
|
31319
|
-
id:
|
|
31649
|
+
id: crypto26.randomUUID(),
|
|
31320
31650
|
projectId,
|
|
31321
31651
|
runId,
|
|
31322
31652
|
overallCitedRate: String(result.health.overallCitedRate),
|
|
@@ -31347,14 +31677,14 @@ var IntelligenceService = class {
|
|
|
31347
31677
|
applySeverityTiering(rawInsights, excludeRunId, projectId) {
|
|
31348
31678
|
const regressions = rawInsights.filter((i) => i.type === "regression");
|
|
31349
31679
|
if (regressions.length === 0) return rawInsights;
|
|
31350
|
-
const gscRows = this.db.select({ query: gscSearchData.query, impressions: gscSearchData.impressions }).from(gscSearchData).where(
|
|
31680
|
+
const gscRows = this.db.select({ query: gscSearchData.query, impressions: gscSearchData.impressions }).from(gscSearchData).where(eq30(gscSearchData.projectId, projectId)).all();
|
|
31351
31681
|
const gscConnected = gscRows.length > 0;
|
|
31352
31682
|
const gscImpressionsByQuery = /* @__PURE__ */ new Map();
|
|
31353
31683
|
for (const row of gscRows) {
|
|
31354
31684
|
const key = row.query.toLowerCase();
|
|
31355
31685
|
gscImpressionsByQuery.set(key, (gscImpressionsByQuery.get(key) ?? 0) + row.impressions);
|
|
31356
31686
|
}
|
|
31357
|
-
const projectRow = this.db.select({ locations: projects.locations }).from(projects).where(
|
|
31687
|
+
const projectRow = this.db.select({ locations: projects.locations }).from(projects).where(eq30(projects.id, projectId)).get();
|
|
31358
31688
|
const locationCount = Math.max(
|
|
31359
31689
|
1,
|
|
31360
31690
|
(projectRow?.locations ?? []).length
|
|
@@ -31362,9 +31692,9 @@ var IntelligenceService = class {
|
|
|
31362
31692
|
const ROWS_PER_GROUP_BUDGET = Math.max(2, locationCount);
|
|
31363
31693
|
const recentRunRows = this.db.select({ id: runs.id, createdAt: runs.createdAt }).from(runs).where(
|
|
31364
31694
|
and23(
|
|
31365
|
-
|
|
31366
|
-
|
|
31367
|
-
or5(
|
|
31695
|
+
eq30(runs.projectId, projectId),
|
|
31696
|
+
eq30(runs.kind, RunKinds["answer-visibility"]),
|
|
31697
|
+
or5(eq30(runs.status, "completed"), eq30(runs.status, "partial")),
|
|
31368
31698
|
// Defensive — see top of file.
|
|
31369
31699
|
ne5(runs.trigger, RunTriggers.probe)
|
|
31370
31700
|
)
|
|
@@ -31384,7 +31714,7 @@ var IntelligenceService = class {
|
|
|
31384
31714
|
const haveHistory = recentRunIds.length > 0;
|
|
31385
31715
|
const priorRegressionsByPair = /* @__PURE__ */ new Map();
|
|
31386
31716
|
if (haveHistory) {
|
|
31387
|
-
const priorRows = this.db.select({ query: insights.query, provider: insights.provider, runId: insights.runId }).from(insights).where(and23(
|
|
31717
|
+
const priorRows = this.db.select({ query: insights.query, provider: insights.provider, runId: insights.runId }).from(insights).where(and23(eq30(insights.type, "regression"), inArray11(insights.runId, recentRunIds))).all();
|
|
31388
31718
|
const regressionGroups = /* @__PURE__ */ new Map();
|
|
31389
31719
|
for (const row of priorRows) {
|
|
31390
31720
|
if (!row.runId) continue;
|
|
@@ -31413,7 +31743,7 @@ var IntelligenceService = class {
|
|
|
31413
31743
|
});
|
|
31414
31744
|
}
|
|
31415
31745
|
buildRunData(runId, projectId, completedAt, location = null) {
|
|
31416
|
-
const projectDomainRow = this.db.select({ canonicalDomain: projects.canonicalDomain, ownedDomains: projects.ownedDomains }).from(projects).where(
|
|
31746
|
+
const projectDomainRow = this.db.select({ canonicalDomain: projects.canonicalDomain, ownedDomains: projects.ownedDomains }).from(projects).where(eq30(projects.id, projectId)).get();
|
|
31417
31747
|
const projectDomains = projectDomainRow ? effectiveDomains({
|
|
31418
31748
|
canonicalDomain: projectDomainRow.canonicalDomain,
|
|
31419
31749
|
ownedDomains: projectDomainRow.ownedDomains
|
|
@@ -31429,7 +31759,7 @@ var IntelligenceService = class {
|
|
|
31429
31759
|
citedDomains: querySnapshots.citedDomains,
|
|
31430
31760
|
competitorOverlap: querySnapshots.competitorOverlap,
|
|
31431
31761
|
snapshotLocation: querySnapshots.location
|
|
31432
|
-
}).from(querySnapshots).leftJoin(queries,
|
|
31762
|
+
}).from(querySnapshots).leftJoin(queries, eq30(querySnapshots.queryId, queries.id)).where(eq30(querySnapshots.runId, runId)).all();
|
|
31433
31763
|
const snapshots = [];
|
|
31434
31764
|
let orphanCount = 0;
|
|
31435
31765
|
for (const r of rows) {
|
|
@@ -31501,6 +31831,7 @@ export {
|
|
|
31501
31831
|
gbpKeywordMonthly,
|
|
31502
31832
|
gbpPlaceActions,
|
|
31503
31833
|
gbpLodgingSnapshots,
|
|
31834
|
+
gbpPlaceDetails,
|
|
31504
31835
|
createClient,
|
|
31505
31836
|
parseJsonColumn,
|
|
31506
31837
|
extractLegacyCredentials,
|
|
@@ -31512,6 +31843,8 @@ export {
|
|
|
31512
31843
|
determineCitationState,
|
|
31513
31844
|
computeCompetitorOverlap,
|
|
31514
31845
|
extractRecommendedCompetitors,
|
|
31846
|
+
getPlaceDetails,
|
|
31847
|
+
hashPlaceDetails,
|
|
31515
31848
|
queueRunIfProjectIdle,
|
|
31516
31849
|
resolveWebhookTarget,
|
|
31517
31850
|
deliverWebhook,
|