@ainyc/canonry 2.4.2 → 2.4.5

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.
@@ -30,7 +30,7 @@ import {
30
30
  runs,
31
31
  schedules,
32
32
  usageCounters
33
- } from "./chunk-GZF3YIHY.js";
33
+ } from "./chunk-32YTAZBL.js";
34
34
 
35
35
  // src/config.ts
36
36
  import fs from "fs";
@@ -625,6 +625,9 @@ function agentBusy(projectName) {
625
625
  function missingDependency(message, details) {
626
626
  return new AppError("MISSING_DEPENDENCY", message, 422, details);
627
627
  }
628
+ function internalError(message, details) {
629
+ return new AppError("INTERNAL_ERROR", message, 500, details);
630
+ }
628
631
 
629
632
  // ../contracts/src/google.ts
630
633
  import { z as z5 } from "zod";
@@ -1549,7 +1552,7 @@ function parseCookies(header) {
1549
1552
  }, {});
1550
1553
  }
1551
1554
  async function authPlugin(app, opts = {}) {
1552
- app.addHook("onRequest", async (request, reply) => {
1555
+ app.addHook("onRequest", async (request) => {
1553
1556
  const url = request.url.split("?")[0];
1554
1557
  if (shouldSkipAuth(url)) return;
1555
1558
  const header = request.headers.authorization;
@@ -1557,15 +1560,13 @@ async function authPlugin(app, opts = {}) {
1557
1560
  if (header) {
1558
1561
  const parts = header.split(" ");
1559
1562
  if (parts.length !== 2 || parts[0] !== "Bearer") {
1560
- const err = authRequired();
1561
- return reply.status(err.statusCode).send(err.toJSON());
1563
+ throw authRequired();
1562
1564
  }
1563
1565
  const token = parts[1];
1564
1566
  const hash = hashKey(token);
1565
1567
  key = app.db.select().from(apiKeys).where(eq(apiKeys.keyHash, hash)).get();
1566
1568
  if (!key || key.revokedAt) {
1567
- const err = authInvalid();
1568
- return reply.status(err.statusCode).send(err.toJSON());
1569
+ throw authInvalid();
1569
1570
  }
1570
1571
  } else if (opts.resolveSessionApiKeyId && opts.sessionCookieName) {
1571
1572
  const sessionId = parseCookies(request.headers.cookie)[opts.sessionCookieName];
@@ -1576,12 +1577,10 @@ async function authPlugin(app, opts = {}) {
1576
1577
  }
1577
1578
  }
1578
1579
  if (!key || key.revokedAt) {
1579
- const err = authRequired();
1580
- return reply.status(err.statusCode).send(err.toJSON());
1580
+ throw authRequired();
1581
1581
  }
1582
1582
  } else {
1583
- const err = authRequired();
1584
- return reply.status(err.statusCode).send(err.toJSON());
1583
+ throw authRequired();
1585
1584
  }
1586
1585
  app.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq(apiKeys.id, key.id)).run();
1587
1586
  });
@@ -2049,12 +2048,7 @@ async function keywordRoutes(app, opts) {
2049
2048
  return reply.send({ keywords: generated, provider });
2050
2049
  } catch (err) {
2051
2050
  request.log.error({ err }, "Key phrase generation failed");
2052
- return reply.status(500).send({
2053
- error: {
2054
- code: "INTERNAL_ERROR",
2055
- message: err instanceof Error ? err.message : "Failed to generate key phrases"
2056
- }
2057
- });
2051
+ throw internalError(err instanceof Error ? err.message : "Failed to generate key phrases");
2058
2052
  }
2059
2053
  });
2060
2054
  }
@@ -6068,7 +6062,7 @@ async function settingsRoutes(app, opts) {
6068
6062
  google: opts.google ?? { configured: false },
6069
6063
  bing: opts.bing ?? { configured: false }
6070
6064
  }));
6071
- app.put("/settings/providers/:name", async (request, reply) => {
6065
+ app.put("/settings/providers/:name", async (request) => {
6072
6066
  const { apiKey, baseUrl, model, quota } = request.body ?? {};
6073
6067
  const name = request.params.name;
6074
6068
  const adapters = opts.providerAdapters ?? [];
@@ -6076,107 +6070,81 @@ async function settingsRoutes(app, opts) {
6076
6070
  const adapterInfo = apiAdapters.find((a) => a.name === name);
6077
6071
  if (!adapterInfo) {
6078
6072
  const validNames = apiAdapters.map((a) => a.name);
6079
- const err = validationError(`Invalid provider: ${name}. Must be one of: ${validNames.join(", ")}`, {
6073
+ throw validationError(`Invalid provider: ${name}. Must be one of: ${validNames.join(", ")}`, {
6080
6074
  provider: name,
6081
6075
  validProviders: validNames
6082
6076
  });
6083
- return reply.status(err.statusCode).send(err.toJSON());
6084
6077
  }
6085
6078
  if (name === "local") {
6086
6079
  if (!baseUrl || typeof baseUrl !== "string") {
6087
- const err = validationError("baseUrl is required for local provider");
6088
- return reply.status(err.statusCode).send(err.toJSON());
6080
+ throw validationError("baseUrl is required for local provider");
6089
6081
  }
6090
6082
  } else if (name === "gemini" && !apiKey) {
6091
6083
  const geminiSummary = (opts.providerSummary ?? []).find((p) => p.name === "gemini");
6092
6084
  if (!geminiSummary?.vertexConfigured) {
6093
- const err = validationError(
6085
+ throw validationError(
6094
6086
  "apiKey is required for Gemini unless Vertex AI is configured (set GEMINI_VERTEX_PROJECT env var or vertexProject in config file)"
6095
6087
  );
6096
- return reply.status(err.statusCode).send(err.toJSON());
6097
6088
  }
6098
6089
  } else {
6099
6090
  if (!apiKey || typeof apiKey !== "string") {
6100
- const err = validationError("apiKey is required");
6101
- return reply.status(err.statusCode).send(err.toJSON());
6091
+ throw validationError("apiKey is required");
6102
6092
  }
6103
6093
  }
6104
6094
  if (model !== void 0) {
6105
6095
  if (!adapterInfo.modelValidationPattern.test(model)) {
6106
- return reply.status(400).send({
6107
- error: { code: "VALIDATION_ERROR", message: `Invalid model "${model}" for provider "${name}" \u2014 ${adapterInfo.modelValidationHint}` }
6108
- });
6096
+ throw validationError(
6097
+ `Invalid model "${model}" for provider "${name}" \u2014 ${adapterInfo.modelValidationHint}`
6098
+ );
6109
6099
  }
6110
6100
  }
6111
6101
  if (!opts.onProviderUpdate) {
6112
- const err = notImplemented("Provider configuration updates are not supported in this deployment");
6113
- return reply.status(err.statusCode).send(err.toJSON());
6102
+ throw notImplemented("Provider configuration updates are not supported in this deployment");
6114
6103
  }
6115
6104
  if (quota !== void 0) {
6116
6105
  if (typeof quota !== "object" || quota === null) {
6117
- return reply.status(400).send({ error: { code: "VALIDATION_ERROR", message: "quota must be an object" } });
6106
+ throw validationError("quota must be an object");
6118
6107
  }
6119
6108
  for (const [key, val] of Object.entries(quota)) {
6120
6109
  if (!["maxConcurrency", "maxRequestsPerMinute", "maxRequestsPerDay"].includes(key)) {
6121
- return reply.status(400).send({ error: { code: "VALIDATION_ERROR", message: `Unknown quota field: ${key}` } });
6110
+ throw validationError(`Unknown quota field: ${key}`);
6122
6111
  }
6123
6112
  if (typeof val !== "number" || !Number.isInteger(val) || val <= 0) {
6124
- return reply.status(400).send({ error: { code: "VALIDATION_ERROR", message: `${key} must be a positive integer` } });
6113
+ throw validationError(`${key} must be a positive integer`);
6125
6114
  }
6126
6115
  }
6127
6116
  }
6128
6117
  const result = opts.onProviderUpdate(name, apiKey ?? "", model, baseUrl, quota);
6129
6118
  if (!result) {
6130
- return reply.status(500).send({
6131
- error: {
6132
- code: "INTERNAL_ERROR",
6133
- message: "Failed to update provider configuration"
6134
- }
6135
- });
6119
+ throw internalError("Failed to update provider configuration");
6136
6120
  }
6137
6121
  return result;
6138
6122
  });
6139
- app.put("/settings/google", async (request, reply) => {
6123
+ app.put("/settings/google", async (request) => {
6140
6124
  const { clientId, clientSecret } = request.body ?? {};
6141
6125
  if (!clientId || typeof clientId !== "string" || !clientSecret || typeof clientSecret !== "string") {
6142
- return reply.status(400).send({
6143
- error: { code: "VALIDATION_ERROR", message: "clientId and clientSecret are required" }
6144
- });
6126
+ throw validationError("clientId and clientSecret are required");
6145
6127
  }
6146
6128
  if (!opts.onGoogleUpdate) {
6147
- const err = notImplemented("Google OAuth configuration updates are not supported in this deployment");
6148
- return reply.status(err.statusCode).send(err.toJSON());
6129
+ throw notImplemented("Google OAuth configuration updates are not supported in this deployment");
6149
6130
  }
6150
6131
  const result = opts.onGoogleUpdate(clientId, clientSecret);
6151
6132
  if (!result) {
6152
- return reply.status(500).send({
6153
- error: {
6154
- code: "INTERNAL_ERROR",
6155
- message: "Failed to update Google OAuth configuration"
6156
- }
6157
- });
6133
+ throw internalError("Failed to update Google OAuth configuration");
6158
6134
  }
6159
6135
  return result;
6160
6136
  });
6161
- app.put("/settings/bing", async (request, reply) => {
6137
+ app.put("/settings/bing", async (request) => {
6162
6138
  const { apiKey } = request.body ?? {};
6163
6139
  if (!apiKey || typeof apiKey !== "string") {
6164
- return reply.status(400).send({
6165
- error: { code: "VALIDATION_ERROR", message: "apiKey is required" }
6166
- });
6140
+ throw validationError("apiKey is required");
6167
6141
  }
6168
6142
  if (!opts.onBingUpdate) {
6169
- const err = notImplemented("Bing configuration updates are not supported in this deployment");
6170
- return reply.status(err.statusCode).send(err.toJSON());
6143
+ throw notImplemented("Bing configuration updates are not supported in this deployment");
6171
6144
  }
6172
6145
  const result = opts.onBingUpdate(apiKey);
6173
6146
  if (!result) {
6174
- return reply.status(500).send({
6175
- error: {
6176
- code: "INTERNAL_ERROR",
6177
- message: "Failed to update Bing configuration"
6178
- }
6179
- });
6147
+ throw internalError("Failed to update Bing configuration");
6180
6148
  }
6181
6149
  return result;
6182
6150
  });
@@ -6184,32 +6152,24 @@ async function settingsRoutes(app, opts) {
6184
6152
 
6185
6153
  // ../api-routes/src/snapshot.ts
6186
6154
  async function snapshotRoutes(app, opts) {
6187
- app.post("/snapshot", async (request, reply) => {
6155
+ app.post("/snapshot", async (request) => {
6188
6156
  const parsed = snapshotRequestSchema.safeParse(request.body);
6189
6157
  if (!parsed.success) {
6190
- const err = validationError("Invalid snapshot payload", {
6158
+ throw validationError("Invalid snapshot payload", {
6191
6159
  issues: parsed.error.issues.map((issue) => ({
6192
6160
  path: issue.path.join("."),
6193
6161
  message: issue.message
6194
6162
  }))
6195
6163
  });
6196
- return reply.status(err.statusCode).send(err.toJSON());
6197
6164
  }
6198
6165
  if (!opts.onSnapshotRequested) {
6199
- const err = notImplemented("Snapshot reporting is not supported in this deployment");
6200
- return reply.status(err.statusCode).send(err.toJSON());
6166
+ throw notImplemented("Snapshot reporting is not supported in this deployment");
6201
6167
  }
6202
6168
  try {
6203
- const report = await opts.onSnapshotRequested(parsed.data);
6204
- return reply.send(report);
6169
+ return await opts.onSnapshotRequested(parsed.data);
6205
6170
  } catch (err) {
6206
6171
  request.log.error({ err }, "Snapshot report generation failed");
6207
- return reply.status(500).send({
6208
- error: {
6209
- code: "INTERNAL_ERROR",
6210
- message: err instanceof Error ? err.message : "Failed to generate snapshot report"
6211
- }
6212
- });
6172
+ throw internalError(err instanceof Error ? err.message : "Failed to generate snapshot report");
6213
6173
  }
6214
6174
  });
6215
6175
  }
@@ -7401,11 +7361,9 @@ async function googleRoutes(app, opts) {
7401
7361
  function getAuthConfig() {
7402
7362
  return opts.getGoogleAuthConfig?.() ?? {};
7403
7363
  }
7404
- function requireConnectionStore(reply) {
7364
+ function requireConnectionStore() {
7405
7365
  if (opts.googleConnectionStore) return opts.googleConnectionStore;
7406
- const err = validationError("Google auth storage is not configured for this deployment");
7407
- reply.status(err.statusCode).send(err.toJSON());
7408
- return null;
7366
+ throw validationError("Google auth storage is not configured for this deployment");
7409
7367
  }
7410
7368
  app.get("/projects/:name/google/connections", async (request) => {
7411
7369
  const project = resolveProject(app.db, request.params.name);
@@ -7421,16 +7379,14 @@ async function googleRoutes(app, opts) {
7421
7379
  updatedAt: connection.updatedAt
7422
7380
  }));
7423
7381
  });
7424
- app.post("/projects/:name/google/connect", async (request, reply) => {
7382
+ app.post("/projects/:name/google/connect", async (request) => {
7425
7383
  const { clientId: googleClientId, clientSecret: googleClientSecret } = getAuthConfig();
7426
7384
  if (!googleClientId || !googleClientSecret) {
7427
- const err = validationError("Google OAuth is not configured. Set Google OAuth credentials in the local Canonry config.");
7428
- return reply.status(err.statusCode).send(err.toJSON());
7385
+ throw validationError("Google OAuth is not configured. Set Google OAuth credentials in the local Canonry config.");
7429
7386
  }
7430
7387
  const { type, propertyId, publicUrl } = request.body ?? {};
7431
7388
  if (!type || type !== "gsc" && type !== "ga4") {
7432
- const err = validationError('type must be "gsc" or "ga4"');
7433
- return reply.status(err.statusCode).send(err.toJSON());
7389
+ throw validationError('type must be "gsc" or "ga4"');
7434
7390
  }
7435
7391
  const project = resolveProject(app.db, request.params.name);
7436
7392
  let redirectUri;
@@ -7456,8 +7412,7 @@ async function googleRoutes(app, opts) {
7456
7412
  if (!googleClientId || !googleClientSecret) {
7457
7413
  return reply.status(500).send("Google OAuth not configured");
7458
7414
  }
7459
- const store = requireConnectionStore(reply);
7460
- if (!store) return;
7415
+ const store = requireConnectionStore();
7461
7416
  const escapeHtml = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
7462
7417
  const { code, state, error } = request.query;
7463
7418
  if (error) {
@@ -7547,13 +7502,11 @@ async function googleRoutes(app, opts) {
7547
7502
  return handleOAuthCallback(request, reply);
7548
7503
  });
7549
7504
  app.delete("/projects/:name/google/connections/:type", async (request, reply) => {
7550
- const store = requireConnectionStore(reply);
7551
- if (!store) return;
7505
+ const store = requireConnectionStore();
7552
7506
  const project = resolveProject(app.db, request.params.name);
7553
7507
  const deleted = store.deleteConnection(project.canonicalDomain, request.params.type);
7554
7508
  if (!deleted) {
7555
- const err = notFound("Google connection", request.params.type);
7556
- return reply.status(err.statusCode).send(err.toJSON());
7509
+ throw notFound("Google connection", request.params.type);
7557
7510
  }
7558
7511
  writeAuditLog(app.db, {
7559
7512
  projectId: project.id,
@@ -7564,27 +7517,23 @@ async function googleRoutes(app, opts) {
7564
7517
  });
7565
7518
  return reply.status(204).send();
7566
7519
  });
7567
- app.get("/projects/:name/google/properties", async (request, reply) => {
7520
+ app.get("/projects/:name/google/properties", async (request) => {
7568
7521
  const { clientId: googleClientId, clientSecret: googleClientSecret } = getAuthConfig();
7569
7522
  if (!googleClientId || !googleClientSecret) {
7570
- const err = validationError("Google OAuth is not configured");
7571
- return reply.status(err.statusCode).send(err.toJSON());
7523
+ throw validationError("Google OAuth is not configured");
7572
7524
  }
7573
- const store = requireConnectionStore(reply);
7574
- if (!store) return;
7525
+ const store = requireConnectionStore();
7575
7526
  const project = resolveProject(app.db, request.params.name);
7576
7527
  const { accessToken } = await getValidToken(store, project.canonicalDomain, "gsc", googleClientId, googleClientSecret);
7577
7528
  const sites = await listSites(accessToken);
7578
7529
  return { sites };
7579
7530
  });
7580
- app.post("/projects/:name/google/gsc/sync", async (request, reply) => {
7581
- const store = requireConnectionStore(reply);
7582
- if (!store) return;
7531
+ app.post("/projects/:name/google/gsc/sync", async (request) => {
7532
+ const store = requireConnectionStore();
7583
7533
  const project = resolveProject(app.db, request.params.name);
7584
7534
  const conn = store.getConnection(project.canonicalDomain, "gsc");
7585
7535
  if (!conn) {
7586
- const err = validationError('No GSC connection found for this domain. Run "canonry google connect" first.');
7587
- return reply.status(err.statusCode).send(err.toJSON());
7536
+ throw validationError('No GSC connection found for this domain. Run "canonry google connect" first.');
7588
7537
  }
7589
7538
  const now = (/* @__PURE__ */ new Date()).toISOString();
7590
7539
  const runId = crypto14.randomUUID();
@@ -7626,24 +7575,20 @@ async function googleRoutes(app, opts) {
7626
7575
  position: parseFloat(r.position)
7627
7576
  }));
7628
7577
  });
7629
- app.post("/projects/:name/google/gsc/inspect", async (request, reply) => {
7578
+ app.post("/projects/:name/google/gsc/inspect", async (request) => {
7630
7579
  const { clientId: googleClientId, clientSecret: googleClientSecret } = getAuthConfig();
7631
7580
  if (!googleClientId || !googleClientSecret) {
7632
- const err = validationError("Google OAuth is not configured");
7633
- return reply.status(err.statusCode).send(err.toJSON());
7581
+ throw validationError("Google OAuth is not configured");
7634
7582
  }
7635
- const store = requireConnectionStore(reply);
7636
- if (!store) return;
7583
+ const store = requireConnectionStore();
7637
7584
  const project = resolveProject(app.db, request.params.name);
7638
7585
  const { url } = request.body ?? {};
7639
7586
  if (!url) {
7640
- const err = validationError("url is required");
7641
- return reply.status(err.statusCode).send(err.toJSON());
7587
+ throw validationError("url is required");
7642
7588
  }
7643
7589
  const { accessToken, propertyId } = await getValidToken(store, project.canonicalDomain, "gsc", googleClientId, googleClientSecret);
7644
7590
  if (!propertyId) {
7645
- const err = validationError("No GSC property configured for this connection");
7646
- return reply.status(err.statusCode).send(err.toJSON());
7591
+ throw validationError("No GSC property configured for this connection");
7647
7592
  }
7648
7593
  const result = await inspectUrl(accessToken, url, propertyId);
7649
7594
  const ir = result.inspectionResult;
@@ -7843,46 +7788,38 @@ async function googleRoutes(app, opts) {
7843
7788
  reasonBreakdown: JSON.parse(r.reasonBreakdown)
7844
7789
  })).reverse();
7845
7790
  });
7846
- app.get("/projects/:name/google/gsc/sitemaps", async (request, reply) => {
7791
+ app.get("/projects/:name/google/gsc/sitemaps", async (request) => {
7847
7792
  const { clientId: googleClientId, clientSecret: googleClientSecret } = getAuthConfig();
7848
7793
  if (!googleClientId || !googleClientSecret) {
7849
- const err = validationError("Google OAuth is not configured");
7850
- return reply.status(err.statusCode).send(err.toJSON());
7794
+ throw validationError("Google OAuth is not configured");
7851
7795
  }
7852
- const store = requireConnectionStore(reply);
7853
- if (!store) return;
7796
+ const store = requireConnectionStore();
7854
7797
  const project = resolveProject(app.db, request.params.name);
7855
7798
  const { accessToken, propertyId } = await getValidToken(store, project.canonicalDomain, "gsc", googleClientId, googleClientSecret);
7856
7799
  if (!propertyId) {
7857
- const err = validationError('No GSC property configured for this connection. Set one with "canonry google set-property".');
7858
- return reply.status(err.statusCode).send(err.toJSON());
7800
+ throw validationError('No GSC property configured for this connection. Set one with "canonry google set-property".');
7859
7801
  }
7860
7802
  const sitemaps = await listSitemaps(accessToken, propertyId);
7861
7803
  return { sitemaps };
7862
7804
  });
7863
- app.post("/projects/:name/google/gsc/discover-sitemaps", async (request, reply) => {
7805
+ app.post("/projects/:name/google/gsc/discover-sitemaps", async (request) => {
7864
7806
  const { clientId: googleClientId, clientSecret: googleClientSecret } = getAuthConfig();
7865
7807
  if (!googleClientId || !googleClientSecret) {
7866
- const err = validationError("Google OAuth is not configured");
7867
- return reply.status(err.statusCode).send(err.toJSON());
7808
+ throw validationError("Google OAuth is not configured");
7868
7809
  }
7869
- const store = requireConnectionStore(reply);
7870
- if (!store) return;
7810
+ const store = requireConnectionStore();
7871
7811
  const project = resolveProject(app.db, request.params.name);
7872
7812
  const conn = store.getConnection(project.canonicalDomain, "gsc");
7873
7813
  if (!conn) {
7874
- const err = validationError('No GSC connection found for this domain. Run "canonry google connect" first.');
7875
- return reply.status(err.statusCode).send(err.toJSON());
7814
+ throw validationError('No GSC connection found for this domain. Run "canonry google connect" first.');
7876
7815
  }
7877
7816
  if (!conn.propertyId) {
7878
- const err = validationError("No GSC property configured for this connection");
7879
- return reply.status(err.statusCode).send(err.toJSON());
7817
+ throw validationError("No GSC property configured for this connection");
7880
7818
  }
7881
7819
  const { accessToken } = await getValidToken(store, project.canonicalDomain, "gsc", googleClientId, googleClientSecret);
7882
7820
  const sitemaps = await listSitemaps(accessToken, conn.propertyId);
7883
7821
  if (sitemaps.length === 0) {
7884
- const err = validationError("No sitemaps found for this GSC property. Submit a sitemap in Google Search Console first.");
7885
- return reply.status(err.statusCode).send(err.toJSON());
7822
+ throw validationError("No sitemaps found for this GSC property. Submit a sitemap in Google Search Console first.");
7886
7823
  }
7887
7824
  const primary = sitemaps.find((s) => !s.isSitemapsIndex) ?? sitemaps[0];
7888
7825
  const sitemapUrl = primary.path;
@@ -7906,18 +7843,15 @@ async function googleRoutes(app, opts) {
7906
7843
  const run = app.db.select().from(runs).where(eq14(runs.id, runId)).get();
7907
7844
  return { sitemaps, primarySitemapUrl: sitemapUrl, run };
7908
7845
  });
7909
- app.post("/projects/:name/google/gsc/inspect-sitemap", async (request, reply) => {
7910
- const store = requireConnectionStore(reply);
7911
- if (!store) return;
7846
+ app.post("/projects/:name/google/gsc/inspect-sitemap", async (request) => {
7847
+ const store = requireConnectionStore();
7912
7848
  const project = resolveProject(app.db, request.params.name);
7913
7849
  const conn = store.getConnection(project.canonicalDomain, "gsc");
7914
7850
  if (!conn) {
7915
- const err = validationError('No GSC connection found for this domain. Run "canonry google connect" first.');
7916
- return reply.status(err.statusCode).send(err.toJSON());
7851
+ throw validationError('No GSC connection found for this domain. Run "canonry google connect" first.');
7917
7852
  }
7918
7853
  if (!conn.propertyId) {
7919
- const err = validationError("No GSC property configured for this connection");
7920
- return reply.status(err.statusCode).send(err.toJSON());
7854
+ throw validationError("No GSC property configured for this connection");
7921
7855
  }
7922
7856
  const now = (/* @__PURE__ */ new Date()).toISOString();
7923
7857
  const runId = crypto14.randomUUID();
@@ -7936,14 +7870,12 @@ async function googleRoutes(app, opts) {
7936
7870
  const run = app.db.select().from(runs).where(eq14(runs.id, runId)).get();
7937
7871
  return run;
7938
7872
  });
7939
- app.put("/projects/:name/google/connections/:type/sitemap", async (request, reply) => {
7940
- const store = requireConnectionStore(reply);
7941
- if (!store) return;
7873
+ app.put("/projects/:name/google/connections/:type/sitemap", async (request) => {
7874
+ const store = requireConnectionStore();
7942
7875
  const project = resolveProject(app.db, request.params.name);
7943
7876
  const { sitemapUrl } = request.body ?? {};
7944
7877
  if (!sitemapUrl || !sitemapUrl.trim()) {
7945
- const err = validationError("sitemapUrl is required");
7946
- return reply.status(err.statusCode).send(err.toJSON());
7878
+ throw validationError("sitemapUrl is required");
7947
7879
  }
7948
7880
  const conn = store.updateConnection(
7949
7881
  project.canonicalDomain,
@@ -7951,19 +7883,16 @@ async function googleRoutes(app, opts) {
7951
7883
  { sitemapUrl: sitemapUrl.trim(), updatedAt: (/* @__PURE__ */ new Date()).toISOString() }
7952
7884
  );
7953
7885
  if (!conn) {
7954
- const err = notFound("Google connection", request.params.type);
7955
- return reply.status(err.statusCode).send(err.toJSON());
7886
+ throw notFound("Google connection", request.params.type);
7956
7887
  }
7957
7888
  return { sitemapUrl: sitemapUrl.trim() };
7958
7889
  });
7959
- app.put("/projects/:name/google/connections/:type/property", async (request, reply) => {
7960
- const store = requireConnectionStore(reply);
7961
- if (!store) return;
7890
+ app.put("/projects/:name/google/connections/:type/property", async (request) => {
7891
+ const store = requireConnectionStore();
7962
7892
  const project = resolveProject(app.db, request.params.name);
7963
7893
  const { propertyId } = request.body ?? {};
7964
7894
  if (!propertyId) {
7965
- const err = validationError("propertyId is required");
7966
- return reply.status(err.statusCode).send(err.toJSON());
7895
+ throw validationError("propertyId is required");
7967
7896
  }
7968
7897
  const conn = store.updateConnection(
7969
7898
  project.canonicalDomain,
@@ -7971,19 +7900,16 @@ async function googleRoutes(app, opts) {
7971
7900
  { propertyId, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }
7972
7901
  );
7973
7902
  if (!conn) {
7974
- const err = notFound("Google connection", request.params.type);
7975
- return reply.status(err.statusCode).send(err.toJSON());
7903
+ throw notFound("Google connection", request.params.type);
7976
7904
  }
7977
7905
  return { propertyId };
7978
7906
  });
7979
- app.post("/projects/:name/google/indexing/request", async (request, reply) => {
7907
+ app.post("/projects/:name/google/indexing/request", async (request) => {
7980
7908
  const { clientId: googleClientId, clientSecret: googleClientSecret } = getAuthConfig();
7981
7909
  if (!googleClientId || !googleClientSecret) {
7982
- const err = validationError("Google OAuth is not configured");
7983
- return reply.status(err.statusCode).send(err.toJSON());
7910
+ throw validationError("Google OAuth is not configured");
7984
7911
  }
7985
- const store = requireConnectionStore(reply);
7986
- if (!store) return;
7912
+ const store = requireConnectionStore();
7987
7913
  const project = resolveProject(app.db, request.params.name);
7988
7914
  const { accessToken } = await getValidToken(store, project.canonicalDomain, "gsc", googleClientId, googleClientSecret);
7989
7915
  let urlsToNotify = request.body?.urls ?? [];
@@ -8002,18 +7928,15 @@ async function googleRoutes(app, opts) {
8002
7928
  }
8003
7929
  }
8004
7930
  if (unindexedUrls.length === 0) {
8005
- const err = validationError('No unindexed URLs found. Run "canonry google inspect-sitemap" first.');
8006
- return reply.status(err.statusCode).send(err.toJSON());
7931
+ throw validationError('No unindexed URLs found. Run "canonry google inspect-sitemap" first.');
8007
7932
  }
8008
7933
  urlsToNotify = unindexedUrls;
8009
7934
  }
8010
7935
  if (urlsToNotify.length === 0) {
8011
- const err = validationError("At least one URL is required (or use allUnindexed: true)");
8012
- return reply.status(err.statusCode).send(err.toJSON());
7936
+ throw validationError("At least one URL is required (or use allUnindexed: true)");
8013
7937
  }
8014
7938
  if (urlsToNotify.length > INDEXING_API_DAILY_LIMIT) {
8015
- const err = validationError(`Cannot request indexing for more than ${INDEXING_API_DAILY_LIMIT} URLs per request (got ${urlsToNotify.length})`);
8016
- return reply.status(err.statusCode).send(err.toJSON());
7939
+ throw validationError(`Cannot request indexing for more than ${INDEXING_API_DAILY_LIMIT} URLs per request (got ${urlsToNotify.length})`);
8017
7940
  }
8018
7941
  const projectDomain = normalizeProjectDomain(project.canonicalDomain);
8019
7942
  const invalidUrls = urlsToNotify.filter((url) => {
@@ -8025,10 +7948,9 @@ async function googleRoutes(app, opts) {
8025
7948
  }
8026
7949
  });
8027
7950
  if (invalidUrls.length > 0) {
8028
- const err = validationError(
7951
+ throw validationError(
8029
7952
  `URLs must belong to project domain "${project.canonicalDomain}". Invalid: ${invalidUrls.slice(0, 5).join(", ")}`
8030
7953
  );
8031
- return reply.status(err.statusCode).send(err.toJSON());
8032
7954
  }
8033
7955
  const results = [];
8034
7956
  for (const url of urlsToNotify) {
@@ -8226,6 +8148,13 @@ async function getKeywordStats(apiKey, siteUrl) {
8226
8148
  const data = await bingFetch(apiKey, `GetQueryStats?siteUrl=${encodedSite}`);
8227
8149
  return data ?? [];
8228
8150
  }
8151
+ async function getCrawlIssues(apiKey, siteUrl) {
8152
+ validateApiKey(apiKey);
8153
+ validateSiteUrl2(siteUrl);
8154
+ const encodedSite = encodeURIComponent(siteUrl);
8155
+ const data = await bingFetch(apiKey, `GetCrawlIssues?siteUrl=${encodedSite}`);
8156
+ return data ?? [];
8157
+ }
8229
8158
 
8230
8159
  // ../api-routes/src/bing.ts
8231
8160
  function parseBingDate(value) {
@@ -8241,29 +8170,47 @@ function bingLog(level, action, ctx) {
8241
8170
  const stream = level === "error" ? process.stderr : process.stdout;
8242
8171
  stream.write(JSON.stringify(entry) + "\n");
8243
8172
  }
8173
+ var CRAWL_ISSUES_CACHE_TTL_MS = 6e4;
8174
+ var crawlIssuesCache = /* @__PURE__ */ new Map();
8175
+ function isBlockingIssueType(issueType) {
8176
+ if (!issueType) return true;
8177
+ const trimmed = issueType.trim();
8178
+ if (!trimmed) return true;
8179
+ return trimmed.split(/\s+/).some((flag) => !/^(None|Seo(Issues|Concerns))$/i.test(flag));
8180
+ }
8181
+ async function loadBlockingCrawlIssues(apiKey, siteUrl, domain) {
8182
+ const now = Date.now();
8183
+ const cached = crawlIssuesCache.get(domain);
8184
+ if (cached && now - cached.fetchedAt < CRAWL_ISSUES_CACHE_TTL_MS) {
8185
+ return cached.blockedUrls;
8186
+ }
8187
+ const issues = await getCrawlIssues(apiKey, siteUrl);
8188
+ const blockedUrls = /* @__PURE__ */ new Set();
8189
+ for (const issue of issues) {
8190
+ if (issue.Url && isBlockingIssueType(issue.IssueType ?? null)) {
8191
+ blockedUrls.add(issue.Url);
8192
+ }
8193
+ }
8194
+ crawlIssuesCache.set(domain, { blockedUrls, fetchedAt: now });
8195
+ return blockedUrls;
8196
+ }
8244
8197
  async function bingRoutes(app, opts) {
8245
- function requireConnectionStore(reply) {
8198
+ function requireConnectionStore() {
8246
8199
  if (opts.bingConnectionStore) return opts.bingConnectionStore;
8247
- const err = validationError("Bing connection storage is not configured for this deployment");
8248
- reply.status(err.statusCode).send(err.toJSON());
8249
- return null;
8200
+ throw validationError("Bing connection storage is not configured for this deployment");
8250
8201
  }
8251
- function requireConnection(store, domain, reply) {
8202
+ function requireConnection(store, domain) {
8252
8203
  const conn = store.getConnection(domain);
8253
8204
  if (!conn) {
8254
- const err = validationError('No Bing connection found for this domain. Run "canonry bing connect <project>" first.');
8255
- reply.status(err.statusCode).send(err.toJSON());
8256
- return null;
8205
+ throw validationError('No Bing connection found for this domain. Run "canonry bing connect <project>" first.');
8257
8206
  }
8258
8207
  return conn;
8259
8208
  }
8260
- app.post("/projects/:name/bing/connect", async (request, reply) => {
8261
- const store = requireConnectionStore(reply);
8262
- if (!store) return;
8209
+ app.post("/projects/:name/bing/connect", async (request) => {
8210
+ const store = requireConnectionStore();
8263
8211
  const { apiKey } = request.body ?? {};
8264
8212
  if (!apiKey || typeof apiKey !== "string") {
8265
- const err = validationError("apiKey is required");
8266
- return reply.status(err.statusCode).send(err.toJSON());
8213
+ throw validationError("apiKey is required");
8267
8214
  }
8268
8215
  const project = resolveProject(app.db, request.params.name);
8269
8216
  let sites;
@@ -8273,8 +8220,7 @@ async function bingRoutes(app, opts) {
8273
8220
  } catch (e) {
8274
8221
  const msg = e instanceof Error ? e.message : String(e);
8275
8222
  bingLog("error", "connect.verify-key-failed", { domain: project.canonicalDomain, error: msg });
8276
- const err = validationError(`Failed to verify Bing API key: ${msg}`);
8277
- return reply.status(err.statusCode).send(err.toJSON());
8223
+ throw validationError(`Failed to verify Bing API key: ${msg}`);
8278
8224
  }
8279
8225
  const now = (/* @__PURE__ */ new Date()).toISOString();
8280
8226
  const existing = store.getConnection(project.canonicalDomain);
@@ -8300,13 +8246,11 @@ async function bingRoutes(app, opts) {
8300
8246
  };
8301
8247
  });
8302
8248
  app.delete("/projects/:name/bing/disconnect", async (request, reply) => {
8303
- const store = requireConnectionStore(reply);
8304
- if (!store) return;
8249
+ const store = requireConnectionStore();
8305
8250
  const project = resolveProject(app.db, request.params.name);
8306
8251
  const deleted = store.deleteConnection(project.canonicalDomain);
8307
8252
  if (!deleted) {
8308
- const err = notFound("Bing connection", project.canonicalDomain);
8309
- return reply.status(err.statusCode).send(err.toJSON());
8253
+ throw notFound("Bing connection", project.canonicalDomain);
8310
8254
  }
8311
8255
  writeAuditLog(app.db, {
8312
8256
  projectId: project.id,
@@ -8317,9 +8261,8 @@ async function bingRoutes(app, opts) {
8317
8261
  });
8318
8262
  return reply.status(204).send();
8319
8263
  });
8320
- app.get("/projects/:name/bing/status", async (request, reply) => {
8321
- const store = requireConnectionStore(reply);
8322
- if (!store) return;
8264
+ app.get("/projects/:name/bing/status", async (request) => {
8265
+ const store = requireConnectionStore();
8323
8266
  const project = resolveProject(app.db, request.params.name);
8324
8267
  const conn = store.getConnection(project.canonicalDomain);
8325
8268
  return {
@@ -8330,25 +8273,20 @@ async function bingRoutes(app, opts) {
8330
8273
  updatedAt: conn?.updatedAt ?? null
8331
8274
  };
8332
8275
  });
8333
- app.get("/projects/:name/bing/sites", async (request, reply) => {
8334
- const store = requireConnectionStore(reply);
8335
- if (!store) return;
8276
+ app.get("/projects/:name/bing/sites", async (request) => {
8277
+ const store = requireConnectionStore();
8336
8278
  const project = resolveProject(app.db, request.params.name);
8337
- const conn = requireConnection(store, project.canonicalDomain, reply);
8338
- if (!conn) return;
8279
+ const conn = requireConnection(store, project.canonicalDomain);
8339
8280
  const sites = await getSites(conn.apiKey);
8340
8281
  return { sites: sites.map((s) => ({ url: s.Url, verified: s.Verified ?? false })) };
8341
8282
  });
8342
- app.post("/projects/:name/bing/set-site", async (request, reply) => {
8343
- const store = requireConnectionStore(reply);
8344
- if (!store) return;
8283
+ app.post("/projects/:name/bing/set-site", async (request) => {
8284
+ const store = requireConnectionStore();
8345
8285
  const project = resolveProject(app.db, request.params.name);
8346
- const conn = requireConnection(store, project.canonicalDomain, reply);
8347
- if (!conn) return;
8286
+ requireConnection(store, project.canonicalDomain);
8348
8287
  const { siteUrl } = request.body ?? {};
8349
8288
  if (!siteUrl || typeof siteUrl !== "string") {
8350
- const err = validationError("siteUrl is required");
8351
- return reply.status(err.statusCode).send(err.toJSON());
8289
+ throw validationError("siteUrl is required");
8352
8290
  }
8353
8291
  store.updateConnection(project.canonicalDomain, {
8354
8292
  siteUrl,
@@ -8356,12 +8294,10 @@ async function bingRoutes(app, opts) {
8356
8294
  });
8357
8295
  return { siteUrl };
8358
8296
  });
8359
- app.get("/projects/:name/bing/coverage", async (request, reply) => {
8360
- const store = requireConnectionStore(reply);
8361
- if (!store) return;
8297
+ app.get("/projects/:name/bing/coverage", async (request) => {
8298
+ const store = requireConnectionStore();
8362
8299
  const project = resolveProject(app.db, request.params.name);
8363
- const conn = requireConnection(store, project.canonicalDomain, reply);
8364
- if (!conn) return;
8300
+ requireConnection(store, project.canonicalDomain);
8365
8301
  const allInspections = app.db.select().from(bingUrlInspections).where(eq15(bingUrlInspections.projectId, project.id)).orderBy(desc6(bingUrlInspections.inspectedAt)).all();
8366
8302
  const latestByUrl = /* @__PURE__ */ new Map();
8367
8303
  const definitiveByUrl = /* @__PURE__ */ new Map();
@@ -8446,9 +8382,8 @@ async function bingRoutes(app, opts) {
8446
8382
  unknown: unknownUrls.map(formatRow)
8447
8383
  };
8448
8384
  });
8449
- app.get("/projects/:name/bing/coverage/history", async (request, reply) => {
8450
- const store = requireConnectionStore(reply);
8451
- if (!store) return;
8385
+ app.get("/projects/:name/bing/coverage/history", async (request) => {
8386
+ requireConnectionStore();
8452
8387
  const project = resolveProject(app.db, request.params.name);
8453
8388
  const parsed = parseInt(request.query.limit ?? "90", 10);
8454
8389
  const limit = Number.isNaN(parsed) || parsed <= 0 ? 90 : parsed;
@@ -8460,9 +8395,8 @@ async function bingRoutes(app, opts) {
8460
8395
  unknown: r.unknown
8461
8396
  }));
8462
8397
  });
8463
- app.get("/projects/:name/bing/inspections", async (request, reply) => {
8464
- const store = requireConnectionStore(reply);
8465
- if (!store) return;
8398
+ app.get("/projects/:name/bing/inspections", async (request) => {
8399
+ requireConnectionStore();
8466
8400
  const project = resolveProject(app.db, request.params.name);
8467
8401
  const { url, limit } = request.query;
8468
8402
  const whereClause = url ? and4(eq15(bingUrlInspections.projectId, project.id), eq15(bingUrlInspections.url, url)) : eq15(bingUrlInspections.projectId, project.id);
@@ -8480,20 +8414,16 @@ async function bingRoutes(app, opts) {
8480
8414
  discoveryDate: r.discoveryDate ?? null
8481
8415
  }));
8482
8416
  });
8483
- app.post("/projects/:name/bing/inspect-url", async (request, reply) => {
8484
- const store = requireConnectionStore(reply);
8485
- if (!store) return;
8417
+ app.post("/projects/:name/bing/inspect-url", async (request) => {
8418
+ const store = requireConnectionStore();
8486
8419
  const project = resolveProject(app.db, request.params.name);
8487
- const conn = requireConnection(store, project.canonicalDomain, reply);
8488
- if (!conn) return;
8420
+ const conn = requireConnection(store, project.canonicalDomain);
8489
8421
  if (!conn.siteUrl) {
8490
- const err = validationError('No Bing site configured. Run "canonry bing set-site <project> <url>" first.');
8491
- return reply.status(err.statusCode).send(err.toJSON());
8422
+ throw validationError('No Bing site configured. Run "canonry bing set-site <project> <url>" first.');
8492
8423
  }
8493
8424
  const { url } = request.body ?? {};
8494
8425
  if (!url) {
8495
- const err = validationError("url is required");
8496
- return reply.status(err.statusCode).send(err.toJSON());
8426
+ throw validationError("url is required");
8497
8427
  }
8498
8428
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
8499
8429
  const runId = crypto15.randomUUID();
@@ -8512,22 +8442,38 @@ async function bingRoutes(app, opts) {
8512
8442
  domain: project.canonicalDomain,
8513
8443
  url,
8514
8444
  httpStatus: result.HttpStatus ?? result.HttpCode ?? null,
8515
- inIndex: result.InIndex ?? null,
8516
8445
  documentSize: result.DocumentSize ?? null,
8517
- lastCrawledDate: result.LastCrawledDate ?? null
8446
+ lastCrawledDate: result.LastCrawledDate ?? null,
8447
+ discoveryDate: result.DiscoveryDate ?? null
8518
8448
  });
8519
8449
  const now = (/* @__PURE__ */ new Date()).toISOString();
8520
8450
  const id = crypto15.randomUUID();
8521
8451
  const httpCode = result.HttpStatus ?? result.HttpCode ?? null;
8522
- let derivedInIndex = null;
8523
- if (result.InIndex != null) {
8524
- derivedInIndex = result.InIndex;
8525
- } else if (result.DocumentSize != null && result.DocumentSize > 0) {
8526
- derivedInIndex = true;
8527
- }
8528
8452
  const lastCrawledDate = parseBingDate(result.LastCrawledDate);
8529
8453
  const inIndexDate = parseBingDate(result.InIndexDate);
8530
8454
  const discoveryDate = parseBingDate(result.DiscoveryDate);
8455
+ let derivedInIndex = null;
8456
+ if (result.DocumentSize != null && result.DocumentSize > 0) {
8457
+ derivedInIndex = true;
8458
+ } else if (lastCrawledDate != null) {
8459
+ const httpStatus = result.HttpStatus ?? result.HttpCode;
8460
+ derivedInIndex = httpStatus != null && httpStatus >= 400 ? false : true;
8461
+ } else if (discoveryDate != null) {
8462
+ derivedInIndex = false;
8463
+ }
8464
+ if (derivedInIndex === true) {
8465
+ try {
8466
+ const blockedUrls = await loadBlockingCrawlIssues(conn.apiKey, conn.siteUrl, project.canonicalDomain);
8467
+ if (blockedUrls.has(url)) {
8468
+ derivedInIndex = false;
8469
+ }
8470
+ } catch (e) {
8471
+ bingLog("warn", "inspect-url.crawl-issues-lookup-failed", {
8472
+ domain: project.canonicalDomain,
8473
+ error: e instanceof Error ? e.message : String(e)
8474
+ });
8475
+ }
8476
+ }
8531
8477
  app.db.insert(bingUrlInspections).values({
8532
8478
  id,
8533
8479
  projectId: project.id,
@@ -8563,15 +8509,12 @@ async function bingRoutes(app, opts) {
8563
8509
  throw e;
8564
8510
  }
8565
8511
  });
8566
- app.post("/projects/:name/bing/request-indexing", async (request, reply) => {
8567
- const store = requireConnectionStore(reply);
8568
- if (!store) return;
8512
+ app.post("/projects/:name/bing/request-indexing", async (request) => {
8513
+ const store = requireConnectionStore();
8569
8514
  const project = resolveProject(app.db, request.params.name);
8570
- const conn = requireConnection(store, project.canonicalDomain, reply);
8571
- if (!conn) return;
8515
+ const conn = requireConnection(store, project.canonicalDomain);
8572
8516
  if (!conn.siteUrl) {
8573
- const err = validationError('No Bing site configured. Run "canonry bing set-site <project> <url>" first.');
8574
- return reply.status(err.statusCode).send(err.toJSON());
8517
+ throw validationError('No Bing site configured. Run "canonry bing set-site <project> <url>" first.');
8575
8518
  }
8576
8519
  let urlsToSubmit = request.body?.urls ?? [];
8577
8520
  if (request.body?.allUnindexed) {
@@ -8589,18 +8532,15 @@ async function bingRoutes(app, opts) {
8589
8532
  }
8590
8533
  }
8591
8534
  if (unindexedUrls.length === 0) {
8592
- const err = validationError('No unindexed or unknown URLs found. Run "canonry bing inspect <project> <url>" first.');
8593
- return reply.status(err.statusCode).send(err.toJSON());
8535
+ throw validationError('No unindexed or unknown URLs found. Run "canonry bing inspect <project> <url>" first.');
8594
8536
  }
8595
8537
  urlsToSubmit = unindexedUrls;
8596
8538
  }
8597
8539
  if (urlsToSubmit.length === 0) {
8598
- const err = validationError("At least one URL is required (or use allUnindexed: true)");
8599
- return reply.status(err.statusCode).send(err.toJSON());
8540
+ throw validationError("At least one URL is required (or use allUnindexed: true)");
8600
8541
  }
8601
8542
  if (urlsToSubmit.length > BING_SUBMIT_URL_DAILY_LIMIT) {
8602
- const err = validationError(`Cannot submit more than ${BING_SUBMIT_URL_DAILY_LIMIT} URLs per day (got ${urlsToSubmit.length})`);
8603
- return reply.status(err.statusCode).send(err.toJSON());
8543
+ throw validationError(`Cannot submit more than ${BING_SUBMIT_URL_DAILY_LIMIT} URLs per day (got ${urlsToSubmit.length})`);
8604
8544
  }
8605
8545
  const results = [];
8606
8546
  bingLog("info", "index-submit.start", { domain: project.canonicalDomain, siteUrl: conn.siteUrl, urlCount: urlsToSubmit.length, allUnindexed: !!request.body?.allUnindexed });
@@ -8643,15 +8583,12 @@ async function bingRoutes(app, opts) {
8643
8583
  results
8644
8584
  };
8645
8585
  });
8646
- app.get("/projects/:name/bing/performance", async (request, reply) => {
8647
- const store = requireConnectionStore(reply);
8648
- if (!store) return;
8586
+ app.get("/projects/:name/bing/performance", async (request) => {
8587
+ const store = requireConnectionStore();
8649
8588
  const project = resolveProject(app.db, request.params.name);
8650
- const conn = requireConnection(store, project.canonicalDomain, reply);
8651
- if (!conn) return;
8589
+ const conn = requireConnection(store, project.canonicalDomain);
8652
8590
  if (!conn.siteUrl) {
8653
- const err = validationError('No Bing site configured. Run "canonry bing set-site <project> <url>" first.');
8654
- return reply.status(err.statusCode).send(err.toJSON());
8591
+ throw validationError('No Bing site configured. Run "canonry bing set-site <project> <url>" first.');
8655
8592
  }
8656
8593
  const stats = await getKeywordStats(conn.apiKey, conn.siteUrl);
8657
8594
  return stats.map((s) => ({
@@ -10558,68 +10495,51 @@ function parseEnvInput(value, fieldName = "env") {
10558
10495
  }
10559
10496
  return env;
10560
10497
  }
10561
- function sendWordpressError(reply, error) {
10562
- if (!(error instanceof WordpressApiError)) return false;
10563
- let appError;
10498
+ function toAppError(error) {
10564
10499
  switch (error.code) {
10565
10500
  case "AUTH_INVALID":
10566
- appError = new AppError("AUTH_INVALID", error.message, error.statusCode);
10567
- break;
10501
+ return new AppError("AUTH_INVALID", error.message, error.statusCode);
10568
10502
  case "NOT_FOUND":
10569
- appError = new AppError("NOT_FOUND", error.message, error.statusCode);
10570
- break;
10571
- case "UPSTREAM_ERROR":
10572
- appError = providerError(error.message, { statusCode: error.statusCode });
10573
- break;
10503
+ return new AppError("NOT_FOUND", error.message, error.statusCode);
10574
10504
  case "UNSUPPORTED":
10575
10505
  case "VALIDATION_ERROR":
10576
- appError = validationError(error.message);
10577
- break;
10506
+ return validationError(error.message);
10507
+ case "UPSTREAM_ERROR":
10578
10508
  default:
10579
- appError = providerError(error.message, { statusCode: error.statusCode });
10580
- break;
10509
+ return providerError(error.message, { statusCode: error.statusCode });
10581
10510
  }
10582
- reply.status(appError.statusCode).send(appError.toJSON());
10583
- return true;
10584
10511
  }
10585
- async function withWordpressErrorHandling(reply, handler) {
10512
+ async function withWordpressErrorHandling(handler) {
10586
10513
  try {
10587
10514
  return await handler();
10588
10515
  } catch (error) {
10589
- if (sendWordpressError(reply, error)) return;
10516
+ if (error instanceof WordpressApiError) throw toAppError(error);
10590
10517
  throw error;
10591
10518
  }
10592
10519
  }
10593
10520
  async function wordpressRoutes(app, opts) {
10594
- function requireStore(reply) {
10521
+ function requireStore() {
10595
10522
  if (opts.wordpressConnectionStore) return opts.wordpressConnectionStore;
10596
- const err = validationError("WordPress connection storage is not configured for this deployment");
10597
- reply.status(err.statusCode).send(err.toJSON());
10598
- return null;
10523
+ throw validationError("WordPress connection storage is not configured for this deployment");
10599
10524
  }
10600
- function requireConnection(store, projectName, reply) {
10525
+ function requireConnection(store, projectName) {
10601
10526
  const connection = store.getConnection(projectName);
10602
10527
  if (!connection) {
10603
- const err = validationError(`No WordPress connection found for project "${projectName}". Run "canonry wordpress connect ${projectName}" first.`);
10604
- reply.status(err.statusCode).send(err.toJSON());
10605
- return null;
10528
+ throw validationError(`No WordPress connection found for project "${projectName}". Run "canonry wordpress connect ${projectName}" first.`);
10606
10529
  }
10607
10530
  return connection;
10608
10531
  }
10609
- app.post("/projects/:name/wordpress/connect", async (request, reply) => {
10610
- return withWordpressErrorHandling(reply, async () => {
10611
- const store = requireStore(reply);
10612
- if (!store) return;
10532
+ app.post("/projects/:name/wordpress/connect", async (request) => {
10533
+ return withWordpressErrorHandling(async () => {
10534
+ const store = requireStore();
10613
10535
  const project = resolveProject(app.db, request.params.name);
10614
10536
  const { url, stagingUrl, username, appPassword } = request.body ?? {};
10615
10537
  if (!url || !username || !appPassword) {
10616
- const err = validationError("url, username, and appPassword are required");
10617
- return reply.status(err.statusCode).send(err.toJSON());
10538
+ throw validationError("url, username, and appPassword are required");
10618
10539
  }
10619
10540
  const defaultEnv = parseEnvInput(request.body?.defaultEnv, "defaultEnv") ?? (stagingUrl ? "staging" : "live");
10620
10541
  if (defaultEnv === "staging" && !stagingUrl) {
10621
- const err = validationError('defaultEnv "staging" requires stagingUrl');
10622
- return reply.status(err.statusCode).send(err.toJSON());
10542
+ throw validationError('defaultEnv "staging" requires stagingUrl');
10623
10543
  }
10624
10544
  const now = (/* @__PURE__ */ new Date()).toISOString();
10625
10545
  const existing = store.getConnection(project.name);
@@ -10655,13 +10575,11 @@ async function wordpressRoutes(app, opts) {
10655
10575
  });
10656
10576
  });
10657
10577
  app.delete("/projects/:name/wordpress/disconnect", async (request, reply) => {
10658
- const store = requireStore(reply);
10659
- if (!store) return;
10578
+ const store = requireStore();
10660
10579
  const project = resolveProject(app.db, request.params.name);
10661
10580
  const deleted = store.deleteConnection(project.name);
10662
10581
  if (!deleted) {
10663
- const err = notFound("WordPress connection", project.name);
10664
- return reply.status(err.statusCode).send(err.toJSON());
10582
+ throw notFound("WordPress connection", project.name);
10665
10583
  }
10666
10584
  writeAuditLog(app.db, {
10667
10585
  projectId: project.id,
@@ -10696,13 +10614,11 @@ async function wordpressRoutes(app, opts) {
10696
10614
  adminUrl: getWpStagingAdminUrl(connection.url)
10697
10615
  };
10698
10616
  });
10699
- app.get("/projects/:name/wordpress/pages", async (request, reply) => {
10700
- return withWordpressErrorHandling(reply, async () => {
10701
- const store = requireStore(reply);
10702
- if (!store) return;
10617
+ app.get("/projects/:name/wordpress/pages", async (request) => {
10618
+ return withWordpressErrorHandling(async () => {
10619
+ const store = requireStore();
10703
10620
  const project = resolveProject(app.db, request.params.name);
10704
- const connection = requireConnection(store, project.name, reply);
10705
- if (!connection) return;
10621
+ const connection = requireConnection(store, project.name);
10706
10622
  const env = parseEnvInput(request.query?.env);
10707
10623
  return {
10708
10624
  env: env ?? connection.defaultEnv,
@@ -10710,34 +10626,28 @@ async function wordpressRoutes(app, opts) {
10710
10626
  };
10711
10627
  });
10712
10628
  });
10713
- app.get("/projects/:name/wordpress/page", async (request, reply) => {
10714
- return withWordpressErrorHandling(reply, async () => {
10715
- const store = requireStore(reply);
10716
- if (!store) return;
10629
+ app.get("/projects/:name/wordpress/page", async (request) => {
10630
+ return withWordpressErrorHandling(async () => {
10631
+ const store = requireStore();
10717
10632
  const project = resolveProject(app.db, request.params.name);
10718
- const connection = requireConnection(store, project.name, reply);
10719
- if (!connection) return;
10633
+ const connection = requireConnection(store, project.name);
10720
10634
  const slug = request.query?.slug?.trim();
10721
10635
  if (!slug) {
10722
- const err = validationError("slug is required");
10723
- return reply.status(err.statusCode).send(err.toJSON());
10636
+ throw validationError("slug is required");
10724
10637
  }
10725
10638
  const env = parseEnvInput(request.query?.env);
10726
10639
  return getPageDetail(connection, slug, env);
10727
10640
  });
10728
10641
  });
10729
- app.post("/projects/:name/wordpress/pages", async (request, reply) => {
10730
- return withWordpressErrorHandling(reply, async () => {
10731
- const store = requireStore(reply);
10732
- if (!store) return;
10642
+ app.post("/projects/:name/wordpress/pages", async (request) => {
10643
+ return withWordpressErrorHandling(async () => {
10644
+ const store = requireStore();
10733
10645
  const project = resolveProject(app.db, request.params.name);
10734
- const connection = requireConnection(store, project.name, reply);
10735
- if (!connection) return;
10646
+ const connection = requireConnection(store, project.name);
10736
10647
  const { title, slug, content, status } = request.body ?? {};
10737
10648
  const env = parseEnvInput(request.body?.env);
10738
10649
  if (!title || !slug || !content) {
10739
- const err = validationError("title, slug, and content are required");
10740
- return reply.status(err.statusCode).send(err.toJSON());
10650
+ throw validationError("title, slug, and content are required");
10741
10651
  }
10742
10652
  const created = await createPage(connection, { title, slug, content, status }, env);
10743
10653
  writeAuditLog(app.db, {
@@ -10750,17 +10660,14 @@ async function wordpressRoutes(app, opts) {
10750
10660
  return created;
10751
10661
  });
10752
10662
  });
10753
- app.put("/projects/:name/wordpress/page", async (request, reply) => {
10754
- return withWordpressErrorHandling(reply, async () => {
10755
- const store = requireStore(reply);
10756
- if (!store) return;
10663
+ app.put("/projects/:name/wordpress/page", async (request) => {
10664
+ return withWordpressErrorHandling(async () => {
10665
+ const store = requireStore();
10757
10666
  const project = resolveProject(app.db, request.params.name);
10758
- const connection = requireConnection(store, project.name, reply);
10759
- if (!connection) return;
10667
+ const connection = requireConnection(store, project.name);
10760
10668
  const currentSlug = request.body?.currentSlug?.trim();
10761
10669
  if (!currentSlug) {
10762
- const err = validationError("currentSlug is required");
10763
- return reply.status(err.statusCode).send(err.toJSON());
10670
+ throw validationError("currentSlug is required");
10764
10671
  }
10765
10672
  const env = parseEnvInput(request.body?.env);
10766
10673
  const updated = await updatePageBySlug(connection, currentSlug, {
@@ -10779,17 +10686,14 @@ async function wordpressRoutes(app, opts) {
10779
10686
  return updated;
10780
10687
  });
10781
10688
  });
10782
- app.post("/projects/:name/wordpress/page/meta", async (request, reply) => {
10783
- return withWordpressErrorHandling(reply, async () => {
10784
- const store = requireStore(reply);
10785
- if (!store) return;
10689
+ app.post("/projects/:name/wordpress/page/meta", async (request) => {
10690
+ return withWordpressErrorHandling(async () => {
10691
+ const store = requireStore();
10786
10692
  const project = resolveProject(app.db, request.params.name);
10787
- const connection = requireConnection(store, project.name, reply);
10788
- if (!connection) return;
10693
+ const connection = requireConnection(store, project.name);
10789
10694
  const slug = request.body?.slug?.trim();
10790
10695
  if (!slug) {
10791
- const err = validationError("slug is required");
10792
- return reply.status(err.statusCode).send(err.toJSON());
10696
+ throw validationError("slug is required");
10793
10697
  }
10794
10698
  const env = parseEnvInput(request.body?.env);
10795
10699
  const updated = await setSeoMeta(connection, slug, {
@@ -10807,22 +10711,18 @@ async function wordpressRoutes(app, opts) {
10807
10711
  return updated;
10808
10712
  });
10809
10713
  });
10810
- app.post("/projects/:name/wordpress/pages/meta/bulk", async (request, reply) => {
10811
- return withWordpressErrorHandling(reply, async () => {
10812
- const store = requireStore(reply);
10813
- if (!store) return;
10714
+ app.post("/projects/:name/wordpress/pages/meta/bulk", async (request) => {
10715
+ return withWordpressErrorHandling(async () => {
10716
+ const store = requireStore();
10814
10717
  const project = resolveProject(app.db, request.params.name);
10815
- const connection = requireConnection(store, project.name, reply);
10816
- if (!connection) return;
10718
+ const connection = requireConnection(store, project.name);
10817
10719
  const entries = request.body?.entries;
10818
10720
  if (!Array.isArray(entries) || entries.length === 0) {
10819
- const err = validationError("entries array is required and must not be empty");
10820
- return reply.status(err.statusCode).send(err.toJSON());
10721
+ throw validationError("entries array is required and must not be empty");
10821
10722
  }
10822
10723
  for (const entry of entries) {
10823
10724
  if (!entry.slug?.trim()) {
10824
- const err = validationError("each entry must have a slug");
10825
- return reply.status(err.statusCode).send(err.toJSON());
10725
+ throw validationError("each entry must have a slug");
10826
10726
  }
10827
10727
  }
10828
10728
  const env = parseEnvInput(request.body?.env);
@@ -10840,126 +10740,103 @@ async function wordpressRoutes(app, opts) {
10840
10740
  return result;
10841
10741
  });
10842
10742
  });
10843
- app.get("/projects/:name/wordpress/schema", async (request, reply) => {
10844
- return withWordpressErrorHandling(reply, async () => {
10845
- const store = requireStore(reply);
10846
- if (!store) return;
10743
+ app.get("/projects/:name/wordpress/schema", async (request) => {
10744
+ return withWordpressErrorHandling(async () => {
10745
+ const store = requireStore();
10847
10746
  const project = resolveProject(app.db, request.params.name);
10848
- const connection = requireConnection(store, project.name, reply);
10849
- if (!connection) return;
10747
+ const connection = requireConnection(store, project.name);
10850
10748
  const slug = request.query?.slug?.trim();
10851
10749
  if (!slug) {
10852
- const err = validationError("slug is required");
10853
- return reply.status(err.statusCode).send(err.toJSON());
10750
+ throw validationError("slug is required");
10854
10751
  }
10855
10752
  const env = parseEnvInput(request.query?.env);
10856
10753
  return getPageSchema(connection, slug, env);
10857
10754
  });
10858
10755
  });
10859
- app.post("/projects/:name/wordpress/schema/manual", async (request, reply) => {
10860
- return withWordpressErrorHandling(reply, async () => {
10861
- const store = requireStore(reply);
10862
- if (!store) return;
10756
+ app.post("/projects/:name/wordpress/schema/manual", async (request) => {
10757
+ return withWordpressErrorHandling(async () => {
10758
+ const store = requireStore();
10863
10759
  const project = resolveProject(app.db, request.params.name);
10864
- const connection = requireConnection(store, project.name, reply);
10865
- if (!connection) return;
10760
+ const connection = requireConnection(store, project.name);
10866
10761
  const slug = request.body?.slug?.trim();
10867
10762
  const json = request.body?.json;
10868
10763
  if (!slug || !json) {
10869
- const err = validationError("slug and json are required");
10870
- return reply.status(err.statusCode).send(err.toJSON());
10764
+ throw validationError("slug and json are required");
10871
10765
  }
10872
10766
  const env = parseEnvInput(request.body?.env);
10873
10767
  return buildManualSchemaUpdate(connection, slug, { type: request.body?.type, json }, env);
10874
10768
  });
10875
10769
  });
10876
- app.post("/projects/:name/wordpress/schema/deploy", async (request, reply) => {
10877
- return withWordpressErrorHandling(reply, async () => {
10878
- const store = requireStore(reply);
10879
- if (!store) return;
10770
+ app.post("/projects/:name/wordpress/schema/deploy", async (request) => {
10771
+ return withWordpressErrorHandling(async () => {
10772
+ const store = requireStore();
10880
10773
  const project = resolveProject(app.db, request.params.name);
10881
- const connection = requireConnection(store, project.name, reply);
10882
- if (!connection) return;
10774
+ const connection = requireConnection(store, project.name);
10883
10775
  const profile = request.body?.profile;
10884
10776
  if (!profile?.business?.name || !profile?.pages || Object.keys(profile.pages).length === 0) {
10885
- const err = validationError("profile with business.name and non-empty pages is required");
10886
- return reply.status(err.statusCode).send(err.toJSON());
10777
+ throw validationError("profile with business.name and non-empty pages is required");
10887
10778
  }
10888
10779
  const env = parseEnvInput(request.body?.env);
10889
10780
  return deploySchemaFromProfile(connection, profile, env);
10890
10781
  });
10891
10782
  });
10892
- app.get("/projects/:name/wordpress/schema/status", async (request, reply) => {
10893
- return withWordpressErrorHandling(reply, async () => {
10894
- const store = requireStore(reply);
10895
- if (!store) return;
10783
+ app.get("/projects/:name/wordpress/schema/status", async (request) => {
10784
+ return withWordpressErrorHandling(async () => {
10785
+ const store = requireStore();
10896
10786
  const project = resolveProject(app.db, request.params.name);
10897
- const connection = requireConnection(store, project.name, reply);
10898
- if (!connection) return;
10787
+ const connection = requireConnection(store, project.name);
10899
10788
  const env = parseEnvInput(request.query?.env);
10900
10789
  return getSchemaStatus(connection, env);
10901
10790
  });
10902
10791
  });
10903
- app.get("/projects/:name/wordpress/llms-txt", async (request, reply) => {
10904
- return withWordpressErrorHandling(reply, async () => {
10905
- const store = requireStore(reply);
10906
- if (!store) return;
10792
+ app.get("/projects/:name/wordpress/llms-txt", async (request) => {
10793
+ return withWordpressErrorHandling(async () => {
10794
+ const store = requireStore();
10907
10795
  const project = resolveProject(app.db, request.params.name);
10908
- const connection = requireConnection(store, project.name, reply);
10909
- if (!connection) return;
10796
+ const connection = requireConnection(store, project.name);
10910
10797
  const env = parseEnvInput(request.query?.env);
10911
10798
  return getLlmsTxt(connection, env);
10912
10799
  });
10913
10800
  });
10914
- app.post("/projects/:name/wordpress/llms-txt/manual", async (request, reply) => {
10915
- return withWordpressErrorHandling(reply, async () => {
10916
- const store = requireStore(reply);
10917
- if (!store) return;
10801
+ app.post("/projects/:name/wordpress/llms-txt/manual", async (request) => {
10802
+ return withWordpressErrorHandling(async () => {
10803
+ const store = requireStore();
10918
10804
  const project = resolveProject(app.db, request.params.name);
10919
- const connection = requireConnection(store, project.name, reply);
10920
- if (!connection) return;
10805
+ const connection = requireConnection(store, project.name);
10921
10806
  const content = request.body?.content;
10922
10807
  if (!content) {
10923
- const err = validationError("content is required");
10924
- return reply.status(err.statusCode).send(err.toJSON());
10808
+ throw validationError("content is required");
10925
10809
  }
10926
10810
  const env = parseEnvInput(request.body?.env);
10927
10811
  return buildManualLlmsTxtUpdate(connection, content, env);
10928
10812
  });
10929
10813
  });
10930
- app.get("/projects/:name/wordpress/audit", async (request, reply) => {
10931
- return withWordpressErrorHandling(reply, async () => {
10932
- const store = requireStore(reply);
10933
- if (!store) return;
10814
+ app.get("/projects/:name/wordpress/audit", async (request) => {
10815
+ return withWordpressErrorHandling(async () => {
10816
+ const store = requireStore();
10934
10817
  const project = resolveProject(app.db, request.params.name);
10935
- const connection = requireConnection(store, project.name, reply);
10936
- if (!connection) return;
10818
+ const connection = requireConnection(store, project.name);
10937
10819
  const env = parseEnvInput(request.query?.env);
10938
10820
  return runAudit(connection, env);
10939
10821
  });
10940
10822
  });
10941
- app.get("/projects/:name/wordpress/diff", async (request, reply) => {
10942
- return withWordpressErrorHandling(reply, async () => {
10943
- const store = requireStore(reply);
10944
- if (!store) return;
10823
+ app.get("/projects/:name/wordpress/diff", async (request) => {
10824
+ return withWordpressErrorHandling(async () => {
10825
+ const store = requireStore();
10945
10826
  const project = resolveProject(app.db, request.params.name);
10946
- const connection = requireConnection(store, project.name, reply);
10947
- if (!connection) return;
10827
+ const connection = requireConnection(store, project.name);
10948
10828
  const slug = request.query?.slug?.trim();
10949
10829
  if (!slug) {
10950
- const err = validationError("slug is required");
10951
- return reply.status(err.statusCode).send(err.toJSON());
10830
+ throw validationError("slug is required");
10952
10831
  }
10953
10832
  return diffPageAcrossEnvironments(connection, slug);
10954
10833
  });
10955
10834
  });
10956
- app.get("/projects/:name/wordpress/staging/status", async (request, reply) => {
10957
- return withWordpressErrorHandling(reply, async () => {
10958
- const store = requireStore(reply);
10959
- if (!store) return;
10835
+ app.get("/projects/:name/wordpress/staging/status", async (request) => {
10836
+ return withWordpressErrorHandling(async () => {
10837
+ const store = requireStore();
10960
10838
  const project = resolveProject(app.db, request.params.name);
10961
- const connection = requireConnection(store, project.name, reply);
10962
- if (!connection) return;
10839
+ const connection = requireConnection(store, project.name);
10963
10840
  const plugins = await listActivePlugins(connection, "live");
10964
10841
  return {
10965
10842
  stagingConfigured: Boolean(connection.stagingUrl),
@@ -10969,34 +10846,28 @@ async function wordpressRoutes(app, opts) {
10969
10846
  };
10970
10847
  });
10971
10848
  });
10972
- app.post("/projects/:name/wordpress/staging/push", async (request, reply) => {
10973
- return withWordpressErrorHandling(reply, async () => {
10974
- const store = requireStore(reply);
10975
- if (!store) return;
10849
+ app.post("/projects/:name/wordpress/staging/push", async (request) => {
10850
+ return withWordpressErrorHandling(async () => {
10851
+ const store = requireStore();
10976
10852
  const project = resolveProject(app.db, request.params.name);
10977
- const connection = requireConnection(store, project.name, reply);
10978
- if (!connection) return;
10853
+ const connection = requireConnection(store, project.name);
10979
10854
  if (!connection.stagingUrl) {
10980
- const err = validationError("No staging URL configured for this project. Reconnect with --staging-url before using staging push.");
10981
- return reply.status(err.statusCode).send(err.toJSON());
10855
+ throw validationError("No staging URL configured for this project. Reconnect with --staging-url before using staging push.");
10982
10856
  }
10983
10857
  return buildManualStagingPush(connection);
10984
10858
  });
10985
10859
  });
10986
- app.post("/projects/:name/wordpress/onboard", async (request, reply) => {
10987
- return withWordpressErrorHandling(reply, async () => {
10988
- const store = requireStore(reply);
10989
- if (!store) return;
10860
+ app.post("/projects/:name/wordpress/onboard", async (request) => {
10861
+ return withWordpressErrorHandling(async () => {
10862
+ const store = requireStore();
10990
10863
  const project = resolveProject(app.db, request.params.name);
10991
10864
  const { url, username, appPassword, stagingUrl, profile, skipSchema, skipSubmit } = request.body ?? {};
10992
10865
  if (!url || !username || !appPassword) {
10993
- const err = validationError("url, username, and appPassword are required");
10994
- return reply.status(err.statusCode).send(err.toJSON());
10866
+ throw validationError("url, username, and appPassword are required");
10995
10867
  }
10996
10868
  const defaultEnv = parseEnvInput(request.body?.defaultEnv, "defaultEnv") ?? (stagingUrl ? "staging" : "live");
10997
10869
  if (defaultEnv === "staging" && !stagingUrl) {
10998
- const err = validationError('defaultEnv "staging" requires stagingUrl');
10999
- return reply.status(err.statusCode).send(err.toJSON());
10870
+ throw validationError('defaultEnv "staging" requires stagingUrl');
11000
10871
  }
11001
10872
  const steps = [];
11002
10873
  let connection = null;
@@ -14543,6 +14414,26 @@ var ProviderExecutionGate = class {
14543
14414
  }
14544
14415
  }
14545
14416
  };
14417
+ async function runWithConcurrency(items, limit, worker) {
14418
+ if (items.length === 0) return;
14419
+ const cap = Math.max(1, Math.min(limit, items.length));
14420
+ let cursor = 0;
14421
+ const next = async () => {
14422
+ while (true) {
14423
+ const idx = cursor++;
14424
+ if (idx >= items.length) return;
14425
+ await worker(items[idx]);
14426
+ }
14427
+ };
14428
+ await Promise.all(Array.from({ length: cap }, next));
14429
+ }
14430
+ var PROVIDER_FANOUT_DEFAULT = 8;
14431
+ function resolveProviderFanout() {
14432
+ const raw = process.env.CANONRY_PROVIDER_FANOUT;
14433
+ if (!raw) return PROVIDER_FANOUT_DEFAULT;
14434
+ const parsed = Number.parseInt(raw, 10);
14435
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : PROVIDER_FANOUT_DEFAULT;
14436
+ }
14546
14437
  var JobRunner = class {
14547
14438
  db;
14548
14439
  registry;
@@ -14750,11 +14641,11 @@ var JobRunner = class {
14750
14641
  }
14751
14642
  }
14752
14643
  };
14753
- await Promise.all(apiProviders.map(async (registeredProvider) => {
14644
+ await runWithConcurrency(apiProviders, resolveProviderFanout(), async (registeredProvider) => {
14754
14645
  await Promise.all(projectKeywords.map(async (kw) => {
14755
14646
  await processKeywordForProvider(registeredProvider, kw);
14756
14647
  }));
14757
- }));
14648
+ });
14758
14649
  for (const registeredProvider of browserProviders) {
14759
14650
  for (const kw of projectKeywords) {
14760
14651
  await processKeywordForProvider(registeredProvider, kw);