@canonry/canonry 7.17.0 → 7.17.2

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 (35) hide show
  1. package/assets/assets/{AuditHistoryPanel-CUde9U_6.js → AuditHistoryPanel-Dga03Niw.js} +1 -1
  2. package/assets/assets/BacklinksPage-CfodWuIt.js +1 -0
  3. package/assets/assets/Hint-DICI5Jq9.js +1 -0
  4. package/assets/assets/{HistoryPage-DZSa1gYf.js → HistoryPage-BN_vTU5L.js} +1 -1
  5. package/assets/assets/{MeasurementPropertyPage-DgW17Jpd.js → MeasurementPropertyPage-CjiyzOmE.js} +1 -1
  6. package/assets/assets/ProjectPage-AxclA3YC.js +8 -0
  7. package/assets/assets/{RunsPage-DRfNd-Jp.js → RunsPage-Ca_ziCGC.js} +1 -1
  8. package/assets/assets/{SettingsPage-D38Gl1Nv.js → SettingsPage-SO1xl0d3.js} +1 -1
  9. package/assets/assets/SiteHealthSection-C9JgRzzs.js +4 -0
  10. package/assets/assets/{TrafficPage-DX3qRPgQ.js → TrafficPage-DrcbXNQB.js} +2 -2
  11. package/assets/assets/{TrafficSourceDetailPage-CigB82vX.js → TrafficSourceDetailPage-HDZGKgyd.js} +1 -1
  12. package/assets/assets/{extract-error-message-DsMnnkxT.js → extract-error-message-D48vwe7I.js} +1 -1
  13. package/assets/assets/index-BaBHBReI.js +86 -0
  14. package/assets/assets/{react-sigma_core.esm.min-CkYxzGpi.js → react-sigma_core.esm.min-DVHs4npB.js} +1 -1
  15. package/assets/assets/{v2-overview-adapter-5x8UiA07.js → v2-overview-adapter-CTEXuiRt.js} +1 -1
  16. package/assets/assets/{vendor-lucide-N6vOpZmh.js → vendor-lucide-Y6jANGfG.js} +1 -1
  17. package/assets/index.html +2 -2
  18. package/dist/{chunk-WEKVQK3O.js → chunk-3LPQ4JV5.js} +130 -103
  19. package/dist/{chunk-2MBI5SYB.js → chunk-4EDH2ORA.js} +3 -3
  20. package/dist/{chunk-PVWGKDY6.js → chunk-MIRAKDRW.js} +111 -79
  21. package/dist/{chunk-QPOXDS43.js → chunk-NPNU6UHD.js} +5 -4
  22. package/dist/{chunk-WVBLFG7H.js → chunk-P2XY5YX7.js} +2 -2
  23. package/dist/{chunk-RBQUGZM4.js → chunk-QAIJH74T.js} +1 -1
  24. package/dist/{chunk-3NAZXITW.js → chunk-YJS4OE6I.js} +615 -579
  25. package/dist/cli.js +15 -16
  26. package/dist/{demo-server-BVJFYR4V.js → demo-server-54SLPSJF.js} +6 -5
  27. package/dist/index.d.ts +7 -5
  28. package/dist/index.js +6 -6
  29. package/dist/{intelligence-service-FK2W72X7.js → intelligence-service-62CEP6M2.js} +2 -2
  30. package/dist/mcp.js +4 -4
  31. package/package.json +10 -10
  32. package/assets/assets/BacklinksPage-DMQPw8ks.js +0 -1
  33. package/assets/assets/ProjectPage-C8vdtqtA.js +0 -8
  34. package/assets/assets/SiteHealthSection-CR3zB84D.js +0 -4
  35. package/assets/assets/index-DBr6ISbx.js +0 -86
@@ -20,23 +20,25 @@ import {
20
20
  saveConfig,
21
21
  saveConfigPatch,
22
22
  sentimentInstallReadiness
23
- } from "./chunk-WVBLFG7H.js";
23
+ } from "./chunk-P2XY5YX7.js";
24
24
  import {
25
25
  computeFactorAverages,
26
26
  deleteSiteCrawlGraphLayout,
27
27
  persistSiteCrawlGraphLayout,
28
28
  prepareSiteCrawlGraphLayout,
29
29
  recordedSharePct
30
- } from "./chunk-QPOXDS43.js";
30
+ } from "./chunk-NPNU6UHD.js";
31
31
  import {
32
32
  CanonryMcpToolNames,
33
33
  CliError,
34
34
  PACKAGE_VERSION,
35
35
  canonryMcpTools
36
- } from "./chunk-RBQUGZM4.js";
36
+ } from "./chunk-QAIJH74T.js";
37
37
  import {
38
38
  CC_CACHE_DIR,
39
39
  DUCKDB_SPEC,
40
+ EgressFailedError,
41
+ EgressRefusedError,
40
42
  GBP_DAILY_METRICS,
41
43
  GSC_DATA_LAG_DAYS,
42
44
  GSC_REPORTING_TIME_ZONE,
@@ -65,6 +67,7 @@ import {
65
67
  apiRoutes,
66
68
  applyCompetitorAutoAliases,
67
69
  assessConversionTrackingIntegrity,
70
+ auditFromRequest,
68
71
  auditLog,
69
72
  backfillProjectAnswerMentions,
70
73
  backfillProjectAnswerMentionsInChunks,
@@ -86,6 +89,7 @@ import {
86
89
  countPopulatedGroups,
87
90
  createCredentialChecker,
88
91
  createFastifyLogger,
92
+ createGuardedFetch,
89
93
  createLogger,
90
94
  createProjectPassQueue,
91
95
  createRunCompetitorResolver,
@@ -133,6 +137,7 @@ import {
133
137
  gtmConnections,
134
138
  gtmRawSnapshots,
135
139
  hasOutstandingProviderBatch,
140
+ hasProxyHeaders,
136
141
  hashApiKey,
137
142
  hashAttributes,
138
143
  hashLodging,
@@ -216,8 +221,9 @@ import {
216
221
  siteCrawlSnapshots,
217
222
  siteLivenessState,
218
223
  toAlertView,
219
- usageCounters
220
- } from "./chunk-3NAZXITW.js";
224
+ usageCounters,
225
+ writeAuditLog
226
+ } from "./chunk-YJS4OE6I.js";
221
227
  import {
222
228
  AGENT_MEMORY_VALUE_MAX_BYTES,
223
229
  AGENT_NONE,
@@ -337,6 +343,7 @@ import {
337
343
  effectiveDomains,
338
344
  embedClientConfigForRequest,
339
345
  embedProjectTabsError,
346
+ escapeRegExp,
340
347
  extractProviderHttpStatus,
341
348
  extractRecommendedCompetitors,
342
349
  factorStatusFromScore,
@@ -432,7 +439,7 @@ import {
432
439
  winnabilityClassLabel,
433
440
  withRetry,
434
441
  withSkippedProviders
435
- } from "./chunk-PVWGKDY6.js";
442
+ } from "./chunk-MIRAKDRW.js";
436
443
 
437
444
  // src/runtime-context.ts
438
445
  function cliRuntimeContext(env = process.env, stdio = process) {
@@ -1240,9 +1247,6 @@ function buildDailyConversionMetricsQuery(options) {
1240
1247
  }
1241
1248
 
1242
1249
  // ../integration-google-ads/src/google-ads-client.ts
1243
- function escapeRegExp(value) {
1244
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1245
- }
1246
1250
  function sanitizeText(value, secrets) {
1247
1251
  let sanitized = value.replace(/(authorization\s*:\s*bearer\s+)[^\s,"'}]+/gi, "$1***").replace(/((?:developer|access)[_-]?token\s*[:=]\s*["']?)[^\s,"'}]+/gi, "$1***");
1248
1252
  for (const secret of secrets) {
@@ -8045,7 +8049,7 @@ function nonBlank(value) {
8045
8049
  return trimmed ? trimmed : void 0;
8046
8050
  }
8047
8051
  function resolveBuildCommit(env = process.env) {
8048
- const embedded = true ? "292663bc656bcd51a4a32fa879fbdcd7f7935398" : void 0;
8052
+ const embedded = true ? "3bf6bc30c036bb0dbbed1d8ed17141d1ad2a387f" : void 0;
8049
8053
  return nonBlank(embedded) ?? nonBlank(env.CANONRY_COMMIT);
8050
8054
  }
8051
8055
  function resolveInstanceIdentity(env = process.env) {
@@ -12143,10 +12147,14 @@ import { eq as eq9 } from "drizzle-orm";
12143
12147
  var log7 = createLogger("SitemapParser");
12144
12148
  var LOC_REGEX = /<loc>([^<]+)<\/loc>/gi;
12145
12149
  var SITEMAP_TAG_REGEX = /<sitemap>[\s\S]*?<\/sitemap>/gi;
12146
- async function validateSitemapUrl(url) {
12147
- const check = await resolveWebhookTarget(url, { allowLoopback: true });
12148
- if (!check.ok) {
12149
- throw new Error(`Sitemap URL rejected: ${check.message.replace(/^"url" /, "")} (${url})`);
12150
+ var sitemapFetch = createGuardedFetch({ allowLoopback: true });
12151
+ async function fetchSitemap(url) {
12152
+ try {
12153
+ return await sitemapFetch(url);
12154
+ } catch (err) {
12155
+ if (err instanceof EgressRefusedError) throw new Error(`Sitemap ${url} rejected: ${err.message}`);
12156
+ if (err instanceof EgressFailedError) throw new Error(`Failed to fetch sitemap at ${url}: ${err.message}`);
12157
+ throw err;
12150
12158
  }
12151
12159
  }
12152
12160
  async function readSitemapBody(res) {
@@ -12178,8 +12186,7 @@ async function parseSitemapRecursive(url, urls, visited, depth, isChild) {
12178
12186
  visited.add(url);
12179
12187
  let res;
12180
12188
  try {
12181
- await validateSitemapUrl(url);
12182
- res = await fetch(url);
12189
+ res = await fetchSitemap(url);
12183
12190
  } catch (err) {
12184
12191
  if (!isChild) throw err;
12185
12192
  log7.warn("child-sitemap.fetch-failed", {
@@ -12864,7 +12871,7 @@ async function executeReleaseSync(db, syncId, opts) {
12864
12871
  const rowsByProject = groupByProject(rows, projectsByDomain);
12865
12872
  for (const p of allProjects) {
12866
12873
  const projectRows = rowsByProject.get(p.id) ?? [];
12867
- const summary = computeSummary(projectRows);
12874
+ const summary = computeBacklinkSummaryMetrics(projectRows);
12868
12875
  tx.insert(backlinkSummaries).values({
12869
12876
  id: crypto17.randomUUID(),
12870
12877
  projectId: p.id,
@@ -12947,20 +12954,6 @@ function groupByProject(rows, projectsByDomain) {
12947
12954
  }
12948
12955
  return out;
12949
12956
  }
12950
- function computeSummary(rows) {
12951
- if (rows.length === 0) {
12952
- return { totalLinkingDomains: 0, totalHosts: 0, top10HostsShare: "0" };
12953
- }
12954
- const sorted = [...rows].sort((a, b) => b.numHosts - a.numHosts);
12955
- const totalHosts = sorted.reduce((acc, r) => acc + r.numHosts, 0);
12956
- const top10Hosts = sorted.slice(0, 10).reduce((acc, r) => acc + r.numHosts, 0);
12957
- const share = totalHosts > 0 ? top10Hosts / totalHosts : 0;
12958
- return {
12959
- totalLinkingDomains: rows.length,
12960
- totalHosts,
12961
- top10HostsShare: share.toFixed(6)
12962
- };
12963
- }
12964
12957
 
12965
12958
  // src/backlink-extract.ts
12966
12959
  import crypto18 from "crypto";
@@ -13031,7 +13024,7 @@ async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
13031
13024
  }));
13032
13025
  tx.insert(backlinkDomains).values(values).run();
13033
13026
  }
13034
- const summary = computeSummary2(rows);
13027
+ const summary = computeSummary(rows);
13035
13028
  tx.insert(backlinkSummaries).values({
13036
13029
  id: crypto18.randomUUID(),
13037
13030
  projectId,
@@ -13071,7 +13064,7 @@ async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
13071
13064
  throw err;
13072
13065
  }
13073
13066
  }
13074
- function computeSummary2(rows) {
13067
+ function computeSummary(rows) {
13075
13068
  return computeBacklinkSummaryMetrics(rows);
13076
13069
  }
13077
13070
 
@@ -15526,11 +15519,17 @@ var SITE_LIVENESS_MAX_PASS_GAP_MS = 45 * 6e4;
15526
15519
  var Notifier = class {
15527
15520
  db;
15528
15521
  serverUrl;
15522
+ /**
15523
+ * The server's `allowLoopbackWebhooks`, so delivery admits exactly the
15524
+ * destinations that creating and testing a webhook admitted.
15525
+ */
15526
+ allowLoopbackWebhooks;
15529
15527
  /** Projects with a review dispatch in progress; see {@link dispatchReviewAlerts}. */
15530
15528
  reviewDispatchInFlight = /* @__PURE__ */ new Set();
15531
- constructor(db, serverUrl) {
15529
+ constructor(db, serverUrl, opts = {}) {
15532
15530
  this.db = db;
15533
15531
  this.serverUrl = serverUrl;
15532
+ this.allowLoopbackWebhooks = opts.allowLoopbackWebhooks === true;
15534
15533
  }
15535
15534
  /** Called after a run completes (success, partial, or failed). */
15536
15535
  async onRunCompleted(runId, projectId, opts) {
@@ -16096,7 +16095,7 @@ var Notifier = class {
16096
16095
  const body = destination.render ? destination.render(toAlertView(payload)) : payload;
16097
16096
  const signingSecret = destination.signed ? webhookSecret : null;
16098
16097
  const targetLabel = redactNotificationUrl(url).urlDisplay;
16099
- const targetCheck = await resolveWebhookTarget(url);
16098
+ const targetCheck = await resolveWebhookTarget(url, { allowLoopback: this.allowLoopbackWebhooks });
16100
16099
  if (!targetCheck.ok) {
16101
16100
  log20.error("webhook.ssrf-blocked", { url: targetLabel, reason: targetCheck.message });
16102
16101
  this.logDelivery(projectId, notificationId, payload.event, "failed", `SSRF: ${targetCheck.message}`);
@@ -21526,6 +21525,10 @@ function waitForServerRuntimeStartup(app) {
21526
21525
  const startup = runtimeStartupByServer.get(app);
21527
21526
  return startup ?? Promise.reject(new Error("Server runtime startup was not registered"));
21528
21527
  }
21528
+ var keylessFirstRunSetupByServer = /* @__PURE__ */ new WeakMap();
21529
+ function allowsKeylessFirstRunSetup(app) {
21530
+ return keylessFirstRunSetupByServer.get(app) ?? false;
21531
+ }
21529
21532
  var DEFAULT_QUOTA = {
21530
21533
  maxConcurrency: 2,
21531
21534
  maxRequestsPerMinute: 10,
@@ -21618,9 +21621,6 @@ function summarizeProviderConfig(config) {
21618
21621
  quota: { ...config?.quota ?? DEFAULT_QUOTA }
21619
21622
  };
21620
21623
  }
21621
- function hashApiKey2(key) {
21622
- return crypto29.createHash("sha256").update(key).digest("hex");
21623
- }
21624
21624
  var DASHBOARD_SCRYPT_KEYLEN = 64;
21625
21625
  var DASHBOARD_SCRYPT_COST = 1 << 15;
21626
21626
  var DASHBOARD_SCRYPT_MAXMEM = 64 * 1024 * 1024;
@@ -21659,7 +21659,7 @@ function verifyDashboardPassword(password, storedHash) {
21659
21659
  };
21660
21660
  }
21661
21661
  if (/^[a-f0-9]{64}$/i.test(storedHash)) {
21662
- const candidate = Buffer.from(hashApiKey2(password), "hex");
21662
+ const candidate = Buffer.from(hashApiKey(password), "hex");
21663
21663
  const expected = Buffer.from(storedHash, "hex");
21664
21664
  if (candidate.length !== expected.length)
21665
21665
  return { ok: false, needsRehash: false };
@@ -21668,22 +21668,6 @@ function verifyDashboardPassword(password, storedHash) {
21668
21668
  }
21669
21669
  return { ok: false, needsRehash: false };
21670
21670
  }
21671
- function parseCookies(header) {
21672
- if (!header) return {};
21673
- return header.split(";").map((part) => part.trim()).filter(Boolean).reduce((cookies, part) => {
21674
- const eqIdx = part.indexOf("=");
21675
- if (eqIdx <= 0) return cookies;
21676
- const name = part.slice(0, eqIdx).trim();
21677
- const value = part.slice(eqIdx + 1).trim();
21678
- if (!name) return cookies;
21679
- try {
21680
- cookies[name] = decodeURIComponent(value);
21681
- } catch {
21682
- cookies[name] = value;
21683
- }
21684
- return cookies;
21685
- }, {});
21686
- }
21687
21671
  function serializeSessionCookie(opts) {
21688
21672
  const parts = [
21689
21673
  `${opts.name}=${opts.value ? encodeURIComponent(opts.value) : ""}`,
@@ -21754,6 +21738,38 @@ function isLoopbackBindHost(host) {
21754
21738
  if (/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(normalized)) return true;
21755
21739
  return false;
21756
21740
  }
21741
+ function isLoopbackPeerAddress(address) {
21742
+ if (!address) return false;
21743
+ const normalized = address.toLowerCase().replace(/^::ffff:(?=\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$)/, "");
21744
+ return isIP(normalized) !== 0 && isLoopbackBindHost(normalized);
21745
+ }
21746
+ function isDirectLocalRequest(request) {
21747
+ if (request.raw.httpVersion !== "1.1") return false;
21748
+ if (!isLoopbackPeerAddress(request.raw.socket.remoteAddress)) return false;
21749
+ if (hasProxyHeaders(request)) return false;
21750
+ const host = request.headers.host;
21751
+ if (!host) return false;
21752
+ let hostname;
21753
+ try {
21754
+ hostname = new URL(`http://${host}`).hostname;
21755
+ } catch {
21756
+ return false;
21757
+ }
21758
+ return hostname !== "" && isLoopbackBindHost(hostname);
21759
+ }
21760
+ function configuresExternalAccess(trustProxy, basePath, ...configuredUrls) {
21761
+ if (trustProxy !== false || basePath !== void 0) return true;
21762
+ return configuredUrls.some((configuredUrl) => {
21763
+ const value = configuredUrl?.trim();
21764
+ if (!value) return false;
21765
+ try {
21766
+ const { hostname } = new URL(value);
21767
+ return hostname === "" || !isLoopbackBindHost(hostname);
21768
+ } catch {
21769
+ return true;
21770
+ }
21771
+ });
21772
+ }
21757
21773
  function isAllowedHost(host, allowedHosts) {
21758
21774
  const normalized = host.trim().toLowerCase().replace(/^\[|\]$/g, "");
21759
21775
  return allowedHosts.has(normalized) || isIP(normalized) !== 0;
@@ -22028,7 +22044,8 @@ async function createServer(opts) {
22028
22044
  onFirstActivation: () => maybeShowActivationNotice()
22029
22045
  });
22030
22046
  jobRunner.recoverStaleRuns();
22031
- const notifier = new Notifier(opts.db, serverUrl);
22047
+ const allowLoopbackWebhooks = process.env.CANONRY_ALLOW_LOOPBACK_WEBHOOKS !== "0";
22048
+ const notifier = new Notifier(opts.db, serverUrl, { allowLoopbackWebhooks });
22032
22049
  const intelligenceService = new IntelligenceService(opts.db);
22033
22050
  const aeroClient = new ApiClient(opts.config.apiUrl, opts.config.apiKey, {
22034
22051
  skipProbe: true,
@@ -22819,7 +22836,7 @@ async function createServer(opts) {
22819
22836
  }
22820
22837
  })();
22821
22838
  if (opts.config.apiKey) {
22822
- const keyHash = hashApiKey2(opts.config.apiKey);
22839
+ const keyHash = hashApiKey(opts.config.apiKey);
22823
22840
  const existing = opts.db.select().from(apiKeys).where(eq29(apiKeys.keyHash, keyHash)).get();
22824
22841
  if (!existing) {
22825
22842
  const prefix = opts.config.apiKey.slice(0, 12);
@@ -22845,11 +22862,11 @@ async function createServer(opts) {
22845
22862
  ne3(dashboardSessions.passwordFingerprint, currentPasswordFingerprint())
22846
22863
  )
22847
22864
  )).run();
22848
- const createSession = (key, passwordBased = false) => {
22865
+ const createSession = (key, passwordBased = false, db = opts.db) => {
22849
22866
  const now = /* @__PURE__ */ new Date();
22850
- opts.db.delete(dashboardSessions).where(lte2(dashboardSessions.expiresAt, now.toISOString())).run();
22867
+ db.delete(dashboardSessions).where(lte2(dashboardSessions.expiresAt, now.toISOString())).run();
22851
22868
  const sessionId = crypto29.randomBytes(32).toString("hex");
22852
- opts.db.insert(dashboardSessions).values({
22869
+ db.insert(dashboardSessions).values({
22853
22870
  tokenHash: hashSessionToken(sessionId),
22854
22871
  apiKeyId: key.id,
22855
22872
  apiKeyHash: key.keyHash,
@@ -22883,12 +22900,9 @@ async function createServer(opts) {
22883
22900
  };
22884
22901
  const getDefaultApiKey = () => {
22885
22902
  if (!opts.config.apiKey) return void 0;
22886
- return opts.db.select().from(apiKeys).where(eq29(apiKeys.keyHash, hashApiKey2(opts.config.apiKey))).get();
22903
+ return opts.db.select().from(apiKeys).where(eq29(apiKeys.keyHash, hashApiKey(opts.config.apiKey))).get();
22887
22904
  };
22888
- const createPasswordSession = (reply) => {
22889
- const key = getDefaultApiKey();
22890
- if (!key || key.revokedAt) return false;
22891
- const sessionId = createSession(key, true);
22905
+ const setSessionCookie = (reply, sessionId) => {
22892
22906
  reply.header(
22893
22907
  "set-cookie",
22894
22908
  serializeSessionCookie({
@@ -22899,19 +22913,27 @@ async function createServer(opts) {
22899
22913
  ttlMs: SESSION_TTL_MS
22900
22914
  })
22901
22915
  );
22916
+ };
22917
+ const createPasswordSession = (reply) => {
22918
+ const key = getDefaultApiKey();
22919
+ if (!key || key.revokedAt) return false;
22920
+ setSessionCookie(reply, createSession(key, true));
22902
22921
  return true;
22903
22922
  };
22904
- const boundToLoopback = isLoopbackBindHost(opts.host);
22905
- const requestHasValidApiKey = (request) => {
22923
+ const keylessLocalSetup = isLoopbackBindHost(opts.host) && !configuresExternalAccess(trustProxy, basePath, opts.config.apiUrl, opts.config.publicUrl);
22924
+ keylessFirstRunSetupByServer.set(app, keylessLocalSetup);
22925
+ const requestPresentsKey = (request, key) => {
22906
22926
  const header = request.headers.authorization;
22907
22927
  if (!header) return false;
22908
22928
  const parts = header.split(" ");
22909
- if (parts.length !== 2 || parts[0] !== "Bearer") return false;
22910
- const key = opts.db.select().from(apiKeys).where(eq29(apiKeys.keyHash, hashApiKey2(parts[1]))).get();
22911
- return Boolean(key && !key.revokedAt);
22929
+ if (parts.length !== 2 || parts[0] !== "Bearer" || !parts[1]) return false;
22930
+ const presented = Buffer.from(hashApiKey(parts[1]), "hex");
22931
+ const expected = Buffer.from(key.keyHash, "hex");
22932
+ return presented.length === expected.length && crypto29.timingSafeEqual(presented, expected);
22912
22933
  };
22913
22934
  const namedAccountsInUse = () => anyUsersExist(opts.db);
22914
22935
  const NAMED_ACCOUNTS_MESSAGE = "This install uses named accounts. Sign in with your name and password.";
22936
+ const serverApiKeyMissing = () => new AppError("AUTH_INVALID", "Server API key not found \u2014 run canonry bootstrap", 401);
22915
22937
  app.get(apiPrefix + "/session", async (request, reply) => {
22916
22938
  if (namedAccountsInUse()) {
22917
22939
  return reply.send({ authenticated: false, setupRequired: false });
@@ -22919,7 +22941,7 @@ async function createServer(opts) {
22919
22941
  if (!dashboardRequirePassword) {
22920
22942
  return reply.send({ authenticated: true, setupRequired: false });
22921
22943
  }
22922
- const sessionId = parseCookies(request.headers.cookie)[SESSION_COOKIE_NAME];
22944
+ const sessionId = parseCookieHeader(request.headers.cookie)[SESSION_COOKIE_NAME];
22923
22945
  return reply.send({
22924
22946
  authenticated: Boolean(sessionId && resolveSessionApiKeyId(sessionId)),
22925
22947
  setupRequired: !opts.config.dashboardPasswordHash
@@ -22933,9 +22955,13 @@ async function createServer(opts) {
22933
22955
  if (!dashboardRequirePassword) {
22934
22956
  return reply.send({ authenticated: true, setupRequired: false });
22935
22957
  }
22936
- if (!boundToLoopback && !requestHasValidApiKey(request)) {
22937
- const err = authRequired(
22938
- "This server is network-reachable; setting the dashboard password requires a valid API key."
22958
+ const found = getDefaultApiKey();
22959
+ const defaultKey = found && !found.revokedAt ? found : void 0;
22960
+ const presentsConfiguredKey = opts.config.apiKey ? requestPresentsKey(request, { keyHash: hashApiKey(opts.config.apiKey) }) : false;
22961
+ const presentedRootKey = defaultKey !== void 0 && presentsConfiguredKey;
22962
+ if (!presentedRootKey && !(keylessLocalSetup && isDirectLocalRequest(request))) {
22963
+ const err = presentsConfiguredKey ? serverApiKeyMissing() : authRequired(
22964
+ "Setting the dashboard password requires the root API key (apiKey in config.yaml), unless the request comes directly from this machine to a loopback-bound server that has no external URL, base path, or trusted proxy configured."
22939
22965
  );
22940
22966
  return reply.status(err.statusCode).send(err.toJSON());
22941
22967
  }
@@ -22948,12 +22974,31 @@ async function createServer(opts) {
22948
22974
  const err = validationError("Password must be at least 8 characters");
22949
22975
  return reply.status(err.statusCode).send(err.toJSON());
22950
22976
  }
22951
- opts.config.dashboardPasswordHash = hashDashboardPassword(password);
22952
- saveConfigPatch(opts.config);
22953
- if (!createPasswordSession(reply)) {
22954
- const err = authInvalid();
22977
+ if (!defaultKey) {
22978
+ const err = serverApiKeyMissing();
22955
22979
  return reply.status(err.statusCode).send(err.toJSON());
22956
22980
  }
22981
+ opts.config.dashboardPasswordHash = hashDashboardPassword(password);
22982
+ let sessionId;
22983
+ try {
22984
+ sessionId = opts.db.transaction((tx) => {
22985
+ writeAuditLog(tx, auditFromRequest(request, {
22986
+ actor: `api-key:${defaultKey.id}`,
22987
+ credentialId: presentedRootKey ? defaultKey.id : null,
22988
+ requestId: request.id,
22989
+ action: "dashboard-password.created",
22990
+ entityType: "dashboard-password",
22991
+ diff: { authorizedBy: presentedRootKey ? "root-api-key" : "local-request" }
22992
+ }));
22993
+ const created = createSession(defaultKey, true, tx);
22994
+ saveConfigPatch(opts.config);
22995
+ return created;
22996
+ });
22997
+ } catch (err) {
22998
+ opts.config.dashboardPasswordHash = void 0;
22999
+ throw err;
23000
+ }
23001
+ setSessionCookie(reply, sessionId);
22957
23002
  return reply.send({ authenticated: true });
22958
23003
  });
22959
23004
  app.post(apiPrefix + "/session", async (request, reply) => {
@@ -22992,40 +23037,26 @@ async function createServer(opts) {
22992
23037
  opts.config.dashboardPasswordHash = migratedHash;
22993
23038
  }
22994
23039
  if (!createPasswordSession(reply)) {
22995
- return reply.status(401).send({
22996
- error: {
22997
- code: "AUTH_INVALID",
22998
- message: "Server API key not found \u2014 run canonry bootstrap"
22999
- }
23000
- });
23040
+ const err2 = serverApiKeyMissing();
23041
+ return reply.status(err2.statusCode).send(err2.toJSON());
23001
23042
  }
23002
23043
  return reply.send({ authenticated: true });
23003
23044
  }
23004
23045
  if (apiKey) {
23005
- const key = opts.db.select().from(apiKeys).where(eq29(apiKeys.keyHash, hashApiKey2(apiKey))).get();
23046
+ const key = opts.db.select().from(apiKeys).where(eq29(apiKeys.keyHash, hashApiKey(apiKey))).get();
23006
23047
  if (!key || key.revokedAt) {
23007
23048
  const err2 = authInvalid();
23008
23049
  return reply.status(err2.statusCode).send(err2.toJSON());
23009
23050
  }
23010
23051
  opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq29(apiKeys.id, key.id)).run();
23011
- const sessionId = createSession(key);
23012
- reply.header(
23013
- "set-cookie",
23014
- serializeSessionCookie({
23015
- name: SESSION_COOKIE_NAME,
23016
- value: sessionId,
23017
- path: sessionCookiePath,
23018
- secure: sessionCookieSecure,
23019
- ttlMs: SESSION_TTL_MS
23020
- })
23021
- );
23052
+ setSessionCookie(reply, createSession(key));
23022
23053
  return reply.send({ authenticated: true });
23023
23054
  }
23024
23055
  const err = validationError("Either password or apiKey is required");
23025
23056
  return reply.status(err.statusCode).send(err.toJSON());
23026
23057
  });
23027
23058
  app.delete(apiPrefix + "/session", async (request, reply) => {
23028
- const sessionId = parseCookies(request.headers.cookie)[SESSION_COOKIE_NAME];
23059
+ const sessionId = parseCookieHeader(request.headers.cookie)[SESSION_COOKIE_NAME];
23029
23060
  clearSession(sessionId);
23030
23061
  reply.header(
23031
23062
  "set-cookie",
@@ -23277,12 +23308,7 @@ async function createServer(opts) {
23277
23308
  getAgentPluginState: opts.getAgentPluginState,
23278
23309
  // Powers the `canonry.version.current` doctor check. Non-blocking.
23279
23310
  getUpdateStatus: () => getServerUpdateStatus(),
23280
- // Local canonry serve runs on the operator's machine, where pointing a
23281
- // webhook at localhost (Discord test container, Pipedream-mock dev server,
23282
- // etc.) is a legitimate workflow. Default to allowing it for the local
23283
- // installer; cloud deployments inherit the secure default of `false` by
23284
- // not passing this option. Override with CANONRY_ALLOW_LOOPBACK_WEBHOOKS=0.
23285
- allowLoopbackWebhooks: process.env.CANONRY_ALLOW_LOOPBACK_WEBHOOKS !== "0",
23311
+ allowLoopbackWebhooks,
23286
23312
  // Wall-clock budget for one incremental Vercel drain. This is the only lever
23287
23313
  // that decides whether a source catches up or falls further behind, and it
23288
23314
  // was previously reachable only from tests, so a source losing ground could
@@ -24164,6 +24190,7 @@ export {
24164
24190
  createGoogleMarketingCredentialStore,
24165
24191
  createGoogleMarketingRuntime,
24166
24192
  waitForServerRuntimeStartup,
24193
+ allowsKeylessFirstRunSetup,
24167
24194
  isLoopbackBindHost,
24168
24195
  createServer
24169
24196
  };
@@ -4,14 +4,14 @@ import {
4
4
  installSkills,
5
5
  loadConfigRaw,
6
6
  saveConfigPatch
7
- } from "./chunk-WVBLFG7H.js";
7
+ } from "./chunk-P2XY5YX7.js";
8
8
  import {
9
9
  PACKAGE_VERSION
10
- } from "./chunk-RBQUGZM4.js";
10
+ } from "./chunk-QAIJH74T.js";
11
11
  import {
12
12
  SKILL_MANIFEST_FILENAME,
13
13
  SkillsClients
14
- } from "./chunk-PVWGKDY6.js";
14
+ } from "./chunk-MIRAKDRW.js";
15
15
 
16
16
  // src/skills-autosync.ts
17
17
  import fs from "fs";