@ainyc/canonry 2.4.2 → 2.4.3

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.
@@ -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) {
@@ -8242,28 +8164,22 @@ function bingLog(level, action, ctx) {
8242
8164
  stream.write(JSON.stringify(entry) + "\n");
8243
8165
  }
8244
8166
  async function bingRoutes(app, opts) {
8245
- function requireConnectionStore(reply) {
8167
+ function requireConnectionStore() {
8246
8168
  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;
8169
+ throw validationError("Bing connection storage is not configured for this deployment");
8250
8170
  }
8251
- function requireConnection(store, domain, reply) {
8171
+ function requireConnection(store, domain) {
8252
8172
  const conn = store.getConnection(domain);
8253
8173
  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;
8174
+ throw validationError('No Bing connection found for this domain. Run "canonry bing connect <project>" first.');
8257
8175
  }
8258
8176
  return conn;
8259
8177
  }
8260
- app.post("/projects/:name/bing/connect", async (request, reply) => {
8261
- const store = requireConnectionStore(reply);
8262
- if (!store) return;
8178
+ app.post("/projects/:name/bing/connect", async (request) => {
8179
+ const store = requireConnectionStore();
8263
8180
  const { apiKey } = request.body ?? {};
8264
8181
  if (!apiKey || typeof apiKey !== "string") {
8265
- const err = validationError("apiKey is required");
8266
- return reply.status(err.statusCode).send(err.toJSON());
8182
+ throw validationError("apiKey is required");
8267
8183
  }
8268
8184
  const project = resolveProject(app.db, request.params.name);
8269
8185
  let sites;
@@ -8273,8 +8189,7 @@ async function bingRoutes(app, opts) {
8273
8189
  } catch (e) {
8274
8190
  const msg = e instanceof Error ? e.message : String(e);
8275
8191
  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());
8192
+ throw validationError(`Failed to verify Bing API key: ${msg}`);
8278
8193
  }
8279
8194
  const now = (/* @__PURE__ */ new Date()).toISOString();
8280
8195
  const existing = store.getConnection(project.canonicalDomain);
@@ -8300,13 +8215,11 @@ async function bingRoutes(app, opts) {
8300
8215
  };
8301
8216
  });
8302
8217
  app.delete("/projects/:name/bing/disconnect", async (request, reply) => {
8303
- const store = requireConnectionStore(reply);
8304
- if (!store) return;
8218
+ const store = requireConnectionStore();
8305
8219
  const project = resolveProject(app.db, request.params.name);
8306
8220
  const deleted = store.deleteConnection(project.canonicalDomain);
8307
8221
  if (!deleted) {
8308
- const err = notFound("Bing connection", project.canonicalDomain);
8309
- return reply.status(err.statusCode).send(err.toJSON());
8222
+ throw notFound("Bing connection", project.canonicalDomain);
8310
8223
  }
8311
8224
  writeAuditLog(app.db, {
8312
8225
  projectId: project.id,
@@ -8317,9 +8230,8 @@ async function bingRoutes(app, opts) {
8317
8230
  });
8318
8231
  return reply.status(204).send();
8319
8232
  });
8320
- app.get("/projects/:name/bing/status", async (request, reply) => {
8321
- const store = requireConnectionStore(reply);
8322
- if (!store) return;
8233
+ app.get("/projects/:name/bing/status", async (request) => {
8234
+ const store = requireConnectionStore();
8323
8235
  const project = resolveProject(app.db, request.params.name);
8324
8236
  const conn = store.getConnection(project.canonicalDomain);
8325
8237
  return {
@@ -8330,25 +8242,20 @@ async function bingRoutes(app, opts) {
8330
8242
  updatedAt: conn?.updatedAt ?? null
8331
8243
  };
8332
8244
  });
8333
- app.get("/projects/:name/bing/sites", async (request, reply) => {
8334
- const store = requireConnectionStore(reply);
8335
- if (!store) return;
8245
+ app.get("/projects/:name/bing/sites", async (request) => {
8246
+ const store = requireConnectionStore();
8336
8247
  const project = resolveProject(app.db, request.params.name);
8337
- const conn = requireConnection(store, project.canonicalDomain, reply);
8338
- if (!conn) return;
8248
+ const conn = requireConnection(store, project.canonicalDomain);
8339
8249
  const sites = await getSites(conn.apiKey);
8340
8250
  return { sites: sites.map((s) => ({ url: s.Url, verified: s.Verified ?? false })) };
8341
8251
  });
8342
- app.post("/projects/:name/bing/set-site", async (request, reply) => {
8343
- const store = requireConnectionStore(reply);
8344
- if (!store) return;
8252
+ app.post("/projects/:name/bing/set-site", async (request) => {
8253
+ const store = requireConnectionStore();
8345
8254
  const project = resolveProject(app.db, request.params.name);
8346
- const conn = requireConnection(store, project.canonicalDomain, reply);
8347
- if (!conn) return;
8255
+ requireConnection(store, project.canonicalDomain);
8348
8256
  const { siteUrl } = request.body ?? {};
8349
8257
  if (!siteUrl || typeof siteUrl !== "string") {
8350
- const err = validationError("siteUrl is required");
8351
- return reply.status(err.statusCode).send(err.toJSON());
8258
+ throw validationError("siteUrl is required");
8352
8259
  }
8353
8260
  store.updateConnection(project.canonicalDomain, {
8354
8261
  siteUrl,
@@ -8356,12 +8263,10 @@ async function bingRoutes(app, opts) {
8356
8263
  });
8357
8264
  return { siteUrl };
8358
8265
  });
8359
- app.get("/projects/:name/bing/coverage", async (request, reply) => {
8360
- const store = requireConnectionStore(reply);
8361
- if (!store) return;
8266
+ app.get("/projects/:name/bing/coverage", async (request) => {
8267
+ const store = requireConnectionStore();
8362
8268
  const project = resolveProject(app.db, request.params.name);
8363
- const conn = requireConnection(store, project.canonicalDomain, reply);
8364
- if (!conn) return;
8269
+ requireConnection(store, project.canonicalDomain);
8365
8270
  const allInspections = app.db.select().from(bingUrlInspections).where(eq15(bingUrlInspections.projectId, project.id)).orderBy(desc6(bingUrlInspections.inspectedAt)).all();
8366
8271
  const latestByUrl = /* @__PURE__ */ new Map();
8367
8272
  const definitiveByUrl = /* @__PURE__ */ new Map();
@@ -8446,9 +8351,8 @@ async function bingRoutes(app, opts) {
8446
8351
  unknown: unknownUrls.map(formatRow)
8447
8352
  };
8448
8353
  });
8449
- app.get("/projects/:name/bing/coverage/history", async (request, reply) => {
8450
- const store = requireConnectionStore(reply);
8451
- if (!store) return;
8354
+ app.get("/projects/:name/bing/coverage/history", async (request) => {
8355
+ requireConnectionStore();
8452
8356
  const project = resolveProject(app.db, request.params.name);
8453
8357
  const parsed = parseInt(request.query.limit ?? "90", 10);
8454
8358
  const limit = Number.isNaN(parsed) || parsed <= 0 ? 90 : parsed;
@@ -8460,9 +8364,8 @@ async function bingRoutes(app, opts) {
8460
8364
  unknown: r.unknown
8461
8365
  }));
8462
8366
  });
8463
- app.get("/projects/:name/bing/inspections", async (request, reply) => {
8464
- const store = requireConnectionStore(reply);
8465
- if (!store) return;
8367
+ app.get("/projects/:name/bing/inspections", async (request) => {
8368
+ requireConnectionStore();
8466
8369
  const project = resolveProject(app.db, request.params.name);
8467
8370
  const { url, limit } = request.query;
8468
8371
  const whereClause = url ? and4(eq15(bingUrlInspections.projectId, project.id), eq15(bingUrlInspections.url, url)) : eq15(bingUrlInspections.projectId, project.id);
@@ -8480,20 +8383,16 @@ async function bingRoutes(app, opts) {
8480
8383
  discoveryDate: r.discoveryDate ?? null
8481
8384
  }));
8482
8385
  });
8483
- app.post("/projects/:name/bing/inspect-url", async (request, reply) => {
8484
- const store = requireConnectionStore(reply);
8485
- if (!store) return;
8386
+ app.post("/projects/:name/bing/inspect-url", async (request) => {
8387
+ const store = requireConnectionStore();
8486
8388
  const project = resolveProject(app.db, request.params.name);
8487
- const conn = requireConnection(store, project.canonicalDomain, reply);
8488
- if (!conn) return;
8389
+ const conn = requireConnection(store, project.canonicalDomain);
8489
8390
  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());
8391
+ throw validationError('No Bing site configured. Run "canonry bing set-site <project> <url>" first.');
8492
8392
  }
8493
8393
  const { url } = request.body ?? {};
8494
8394
  if (!url) {
8495
- const err = validationError("url is required");
8496
- return reply.status(err.statusCode).send(err.toJSON());
8395
+ throw validationError("url is required");
8497
8396
  }
8498
8397
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
8499
8398
  const runId = crypto15.randomUUID();
@@ -8563,15 +8462,12 @@ async function bingRoutes(app, opts) {
8563
8462
  throw e;
8564
8463
  }
8565
8464
  });
8566
- app.post("/projects/:name/bing/request-indexing", async (request, reply) => {
8567
- const store = requireConnectionStore(reply);
8568
- if (!store) return;
8465
+ app.post("/projects/:name/bing/request-indexing", async (request) => {
8466
+ const store = requireConnectionStore();
8569
8467
  const project = resolveProject(app.db, request.params.name);
8570
- const conn = requireConnection(store, project.canonicalDomain, reply);
8571
- if (!conn) return;
8468
+ const conn = requireConnection(store, project.canonicalDomain);
8572
8469
  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());
8470
+ throw validationError('No Bing site configured. Run "canonry bing set-site <project> <url>" first.');
8575
8471
  }
8576
8472
  let urlsToSubmit = request.body?.urls ?? [];
8577
8473
  if (request.body?.allUnindexed) {
@@ -8589,18 +8485,15 @@ async function bingRoutes(app, opts) {
8589
8485
  }
8590
8486
  }
8591
8487
  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());
8488
+ throw validationError('No unindexed or unknown URLs found. Run "canonry bing inspect <project> <url>" first.');
8594
8489
  }
8595
8490
  urlsToSubmit = unindexedUrls;
8596
8491
  }
8597
8492
  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());
8493
+ throw validationError("At least one URL is required (or use allUnindexed: true)");
8600
8494
  }
8601
8495
  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());
8496
+ throw validationError(`Cannot submit more than ${BING_SUBMIT_URL_DAILY_LIMIT} URLs per day (got ${urlsToSubmit.length})`);
8604
8497
  }
8605
8498
  const results = [];
8606
8499
  bingLog("info", "index-submit.start", { domain: project.canonicalDomain, siteUrl: conn.siteUrl, urlCount: urlsToSubmit.length, allUnindexed: !!request.body?.allUnindexed });
@@ -8643,15 +8536,12 @@ async function bingRoutes(app, opts) {
8643
8536
  results
8644
8537
  };
8645
8538
  });
8646
- app.get("/projects/:name/bing/performance", async (request, reply) => {
8647
- const store = requireConnectionStore(reply);
8648
- if (!store) return;
8539
+ app.get("/projects/:name/bing/performance", async (request) => {
8540
+ const store = requireConnectionStore();
8649
8541
  const project = resolveProject(app.db, request.params.name);
8650
- const conn = requireConnection(store, project.canonicalDomain, reply);
8651
- if (!conn) return;
8542
+ const conn = requireConnection(store, project.canonicalDomain);
8652
8543
  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());
8544
+ throw validationError('No Bing site configured. Run "canonry bing set-site <project> <url>" first.');
8655
8545
  }
8656
8546
  const stats = await getKeywordStats(conn.apiKey, conn.siteUrl);
8657
8547
  return stats.map((s) => ({
@@ -10558,68 +10448,51 @@ function parseEnvInput(value, fieldName = "env") {
10558
10448
  }
10559
10449
  return env;
10560
10450
  }
10561
- function sendWordpressError(reply, error) {
10562
- if (!(error instanceof WordpressApiError)) return false;
10563
- let appError;
10451
+ function toAppError(error) {
10564
10452
  switch (error.code) {
10565
10453
  case "AUTH_INVALID":
10566
- appError = new AppError("AUTH_INVALID", error.message, error.statusCode);
10567
- break;
10454
+ return new AppError("AUTH_INVALID", error.message, error.statusCode);
10568
10455
  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;
10456
+ return new AppError("NOT_FOUND", error.message, error.statusCode);
10574
10457
  case "UNSUPPORTED":
10575
10458
  case "VALIDATION_ERROR":
10576
- appError = validationError(error.message);
10577
- break;
10459
+ return validationError(error.message);
10460
+ case "UPSTREAM_ERROR":
10578
10461
  default:
10579
- appError = providerError(error.message, { statusCode: error.statusCode });
10580
- break;
10462
+ return providerError(error.message, { statusCode: error.statusCode });
10581
10463
  }
10582
- reply.status(appError.statusCode).send(appError.toJSON());
10583
- return true;
10584
10464
  }
10585
- async function withWordpressErrorHandling(reply, handler) {
10465
+ async function withWordpressErrorHandling(handler) {
10586
10466
  try {
10587
10467
  return await handler();
10588
10468
  } catch (error) {
10589
- if (sendWordpressError(reply, error)) return;
10469
+ if (error instanceof WordpressApiError) throw toAppError(error);
10590
10470
  throw error;
10591
10471
  }
10592
10472
  }
10593
10473
  async function wordpressRoutes(app, opts) {
10594
- function requireStore(reply) {
10474
+ function requireStore() {
10595
10475
  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;
10476
+ throw validationError("WordPress connection storage is not configured for this deployment");
10599
10477
  }
10600
- function requireConnection(store, projectName, reply) {
10478
+ function requireConnection(store, projectName) {
10601
10479
  const connection = store.getConnection(projectName);
10602
10480
  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;
10481
+ throw validationError(`No WordPress connection found for project "${projectName}". Run "canonry wordpress connect ${projectName}" first.`);
10606
10482
  }
10607
10483
  return connection;
10608
10484
  }
10609
- app.post("/projects/:name/wordpress/connect", async (request, reply) => {
10610
- return withWordpressErrorHandling(reply, async () => {
10611
- const store = requireStore(reply);
10612
- if (!store) return;
10485
+ app.post("/projects/:name/wordpress/connect", async (request) => {
10486
+ return withWordpressErrorHandling(async () => {
10487
+ const store = requireStore();
10613
10488
  const project = resolveProject(app.db, request.params.name);
10614
10489
  const { url, stagingUrl, username, appPassword } = request.body ?? {};
10615
10490
  if (!url || !username || !appPassword) {
10616
- const err = validationError("url, username, and appPassword are required");
10617
- return reply.status(err.statusCode).send(err.toJSON());
10491
+ throw validationError("url, username, and appPassword are required");
10618
10492
  }
10619
10493
  const defaultEnv = parseEnvInput(request.body?.defaultEnv, "defaultEnv") ?? (stagingUrl ? "staging" : "live");
10620
10494
  if (defaultEnv === "staging" && !stagingUrl) {
10621
- const err = validationError('defaultEnv "staging" requires stagingUrl');
10622
- return reply.status(err.statusCode).send(err.toJSON());
10495
+ throw validationError('defaultEnv "staging" requires stagingUrl');
10623
10496
  }
10624
10497
  const now = (/* @__PURE__ */ new Date()).toISOString();
10625
10498
  const existing = store.getConnection(project.name);
@@ -10655,13 +10528,11 @@ async function wordpressRoutes(app, opts) {
10655
10528
  });
10656
10529
  });
10657
10530
  app.delete("/projects/:name/wordpress/disconnect", async (request, reply) => {
10658
- const store = requireStore(reply);
10659
- if (!store) return;
10531
+ const store = requireStore();
10660
10532
  const project = resolveProject(app.db, request.params.name);
10661
10533
  const deleted = store.deleteConnection(project.name);
10662
10534
  if (!deleted) {
10663
- const err = notFound("WordPress connection", project.name);
10664
- return reply.status(err.statusCode).send(err.toJSON());
10535
+ throw notFound("WordPress connection", project.name);
10665
10536
  }
10666
10537
  writeAuditLog(app.db, {
10667
10538
  projectId: project.id,
@@ -10696,13 +10567,11 @@ async function wordpressRoutes(app, opts) {
10696
10567
  adminUrl: getWpStagingAdminUrl(connection.url)
10697
10568
  };
10698
10569
  });
10699
- app.get("/projects/:name/wordpress/pages", async (request, reply) => {
10700
- return withWordpressErrorHandling(reply, async () => {
10701
- const store = requireStore(reply);
10702
- if (!store) return;
10570
+ app.get("/projects/:name/wordpress/pages", async (request) => {
10571
+ return withWordpressErrorHandling(async () => {
10572
+ const store = requireStore();
10703
10573
  const project = resolveProject(app.db, request.params.name);
10704
- const connection = requireConnection(store, project.name, reply);
10705
- if (!connection) return;
10574
+ const connection = requireConnection(store, project.name);
10706
10575
  const env = parseEnvInput(request.query?.env);
10707
10576
  return {
10708
10577
  env: env ?? connection.defaultEnv,
@@ -10710,34 +10579,28 @@ async function wordpressRoutes(app, opts) {
10710
10579
  };
10711
10580
  });
10712
10581
  });
10713
- app.get("/projects/:name/wordpress/page", async (request, reply) => {
10714
- return withWordpressErrorHandling(reply, async () => {
10715
- const store = requireStore(reply);
10716
- if (!store) return;
10582
+ app.get("/projects/:name/wordpress/page", async (request) => {
10583
+ return withWordpressErrorHandling(async () => {
10584
+ const store = requireStore();
10717
10585
  const project = resolveProject(app.db, request.params.name);
10718
- const connection = requireConnection(store, project.name, reply);
10719
- if (!connection) return;
10586
+ const connection = requireConnection(store, project.name);
10720
10587
  const slug = request.query?.slug?.trim();
10721
10588
  if (!slug) {
10722
- const err = validationError("slug is required");
10723
- return reply.status(err.statusCode).send(err.toJSON());
10589
+ throw validationError("slug is required");
10724
10590
  }
10725
10591
  const env = parseEnvInput(request.query?.env);
10726
10592
  return getPageDetail(connection, slug, env);
10727
10593
  });
10728
10594
  });
10729
- app.post("/projects/:name/wordpress/pages", async (request, reply) => {
10730
- return withWordpressErrorHandling(reply, async () => {
10731
- const store = requireStore(reply);
10732
- if (!store) return;
10595
+ app.post("/projects/:name/wordpress/pages", async (request) => {
10596
+ return withWordpressErrorHandling(async () => {
10597
+ const store = requireStore();
10733
10598
  const project = resolveProject(app.db, request.params.name);
10734
- const connection = requireConnection(store, project.name, reply);
10735
- if (!connection) return;
10599
+ const connection = requireConnection(store, project.name);
10736
10600
  const { title, slug, content, status } = request.body ?? {};
10737
10601
  const env = parseEnvInput(request.body?.env);
10738
10602
  if (!title || !slug || !content) {
10739
- const err = validationError("title, slug, and content are required");
10740
- return reply.status(err.statusCode).send(err.toJSON());
10603
+ throw validationError("title, slug, and content are required");
10741
10604
  }
10742
10605
  const created = await createPage(connection, { title, slug, content, status }, env);
10743
10606
  writeAuditLog(app.db, {
@@ -10750,17 +10613,14 @@ async function wordpressRoutes(app, opts) {
10750
10613
  return created;
10751
10614
  });
10752
10615
  });
10753
- app.put("/projects/:name/wordpress/page", async (request, reply) => {
10754
- return withWordpressErrorHandling(reply, async () => {
10755
- const store = requireStore(reply);
10756
- if (!store) return;
10616
+ app.put("/projects/:name/wordpress/page", async (request) => {
10617
+ return withWordpressErrorHandling(async () => {
10618
+ const store = requireStore();
10757
10619
  const project = resolveProject(app.db, request.params.name);
10758
- const connection = requireConnection(store, project.name, reply);
10759
- if (!connection) return;
10620
+ const connection = requireConnection(store, project.name);
10760
10621
  const currentSlug = request.body?.currentSlug?.trim();
10761
10622
  if (!currentSlug) {
10762
- const err = validationError("currentSlug is required");
10763
- return reply.status(err.statusCode).send(err.toJSON());
10623
+ throw validationError("currentSlug is required");
10764
10624
  }
10765
10625
  const env = parseEnvInput(request.body?.env);
10766
10626
  const updated = await updatePageBySlug(connection, currentSlug, {
@@ -10779,17 +10639,14 @@ async function wordpressRoutes(app, opts) {
10779
10639
  return updated;
10780
10640
  });
10781
10641
  });
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;
10642
+ app.post("/projects/:name/wordpress/page/meta", async (request) => {
10643
+ return withWordpressErrorHandling(async () => {
10644
+ const store = requireStore();
10786
10645
  const project = resolveProject(app.db, request.params.name);
10787
- const connection = requireConnection(store, project.name, reply);
10788
- if (!connection) return;
10646
+ const connection = requireConnection(store, project.name);
10789
10647
  const slug = request.body?.slug?.trim();
10790
10648
  if (!slug) {
10791
- const err = validationError("slug is required");
10792
- return reply.status(err.statusCode).send(err.toJSON());
10649
+ throw validationError("slug is required");
10793
10650
  }
10794
10651
  const env = parseEnvInput(request.body?.env);
10795
10652
  const updated = await setSeoMeta(connection, slug, {
@@ -10807,22 +10664,18 @@ async function wordpressRoutes(app, opts) {
10807
10664
  return updated;
10808
10665
  });
10809
10666
  });
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;
10667
+ app.post("/projects/:name/wordpress/pages/meta/bulk", async (request) => {
10668
+ return withWordpressErrorHandling(async () => {
10669
+ const store = requireStore();
10814
10670
  const project = resolveProject(app.db, request.params.name);
10815
- const connection = requireConnection(store, project.name, reply);
10816
- if (!connection) return;
10671
+ const connection = requireConnection(store, project.name);
10817
10672
  const entries = request.body?.entries;
10818
10673
  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());
10674
+ throw validationError("entries array is required and must not be empty");
10821
10675
  }
10822
10676
  for (const entry of entries) {
10823
10677
  if (!entry.slug?.trim()) {
10824
- const err = validationError("each entry must have a slug");
10825
- return reply.status(err.statusCode).send(err.toJSON());
10678
+ throw validationError("each entry must have a slug");
10826
10679
  }
10827
10680
  }
10828
10681
  const env = parseEnvInput(request.body?.env);
@@ -10840,126 +10693,103 @@ async function wordpressRoutes(app, opts) {
10840
10693
  return result;
10841
10694
  });
10842
10695
  });
10843
- app.get("/projects/:name/wordpress/schema", async (request, reply) => {
10844
- return withWordpressErrorHandling(reply, async () => {
10845
- const store = requireStore(reply);
10846
- if (!store) return;
10696
+ app.get("/projects/:name/wordpress/schema", async (request) => {
10697
+ return withWordpressErrorHandling(async () => {
10698
+ const store = requireStore();
10847
10699
  const project = resolveProject(app.db, request.params.name);
10848
- const connection = requireConnection(store, project.name, reply);
10849
- if (!connection) return;
10700
+ const connection = requireConnection(store, project.name);
10850
10701
  const slug = request.query?.slug?.trim();
10851
10702
  if (!slug) {
10852
- const err = validationError("slug is required");
10853
- return reply.status(err.statusCode).send(err.toJSON());
10703
+ throw validationError("slug is required");
10854
10704
  }
10855
10705
  const env = parseEnvInput(request.query?.env);
10856
10706
  return getPageSchema(connection, slug, env);
10857
10707
  });
10858
10708
  });
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;
10709
+ app.post("/projects/:name/wordpress/schema/manual", async (request) => {
10710
+ return withWordpressErrorHandling(async () => {
10711
+ const store = requireStore();
10863
10712
  const project = resolveProject(app.db, request.params.name);
10864
- const connection = requireConnection(store, project.name, reply);
10865
- if (!connection) return;
10713
+ const connection = requireConnection(store, project.name);
10866
10714
  const slug = request.body?.slug?.trim();
10867
10715
  const json = request.body?.json;
10868
10716
  if (!slug || !json) {
10869
- const err = validationError("slug and json are required");
10870
- return reply.status(err.statusCode).send(err.toJSON());
10717
+ throw validationError("slug and json are required");
10871
10718
  }
10872
10719
  const env = parseEnvInput(request.body?.env);
10873
10720
  return buildManualSchemaUpdate(connection, slug, { type: request.body?.type, json }, env);
10874
10721
  });
10875
10722
  });
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;
10723
+ app.post("/projects/:name/wordpress/schema/deploy", async (request) => {
10724
+ return withWordpressErrorHandling(async () => {
10725
+ const store = requireStore();
10880
10726
  const project = resolveProject(app.db, request.params.name);
10881
- const connection = requireConnection(store, project.name, reply);
10882
- if (!connection) return;
10727
+ const connection = requireConnection(store, project.name);
10883
10728
  const profile = request.body?.profile;
10884
10729
  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());
10730
+ throw validationError("profile with business.name and non-empty pages is required");
10887
10731
  }
10888
10732
  const env = parseEnvInput(request.body?.env);
10889
10733
  return deploySchemaFromProfile(connection, profile, env);
10890
10734
  });
10891
10735
  });
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;
10736
+ app.get("/projects/:name/wordpress/schema/status", async (request) => {
10737
+ return withWordpressErrorHandling(async () => {
10738
+ const store = requireStore();
10896
10739
  const project = resolveProject(app.db, request.params.name);
10897
- const connection = requireConnection(store, project.name, reply);
10898
- if (!connection) return;
10740
+ const connection = requireConnection(store, project.name);
10899
10741
  const env = parseEnvInput(request.query?.env);
10900
10742
  return getSchemaStatus(connection, env);
10901
10743
  });
10902
10744
  });
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;
10745
+ app.get("/projects/:name/wordpress/llms-txt", async (request) => {
10746
+ return withWordpressErrorHandling(async () => {
10747
+ const store = requireStore();
10907
10748
  const project = resolveProject(app.db, request.params.name);
10908
- const connection = requireConnection(store, project.name, reply);
10909
- if (!connection) return;
10749
+ const connection = requireConnection(store, project.name);
10910
10750
  const env = parseEnvInput(request.query?.env);
10911
10751
  return getLlmsTxt(connection, env);
10912
10752
  });
10913
10753
  });
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;
10754
+ app.post("/projects/:name/wordpress/llms-txt/manual", async (request) => {
10755
+ return withWordpressErrorHandling(async () => {
10756
+ const store = requireStore();
10918
10757
  const project = resolveProject(app.db, request.params.name);
10919
- const connection = requireConnection(store, project.name, reply);
10920
- if (!connection) return;
10758
+ const connection = requireConnection(store, project.name);
10921
10759
  const content = request.body?.content;
10922
10760
  if (!content) {
10923
- const err = validationError("content is required");
10924
- return reply.status(err.statusCode).send(err.toJSON());
10761
+ throw validationError("content is required");
10925
10762
  }
10926
10763
  const env = parseEnvInput(request.body?.env);
10927
10764
  return buildManualLlmsTxtUpdate(connection, content, env);
10928
10765
  });
10929
10766
  });
10930
- app.get("/projects/:name/wordpress/audit", async (request, reply) => {
10931
- return withWordpressErrorHandling(reply, async () => {
10932
- const store = requireStore(reply);
10933
- if (!store) return;
10767
+ app.get("/projects/:name/wordpress/audit", async (request) => {
10768
+ return withWordpressErrorHandling(async () => {
10769
+ const store = requireStore();
10934
10770
  const project = resolveProject(app.db, request.params.name);
10935
- const connection = requireConnection(store, project.name, reply);
10936
- if (!connection) return;
10771
+ const connection = requireConnection(store, project.name);
10937
10772
  const env = parseEnvInput(request.query?.env);
10938
10773
  return runAudit(connection, env);
10939
10774
  });
10940
10775
  });
10941
- app.get("/projects/:name/wordpress/diff", async (request, reply) => {
10942
- return withWordpressErrorHandling(reply, async () => {
10943
- const store = requireStore(reply);
10944
- if (!store) return;
10776
+ app.get("/projects/:name/wordpress/diff", async (request) => {
10777
+ return withWordpressErrorHandling(async () => {
10778
+ const store = requireStore();
10945
10779
  const project = resolveProject(app.db, request.params.name);
10946
- const connection = requireConnection(store, project.name, reply);
10947
- if (!connection) return;
10780
+ const connection = requireConnection(store, project.name);
10948
10781
  const slug = request.query?.slug?.trim();
10949
10782
  if (!slug) {
10950
- const err = validationError("slug is required");
10951
- return reply.status(err.statusCode).send(err.toJSON());
10783
+ throw validationError("slug is required");
10952
10784
  }
10953
10785
  return diffPageAcrossEnvironments(connection, slug);
10954
10786
  });
10955
10787
  });
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;
10788
+ app.get("/projects/:name/wordpress/staging/status", async (request) => {
10789
+ return withWordpressErrorHandling(async () => {
10790
+ const store = requireStore();
10960
10791
  const project = resolveProject(app.db, request.params.name);
10961
- const connection = requireConnection(store, project.name, reply);
10962
- if (!connection) return;
10792
+ const connection = requireConnection(store, project.name);
10963
10793
  const plugins = await listActivePlugins(connection, "live");
10964
10794
  return {
10965
10795
  stagingConfigured: Boolean(connection.stagingUrl),
@@ -10969,34 +10799,28 @@ async function wordpressRoutes(app, opts) {
10969
10799
  };
10970
10800
  });
10971
10801
  });
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;
10802
+ app.post("/projects/:name/wordpress/staging/push", async (request) => {
10803
+ return withWordpressErrorHandling(async () => {
10804
+ const store = requireStore();
10976
10805
  const project = resolveProject(app.db, request.params.name);
10977
- const connection = requireConnection(store, project.name, reply);
10978
- if (!connection) return;
10806
+ const connection = requireConnection(store, project.name);
10979
10807
  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());
10808
+ throw validationError("No staging URL configured for this project. Reconnect with --staging-url before using staging push.");
10982
10809
  }
10983
10810
  return buildManualStagingPush(connection);
10984
10811
  });
10985
10812
  });
10986
- app.post("/projects/:name/wordpress/onboard", async (request, reply) => {
10987
- return withWordpressErrorHandling(reply, async () => {
10988
- const store = requireStore(reply);
10989
- if (!store) return;
10813
+ app.post("/projects/:name/wordpress/onboard", async (request) => {
10814
+ return withWordpressErrorHandling(async () => {
10815
+ const store = requireStore();
10990
10816
  const project = resolveProject(app.db, request.params.name);
10991
10817
  const { url, username, appPassword, stagingUrl, profile, skipSchema, skipSubmit } = request.body ?? {};
10992
10818
  if (!url || !username || !appPassword) {
10993
- const err = validationError("url, username, and appPassword are required");
10994
- return reply.status(err.statusCode).send(err.toJSON());
10819
+ throw validationError("url, username, and appPassword are required");
10995
10820
  }
10996
10821
  const defaultEnv = parseEnvInput(request.body?.defaultEnv, "defaultEnv") ?? (stagingUrl ? "staging" : "live");
10997
10822
  if (defaultEnv === "staging" && !stagingUrl) {
10998
- const err = validationError('defaultEnv "staging" requires stagingUrl');
10999
- return reply.status(err.statusCode).send(err.toJSON());
10823
+ throw validationError('defaultEnv "staging" requires stagingUrl');
11000
10824
  }
11001
10825
  const steps = [];
11002
10826
  let connection = null;
@@ -14543,6 +14367,26 @@ var ProviderExecutionGate = class {
14543
14367
  }
14544
14368
  }
14545
14369
  };
14370
+ async function runWithConcurrency(items, limit, worker) {
14371
+ if (items.length === 0) return;
14372
+ const cap = Math.max(1, Math.min(limit, items.length));
14373
+ let cursor = 0;
14374
+ const next = async () => {
14375
+ while (true) {
14376
+ const idx = cursor++;
14377
+ if (idx >= items.length) return;
14378
+ await worker(items[idx]);
14379
+ }
14380
+ };
14381
+ await Promise.all(Array.from({ length: cap }, next));
14382
+ }
14383
+ var PROVIDER_FANOUT_DEFAULT = 8;
14384
+ function resolveProviderFanout() {
14385
+ const raw = process.env.CANONRY_PROVIDER_FANOUT;
14386
+ if (!raw) return PROVIDER_FANOUT_DEFAULT;
14387
+ const parsed = Number.parseInt(raw, 10);
14388
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : PROVIDER_FANOUT_DEFAULT;
14389
+ }
14546
14390
  var JobRunner = class {
14547
14391
  db;
14548
14392
  registry;
@@ -14750,11 +14594,11 @@ var JobRunner = class {
14750
14594
  }
14751
14595
  }
14752
14596
  };
14753
- await Promise.all(apiProviders.map(async (registeredProvider) => {
14597
+ await runWithConcurrency(apiProviders, resolveProviderFanout(), async (registeredProvider) => {
14754
14598
  await Promise.all(projectKeywords.map(async (kw) => {
14755
14599
  await processKeywordForProvider(registeredProvider, kw);
14756
14600
  }));
14757
- }));
14601
+ });
14758
14602
  for (const registeredProvider of browserProviders) {
14759
14603
  for (const kw of projectKeywords) {
14760
14604
  await processKeywordForProvider(registeredProvider, kw);