@ainyc/canonry 4.88.0 → 4.90.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.
Files changed (30) hide show
  1. package/assets/agent-workspace/skills/aero/references/regression-playbook.md +2 -2
  2. package/assets/agent-workspace/skills/canonry/SKILL.md +1 -1
  3. package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +20 -2
  4. package/assets/agent-workspace/skills/canonry/references/google-business-profile.md +11 -11
  5. package/assets/agent-workspace/skills/canonry/references/server-side-traffic.md +4 -1
  6. package/assets/assets/{BacklinksPage-Dpx6ylmU.js → BacklinksPage-CzZbUMMc.js} +1 -1
  7. package/assets/assets/{ChartPrimitives-CG5EGu62.js → ChartPrimitives-BBcVCGyK.js} +1 -1
  8. package/assets/assets/{ProjectPage-B_xnYE7W.js → ProjectPage-BZwzpRBL.js} +1 -1
  9. package/assets/assets/{RunRow-C0V4vf-u.js → RunRow-B-Xzc9el.js} +1 -1
  10. package/assets/assets/{RunsPage-9gWTjteG.js → RunsPage-idZt03Qs.js} +1 -1
  11. package/assets/assets/{SettingsPage-kBwz4Glg.js → SettingsPage-BGMJxf9N.js} +1 -1
  12. package/assets/assets/{TrafficPage-D4adjs8S.js → TrafficPage-UpFu1vki.js} +1 -1
  13. package/assets/assets/{TrafficSourceDetailPage-CmURRHaU.js → TrafficSourceDetailPage-m0OEVrkk.js} +1 -1
  14. package/assets/assets/{arrow-left-DJ0-oFbd.js → arrow-left-Cgp2_UfR.js} +1 -1
  15. package/assets/assets/{extract-error-message-DfBzETgi.js → extract-error-message-BiiNM-57.js} +1 -1
  16. package/assets/assets/{index-BeuGfMy8.js → index-DPPOgIIL.js} +92 -92
  17. package/assets/assets/index-fLUYE9Z5.css +1 -0
  18. package/assets/assets/{trash-2-Bjf6mCqw.js → trash-2-Bm_IgAE2.js} +1 -1
  19. package/assets/index.html +2 -2
  20. package/dist/{chunk-2VEFNAQ3.js → chunk-I4FR66PU.js} +52 -7
  21. package/dist/{chunk-WMZJO56S.js → chunk-KO3T36E5.js} +80 -9
  22. package/dist/{chunk-JZ67YCHT.js → chunk-PL2ODHUC.js} +77 -1
  23. package/dist/{chunk-RH3QYFX3.js → chunk-XG7JKKTC.js} +2 -2
  24. package/dist/cli.js +36 -12
  25. package/dist/index.d.ts +9 -1
  26. package/dist/index.js +4 -4
  27. package/dist/{intelligence-service-WTEEV46F.js → intelligence-service-DZK4FOAJ.js} +2 -2
  28. package/dist/mcp.js +2 -2
  29. package/package.json +7 -7
  30. package/assets/assets/index-ClkRAeHL.css +0 -1
@@ -9,7 +9,7 @@ import {
9
9
  loadConfig,
10
10
  loadConfigRaw,
11
11
  saveConfigPatch
12
- } from "./chunk-RH3QYFX3.js";
12
+ } from "./chunk-XG7JKKTC.js";
13
13
  import {
14
14
  CC_CACHE_DIR,
15
15
  DUCKDB_SPEC,
@@ -104,7 +104,7 @@ import {
104
104
  siteAuditPages,
105
105
  siteAuditSnapshots,
106
106
  usageCounters
107
- } from "./chunk-WMZJO56S.js";
107
+ } from "./chunk-KO3T36E5.js";
108
108
  import {
109
109
  AGENT_MEMORY_VALUE_MAX_BYTES,
110
110
  AGENT_PROVIDER_IDS,
@@ -136,6 +136,7 @@ import {
136
136
  agentMemoryUpsertRequestSchema,
137
137
  authInvalid,
138
138
  authRequired,
139
+ buildEmbedClientConfig,
139
140
  buildRunErrorFromMessages,
140
141
  classifySkillFile,
141
142
  coerceSkillManifest,
@@ -147,20 +148,23 @@ import {
147
148
  effectiveBrandNames,
148
149
  effectiveDomains,
149
150
  factorStatusFromScore,
151
+ frameAncestorsHeaderValue,
150
152
  hostOf,
151
153
  isAgentProviderId,
152
154
  isBrowserProvider,
153
155
  isRetryableHttpError,
154
156
  normalizeUrlPath,
155
157
  notFound,
158
+ parseOriginList,
156
159
  providerError,
157
160
  resolveSnapshotRequestQueries,
158
161
  serializeRunError,
159
162
  skillsClientSchema,
163
+ splitList,
160
164
  validationError,
161
165
  winnabilityClassLabel,
162
166
  withRetry
163
- } from "./chunk-JZ67YCHT.js";
167
+ } from "./chunk-PL2ODHUC.js";
164
168
 
165
169
  // src/telemetry.ts
166
170
  import crypto from "crypto";
@@ -2728,6 +2732,34 @@ var perplexityAdapter = {
2728
2732
  }
2729
2733
  };
2730
2734
 
2735
+ // src/embed.ts
2736
+ function resolveEmbedConfig(env, config) {
2737
+ const embed = config.embed;
2738
+ const rawEnabled = env.CANONRY_EMBED?.trim();
2739
+ const enabled = rawEnabled ? rawEnabled === "1" || rawEnabled.toLowerCase() === "true" : embed?.enabled === true;
2740
+ const allowedOrigins = env.CANONRY_EMBED_ORIGINS !== void 0 ? parseOriginList(env.CANONRY_EMBED_ORIGINS) : parseOriginList(embed?.allowOrigins);
2741
+ const rawViews = env.CANONRY_EMBED_VIEWS !== void 0 ? splitList(env.CANONRY_EMBED_VIEWS) : splitList(embed?.views);
2742
+ const views = normalizeViews(rawViews);
2743
+ return {
2744
+ enabled,
2745
+ allowedOrigins,
2746
+ ...views ? { views } : {},
2747
+ ...embed?.theme ? { theme: embed.theme } : {}
2748
+ };
2749
+ }
2750
+ function normalizeViews(raw) {
2751
+ if (raw.length === 0) return void 0;
2752
+ const seen = /* @__PURE__ */ new Set();
2753
+ const out = [];
2754
+ for (const view of raw) {
2755
+ const id = view.toLowerCase();
2756
+ if (seen.has(id)) continue;
2757
+ seen.add(id);
2758
+ out.push(id);
2759
+ }
2760
+ return out.length > 0 ? out : void 0;
2761
+ }
2762
+
2731
2763
  // src/places-config.ts
2732
2764
  var DEFAULT_TIER = "atmosphere";
2733
2765
  var DEFAULT_REFRESH_INTERVAL_DAYS = 7;
@@ -4035,6 +4067,8 @@ async function executeGbpSync(db, runId, projectId, opts) {
4035
4067
  syncedAt: insertNow,
4036
4068
  syncRunId: runId
4037
4069
  }).run();
4070
+ } else if (lodging !== null && latestLodging) {
4071
+ tx.update(gbpLodgingSnapshots).set({ syncedAt: insertNow, syncRunId: runId }).where(eq3(gbpLodgingSnapshots.id, latestLodging.id)).run();
4038
4072
  }
4039
4073
  if (placeToWrite) {
4040
4074
  tx.insert(gbpPlaceDetails).values({
@@ -6289,7 +6323,7 @@ function readStoredGroundingSources(rawResponse) {
6289
6323
  return result;
6290
6324
  }
6291
6325
  async function backfillInsightsCommand(project, opts) {
6292
- const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-WTEEV46F.js");
6326
+ const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-DZK4FOAJ.js");
6293
6327
  const config = loadConfig();
6294
6328
  const db = createClient(config.database);
6295
6329
  migrate(db);
@@ -10538,6 +10572,8 @@ async function createServer(opts) {
10538
10572
  const rawBasePath = process.env.CANONRY_BASE_PATH ?? opts.config.basePath;
10539
10573
  const normalizedBasePath = rawBasePath ? "/" + rawBasePath.replace(/^\//, "").replace(/\/?$/, "/") : void 0;
10540
10574
  const basePath = normalizedBasePath === "/" ? void 0 : normalizedBasePath;
10575
+ const embed = resolveEmbedConfig(process.env, opts.config);
10576
+ const embedCsp = embed.enabled ? frameAncestorsHeaderValue(embed.allowedOrigins) : void 0;
10541
10577
  const apiPrefix = basePath ? `${basePath}api/v1` : "/api/v1";
10542
10578
  if (opts.config.apiKey) {
10543
10579
  const keyHash = hashApiKey(opts.config.apiKey);
@@ -11173,16 +11209,25 @@ async function createServer(opts) {
11173
11209
  }
11174
11210
  });
11175
11211
  const dirname = path9.dirname(fileURLToPath3(import.meta.url));
11176
- const assetsDir = path9.join(dirname, "..", "assets");
11212
+ const assetsDir = opts.assetsDir ?? path9.join(dirname, "..", "assets");
11177
11213
  if (fs8.existsSync(assetsDir)) {
11178
11214
  const indexPath = path9.join(assetsDir, "index.html");
11179
11215
  const injectConfig = (html) => {
11180
11216
  const clientConfig = {};
11181
11217
  if (basePath) clientConfig.basePath = basePath;
11218
+ if (embed.enabled) {
11219
+ const embedClient = buildEmbedClientConfig(embed);
11220
+ if (embedClient) clientConfig.embed = embedClient;
11221
+ }
11182
11222
  const configScript = `<script>window.__CANONRY_CONFIG__=${JSON.stringify(clientConfig)}</script>`;
11183
11223
  const baseTag = `<base href="${basePath ?? "/"}">`;
11184
11224
  return html.replace("<head>", `<head>${baseTag}`).replace("</head>", `${configScript}</head>`);
11185
11225
  };
11226
+ const sendSpaDocument = (reply, html) => {
11227
+ reply.header("Cache-Control", "no-cache, must-revalidate");
11228
+ if (embedCsp) reply.header("Content-Security-Policy", embedCsp);
11229
+ return reply.type("text/html").send(injectConfig(html));
11230
+ };
11186
11231
  const fastifyStatic = await import("@fastify/static");
11187
11232
  await app.register(fastifyStatic.default, {
11188
11233
  root: assetsDir,
@@ -11209,7 +11254,7 @@ async function createServer(opts) {
11209
11254
  const serveIndex = (_request, reply) => {
11210
11255
  if (fs8.existsSync(indexPath)) {
11211
11256
  const html = fs8.readFileSync(indexPath, "utf-8");
11212
- return reply.header("Cache-Control", "no-cache, must-revalidate").type("text/html").send(injectConfig(html));
11257
+ return sendSpaDocument(reply, html);
11213
11258
  }
11214
11259
  return reply.status(404).send({ error: "Dashboard not built" });
11215
11260
  };
@@ -11230,7 +11275,7 @@ async function createServer(opts) {
11230
11275
  }
11231
11276
  if (fs8.existsSync(indexPath)) {
11232
11277
  const html = fs8.readFileSync(indexPath, "utf-8");
11233
- return reply.header("Cache-Control", "no-cache, must-revalidate").type("text/html").send(injectConfig(html));
11278
+ return sendSpaDocument(reply, html);
11234
11279
  }
11235
11280
  return reply.status(404).send({ error: "Not found" });
11236
11281
  });
@@ -258,7 +258,7 @@ import {
258
258
  wordpressSchemaDeployResultDtoSchema,
259
259
  wordpressSchemaStatusResultDtoSchema,
260
260
  wordpressStatusDtoSchema
261
- } from "./chunk-JZ67YCHT.js";
261
+ } from "./chunk-PL2ODHUC.js";
262
262
 
263
263
  // src/intelligence-service.ts
264
264
  import { eq as eq37, desc as desc18, asc as asc5, and as and27, ne as ne5, or as or5, inArray as inArray14, gte as gte7, lte as lte4 } from "drizzle-orm";
@@ -1170,6 +1170,16 @@ var gbpLocations = sqliteTable("gbp_locations", {
1170
1170
  // rendered-listing data. Null when Google has not assigned a Place ID.
1171
1171
  placeId: text("place_id"),
1172
1172
  mapsUri: text("maps_uri"),
1173
+ // Owner-authored profile content from the Business Information v1 Location
1174
+ // resource — the entity-anchor + qualifier signals AI answer engines weight.
1175
+ // `serviceArea` / `regularHours` are stored verbatim (presence + raw shape).
1176
+ additionalCategories: text("additional_categories", { mode: "json" }).$type(),
1177
+ description: text("description"),
1178
+ serviceArea: text("service_area", { mode: "json" }).$type(),
1179
+ regularHours: text("regular_hours", { mode: "json" }).$type(),
1180
+ primaryPhone: text("primary_phone"),
1181
+ openStatus: text("open_status"),
1182
+ openingDate: text("opening_date"),
1173
1183
  selected: integer("selected", { mode: "boolean" }).notNull().default(true),
1174
1184
  syncedAt: text("synced_at"),
1175
1185
  createdAt: text("created_at").notNull(),
@@ -3134,6 +3144,28 @@ var MIGRATION_VERSIONS = [
3134
3144
  db.run(sql.raw(`ALTER TABLE health_snapshots ADD COLUMN mentioned_pairs INTEGER`));
3135
3145
  }
3136
3146
  }
3147
+ },
3148
+ {
3149
+ version: 81,
3150
+ name: "gbp-locations-owner-content",
3151
+ statements: [],
3152
+ run: (db) => {
3153
+ if (!tableExists(db, "gbp_locations")) return;
3154
+ const cols = [
3155
+ "additional_categories",
3156
+ "description",
3157
+ "service_area",
3158
+ "regular_hours",
3159
+ "primary_phone",
3160
+ "open_status",
3161
+ "opening_date"
3162
+ ];
3163
+ for (const col of cols) {
3164
+ if (!columnExists(db, "gbp_locations", col)) {
3165
+ db.run(sql.raw(`ALTER TABLE gbp_locations ADD COLUMN ${col} TEXT`));
3166
+ }
3167
+ }
3168
+ }
3137
3169
  }
3138
3170
  ];
3139
3171
  function rebuildBacklinkTableWithSource(tx, table) {
@@ -5217,22 +5249,22 @@ function analyzeGbp(signals) {
5217
5249
  drafts.push({
5218
5250
  ...base,
5219
5251
  type: "gbp-listing-discrepancy",
5220
- severity: "high",
5221
- title: `${loc.displayName}: public listing shows ${loc.placesAmenities.length} amenit${loc.placesAmenities.length === 1 ? "y" : "ies"} your GBP profile doesn\u2019t`,
5252
+ severity: "medium",
5253
+ title: `${loc.displayName}: public listing advertises ${loc.placesAmenities.length} amenit${loc.placesAmenities.length === 1 ? "y" : "ies"} canonry can\u2019t confirm via the GBP API`,
5222
5254
  recommendation: {
5223
- 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",
5224
- 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.`
5255
+ action: 'Verify these amenities are set in the Google Business Profile "Hotel details" panel so the structured profile matches what the public listing advertises.',
5256
+ reason: `Google\u2019s rendered listing advertises ${amenityList}, but the GBP Lodging API returns no structured attributes for this location. The Lodging API does not expose the owner-set "Hotel details" amenity panel, so the amenities may already be set there and simply not be readable via the API. Verify in Hotel details: if any are missing, add them, since the structured attributes are the amenity data you directly control and that AI answer engines cite.`
5225
5257
  }
5226
5258
  });
5227
5259
  } else {
5228
5260
  drafts.push({
5229
5261
  ...base,
5230
5262
  type: "gbp-lodging-gap",
5231
- severity: "high",
5232
- title: `${loc.displayName}: lodging profile has no structured attributes`,
5263
+ severity: "low",
5264
+ title: `${loc.displayName}: structured lodging attributes not readable via the GBP API`,
5233
5265
  recommendation: {
5234
- action: "Populate the hotel\u2019s structured amenity attributes in Google Business Profile \u2014 the amenity source you directly control",
5235
- 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."
5266
+ action: 'Verify the hotel amenities in the Google Business Profile "Hotel details" panel. If they are already set there, no change is needed.',
5267
+ reason: 'The GBP Lodging API returns no structured attributes for this location. That resource is commonly empty even for complete hotels, because the owner-set "Hotel details" amenity panel (breakfast, wifi, parking, accessibility, and the like) writes to a separate surface the Lodging API does not expose. Treat this as a verify, not a confirmed gap: confirm the amenities are set in Hotel details, the amenity source you directly control and that AI answer engines cite.'
5236
5268
  }
5237
5269
  });
5238
5270
  }
@@ -19710,6 +19742,15 @@ var GBP_LOCATIONS_DEFAULT_READ_MASK = [
19710
19742
  "storefrontAddress",
19711
19743
  "websiteUri",
19712
19744
  "categories.primaryCategory.displayName",
19745
+ // Owner-authored profile content — the entity-anchor and qualifier signals AI
19746
+ // answer engines weight most. All ride the same Business Information v1
19747
+ // Location resource canonry already reads (no new auth / API enablement).
19748
+ "categories.additionalCategories.displayName",
19749
+ "profile.description",
19750
+ "serviceArea",
19751
+ "regularHours",
19752
+ "phoneNumbers",
19753
+ "openInfo",
19713
19754
  "metadata.placeId",
19714
19755
  "metadata.mapsUri"
19715
19756
  ].join(",");
@@ -19847,6 +19888,26 @@ function formatStorefrontAddress(loc) {
19847
19888
  ].filter((p) => Boolean(p));
19848
19889
  return parts.length ? parts.join(", ") : null;
19849
19890
  }
19891
+ function buildLocationProfileFields(loc) {
19892
+ return {
19893
+ additionalCategories: (loc.categories?.additionalCategories ?? []).map((c) => c.displayName).filter((n) => Boolean(n)),
19894
+ description: loc.profile?.description ?? null,
19895
+ serviceArea: loc.serviceArea ?? null,
19896
+ regularHours: loc.regularHours ?? null,
19897
+ primaryPhone: loc.phoneNumbers?.primaryPhone ?? null,
19898
+ openStatus: loc.openInfo?.status ?? null,
19899
+ openingDate: formatOpeningDate(loc.openInfo?.openingDate)
19900
+ };
19901
+ }
19902
+ function formatOpeningDate(date) {
19903
+ if (!date?.year) return null;
19904
+ let out = String(date.year);
19905
+ if (date.month) {
19906
+ out += `-${String(date.month).padStart(2, "0")}`;
19907
+ if (date.day) out += `-${String(date.day).padStart(2, "0")}`;
19908
+ }
19909
+ return out;
19910
+ }
19850
19911
 
19851
19912
  // ../integration-google-business-profile/src/performance-client.ts
19852
19913
  function pad2(n) {
@@ -20863,6 +20924,13 @@ async function googleRoutes(app, opts) {
20863
20924
  websiteUri: row.websiteUri ?? null,
20864
20925
  placeId: row.placeId ?? null,
20865
20926
  mapsUri: row.mapsUri ?? null,
20927
+ additionalCategories: row.additionalCategories ?? [],
20928
+ description: row.description ?? null,
20929
+ serviceArea: row.serviceArea ?? null,
20930
+ regularHours: row.regularHours ?? null,
20931
+ primaryPhone: row.primaryPhone ?? null,
20932
+ openStatus: row.openStatus ?? null,
20933
+ openingDate: row.openingDate ?? null,
20866
20934
  selected: Boolean(row.selected),
20867
20935
  syncedAt: row.syncedAt ?? null,
20868
20936
  createdAt: row.createdAt,
@@ -20958,6 +21026,7 @@ async function googleRoutes(app, opts) {
20958
21026
  if (switching) clearGbpProjectData(tx, project.id);
20959
21027
  for (const remote of remoteLocations) {
20960
21028
  const existing = tx.select().from(gbpLocations).where(and14(eq21(gbpLocations.projectId, project.id), eq21(gbpLocations.locationName, remote.name))).get();
21029
+ const profile = buildLocationProfileFields(remote);
20961
21030
  if (existing) {
20962
21031
  tx.update(gbpLocations).set({
20963
21032
  accountName,
@@ -20967,6 +21036,7 @@ async function googleRoutes(app, opts) {
20967
21036
  websiteUri: remote.websiteUri ?? null,
20968
21037
  placeId: remote.metadata?.placeId ?? null,
20969
21038
  mapsUri: remote.metadata?.mapsUri ?? null,
21039
+ ...profile,
20970
21040
  updatedAt: now
20971
21041
  }).where(eq21(gbpLocations.id, existing.id)).run();
20972
21042
  } else {
@@ -20981,6 +21051,7 @@ async function googleRoutes(app, opts) {
20981
21051
  websiteUri: remote.websiteUri ?? null,
20982
21052
  placeId: remote.metadata?.placeId ?? null,
20983
21053
  mapsUri: remote.metadata?.mapsUri ?? null,
21054
+ ...profile,
20984
21055
  selected: selectAllNew,
20985
21056
  createdAt: now,
20986
21057
  updatedAt: now
@@ -901,6 +901,17 @@ var gbpLocationDtoSchema = z8.object({
901
901
  // the location is not on Maps). `placeId` is the join key to the Places API.
902
902
  placeId: z8.string().nullable(),
903
903
  mapsUri: z8.string().nullable(),
904
+ // Owner-authored profile content (Business Information v1 Location resource).
905
+ // These are the entity-anchor + qualifier signals AI answer engines weight:
906
+ // secondary categories, the business description, the service area + hours
907
+ // (stored verbatim as JSON), the primary phone, and the open state.
908
+ additionalCategories: z8.array(z8.string()),
909
+ description: z8.string().nullable(),
910
+ serviceArea: z8.record(z8.string(), z8.unknown()).nullable(),
911
+ regularHours: z8.record(z8.string(), z8.unknown()).nullable(),
912
+ primaryPhone: z8.string().nullable(),
913
+ openStatus: z8.string().nullable(),
914
+ openingDate: z8.string().nullable(),
904
915
  selected: z8.boolean(),
905
916
  syncedAt: z8.string().nullable(),
906
917
  createdAt: z8.string(),
@@ -4827,6 +4838,7 @@ var TrafficPathClasses = {
4827
4838
  other: "other"
4828
4839
  };
4829
4840
  var ROBOTS_BASENAMES = /* @__PURE__ */ new Set(["robots.txt", "llms.txt", "llms-full.txt"]);
4841
+ var INFRA_BASENAMES = /* @__PURE__ */ new Set(["xmlrpc.php", "wp-cron.php"]);
4830
4842
  var ASSET_EXTENSIONS = /* @__PURE__ */ new Set([
4831
4843
  "css",
4832
4844
  "js",
@@ -4925,6 +4937,9 @@ function classifyTrafficPath(pathNormalized) {
4925
4937
  if (ROBOTS_BASENAMES.has(basename)) return TrafficPathClasses.robots;
4926
4938
  if (lower.endsWith(".xml") || lower.endsWith(".xml.gz")) return TrafficPathClasses.sitemap;
4927
4939
  if (SITEMAP_BASENAME.test(basename)) return TrafficPathClasses.sitemap;
4940
+ if (INFRA_BASENAMES.has(basename)) return TrafficPathClasses.other;
4941
+ if (lower === "/wp-json" || lower.startsWith("/wp-json/")) return TrafficPathClasses.other;
4942
+ if (lower.endsWith("/feed") || lower.includes("/feed/")) return TrafficPathClasses.other;
4928
4943
  const dot = basename.lastIndexOf(".");
4929
4944
  const ext = dot > 0 ? basename.slice(dot + 1) : "";
4930
4945
  if (ext) {
@@ -5183,6 +5198,63 @@ function escapeLikePattern(value) {
5183
5198
  return value.replace(/[\\%_]/g, (match) => `\\${match}`);
5184
5199
  }
5185
5200
 
5201
+ // ../contracts/src/embed.ts
5202
+ var SELF_TOKEN = "'self'";
5203
+ function normalizeFrameOrigin(raw) {
5204
+ const trimmed = raw.trim();
5205
+ if (!trimmed) return null;
5206
+ if (trimmed === SELF_TOKEN) return SELF_TOKEN;
5207
+ if (trimmed.includes("*")) return null;
5208
+ let url;
5209
+ try {
5210
+ url = new URL(trimmed);
5211
+ } catch {
5212
+ return null;
5213
+ }
5214
+ if (url.protocol !== "http:" && url.protocol !== "https:") return null;
5215
+ if (url.username || url.password) return null;
5216
+ if (url.pathname && url.pathname !== "/" || url.search || url.hash) return null;
5217
+ if (!url.hostname) return null;
5218
+ return url.origin;
5219
+ }
5220
+ function splitList(raw) {
5221
+ if (raw === void 0) return [];
5222
+ const tokens = (Array.isArray(raw) ? raw : [raw]).flatMap(
5223
+ (entry) => String(entry).split(/[\s,]+/)
5224
+ );
5225
+ const seen = /* @__PURE__ */ new Set();
5226
+ const out = [];
5227
+ for (const token of tokens) {
5228
+ const trimmed = token.trim();
5229
+ if (!trimmed || seen.has(trimmed)) continue;
5230
+ seen.add(trimmed);
5231
+ out.push(trimmed);
5232
+ }
5233
+ return out;
5234
+ }
5235
+ function parseOriginList(raw) {
5236
+ const seen = /* @__PURE__ */ new Set();
5237
+ const out = [];
5238
+ for (const token of splitList(raw)) {
5239
+ const origin = normalizeFrameOrigin(token);
5240
+ if (!origin || seen.has(origin)) continue;
5241
+ seen.add(origin);
5242
+ out.push(origin);
5243
+ }
5244
+ return out;
5245
+ }
5246
+ function frameAncestorsHeaderValue(origins) {
5247
+ if (origins.length === 0) return "frame-ancestors 'none'";
5248
+ return `frame-ancestors ${origins.join(" ")}`;
5249
+ }
5250
+ function buildEmbedClientConfig(resolved) {
5251
+ if (!resolved.enabled) return void 0;
5252
+ const client = { enabled: true };
5253
+ if (resolved.views && resolved.views.length > 0) client.views = resolved.views;
5254
+ if (resolved.theme && Object.keys(resolved.theme).length > 0) client.theme = resolved.theme;
5255
+ return client;
5256
+ }
5257
+
5186
5258
  export {
5187
5259
  __export,
5188
5260
  apiKeyDtoSchema,
@@ -5480,5 +5552,9 @@ export {
5480
5552
  AI_PROVIDER_INFRA_DOMAINS,
5481
5553
  escapeLikePattern,
5482
5554
  READ_ONLY_SCOPE,
5483
- isReadOnlyKey
5555
+ isReadOnlyKey,
5556
+ splitList,
5557
+ parseOriginList,
5558
+ frameAncestorsHeaderValue,
5559
+ buildEmbedClientConfig
5484
5560
  };
@@ -23,7 +23,7 @@ import {
23
23
  trafficConnectVercelRequestSchema,
24
24
  trafficConnectWordpressRequestSchema,
25
25
  trafficEventKindSchema
26
- } from "./chunk-JZ67YCHT.js";
26
+ } from "./chunk-PL2ODHUC.js";
27
27
 
28
28
  // src/config.ts
29
29
  import fs from "fs";
@@ -6291,7 +6291,7 @@ var canonryMcpTools = [
6291
6291
  defineTool({
6292
6292
  name: "canonry_gbp_lodging",
6293
6293
  title: "Get GBP lodging attributes",
6294
- description: "List the latest Google Business Profile lodging snapshot per location (hotel structured attributes). populatedGroupCount=0 means an empty profile \u2014 an AEO gap.",
6294
+ description: 'List the latest Google Business Profile lodging snapshot per location (hotel structured attributes). populatedGroupCount=0 means the Lodging API returned no structured attributes, which is common even for complete hotels because the owner-set "Hotel details" amenity panel is a separate surface the API does not expose. Treat it as a "verify the Hotel details panel", not a confirmed gap.',
6295
6295
  access: "read",
6296
6296
  tier: "gbp",
6297
6297
  inputSchema: gbpLocationScopedInputSchema,
package/dist/cli.js CHANGED
@@ -27,7 +27,7 @@ import {
27
27
  setTelemetrySource,
28
28
  showFirstRunNotice,
29
29
  trackEvent
30
- } from "./chunk-2VEFNAQ3.js";
30
+ } from "./chunk-I4FR66PU.js";
31
31
  import {
32
32
  CliError,
33
33
  EXIT_SYSTEM_ERROR,
@@ -44,7 +44,7 @@ import {
44
44
  saveConfig,
45
45
  saveConfigPatch,
46
46
  usageError
47
- } from "./chunk-RH3QYFX3.js";
47
+ } from "./chunk-XG7JKKTC.js";
48
48
  import {
49
49
  apiKeys,
50
50
  createClient,
@@ -52,7 +52,7 @@ import {
52
52
  projects,
53
53
  queries,
54
54
  renderReportHtml
55
- } from "./chunk-WMZJO56S.js";
55
+ } from "./chunk-KO3T36E5.js";
56
56
  import {
57
57
  BacklinkSources,
58
58
  CcReleaseSyncStatuses,
@@ -77,7 +77,7 @@ import {
77
77
  providerQuotaPolicySchema,
78
78
  resolveProviderInput,
79
79
  winnabilityClassSchema
80
- } from "./chunk-JZ67YCHT.js";
80
+ } from "./chunk-PL2ODHUC.js";
81
81
 
82
82
  // src/cli.ts
83
83
  import { pathToFileURL } from "url";
@@ -3514,7 +3514,7 @@ async function gbpLodging(project, opts) {
3514
3514
  }
3515
3515
  console.log(`${response.total} lodging profile(s):`);
3516
3516
  for (const l of response.lodging) {
3517
- const note = l.populatedGroupCount === 0 ? " \u2014 EMPTY (AEO gap: no structured amenities for AI engines to cite)" : "";
3517
+ const note = l.populatedGroupCount === 0 ? ' (none readable via the Lodging API; verify the "Hotel details" panel, often set there but not exposed by the API)' : "";
3518
3518
  console.log(` ${l.locationName} ${l.populatedGroupCount} attribute group(s)${note}`);
3519
3519
  }
3520
3520
  }
@@ -10299,6 +10299,18 @@ async function waitForReady(host, port, maxMs = 1e4) {
10299
10299
  }
10300
10300
  return false;
10301
10301
  }
10302
+ function buildServeForwardArgs(opts) {
10303
+ const args = [];
10304
+ if (opts.port) args.push("--port", opts.port);
10305
+ if (opts.host) args.push("--host", opts.host);
10306
+ if (opts.basePath) args.push("--base-path", opts.basePath);
10307
+ if (opts.embed) {
10308
+ args.push("--embed");
10309
+ for (const origin of opts.embedAllowOrigins ?? []) args.push("--embed-allow-origin", origin);
10310
+ for (const view of opts.embedViews ?? []) args.push("--embed-view", view);
10311
+ }
10312
+ return args;
10313
+ }
10302
10314
  async function startDaemon(opts) {
10303
10315
  const pidPath = getPidPath();
10304
10316
  const format = opts.format ?? "text";
@@ -10319,9 +10331,7 @@ async function startDaemon(opts) {
10319
10331
  const cliPath = path7.resolve(new URL(import.meta.url).pathname);
10320
10332
  const inSourceMode = new URL(import.meta.url).pathname.endsWith(".ts");
10321
10333
  const args = inSourceMode ? ["--import", "tsx", cliPath, "serve"] : [cliPath, "serve"];
10322
- if (opts.port) args.push("--port", opts.port);
10323
- if (opts.host) args.push("--host", opts.host);
10324
- if (opts.basePath) args.push("--base-path", opts.basePath);
10334
+ args.push(...buildServeForwardArgs(opts));
10325
10335
  const child = spawn(process.execPath, args, {
10326
10336
  detached: true,
10327
10337
  stdio: "ignore"
@@ -11002,6 +11012,11 @@ function applyServerEnv(values) {
11002
11012
  if (port) process.env.CANONRY_PORT = port;
11003
11013
  if (host) process.env.CANONRY_HOST = host;
11004
11014
  if (basePath) process.env.CANONRY_BASE_PATH = basePath;
11015
+ const embedOrigins = getStringArray(values, "embed-allow-origin");
11016
+ const embedViews = getStringArray(values, "embed-view");
11017
+ if (getBoolean(values, "embed")) process.env.CANONRY_EMBED = "1";
11018
+ if (embedOrigins && embedOrigins.length > 0) process.env.CANONRY_EMBED_ORIGINS = embedOrigins.join(",");
11019
+ if (embedViews && embedViews.length > 0) process.env.CANONRY_EMBED_VIEWS = embedViews.join(",");
11005
11020
  }
11006
11021
  var SYSTEM_CLI_COMMANDS = [
11007
11022
  {
@@ -11058,11 +11073,14 @@ var SYSTEM_CLI_COMMANDS = [
11058
11073
  },
11059
11074
  {
11060
11075
  path: ["serve"],
11061
- usage: "canonry serve [--port <port>] [--host <host>] [--base-path <path>] [--format json]",
11076
+ usage: "canonry serve [--port <port>] [--host <host>] [--base-path <path>] [--embed] [--embed-allow-origin <origin>...] [--embed-view <view>...] [--format json]",
11062
11077
  options: {
11063
11078
  port: stringOption(),
11064
11079
  host: stringOption(),
11065
- "base-path": stringOption()
11080
+ "base-path": stringOption(),
11081
+ embed: { type: "boolean", default: false },
11082
+ "embed-allow-origin": multiStringOption(),
11083
+ "embed-view": multiStringOption()
11066
11084
  },
11067
11085
  allowPositionals: false,
11068
11086
  run: async (input) => {
@@ -11072,11 +11090,14 @@ var SYSTEM_CLI_COMMANDS = [
11072
11090
  },
11073
11091
  {
11074
11092
  path: ["start"],
11075
- usage: "canonry start [--port <port>] [--host <host>] [--base-path <path>] [--format json]",
11093
+ usage: "canonry start [--port <port>] [--host <host>] [--base-path <path>] [--embed] [--embed-allow-origin <origin>...] [--embed-view <view>...] [--format json]",
11076
11094
  options: {
11077
11095
  port: stringOption(),
11078
11096
  host: stringOption(),
11079
- "base-path": stringOption()
11097
+ "base-path": stringOption(),
11098
+ embed: { type: "boolean", default: false },
11099
+ "embed-allow-origin": multiStringOption(),
11100
+ "embed-view": multiStringOption()
11080
11101
  },
11081
11102
  allowPositionals: false,
11082
11103
  run: async (input) => {
@@ -11084,6 +11105,9 @@ var SYSTEM_CLI_COMMANDS = [
11084
11105
  port: getString(input.values, "port"),
11085
11106
  host: getString(input.values, "host"),
11086
11107
  basePath: getString(input.values, "base-path"),
11108
+ embed: getBoolean(input.values, "embed"),
11109
+ embedAllowOrigins: getStringArray(input.values, "embed-allow-origin"),
11110
+ embedViews: getStringArray(input.values, "embed-view"),
11087
11111
  format: input.format
11088
11112
  });
11089
11113
  }
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { FastifyInstance } from 'fastify';
2
2
  import { DatabaseClient } from '@ainyc/canonry-db';
3
- import { ProviderQuotaPolicy } from '@ainyc/canonry-contracts';
3
+ import { ProviderQuotaPolicy, EmbedConfigEntry } from '@ainyc/canonry-contracts';
4
4
 
5
5
  type GoogleConnectionType = 'gsc' | 'ga4' | 'gbp';
6
6
  interface ProviderConfigEntry {
@@ -205,6 +205,7 @@ interface CanonryConfig {
205
205
  lastKnownLatestVersion?: string;
206
206
  agent?: AgentConfigEntry;
207
207
  places?: PlacesConfigEntry;
208
+ embed?: EmbedConfigEntry;
208
209
  }
209
210
  declare function loadConfig(): CanonryConfig;
210
211
 
@@ -223,6 +224,13 @@ declare function createServer(opts: {
223
224
  * Defaults to loopback when unset (programmatic/test callers).
224
225
  */
225
226
  host?: string;
227
+ /**
228
+ * Override for the directory the pre-built SPA is served from. Defaults to
229
+ * the package's bundled `assets/` (resolved from `import.meta.url`). Exposed
230
+ * so tests can point at a temp dir containing a fixture `index.html` and
231
+ * assert the injected config + framing header on the served document.
232
+ */
233
+ assetsDir?: string;
226
234
  }): Promise<FastifyInstance>;
227
235
 
228
236
  export { type CanonryConfig, createServer, loadConfig };
package/dist/index.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  createServer
3
- } from "./chunk-2VEFNAQ3.js";
3
+ } from "./chunk-I4FR66PU.js";
4
4
  import {
5
5
  loadConfig
6
- } from "./chunk-RH3QYFX3.js";
7
- import "./chunk-WMZJO56S.js";
8
- import "./chunk-JZ67YCHT.js";
6
+ } from "./chunk-XG7JKKTC.js";
7
+ import "./chunk-KO3T36E5.js";
8
+ import "./chunk-PL2ODHUC.js";
9
9
  export {
10
10
  createServer,
11
11
  loadConfig
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  IntelligenceService
3
- } from "./chunk-WMZJO56S.js";
4
- import "./chunk-JZ67YCHT.js";
3
+ } from "./chunk-KO3T36E5.js";
4
+ import "./chunk-PL2ODHUC.js";
5
5
  export {
6
6
  IntelligenceService
7
7
  };
package/dist/mcp.js CHANGED
@@ -3,10 +3,10 @@ import {
3
3
  PACKAGE_VERSION,
4
4
  canonryMcpTools,
5
5
  createApiClient
6
- } from "./chunk-RH3QYFX3.js";
6
+ } from "./chunk-XG7JKKTC.js";
7
7
  import {
8
8
  isReadOnlyKey
9
- } from "./chunk-JZ67YCHT.js";
9
+ } from "./chunk-PL2ODHUC.js";
10
10
 
11
11
  // src/mcp/cli.ts
12
12
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";