@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.
- package/dist/{chunk-Y7GCG4GD.js → chunk-KGOT5OFT.js} +247 -403
- package/dist/cli.js +1 -1
- package/dist/index.js +1 -1
- package/package.json +7 -7
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1580
|
-
return reply.status(err.statusCode).send(err.toJSON());
|
|
1580
|
+
throw authRequired();
|
|
1581
1581
|
}
|
|
1582
1582
|
} else {
|
|
1583
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6107
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6110
|
+
throw validationError(`Unknown quota field: ${key}`);
|
|
6122
6111
|
}
|
|
6123
6112
|
if (typeof val !== "number" || !Number.isInteger(val) || val <= 0) {
|
|
6124
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
6137
|
+
app.put("/settings/bing", async (request) => {
|
|
6162
6138
|
const { apiKey } = request.body ?? {};
|
|
6163
6139
|
if (!apiKey || typeof apiKey !== "string") {
|
|
6164
|
-
|
|
6165
|
-
error: { code: "VALIDATION_ERROR", message: "apiKey is required" }
|
|
6166
|
-
});
|
|
6140
|
+
throw validationError("apiKey is required");
|
|
6167
6141
|
}
|
|
6168
6142
|
if (!opts.onBingUpdate) {
|
|
6169
|
-
|
|
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
|
-
|
|
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
|
|
6155
|
+
app.post("/snapshot", async (request) => {
|
|
6188
6156
|
const parsed = snapshotRequestSchema.safeParse(request.body);
|
|
6189
6157
|
if (!parsed.success) {
|
|
6190
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
7364
|
+
function requireConnectionStore() {
|
|
7405
7365
|
if (opts.googleConnectionStore) return opts.googleConnectionStore;
|
|
7406
|
-
|
|
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
|
|
7382
|
+
app.post("/projects/:name/google/connect", async (request) => {
|
|
7425
7383
|
const { clientId: googleClientId, clientSecret: googleClientSecret } = getAuthConfig();
|
|
7426
7384
|
if (!googleClientId || !googleClientSecret) {
|
|
7427
|
-
|
|
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
|
-
|
|
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(
|
|
7460
|
-
if (!store) return;
|
|
7415
|
+
const store = requireConnectionStore();
|
|
7461
7416
|
const escapeHtml = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[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(
|
|
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
|
-
|
|
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
|
|
7520
|
+
app.get("/projects/:name/google/properties", async (request) => {
|
|
7568
7521
|
const { clientId: googleClientId, clientSecret: googleClientSecret } = getAuthConfig();
|
|
7569
7522
|
if (!googleClientId || !googleClientSecret) {
|
|
7570
|
-
|
|
7571
|
-
return reply.status(err.statusCode).send(err.toJSON());
|
|
7523
|
+
throw validationError("Google OAuth is not configured");
|
|
7572
7524
|
}
|
|
7573
|
-
const store = requireConnectionStore(
|
|
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
|
|
7581
|
-
const store = requireConnectionStore(
|
|
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
|
-
|
|
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
|
|
7578
|
+
app.post("/projects/:name/google/gsc/inspect", async (request) => {
|
|
7630
7579
|
const { clientId: googleClientId, clientSecret: googleClientSecret } = getAuthConfig();
|
|
7631
7580
|
if (!googleClientId || !googleClientSecret) {
|
|
7632
|
-
|
|
7633
|
-
return reply.status(err.statusCode).send(err.toJSON());
|
|
7581
|
+
throw validationError("Google OAuth is not configured");
|
|
7634
7582
|
}
|
|
7635
|
-
const store = requireConnectionStore(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
7791
|
+
app.get("/projects/:name/google/gsc/sitemaps", async (request) => {
|
|
7847
7792
|
const { clientId: googleClientId, clientSecret: googleClientSecret } = getAuthConfig();
|
|
7848
7793
|
if (!googleClientId || !googleClientSecret) {
|
|
7849
|
-
|
|
7850
|
-
return reply.status(err.statusCode).send(err.toJSON());
|
|
7794
|
+
throw validationError("Google OAuth is not configured");
|
|
7851
7795
|
}
|
|
7852
|
-
const store = requireConnectionStore(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
7867
|
-
return reply.status(err.statusCode).send(err.toJSON());
|
|
7808
|
+
throw validationError("Google OAuth is not configured");
|
|
7868
7809
|
}
|
|
7869
|
-
const store = requireConnectionStore(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
7910
|
-
const store = requireConnectionStore(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
7940
|
-
const store = requireConnectionStore(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
7960
|
-
const store = requireConnectionStore(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
7907
|
+
app.post("/projects/:name/google/indexing/request", async (request) => {
|
|
7980
7908
|
const { clientId: googleClientId, clientSecret: googleClientSecret } = getAuthConfig();
|
|
7981
7909
|
if (!googleClientId || !googleClientSecret) {
|
|
7982
|
-
|
|
7983
|
-
return reply.status(err.statusCode).send(err.toJSON());
|
|
7910
|
+
throw validationError("Google OAuth is not configured");
|
|
7984
7911
|
}
|
|
7985
|
-
const store = requireConnectionStore(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
8167
|
+
function requireConnectionStore() {
|
|
8246
8168
|
if (opts.bingConnectionStore) return opts.bingConnectionStore;
|
|
8247
|
-
|
|
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
|
|
8171
|
+
function requireConnection(store, domain) {
|
|
8252
8172
|
const conn = store.getConnection(domain);
|
|
8253
8173
|
if (!conn) {
|
|
8254
|
-
|
|
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
|
|
8261
|
-
const store = requireConnectionStore(
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
8321
|
-
const store = requireConnectionStore(
|
|
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
|
|
8334
|
-
const store = requireConnectionStore(
|
|
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
|
|
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
|
|
8343
|
-
const store = requireConnectionStore(
|
|
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
|
-
|
|
8347
|
-
if (!conn) return;
|
|
8255
|
+
requireConnection(store, project.canonicalDomain);
|
|
8348
8256
|
const { siteUrl } = request.body ?? {};
|
|
8349
8257
|
if (!siteUrl || typeof siteUrl !== "string") {
|
|
8350
|
-
|
|
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
|
|
8360
|
-
const store = requireConnectionStore(
|
|
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
|
-
|
|
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
|
|
8450
|
-
|
|
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
|
|
8464
|
-
|
|
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
|
|
8484
|
-
const store = requireConnectionStore(
|
|
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
|
|
8488
|
-
if (!conn) return;
|
|
8389
|
+
const conn = requireConnection(store, project.canonicalDomain);
|
|
8489
8390
|
if (!conn.siteUrl) {
|
|
8490
|
-
|
|
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
|
-
|
|
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
|
|
8567
|
-
const store = requireConnectionStore(
|
|
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
|
|
8571
|
-
if (!conn) return;
|
|
8468
|
+
const conn = requireConnection(store, project.canonicalDomain);
|
|
8572
8469
|
if (!conn.siteUrl) {
|
|
8573
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
8647
|
-
const store = requireConnectionStore(
|
|
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
|
|
8651
|
-
if (!conn) return;
|
|
8542
|
+
const conn = requireConnection(store, project.canonicalDomain);
|
|
8652
8543
|
if (!conn.siteUrl) {
|
|
8653
|
-
|
|
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
|
|
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
|
-
|
|
10567
|
-
break;
|
|
10454
|
+
return new AppError("AUTH_INVALID", error.message, error.statusCode);
|
|
10568
10455
|
case "NOT_FOUND":
|
|
10569
|
-
|
|
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
|
-
|
|
10577
|
-
|
|
10459
|
+
return validationError(error.message);
|
|
10460
|
+
case "UPSTREAM_ERROR":
|
|
10578
10461
|
default:
|
|
10579
|
-
|
|
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(
|
|
10465
|
+
async function withWordpressErrorHandling(handler) {
|
|
10586
10466
|
try {
|
|
10587
10467
|
return await handler();
|
|
10588
10468
|
} catch (error) {
|
|
10589
|
-
if (
|
|
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(
|
|
10474
|
+
function requireStore() {
|
|
10595
10475
|
if (opts.wordpressConnectionStore) return opts.wordpressConnectionStore;
|
|
10596
|
-
|
|
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
|
|
10478
|
+
function requireConnection(store, projectName) {
|
|
10601
10479
|
const connection = store.getConnection(projectName);
|
|
10602
10480
|
if (!connection) {
|
|
10603
|
-
|
|
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
|
|
10610
|
-
return withWordpressErrorHandling(
|
|
10611
|
-
const store = requireStore(
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
10700
|
-
return withWordpressErrorHandling(
|
|
10701
|
-
const store = requireStore(
|
|
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
|
|
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
|
|
10714
|
-
return withWordpressErrorHandling(
|
|
10715
|
-
const store = requireStore(
|
|
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
|
|
10719
|
-
if (!connection) return;
|
|
10586
|
+
const connection = requireConnection(store, project.name);
|
|
10720
10587
|
const slug = request.query?.slug?.trim();
|
|
10721
10588
|
if (!slug) {
|
|
10722
|
-
|
|
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
|
|
10730
|
-
return withWordpressErrorHandling(
|
|
10731
|
-
const store = requireStore(
|
|
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
|
|
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
|
-
|
|
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
|
|
10754
|
-
return withWordpressErrorHandling(
|
|
10755
|
-
const store = requireStore(
|
|
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
|
|
10759
|
-
if (!connection) return;
|
|
10620
|
+
const connection = requireConnection(store, project.name);
|
|
10760
10621
|
const currentSlug = request.body?.currentSlug?.trim();
|
|
10761
10622
|
if (!currentSlug) {
|
|
10762
|
-
|
|
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
|
|
10783
|
-
return withWordpressErrorHandling(
|
|
10784
|
-
const store = requireStore(
|
|
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
|
|
10788
|
-
if (!connection) return;
|
|
10646
|
+
const connection = requireConnection(store, project.name);
|
|
10789
10647
|
const slug = request.body?.slug?.trim();
|
|
10790
10648
|
if (!slug) {
|
|
10791
|
-
|
|
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
|
|
10811
|
-
return withWordpressErrorHandling(
|
|
10812
|
-
const store = requireStore(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
10844
|
-
return withWordpressErrorHandling(
|
|
10845
|
-
const store = requireStore(
|
|
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
|
|
10849
|
-
if (!connection) return;
|
|
10700
|
+
const connection = requireConnection(store, project.name);
|
|
10850
10701
|
const slug = request.query?.slug?.trim();
|
|
10851
10702
|
if (!slug) {
|
|
10852
|
-
|
|
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
|
|
10860
|
-
return withWordpressErrorHandling(
|
|
10861
|
-
const store = requireStore(
|
|
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
|
|
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
|
-
|
|
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
|
|
10877
|
-
return withWordpressErrorHandling(
|
|
10878
|
-
const store = requireStore(
|
|
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
|
|
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
|
-
|
|
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
|
|
10893
|
-
return withWordpressErrorHandling(
|
|
10894
|
-
const store = requireStore(
|
|
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
|
|
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
|
|
10904
|
-
return withWordpressErrorHandling(
|
|
10905
|
-
const store = requireStore(
|
|
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
|
|
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
|
|
10915
|
-
return withWordpressErrorHandling(
|
|
10916
|
-
const store = requireStore(
|
|
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
|
|
10920
|
-
if (!connection) return;
|
|
10758
|
+
const connection = requireConnection(store, project.name);
|
|
10921
10759
|
const content = request.body?.content;
|
|
10922
10760
|
if (!content) {
|
|
10923
|
-
|
|
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
|
|
10931
|
-
return withWordpressErrorHandling(
|
|
10932
|
-
const store = requireStore(
|
|
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
|
|
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
|
|
10942
|
-
return withWordpressErrorHandling(
|
|
10943
|
-
const store = requireStore(
|
|
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
|
|
10947
|
-
if (!connection) return;
|
|
10780
|
+
const connection = requireConnection(store, project.name);
|
|
10948
10781
|
const slug = request.query?.slug?.trim();
|
|
10949
10782
|
if (!slug) {
|
|
10950
|
-
|
|
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
|
|
10957
|
-
return withWordpressErrorHandling(
|
|
10958
|
-
const store = requireStore(
|
|
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
|
|
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
|
|
10973
|
-
return withWordpressErrorHandling(
|
|
10974
|
-
const store = requireStore(
|
|
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
|
|
10978
|
-
if (!connection) return;
|
|
10806
|
+
const connection = requireConnection(store, project.name);
|
|
10979
10807
|
if (!connection.stagingUrl) {
|
|
10980
|
-
|
|
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
|
|
10987
|
-
return withWordpressErrorHandling(
|
|
10988
|
-
const store = requireStore(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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);
|