@ainyc/canonry 2.12.1 → 2.13.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.
package/assets/index.html CHANGED
@@ -12,7 +12,7 @@
12
12
  <link rel="icon" type="image/png" sizes="32x32" href="./favicon-32.png" />
13
13
  <link rel="apple-touch-icon" href="./apple-touch-icon.png" />
14
14
  <title>Canonry</title>
15
- <script type="module" crossorigin src="./assets/index-Ckr4V5dK.js"></script>
15
+ <script type="module" crossorigin src="./assets/index-CKzK0os-.js"></script>
16
16
  <link rel="stylesheet" crossorigin href="./assets/index-CAewPdsZ.css">
17
17
  </head>
18
18
  <body>
@@ -6,6 +6,9 @@ import {
6
6
  ApiClient,
7
7
  AppError,
8
8
  CcReleaseSyncStatuses,
9
+ CheckCategories,
10
+ CheckScopes,
11
+ CheckStatuses,
9
12
  CitationStates,
10
13
  MemorySources,
11
14
  RunKinds,
@@ -50,12 +53,13 @@ import {
50
53
  scheduleUpsertRequestSchema,
51
54
  serializeRunError,
52
55
  snapshotRequestSchema,
56
+ summarizeCheckResults,
53
57
  unsupportedKind,
54
58
  validationError,
55
59
  visibilityStateFromAnswerMentioned,
56
60
  windowCutoff,
57
61
  wordpressEnvSchema
58
- } from "./chunk-PLI7EOPM.js";
62
+ } from "./chunk-XJS7NALL.js";
59
63
  import {
60
64
  IntelligenceService,
61
65
  agentMemory,
@@ -5303,6 +5307,44 @@ var routeCatalog = [
5303
5307
  404: { description: "Project not found." }
5304
5308
  }
5305
5309
  },
5310
+ {
5311
+ method: "get",
5312
+ path: "/api/v1/doctor",
5313
+ summary: "Run global health checks",
5314
+ description: "Runs all global-scope checks (provider keys, etc.). Use ?check=<id> or ?check=<prefix>* (comma-separated) to filter. Returns a structured DoctorReport with per-check status, code, summary, remediation, and details.",
5315
+ tags: ["doctor"],
5316
+ parameters: [
5317
+ {
5318
+ name: "check",
5319
+ in: "query",
5320
+ description: 'Optional comma-separated list of check IDs or wildcard prefixes (e.g. "config.*").',
5321
+ schema: stringSchema
5322
+ }
5323
+ ],
5324
+ responses: {
5325
+ 200: { description: "Doctor report returned." }
5326
+ }
5327
+ },
5328
+ {
5329
+ method: "get",
5330
+ path: "/api/v1/projects/{name}/doctor",
5331
+ summary: "Run project health checks",
5332
+ description: "Runs project-scoped checks (Google auth, GA auth, etc.). Use ?check=<id> or ?check=<prefix>* (comma-separated) to filter \u2014 e.g. ?check=google.* for just Google auth checks. Returns a structured DoctorReport.",
5333
+ tags: ["doctor"],
5334
+ parameters: [
5335
+ nameParameter,
5336
+ {
5337
+ name: "check",
5338
+ in: "query",
5339
+ description: 'Optional comma-separated list of check IDs or wildcard prefixes (e.g. "google.auth.*").',
5340
+ schema: stringSchema
5341
+ }
5342
+ ],
5343
+ responses: {
5344
+ 200: { description: "Doctor report returned." },
5345
+ 404: { description: "Project not found." }
5346
+ }
5347
+ },
5306
5348
  {
5307
5349
  method: "get",
5308
5350
  path: "/api/v1/backlinks/status",
@@ -11193,6 +11235,595 @@ async function backlinksRoutes(app, opts) {
11193
11235
  );
11194
11236
  }
11195
11237
 
11238
+ // ../api-routes/src/doctor/checks/ga-auth.ts
11239
+ async function checkServiceAccount(conn) {
11240
+ if (!conn.propertyId) {
11241
+ return {
11242
+ status: CheckStatuses.fail,
11243
+ code: "ga.auth.no-property-selected",
11244
+ summary: "GA4 service account record has no property ID set.",
11245
+ remediation: "Set a propertyId in the GA4 credential record (config.yaml `ga4.connections[].propertyId`)."
11246
+ };
11247
+ }
11248
+ if (!conn.clientEmail || !conn.privateKey) {
11249
+ return {
11250
+ status: CheckStatuses.fail,
11251
+ code: "ga.auth.service-account-incomplete",
11252
+ summary: "GA4 service account is missing clientEmail or privateKey.",
11253
+ remediation: "Provide a complete service account JSON key (clientEmail + privateKey) in config.yaml.",
11254
+ details: {
11255
+ hasClientEmail: Boolean(conn.clientEmail),
11256
+ hasPrivateKey: Boolean(conn.privateKey)
11257
+ }
11258
+ };
11259
+ }
11260
+ try {
11261
+ await verifyConnection(conn.clientEmail, conn.privateKey, conn.propertyId);
11262
+ } catch (err) {
11263
+ const message = err instanceof Error ? err.message : String(err);
11264
+ return {
11265
+ status: CheckStatuses.fail,
11266
+ code: "ga.auth.verify-failed",
11267
+ summary: "GA4 service account could not authenticate against the configured property.",
11268
+ remediation: `Verify the service account has Viewer access on property ${conn.propertyId}, and that the private key in config.yaml is the active key for the service account.`,
11269
+ details: { propertyId: conn.propertyId, error: message, authMethod: "service-account" }
11270
+ };
11271
+ }
11272
+ return {
11273
+ status: CheckStatuses.ok,
11274
+ code: "ga.auth.verified",
11275
+ summary: `GA4 property ${conn.propertyId} is reachable with the configured service account.`,
11276
+ remediation: null,
11277
+ details: { propertyId: conn.propertyId, clientEmail: conn.clientEmail, authMethod: "service-account" }
11278
+ };
11279
+ }
11280
+ async function checkOAuthConnection(ctx, projectName, conn) {
11281
+ if (!conn.propertyId) {
11282
+ return {
11283
+ status: CheckStatuses.fail,
11284
+ code: "ga.auth.no-property-selected",
11285
+ summary: "GA4 OAuth connection has no property selected.",
11286
+ remediation: `Run \`canonry google connect ${projectName} --type ga4\` to select a property.`
11287
+ };
11288
+ }
11289
+ if (!conn.refreshToken) {
11290
+ return {
11291
+ status: CheckStatuses.fail,
11292
+ code: "ga.auth.no-refresh-token",
11293
+ summary: "GA4 OAuth connection has no refresh token stored.",
11294
+ remediation: `Run \`canonry google connect ${projectName} --type ga4\` to re-authorize and capture a refresh token.`,
11295
+ details: { propertyId: conn.propertyId }
11296
+ };
11297
+ }
11298
+ const auth = ctx.getGoogleAuthConfig?.() ?? {};
11299
+ if (!auth.clientId || !auth.clientSecret) {
11300
+ return {
11301
+ status: CheckStatuses.fail,
11302
+ code: "ga.auth.oauth-not-configured",
11303
+ summary: "GA4 OAuth connection exists but Google OAuth client ID/secret is missing.",
11304
+ remediation: "Set `google.clientId` and `google.clientSecret` in ~/.canonry/config.yaml."
11305
+ };
11306
+ }
11307
+ let accessToken;
11308
+ try {
11309
+ const tokens = await refreshAccessToken(auth.clientId, auth.clientSecret, conn.refreshToken);
11310
+ accessToken = tokens.access_token;
11311
+ } catch (err) {
11312
+ const message = err instanceof Error ? err.message : String(err);
11313
+ return {
11314
+ status: CheckStatuses.fail,
11315
+ code: "ga.auth.refresh-failed",
11316
+ summary: "GA4 OAuth refresh token rejected by Google.",
11317
+ remediation: `Run \`canonry google connect ${projectName} --type ga4\` to re-authorize.`,
11318
+ details: { propertyId: conn.propertyId, error: message, authMethod: "oauth" }
11319
+ };
11320
+ }
11321
+ try {
11322
+ await verifyConnectionWithToken(accessToken, conn.propertyId);
11323
+ } catch (err) {
11324
+ const message = err instanceof Error ? err.message : String(err);
11325
+ return {
11326
+ status: CheckStatuses.fail,
11327
+ code: "ga.auth.verify-failed",
11328
+ summary: "GA4 OAuth token cannot reach the configured property.",
11329
+ remediation: `Verify the authorized Google account has access to property ${conn.propertyId}, or run \`canonry google connect ${projectName} --type ga4\` to re-authorize.`,
11330
+ details: { propertyId: conn.propertyId, error: message, authMethod: "oauth" }
11331
+ };
11332
+ }
11333
+ return {
11334
+ status: CheckStatuses.ok,
11335
+ code: "ga.auth.verified",
11336
+ summary: `GA4 property ${conn.propertyId} is reachable via OAuth.`,
11337
+ remediation: null,
11338
+ details: { propertyId: conn.propertyId, authMethod: "oauth" }
11339
+ };
11340
+ }
11341
+ var ga4ConnectionCheck = {
11342
+ id: "ga.auth.connection",
11343
+ category: CheckCategories.auth,
11344
+ scope: CheckScopes.project,
11345
+ title: "GA4 connection",
11346
+ run: async (ctx) => {
11347
+ if (!ctx.project) {
11348
+ return {
11349
+ status: CheckStatuses.skipped,
11350
+ code: "ga.auth.no-project",
11351
+ summary: "Project context required.",
11352
+ remediation: null
11353
+ };
11354
+ }
11355
+ const saStore = ctx.ga4CredentialStore;
11356
+ const oauthStore = ctx.googleConnectionStore;
11357
+ if (!saStore && !oauthStore) {
11358
+ return {
11359
+ status: CheckStatuses.skipped,
11360
+ code: "ga.auth.store-unavailable",
11361
+ summary: "No GA4 credential store configured for this deployment.",
11362
+ remediation: null
11363
+ };
11364
+ }
11365
+ const saConn = saStore?.getConnection(ctx.project.name);
11366
+ if (saConn) return checkServiceAccount(saConn);
11367
+ const oauthConn = oauthStore?.getConnection(ctx.project.canonicalDomain, "ga4");
11368
+ if (oauthConn) return checkOAuthConnection(ctx, ctx.project.name, oauthConn);
11369
+ return {
11370
+ status: CheckStatuses.warn,
11371
+ code: "ga.auth.no-connection",
11372
+ summary: "No GA4 connection configured for this project.",
11373
+ remediation: `Run \`canonry google connect ${ctx.project.name} --type ga4\` to authorize via OAuth, or set up a service account in ~/.canonry/config.yaml under \`ga4.connections\`.`
11374
+ };
11375
+ }
11376
+ };
11377
+ var GA_AUTH_CHECKS = [ga4ConnectionCheck];
11378
+
11379
+ // ../api-routes/src/doctor/checks/google-auth.ts
11380
+ var REQUIRED_GSC_SCOPES = [GSC_SCOPE, INDEXING_SCOPE];
11381
+ async function resolveAccessToken(ctx) {
11382
+ if (!ctx.project) {
11383
+ return { ok: false, output: skippedNoProject() };
11384
+ }
11385
+ const store = ctx.googleConnectionStore;
11386
+ if (!store) {
11387
+ return {
11388
+ ok: false,
11389
+ output: {
11390
+ status: CheckStatuses.skipped,
11391
+ code: "google.auth.store-unavailable",
11392
+ summary: "Google connection store is not configured for this deployment.",
11393
+ remediation: null
11394
+ }
11395
+ };
11396
+ }
11397
+ const auth = ctx.getGoogleAuthConfig?.() ?? {};
11398
+ if (!auth.clientId || !auth.clientSecret) {
11399
+ return {
11400
+ ok: false,
11401
+ output: {
11402
+ status: CheckStatuses.fail,
11403
+ code: "google.auth.oauth-not-configured",
11404
+ summary: "Google OAuth client ID or secret is missing.",
11405
+ remediation: "Set Google OAuth credentials in ~/.canonry/config.yaml under `google.clientId` and `google.clientSecret`."
11406
+ }
11407
+ };
11408
+ }
11409
+ const conn = store.getConnection(ctx.project.canonicalDomain, "gsc");
11410
+ if (!conn) {
11411
+ return {
11412
+ ok: false,
11413
+ output: {
11414
+ status: CheckStatuses.fail,
11415
+ code: "google.auth.no-connection",
11416
+ summary: `No GSC connection for ${ctx.project.canonicalDomain}.`,
11417
+ remediation: `Run \`canonry google connect ${ctx.project.name} --type gsc\` to authorize.`
11418
+ }
11419
+ };
11420
+ }
11421
+ if (!conn.refreshToken) {
11422
+ return {
11423
+ ok: false,
11424
+ output: {
11425
+ status: CheckStatuses.fail,
11426
+ code: "google.auth.no-refresh-token",
11427
+ summary: "GSC connection exists but has no refresh token stored.",
11428
+ remediation: `Run \`canonry google connect ${ctx.project.name} --type gsc\` to re-authorize and capture a refresh token.`,
11429
+ details: { domain: conn.domain }
11430
+ }
11431
+ };
11432
+ }
11433
+ try {
11434
+ const tokens = await refreshAccessToken(auth.clientId, auth.clientSecret, conn.refreshToken);
11435
+ return { ok: true, token: { accessToken: tokens.access_token } };
11436
+ } catch (err) {
11437
+ const message = err instanceof Error ? err.message : String(err);
11438
+ return {
11439
+ ok: false,
11440
+ output: {
11441
+ status: CheckStatuses.fail,
11442
+ code: "google.auth.refresh-failed",
11443
+ summary: "Refresh token rejected by Google.",
11444
+ remediation: `Run \`canonry google connect ${ctx.project.name} --type gsc\` to re-authorize. Refresh tokens are revoked if the user changes their password or the OAuth client is rotated.`,
11445
+ details: { domain: conn.domain, error: message }
11446
+ }
11447
+ };
11448
+ }
11449
+ }
11450
+ function skippedNoProject() {
11451
+ return {
11452
+ status: CheckStatuses.skipped,
11453
+ code: "google.auth.no-project",
11454
+ summary: "Project context required.",
11455
+ remediation: null
11456
+ };
11457
+ }
11458
+ var connectionCheck = {
11459
+ id: "google.auth.connection",
11460
+ category: CheckCategories.auth,
11461
+ scope: CheckScopes.project,
11462
+ title: "GSC OAuth connection",
11463
+ run: async (ctx) => {
11464
+ const resolved = await resolveAccessToken(ctx);
11465
+ if (!resolved.ok) return resolved.output;
11466
+ return {
11467
+ status: CheckStatuses.ok,
11468
+ code: "google.auth.connected",
11469
+ summary: "GSC OAuth connection is valid and refreshable.",
11470
+ remediation: null
11471
+ };
11472
+ }
11473
+ };
11474
+ var propertyAccessCheck = {
11475
+ id: "google.auth.property-access",
11476
+ category: CheckCategories.auth,
11477
+ scope: CheckScopes.project,
11478
+ title: "GSC property access",
11479
+ run: async (ctx) => {
11480
+ if (!ctx.project) return skippedNoProject();
11481
+ const store = ctx.googleConnectionStore;
11482
+ if (!store) {
11483
+ return {
11484
+ status: CheckStatuses.skipped,
11485
+ code: "google.auth.store-unavailable",
11486
+ summary: "Google connection store is not configured for this deployment.",
11487
+ remediation: null
11488
+ };
11489
+ }
11490
+ const conn = store.getConnection(ctx.project.canonicalDomain, "gsc");
11491
+ if (!conn) {
11492
+ return {
11493
+ status: CheckStatuses.skipped,
11494
+ code: "google.auth.no-connection",
11495
+ summary: "No GSC connection \u2014 run google.auth.connection first.",
11496
+ remediation: null
11497
+ };
11498
+ }
11499
+ if (!conn.propertyId) {
11500
+ return {
11501
+ status: CheckStatuses.fail,
11502
+ code: "google.auth.no-property-selected",
11503
+ summary: "GSC connection has no property selected.",
11504
+ remediation: `Run \`canonry google properties ${ctx.project.name}\` to list available properties, then \`canonry google set-property ${ctx.project.name} <siteUrl>\`.`
11505
+ };
11506
+ }
11507
+ const resolved = await resolveAccessToken(ctx);
11508
+ if (!resolved.ok) {
11509
+ return {
11510
+ status: CheckStatuses.skipped,
11511
+ code: "google.auth.token-unresolved",
11512
+ summary: "Skipped \u2014 token could not be refreshed (see google.auth.connection).",
11513
+ remediation: null
11514
+ };
11515
+ }
11516
+ let sites;
11517
+ try {
11518
+ sites = await listSites(resolved.token.accessToken);
11519
+ } catch (err) {
11520
+ if (err instanceof GoogleApiError && err.status === 403) {
11521
+ return {
11522
+ status: CheckStatuses.fail,
11523
+ code: "google.auth.principal-forbidden",
11524
+ summary: "The authorized Google account is forbidden from listing GSC sites.",
11525
+ remediation: `Reconnect with a Google account that has access in Search Console: \`canonry google connect ${ctx.project.name} --type gsc\`.`,
11526
+ details: { error: err.message }
11527
+ };
11528
+ }
11529
+ const message = err instanceof Error ? err.message : String(err);
11530
+ return {
11531
+ status: CheckStatuses.fail,
11532
+ code: "google.auth.list-sites-failed",
11533
+ summary: "Failed to list GSC sites for the authorized account.",
11534
+ remediation: "Check Google Search Console availability, then re-run.",
11535
+ details: { error: message }
11536
+ };
11537
+ }
11538
+ const match = sites.find((site) => site.siteUrl === conn.propertyId);
11539
+ if (!match) {
11540
+ return {
11541
+ status: CheckStatuses.fail,
11542
+ code: "google.auth.property-not-accessible",
11543
+ summary: `Selected property "${conn.propertyId}" is not in the authorized account's accessible sites list.`,
11544
+ remediation: `Either grant the authorizing Google account access to "${conn.propertyId}" in Search Console, or run \`canonry google set-property ${ctx.project.name} <siteUrl>\` to pick an accessible site.`,
11545
+ details: {
11546
+ selectedProperty: conn.propertyId,
11547
+ accessibleSites: sites.map((s) => s.siteUrl)
11548
+ }
11549
+ };
11550
+ }
11551
+ return {
11552
+ status: CheckStatuses.ok,
11553
+ code: "google.auth.property-accessible",
11554
+ summary: `Property "${conn.propertyId}" is accessible (permission: ${match.permissionLevel}).`,
11555
+ remediation: null,
11556
+ details: {
11557
+ selectedProperty: conn.propertyId,
11558
+ permissionLevel: match.permissionLevel
11559
+ }
11560
+ };
11561
+ }
11562
+ };
11563
+ var redirectUriCheck = {
11564
+ id: "google.auth.redirect-uri",
11565
+ category: CheckCategories.auth,
11566
+ scope: CheckScopes.project,
11567
+ title: "OAuth redirect URI",
11568
+ run: async (ctx) => {
11569
+ if (!ctx.project) return skippedNoProject();
11570
+ const auth = ctx.getGoogleAuthConfig?.() ?? {};
11571
+ if (!auth.clientId || !auth.clientSecret) {
11572
+ return {
11573
+ status: CheckStatuses.fail,
11574
+ code: "google.auth.oauth-not-configured",
11575
+ summary: "Google OAuth client ID or secret is missing.",
11576
+ remediation: "Set `google.clientId` and `google.clientSecret` in ~/.canonry/config.yaml."
11577
+ };
11578
+ }
11579
+ if (!ctx.redirectUri) {
11580
+ return {
11581
+ status: CheckStatuses.warn,
11582
+ code: "google.auth.redirect-uri-auto-detected",
11583
+ summary: "No publicUrl configured \u2014 OAuth callback will be auto-detected from request headers each connect.",
11584
+ remediation: "Set `publicUrl` in ~/.canonry/config.yaml so canonry uses a stable redirect URI (e.g. http://localhost:4100). Then register that exact URI in Google Cloud Console under your OAuth client."
11585
+ };
11586
+ }
11587
+ let parsed;
11588
+ try {
11589
+ parsed = new URL(ctx.redirectUri);
11590
+ } catch {
11591
+ return {
11592
+ status: CheckStatuses.fail,
11593
+ code: "google.auth.redirect-uri-invalid",
11594
+ summary: `Configured redirect URI is not a valid URL: ${ctx.redirectUri}`,
11595
+ remediation: "Set `publicUrl` to a valid http(s) URL in ~/.canonry/config.yaml.",
11596
+ details: { redirectUri: ctx.redirectUri }
11597
+ };
11598
+ }
11599
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
11600
+ return {
11601
+ status: CheckStatuses.fail,
11602
+ code: "google.auth.redirect-uri-invalid",
11603
+ summary: `Redirect URI must use http or https: ${ctx.redirectUri}`,
11604
+ remediation: "Set `publicUrl` to an http(s) URL.",
11605
+ details: { redirectUri: ctx.redirectUri }
11606
+ };
11607
+ }
11608
+ return {
11609
+ status: CheckStatuses.ok,
11610
+ code: "google.auth.redirect-uri-configured",
11611
+ summary: `Redirect URI is ${ctx.redirectUri}.`,
11612
+ remediation: `Ensure this exact URI is listed in your OAuth client's authorized redirect URIs in Google Cloud Console.`,
11613
+ details: { redirectUri: ctx.redirectUri }
11614
+ };
11615
+ }
11616
+ };
11617
+ var scopesCheck = {
11618
+ id: "google.auth.scopes",
11619
+ category: CheckCategories.auth,
11620
+ scope: CheckScopes.project,
11621
+ title: "GSC granted scopes",
11622
+ run: async (ctx) => {
11623
+ if (!ctx.project) return skippedNoProject();
11624
+ const store = ctx.googleConnectionStore;
11625
+ if (!store) {
11626
+ return {
11627
+ status: CheckStatuses.skipped,
11628
+ code: "google.auth.store-unavailable",
11629
+ summary: "Google connection store is not configured for this deployment.",
11630
+ remediation: null
11631
+ };
11632
+ }
11633
+ const conn = store.getConnection(ctx.project.canonicalDomain, "gsc");
11634
+ if (!conn) {
11635
+ return {
11636
+ status: CheckStatuses.skipped,
11637
+ code: "google.auth.no-connection",
11638
+ summary: "No GSC connection \u2014 run google.auth.connection first.",
11639
+ remediation: null
11640
+ };
11641
+ }
11642
+ const granted = new Set(conn.scopes ?? []);
11643
+ const missing = REQUIRED_GSC_SCOPES.filter((scope) => !granted.has(scope));
11644
+ if (missing.length === 0) {
11645
+ return {
11646
+ status: CheckStatuses.ok,
11647
+ code: "google.auth.scopes-ok",
11648
+ summary: "All required GSC scopes are granted.",
11649
+ remediation: null,
11650
+ details: { granted: [...granted] }
11651
+ };
11652
+ }
11653
+ const gscMissing = missing.includes(GSC_SCOPE);
11654
+ const indexingOnlyMissing = !gscMissing && missing.includes(INDEXING_SCOPE) && missing.length === 1;
11655
+ return {
11656
+ status: indexingOnlyMissing ? CheckStatuses.warn : CheckStatuses.fail,
11657
+ code: indexingOnlyMissing ? "google.auth.indexing-scope-missing" : "google.auth.required-scope-missing",
11658
+ summary: indexingOnlyMissing ? "Indexing API scope is not granted \u2014 `canonry google request-indexing` will fail." : `Missing required scopes: ${missing.join(", ")}.`,
11659
+ remediation: `Reconnect to grant missing scopes: \`canonry google connect ${ctx.project.name} --type gsc\`.`,
11660
+ details: { granted: [...granted], missing }
11661
+ };
11662
+ }
11663
+ };
11664
+ var GOOGLE_AUTH_CHECKS = [
11665
+ connectionCheck,
11666
+ propertyAccessCheck,
11667
+ redirectUriCheck,
11668
+ scopesCheck
11669
+ ];
11670
+ var GOOGLE_AUTH_CHECK_BY_ID = Object.fromEntries(
11671
+ GOOGLE_AUTH_CHECKS.map((check) => [check.id, check])
11672
+ );
11673
+
11674
+ // ../api-routes/src/doctor/checks/providers.ts
11675
+ var providersConfiguredCheck = {
11676
+ id: "config.providers",
11677
+ category: CheckCategories.providers,
11678
+ scope: CheckScopes.global,
11679
+ title: "Provider keys",
11680
+ run: (ctx) => {
11681
+ const summary = ctx.providerSummary;
11682
+ if (!summary) {
11683
+ return {
11684
+ status: CheckStatuses.skipped,
11685
+ code: "providers.summary-unavailable",
11686
+ summary: "Provider summary is not available in this deployment.",
11687
+ remediation: null
11688
+ };
11689
+ }
11690
+ const configured = summary.filter((entry) => entry.configured).map((entry) => entry.name);
11691
+ const total = summary.length;
11692
+ if (configured.length === 0) {
11693
+ return {
11694
+ status: CheckStatuses.fail,
11695
+ code: "providers.none-configured",
11696
+ summary: "No answer-engine providers have credentials configured.",
11697
+ remediation: "Run `canonry init` to set provider keys interactively, or add them via flags (`--gemini-key`, `--openai-key`, `--claude-key`, `--perplexity-key`).",
11698
+ details: { available: summary.map((entry) => entry.name) }
11699
+ };
11700
+ }
11701
+ return {
11702
+ status: CheckStatuses.ok,
11703
+ code: "providers.configured",
11704
+ summary: `${configured.length} of ${total} providers configured: ${configured.join(", ")}.`,
11705
+ remediation: null,
11706
+ details: { configured, total }
11707
+ };
11708
+ }
11709
+ };
11710
+ var PROVIDERS_CHECKS = [providersConfiguredCheck];
11711
+
11712
+ // ../api-routes/src/doctor/registry.ts
11713
+ var ALL_CHECKS = [
11714
+ ...GOOGLE_AUTH_CHECKS,
11715
+ ...GA_AUTH_CHECKS,
11716
+ ...PROVIDERS_CHECKS
11717
+ ];
11718
+ var CHECK_BY_ID = Object.fromEntries(
11719
+ ALL_CHECKS.map((check) => [check.id, check])
11720
+ );
11721
+
11722
+ // ../api-routes/src/doctor/runner.ts
11723
+ function matchesCheckId(checkId, filters) {
11724
+ if (filters.length === 0) return true;
11725
+ for (const filter of filters) {
11726
+ if (filter === checkId) return true;
11727
+ if (filter.endsWith("*")) {
11728
+ const prefix = filter.slice(0, -1);
11729
+ if (checkId.startsWith(prefix)) return true;
11730
+ }
11731
+ }
11732
+ return false;
11733
+ }
11734
+ async function runChecks(ctx, checks, options = {}) {
11735
+ const startedAt = /* @__PURE__ */ new Date();
11736
+ const filters = options.checkIds ?? [];
11737
+ const targetScope = ctx.project ? CheckScopes.project : CheckScopes.global;
11738
+ const projectName = ctx.project?.name ?? null;
11739
+ const selected = checks.filter((check) => {
11740
+ if (check.scope !== targetScope) return false;
11741
+ return matchesCheckId(check.id, filters);
11742
+ });
11743
+ const results = [];
11744
+ for (const definition of selected) {
11745
+ const checkStarted = Date.now();
11746
+ let output;
11747
+ try {
11748
+ output = await definition.run(ctx);
11749
+ } catch (err) {
11750
+ const message = err instanceof Error ? err.message : String(err);
11751
+ output = {
11752
+ status: CheckStatuses.fail,
11753
+ code: `${definition.id}.runtime-error`,
11754
+ summary: `Check threw an unexpected error: ${message}`,
11755
+ remediation: null,
11756
+ details: { error: message }
11757
+ };
11758
+ }
11759
+ results.push({
11760
+ id: definition.id,
11761
+ category: definition.category,
11762
+ scope: definition.scope,
11763
+ title: definition.title,
11764
+ status: output.status,
11765
+ code: output.code,
11766
+ summary: output.summary,
11767
+ remediation: output.remediation ?? null,
11768
+ details: output.details,
11769
+ durationMs: Date.now() - checkStarted
11770
+ });
11771
+ }
11772
+ return {
11773
+ scope: targetScope,
11774
+ project: projectName,
11775
+ generatedAt: startedAt.toISOString(),
11776
+ durationMs: Date.now() - startedAt.getTime(),
11777
+ summary: summarizeCheckResults(results),
11778
+ checks: results
11779
+ };
11780
+ }
11781
+
11782
+ // ../api-routes/src/doctor.ts
11783
+ function parseCheckIds(raw) {
11784
+ if (!raw) return [];
11785
+ return raw.split(",").map((token) => token.trim()).filter((token) => token.length > 0);
11786
+ }
11787
+ function resolveRedirectUri(opts) {
11788
+ if (!opts.publicUrl) return void 0;
11789
+ return `${opts.publicUrl.replace(/\/$/, "")}/api/v1/google/callback`;
11790
+ }
11791
+ async function doctorRoutes(app, opts) {
11792
+ const redirectUri = resolveRedirectUri(opts);
11793
+ app.get("/doctor", async (request) => {
11794
+ const checkIds = parseCheckIds(request.query.check);
11795
+ const ctx = {
11796
+ db: app.db,
11797
+ project: null,
11798
+ googleConnectionStore: opts.googleConnectionStore,
11799
+ ga4CredentialStore: opts.ga4CredentialStore,
11800
+ getGoogleAuthConfig: opts.getGoogleAuthConfig,
11801
+ redirectUri,
11802
+ providerSummary: opts.providerSummary
11803
+ };
11804
+ return runChecks(ctx, ALL_CHECKS, { checkIds });
11805
+ });
11806
+ app.get("/projects/:name/doctor", async (request) => {
11807
+ const project = resolveProject(app.db, request.params.name);
11808
+ const checkIds = parseCheckIds(request.query.check);
11809
+ const ctx = {
11810
+ db: app.db,
11811
+ project: {
11812
+ id: project.id,
11813
+ name: project.name,
11814
+ canonicalDomain: project.canonicalDomain,
11815
+ displayName: project.displayName
11816
+ },
11817
+ googleConnectionStore: opts.googleConnectionStore,
11818
+ ga4CredentialStore: opts.ga4CredentialStore,
11819
+ getGoogleAuthConfig: opts.getGoogleAuthConfig,
11820
+ redirectUri,
11821
+ providerSummary: opts.providerSummary
11822
+ };
11823
+ return runChecks(ctx, ALL_CHECKS, { checkIds });
11824
+ });
11825
+ }
11826
+
11196
11827
  // ../api-routes/src/index.ts
11197
11828
  async function apiRoutes(app, opts) {
11198
11829
  app.decorate("db", opts.db);
@@ -11312,6 +11943,13 @@ async function apiRoutes(app, opts) {
11312
11943
  onBacklinksPruneCache: opts.onBacklinksPruneCache,
11313
11944
  listCachedReleases: opts.listCachedReleases
11314
11945
  });
11946
+ await api.register(doctorRoutes, {
11947
+ googleConnectionStore: opts.googleConnectionStore,
11948
+ ga4CredentialStore: opts.ga4CredentialStore,
11949
+ getGoogleAuthConfig: opts.getGoogleAuthConfig,
11950
+ publicUrl: opts.publicUrl,
11951
+ providerSummary: opts.providerSummary
11952
+ });
11315
11953
  if (opts.registerAuthenticatedRoutes) {
11316
11954
  await opts.registerAuthenticatedRoutes(api);
11317
11955
  }