@canonry/canonry 7.17.1 → 7.18.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/portfolio-analysis.md +8 -6
- package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +1 -1
- package/assets/assets/{AuditHistoryPanel-CzjskfMX.js → AuditHistoryPanel-CgKpjgpq.js} +1 -1
- package/assets/assets/BacklinksPage-BBqbRSeG.js +1 -0
- package/assets/assets/Hint-DICI5Jq9.js +1 -0
- package/assets/assets/{HistoryPage-BZgVoYGm.js → HistoryPage-D8nQ8E8_.js} +1 -1
- package/assets/assets/{MeasurementPropertyPage-EBI5sn-D.js → MeasurementPropertyPage-CQ8lLoeA.js} +1 -1
- package/assets/assets/{ProjectPage-lGGjM_qj.js → ProjectPage-CP4fvluh.js} +6 -6
- package/assets/assets/{RunsPage-CuHi8241.js → RunsPage-D0qkTwas.js} +1 -1
- package/assets/assets/{SettingsPage-D0fgAoVu.js → SettingsPage-DaqR1wKC.js} +1 -1
- package/assets/assets/SiteHealthSection-dGXxZ8IR.js +4 -0
- package/assets/assets/{TrafficPage-DD65RqeR.js → TrafficPage-DodOmnVv.js} +1 -1
- package/assets/assets/{TrafficSourceDetailPage-CFVCj13J.js → TrafficSourceDetailPage-C7b-BcKF.js} +1 -1
- package/assets/assets/{extract-error-message-D118msoE.js → extract-error-message-BYvzTsML.js} +1 -1
- package/assets/assets/index-BVzozur0.js +86 -0
- package/assets/assets/{react-sigma_core.esm.min-DdlHLMEz.js → react-sigma_core.esm.min-DXcq1O0r.js} +1 -1
- package/assets/assets/{v2-overview-adapter-CoCb2aB0.js → v2-overview-adapter-1MHnrdnr.js} +1 -1
- package/assets/assets/{vendor-lucide-N6vOpZmh.js → vendor-lucide-Y6jANGfG.js} +1 -1
- package/assets/index.html +2 -2
- package/dist/{chunk-ORA5R7QM.js → chunk-5M2RY5WU.js} +132 -104
- package/dist/{chunk-ZQ7C6IFA.js → chunk-COQOQYRZ.js} +749 -614
- package/dist/{chunk-FK7AWV57.js → chunk-EAAM47H6.js} +24 -9
- package/dist/{chunk-PLS4EM2Y.js → chunk-JMC7CEJB.js} +4 -4
- package/dist/{chunk-OPJCFDNR.js → chunk-JUOFNJLV.js} +3 -3
- package/dist/{chunk-C6GWKYGE.js → chunk-U7D7LNVJ.js} +5 -4
- package/dist/{chunk-MHB23E3N.js → chunk-X2S7D3MN.js} +123 -82
- package/dist/cli.js +30 -22
- package/dist/{demo-server-WQDHQG6P.js → demo-server-XWRIXME5.js} +6 -5
- package/dist/index.d.ts +7 -5
- package/dist/index.js +6 -6
- package/dist/{intelligence-service-JWZRZ6JZ.js → intelligence-service-332S7D7Y.js} +2 -2
- package/dist/mcp.js +4 -4
- package/package.json +12 -12
- package/assets/assets/BacklinksPage-ToENGBdp.js +0 -1
- package/assets/assets/SiteHealthSection-DhLxldgs.js +0 -4
- package/assets/assets/index-DxFd1t4G.js +0 -86
|
@@ -20,23 +20,25 @@ import {
|
|
|
20
20
|
saveConfig,
|
|
21
21
|
saveConfigPatch,
|
|
22
22
|
sentimentInstallReadiness
|
|
23
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-JMC7CEJB.js";
|
|
24
24
|
import {
|
|
25
25
|
computeFactorAverages,
|
|
26
26
|
deleteSiteCrawlGraphLayout,
|
|
27
27
|
persistSiteCrawlGraphLayout,
|
|
28
28
|
prepareSiteCrawlGraphLayout,
|
|
29
29
|
recordedSharePct
|
|
30
|
-
} from "./chunk-
|
|
30
|
+
} from "./chunk-U7D7LNVJ.js";
|
|
31
31
|
import {
|
|
32
32
|
CanonryMcpToolNames,
|
|
33
33
|
CliError,
|
|
34
34
|
PACKAGE_VERSION,
|
|
35
35
|
canonryMcpTools
|
|
36
|
-
} from "./chunk-
|
|
36
|
+
} from "./chunk-EAAM47H6.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
|
-
|
|
224
|
+
usageCounters,
|
|
225
|
+
writeAuditLog
|
|
226
|
+
} from "./chunk-COQOQYRZ.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-
|
|
442
|
+
} from "./chunk-X2S7D3MN.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 ? "
|
|
8052
|
+
const embedded = true ? "32caa11b2aaf9890ca60a6eb4abfcffe902b69f6" : 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
|
-
|
|
12147
|
-
|
|
12148
|
-
|
|
12149
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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}`);
|
|
@@ -18781,7 +18780,8 @@ var ADVANCED_TOOLS = [
|
|
|
18781
18780
|
var LEGACY_TOOLS = ["canonry_measurement_overview", "canonry_measurement_plan_get", "canonry_run_completeness"];
|
|
18782
18781
|
var SIMPLE_TOOLS = ["canonry_visibility_report"];
|
|
18783
18782
|
var ADVANCED_ROUTES = [
|
|
18784
|
-
|
|
18783
|
+
'Weakest or strongest Properties: canonry_measurement_portfolio_summary (mentionRanking for both ends). Compact results carry detailsOmitted; use Property evidence reads for omitted names and cited domains. The default cursor enumerates weakest Properties only; set list=strongest-mentions or list=weakest-mentions to enumerate the chosen ranking. Follow nextCursor with unchanged list and filters; pageList names that selection. Use groupKey, or page canonry_measurement_overview with queryClass "non-brand", for all Property rows.',
|
|
18784
|
+
`Rates for one Property, group or market: canonry_measurement_overview with queryClass "non-brand". Branded is a separate call (queryClass "branded"), reported as its own figure; never quote queryClass "all", which pools both. For a market, use scope "market" with its marketKey (a kind market id in canonry_visibility_report scopeOptions): it counts only that market's queries, as the dashboard does. Scope "group" counts every query of its Properties, so never quote a group figure under a market's name; quote the returned scope kind and label.`,
|
|
18785
18785
|
"Which metros have the biggest gaps: the compact summary's weakestMarkets, whose rates cover full top-level metros. Set list=markets and follow nextCursor with unchanged list and filters for every market; tiedAtWeakest describes only the zero-signal Property cohort, never full metro rates.",
|
|
18786
18786
|
"One Property: canonry_measurement_property_evidence and canonry_measurement_property_competitors (names written instead of it, and citedDomains, its own cited sources). Portfolio lists such as weakestAnswerSources pool many Properties.",
|
|
18787
18787
|
'Which names answers give instead across the portfolio: canonry_competitor_landscape with queryClass and runId "latest", answers=not-mentioned. This selects answers naming none of their targeted Properties; preserve populationSize, answerCount and unknownMentionAnswers. Per-Property named-instead lists are samples of weak Properties.',
|
|
@@ -21526,6 +21526,10 @@ function waitForServerRuntimeStartup(app) {
|
|
|
21526
21526
|
const startup = runtimeStartupByServer.get(app);
|
|
21527
21527
|
return startup ?? Promise.reject(new Error("Server runtime startup was not registered"));
|
|
21528
21528
|
}
|
|
21529
|
+
var keylessFirstRunSetupByServer = /* @__PURE__ */ new WeakMap();
|
|
21530
|
+
function allowsKeylessFirstRunSetup(app) {
|
|
21531
|
+
return keylessFirstRunSetupByServer.get(app) ?? false;
|
|
21532
|
+
}
|
|
21529
21533
|
var DEFAULT_QUOTA = {
|
|
21530
21534
|
maxConcurrency: 2,
|
|
21531
21535
|
maxRequestsPerMinute: 10,
|
|
@@ -21618,9 +21622,6 @@ function summarizeProviderConfig(config) {
|
|
|
21618
21622
|
quota: { ...config?.quota ?? DEFAULT_QUOTA }
|
|
21619
21623
|
};
|
|
21620
21624
|
}
|
|
21621
|
-
function hashApiKey2(key) {
|
|
21622
|
-
return crypto29.createHash("sha256").update(key).digest("hex");
|
|
21623
|
-
}
|
|
21624
21625
|
var DASHBOARD_SCRYPT_KEYLEN = 64;
|
|
21625
21626
|
var DASHBOARD_SCRYPT_COST = 1 << 15;
|
|
21626
21627
|
var DASHBOARD_SCRYPT_MAXMEM = 64 * 1024 * 1024;
|
|
@@ -21659,7 +21660,7 @@ function verifyDashboardPassword(password, storedHash) {
|
|
|
21659
21660
|
};
|
|
21660
21661
|
}
|
|
21661
21662
|
if (/^[a-f0-9]{64}$/i.test(storedHash)) {
|
|
21662
|
-
const candidate = Buffer.from(
|
|
21663
|
+
const candidate = Buffer.from(hashApiKey(password), "hex");
|
|
21663
21664
|
const expected = Buffer.from(storedHash, "hex");
|
|
21664
21665
|
if (candidate.length !== expected.length)
|
|
21665
21666
|
return { ok: false, needsRehash: false };
|
|
@@ -21668,22 +21669,6 @@ function verifyDashboardPassword(password, storedHash) {
|
|
|
21668
21669
|
}
|
|
21669
21670
|
return { ok: false, needsRehash: false };
|
|
21670
21671
|
}
|
|
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
21672
|
function serializeSessionCookie(opts) {
|
|
21688
21673
|
const parts = [
|
|
21689
21674
|
`${opts.name}=${opts.value ? encodeURIComponent(opts.value) : ""}`,
|
|
@@ -21754,6 +21739,38 @@ function isLoopbackBindHost(host) {
|
|
|
21754
21739
|
if (/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(normalized)) return true;
|
|
21755
21740
|
return false;
|
|
21756
21741
|
}
|
|
21742
|
+
function isLoopbackPeerAddress(address) {
|
|
21743
|
+
if (!address) return false;
|
|
21744
|
+
const normalized = address.toLowerCase().replace(/^::ffff:(?=\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$)/, "");
|
|
21745
|
+
return isIP(normalized) !== 0 && isLoopbackBindHost(normalized);
|
|
21746
|
+
}
|
|
21747
|
+
function isDirectLocalRequest(request) {
|
|
21748
|
+
if (request.raw.httpVersion !== "1.1") return false;
|
|
21749
|
+
if (!isLoopbackPeerAddress(request.raw.socket.remoteAddress)) return false;
|
|
21750
|
+
if (hasProxyHeaders(request)) return false;
|
|
21751
|
+
const host = request.headers.host;
|
|
21752
|
+
if (!host) return false;
|
|
21753
|
+
let hostname;
|
|
21754
|
+
try {
|
|
21755
|
+
hostname = new URL(`http://${host}`).hostname;
|
|
21756
|
+
} catch {
|
|
21757
|
+
return false;
|
|
21758
|
+
}
|
|
21759
|
+
return hostname !== "" && isLoopbackBindHost(hostname);
|
|
21760
|
+
}
|
|
21761
|
+
function configuresExternalAccess(trustProxy, basePath, ...configuredUrls) {
|
|
21762
|
+
if (trustProxy !== false || basePath !== void 0) return true;
|
|
21763
|
+
return configuredUrls.some((configuredUrl) => {
|
|
21764
|
+
const value = configuredUrl?.trim();
|
|
21765
|
+
if (!value) return false;
|
|
21766
|
+
try {
|
|
21767
|
+
const { hostname } = new URL(value);
|
|
21768
|
+
return hostname === "" || !isLoopbackBindHost(hostname);
|
|
21769
|
+
} catch {
|
|
21770
|
+
return true;
|
|
21771
|
+
}
|
|
21772
|
+
});
|
|
21773
|
+
}
|
|
21757
21774
|
function isAllowedHost(host, allowedHosts) {
|
|
21758
21775
|
const normalized = host.trim().toLowerCase().replace(/^\[|\]$/g, "");
|
|
21759
21776
|
return allowedHosts.has(normalized) || isIP(normalized) !== 0;
|
|
@@ -22028,7 +22045,8 @@ async function createServer(opts) {
|
|
|
22028
22045
|
onFirstActivation: () => maybeShowActivationNotice()
|
|
22029
22046
|
});
|
|
22030
22047
|
jobRunner.recoverStaleRuns();
|
|
22031
|
-
const
|
|
22048
|
+
const allowLoopbackWebhooks = process.env.CANONRY_ALLOW_LOOPBACK_WEBHOOKS !== "0";
|
|
22049
|
+
const notifier = new Notifier(opts.db, serverUrl, { allowLoopbackWebhooks });
|
|
22032
22050
|
const intelligenceService = new IntelligenceService(opts.db);
|
|
22033
22051
|
const aeroClient = new ApiClient(opts.config.apiUrl, opts.config.apiKey, {
|
|
22034
22052
|
skipProbe: true,
|
|
@@ -22819,7 +22837,7 @@ async function createServer(opts) {
|
|
|
22819
22837
|
}
|
|
22820
22838
|
})();
|
|
22821
22839
|
if (opts.config.apiKey) {
|
|
22822
|
-
const keyHash =
|
|
22840
|
+
const keyHash = hashApiKey(opts.config.apiKey);
|
|
22823
22841
|
const existing = opts.db.select().from(apiKeys).where(eq29(apiKeys.keyHash, keyHash)).get();
|
|
22824
22842
|
if (!existing) {
|
|
22825
22843
|
const prefix = opts.config.apiKey.slice(0, 12);
|
|
@@ -22845,11 +22863,11 @@ async function createServer(opts) {
|
|
|
22845
22863
|
ne3(dashboardSessions.passwordFingerprint, currentPasswordFingerprint())
|
|
22846
22864
|
)
|
|
22847
22865
|
)).run();
|
|
22848
|
-
const createSession = (key, passwordBased = false) => {
|
|
22866
|
+
const createSession = (key, passwordBased = false, db = opts.db) => {
|
|
22849
22867
|
const now = /* @__PURE__ */ new Date();
|
|
22850
|
-
|
|
22868
|
+
db.delete(dashboardSessions).where(lte2(dashboardSessions.expiresAt, now.toISOString())).run();
|
|
22851
22869
|
const sessionId = crypto29.randomBytes(32).toString("hex");
|
|
22852
|
-
|
|
22870
|
+
db.insert(dashboardSessions).values({
|
|
22853
22871
|
tokenHash: hashSessionToken(sessionId),
|
|
22854
22872
|
apiKeyId: key.id,
|
|
22855
22873
|
apiKeyHash: key.keyHash,
|
|
@@ -22883,12 +22901,9 @@ async function createServer(opts) {
|
|
|
22883
22901
|
};
|
|
22884
22902
|
const getDefaultApiKey = () => {
|
|
22885
22903
|
if (!opts.config.apiKey) return void 0;
|
|
22886
|
-
return opts.db.select().from(apiKeys).where(eq29(apiKeys.keyHash,
|
|
22904
|
+
return opts.db.select().from(apiKeys).where(eq29(apiKeys.keyHash, hashApiKey(opts.config.apiKey))).get();
|
|
22887
22905
|
};
|
|
22888
|
-
const
|
|
22889
|
-
const key = getDefaultApiKey();
|
|
22890
|
-
if (!key || key.revokedAt) return false;
|
|
22891
|
-
const sessionId = createSession(key, true);
|
|
22906
|
+
const setSessionCookie = (reply, sessionId) => {
|
|
22892
22907
|
reply.header(
|
|
22893
22908
|
"set-cookie",
|
|
22894
22909
|
serializeSessionCookie({
|
|
@@ -22899,19 +22914,27 @@ async function createServer(opts) {
|
|
|
22899
22914
|
ttlMs: SESSION_TTL_MS
|
|
22900
22915
|
})
|
|
22901
22916
|
);
|
|
22917
|
+
};
|
|
22918
|
+
const createPasswordSession = (reply) => {
|
|
22919
|
+
const key = getDefaultApiKey();
|
|
22920
|
+
if (!key || key.revokedAt) return false;
|
|
22921
|
+
setSessionCookie(reply, createSession(key, true));
|
|
22902
22922
|
return true;
|
|
22903
22923
|
};
|
|
22904
|
-
const
|
|
22905
|
-
|
|
22924
|
+
const keylessLocalSetup = isLoopbackBindHost(opts.host) && !configuresExternalAccess(trustProxy, basePath, opts.config.apiUrl, opts.config.publicUrl);
|
|
22925
|
+
keylessFirstRunSetupByServer.set(app, keylessLocalSetup);
|
|
22926
|
+
const requestPresentsKey = (request, key) => {
|
|
22906
22927
|
const header = request.headers.authorization;
|
|
22907
22928
|
if (!header) return false;
|
|
22908
22929
|
const parts = header.split(" ");
|
|
22909
|
-
if (parts.length !== 2 || parts[0] !== "Bearer") return false;
|
|
22910
|
-
const
|
|
22911
|
-
|
|
22930
|
+
if (parts.length !== 2 || parts[0] !== "Bearer" || !parts[1]) return false;
|
|
22931
|
+
const presented = Buffer.from(hashApiKey(parts[1]), "hex");
|
|
22932
|
+
const expected = Buffer.from(key.keyHash, "hex");
|
|
22933
|
+
return presented.length === expected.length && crypto29.timingSafeEqual(presented, expected);
|
|
22912
22934
|
};
|
|
22913
22935
|
const namedAccountsInUse = () => anyUsersExist(opts.db);
|
|
22914
22936
|
const NAMED_ACCOUNTS_MESSAGE = "This install uses named accounts. Sign in with your name and password.";
|
|
22937
|
+
const serverApiKeyMissing = () => new AppError("AUTH_INVALID", "Server API key not found \u2014 run canonry bootstrap", 401);
|
|
22915
22938
|
app.get(apiPrefix + "/session", async (request, reply) => {
|
|
22916
22939
|
if (namedAccountsInUse()) {
|
|
22917
22940
|
return reply.send({ authenticated: false, setupRequired: false });
|
|
@@ -22919,7 +22942,7 @@ async function createServer(opts) {
|
|
|
22919
22942
|
if (!dashboardRequirePassword) {
|
|
22920
22943
|
return reply.send({ authenticated: true, setupRequired: false });
|
|
22921
22944
|
}
|
|
22922
|
-
const sessionId =
|
|
22945
|
+
const sessionId = parseCookieHeader(request.headers.cookie)[SESSION_COOKIE_NAME];
|
|
22923
22946
|
return reply.send({
|
|
22924
22947
|
authenticated: Boolean(sessionId && resolveSessionApiKeyId(sessionId)),
|
|
22925
22948
|
setupRequired: !opts.config.dashboardPasswordHash
|
|
@@ -22933,9 +22956,13 @@ async function createServer(opts) {
|
|
|
22933
22956
|
if (!dashboardRequirePassword) {
|
|
22934
22957
|
return reply.send({ authenticated: true, setupRequired: false });
|
|
22935
22958
|
}
|
|
22936
|
-
|
|
22937
|
-
|
|
22938
|
-
|
|
22959
|
+
const found = getDefaultApiKey();
|
|
22960
|
+
const defaultKey = found && !found.revokedAt ? found : void 0;
|
|
22961
|
+
const presentsConfiguredKey = opts.config.apiKey ? requestPresentsKey(request, { keyHash: hashApiKey(opts.config.apiKey) }) : false;
|
|
22962
|
+
const presentedRootKey = defaultKey !== void 0 && presentsConfiguredKey;
|
|
22963
|
+
if (!presentedRootKey && !(keylessLocalSetup && isDirectLocalRequest(request))) {
|
|
22964
|
+
const err = presentsConfiguredKey ? serverApiKeyMissing() : authRequired(
|
|
22965
|
+
"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
22966
|
);
|
|
22940
22967
|
return reply.status(err.statusCode).send(err.toJSON());
|
|
22941
22968
|
}
|
|
@@ -22948,12 +22975,31 @@ async function createServer(opts) {
|
|
|
22948
22975
|
const err = validationError("Password must be at least 8 characters");
|
|
22949
22976
|
return reply.status(err.statusCode).send(err.toJSON());
|
|
22950
22977
|
}
|
|
22951
|
-
|
|
22952
|
-
|
|
22953
|
-
if (!createPasswordSession(reply)) {
|
|
22954
|
-
const err = authInvalid();
|
|
22978
|
+
if (!defaultKey) {
|
|
22979
|
+
const err = serverApiKeyMissing();
|
|
22955
22980
|
return reply.status(err.statusCode).send(err.toJSON());
|
|
22956
22981
|
}
|
|
22982
|
+
opts.config.dashboardPasswordHash = hashDashboardPassword(password);
|
|
22983
|
+
let sessionId;
|
|
22984
|
+
try {
|
|
22985
|
+
sessionId = opts.db.transaction((tx) => {
|
|
22986
|
+
writeAuditLog(tx, auditFromRequest(request, {
|
|
22987
|
+
actor: `api-key:${defaultKey.id}`,
|
|
22988
|
+
credentialId: presentedRootKey ? defaultKey.id : null,
|
|
22989
|
+
requestId: request.id,
|
|
22990
|
+
action: "dashboard-password.created",
|
|
22991
|
+
entityType: "dashboard-password",
|
|
22992
|
+
diff: { authorizedBy: presentedRootKey ? "root-api-key" : "local-request" }
|
|
22993
|
+
}));
|
|
22994
|
+
const created = createSession(defaultKey, true, tx);
|
|
22995
|
+
saveConfigPatch(opts.config);
|
|
22996
|
+
return created;
|
|
22997
|
+
});
|
|
22998
|
+
} catch (err) {
|
|
22999
|
+
opts.config.dashboardPasswordHash = void 0;
|
|
23000
|
+
throw err;
|
|
23001
|
+
}
|
|
23002
|
+
setSessionCookie(reply, sessionId);
|
|
22957
23003
|
return reply.send({ authenticated: true });
|
|
22958
23004
|
});
|
|
22959
23005
|
app.post(apiPrefix + "/session", async (request, reply) => {
|
|
@@ -22992,40 +23038,26 @@ async function createServer(opts) {
|
|
|
22992
23038
|
opts.config.dashboardPasswordHash = migratedHash;
|
|
22993
23039
|
}
|
|
22994
23040
|
if (!createPasswordSession(reply)) {
|
|
22995
|
-
|
|
22996
|
-
|
|
22997
|
-
code: "AUTH_INVALID",
|
|
22998
|
-
message: "Server API key not found \u2014 run canonry bootstrap"
|
|
22999
|
-
}
|
|
23000
|
-
});
|
|
23041
|
+
const err2 = serverApiKeyMissing();
|
|
23042
|
+
return reply.status(err2.statusCode).send(err2.toJSON());
|
|
23001
23043
|
}
|
|
23002
23044
|
return reply.send({ authenticated: true });
|
|
23003
23045
|
}
|
|
23004
23046
|
if (apiKey) {
|
|
23005
|
-
const key = opts.db.select().from(apiKeys).where(eq29(apiKeys.keyHash,
|
|
23047
|
+
const key = opts.db.select().from(apiKeys).where(eq29(apiKeys.keyHash, hashApiKey(apiKey))).get();
|
|
23006
23048
|
if (!key || key.revokedAt) {
|
|
23007
23049
|
const err2 = authInvalid();
|
|
23008
23050
|
return reply.status(err2.statusCode).send(err2.toJSON());
|
|
23009
23051
|
}
|
|
23010
23052
|
opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq29(apiKeys.id, key.id)).run();
|
|
23011
|
-
|
|
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
|
-
);
|
|
23053
|
+
setSessionCookie(reply, createSession(key));
|
|
23022
23054
|
return reply.send({ authenticated: true });
|
|
23023
23055
|
}
|
|
23024
23056
|
const err = validationError("Either password or apiKey is required");
|
|
23025
23057
|
return reply.status(err.statusCode).send(err.toJSON());
|
|
23026
23058
|
});
|
|
23027
23059
|
app.delete(apiPrefix + "/session", async (request, reply) => {
|
|
23028
|
-
const sessionId =
|
|
23060
|
+
const sessionId = parseCookieHeader(request.headers.cookie)[SESSION_COOKIE_NAME];
|
|
23029
23061
|
clearSession(sessionId);
|
|
23030
23062
|
reply.header(
|
|
23031
23063
|
"set-cookie",
|
|
@@ -23277,12 +23309,7 @@ async function createServer(opts) {
|
|
|
23277
23309
|
getAgentPluginState: opts.getAgentPluginState,
|
|
23278
23310
|
// Powers the `canonry.version.current` doctor check. Non-blocking.
|
|
23279
23311
|
getUpdateStatus: () => getServerUpdateStatus(),
|
|
23280
|
-
|
|
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",
|
|
23312
|
+
allowLoopbackWebhooks,
|
|
23286
23313
|
// Wall-clock budget for one incremental Vercel drain. This is the only lever
|
|
23287
23314
|
// that decides whether a source catches up or falls further behind, and it
|
|
23288
23315
|
// was previously reachable only from tests, so a source losing ground could
|
|
@@ -24164,6 +24191,7 @@ export {
|
|
|
24164
24191
|
createGoogleMarketingCredentialStore,
|
|
24165
24192
|
createGoogleMarketingRuntime,
|
|
24166
24193
|
waitForServerRuntimeStartup,
|
|
24194
|
+
allowsKeylessFirstRunSetup,
|
|
24167
24195
|
isLoopbackBindHost,
|
|
24168
24196
|
createServer
|
|
24169
24197
|
};
|