@ainyc/canonry 2.4.2 → 2.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
runs,
|
|
31
31
|
schedules,
|
|
32
32
|
usageCounters
|
|
33
|
-
} from "./chunk-
|
|
33
|
+
} from "./chunk-32YTAZBL.js";
|
|
34
34
|
|
|
35
35
|
// src/config.ts
|
|
36
36
|
import fs from "fs";
|
|
@@ -625,6 +625,9 @@ function agentBusy(projectName) {
|
|
|
625
625
|
function missingDependency(message, details) {
|
|
626
626
|
return new AppError("MISSING_DEPENDENCY", message, 422, details);
|
|
627
627
|
}
|
|
628
|
+
function internalError(message, details) {
|
|
629
|
+
return new AppError("INTERNAL_ERROR", message, 500, details);
|
|
630
|
+
}
|
|
628
631
|
|
|
629
632
|
// ../contracts/src/google.ts
|
|
630
633
|
import { z as z5 } from "zod";
|
|
@@ -1549,7 +1552,7 @@ function parseCookies(header) {
|
|
|
1549
1552
|
}, {});
|
|
1550
1553
|
}
|
|
1551
1554
|
async function authPlugin(app, opts = {}) {
|
|
1552
|
-
app.addHook("onRequest", async (request
|
|
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) {
|
|
@@ -8226,6 +8148,13 @@ async function getKeywordStats(apiKey, siteUrl) {
|
|
|
8226
8148
|
const data = await bingFetch(apiKey, `GetQueryStats?siteUrl=${encodedSite}`);
|
|
8227
8149
|
return data ?? [];
|
|
8228
8150
|
}
|
|
8151
|
+
async function getCrawlIssues(apiKey, siteUrl) {
|
|
8152
|
+
validateApiKey(apiKey);
|
|
8153
|
+
validateSiteUrl2(siteUrl);
|
|
8154
|
+
const encodedSite = encodeURIComponent(siteUrl);
|
|
8155
|
+
const data = await bingFetch(apiKey, `GetCrawlIssues?siteUrl=${encodedSite}`);
|
|
8156
|
+
return data ?? [];
|
|
8157
|
+
}
|
|
8229
8158
|
|
|
8230
8159
|
// ../api-routes/src/bing.ts
|
|
8231
8160
|
function parseBingDate(value) {
|
|
@@ -8241,29 +8170,47 @@ function bingLog(level, action, ctx) {
|
|
|
8241
8170
|
const stream = level === "error" ? process.stderr : process.stdout;
|
|
8242
8171
|
stream.write(JSON.stringify(entry) + "\n");
|
|
8243
8172
|
}
|
|
8173
|
+
var CRAWL_ISSUES_CACHE_TTL_MS = 6e4;
|
|
8174
|
+
var crawlIssuesCache = /* @__PURE__ */ new Map();
|
|
8175
|
+
function isBlockingIssueType(issueType) {
|
|
8176
|
+
if (!issueType) return true;
|
|
8177
|
+
const trimmed = issueType.trim();
|
|
8178
|
+
if (!trimmed) return true;
|
|
8179
|
+
return trimmed.split(/\s+/).some((flag) => !/^(None|Seo(Issues|Concerns))$/i.test(flag));
|
|
8180
|
+
}
|
|
8181
|
+
async function loadBlockingCrawlIssues(apiKey, siteUrl, domain) {
|
|
8182
|
+
const now = Date.now();
|
|
8183
|
+
const cached = crawlIssuesCache.get(domain);
|
|
8184
|
+
if (cached && now - cached.fetchedAt < CRAWL_ISSUES_CACHE_TTL_MS) {
|
|
8185
|
+
return cached.blockedUrls;
|
|
8186
|
+
}
|
|
8187
|
+
const issues = await getCrawlIssues(apiKey, siteUrl);
|
|
8188
|
+
const blockedUrls = /* @__PURE__ */ new Set();
|
|
8189
|
+
for (const issue of issues) {
|
|
8190
|
+
if (issue.Url && isBlockingIssueType(issue.IssueType ?? null)) {
|
|
8191
|
+
blockedUrls.add(issue.Url);
|
|
8192
|
+
}
|
|
8193
|
+
}
|
|
8194
|
+
crawlIssuesCache.set(domain, { blockedUrls, fetchedAt: now });
|
|
8195
|
+
return blockedUrls;
|
|
8196
|
+
}
|
|
8244
8197
|
async function bingRoutes(app, opts) {
|
|
8245
|
-
function requireConnectionStore(
|
|
8198
|
+
function requireConnectionStore() {
|
|
8246
8199
|
if (opts.bingConnectionStore) return opts.bingConnectionStore;
|
|
8247
|
-
|
|
8248
|
-
reply.status(err.statusCode).send(err.toJSON());
|
|
8249
|
-
return null;
|
|
8200
|
+
throw validationError("Bing connection storage is not configured for this deployment");
|
|
8250
8201
|
}
|
|
8251
|
-
function requireConnection(store, domain
|
|
8202
|
+
function requireConnection(store, domain) {
|
|
8252
8203
|
const conn = store.getConnection(domain);
|
|
8253
8204
|
if (!conn) {
|
|
8254
|
-
|
|
8255
|
-
reply.status(err.statusCode).send(err.toJSON());
|
|
8256
|
-
return null;
|
|
8205
|
+
throw validationError('No Bing connection found for this domain. Run "canonry bing connect <project>" first.');
|
|
8257
8206
|
}
|
|
8258
8207
|
return conn;
|
|
8259
8208
|
}
|
|
8260
|
-
app.post("/projects/:name/bing/connect", async (request
|
|
8261
|
-
const store = requireConnectionStore(
|
|
8262
|
-
if (!store) return;
|
|
8209
|
+
app.post("/projects/:name/bing/connect", async (request) => {
|
|
8210
|
+
const store = requireConnectionStore();
|
|
8263
8211
|
const { apiKey } = request.body ?? {};
|
|
8264
8212
|
if (!apiKey || typeof apiKey !== "string") {
|
|
8265
|
-
|
|
8266
|
-
return reply.status(err.statusCode).send(err.toJSON());
|
|
8213
|
+
throw validationError("apiKey is required");
|
|
8267
8214
|
}
|
|
8268
8215
|
const project = resolveProject(app.db, request.params.name);
|
|
8269
8216
|
let sites;
|
|
@@ -8273,8 +8220,7 @@ async function bingRoutes(app, opts) {
|
|
|
8273
8220
|
} catch (e) {
|
|
8274
8221
|
const msg = e instanceof Error ? e.message : String(e);
|
|
8275
8222
|
bingLog("error", "connect.verify-key-failed", { domain: project.canonicalDomain, error: msg });
|
|
8276
|
-
|
|
8277
|
-
return reply.status(err.statusCode).send(err.toJSON());
|
|
8223
|
+
throw validationError(`Failed to verify Bing API key: ${msg}`);
|
|
8278
8224
|
}
|
|
8279
8225
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8280
8226
|
const existing = store.getConnection(project.canonicalDomain);
|
|
@@ -8300,13 +8246,11 @@ async function bingRoutes(app, opts) {
|
|
|
8300
8246
|
};
|
|
8301
8247
|
});
|
|
8302
8248
|
app.delete("/projects/:name/bing/disconnect", async (request, reply) => {
|
|
8303
|
-
const store = requireConnectionStore(
|
|
8304
|
-
if (!store) return;
|
|
8249
|
+
const store = requireConnectionStore();
|
|
8305
8250
|
const project = resolveProject(app.db, request.params.name);
|
|
8306
8251
|
const deleted = store.deleteConnection(project.canonicalDomain);
|
|
8307
8252
|
if (!deleted) {
|
|
8308
|
-
|
|
8309
|
-
return reply.status(err.statusCode).send(err.toJSON());
|
|
8253
|
+
throw notFound("Bing connection", project.canonicalDomain);
|
|
8310
8254
|
}
|
|
8311
8255
|
writeAuditLog(app.db, {
|
|
8312
8256
|
projectId: project.id,
|
|
@@ -8317,9 +8261,8 @@ async function bingRoutes(app, opts) {
|
|
|
8317
8261
|
});
|
|
8318
8262
|
return reply.status(204).send();
|
|
8319
8263
|
});
|
|
8320
|
-
app.get("/projects/:name/bing/status", async (request
|
|
8321
|
-
const store = requireConnectionStore(
|
|
8322
|
-
if (!store) return;
|
|
8264
|
+
app.get("/projects/:name/bing/status", async (request) => {
|
|
8265
|
+
const store = requireConnectionStore();
|
|
8323
8266
|
const project = resolveProject(app.db, request.params.name);
|
|
8324
8267
|
const conn = store.getConnection(project.canonicalDomain);
|
|
8325
8268
|
return {
|
|
@@ -8330,25 +8273,20 @@ async function bingRoutes(app, opts) {
|
|
|
8330
8273
|
updatedAt: conn?.updatedAt ?? null
|
|
8331
8274
|
};
|
|
8332
8275
|
});
|
|
8333
|
-
app.get("/projects/:name/bing/sites", async (request
|
|
8334
|
-
const store = requireConnectionStore(
|
|
8335
|
-
if (!store) return;
|
|
8276
|
+
app.get("/projects/:name/bing/sites", async (request) => {
|
|
8277
|
+
const store = requireConnectionStore();
|
|
8336
8278
|
const project = resolveProject(app.db, request.params.name);
|
|
8337
|
-
const conn = requireConnection(store, project.canonicalDomain
|
|
8338
|
-
if (!conn) return;
|
|
8279
|
+
const conn = requireConnection(store, project.canonicalDomain);
|
|
8339
8280
|
const sites = await getSites(conn.apiKey);
|
|
8340
8281
|
return { sites: sites.map((s) => ({ url: s.Url, verified: s.Verified ?? false })) };
|
|
8341
8282
|
});
|
|
8342
|
-
app.post("/projects/:name/bing/set-site", async (request
|
|
8343
|
-
const store = requireConnectionStore(
|
|
8344
|
-
if (!store) return;
|
|
8283
|
+
app.post("/projects/:name/bing/set-site", async (request) => {
|
|
8284
|
+
const store = requireConnectionStore();
|
|
8345
8285
|
const project = resolveProject(app.db, request.params.name);
|
|
8346
|
-
|
|
8347
|
-
if (!conn) return;
|
|
8286
|
+
requireConnection(store, project.canonicalDomain);
|
|
8348
8287
|
const { siteUrl } = request.body ?? {};
|
|
8349
8288
|
if (!siteUrl || typeof siteUrl !== "string") {
|
|
8350
|
-
|
|
8351
|
-
return reply.status(err.statusCode).send(err.toJSON());
|
|
8289
|
+
throw validationError("siteUrl is required");
|
|
8352
8290
|
}
|
|
8353
8291
|
store.updateConnection(project.canonicalDomain, {
|
|
8354
8292
|
siteUrl,
|
|
@@ -8356,12 +8294,10 @@ async function bingRoutes(app, opts) {
|
|
|
8356
8294
|
});
|
|
8357
8295
|
return { siteUrl };
|
|
8358
8296
|
});
|
|
8359
|
-
app.get("/projects/:name/bing/coverage", async (request
|
|
8360
|
-
const store = requireConnectionStore(
|
|
8361
|
-
if (!store) return;
|
|
8297
|
+
app.get("/projects/:name/bing/coverage", async (request) => {
|
|
8298
|
+
const store = requireConnectionStore();
|
|
8362
8299
|
const project = resolveProject(app.db, request.params.name);
|
|
8363
|
-
|
|
8364
|
-
if (!conn) return;
|
|
8300
|
+
requireConnection(store, project.canonicalDomain);
|
|
8365
8301
|
const allInspections = app.db.select().from(bingUrlInspections).where(eq15(bingUrlInspections.projectId, project.id)).orderBy(desc6(bingUrlInspections.inspectedAt)).all();
|
|
8366
8302
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
8367
8303
|
const definitiveByUrl = /* @__PURE__ */ new Map();
|
|
@@ -8446,9 +8382,8 @@ async function bingRoutes(app, opts) {
|
|
|
8446
8382
|
unknown: unknownUrls.map(formatRow)
|
|
8447
8383
|
};
|
|
8448
8384
|
});
|
|
8449
|
-
app.get("/projects/:name/bing/coverage/history", async (request
|
|
8450
|
-
|
|
8451
|
-
if (!store) return;
|
|
8385
|
+
app.get("/projects/:name/bing/coverage/history", async (request) => {
|
|
8386
|
+
requireConnectionStore();
|
|
8452
8387
|
const project = resolveProject(app.db, request.params.name);
|
|
8453
8388
|
const parsed = parseInt(request.query.limit ?? "90", 10);
|
|
8454
8389
|
const limit = Number.isNaN(parsed) || parsed <= 0 ? 90 : parsed;
|
|
@@ -8460,9 +8395,8 @@ async function bingRoutes(app, opts) {
|
|
|
8460
8395
|
unknown: r.unknown
|
|
8461
8396
|
}));
|
|
8462
8397
|
});
|
|
8463
|
-
app.get("/projects/:name/bing/inspections", async (request
|
|
8464
|
-
|
|
8465
|
-
if (!store) return;
|
|
8398
|
+
app.get("/projects/:name/bing/inspections", async (request) => {
|
|
8399
|
+
requireConnectionStore();
|
|
8466
8400
|
const project = resolveProject(app.db, request.params.name);
|
|
8467
8401
|
const { url, limit } = request.query;
|
|
8468
8402
|
const whereClause = url ? and4(eq15(bingUrlInspections.projectId, project.id), eq15(bingUrlInspections.url, url)) : eq15(bingUrlInspections.projectId, project.id);
|
|
@@ -8480,20 +8414,16 @@ async function bingRoutes(app, opts) {
|
|
|
8480
8414
|
discoveryDate: r.discoveryDate ?? null
|
|
8481
8415
|
}));
|
|
8482
8416
|
});
|
|
8483
|
-
app.post("/projects/:name/bing/inspect-url", async (request
|
|
8484
|
-
const store = requireConnectionStore(
|
|
8485
|
-
if (!store) return;
|
|
8417
|
+
app.post("/projects/:name/bing/inspect-url", async (request) => {
|
|
8418
|
+
const store = requireConnectionStore();
|
|
8486
8419
|
const project = resolveProject(app.db, request.params.name);
|
|
8487
|
-
const conn = requireConnection(store, project.canonicalDomain
|
|
8488
|
-
if (!conn) return;
|
|
8420
|
+
const conn = requireConnection(store, project.canonicalDomain);
|
|
8489
8421
|
if (!conn.siteUrl) {
|
|
8490
|
-
|
|
8491
|
-
return reply.status(err.statusCode).send(err.toJSON());
|
|
8422
|
+
throw validationError('No Bing site configured. Run "canonry bing set-site <project> <url>" first.');
|
|
8492
8423
|
}
|
|
8493
8424
|
const { url } = request.body ?? {};
|
|
8494
8425
|
if (!url) {
|
|
8495
|
-
|
|
8496
|
-
return reply.status(err.statusCode).send(err.toJSON());
|
|
8426
|
+
throw validationError("url is required");
|
|
8497
8427
|
}
|
|
8498
8428
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
8499
8429
|
const runId = crypto15.randomUUID();
|
|
@@ -8512,22 +8442,38 @@ async function bingRoutes(app, opts) {
|
|
|
8512
8442
|
domain: project.canonicalDomain,
|
|
8513
8443
|
url,
|
|
8514
8444
|
httpStatus: result.HttpStatus ?? result.HttpCode ?? null,
|
|
8515
|
-
inIndex: result.InIndex ?? null,
|
|
8516
8445
|
documentSize: result.DocumentSize ?? null,
|
|
8517
|
-
lastCrawledDate: result.LastCrawledDate ?? null
|
|
8446
|
+
lastCrawledDate: result.LastCrawledDate ?? null,
|
|
8447
|
+
discoveryDate: result.DiscoveryDate ?? null
|
|
8518
8448
|
});
|
|
8519
8449
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8520
8450
|
const id = crypto15.randomUUID();
|
|
8521
8451
|
const httpCode = result.HttpStatus ?? result.HttpCode ?? null;
|
|
8522
|
-
let derivedInIndex = null;
|
|
8523
|
-
if (result.InIndex != null) {
|
|
8524
|
-
derivedInIndex = result.InIndex;
|
|
8525
|
-
} else if (result.DocumentSize != null && result.DocumentSize > 0) {
|
|
8526
|
-
derivedInIndex = true;
|
|
8527
|
-
}
|
|
8528
8452
|
const lastCrawledDate = parseBingDate(result.LastCrawledDate);
|
|
8529
8453
|
const inIndexDate = parseBingDate(result.InIndexDate);
|
|
8530
8454
|
const discoveryDate = parseBingDate(result.DiscoveryDate);
|
|
8455
|
+
let derivedInIndex = null;
|
|
8456
|
+
if (result.DocumentSize != null && result.DocumentSize > 0) {
|
|
8457
|
+
derivedInIndex = true;
|
|
8458
|
+
} else if (lastCrawledDate != null) {
|
|
8459
|
+
const httpStatus = result.HttpStatus ?? result.HttpCode;
|
|
8460
|
+
derivedInIndex = httpStatus != null && httpStatus >= 400 ? false : true;
|
|
8461
|
+
} else if (discoveryDate != null) {
|
|
8462
|
+
derivedInIndex = false;
|
|
8463
|
+
}
|
|
8464
|
+
if (derivedInIndex === true) {
|
|
8465
|
+
try {
|
|
8466
|
+
const blockedUrls = await loadBlockingCrawlIssues(conn.apiKey, conn.siteUrl, project.canonicalDomain);
|
|
8467
|
+
if (blockedUrls.has(url)) {
|
|
8468
|
+
derivedInIndex = false;
|
|
8469
|
+
}
|
|
8470
|
+
} catch (e) {
|
|
8471
|
+
bingLog("warn", "inspect-url.crawl-issues-lookup-failed", {
|
|
8472
|
+
domain: project.canonicalDomain,
|
|
8473
|
+
error: e instanceof Error ? e.message : String(e)
|
|
8474
|
+
});
|
|
8475
|
+
}
|
|
8476
|
+
}
|
|
8531
8477
|
app.db.insert(bingUrlInspections).values({
|
|
8532
8478
|
id,
|
|
8533
8479
|
projectId: project.id,
|
|
@@ -8563,15 +8509,12 @@ async function bingRoutes(app, opts) {
|
|
|
8563
8509
|
throw e;
|
|
8564
8510
|
}
|
|
8565
8511
|
});
|
|
8566
|
-
app.post("/projects/:name/bing/request-indexing", async (request
|
|
8567
|
-
const store = requireConnectionStore(
|
|
8568
|
-
if (!store) return;
|
|
8512
|
+
app.post("/projects/:name/bing/request-indexing", async (request) => {
|
|
8513
|
+
const store = requireConnectionStore();
|
|
8569
8514
|
const project = resolveProject(app.db, request.params.name);
|
|
8570
|
-
const conn = requireConnection(store, project.canonicalDomain
|
|
8571
|
-
if (!conn) return;
|
|
8515
|
+
const conn = requireConnection(store, project.canonicalDomain);
|
|
8572
8516
|
if (!conn.siteUrl) {
|
|
8573
|
-
|
|
8574
|
-
return reply.status(err.statusCode).send(err.toJSON());
|
|
8517
|
+
throw validationError('No Bing site configured. Run "canonry bing set-site <project> <url>" first.');
|
|
8575
8518
|
}
|
|
8576
8519
|
let urlsToSubmit = request.body?.urls ?? [];
|
|
8577
8520
|
if (request.body?.allUnindexed) {
|
|
@@ -8589,18 +8532,15 @@ async function bingRoutes(app, opts) {
|
|
|
8589
8532
|
}
|
|
8590
8533
|
}
|
|
8591
8534
|
if (unindexedUrls.length === 0) {
|
|
8592
|
-
|
|
8593
|
-
return reply.status(err.statusCode).send(err.toJSON());
|
|
8535
|
+
throw validationError('No unindexed or unknown URLs found. Run "canonry bing inspect <project> <url>" first.');
|
|
8594
8536
|
}
|
|
8595
8537
|
urlsToSubmit = unindexedUrls;
|
|
8596
8538
|
}
|
|
8597
8539
|
if (urlsToSubmit.length === 0) {
|
|
8598
|
-
|
|
8599
|
-
return reply.status(err.statusCode).send(err.toJSON());
|
|
8540
|
+
throw validationError("At least one URL is required (or use allUnindexed: true)");
|
|
8600
8541
|
}
|
|
8601
8542
|
if (urlsToSubmit.length > BING_SUBMIT_URL_DAILY_LIMIT) {
|
|
8602
|
-
|
|
8603
|
-
return reply.status(err.statusCode).send(err.toJSON());
|
|
8543
|
+
throw validationError(`Cannot submit more than ${BING_SUBMIT_URL_DAILY_LIMIT} URLs per day (got ${urlsToSubmit.length})`);
|
|
8604
8544
|
}
|
|
8605
8545
|
const results = [];
|
|
8606
8546
|
bingLog("info", "index-submit.start", { domain: project.canonicalDomain, siteUrl: conn.siteUrl, urlCount: urlsToSubmit.length, allUnindexed: !!request.body?.allUnindexed });
|
|
@@ -8643,15 +8583,12 @@ async function bingRoutes(app, opts) {
|
|
|
8643
8583
|
results
|
|
8644
8584
|
};
|
|
8645
8585
|
});
|
|
8646
|
-
app.get("/projects/:name/bing/performance", async (request
|
|
8647
|
-
const store = requireConnectionStore(
|
|
8648
|
-
if (!store) return;
|
|
8586
|
+
app.get("/projects/:name/bing/performance", async (request) => {
|
|
8587
|
+
const store = requireConnectionStore();
|
|
8649
8588
|
const project = resolveProject(app.db, request.params.name);
|
|
8650
|
-
const conn = requireConnection(store, project.canonicalDomain
|
|
8651
|
-
if (!conn) return;
|
|
8589
|
+
const conn = requireConnection(store, project.canonicalDomain);
|
|
8652
8590
|
if (!conn.siteUrl) {
|
|
8653
|
-
|
|
8654
|
-
return reply.status(err.statusCode).send(err.toJSON());
|
|
8591
|
+
throw validationError('No Bing site configured. Run "canonry bing set-site <project> <url>" first.');
|
|
8655
8592
|
}
|
|
8656
8593
|
const stats = await getKeywordStats(conn.apiKey, conn.siteUrl);
|
|
8657
8594
|
return stats.map((s) => ({
|
|
@@ -10558,68 +10495,51 @@ function parseEnvInput(value, fieldName = "env") {
|
|
|
10558
10495
|
}
|
|
10559
10496
|
return env;
|
|
10560
10497
|
}
|
|
10561
|
-
function
|
|
10562
|
-
if (!(error instanceof WordpressApiError)) return false;
|
|
10563
|
-
let appError;
|
|
10498
|
+
function toAppError(error) {
|
|
10564
10499
|
switch (error.code) {
|
|
10565
10500
|
case "AUTH_INVALID":
|
|
10566
|
-
|
|
10567
|
-
break;
|
|
10501
|
+
return new AppError("AUTH_INVALID", error.message, error.statusCode);
|
|
10568
10502
|
case "NOT_FOUND":
|
|
10569
|
-
|
|
10570
|
-
break;
|
|
10571
|
-
case "UPSTREAM_ERROR":
|
|
10572
|
-
appError = providerError(error.message, { statusCode: error.statusCode });
|
|
10573
|
-
break;
|
|
10503
|
+
return new AppError("NOT_FOUND", error.message, error.statusCode);
|
|
10574
10504
|
case "UNSUPPORTED":
|
|
10575
10505
|
case "VALIDATION_ERROR":
|
|
10576
|
-
|
|
10577
|
-
|
|
10506
|
+
return validationError(error.message);
|
|
10507
|
+
case "UPSTREAM_ERROR":
|
|
10578
10508
|
default:
|
|
10579
|
-
|
|
10580
|
-
break;
|
|
10509
|
+
return providerError(error.message, { statusCode: error.statusCode });
|
|
10581
10510
|
}
|
|
10582
|
-
reply.status(appError.statusCode).send(appError.toJSON());
|
|
10583
|
-
return true;
|
|
10584
10511
|
}
|
|
10585
|
-
async function withWordpressErrorHandling(
|
|
10512
|
+
async function withWordpressErrorHandling(handler) {
|
|
10586
10513
|
try {
|
|
10587
10514
|
return await handler();
|
|
10588
10515
|
} catch (error) {
|
|
10589
|
-
if (
|
|
10516
|
+
if (error instanceof WordpressApiError) throw toAppError(error);
|
|
10590
10517
|
throw error;
|
|
10591
10518
|
}
|
|
10592
10519
|
}
|
|
10593
10520
|
async function wordpressRoutes(app, opts) {
|
|
10594
|
-
function requireStore(
|
|
10521
|
+
function requireStore() {
|
|
10595
10522
|
if (opts.wordpressConnectionStore) return opts.wordpressConnectionStore;
|
|
10596
|
-
|
|
10597
|
-
reply.status(err.statusCode).send(err.toJSON());
|
|
10598
|
-
return null;
|
|
10523
|
+
throw validationError("WordPress connection storage is not configured for this deployment");
|
|
10599
10524
|
}
|
|
10600
|
-
function requireConnection(store, projectName
|
|
10525
|
+
function requireConnection(store, projectName) {
|
|
10601
10526
|
const connection = store.getConnection(projectName);
|
|
10602
10527
|
if (!connection) {
|
|
10603
|
-
|
|
10604
|
-
reply.status(err.statusCode).send(err.toJSON());
|
|
10605
|
-
return null;
|
|
10528
|
+
throw validationError(`No WordPress connection found for project "${projectName}". Run "canonry wordpress connect ${projectName}" first.`);
|
|
10606
10529
|
}
|
|
10607
10530
|
return connection;
|
|
10608
10531
|
}
|
|
10609
|
-
app.post("/projects/:name/wordpress/connect", async (request
|
|
10610
|
-
return withWordpressErrorHandling(
|
|
10611
|
-
const store = requireStore(
|
|
10612
|
-
if (!store) return;
|
|
10532
|
+
app.post("/projects/:name/wordpress/connect", async (request) => {
|
|
10533
|
+
return withWordpressErrorHandling(async () => {
|
|
10534
|
+
const store = requireStore();
|
|
10613
10535
|
const project = resolveProject(app.db, request.params.name);
|
|
10614
10536
|
const { url, stagingUrl, username, appPassword } = request.body ?? {};
|
|
10615
10537
|
if (!url || !username || !appPassword) {
|
|
10616
|
-
|
|
10617
|
-
return reply.status(err.statusCode).send(err.toJSON());
|
|
10538
|
+
throw validationError("url, username, and appPassword are required");
|
|
10618
10539
|
}
|
|
10619
10540
|
const defaultEnv = parseEnvInput(request.body?.defaultEnv, "defaultEnv") ?? (stagingUrl ? "staging" : "live");
|
|
10620
10541
|
if (defaultEnv === "staging" && !stagingUrl) {
|
|
10621
|
-
|
|
10622
|
-
return reply.status(err.statusCode).send(err.toJSON());
|
|
10542
|
+
throw validationError('defaultEnv "staging" requires stagingUrl');
|
|
10623
10543
|
}
|
|
10624
10544
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
10625
10545
|
const existing = store.getConnection(project.name);
|
|
@@ -10655,13 +10575,11 @@ async function wordpressRoutes(app, opts) {
|
|
|
10655
10575
|
});
|
|
10656
10576
|
});
|
|
10657
10577
|
app.delete("/projects/:name/wordpress/disconnect", async (request, reply) => {
|
|
10658
|
-
const store = requireStore(
|
|
10659
|
-
if (!store) return;
|
|
10578
|
+
const store = requireStore();
|
|
10660
10579
|
const project = resolveProject(app.db, request.params.name);
|
|
10661
10580
|
const deleted = store.deleteConnection(project.name);
|
|
10662
10581
|
if (!deleted) {
|
|
10663
|
-
|
|
10664
|
-
return reply.status(err.statusCode).send(err.toJSON());
|
|
10582
|
+
throw notFound("WordPress connection", project.name);
|
|
10665
10583
|
}
|
|
10666
10584
|
writeAuditLog(app.db, {
|
|
10667
10585
|
projectId: project.id,
|
|
@@ -10696,13 +10614,11 @@ async function wordpressRoutes(app, opts) {
|
|
|
10696
10614
|
adminUrl: getWpStagingAdminUrl(connection.url)
|
|
10697
10615
|
};
|
|
10698
10616
|
});
|
|
10699
|
-
app.get("/projects/:name/wordpress/pages", async (request
|
|
10700
|
-
return withWordpressErrorHandling(
|
|
10701
|
-
const store = requireStore(
|
|
10702
|
-
if (!store) return;
|
|
10617
|
+
app.get("/projects/:name/wordpress/pages", async (request) => {
|
|
10618
|
+
return withWordpressErrorHandling(async () => {
|
|
10619
|
+
const store = requireStore();
|
|
10703
10620
|
const project = resolveProject(app.db, request.params.name);
|
|
10704
|
-
const connection = requireConnection(store, project.name
|
|
10705
|
-
if (!connection) return;
|
|
10621
|
+
const connection = requireConnection(store, project.name);
|
|
10706
10622
|
const env = parseEnvInput(request.query?.env);
|
|
10707
10623
|
return {
|
|
10708
10624
|
env: env ?? connection.defaultEnv,
|
|
@@ -10710,34 +10626,28 @@ async function wordpressRoutes(app, opts) {
|
|
|
10710
10626
|
};
|
|
10711
10627
|
});
|
|
10712
10628
|
});
|
|
10713
|
-
app.get("/projects/:name/wordpress/page", async (request
|
|
10714
|
-
return withWordpressErrorHandling(
|
|
10715
|
-
const store = requireStore(
|
|
10716
|
-
if (!store) return;
|
|
10629
|
+
app.get("/projects/:name/wordpress/page", async (request) => {
|
|
10630
|
+
return withWordpressErrorHandling(async () => {
|
|
10631
|
+
const store = requireStore();
|
|
10717
10632
|
const project = resolveProject(app.db, request.params.name);
|
|
10718
|
-
const connection = requireConnection(store, project.name
|
|
10719
|
-
if (!connection) return;
|
|
10633
|
+
const connection = requireConnection(store, project.name);
|
|
10720
10634
|
const slug = request.query?.slug?.trim();
|
|
10721
10635
|
if (!slug) {
|
|
10722
|
-
|
|
10723
|
-
return reply.status(err.statusCode).send(err.toJSON());
|
|
10636
|
+
throw validationError("slug is required");
|
|
10724
10637
|
}
|
|
10725
10638
|
const env = parseEnvInput(request.query?.env);
|
|
10726
10639
|
return getPageDetail(connection, slug, env);
|
|
10727
10640
|
});
|
|
10728
10641
|
});
|
|
10729
|
-
app.post("/projects/:name/wordpress/pages", async (request
|
|
10730
|
-
return withWordpressErrorHandling(
|
|
10731
|
-
const store = requireStore(
|
|
10732
|
-
if (!store) return;
|
|
10642
|
+
app.post("/projects/:name/wordpress/pages", async (request) => {
|
|
10643
|
+
return withWordpressErrorHandling(async () => {
|
|
10644
|
+
const store = requireStore();
|
|
10733
10645
|
const project = resolveProject(app.db, request.params.name);
|
|
10734
|
-
const connection = requireConnection(store, project.name
|
|
10735
|
-
if (!connection) return;
|
|
10646
|
+
const connection = requireConnection(store, project.name);
|
|
10736
10647
|
const { title, slug, content, status } = request.body ?? {};
|
|
10737
10648
|
const env = parseEnvInput(request.body?.env);
|
|
10738
10649
|
if (!title || !slug || !content) {
|
|
10739
|
-
|
|
10740
|
-
return reply.status(err.statusCode).send(err.toJSON());
|
|
10650
|
+
throw validationError("title, slug, and content are required");
|
|
10741
10651
|
}
|
|
10742
10652
|
const created = await createPage(connection, { title, slug, content, status }, env);
|
|
10743
10653
|
writeAuditLog(app.db, {
|
|
@@ -10750,17 +10660,14 @@ async function wordpressRoutes(app, opts) {
|
|
|
10750
10660
|
return created;
|
|
10751
10661
|
});
|
|
10752
10662
|
});
|
|
10753
|
-
app.put("/projects/:name/wordpress/page", async (request
|
|
10754
|
-
return withWordpressErrorHandling(
|
|
10755
|
-
const store = requireStore(
|
|
10756
|
-
if (!store) return;
|
|
10663
|
+
app.put("/projects/:name/wordpress/page", async (request) => {
|
|
10664
|
+
return withWordpressErrorHandling(async () => {
|
|
10665
|
+
const store = requireStore();
|
|
10757
10666
|
const project = resolveProject(app.db, request.params.name);
|
|
10758
|
-
const connection = requireConnection(store, project.name
|
|
10759
|
-
if (!connection) return;
|
|
10667
|
+
const connection = requireConnection(store, project.name);
|
|
10760
10668
|
const currentSlug = request.body?.currentSlug?.trim();
|
|
10761
10669
|
if (!currentSlug) {
|
|
10762
|
-
|
|
10763
|
-
return reply.status(err.statusCode).send(err.toJSON());
|
|
10670
|
+
throw validationError("currentSlug is required");
|
|
10764
10671
|
}
|
|
10765
10672
|
const env = parseEnvInput(request.body?.env);
|
|
10766
10673
|
const updated = await updatePageBySlug(connection, currentSlug, {
|
|
@@ -10779,17 +10686,14 @@ async function wordpressRoutes(app, opts) {
|
|
|
10779
10686
|
return updated;
|
|
10780
10687
|
});
|
|
10781
10688
|
});
|
|
10782
|
-
app.post("/projects/:name/wordpress/page/meta", async (request
|
|
10783
|
-
return withWordpressErrorHandling(
|
|
10784
|
-
const store = requireStore(
|
|
10785
|
-
if (!store) return;
|
|
10689
|
+
app.post("/projects/:name/wordpress/page/meta", async (request) => {
|
|
10690
|
+
return withWordpressErrorHandling(async () => {
|
|
10691
|
+
const store = requireStore();
|
|
10786
10692
|
const project = resolveProject(app.db, request.params.name);
|
|
10787
|
-
const connection = requireConnection(store, project.name
|
|
10788
|
-
if (!connection) return;
|
|
10693
|
+
const connection = requireConnection(store, project.name);
|
|
10789
10694
|
const slug = request.body?.slug?.trim();
|
|
10790
10695
|
if (!slug) {
|
|
10791
|
-
|
|
10792
|
-
return reply.status(err.statusCode).send(err.toJSON());
|
|
10696
|
+
throw validationError("slug is required");
|
|
10793
10697
|
}
|
|
10794
10698
|
const env = parseEnvInput(request.body?.env);
|
|
10795
10699
|
const updated = await setSeoMeta(connection, slug, {
|
|
@@ -10807,22 +10711,18 @@ async function wordpressRoutes(app, opts) {
|
|
|
10807
10711
|
return updated;
|
|
10808
10712
|
});
|
|
10809
10713
|
});
|
|
10810
|
-
app.post("/projects/:name/wordpress/pages/meta/bulk", async (request
|
|
10811
|
-
return withWordpressErrorHandling(
|
|
10812
|
-
const store = requireStore(
|
|
10813
|
-
if (!store) return;
|
|
10714
|
+
app.post("/projects/:name/wordpress/pages/meta/bulk", async (request) => {
|
|
10715
|
+
return withWordpressErrorHandling(async () => {
|
|
10716
|
+
const store = requireStore();
|
|
10814
10717
|
const project = resolveProject(app.db, request.params.name);
|
|
10815
|
-
const connection = requireConnection(store, project.name
|
|
10816
|
-
if (!connection) return;
|
|
10718
|
+
const connection = requireConnection(store, project.name);
|
|
10817
10719
|
const entries = request.body?.entries;
|
|
10818
10720
|
if (!Array.isArray(entries) || entries.length === 0) {
|
|
10819
|
-
|
|
10820
|
-
return reply.status(err.statusCode).send(err.toJSON());
|
|
10721
|
+
throw validationError("entries array is required and must not be empty");
|
|
10821
10722
|
}
|
|
10822
10723
|
for (const entry of entries) {
|
|
10823
10724
|
if (!entry.slug?.trim()) {
|
|
10824
|
-
|
|
10825
|
-
return reply.status(err.statusCode).send(err.toJSON());
|
|
10725
|
+
throw validationError("each entry must have a slug");
|
|
10826
10726
|
}
|
|
10827
10727
|
}
|
|
10828
10728
|
const env = parseEnvInput(request.body?.env);
|
|
@@ -10840,126 +10740,103 @@ async function wordpressRoutes(app, opts) {
|
|
|
10840
10740
|
return result;
|
|
10841
10741
|
});
|
|
10842
10742
|
});
|
|
10843
|
-
app.get("/projects/:name/wordpress/schema", async (request
|
|
10844
|
-
return withWordpressErrorHandling(
|
|
10845
|
-
const store = requireStore(
|
|
10846
|
-
if (!store) return;
|
|
10743
|
+
app.get("/projects/:name/wordpress/schema", async (request) => {
|
|
10744
|
+
return withWordpressErrorHandling(async () => {
|
|
10745
|
+
const store = requireStore();
|
|
10847
10746
|
const project = resolveProject(app.db, request.params.name);
|
|
10848
|
-
const connection = requireConnection(store, project.name
|
|
10849
|
-
if (!connection) return;
|
|
10747
|
+
const connection = requireConnection(store, project.name);
|
|
10850
10748
|
const slug = request.query?.slug?.trim();
|
|
10851
10749
|
if (!slug) {
|
|
10852
|
-
|
|
10853
|
-
return reply.status(err.statusCode).send(err.toJSON());
|
|
10750
|
+
throw validationError("slug is required");
|
|
10854
10751
|
}
|
|
10855
10752
|
const env = parseEnvInput(request.query?.env);
|
|
10856
10753
|
return getPageSchema(connection, slug, env);
|
|
10857
10754
|
});
|
|
10858
10755
|
});
|
|
10859
|
-
app.post("/projects/:name/wordpress/schema/manual", async (request
|
|
10860
|
-
return withWordpressErrorHandling(
|
|
10861
|
-
const store = requireStore(
|
|
10862
|
-
if (!store) return;
|
|
10756
|
+
app.post("/projects/:name/wordpress/schema/manual", async (request) => {
|
|
10757
|
+
return withWordpressErrorHandling(async () => {
|
|
10758
|
+
const store = requireStore();
|
|
10863
10759
|
const project = resolveProject(app.db, request.params.name);
|
|
10864
|
-
const connection = requireConnection(store, project.name
|
|
10865
|
-
if (!connection) return;
|
|
10760
|
+
const connection = requireConnection(store, project.name);
|
|
10866
10761
|
const slug = request.body?.slug?.trim();
|
|
10867
10762
|
const json = request.body?.json;
|
|
10868
10763
|
if (!slug || !json) {
|
|
10869
|
-
|
|
10870
|
-
return reply.status(err.statusCode).send(err.toJSON());
|
|
10764
|
+
throw validationError("slug and json are required");
|
|
10871
10765
|
}
|
|
10872
10766
|
const env = parseEnvInput(request.body?.env);
|
|
10873
10767
|
return buildManualSchemaUpdate(connection, slug, { type: request.body?.type, json }, env);
|
|
10874
10768
|
});
|
|
10875
10769
|
});
|
|
10876
|
-
app.post("/projects/:name/wordpress/schema/deploy", async (request
|
|
10877
|
-
return withWordpressErrorHandling(
|
|
10878
|
-
const store = requireStore(
|
|
10879
|
-
if (!store) return;
|
|
10770
|
+
app.post("/projects/:name/wordpress/schema/deploy", async (request) => {
|
|
10771
|
+
return withWordpressErrorHandling(async () => {
|
|
10772
|
+
const store = requireStore();
|
|
10880
10773
|
const project = resolveProject(app.db, request.params.name);
|
|
10881
|
-
const connection = requireConnection(store, project.name
|
|
10882
|
-
if (!connection) return;
|
|
10774
|
+
const connection = requireConnection(store, project.name);
|
|
10883
10775
|
const profile = request.body?.profile;
|
|
10884
10776
|
if (!profile?.business?.name || !profile?.pages || Object.keys(profile.pages).length === 0) {
|
|
10885
|
-
|
|
10886
|
-
return reply.status(err.statusCode).send(err.toJSON());
|
|
10777
|
+
throw validationError("profile with business.name and non-empty pages is required");
|
|
10887
10778
|
}
|
|
10888
10779
|
const env = parseEnvInput(request.body?.env);
|
|
10889
10780
|
return deploySchemaFromProfile(connection, profile, env);
|
|
10890
10781
|
});
|
|
10891
10782
|
});
|
|
10892
|
-
app.get("/projects/:name/wordpress/schema/status", async (request
|
|
10893
|
-
return withWordpressErrorHandling(
|
|
10894
|
-
const store = requireStore(
|
|
10895
|
-
if (!store) return;
|
|
10783
|
+
app.get("/projects/:name/wordpress/schema/status", async (request) => {
|
|
10784
|
+
return withWordpressErrorHandling(async () => {
|
|
10785
|
+
const store = requireStore();
|
|
10896
10786
|
const project = resolveProject(app.db, request.params.name);
|
|
10897
|
-
const connection = requireConnection(store, project.name
|
|
10898
|
-
if (!connection) return;
|
|
10787
|
+
const connection = requireConnection(store, project.name);
|
|
10899
10788
|
const env = parseEnvInput(request.query?.env);
|
|
10900
10789
|
return getSchemaStatus(connection, env);
|
|
10901
10790
|
});
|
|
10902
10791
|
});
|
|
10903
|
-
app.get("/projects/:name/wordpress/llms-txt", async (request
|
|
10904
|
-
return withWordpressErrorHandling(
|
|
10905
|
-
const store = requireStore(
|
|
10906
|
-
if (!store) return;
|
|
10792
|
+
app.get("/projects/:name/wordpress/llms-txt", async (request) => {
|
|
10793
|
+
return withWordpressErrorHandling(async () => {
|
|
10794
|
+
const store = requireStore();
|
|
10907
10795
|
const project = resolveProject(app.db, request.params.name);
|
|
10908
|
-
const connection = requireConnection(store, project.name
|
|
10909
|
-
if (!connection) return;
|
|
10796
|
+
const connection = requireConnection(store, project.name);
|
|
10910
10797
|
const env = parseEnvInput(request.query?.env);
|
|
10911
10798
|
return getLlmsTxt(connection, env);
|
|
10912
10799
|
});
|
|
10913
10800
|
});
|
|
10914
|
-
app.post("/projects/:name/wordpress/llms-txt/manual", async (request
|
|
10915
|
-
return withWordpressErrorHandling(
|
|
10916
|
-
const store = requireStore(
|
|
10917
|
-
if (!store) return;
|
|
10801
|
+
app.post("/projects/:name/wordpress/llms-txt/manual", async (request) => {
|
|
10802
|
+
return withWordpressErrorHandling(async () => {
|
|
10803
|
+
const store = requireStore();
|
|
10918
10804
|
const project = resolveProject(app.db, request.params.name);
|
|
10919
|
-
const connection = requireConnection(store, project.name
|
|
10920
|
-
if (!connection) return;
|
|
10805
|
+
const connection = requireConnection(store, project.name);
|
|
10921
10806
|
const content = request.body?.content;
|
|
10922
10807
|
if (!content) {
|
|
10923
|
-
|
|
10924
|
-
return reply.status(err.statusCode).send(err.toJSON());
|
|
10808
|
+
throw validationError("content is required");
|
|
10925
10809
|
}
|
|
10926
10810
|
const env = parseEnvInput(request.body?.env);
|
|
10927
10811
|
return buildManualLlmsTxtUpdate(connection, content, env);
|
|
10928
10812
|
});
|
|
10929
10813
|
});
|
|
10930
|
-
app.get("/projects/:name/wordpress/audit", async (request
|
|
10931
|
-
return withWordpressErrorHandling(
|
|
10932
|
-
const store = requireStore(
|
|
10933
|
-
if (!store) return;
|
|
10814
|
+
app.get("/projects/:name/wordpress/audit", async (request) => {
|
|
10815
|
+
return withWordpressErrorHandling(async () => {
|
|
10816
|
+
const store = requireStore();
|
|
10934
10817
|
const project = resolveProject(app.db, request.params.name);
|
|
10935
|
-
const connection = requireConnection(store, project.name
|
|
10936
|
-
if (!connection) return;
|
|
10818
|
+
const connection = requireConnection(store, project.name);
|
|
10937
10819
|
const env = parseEnvInput(request.query?.env);
|
|
10938
10820
|
return runAudit(connection, env);
|
|
10939
10821
|
});
|
|
10940
10822
|
});
|
|
10941
|
-
app.get("/projects/:name/wordpress/diff", async (request
|
|
10942
|
-
return withWordpressErrorHandling(
|
|
10943
|
-
const store = requireStore(
|
|
10944
|
-
if (!store) return;
|
|
10823
|
+
app.get("/projects/:name/wordpress/diff", async (request) => {
|
|
10824
|
+
return withWordpressErrorHandling(async () => {
|
|
10825
|
+
const store = requireStore();
|
|
10945
10826
|
const project = resolveProject(app.db, request.params.name);
|
|
10946
|
-
const connection = requireConnection(store, project.name
|
|
10947
|
-
if (!connection) return;
|
|
10827
|
+
const connection = requireConnection(store, project.name);
|
|
10948
10828
|
const slug = request.query?.slug?.trim();
|
|
10949
10829
|
if (!slug) {
|
|
10950
|
-
|
|
10951
|
-
return reply.status(err.statusCode).send(err.toJSON());
|
|
10830
|
+
throw validationError("slug is required");
|
|
10952
10831
|
}
|
|
10953
10832
|
return diffPageAcrossEnvironments(connection, slug);
|
|
10954
10833
|
});
|
|
10955
10834
|
});
|
|
10956
|
-
app.get("/projects/:name/wordpress/staging/status", async (request
|
|
10957
|
-
return withWordpressErrorHandling(
|
|
10958
|
-
const store = requireStore(
|
|
10959
|
-
if (!store) return;
|
|
10835
|
+
app.get("/projects/:name/wordpress/staging/status", async (request) => {
|
|
10836
|
+
return withWordpressErrorHandling(async () => {
|
|
10837
|
+
const store = requireStore();
|
|
10960
10838
|
const project = resolveProject(app.db, request.params.name);
|
|
10961
|
-
const connection = requireConnection(store, project.name
|
|
10962
|
-
if (!connection) return;
|
|
10839
|
+
const connection = requireConnection(store, project.name);
|
|
10963
10840
|
const plugins = await listActivePlugins(connection, "live");
|
|
10964
10841
|
return {
|
|
10965
10842
|
stagingConfigured: Boolean(connection.stagingUrl),
|
|
@@ -10969,34 +10846,28 @@ async function wordpressRoutes(app, opts) {
|
|
|
10969
10846
|
};
|
|
10970
10847
|
});
|
|
10971
10848
|
});
|
|
10972
|
-
app.post("/projects/:name/wordpress/staging/push", async (request
|
|
10973
|
-
return withWordpressErrorHandling(
|
|
10974
|
-
const store = requireStore(
|
|
10975
|
-
if (!store) return;
|
|
10849
|
+
app.post("/projects/:name/wordpress/staging/push", async (request) => {
|
|
10850
|
+
return withWordpressErrorHandling(async () => {
|
|
10851
|
+
const store = requireStore();
|
|
10976
10852
|
const project = resolveProject(app.db, request.params.name);
|
|
10977
|
-
const connection = requireConnection(store, project.name
|
|
10978
|
-
if (!connection) return;
|
|
10853
|
+
const connection = requireConnection(store, project.name);
|
|
10979
10854
|
if (!connection.stagingUrl) {
|
|
10980
|
-
|
|
10981
|
-
return reply.status(err.statusCode).send(err.toJSON());
|
|
10855
|
+
throw validationError("No staging URL configured for this project. Reconnect with --staging-url before using staging push.");
|
|
10982
10856
|
}
|
|
10983
10857
|
return buildManualStagingPush(connection);
|
|
10984
10858
|
});
|
|
10985
10859
|
});
|
|
10986
|
-
app.post("/projects/:name/wordpress/onboard", async (request
|
|
10987
|
-
return withWordpressErrorHandling(
|
|
10988
|
-
const store = requireStore(
|
|
10989
|
-
if (!store) return;
|
|
10860
|
+
app.post("/projects/:name/wordpress/onboard", async (request) => {
|
|
10861
|
+
return withWordpressErrorHandling(async () => {
|
|
10862
|
+
const store = requireStore();
|
|
10990
10863
|
const project = resolveProject(app.db, request.params.name);
|
|
10991
10864
|
const { url, username, appPassword, stagingUrl, profile, skipSchema, skipSubmit } = request.body ?? {};
|
|
10992
10865
|
if (!url || !username || !appPassword) {
|
|
10993
|
-
|
|
10994
|
-
return reply.status(err.statusCode).send(err.toJSON());
|
|
10866
|
+
throw validationError("url, username, and appPassword are required");
|
|
10995
10867
|
}
|
|
10996
10868
|
const defaultEnv = parseEnvInput(request.body?.defaultEnv, "defaultEnv") ?? (stagingUrl ? "staging" : "live");
|
|
10997
10869
|
if (defaultEnv === "staging" && !stagingUrl) {
|
|
10998
|
-
|
|
10999
|
-
return reply.status(err.statusCode).send(err.toJSON());
|
|
10870
|
+
throw validationError('defaultEnv "staging" requires stagingUrl');
|
|
11000
10871
|
}
|
|
11001
10872
|
const steps = [];
|
|
11002
10873
|
let connection = null;
|
|
@@ -14543,6 +14414,26 @@ var ProviderExecutionGate = class {
|
|
|
14543
14414
|
}
|
|
14544
14415
|
}
|
|
14545
14416
|
};
|
|
14417
|
+
async function runWithConcurrency(items, limit, worker) {
|
|
14418
|
+
if (items.length === 0) return;
|
|
14419
|
+
const cap = Math.max(1, Math.min(limit, items.length));
|
|
14420
|
+
let cursor = 0;
|
|
14421
|
+
const next = async () => {
|
|
14422
|
+
while (true) {
|
|
14423
|
+
const idx = cursor++;
|
|
14424
|
+
if (idx >= items.length) return;
|
|
14425
|
+
await worker(items[idx]);
|
|
14426
|
+
}
|
|
14427
|
+
};
|
|
14428
|
+
await Promise.all(Array.from({ length: cap }, next));
|
|
14429
|
+
}
|
|
14430
|
+
var PROVIDER_FANOUT_DEFAULT = 8;
|
|
14431
|
+
function resolveProviderFanout() {
|
|
14432
|
+
const raw = process.env.CANONRY_PROVIDER_FANOUT;
|
|
14433
|
+
if (!raw) return PROVIDER_FANOUT_DEFAULT;
|
|
14434
|
+
const parsed = Number.parseInt(raw, 10);
|
|
14435
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : PROVIDER_FANOUT_DEFAULT;
|
|
14436
|
+
}
|
|
14546
14437
|
var JobRunner = class {
|
|
14547
14438
|
db;
|
|
14548
14439
|
registry;
|
|
@@ -14750,11 +14641,11 @@ var JobRunner = class {
|
|
|
14750
14641
|
}
|
|
14751
14642
|
}
|
|
14752
14643
|
};
|
|
14753
|
-
await
|
|
14644
|
+
await runWithConcurrency(apiProviders, resolveProviderFanout(), async (registeredProvider) => {
|
|
14754
14645
|
await Promise.all(projectKeywords.map(async (kw) => {
|
|
14755
14646
|
await processKeywordForProvider(registeredProvider, kw);
|
|
14756
14647
|
}));
|
|
14757
|
-
})
|
|
14648
|
+
});
|
|
14758
14649
|
for (const registeredProvider of browserProviders) {
|
|
14759
14650
|
for (const kw of projectKeywords) {
|
|
14760
14651
|
await processKeywordForProvider(registeredProvider, kw);
|