@ainyc/canonry 2.3.1 → 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.
@@ -30,7 +30,7 @@ import {
30
30
  runs,
31
31
  schedules,
32
32
  usageCounters
33
- } from "./chunk-CW6CAPBQ.js";
33
+ } from "./chunk-GZF3YIHY.js";
34
34
 
35
35
  // src/config.ts
36
36
  import fs from "fs";
@@ -347,7 +347,7 @@ function printCliError(err, format) {
347
347
  // src/server.ts
348
348
  import { createRequire as createRequire3 } from "module";
349
349
  import crypto27 from "crypto";
350
- import fs12 from "fs";
350
+ import fs13 from "fs";
351
351
  import path15 from "path";
352
352
  import { fileURLToPath as fileURLToPath2 } from "url";
353
353
  import { eq as eq29 } from "drizzle-orm";
@@ -448,6 +448,7 @@ var projectUpsertRequestSchema = z3.object({
448
448
  providers: z3.array(providerNameSchema).optional(),
449
449
  locations: z3.array(locationContextSchema).optional(),
450
450
  defaultLocation: z3.string().nullable().optional(),
451
+ autoExtractBacklinks: z3.boolean().optional(),
451
452
  configSource: configSourceSchema.optional()
452
453
  });
453
454
  var projectDtoSchema = z3.object({
@@ -462,6 +463,7 @@ var projectDtoSchema = z3.object({
462
463
  labels: z3.record(z3.string(), z3.string()).default({}),
463
464
  locations: z3.array(locationContextSchema).default([]),
464
465
  defaultLocation: z3.string().nullable().optional(),
466
+ autoExtractBacklinks: z3.boolean().default(false),
465
467
  configSource: configSourceSchema.default("cli"),
466
468
  configRevision: z3.number().int().positive().default(1),
467
469
  createdAt: z3.string().optional(),
@@ -535,7 +537,8 @@ var configSpecSchema = z4.object({
535
537
  defaultLocation: z4.string().optional(),
536
538
  schedule: configScheduleSchema,
537
539
  notifications: z4.array(configNotificationSchema).optional().default([]),
538
- google: configGoogleSchema
540
+ google: configGoogleSchema,
541
+ autoExtractBacklinks: z4.boolean().optional().default(false)
539
542
  }).superRefine((spec, ctx) => {
540
543
  const duplicateLabels = findDuplicateLocationLabels(spec.locations);
541
544
  if (duplicateLabels.length > 0) {
@@ -622,6 +625,9 @@ function agentBusy(projectName) {
622
625
  function missingDependency(message, details) {
623
626
  return new AppError("MISSING_DEPENDENCY", message, 422, details);
624
627
  }
628
+ function internalError(message, details) {
629
+ return new AppError("INTERNAL_ERROR", message, 500, details);
630
+ }
625
631
 
626
632
  // ../contracts/src/google.ts
627
633
  import { z as z5 } from "zod";
@@ -1546,7 +1552,7 @@ function parseCookies(header) {
1546
1552
  }, {});
1547
1553
  }
1548
1554
  async function authPlugin(app, opts = {}) {
1549
- app.addHook("onRequest", async (request, reply) => {
1555
+ app.addHook("onRequest", async (request) => {
1550
1556
  const url = request.url.split("?")[0];
1551
1557
  if (shouldSkipAuth(url)) return;
1552
1558
  const header = request.headers.authorization;
@@ -1554,15 +1560,13 @@ async function authPlugin(app, opts = {}) {
1554
1560
  if (header) {
1555
1561
  const parts = header.split(" ");
1556
1562
  if (parts.length !== 2 || parts[0] !== "Bearer") {
1557
- const err = authRequired();
1558
- return reply.status(err.statusCode).send(err.toJSON());
1563
+ throw authRequired();
1559
1564
  }
1560
1565
  const token = parts[1];
1561
1566
  const hash = hashKey(token);
1562
1567
  key = app.db.select().from(apiKeys).where(eq(apiKeys.keyHash, hash)).get();
1563
1568
  if (!key || key.revokedAt) {
1564
- const err = authInvalid();
1565
- return reply.status(err.statusCode).send(err.toJSON());
1569
+ throw authInvalid();
1566
1570
  }
1567
1571
  } else if (opts.resolveSessionApiKeyId && opts.sessionCookieName) {
1568
1572
  const sessionId = parseCookies(request.headers.cookie)[opts.sessionCookieName];
@@ -1573,12 +1577,10 @@ async function authPlugin(app, opts = {}) {
1573
1577
  }
1574
1578
  }
1575
1579
  if (!key || key.revokedAt) {
1576
- const err = authRequired();
1577
- return reply.status(err.statusCode).send(err.toJSON());
1580
+ throw authRequired();
1578
1581
  }
1579
1582
  } else {
1580
- const err = authRequired();
1581
- return reply.status(err.statusCode).send(err.toJSON());
1583
+ throw authRequired();
1582
1584
  }
1583
1585
  app.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq(apiKeys.id, key.id)).run();
1584
1586
  });
@@ -1679,6 +1681,7 @@ async function projectRoutes(app, opts) {
1679
1681
  defaultLocation: nextDefaultLocation
1680
1682
  });
1681
1683
  }
1684
+ const nextAutoExtractBacklinks = body.autoExtractBacklinks !== void 0 ? body.autoExtractBacklinks ? 1 : 0 : existing?.autoExtractBacklinks ?? 0;
1682
1685
  if (existing) {
1683
1686
  app.db.transaction((tx) => {
1684
1687
  tx.update(projects).set({
@@ -1692,6 +1695,7 @@ async function projectRoutes(app, opts) {
1692
1695
  providers: JSON.stringify(body.providers ?? []),
1693
1696
  locations: JSON.stringify(nextLocations),
1694
1697
  defaultLocation: nextDefaultLocation,
1698
+ autoExtractBacklinks: nextAutoExtractBacklinks,
1695
1699
  configSource: body.configSource ?? "api",
1696
1700
  configRevision: existing.configRevision + 1,
1697
1701
  updatedAt: now
@@ -1723,6 +1727,7 @@ async function projectRoutes(app, opts) {
1723
1727
  providers: JSON.stringify(body.providers ?? []),
1724
1728
  locations: JSON.stringify(nextLocations),
1725
1729
  defaultLocation: nextDefaultLocation,
1730
+ autoExtractBacklinks: nextAutoExtractBacklinks,
1726
1731
  configSource: body.configSource ?? "api",
1727
1732
  configRevision: 1,
1728
1733
  createdAt: now,
@@ -1869,6 +1874,7 @@ async function projectRoutes(app, opts) {
1869
1874
  providers: parseJsonColumn(project.providers, []),
1870
1875
  locations: parseJsonColumn(project.locations, []),
1871
1876
  ...project.defaultLocation ? { defaultLocation: project.defaultLocation } : {},
1877
+ ...project.autoExtractBacklinks === 1 ? { autoExtractBacklinks: true } : {},
1872
1878
  notifications: notificationRows.map((row) => {
1873
1879
  const cfg = parseJsonColumn(row.config, { url: "", events: [] });
1874
1880
  return {
@@ -1903,6 +1909,7 @@ function formatProject(row) {
1903
1909
  providers: parseJsonColumn(row.providers, []),
1904
1910
  locations: parseJsonColumn(row.locations, []),
1905
1911
  defaultLocation: row.defaultLocation,
1912
+ autoExtractBacklinks: row.autoExtractBacklinks === 1,
1906
1913
  configSource: row.configSource,
1907
1914
  configRevision: row.configRevision,
1908
1915
  createdAt: row.createdAt,
@@ -2041,12 +2048,7 @@ async function keywordRoutes(app, opts) {
2041
2048
  return reply.send({ keywords: generated, provider });
2042
2049
  } catch (err) {
2043
2050
  request.log.error({ err }, "Key phrase generation failed");
2044
- return reply.status(500).send({
2045
- error: {
2046
- code: "INTERNAL_ERROR",
2047
- message: err instanceof Error ? err.message : "Failed to generate key phrases"
2048
- }
2049
- });
2051
+ throw internalError(err instanceof Error ? err.message : "Failed to generate key phrases");
2050
2052
  }
2051
2053
  });
2052
2054
  }
@@ -2742,6 +2744,7 @@ async function applyRoutes(app, opts) {
2742
2744
  providers: JSON.stringify(config.spec.providers ?? []),
2743
2745
  locations: JSON.stringify(config.spec.locations ?? []),
2744
2746
  defaultLocation: config.spec.defaultLocation ?? null,
2747
+ autoExtractBacklinks: config.spec.autoExtractBacklinks ? 1 : 0,
2745
2748
  configSource: "config-file",
2746
2749
  configRevision: existing.configRevision + 1,
2747
2750
  updatedAt: now
@@ -2768,6 +2771,7 @@ async function applyRoutes(app, opts) {
2768
2771
  providers: JSON.stringify(config.spec.providers ?? []),
2769
2772
  locations: JSON.stringify(config.spec.locations ?? []),
2770
2773
  defaultLocation: config.spec.defaultLocation ?? null,
2774
+ autoExtractBacklinks: config.spec.autoExtractBacklinks ? 1 : 0,
2771
2775
  configSource: "config-file",
2772
2776
  configRevision: 1,
2773
2777
  createdAt: now,
@@ -2891,6 +2895,7 @@ async function applyRoutes(app, opts) {
2891
2895
  providers: parseJsonColumn(project.providers, []),
2892
2896
  locations: parseJsonColumn(project.locations, []),
2893
2897
  defaultLocation: project.defaultLocation,
2898
+ autoExtractBacklinks: project.autoExtractBacklinks === 1,
2894
2899
  configSource: project.configSource,
2895
2900
  configRevision: project.configRevision,
2896
2901
  createdAt: project.createdAt,
@@ -6057,7 +6062,7 @@ async function settingsRoutes(app, opts) {
6057
6062
  google: opts.google ?? { configured: false },
6058
6063
  bing: opts.bing ?? { configured: false }
6059
6064
  }));
6060
- app.put("/settings/providers/:name", async (request, reply) => {
6065
+ app.put("/settings/providers/:name", async (request) => {
6061
6066
  const { apiKey, baseUrl, model, quota } = request.body ?? {};
6062
6067
  const name = request.params.name;
6063
6068
  const adapters = opts.providerAdapters ?? [];
@@ -6065,107 +6070,81 @@ async function settingsRoutes(app, opts) {
6065
6070
  const adapterInfo = apiAdapters.find((a) => a.name === name);
6066
6071
  if (!adapterInfo) {
6067
6072
  const validNames = apiAdapters.map((a) => a.name);
6068
- const err = validationError(`Invalid provider: ${name}. Must be one of: ${validNames.join(", ")}`, {
6073
+ throw validationError(`Invalid provider: ${name}. Must be one of: ${validNames.join(", ")}`, {
6069
6074
  provider: name,
6070
6075
  validProviders: validNames
6071
6076
  });
6072
- return reply.status(err.statusCode).send(err.toJSON());
6073
6077
  }
6074
6078
  if (name === "local") {
6075
6079
  if (!baseUrl || typeof baseUrl !== "string") {
6076
- const err = validationError("baseUrl is required for local provider");
6077
- return reply.status(err.statusCode).send(err.toJSON());
6080
+ throw validationError("baseUrl is required for local provider");
6078
6081
  }
6079
6082
  } else if (name === "gemini" && !apiKey) {
6080
6083
  const geminiSummary = (opts.providerSummary ?? []).find((p) => p.name === "gemini");
6081
6084
  if (!geminiSummary?.vertexConfigured) {
6082
- const err = validationError(
6085
+ throw validationError(
6083
6086
  "apiKey is required for Gemini unless Vertex AI is configured (set GEMINI_VERTEX_PROJECT env var or vertexProject in config file)"
6084
6087
  );
6085
- return reply.status(err.statusCode).send(err.toJSON());
6086
6088
  }
6087
6089
  } else {
6088
6090
  if (!apiKey || typeof apiKey !== "string") {
6089
- const err = validationError("apiKey is required");
6090
- return reply.status(err.statusCode).send(err.toJSON());
6091
+ throw validationError("apiKey is required");
6091
6092
  }
6092
6093
  }
6093
6094
  if (model !== void 0) {
6094
6095
  if (!adapterInfo.modelValidationPattern.test(model)) {
6095
- return reply.status(400).send({
6096
- error: { code: "VALIDATION_ERROR", message: `Invalid model "${model}" for provider "${name}" \u2014 ${adapterInfo.modelValidationHint}` }
6097
- });
6096
+ throw validationError(
6097
+ `Invalid model "${model}" for provider "${name}" \u2014 ${adapterInfo.modelValidationHint}`
6098
+ );
6098
6099
  }
6099
6100
  }
6100
6101
  if (!opts.onProviderUpdate) {
6101
- const err = notImplemented("Provider configuration updates are not supported in this deployment");
6102
- return reply.status(err.statusCode).send(err.toJSON());
6102
+ throw notImplemented("Provider configuration updates are not supported in this deployment");
6103
6103
  }
6104
6104
  if (quota !== void 0) {
6105
6105
  if (typeof quota !== "object" || quota === null) {
6106
- return reply.status(400).send({ error: { code: "VALIDATION_ERROR", message: "quota must be an object" } });
6106
+ throw validationError("quota must be an object");
6107
6107
  }
6108
6108
  for (const [key, val] of Object.entries(quota)) {
6109
6109
  if (!["maxConcurrency", "maxRequestsPerMinute", "maxRequestsPerDay"].includes(key)) {
6110
- return reply.status(400).send({ error: { code: "VALIDATION_ERROR", message: `Unknown quota field: ${key}` } });
6110
+ throw validationError(`Unknown quota field: ${key}`);
6111
6111
  }
6112
6112
  if (typeof val !== "number" || !Number.isInteger(val) || val <= 0) {
6113
- 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`);
6114
6114
  }
6115
6115
  }
6116
6116
  }
6117
6117
  const result = opts.onProviderUpdate(name, apiKey ?? "", model, baseUrl, quota);
6118
6118
  if (!result) {
6119
- return reply.status(500).send({
6120
- error: {
6121
- code: "INTERNAL_ERROR",
6122
- message: "Failed to update provider configuration"
6123
- }
6124
- });
6119
+ throw internalError("Failed to update provider configuration");
6125
6120
  }
6126
6121
  return result;
6127
6122
  });
6128
- app.put("/settings/google", async (request, reply) => {
6123
+ app.put("/settings/google", async (request) => {
6129
6124
  const { clientId, clientSecret } = request.body ?? {};
6130
6125
  if (!clientId || typeof clientId !== "string" || !clientSecret || typeof clientSecret !== "string") {
6131
- return reply.status(400).send({
6132
- error: { code: "VALIDATION_ERROR", message: "clientId and clientSecret are required" }
6133
- });
6126
+ throw validationError("clientId and clientSecret are required");
6134
6127
  }
6135
6128
  if (!opts.onGoogleUpdate) {
6136
- const err = notImplemented("Google OAuth configuration updates are not supported in this deployment");
6137
- return reply.status(err.statusCode).send(err.toJSON());
6129
+ throw notImplemented("Google OAuth configuration updates are not supported in this deployment");
6138
6130
  }
6139
6131
  const result = opts.onGoogleUpdate(clientId, clientSecret);
6140
6132
  if (!result) {
6141
- return reply.status(500).send({
6142
- error: {
6143
- code: "INTERNAL_ERROR",
6144
- message: "Failed to update Google OAuth configuration"
6145
- }
6146
- });
6133
+ throw internalError("Failed to update Google OAuth configuration");
6147
6134
  }
6148
6135
  return result;
6149
6136
  });
6150
- app.put("/settings/bing", async (request, reply) => {
6137
+ app.put("/settings/bing", async (request) => {
6151
6138
  const { apiKey } = request.body ?? {};
6152
6139
  if (!apiKey || typeof apiKey !== "string") {
6153
- return reply.status(400).send({
6154
- error: { code: "VALIDATION_ERROR", message: "apiKey is required" }
6155
- });
6140
+ throw validationError("apiKey is required");
6156
6141
  }
6157
6142
  if (!opts.onBingUpdate) {
6158
- const err = notImplemented("Bing configuration updates are not supported in this deployment");
6159
- return reply.status(err.statusCode).send(err.toJSON());
6143
+ throw notImplemented("Bing configuration updates are not supported in this deployment");
6160
6144
  }
6161
6145
  const result = opts.onBingUpdate(apiKey);
6162
6146
  if (!result) {
6163
- return reply.status(500).send({
6164
- error: {
6165
- code: "INTERNAL_ERROR",
6166
- message: "Failed to update Bing configuration"
6167
- }
6168
- });
6147
+ throw internalError("Failed to update Bing configuration");
6169
6148
  }
6170
6149
  return result;
6171
6150
  });
@@ -6173,32 +6152,24 @@ async function settingsRoutes(app, opts) {
6173
6152
 
6174
6153
  // ../api-routes/src/snapshot.ts
6175
6154
  async function snapshotRoutes(app, opts) {
6176
- app.post("/snapshot", async (request, reply) => {
6155
+ app.post("/snapshot", async (request) => {
6177
6156
  const parsed = snapshotRequestSchema.safeParse(request.body);
6178
6157
  if (!parsed.success) {
6179
- const err = validationError("Invalid snapshot payload", {
6158
+ throw validationError("Invalid snapshot payload", {
6180
6159
  issues: parsed.error.issues.map((issue) => ({
6181
6160
  path: issue.path.join("."),
6182
6161
  message: issue.message
6183
6162
  }))
6184
6163
  });
6185
- return reply.status(err.statusCode).send(err.toJSON());
6186
6164
  }
6187
6165
  if (!opts.onSnapshotRequested) {
6188
- const err = notImplemented("Snapshot reporting is not supported in this deployment");
6189
- return reply.status(err.statusCode).send(err.toJSON());
6166
+ throw notImplemented("Snapshot reporting is not supported in this deployment");
6190
6167
  }
6191
6168
  try {
6192
- const report = await opts.onSnapshotRequested(parsed.data);
6193
- return reply.send(report);
6169
+ return await opts.onSnapshotRequested(parsed.data);
6194
6170
  } catch (err) {
6195
6171
  request.log.error({ err }, "Snapshot report generation failed");
6196
- return reply.status(500).send({
6197
- error: {
6198
- code: "INTERNAL_ERROR",
6199
- message: err instanceof Error ? err.message : "Failed to generate snapshot report"
6200
- }
6201
- });
6172
+ throw internalError(err instanceof Error ? err.message : "Failed to generate snapshot report");
6202
6173
  }
6203
6174
  });
6204
6175
  }
@@ -7390,11 +7361,9 @@ async function googleRoutes(app, opts) {
7390
7361
  function getAuthConfig() {
7391
7362
  return opts.getGoogleAuthConfig?.() ?? {};
7392
7363
  }
7393
- function requireConnectionStore(reply) {
7364
+ function requireConnectionStore() {
7394
7365
  if (opts.googleConnectionStore) return opts.googleConnectionStore;
7395
- const err = validationError("Google auth storage is not configured for this deployment");
7396
- reply.status(err.statusCode).send(err.toJSON());
7397
- return null;
7366
+ throw validationError("Google auth storage is not configured for this deployment");
7398
7367
  }
7399
7368
  app.get("/projects/:name/google/connections", async (request) => {
7400
7369
  const project = resolveProject(app.db, request.params.name);
@@ -7410,16 +7379,14 @@ async function googleRoutes(app, opts) {
7410
7379
  updatedAt: connection.updatedAt
7411
7380
  }));
7412
7381
  });
7413
- app.post("/projects/:name/google/connect", async (request, reply) => {
7382
+ app.post("/projects/:name/google/connect", async (request) => {
7414
7383
  const { clientId: googleClientId, clientSecret: googleClientSecret } = getAuthConfig();
7415
7384
  if (!googleClientId || !googleClientSecret) {
7416
- const err = validationError("Google OAuth is not configured. Set Google OAuth credentials in the local Canonry config.");
7417
- 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.");
7418
7386
  }
7419
7387
  const { type, propertyId, publicUrl } = request.body ?? {};
7420
7388
  if (!type || type !== "gsc" && type !== "ga4") {
7421
- const err = validationError('type must be "gsc" or "ga4"');
7422
- return reply.status(err.statusCode).send(err.toJSON());
7389
+ throw validationError('type must be "gsc" or "ga4"');
7423
7390
  }
7424
7391
  const project = resolveProject(app.db, request.params.name);
7425
7392
  let redirectUri;
@@ -7445,8 +7412,7 @@ async function googleRoutes(app, opts) {
7445
7412
  if (!googleClientId || !googleClientSecret) {
7446
7413
  return reply.status(500).send("Google OAuth not configured");
7447
7414
  }
7448
- const store = requireConnectionStore(reply);
7449
- if (!store) return;
7415
+ const store = requireConnectionStore();
7450
7416
  const escapeHtml = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
7451
7417
  const { code, state, error } = request.query;
7452
7418
  if (error) {
@@ -7536,13 +7502,11 @@ async function googleRoutes(app, opts) {
7536
7502
  return handleOAuthCallback(request, reply);
7537
7503
  });
7538
7504
  app.delete("/projects/:name/google/connections/:type", async (request, reply) => {
7539
- const store = requireConnectionStore(reply);
7540
- if (!store) return;
7505
+ const store = requireConnectionStore();
7541
7506
  const project = resolveProject(app.db, request.params.name);
7542
7507
  const deleted = store.deleteConnection(project.canonicalDomain, request.params.type);
7543
7508
  if (!deleted) {
7544
- const err = notFound("Google connection", request.params.type);
7545
- return reply.status(err.statusCode).send(err.toJSON());
7509
+ throw notFound("Google connection", request.params.type);
7546
7510
  }
7547
7511
  writeAuditLog(app.db, {
7548
7512
  projectId: project.id,
@@ -7553,27 +7517,23 @@ async function googleRoutes(app, opts) {
7553
7517
  });
7554
7518
  return reply.status(204).send();
7555
7519
  });
7556
- app.get("/projects/:name/google/properties", async (request, reply) => {
7520
+ app.get("/projects/:name/google/properties", async (request) => {
7557
7521
  const { clientId: googleClientId, clientSecret: googleClientSecret } = getAuthConfig();
7558
7522
  if (!googleClientId || !googleClientSecret) {
7559
- const err = validationError("Google OAuth is not configured");
7560
- return reply.status(err.statusCode).send(err.toJSON());
7523
+ throw validationError("Google OAuth is not configured");
7561
7524
  }
7562
- const store = requireConnectionStore(reply);
7563
- if (!store) return;
7525
+ const store = requireConnectionStore();
7564
7526
  const project = resolveProject(app.db, request.params.name);
7565
7527
  const { accessToken } = await getValidToken(store, project.canonicalDomain, "gsc", googleClientId, googleClientSecret);
7566
7528
  const sites = await listSites(accessToken);
7567
7529
  return { sites };
7568
7530
  });
7569
- app.post("/projects/:name/google/gsc/sync", async (request, reply) => {
7570
- const store = requireConnectionStore(reply);
7571
- if (!store) return;
7531
+ app.post("/projects/:name/google/gsc/sync", async (request) => {
7532
+ const store = requireConnectionStore();
7572
7533
  const project = resolveProject(app.db, request.params.name);
7573
7534
  const conn = store.getConnection(project.canonicalDomain, "gsc");
7574
7535
  if (!conn) {
7575
- const err = validationError('No GSC connection found for this domain. Run "canonry google connect" first.');
7576
- return reply.status(err.statusCode).send(err.toJSON());
7536
+ throw validationError('No GSC connection found for this domain. Run "canonry google connect" first.');
7577
7537
  }
7578
7538
  const now = (/* @__PURE__ */ new Date()).toISOString();
7579
7539
  const runId = crypto14.randomUUID();
@@ -7615,24 +7575,20 @@ async function googleRoutes(app, opts) {
7615
7575
  position: parseFloat(r.position)
7616
7576
  }));
7617
7577
  });
7618
- app.post("/projects/:name/google/gsc/inspect", async (request, reply) => {
7578
+ app.post("/projects/:name/google/gsc/inspect", async (request) => {
7619
7579
  const { clientId: googleClientId, clientSecret: googleClientSecret } = getAuthConfig();
7620
7580
  if (!googleClientId || !googleClientSecret) {
7621
- const err = validationError("Google OAuth is not configured");
7622
- return reply.status(err.statusCode).send(err.toJSON());
7581
+ throw validationError("Google OAuth is not configured");
7623
7582
  }
7624
- const store = requireConnectionStore(reply);
7625
- if (!store) return;
7583
+ const store = requireConnectionStore();
7626
7584
  const project = resolveProject(app.db, request.params.name);
7627
7585
  const { url } = request.body ?? {};
7628
7586
  if (!url) {
7629
- const err = validationError("url is required");
7630
- return reply.status(err.statusCode).send(err.toJSON());
7587
+ throw validationError("url is required");
7631
7588
  }
7632
7589
  const { accessToken, propertyId } = await getValidToken(store, project.canonicalDomain, "gsc", googleClientId, googleClientSecret);
7633
7590
  if (!propertyId) {
7634
- const err = validationError("No GSC property configured for this connection");
7635
- return reply.status(err.statusCode).send(err.toJSON());
7591
+ throw validationError("No GSC property configured for this connection");
7636
7592
  }
7637
7593
  const result = await inspectUrl(accessToken, url, propertyId);
7638
7594
  const ir = result.inspectionResult;
@@ -7832,46 +7788,38 @@ async function googleRoutes(app, opts) {
7832
7788
  reasonBreakdown: JSON.parse(r.reasonBreakdown)
7833
7789
  })).reverse();
7834
7790
  });
7835
- app.get("/projects/:name/google/gsc/sitemaps", async (request, reply) => {
7791
+ app.get("/projects/:name/google/gsc/sitemaps", async (request) => {
7836
7792
  const { clientId: googleClientId, clientSecret: googleClientSecret } = getAuthConfig();
7837
7793
  if (!googleClientId || !googleClientSecret) {
7838
- const err = validationError("Google OAuth is not configured");
7839
- return reply.status(err.statusCode).send(err.toJSON());
7794
+ throw validationError("Google OAuth is not configured");
7840
7795
  }
7841
- const store = requireConnectionStore(reply);
7842
- if (!store) return;
7796
+ const store = requireConnectionStore();
7843
7797
  const project = resolveProject(app.db, request.params.name);
7844
7798
  const { accessToken, propertyId } = await getValidToken(store, project.canonicalDomain, "gsc", googleClientId, googleClientSecret);
7845
7799
  if (!propertyId) {
7846
- const err = validationError('No GSC property configured for this connection. Set one with "canonry google set-property".');
7847
- 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".');
7848
7801
  }
7849
7802
  const sitemaps = await listSitemaps(accessToken, propertyId);
7850
7803
  return { sitemaps };
7851
7804
  });
7852
- app.post("/projects/:name/google/gsc/discover-sitemaps", async (request, reply) => {
7805
+ app.post("/projects/:name/google/gsc/discover-sitemaps", async (request) => {
7853
7806
  const { clientId: googleClientId, clientSecret: googleClientSecret } = getAuthConfig();
7854
7807
  if (!googleClientId || !googleClientSecret) {
7855
- const err = validationError("Google OAuth is not configured");
7856
- return reply.status(err.statusCode).send(err.toJSON());
7808
+ throw validationError("Google OAuth is not configured");
7857
7809
  }
7858
- const store = requireConnectionStore(reply);
7859
- if (!store) return;
7810
+ const store = requireConnectionStore();
7860
7811
  const project = resolveProject(app.db, request.params.name);
7861
7812
  const conn = store.getConnection(project.canonicalDomain, "gsc");
7862
7813
  if (!conn) {
7863
- const err = validationError('No GSC connection found for this domain. Run "canonry google connect" first.');
7864
- return reply.status(err.statusCode).send(err.toJSON());
7814
+ throw validationError('No GSC connection found for this domain. Run "canonry google connect" first.');
7865
7815
  }
7866
7816
  if (!conn.propertyId) {
7867
- const err = validationError("No GSC property configured for this connection");
7868
- return reply.status(err.statusCode).send(err.toJSON());
7817
+ throw validationError("No GSC property configured for this connection");
7869
7818
  }
7870
7819
  const { accessToken } = await getValidToken(store, project.canonicalDomain, "gsc", googleClientId, googleClientSecret);
7871
7820
  const sitemaps = await listSitemaps(accessToken, conn.propertyId);
7872
7821
  if (sitemaps.length === 0) {
7873
- const err = validationError("No sitemaps found for this GSC property. Submit a sitemap in Google Search Console first.");
7874
- 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.");
7875
7823
  }
7876
7824
  const primary = sitemaps.find((s) => !s.isSitemapsIndex) ?? sitemaps[0];
7877
7825
  const sitemapUrl = primary.path;
@@ -7895,18 +7843,15 @@ async function googleRoutes(app, opts) {
7895
7843
  const run = app.db.select().from(runs).where(eq14(runs.id, runId)).get();
7896
7844
  return { sitemaps, primarySitemapUrl: sitemapUrl, run };
7897
7845
  });
7898
- app.post("/projects/:name/google/gsc/inspect-sitemap", async (request, reply) => {
7899
- const store = requireConnectionStore(reply);
7900
- if (!store) return;
7846
+ app.post("/projects/:name/google/gsc/inspect-sitemap", async (request) => {
7847
+ const store = requireConnectionStore();
7901
7848
  const project = resolveProject(app.db, request.params.name);
7902
7849
  const conn = store.getConnection(project.canonicalDomain, "gsc");
7903
7850
  if (!conn) {
7904
- const err = validationError('No GSC connection found for this domain. Run "canonry google connect" first.');
7905
- return reply.status(err.statusCode).send(err.toJSON());
7851
+ throw validationError('No GSC connection found for this domain. Run "canonry google connect" first.');
7906
7852
  }
7907
7853
  if (!conn.propertyId) {
7908
- const err = validationError("No GSC property configured for this connection");
7909
- return reply.status(err.statusCode).send(err.toJSON());
7854
+ throw validationError("No GSC property configured for this connection");
7910
7855
  }
7911
7856
  const now = (/* @__PURE__ */ new Date()).toISOString();
7912
7857
  const runId = crypto14.randomUUID();
@@ -7925,14 +7870,12 @@ async function googleRoutes(app, opts) {
7925
7870
  const run = app.db.select().from(runs).where(eq14(runs.id, runId)).get();
7926
7871
  return run;
7927
7872
  });
7928
- app.put("/projects/:name/google/connections/:type/sitemap", async (request, reply) => {
7929
- const store = requireConnectionStore(reply);
7930
- if (!store) return;
7873
+ app.put("/projects/:name/google/connections/:type/sitemap", async (request) => {
7874
+ const store = requireConnectionStore();
7931
7875
  const project = resolveProject(app.db, request.params.name);
7932
7876
  const { sitemapUrl } = request.body ?? {};
7933
7877
  if (!sitemapUrl || !sitemapUrl.trim()) {
7934
- const err = validationError("sitemapUrl is required");
7935
- return reply.status(err.statusCode).send(err.toJSON());
7878
+ throw validationError("sitemapUrl is required");
7936
7879
  }
7937
7880
  const conn = store.updateConnection(
7938
7881
  project.canonicalDomain,
@@ -7940,19 +7883,16 @@ async function googleRoutes(app, opts) {
7940
7883
  { sitemapUrl: sitemapUrl.trim(), updatedAt: (/* @__PURE__ */ new Date()).toISOString() }
7941
7884
  );
7942
7885
  if (!conn) {
7943
- const err = notFound("Google connection", request.params.type);
7944
- return reply.status(err.statusCode).send(err.toJSON());
7886
+ throw notFound("Google connection", request.params.type);
7945
7887
  }
7946
7888
  return { sitemapUrl: sitemapUrl.trim() };
7947
7889
  });
7948
- app.put("/projects/:name/google/connections/:type/property", async (request, reply) => {
7949
- const store = requireConnectionStore(reply);
7950
- if (!store) return;
7890
+ app.put("/projects/:name/google/connections/:type/property", async (request) => {
7891
+ const store = requireConnectionStore();
7951
7892
  const project = resolveProject(app.db, request.params.name);
7952
7893
  const { propertyId } = request.body ?? {};
7953
7894
  if (!propertyId) {
7954
- const err = validationError("propertyId is required");
7955
- return reply.status(err.statusCode).send(err.toJSON());
7895
+ throw validationError("propertyId is required");
7956
7896
  }
7957
7897
  const conn = store.updateConnection(
7958
7898
  project.canonicalDomain,
@@ -7960,19 +7900,16 @@ async function googleRoutes(app, opts) {
7960
7900
  { propertyId, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }
7961
7901
  );
7962
7902
  if (!conn) {
7963
- const err = notFound("Google connection", request.params.type);
7964
- return reply.status(err.statusCode).send(err.toJSON());
7903
+ throw notFound("Google connection", request.params.type);
7965
7904
  }
7966
7905
  return { propertyId };
7967
7906
  });
7968
- app.post("/projects/:name/google/indexing/request", async (request, reply) => {
7907
+ app.post("/projects/:name/google/indexing/request", async (request) => {
7969
7908
  const { clientId: googleClientId, clientSecret: googleClientSecret } = getAuthConfig();
7970
7909
  if (!googleClientId || !googleClientSecret) {
7971
- const err = validationError("Google OAuth is not configured");
7972
- return reply.status(err.statusCode).send(err.toJSON());
7910
+ throw validationError("Google OAuth is not configured");
7973
7911
  }
7974
- const store = requireConnectionStore(reply);
7975
- if (!store) return;
7912
+ const store = requireConnectionStore();
7976
7913
  const project = resolveProject(app.db, request.params.name);
7977
7914
  const { accessToken } = await getValidToken(store, project.canonicalDomain, "gsc", googleClientId, googleClientSecret);
7978
7915
  let urlsToNotify = request.body?.urls ?? [];
@@ -7991,18 +7928,15 @@ async function googleRoutes(app, opts) {
7991
7928
  }
7992
7929
  }
7993
7930
  if (unindexedUrls.length === 0) {
7994
- const err = validationError('No unindexed URLs found. Run "canonry google inspect-sitemap" first.');
7995
- return reply.status(err.statusCode).send(err.toJSON());
7931
+ throw validationError('No unindexed URLs found. Run "canonry google inspect-sitemap" first.');
7996
7932
  }
7997
7933
  urlsToNotify = unindexedUrls;
7998
7934
  }
7999
7935
  if (urlsToNotify.length === 0) {
8000
- const err = validationError("At least one URL is required (or use allUnindexed: true)");
8001
- return reply.status(err.statusCode).send(err.toJSON());
7936
+ throw validationError("At least one URL is required (or use allUnindexed: true)");
8002
7937
  }
8003
7938
  if (urlsToNotify.length > INDEXING_API_DAILY_LIMIT) {
8004
- const err = validationError(`Cannot request indexing for more than ${INDEXING_API_DAILY_LIMIT} URLs per request (got ${urlsToNotify.length})`);
8005
- 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})`);
8006
7940
  }
8007
7941
  const projectDomain = normalizeProjectDomain(project.canonicalDomain);
8008
7942
  const invalidUrls = urlsToNotify.filter((url) => {
@@ -8014,10 +7948,9 @@ async function googleRoutes(app, opts) {
8014
7948
  }
8015
7949
  });
8016
7950
  if (invalidUrls.length > 0) {
8017
- const err = validationError(
7951
+ throw validationError(
8018
7952
  `URLs must belong to project domain "${project.canonicalDomain}". Invalid: ${invalidUrls.slice(0, 5).join(", ")}`
8019
7953
  );
8020
- return reply.status(err.statusCode).send(err.toJSON());
8021
7954
  }
8022
7955
  const results = [];
8023
7956
  for (const url of urlsToNotify) {
@@ -8231,28 +8164,22 @@ function bingLog(level, action, ctx) {
8231
8164
  stream.write(JSON.stringify(entry) + "\n");
8232
8165
  }
8233
8166
  async function bingRoutes(app, opts) {
8234
- function requireConnectionStore(reply) {
8167
+ function requireConnectionStore() {
8235
8168
  if (opts.bingConnectionStore) return opts.bingConnectionStore;
8236
- const err = validationError("Bing connection storage is not configured for this deployment");
8237
- reply.status(err.statusCode).send(err.toJSON());
8238
- return null;
8169
+ throw validationError("Bing connection storage is not configured for this deployment");
8239
8170
  }
8240
- function requireConnection(store, domain, reply) {
8171
+ function requireConnection(store, domain) {
8241
8172
  const conn = store.getConnection(domain);
8242
8173
  if (!conn) {
8243
- const err = validationError('No Bing connection found for this domain. Run "canonry bing connect <project>" first.');
8244
- reply.status(err.statusCode).send(err.toJSON());
8245
- return null;
8174
+ throw validationError('No Bing connection found for this domain. Run "canonry bing connect <project>" first.');
8246
8175
  }
8247
8176
  return conn;
8248
8177
  }
8249
- app.post("/projects/:name/bing/connect", async (request, reply) => {
8250
- const store = requireConnectionStore(reply);
8251
- if (!store) return;
8178
+ app.post("/projects/:name/bing/connect", async (request) => {
8179
+ const store = requireConnectionStore();
8252
8180
  const { apiKey } = request.body ?? {};
8253
8181
  if (!apiKey || typeof apiKey !== "string") {
8254
- const err = validationError("apiKey is required");
8255
- return reply.status(err.statusCode).send(err.toJSON());
8182
+ throw validationError("apiKey is required");
8256
8183
  }
8257
8184
  const project = resolveProject(app.db, request.params.name);
8258
8185
  let sites;
@@ -8262,8 +8189,7 @@ async function bingRoutes(app, opts) {
8262
8189
  } catch (e) {
8263
8190
  const msg = e instanceof Error ? e.message : String(e);
8264
8191
  bingLog("error", "connect.verify-key-failed", { domain: project.canonicalDomain, error: msg });
8265
- const err = validationError(`Failed to verify Bing API key: ${msg}`);
8266
- return reply.status(err.statusCode).send(err.toJSON());
8192
+ throw validationError(`Failed to verify Bing API key: ${msg}`);
8267
8193
  }
8268
8194
  const now = (/* @__PURE__ */ new Date()).toISOString();
8269
8195
  const existing = store.getConnection(project.canonicalDomain);
@@ -8289,13 +8215,11 @@ async function bingRoutes(app, opts) {
8289
8215
  };
8290
8216
  });
8291
8217
  app.delete("/projects/:name/bing/disconnect", async (request, reply) => {
8292
- const store = requireConnectionStore(reply);
8293
- if (!store) return;
8218
+ const store = requireConnectionStore();
8294
8219
  const project = resolveProject(app.db, request.params.name);
8295
8220
  const deleted = store.deleteConnection(project.canonicalDomain);
8296
8221
  if (!deleted) {
8297
- const err = notFound("Bing connection", project.canonicalDomain);
8298
- return reply.status(err.statusCode).send(err.toJSON());
8222
+ throw notFound("Bing connection", project.canonicalDomain);
8299
8223
  }
8300
8224
  writeAuditLog(app.db, {
8301
8225
  projectId: project.id,
@@ -8306,9 +8230,8 @@ async function bingRoutes(app, opts) {
8306
8230
  });
8307
8231
  return reply.status(204).send();
8308
8232
  });
8309
- app.get("/projects/:name/bing/status", async (request, reply) => {
8310
- const store = requireConnectionStore(reply);
8311
- if (!store) return;
8233
+ app.get("/projects/:name/bing/status", async (request) => {
8234
+ const store = requireConnectionStore();
8312
8235
  const project = resolveProject(app.db, request.params.name);
8313
8236
  const conn = store.getConnection(project.canonicalDomain);
8314
8237
  return {
@@ -8319,25 +8242,20 @@ async function bingRoutes(app, opts) {
8319
8242
  updatedAt: conn?.updatedAt ?? null
8320
8243
  };
8321
8244
  });
8322
- app.get("/projects/:name/bing/sites", async (request, reply) => {
8323
- const store = requireConnectionStore(reply);
8324
- if (!store) return;
8245
+ app.get("/projects/:name/bing/sites", async (request) => {
8246
+ const store = requireConnectionStore();
8325
8247
  const project = resolveProject(app.db, request.params.name);
8326
- const conn = requireConnection(store, project.canonicalDomain, reply);
8327
- if (!conn) return;
8248
+ const conn = requireConnection(store, project.canonicalDomain);
8328
8249
  const sites = await getSites(conn.apiKey);
8329
8250
  return { sites: sites.map((s) => ({ url: s.Url, verified: s.Verified ?? false })) };
8330
8251
  });
8331
- app.post("/projects/:name/bing/set-site", async (request, reply) => {
8332
- const store = requireConnectionStore(reply);
8333
- if (!store) return;
8252
+ app.post("/projects/:name/bing/set-site", async (request) => {
8253
+ const store = requireConnectionStore();
8334
8254
  const project = resolveProject(app.db, request.params.name);
8335
- const conn = requireConnection(store, project.canonicalDomain, reply);
8336
- if (!conn) return;
8255
+ requireConnection(store, project.canonicalDomain);
8337
8256
  const { siteUrl } = request.body ?? {};
8338
8257
  if (!siteUrl || typeof siteUrl !== "string") {
8339
- const err = validationError("siteUrl is required");
8340
- return reply.status(err.statusCode).send(err.toJSON());
8258
+ throw validationError("siteUrl is required");
8341
8259
  }
8342
8260
  store.updateConnection(project.canonicalDomain, {
8343
8261
  siteUrl,
@@ -8345,12 +8263,10 @@ async function bingRoutes(app, opts) {
8345
8263
  });
8346
8264
  return { siteUrl };
8347
8265
  });
8348
- app.get("/projects/:name/bing/coverage", async (request, reply) => {
8349
- const store = requireConnectionStore(reply);
8350
- if (!store) return;
8266
+ app.get("/projects/:name/bing/coverage", async (request) => {
8267
+ const store = requireConnectionStore();
8351
8268
  const project = resolveProject(app.db, request.params.name);
8352
- const conn = requireConnection(store, project.canonicalDomain, reply);
8353
- if (!conn) return;
8269
+ requireConnection(store, project.canonicalDomain);
8354
8270
  const allInspections = app.db.select().from(bingUrlInspections).where(eq15(bingUrlInspections.projectId, project.id)).orderBy(desc6(bingUrlInspections.inspectedAt)).all();
8355
8271
  const latestByUrl = /* @__PURE__ */ new Map();
8356
8272
  const definitiveByUrl = /* @__PURE__ */ new Map();
@@ -8435,9 +8351,8 @@ async function bingRoutes(app, opts) {
8435
8351
  unknown: unknownUrls.map(formatRow)
8436
8352
  };
8437
8353
  });
8438
- app.get("/projects/:name/bing/coverage/history", async (request, reply) => {
8439
- const store = requireConnectionStore(reply);
8440
- if (!store) return;
8354
+ app.get("/projects/:name/bing/coverage/history", async (request) => {
8355
+ requireConnectionStore();
8441
8356
  const project = resolveProject(app.db, request.params.name);
8442
8357
  const parsed = parseInt(request.query.limit ?? "90", 10);
8443
8358
  const limit = Number.isNaN(parsed) || parsed <= 0 ? 90 : parsed;
@@ -8449,9 +8364,8 @@ async function bingRoutes(app, opts) {
8449
8364
  unknown: r.unknown
8450
8365
  }));
8451
8366
  });
8452
- app.get("/projects/:name/bing/inspections", async (request, reply) => {
8453
- const store = requireConnectionStore(reply);
8454
- if (!store) return;
8367
+ app.get("/projects/:name/bing/inspections", async (request) => {
8368
+ requireConnectionStore();
8455
8369
  const project = resolveProject(app.db, request.params.name);
8456
8370
  const { url, limit } = request.query;
8457
8371
  const whereClause = url ? and4(eq15(bingUrlInspections.projectId, project.id), eq15(bingUrlInspections.url, url)) : eq15(bingUrlInspections.projectId, project.id);
@@ -8469,20 +8383,16 @@ async function bingRoutes(app, opts) {
8469
8383
  discoveryDate: r.discoveryDate ?? null
8470
8384
  }));
8471
8385
  });
8472
- app.post("/projects/:name/bing/inspect-url", async (request, reply) => {
8473
- const store = requireConnectionStore(reply);
8474
- if (!store) return;
8386
+ app.post("/projects/:name/bing/inspect-url", async (request) => {
8387
+ const store = requireConnectionStore();
8475
8388
  const project = resolveProject(app.db, request.params.name);
8476
- const conn = requireConnection(store, project.canonicalDomain, reply);
8477
- if (!conn) return;
8389
+ const conn = requireConnection(store, project.canonicalDomain);
8478
8390
  if (!conn.siteUrl) {
8479
- const err = validationError('No Bing site configured. Run "canonry bing set-site <project> <url>" first.');
8480
- return reply.status(err.statusCode).send(err.toJSON());
8391
+ throw validationError('No Bing site configured. Run "canonry bing set-site <project> <url>" first.');
8481
8392
  }
8482
8393
  const { url } = request.body ?? {};
8483
8394
  if (!url) {
8484
- const err = validationError("url is required");
8485
- return reply.status(err.statusCode).send(err.toJSON());
8395
+ throw validationError("url is required");
8486
8396
  }
8487
8397
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
8488
8398
  const runId = crypto15.randomUUID();
@@ -8552,15 +8462,12 @@ async function bingRoutes(app, opts) {
8552
8462
  throw e;
8553
8463
  }
8554
8464
  });
8555
- app.post("/projects/:name/bing/request-indexing", async (request, reply) => {
8556
- const store = requireConnectionStore(reply);
8557
- if (!store) return;
8465
+ app.post("/projects/:name/bing/request-indexing", async (request) => {
8466
+ const store = requireConnectionStore();
8558
8467
  const project = resolveProject(app.db, request.params.name);
8559
- const conn = requireConnection(store, project.canonicalDomain, reply);
8560
- if (!conn) return;
8468
+ const conn = requireConnection(store, project.canonicalDomain);
8561
8469
  if (!conn.siteUrl) {
8562
- const err = validationError('No Bing site configured. Run "canonry bing set-site <project> <url>" first.');
8563
- return reply.status(err.statusCode).send(err.toJSON());
8470
+ throw validationError('No Bing site configured. Run "canonry bing set-site <project> <url>" first.');
8564
8471
  }
8565
8472
  let urlsToSubmit = request.body?.urls ?? [];
8566
8473
  if (request.body?.allUnindexed) {
@@ -8578,18 +8485,15 @@ async function bingRoutes(app, opts) {
8578
8485
  }
8579
8486
  }
8580
8487
  if (unindexedUrls.length === 0) {
8581
- const err = validationError('No unindexed or unknown URLs found. Run "canonry bing inspect <project> <url>" first.');
8582
- return reply.status(err.statusCode).send(err.toJSON());
8488
+ throw validationError('No unindexed or unknown URLs found. Run "canonry bing inspect <project> <url>" first.');
8583
8489
  }
8584
8490
  urlsToSubmit = unindexedUrls;
8585
8491
  }
8586
8492
  if (urlsToSubmit.length === 0) {
8587
- const err = validationError("At least one URL is required (or use allUnindexed: true)");
8588
- return reply.status(err.statusCode).send(err.toJSON());
8493
+ throw validationError("At least one URL is required (or use allUnindexed: true)");
8589
8494
  }
8590
8495
  if (urlsToSubmit.length > BING_SUBMIT_URL_DAILY_LIMIT) {
8591
- const err = validationError(`Cannot submit more than ${BING_SUBMIT_URL_DAILY_LIMIT} URLs per day (got ${urlsToSubmit.length})`);
8592
- 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})`);
8593
8497
  }
8594
8498
  const results = [];
8595
8499
  bingLog("info", "index-submit.start", { domain: project.canonicalDomain, siteUrl: conn.siteUrl, urlCount: urlsToSubmit.length, allUnindexed: !!request.body?.allUnindexed });
@@ -8632,15 +8536,12 @@ async function bingRoutes(app, opts) {
8632
8536
  results
8633
8537
  };
8634
8538
  });
8635
- app.get("/projects/:name/bing/performance", async (request, reply) => {
8636
- const store = requireConnectionStore(reply);
8637
- if (!store) return;
8539
+ app.get("/projects/:name/bing/performance", async (request) => {
8540
+ const store = requireConnectionStore();
8638
8541
  const project = resolveProject(app.db, request.params.name);
8639
- const conn = requireConnection(store, project.canonicalDomain, reply);
8640
- if (!conn) return;
8542
+ const conn = requireConnection(store, project.canonicalDomain);
8641
8543
  if (!conn.siteUrl) {
8642
- const err = validationError('No Bing site configured. Run "canonry bing set-site <project> <url>" first.');
8643
- return reply.status(err.statusCode).send(err.toJSON());
8544
+ throw validationError('No Bing site configured. Run "canonry bing set-site <project> <url>" first.');
8644
8545
  }
8645
8546
  const stats = await getKeywordStats(conn.apiKey, conn.siteUrl);
8646
8547
  return stats.map((s) => ({
@@ -10547,68 +10448,51 @@ function parseEnvInput(value, fieldName = "env") {
10547
10448
  }
10548
10449
  return env;
10549
10450
  }
10550
- function sendWordpressError(reply, error) {
10551
- if (!(error instanceof WordpressApiError)) return false;
10552
- let appError;
10451
+ function toAppError(error) {
10553
10452
  switch (error.code) {
10554
10453
  case "AUTH_INVALID":
10555
- appError = new AppError("AUTH_INVALID", error.message, error.statusCode);
10556
- break;
10454
+ return new AppError("AUTH_INVALID", error.message, error.statusCode);
10557
10455
  case "NOT_FOUND":
10558
- appError = new AppError("NOT_FOUND", error.message, error.statusCode);
10559
- break;
10560
- case "UPSTREAM_ERROR":
10561
- appError = providerError(error.message, { statusCode: error.statusCode });
10562
- break;
10456
+ return new AppError("NOT_FOUND", error.message, error.statusCode);
10563
10457
  case "UNSUPPORTED":
10564
10458
  case "VALIDATION_ERROR":
10565
- appError = validationError(error.message);
10566
- break;
10459
+ return validationError(error.message);
10460
+ case "UPSTREAM_ERROR":
10567
10461
  default:
10568
- appError = providerError(error.message, { statusCode: error.statusCode });
10569
- break;
10462
+ return providerError(error.message, { statusCode: error.statusCode });
10570
10463
  }
10571
- reply.status(appError.statusCode).send(appError.toJSON());
10572
- return true;
10573
10464
  }
10574
- async function withWordpressErrorHandling(reply, handler) {
10465
+ async function withWordpressErrorHandling(handler) {
10575
10466
  try {
10576
10467
  return await handler();
10577
10468
  } catch (error) {
10578
- if (sendWordpressError(reply, error)) return;
10469
+ if (error instanceof WordpressApiError) throw toAppError(error);
10579
10470
  throw error;
10580
10471
  }
10581
10472
  }
10582
10473
  async function wordpressRoutes(app, opts) {
10583
- function requireStore(reply) {
10474
+ function requireStore() {
10584
10475
  if (opts.wordpressConnectionStore) return opts.wordpressConnectionStore;
10585
- const err = validationError("WordPress connection storage is not configured for this deployment");
10586
- reply.status(err.statusCode).send(err.toJSON());
10587
- return null;
10476
+ throw validationError("WordPress connection storage is not configured for this deployment");
10588
10477
  }
10589
- function requireConnection(store, projectName, reply) {
10478
+ function requireConnection(store, projectName) {
10590
10479
  const connection = store.getConnection(projectName);
10591
10480
  if (!connection) {
10592
- const err = validationError(`No WordPress connection found for project "${projectName}". Run "canonry wordpress connect ${projectName}" first.`);
10593
- reply.status(err.statusCode).send(err.toJSON());
10594
- return null;
10481
+ throw validationError(`No WordPress connection found for project "${projectName}". Run "canonry wordpress connect ${projectName}" first.`);
10595
10482
  }
10596
10483
  return connection;
10597
10484
  }
10598
- app.post("/projects/:name/wordpress/connect", async (request, reply) => {
10599
- return withWordpressErrorHandling(reply, async () => {
10600
- const store = requireStore(reply);
10601
- if (!store) return;
10485
+ app.post("/projects/:name/wordpress/connect", async (request) => {
10486
+ return withWordpressErrorHandling(async () => {
10487
+ const store = requireStore();
10602
10488
  const project = resolveProject(app.db, request.params.name);
10603
10489
  const { url, stagingUrl, username, appPassword } = request.body ?? {};
10604
10490
  if (!url || !username || !appPassword) {
10605
- const err = validationError("url, username, and appPassword are required");
10606
- return reply.status(err.statusCode).send(err.toJSON());
10491
+ throw validationError("url, username, and appPassword are required");
10607
10492
  }
10608
10493
  const defaultEnv = parseEnvInput(request.body?.defaultEnv, "defaultEnv") ?? (stagingUrl ? "staging" : "live");
10609
10494
  if (defaultEnv === "staging" && !stagingUrl) {
10610
- const err = validationError('defaultEnv "staging" requires stagingUrl');
10611
- return reply.status(err.statusCode).send(err.toJSON());
10495
+ throw validationError('defaultEnv "staging" requires stagingUrl');
10612
10496
  }
10613
10497
  const now = (/* @__PURE__ */ new Date()).toISOString();
10614
10498
  const existing = store.getConnection(project.name);
@@ -10644,13 +10528,11 @@ async function wordpressRoutes(app, opts) {
10644
10528
  });
10645
10529
  });
10646
10530
  app.delete("/projects/:name/wordpress/disconnect", async (request, reply) => {
10647
- const store = requireStore(reply);
10648
- if (!store) return;
10531
+ const store = requireStore();
10649
10532
  const project = resolveProject(app.db, request.params.name);
10650
10533
  const deleted = store.deleteConnection(project.name);
10651
10534
  if (!deleted) {
10652
- const err = notFound("WordPress connection", project.name);
10653
- return reply.status(err.statusCode).send(err.toJSON());
10535
+ throw notFound("WordPress connection", project.name);
10654
10536
  }
10655
10537
  writeAuditLog(app.db, {
10656
10538
  projectId: project.id,
@@ -10685,13 +10567,11 @@ async function wordpressRoutes(app, opts) {
10685
10567
  adminUrl: getWpStagingAdminUrl(connection.url)
10686
10568
  };
10687
10569
  });
10688
- app.get("/projects/:name/wordpress/pages", async (request, reply) => {
10689
- return withWordpressErrorHandling(reply, async () => {
10690
- const store = requireStore(reply);
10691
- if (!store) return;
10570
+ app.get("/projects/:name/wordpress/pages", async (request) => {
10571
+ return withWordpressErrorHandling(async () => {
10572
+ const store = requireStore();
10692
10573
  const project = resolveProject(app.db, request.params.name);
10693
- const connection = requireConnection(store, project.name, reply);
10694
- if (!connection) return;
10574
+ const connection = requireConnection(store, project.name);
10695
10575
  const env = parseEnvInput(request.query?.env);
10696
10576
  return {
10697
10577
  env: env ?? connection.defaultEnv,
@@ -10699,34 +10579,28 @@ async function wordpressRoutes(app, opts) {
10699
10579
  };
10700
10580
  });
10701
10581
  });
10702
- app.get("/projects/:name/wordpress/page", async (request, reply) => {
10703
- return withWordpressErrorHandling(reply, async () => {
10704
- const store = requireStore(reply);
10705
- if (!store) return;
10582
+ app.get("/projects/:name/wordpress/page", async (request) => {
10583
+ return withWordpressErrorHandling(async () => {
10584
+ const store = requireStore();
10706
10585
  const project = resolveProject(app.db, request.params.name);
10707
- const connection = requireConnection(store, project.name, reply);
10708
- if (!connection) return;
10586
+ const connection = requireConnection(store, project.name);
10709
10587
  const slug = request.query?.slug?.trim();
10710
10588
  if (!slug) {
10711
- const err = validationError("slug is required");
10712
- return reply.status(err.statusCode).send(err.toJSON());
10589
+ throw validationError("slug is required");
10713
10590
  }
10714
10591
  const env = parseEnvInput(request.query?.env);
10715
10592
  return getPageDetail(connection, slug, env);
10716
10593
  });
10717
10594
  });
10718
- app.post("/projects/:name/wordpress/pages", async (request, reply) => {
10719
- return withWordpressErrorHandling(reply, async () => {
10720
- const store = requireStore(reply);
10721
- if (!store) return;
10595
+ app.post("/projects/:name/wordpress/pages", async (request) => {
10596
+ return withWordpressErrorHandling(async () => {
10597
+ const store = requireStore();
10722
10598
  const project = resolveProject(app.db, request.params.name);
10723
- const connection = requireConnection(store, project.name, reply);
10724
- if (!connection) return;
10599
+ const connection = requireConnection(store, project.name);
10725
10600
  const { title, slug, content, status } = request.body ?? {};
10726
10601
  const env = parseEnvInput(request.body?.env);
10727
10602
  if (!title || !slug || !content) {
10728
- const err = validationError("title, slug, and content are required");
10729
- return reply.status(err.statusCode).send(err.toJSON());
10603
+ throw validationError("title, slug, and content are required");
10730
10604
  }
10731
10605
  const created = await createPage(connection, { title, slug, content, status }, env);
10732
10606
  writeAuditLog(app.db, {
@@ -10739,17 +10613,14 @@ async function wordpressRoutes(app, opts) {
10739
10613
  return created;
10740
10614
  });
10741
10615
  });
10742
- app.put("/projects/:name/wordpress/page", async (request, reply) => {
10743
- return withWordpressErrorHandling(reply, async () => {
10744
- const store = requireStore(reply);
10745
- if (!store) return;
10616
+ app.put("/projects/:name/wordpress/page", async (request) => {
10617
+ return withWordpressErrorHandling(async () => {
10618
+ const store = requireStore();
10746
10619
  const project = resolveProject(app.db, request.params.name);
10747
- const connection = requireConnection(store, project.name, reply);
10748
- if (!connection) return;
10620
+ const connection = requireConnection(store, project.name);
10749
10621
  const currentSlug = request.body?.currentSlug?.trim();
10750
10622
  if (!currentSlug) {
10751
- const err = validationError("currentSlug is required");
10752
- return reply.status(err.statusCode).send(err.toJSON());
10623
+ throw validationError("currentSlug is required");
10753
10624
  }
10754
10625
  const env = parseEnvInput(request.body?.env);
10755
10626
  const updated = await updatePageBySlug(connection, currentSlug, {
@@ -10768,17 +10639,14 @@ async function wordpressRoutes(app, opts) {
10768
10639
  return updated;
10769
10640
  });
10770
10641
  });
10771
- app.post("/projects/:name/wordpress/page/meta", async (request, reply) => {
10772
- return withWordpressErrorHandling(reply, async () => {
10773
- const store = requireStore(reply);
10774
- if (!store) return;
10642
+ app.post("/projects/:name/wordpress/page/meta", async (request) => {
10643
+ return withWordpressErrorHandling(async () => {
10644
+ const store = requireStore();
10775
10645
  const project = resolveProject(app.db, request.params.name);
10776
- const connection = requireConnection(store, project.name, reply);
10777
- if (!connection) return;
10646
+ const connection = requireConnection(store, project.name);
10778
10647
  const slug = request.body?.slug?.trim();
10779
10648
  if (!slug) {
10780
- const err = validationError("slug is required");
10781
- return reply.status(err.statusCode).send(err.toJSON());
10649
+ throw validationError("slug is required");
10782
10650
  }
10783
10651
  const env = parseEnvInput(request.body?.env);
10784
10652
  const updated = await setSeoMeta(connection, slug, {
@@ -10796,22 +10664,18 @@ async function wordpressRoutes(app, opts) {
10796
10664
  return updated;
10797
10665
  });
10798
10666
  });
10799
- app.post("/projects/:name/wordpress/pages/meta/bulk", async (request, reply) => {
10800
- return withWordpressErrorHandling(reply, async () => {
10801
- const store = requireStore(reply);
10802
- if (!store) return;
10667
+ app.post("/projects/:name/wordpress/pages/meta/bulk", async (request) => {
10668
+ return withWordpressErrorHandling(async () => {
10669
+ const store = requireStore();
10803
10670
  const project = resolveProject(app.db, request.params.name);
10804
- const connection = requireConnection(store, project.name, reply);
10805
- if (!connection) return;
10671
+ const connection = requireConnection(store, project.name);
10806
10672
  const entries = request.body?.entries;
10807
10673
  if (!Array.isArray(entries) || entries.length === 0) {
10808
- const err = validationError("entries array is required and must not be empty");
10809
- return reply.status(err.statusCode).send(err.toJSON());
10674
+ throw validationError("entries array is required and must not be empty");
10810
10675
  }
10811
10676
  for (const entry of entries) {
10812
10677
  if (!entry.slug?.trim()) {
10813
- const err = validationError("each entry must have a slug");
10814
- return reply.status(err.statusCode).send(err.toJSON());
10678
+ throw validationError("each entry must have a slug");
10815
10679
  }
10816
10680
  }
10817
10681
  const env = parseEnvInput(request.body?.env);
@@ -10829,126 +10693,103 @@ async function wordpressRoutes(app, opts) {
10829
10693
  return result;
10830
10694
  });
10831
10695
  });
10832
- app.get("/projects/:name/wordpress/schema", async (request, reply) => {
10833
- return withWordpressErrorHandling(reply, async () => {
10834
- const store = requireStore(reply);
10835
- if (!store) return;
10696
+ app.get("/projects/:name/wordpress/schema", async (request) => {
10697
+ return withWordpressErrorHandling(async () => {
10698
+ const store = requireStore();
10836
10699
  const project = resolveProject(app.db, request.params.name);
10837
- const connection = requireConnection(store, project.name, reply);
10838
- if (!connection) return;
10700
+ const connection = requireConnection(store, project.name);
10839
10701
  const slug = request.query?.slug?.trim();
10840
10702
  if (!slug) {
10841
- const err = validationError("slug is required");
10842
- return reply.status(err.statusCode).send(err.toJSON());
10703
+ throw validationError("slug is required");
10843
10704
  }
10844
10705
  const env = parseEnvInput(request.query?.env);
10845
10706
  return getPageSchema(connection, slug, env);
10846
10707
  });
10847
10708
  });
10848
- app.post("/projects/:name/wordpress/schema/manual", async (request, reply) => {
10849
- return withWordpressErrorHandling(reply, async () => {
10850
- const store = requireStore(reply);
10851
- if (!store) return;
10709
+ app.post("/projects/:name/wordpress/schema/manual", async (request) => {
10710
+ return withWordpressErrorHandling(async () => {
10711
+ const store = requireStore();
10852
10712
  const project = resolveProject(app.db, request.params.name);
10853
- const connection = requireConnection(store, project.name, reply);
10854
- if (!connection) return;
10713
+ const connection = requireConnection(store, project.name);
10855
10714
  const slug = request.body?.slug?.trim();
10856
10715
  const json = request.body?.json;
10857
10716
  if (!slug || !json) {
10858
- const err = validationError("slug and json are required");
10859
- return reply.status(err.statusCode).send(err.toJSON());
10717
+ throw validationError("slug and json are required");
10860
10718
  }
10861
10719
  const env = parseEnvInput(request.body?.env);
10862
10720
  return buildManualSchemaUpdate(connection, slug, { type: request.body?.type, json }, env);
10863
10721
  });
10864
10722
  });
10865
- app.post("/projects/:name/wordpress/schema/deploy", async (request, reply) => {
10866
- return withWordpressErrorHandling(reply, async () => {
10867
- const store = requireStore(reply);
10868
- if (!store) return;
10723
+ app.post("/projects/:name/wordpress/schema/deploy", async (request) => {
10724
+ return withWordpressErrorHandling(async () => {
10725
+ const store = requireStore();
10869
10726
  const project = resolveProject(app.db, request.params.name);
10870
- const connection = requireConnection(store, project.name, reply);
10871
- if (!connection) return;
10727
+ const connection = requireConnection(store, project.name);
10872
10728
  const profile = request.body?.profile;
10873
10729
  if (!profile?.business?.name || !profile?.pages || Object.keys(profile.pages).length === 0) {
10874
- const err = validationError("profile with business.name and non-empty pages is required");
10875
- return reply.status(err.statusCode).send(err.toJSON());
10730
+ throw validationError("profile with business.name and non-empty pages is required");
10876
10731
  }
10877
10732
  const env = parseEnvInput(request.body?.env);
10878
10733
  return deploySchemaFromProfile(connection, profile, env);
10879
10734
  });
10880
10735
  });
10881
- app.get("/projects/:name/wordpress/schema/status", async (request, reply) => {
10882
- return withWordpressErrorHandling(reply, async () => {
10883
- const store = requireStore(reply);
10884
- if (!store) return;
10736
+ app.get("/projects/:name/wordpress/schema/status", async (request) => {
10737
+ return withWordpressErrorHandling(async () => {
10738
+ const store = requireStore();
10885
10739
  const project = resolveProject(app.db, request.params.name);
10886
- const connection = requireConnection(store, project.name, reply);
10887
- if (!connection) return;
10740
+ const connection = requireConnection(store, project.name);
10888
10741
  const env = parseEnvInput(request.query?.env);
10889
10742
  return getSchemaStatus(connection, env);
10890
10743
  });
10891
10744
  });
10892
- app.get("/projects/:name/wordpress/llms-txt", async (request, reply) => {
10893
- return withWordpressErrorHandling(reply, async () => {
10894
- const store = requireStore(reply);
10895
- if (!store) return;
10745
+ app.get("/projects/:name/wordpress/llms-txt", async (request) => {
10746
+ return withWordpressErrorHandling(async () => {
10747
+ const store = requireStore();
10896
10748
  const project = resolveProject(app.db, request.params.name);
10897
- const connection = requireConnection(store, project.name, reply);
10898
- if (!connection) return;
10749
+ const connection = requireConnection(store, project.name);
10899
10750
  const env = parseEnvInput(request.query?.env);
10900
10751
  return getLlmsTxt(connection, env);
10901
10752
  });
10902
10753
  });
10903
- app.post("/projects/:name/wordpress/llms-txt/manual", async (request, reply) => {
10904
- return withWordpressErrorHandling(reply, async () => {
10905
- const store = requireStore(reply);
10906
- if (!store) return;
10754
+ app.post("/projects/:name/wordpress/llms-txt/manual", async (request) => {
10755
+ return withWordpressErrorHandling(async () => {
10756
+ const store = requireStore();
10907
10757
  const project = resolveProject(app.db, request.params.name);
10908
- const connection = requireConnection(store, project.name, reply);
10909
- if (!connection) return;
10758
+ const connection = requireConnection(store, project.name);
10910
10759
  const content = request.body?.content;
10911
10760
  if (!content) {
10912
- const err = validationError("content is required");
10913
- return reply.status(err.statusCode).send(err.toJSON());
10761
+ throw validationError("content is required");
10914
10762
  }
10915
10763
  const env = parseEnvInput(request.body?.env);
10916
10764
  return buildManualLlmsTxtUpdate(connection, content, env);
10917
10765
  });
10918
10766
  });
10919
- app.get("/projects/:name/wordpress/audit", async (request, reply) => {
10920
- return withWordpressErrorHandling(reply, async () => {
10921
- const store = requireStore(reply);
10922
- if (!store) return;
10767
+ app.get("/projects/:name/wordpress/audit", async (request) => {
10768
+ return withWordpressErrorHandling(async () => {
10769
+ const store = requireStore();
10923
10770
  const project = resolveProject(app.db, request.params.name);
10924
- const connection = requireConnection(store, project.name, reply);
10925
- if (!connection) return;
10771
+ const connection = requireConnection(store, project.name);
10926
10772
  const env = parseEnvInput(request.query?.env);
10927
10773
  return runAudit(connection, env);
10928
10774
  });
10929
10775
  });
10930
- app.get("/projects/:name/wordpress/diff", async (request, reply) => {
10931
- return withWordpressErrorHandling(reply, async () => {
10932
- const store = requireStore(reply);
10933
- if (!store) return;
10776
+ app.get("/projects/:name/wordpress/diff", async (request) => {
10777
+ return withWordpressErrorHandling(async () => {
10778
+ const store = requireStore();
10934
10779
  const project = resolveProject(app.db, request.params.name);
10935
- const connection = requireConnection(store, project.name, reply);
10936
- if (!connection) return;
10780
+ const connection = requireConnection(store, project.name);
10937
10781
  const slug = request.query?.slug?.trim();
10938
10782
  if (!slug) {
10939
- const err = validationError("slug is required");
10940
- return reply.status(err.statusCode).send(err.toJSON());
10783
+ throw validationError("slug is required");
10941
10784
  }
10942
10785
  return diffPageAcrossEnvironments(connection, slug);
10943
10786
  });
10944
10787
  });
10945
- app.get("/projects/:name/wordpress/staging/status", async (request, reply) => {
10946
- return withWordpressErrorHandling(reply, async () => {
10947
- const store = requireStore(reply);
10948
- if (!store) return;
10788
+ app.get("/projects/:name/wordpress/staging/status", async (request) => {
10789
+ return withWordpressErrorHandling(async () => {
10790
+ const store = requireStore();
10949
10791
  const project = resolveProject(app.db, request.params.name);
10950
- const connection = requireConnection(store, project.name, reply);
10951
- if (!connection) return;
10792
+ const connection = requireConnection(store, project.name);
10952
10793
  const plugins = await listActivePlugins(connection, "live");
10953
10794
  return {
10954
10795
  stagingConfigured: Boolean(connection.stagingUrl),
@@ -10958,34 +10799,28 @@ async function wordpressRoutes(app, opts) {
10958
10799
  };
10959
10800
  });
10960
10801
  });
10961
- app.post("/projects/:name/wordpress/staging/push", async (request, reply) => {
10962
- return withWordpressErrorHandling(reply, async () => {
10963
- const store = requireStore(reply);
10964
- if (!store) return;
10802
+ app.post("/projects/:name/wordpress/staging/push", async (request) => {
10803
+ return withWordpressErrorHandling(async () => {
10804
+ const store = requireStore();
10965
10805
  const project = resolveProject(app.db, request.params.name);
10966
- const connection = requireConnection(store, project.name, reply);
10967
- if (!connection) return;
10806
+ const connection = requireConnection(store, project.name);
10968
10807
  if (!connection.stagingUrl) {
10969
- const err = validationError("No staging URL configured for this project. Reconnect with --staging-url before using staging push.");
10970
- 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.");
10971
10809
  }
10972
10810
  return buildManualStagingPush(connection);
10973
10811
  });
10974
10812
  });
10975
- app.post("/projects/:name/wordpress/onboard", async (request, reply) => {
10976
- return withWordpressErrorHandling(reply, async () => {
10977
- const store = requireStore(reply);
10978
- if (!store) return;
10813
+ app.post("/projects/:name/wordpress/onboard", async (request) => {
10814
+ return withWordpressErrorHandling(async () => {
10815
+ const store = requireStore();
10979
10816
  const project = resolveProject(app.db, request.params.name);
10980
10817
  const { url, username, appPassword, stagingUrl, profile, skipSchema, skipSubmit } = request.body ?? {};
10981
10818
  if (!url || !username || !appPassword) {
10982
- const err = validationError("url, username, and appPassword are required");
10983
- return reply.status(err.statusCode).send(err.toJSON());
10819
+ throw validationError("url, username, and appPassword are required");
10984
10820
  }
10985
10821
  const defaultEnv = parseEnvInput(request.body?.defaultEnv, "defaultEnv") ?? (stagingUrl ? "staging" : "live");
10986
10822
  if (defaultEnv === "staging" && !stagingUrl) {
10987
- const err = validationError('defaultEnv "staging" requires stagingUrl');
10988
- return reply.status(err.statusCode).send(err.toJSON());
10823
+ throw validationError('defaultEnv "staging" requires stagingUrl');
10989
10824
  }
10990
10825
  const steps = [];
10991
10826
  let connection = null;
@@ -14532,6 +14367,26 @@ var ProviderExecutionGate = class {
14532
14367
  }
14533
14368
  }
14534
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
+ }
14535
14390
  var JobRunner = class {
14536
14391
  db;
14537
14392
  registry;
@@ -14739,11 +14594,11 @@ var JobRunner = class {
14739
14594
  }
14740
14595
  }
14741
14596
  };
14742
- await Promise.all(apiProviders.map(async (registeredProvider) => {
14597
+ await runWithConcurrency(apiProviders, resolveProviderFanout(), async (registeredProvider) => {
14743
14598
  await Promise.all(projectKeywords.map(async (kw) => {
14744
14599
  await processKeywordForProvider(registeredProvider, kw);
14745
14600
  }));
14746
- }));
14601
+ });
14747
14602
  for (const registeredProvider of browserProviders) {
14748
14603
  for (const kw of projectKeywords) {
14749
14604
  await processKeywordForProvider(registeredProvider, kw);
@@ -15361,6 +15216,21 @@ async function executeReleaseSync(db, syncId, opts) {
15361
15216
  projectsProcessed: allProjects.length,
15362
15217
  domainsDiscovered: rows.length
15363
15218
  });
15219
+ if (deps.enqueueAutoExtract) {
15220
+ const autoExtractProjects = allProjects.filter((p) => p.autoExtractBacklinks === 1);
15221
+ for (const p of autoExtractProjects) {
15222
+ try {
15223
+ deps.enqueueAutoExtract({ projectId: p.id, release });
15224
+ } catch (err) {
15225
+ log4.error("auto-extract.enqueue-failed", {
15226
+ syncId,
15227
+ release,
15228
+ projectId: p.id,
15229
+ error: err instanceof Error ? err.message : String(err)
15230
+ });
15231
+ }
15232
+ }
15233
+ }
15364
15234
  } catch (err) {
15365
15235
  const errorMsg = err instanceof Error ? err.message : String(err);
15366
15236
  const finishedAt = deps.now().toISOString();
@@ -15404,6 +15274,7 @@ function computeSummary(rows) {
15404
15274
 
15405
15275
  // src/backlink-extract.ts
15406
15276
  import crypto23 from "crypto";
15277
+ import fs9 from "fs";
15407
15278
  import { and as and12, desc as desc9, eq as eq23 } from "drizzle-orm";
15408
15279
  var log5 = createLogger("BacklinkExtract");
15409
15280
  function defaultDeps2() {
@@ -15432,6 +15303,11 @@ async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
15432
15303
  if (!sync.vertexPath || !sync.edgesPath) {
15433
15304
  throw new Error(`Release ${sync.release} is missing cached file paths`);
15434
15305
  }
15306
+ if (!fs9.existsSync(sync.vertexPath) || !fs9.existsSync(sync.edgesPath)) {
15307
+ throw new Error(
15308
+ `Cache for release ${sync.release} is missing from disk (expected at ${sync.vertexPath}). The sync record exists in the database, but the ~16 GB dump was deleted or never present on this machine. Re-sync this release from the Backlinks admin page to restore the cache.`
15309
+ );
15310
+ }
15435
15311
  const duckdb = deps.loadDuckdb();
15436
15312
  const rows = await deps.queryBacklinks({
15437
15313
  vertexPath: sync.vertexPath,
@@ -15946,7 +15822,7 @@ import crypto26 from "crypto";
15946
15822
  import { eq as eq27 } from "drizzle-orm";
15947
15823
 
15948
15824
  // src/agent/session.ts
15949
- import fs11 from "fs";
15825
+ import fs12 from "fs";
15950
15826
  import path14 from "path";
15951
15827
  import { Agent } from "@mariozechner/pi-agent-core";
15952
15828
  import { registerBuiltInApiProviders } from "@mariozechner/pi-ai";
@@ -16048,7 +15924,7 @@ function buildAgentProvidersResponse(config) {
16048
15924
  }
16049
15925
 
16050
15926
  // src/agent/skill-paths.ts
16051
- import fs9 from "fs";
15927
+ import fs10 from "fs";
16052
15928
  import path12 from "path";
16053
15929
  import { fileURLToPath } from "url";
16054
15930
  function resolveAeroSkillDir(pkgDir) {
@@ -16059,14 +15935,14 @@ function resolveAeroSkillDir(pkgDir) {
16059
15935
  path12.join(here, "../../../../skills/aero")
16060
15936
  ];
16061
15937
  for (const candidate of candidates) {
16062
- if (fs9.existsSync(path12.join(candidate, "SKILL.md"))) return candidate;
15938
+ if (fs10.existsSync(path12.join(candidate, "SKILL.md"))) return candidate;
16063
15939
  }
16064
15940
  throw new Error(`Aero skill not found. Searched:
16065
15941
  ${candidates.join("\n ")}`);
16066
15942
  }
16067
15943
 
16068
15944
  // src/agent/skill-tools.ts
16069
- import fs10 from "fs";
15945
+ import fs11 from "fs";
16070
15946
  import path13 from "path";
16071
15947
  import { Type } from "@sinclair/typebox";
16072
15948
  var MAX_DOC_CHARS = 2e4;
@@ -16089,12 +15965,12 @@ function parseDescription(body) {
16089
15965
  }
16090
15966
  function scanSkillDocs(skillDir) {
16091
15967
  const refsDir = path13.join(skillDir ?? resolveAeroSkillDir(), "references");
16092
- if (!fs10.existsSync(refsDir)) return [];
15968
+ if (!fs11.existsSync(refsDir)) return [];
16093
15969
  const entries = [];
16094
- for (const file of fs10.readdirSync(refsDir)) {
15970
+ for (const file of fs11.readdirSync(refsDir)) {
16095
15971
  if (!file.endsWith(".md")) continue;
16096
15972
  const filePath = path13.join(refsDir, file);
16097
- const body = fs10.readFileSync(filePath, "utf-8");
15973
+ const body = fs11.readFileSync(filePath, "utf-8");
16098
15974
  entries.push({
16099
15975
  slug: file.replace(/\.md$/, ""),
16100
15976
  description: parseDescription(body),
@@ -16138,7 +16014,7 @@ function buildReadSkillDocTool() {
16138
16014
  });
16139
16015
  }
16140
16016
  const filePath = path13.join(skillDir, "references", `${match.slug}.md`);
16141
- const content = fs10.readFileSync(filePath, "utf-8");
16017
+ const content = fs11.readFileSync(filePath, "utf-8");
16142
16018
  if (content.length > MAX_DOC_CHARS) {
16143
16019
  return textResult({
16144
16020
  slug: match.slug,
@@ -16692,10 +16568,10 @@ function ensureBuiltinsRegistered() {
16692
16568
  }
16693
16569
  function loadAeroSystemPrompt(pkgDir) {
16694
16570
  const skillDir = resolveAeroSkillDir(pkgDir);
16695
- const skillBody = fs11.readFileSync(path14.join(skillDir, "SKILL.md"), "utf-8");
16571
+ const skillBody = fs12.readFileSync(path14.join(skillDir, "SKILL.md"), "utf-8");
16696
16572
  const soulPath = path14.join(skillDir, "soul.md");
16697
- if (!fs11.existsSync(soulPath)) return skillBody;
16698
- const soulBody = fs11.readFileSync(soulPath, "utf-8");
16573
+ if (!fs12.existsSync(soulPath)) return skillBody;
16574
+ const soulBody = fs12.readFileSync(soulPath, "utf-8");
16699
16575
  return `${soulBody.trimEnd()}
16700
16576
 
16701
16577
  ---
@@ -18990,7 +18866,7 @@ async function createServer(opts) {
18990
18866
  jobRunner.onRunCompleted = (runId, projectId) => runCoordinator.onRunCompleted(runId, projectId);
18991
18867
  const snapshotService = new SnapshotService(registry);
18992
18868
  const orphanedOpenClawDir = path15.join(os6.homedir(), ".openclaw-aero");
18993
- if (fs12.existsSync(orphanedOpenClawDir)) {
18869
+ if (fs13.existsSync(orphanedOpenClawDir)) {
18994
18870
  app.log.warn(
18995
18871
  { path: orphanedOpenClawDir },
18996
18872
  "OpenClaw gateway is no longer used. Remove ~/.openclaw-aero/ manually to reclaim the directory."
@@ -19314,7 +19190,26 @@ async function createServer(opts) {
19314
19190
  };
19315
19191
  },
19316
19192
  onReleaseSyncRequested: (syncId, release) => {
19317
- executeReleaseSync(opts.db, syncId, { release }).catch((err) => {
19193
+ executeReleaseSync(opts.db, syncId, {
19194
+ release,
19195
+ deps: {
19196
+ enqueueAutoExtract: ({ projectId, release: r }) => {
19197
+ const now = (/* @__PURE__ */ new Date()).toISOString();
19198
+ const runId = crypto27.randomUUID();
19199
+ opts.db.insert(runs).values({
19200
+ id: runId,
19201
+ projectId,
19202
+ kind: RunKinds["backlink-extract"],
19203
+ status: RunStatuses.queued,
19204
+ trigger: RunTriggers.scheduled,
19205
+ createdAt: now
19206
+ }).run();
19207
+ executeBacklinkExtract(opts.db, runId, projectId, { release: r }).catch((err) => {
19208
+ app.log.error({ runId, projectId, err }, "Auto backlink extract failed");
19209
+ });
19210
+ }
19211
+ }
19212
+ }).catch((err) => {
19318
19213
  app.log.error({ syncId, err }, "Common Crawl release sync failed");
19319
19214
  });
19320
19215
  },
@@ -19559,7 +19454,7 @@ async function createServer(opts) {
19559
19454
  });
19560
19455
  const dirname = path15.dirname(fileURLToPath2(import.meta.url));
19561
19456
  const assetsDir = path15.join(dirname, "..", "assets");
19562
- if (fs12.existsSync(assetsDir)) {
19457
+ if (fs13.existsSync(assetsDir)) {
19563
19458
  const indexPath = path15.join(assetsDir, "index.html");
19564
19459
  const injectConfig = (html) => {
19565
19460
  const clientConfig = {};
@@ -19578,8 +19473,8 @@ async function createServer(opts) {
19578
19473
  index: false
19579
19474
  });
19580
19475
  const serveIndex = (_request, reply) => {
19581
- if (fs12.existsSync(indexPath)) {
19582
- const html = fs12.readFileSync(indexPath, "utf-8");
19476
+ if (fs13.existsSync(indexPath)) {
19477
+ const html = fs13.readFileSync(indexPath, "utf-8");
19583
19478
  return reply.type("text/html").send(injectConfig(html));
19584
19479
  }
19585
19480
  return reply.status(404).send({ error: "Dashboard not built" });
@@ -19599,8 +19494,8 @@ async function createServer(opts) {
19599
19494
  if (basePath && !url.startsWith(basePath)) {
19600
19495
  return reply.status(404).send({ error: "Not found", path: request.url });
19601
19496
  }
19602
- if (fs12.existsSync(indexPath)) {
19603
- const html = fs12.readFileSync(indexPath, "utf-8");
19497
+ if (fs13.existsSync(indexPath)) {
19498
+ const html = fs13.readFileSync(indexPath, "utf-8");
19604
19499
  return reply.type("text/html").send(injectConfig(html));
19605
19500
  }
19606
19501
  return reply.status(404).send({ error: "Not found" });
@@ -19690,8 +19585,10 @@ export {
19690
19585
  resolveProviderInput,
19691
19586
  notificationEventSchema,
19692
19587
  effectiveDomains,
19588
+ RunStatuses,
19693
19589
  RunKinds,
19694
19590
  determineAnswerMentioned,
19591
+ CcReleaseSyncStatuses,
19695
19592
  reparseStoredResult2 as reparseStoredResult,
19696
19593
  reparseStoredResult3 as reparseStoredResult2,
19697
19594
  reparseStoredResult as reparseStoredResult3,