@ainyc/canonry 1.20.0 → 1.21.0
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/README.md +42 -4
- package/assets/assets/index-3PC6-Oip.css +1 -0
- package/assets/assets/index-Bd0xUz_T.js +246 -0
- package/assets/index.html +2 -2
- package/bin/canonry.mjs +1 -1
- package/dist/{chunk-V7JJJDPL.js → chunk-FR3LEBGX.js} +2012 -633
- package/dist/cli.js +4199 -3067
- package/dist/index.d.ts +1 -6
- package/dist/index.js +1 -1
- package/package.json +6 -5
- package/assets/assets/index-BAlQzE7t.js +0 -246
- package/assets/assets/index-D-2VeJdG.css +0 -1
|
@@ -35,7 +35,7 @@ function loadConfig() {
|
|
|
35
35
|
throw new Error(
|
|
36
36
|
`Config not found at ${configPath}.
|
|
37
37
|
Run "canonry init" to set up interactively, or "canonry init --gemini-key <key>" for non-interactive setup.
|
|
38
|
-
For CI/Docker, use "canonry bootstrap" with env vars (GEMINI_API_KEY, OPENAI_API_KEY, ANTHROPIC_API_KEY, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET).`
|
|
38
|
+
For CI/Docker, use "canonry bootstrap" with env vars (GEMINI_API_KEY, OPENAI_API_KEY, ANTHROPIC_API_KEY, PERPLEXITY_API_KEY, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET).`
|
|
39
39
|
);
|
|
40
40
|
}
|
|
41
41
|
const raw = fs.readFileSync(configPath, "utf-8");
|
|
@@ -72,6 +72,16 @@ Do not write config.yaml by hand; use "canonry init", "canonry settings", or "ca
|
|
|
72
72
|
} catch {
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
|
+
if (parsed.basePath) {
|
|
76
|
+
const normalizedBase = "/" + parsed.basePath.replace(/^\/|\/$/g, "");
|
|
77
|
+
try {
|
|
78
|
+
const url = new URL(parsed.apiUrl);
|
|
79
|
+
if (normalizedBase !== "/" && !url.pathname.startsWith(normalizedBase)) {
|
|
80
|
+
parsed.apiUrl = url.origin + normalizedBase;
|
|
81
|
+
}
|
|
82
|
+
} catch {
|
|
83
|
+
}
|
|
84
|
+
}
|
|
75
85
|
return parsed;
|
|
76
86
|
}
|
|
77
87
|
function saveConfig(config) {
|
|
@@ -167,7 +177,7 @@ import { fileURLToPath } from "url";
|
|
|
167
177
|
import Fastify from "fastify";
|
|
168
178
|
|
|
169
179
|
// ../contracts/src/config-schema.ts
|
|
170
|
-
import { z as
|
|
180
|
+
import { z as z4 } from "zod";
|
|
171
181
|
|
|
172
182
|
// ../contracts/src/provider.ts
|
|
173
183
|
import { z } from "zod";
|
|
@@ -176,30 +186,18 @@ var providerQuotaPolicySchema = z.object({
|
|
|
176
186
|
maxRequestsPerMinute: z.number().int().positive(),
|
|
177
187
|
maxRequestsPerDay: z.number().int().positive()
|
|
178
188
|
});
|
|
179
|
-
var
|
|
180
|
-
var
|
|
181
|
-
var PROVIDER_MODE = {
|
|
182
|
-
gemini: "api",
|
|
183
|
-
openai: "api",
|
|
184
|
-
claude: "api",
|
|
185
|
-
local: "api",
|
|
186
|
-
"cdp:chatgpt": "browser"
|
|
187
|
-
};
|
|
189
|
+
var providerNameSchema = z.string().min(1);
|
|
190
|
+
var apiProviderNameSchema = z.string().min(1);
|
|
188
191
|
function isBrowserProvider(name) {
|
|
189
|
-
return
|
|
192
|
+
return name.startsWith("cdp:");
|
|
190
193
|
}
|
|
191
194
|
var CDP_TARGETS = ["cdp:chatgpt"];
|
|
192
|
-
function parseProviderName(input) {
|
|
193
|
-
const lower = input.trim().toLowerCase();
|
|
194
|
-
return PROVIDER_NAMES.includes(lower) ? lower : void 0;
|
|
195
|
-
}
|
|
196
195
|
function resolveProviderInput(input) {
|
|
197
196
|
const lower = input.trim().toLowerCase();
|
|
198
197
|
if (lower === "cdp") {
|
|
199
198
|
return [...CDP_TARGETS];
|
|
200
199
|
}
|
|
201
|
-
|
|
202
|
-
return parsed ? [parsed] : [];
|
|
200
|
+
return lower ? [lower] : [];
|
|
203
201
|
}
|
|
204
202
|
var locationContextSchema = z.object({
|
|
205
203
|
label: z.string().min(1),
|
|
@@ -229,118 +227,148 @@ var notificationDtoSchema = z2.object({
|
|
|
229
227
|
updatedAt: z2.string()
|
|
230
228
|
});
|
|
231
229
|
|
|
230
|
+
// ../contracts/src/project.ts
|
|
231
|
+
import { z as z3 } from "zod";
|
|
232
|
+
var configSourceSchema = z3.enum(["cli", "api", "config-file"]);
|
|
233
|
+
function findDuplicateLocationLabels(locations) {
|
|
234
|
+
const seen = /* @__PURE__ */ new Set();
|
|
235
|
+
const duplicates = /* @__PURE__ */ new Set();
|
|
236
|
+
for (const location of locations) {
|
|
237
|
+
if (seen.has(location.label)) {
|
|
238
|
+
duplicates.add(location.label);
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
seen.add(location.label);
|
|
242
|
+
}
|
|
243
|
+
return [...duplicates];
|
|
244
|
+
}
|
|
245
|
+
function hasLocationLabel(locations, label) {
|
|
246
|
+
if (!label) return true;
|
|
247
|
+
return locations.some((location) => location.label === label);
|
|
248
|
+
}
|
|
249
|
+
var projectUpsertRequestSchema = z3.object({
|
|
250
|
+
displayName: z3.string().min(1),
|
|
251
|
+
canonicalDomain: z3.string().min(1),
|
|
252
|
+
ownedDomains: z3.array(z3.string().min(1)).optional(),
|
|
253
|
+
country: z3.string().length(2),
|
|
254
|
+
language: z3.string().min(2),
|
|
255
|
+
tags: z3.array(z3.string()).optional(),
|
|
256
|
+
labels: z3.record(z3.string(), z3.string()).optional(),
|
|
257
|
+
providers: z3.array(providerNameSchema).optional(),
|
|
258
|
+
locations: z3.array(locationContextSchema).optional(),
|
|
259
|
+
defaultLocation: z3.string().nullable().optional(),
|
|
260
|
+
configSource: configSourceSchema.optional()
|
|
261
|
+
});
|
|
262
|
+
var projectDtoSchema = z3.object({
|
|
263
|
+
id: z3.string(),
|
|
264
|
+
name: z3.string(),
|
|
265
|
+
displayName: z3.string().optional(),
|
|
266
|
+
canonicalDomain: z3.string(),
|
|
267
|
+
ownedDomains: z3.array(z3.string()).default([]),
|
|
268
|
+
country: z3.string().length(2),
|
|
269
|
+
language: z3.string().min(2),
|
|
270
|
+
tags: z3.array(z3.string()).default([]),
|
|
271
|
+
labels: z3.record(z3.string(), z3.string()).default({}),
|
|
272
|
+
locations: z3.array(locationContextSchema).default([]),
|
|
273
|
+
defaultLocation: z3.string().nullable().optional(),
|
|
274
|
+
configSource: configSourceSchema.default("cli"),
|
|
275
|
+
configRevision: z3.number().int().positive().default(1),
|
|
276
|
+
createdAt: z3.string().optional(),
|
|
277
|
+
updatedAt: z3.string().optional()
|
|
278
|
+
});
|
|
279
|
+
function normalizeProjectDomain(input) {
|
|
280
|
+
let domain = input.trim().toLowerCase();
|
|
281
|
+
try {
|
|
282
|
+
if (domain.includes("://")) {
|
|
283
|
+
domain = new URL(domain).hostname.toLowerCase();
|
|
284
|
+
}
|
|
285
|
+
} catch {
|
|
286
|
+
}
|
|
287
|
+
return domain.replace(/^www\./, "");
|
|
288
|
+
}
|
|
289
|
+
function effectiveDomains(project) {
|
|
290
|
+
const all = [project.canonicalDomain, ...project.ownedDomains ?? []];
|
|
291
|
+
const seen = /* @__PURE__ */ new Set();
|
|
292
|
+
const result = [];
|
|
293
|
+
for (const d of all) {
|
|
294
|
+
const trimmed = d.trim();
|
|
295
|
+
if (!trimmed) continue;
|
|
296
|
+
const norm = normalizeProjectDomain(trimmed);
|
|
297
|
+
if (seen.has(norm)) continue;
|
|
298
|
+
seen.add(norm);
|
|
299
|
+
result.push(trimmed);
|
|
300
|
+
}
|
|
301
|
+
return result;
|
|
302
|
+
}
|
|
303
|
+
|
|
232
304
|
// ../contracts/src/config-schema.ts
|
|
233
|
-
var configMetadataSchema =
|
|
234
|
-
name:
|
|
305
|
+
var configMetadataSchema = z4.object({
|
|
306
|
+
name: z4.string().min(1).max(63).regex(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/, {
|
|
235
307
|
message: "Name must be a lowercase slug (letters, numbers, hyphens)"
|
|
236
308
|
}),
|
|
237
|
-
labels:
|
|
309
|
+
labels: z4.record(z4.string(), z4.string()).optional().default({})
|
|
238
310
|
});
|
|
239
|
-
var configScheduleSchema =
|
|
240
|
-
preset:
|
|
241
|
-
cron:
|
|
242
|
-
timezone:
|
|
243
|
-
providers:
|
|
311
|
+
var configScheduleSchema = z4.object({
|
|
312
|
+
preset: z4.string().optional(),
|
|
313
|
+
cron: z4.string().optional(),
|
|
314
|
+
timezone: z4.string().optional().default("UTC"),
|
|
315
|
+
providers: z4.array(providerNameSchema).optional().default([])
|
|
244
316
|
}).refine(
|
|
245
317
|
(data) => data.preset && !data.cron || !data.preset && data.cron,
|
|
246
318
|
{ message: 'Exactly one of "preset" or "cron" must be provided' }
|
|
247
319
|
).optional();
|
|
248
|
-
var configNotificationSchema =
|
|
249
|
-
channel:
|
|
250
|
-
url:
|
|
251
|
-
events:
|
|
320
|
+
var configNotificationSchema = z4.object({
|
|
321
|
+
channel: z4.literal("webhook"),
|
|
322
|
+
url: z4.string().url(),
|
|
323
|
+
events: z4.array(notificationEventSchema).min(1)
|
|
252
324
|
});
|
|
253
|
-
var configGoogleSchema =
|
|
254
|
-
gsc:
|
|
255
|
-
propertyUrl:
|
|
325
|
+
var configGoogleSchema = z4.object({
|
|
326
|
+
gsc: z4.object({
|
|
327
|
+
propertyUrl: z4.string()
|
|
256
328
|
}).optional(),
|
|
257
|
-
syncSchedule:
|
|
258
|
-
preset:
|
|
259
|
-
cron:
|
|
329
|
+
syncSchedule: z4.object({
|
|
330
|
+
preset: z4.string().optional(),
|
|
331
|
+
cron: z4.string().optional()
|
|
260
332
|
}).optional()
|
|
261
333
|
}).optional();
|
|
262
|
-
var configSpecSchema =
|
|
263
|
-
displayName:
|
|
264
|
-
canonicalDomain:
|
|
265
|
-
ownedDomains:
|
|
266
|
-
country:
|
|
267
|
-
language:
|
|
268
|
-
keywords:
|
|
269
|
-
competitors:
|
|
270
|
-
providers:
|
|
271
|
-
locations:
|
|
272
|
-
defaultLocation:
|
|
334
|
+
var configSpecSchema = z4.object({
|
|
335
|
+
displayName: z4.string().min(1),
|
|
336
|
+
canonicalDomain: z4.string().min(1),
|
|
337
|
+
ownedDomains: z4.array(z4.string().min(1)).optional().default([]),
|
|
338
|
+
country: z4.string().length(2),
|
|
339
|
+
language: z4.string().min(2),
|
|
340
|
+
keywords: z4.array(z4.string().min(1)).optional().default([]),
|
|
341
|
+
competitors: z4.array(z4.string().min(1)).optional().default([]),
|
|
342
|
+
providers: z4.array(providerNameSchema).optional().default([]),
|
|
343
|
+
locations: z4.array(locationContextSchema).optional().default([]),
|
|
344
|
+
defaultLocation: z4.string().optional(),
|
|
273
345
|
schedule: configScheduleSchema,
|
|
274
|
-
notifications:
|
|
346
|
+
notifications: z4.array(configNotificationSchema).optional().default([]),
|
|
275
347
|
google: configGoogleSchema
|
|
348
|
+
}).superRefine((spec, ctx) => {
|
|
349
|
+
const duplicateLabels = findDuplicateLocationLabels(spec.locations);
|
|
350
|
+
if (duplicateLabels.length > 0) {
|
|
351
|
+
ctx.addIssue({
|
|
352
|
+
code: "custom",
|
|
353
|
+
message: `Duplicate location labels are not allowed: ${duplicateLabels.join(", ")}`,
|
|
354
|
+
path: ["locations"]
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
if (!hasLocationLabel(spec.locations, spec.defaultLocation)) {
|
|
358
|
+
ctx.addIssue({
|
|
359
|
+
code: "custom",
|
|
360
|
+
message: `defaultLocation "${spec.defaultLocation}" must match a configured location label`,
|
|
361
|
+
path: ["defaultLocation"]
|
|
362
|
+
});
|
|
363
|
+
}
|
|
276
364
|
});
|
|
277
|
-
var projectConfigSchema =
|
|
278
|
-
apiVersion:
|
|
279
|
-
kind:
|
|
365
|
+
var projectConfigSchema = z4.object({
|
|
366
|
+
apiVersion: z4.literal("canonry/v1"),
|
|
367
|
+
kind: z4.literal("Project"),
|
|
280
368
|
metadata: configMetadataSchema,
|
|
281
369
|
spec: configSpecSchema
|
|
282
370
|
});
|
|
283
371
|
|
|
284
|
-
// ../contracts/src/models.ts
|
|
285
|
-
var MODEL_REGISTRY = {
|
|
286
|
-
gemini: {
|
|
287
|
-
defaultModel: "gemini-3-flash",
|
|
288
|
-
validationPattern: /^gemini-/,
|
|
289
|
-
validationHint: 'model name must start with "gemini-" (e.g. gemini-3-flash)',
|
|
290
|
-
knownModels: [
|
|
291
|
-
{ id: "gemini-3.1-pro-preview", displayName: "Gemini 3.1 Pro (Preview)", tier: "flagship" },
|
|
292
|
-
{ id: "gemini-3-flash-preview", displayName: "Gemini 3 Flash (Preview)", tier: "standard" },
|
|
293
|
-
{ id: "gemini-3.1-flash-lite-preview", displayName: "Gemini 3.1 Flash-Lite (Preview)", tier: "economy" },
|
|
294
|
-
{ id: "gemini-2.5-flash", displayName: "Gemini 2.5 Flash", tier: "standard" }
|
|
295
|
-
]
|
|
296
|
-
},
|
|
297
|
-
openai: {
|
|
298
|
-
defaultModel: "gpt-5.4",
|
|
299
|
-
validationPattern: /^(gpt-|o\d)/,
|
|
300
|
-
validationHint: "expected a GPT or o-series model name (e.g. gpt-5.4, o3)",
|
|
301
|
-
knownModels: [
|
|
302
|
-
{ id: "gpt-5.4", displayName: "GPT-5.4", tier: "flagship" },
|
|
303
|
-
{ id: "gpt-5.4-pro", displayName: "GPT-5.4 Pro", tier: "flagship" },
|
|
304
|
-
{ id: "gpt-5-mini", displayName: "GPT-5 Mini", tier: "fast" },
|
|
305
|
-
{ id: "gpt-5-nano", displayName: "GPT-5 Nano", tier: "economy" },
|
|
306
|
-
{ id: "gpt-5", displayName: "GPT-5", tier: "standard" },
|
|
307
|
-
{ id: "gpt-4.1", displayName: "GPT-4.1", tier: "standard" }
|
|
308
|
-
]
|
|
309
|
-
},
|
|
310
|
-
claude: {
|
|
311
|
-
defaultModel: "claude-sonnet-4-6",
|
|
312
|
-
validationPattern: /^claude-/,
|
|
313
|
-
validationHint: 'model name must start with "claude-" (e.g. claude-sonnet-4-6)',
|
|
314
|
-
knownModels: [
|
|
315
|
-
{ id: "claude-opus-4-6", displayName: "Claude Opus 4.6", tier: "flagship" },
|
|
316
|
-
{ id: "claude-sonnet-4-6", displayName: "Claude Sonnet 4.6", tier: "standard" },
|
|
317
|
-
{ id: "claude-haiku-4-5", displayName: "Claude Haiku 4.5", tier: "fast" }
|
|
318
|
-
]
|
|
319
|
-
},
|
|
320
|
-
local: {
|
|
321
|
-
defaultModel: "llama3",
|
|
322
|
-
validationPattern: /./,
|
|
323
|
-
validationHint: "any model name accepted",
|
|
324
|
-
knownModels: [
|
|
325
|
-
{ id: "llama3", displayName: "Llama 3", tier: "standard" }
|
|
326
|
-
]
|
|
327
|
-
},
|
|
328
|
-
"cdp:chatgpt": {
|
|
329
|
-
defaultModel: "chatgpt-web",
|
|
330
|
-
validationPattern: /./,
|
|
331
|
-
validationHint: "model is detected from the ChatGPT web UI",
|
|
332
|
-
knownModels: [
|
|
333
|
-
{ id: "chatgpt-web", displayName: "ChatGPT (Web UI)", tier: "standard" }
|
|
334
|
-
]
|
|
335
|
-
}
|
|
336
|
-
};
|
|
337
|
-
function getDefaultModel(provider) {
|
|
338
|
-
return MODEL_REGISTRY[provider].defaultModel;
|
|
339
|
-
}
|
|
340
|
-
function isValidModelName(provider, model) {
|
|
341
|
-
return MODEL_REGISTRY[provider].validationPattern.test(model);
|
|
342
|
-
}
|
|
343
|
-
|
|
344
372
|
// ../contracts/src/errors.ts
|
|
345
373
|
var AppError = class extends Error {
|
|
346
374
|
code;
|
|
@@ -378,184 +406,145 @@ function authInvalid() {
|
|
|
378
406
|
function runInProgress(projectName) {
|
|
379
407
|
return new AppError("RUN_IN_PROGRESS", `A run is already in progress for '${projectName}'`, 409);
|
|
380
408
|
}
|
|
409
|
+
function runNotCancellable(runId, status) {
|
|
410
|
+
return new AppError("RUN_NOT_CANCELLABLE", `Run '${runId}' is already in terminal state '${status}' and cannot be cancelled`, 409);
|
|
411
|
+
}
|
|
381
412
|
function unsupportedKind(kind) {
|
|
382
413
|
return new AppError("UNSUPPORTED_KIND", `Kind '${kind}' is not supported in this version`, 400);
|
|
383
414
|
}
|
|
415
|
+
function notImplemented(message) {
|
|
416
|
+
return new AppError("NOT_IMPLEMENTED", message, 501);
|
|
417
|
+
}
|
|
384
418
|
|
|
385
419
|
// ../contracts/src/google.ts
|
|
386
|
-
import { z as z4 } from "zod";
|
|
387
|
-
var googleConnectionTypeSchema = z4.enum(["gsc", "ga4"]);
|
|
388
|
-
var googleConnectionDtoSchema = z4.object({
|
|
389
|
-
id: z4.string(),
|
|
390
|
-
domain: z4.string(),
|
|
391
|
-
connectionType: googleConnectionTypeSchema,
|
|
392
|
-
propertyId: z4.string().nullable().optional(),
|
|
393
|
-
sitemapUrl: z4.string().nullable().optional(),
|
|
394
|
-
scopes: z4.array(z4.string()).default([]),
|
|
395
|
-
createdAt: z4.string(),
|
|
396
|
-
updatedAt: z4.string()
|
|
397
|
-
});
|
|
398
|
-
var gscSearchDataDtoSchema = z4.object({
|
|
399
|
-
date: z4.string(),
|
|
400
|
-
query: z4.string(),
|
|
401
|
-
page: z4.string(),
|
|
402
|
-
country: z4.string().nullable().optional(),
|
|
403
|
-
device: z4.string().nullable().optional(),
|
|
404
|
-
clicks: z4.number(),
|
|
405
|
-
impressions: z4.number(),
|
|
406
|
-
ctr: z4.number(),
|
|
407
|
-
position: z4.number()
|
|
408
|
-
});
|
|
409
|
-
var gscUrlInspectionDtoSchema = z4.object({
|
|
410
|
-
id: z4.string(),
|
|
411
|
-
url: z4.string(),
|
|
412
|
-
indexingState: z4.string().nullable().optional(),
|
|
413
|
-
verdict: z4.string().nullable().optional(),
|
|
414
|
-
coverageState: z4.string().nullable().optional(),
|
|
415
|
-
pageFetchState: z4.string().nullable().optional(),
|
|
416
|
-
robotsTxtState: z4.string().nullable().optional(),
|
|
417
|
-
crawlTime: z4.string().nullable().optional(),
|
|
418
|
-
lastCrawlResult: z4.string().nullable().optional(),
|
|
419
|
-
isMobileFriendly: z4.boolean().nullable().optional(),
|
|
420
|
-
richResults: z4.array(z4.string()).default([]),
|
|
421
|
-
inspectedAt: z4.string()
|
|
422
|
-
});
|
|
423
|
-
var indexTransitionSchema = z4.enum(["stable", "reindexed", "deindexed", "still-missing", "new"]);
|
|
424
|
-
var gscDeindexedRowSchema = z4.object({
|
|
425
|
-
url: z4.string(),
|
|
426
|
-
previousState: z4.string().nullable(),
|
|
427
|
-
currentState: z4.string().nullable(),
|
|
428
|
-
transitionDate: z4.string()
|
|
429
|
-
});
|
|
430
|
-
var gscReasonGroupSchema = z4.object({
|
|
431
|
-
reason: z4.string(),
|
|
432
|
-
count: z4.number(),
|
|
433
|
-
urls: z4.array(gscUrlInspectionDtoSchema).default([])
|
|
434
|
-
});
|
|
435
|
-
var gscCoverageSummaryDtoSchema = z4.object({
|
|
436
|
-
summary: z4.object({
|
|
437
|
-
total: z4.number(),
|
|
438
|
-
indexed: z4.number(),
|
|
439
|
-
notIndexed: z4.number(),
|
|
440
|
-
deindexed: z4.number(),
|
|
441
|
-
percentage: z4.number()
|
|
442
|
-
}),
|
|
443
|
-
lastInspectedAt: z4.string().nullable(),
|
|
444
|
-
indexed: z4.array(gscUrlInspectionDtoSchema).default([]),
|
|
445
|
-
notIndexed: z4.array(gscUrlInspectionDtoSchema).default([]),
|
|
446
|
-
deindexed: z4.array(gscDeindexedRowSchema).default([]),
|
|
447
|
-
reasonGroups: z4.array(gscReasonGroupSchema).default([])
|
|
448
|
-
});
|
|
449
|
-
var indexingNotificationDtoSchema = z4.object({
|
|
450
|
-
url: z4.string(),
|
|
451
|
-
type: z4.enum(["URL_UPDATED", "URL_DELETED"]),
|
|
452
|
-
notifiedAt: z4.string()
|
|
453
|
-
});
|
|
454
|
-
var indexingRequestResultDtoSchema = z4.object({
|
|
455
|
-
url: z4.string(),
|
|
456
|
-
type: z4.enum(["URL_UPDATED", "URL_DELETED"]),
|
|
457
|
-
notifiedAt: z4.string(),
|
|
458
|
-
status: z4.enum(["success", "error"]),
|
|
459
|
-
error: z4.string().optional()
|
|
460
|
-
});
|
|
461
|
-
var gscCoverageSnapshotDtoSchema = z4.object({
|
|
462
|
-
date: z4.string(),
|
|
463
|
-
indexed: z4.number(),
|
|
464
|
-
notIndexed: z4.number(),
|
|
465
|
-
reasonBreakdown: z4.record(z4.string(), z4.number()).default({})
|
|
466
|
-
});
|
|
467
|
-
|
|
468
|
-
// ../contracts/src/bing.ts
|
|
469
420
|
import { z as z5 } from "zod";
|
|
470
|
-
var
|
|
421
|
+
var googleConnectionTypeSchema = z5.enum(["gsc", "ga4"]);
|
|
422
|
+
var googleConnectionDtoSchema = z5.object({
|
|
471
423
|
id: z5.string(),
|
|
472
424
|
domain: z5.string(),
|
|
473
|
-
|
|
425
|
+
connectionType: googleConnectionTypeSchema,
|
|
426
|
+
propertyId: z5.string().nullable().optional(),
|
|
427
|
+
sitemapUrl: z5.string().nullable().optional(),
|
|
428
|
+
scopes: z5.array(z5.string()).default([]),
|
|
474
429
|
createdAt: z5.string(),
|
|
475
430
|
updatedAt: z5.string()
|
|
476
431
|
});
|
|
477
|
-
var
|
|
432
|
+
var gscSearchDataDtoSchema = z5.object({
|
|
433
|
+
date: z5.string(),
|
|
434
|
+
query: z5.string(),
|
|
435
|
+
page: z5.string(),
|
|
436
|
+
country: z5.string().nullable().optional(),
|
|
437
|
+
device: z5.string().nullable().optional(),
|
|
438
|
+
clicks: z5.number(),
|
|
439
|
+
impressions: z5.number(),
|
|
440
|
+
ctr: z5.number(),
|
|
441
|
+
position: z5.number()
|
|
442
|
+
});
|
|
443
|
+
var gscUrlInspectionDtoSchema = z5.object({
|
|
478
444
|
id: z5.string(),
|
|
479
445
|
url: z5.string(),
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
446
|
+
indexingState: z5.string().nullable().optional(),
|
|
447
|
+
verdict: z5.string().nullable().optional(),
|
|
448
|
+
coverageState: z5.string().nullable().optional(),
|
|
449
|
+
pageFetchState: z5.string().nullable().optional(),
|
|
450
|
+
robotsTxtState: z5.string().nullable().optional(),
|
|
451
|
+
crawlTime: z5.string().nullable().optional(),
|
|
452
|
+
lastCrawlResult: z5.string().nullable().optional(),
|
|
453
|
+
isMobileFriendly: z5.boolean().nullable().optional(),
|
|
454
|
+
richResults: z5.array(z5.string()).default([]),
|
|
484
455
|
inspectedAt: z5.string()
|
|
485
456
|
});
|
|
486
|
-
var
|
|
457
|
+
var indexTransitionSchema = z5.enum(["stable", "reindexed", "deindexed", "still-missing", "new"]);
|
|
458
|
+
var gscDeindexedRowSchema = z5.object({
|
|
459
|
+
url: z5.string(),
|
|
460
|
+
previousState: z5.string().nullable(),
|
|
461
|
+
currentState: z5.string().nullable(),
|
|
462
|
+
transitionDate: z5.string()
|
|
463
|
+
});
|
|
464
|
+
var gscReasonGroupSchema = z5.object({
|
|
465
|
+
reason: z5.string(),
|
|
466
|
+
count: z5.number(),
|
|
467
|
+
urls: z5.array(gscUrlInspectionDtoSchema).default([])
|
|
468
|
+
});
|
|
469
|
+
var gscCoverageSummaryDtoSchema = z5.object({
|
|
487
470
|
summary: z5.object({
|
|
488
471
|
total: z5.number(),
|
|
489
472
|
indexed: z5.number(),
|
|
490
473
|
notIndexed: z5.number(),
|
|
474
|
+
deindexed: z5.number(),
|
|
491
475
|
percentage: z5.number()
|
|
492
476
|
}),
|
|
493
477
|
lastInspectedAt: z5.string().nullable(),
|
|
494
|
-
indexed: z5.array(
|
|
495
|
-
notIndexed: z5.array(
|
|
478
|
+
indexed: z5.array(gscUrlInspectionDtoSchema).default([]),
|
|
479
|
+
notIndexed: z5.array(gscUrlInspectionDtoSchema).default([]),
|
|
480
|
+
deindexed: z5.array(gscDeindexedRowSchema).default([]),
|
|
481
|
+
reasonGroups: z5.array(gscReasonGroupSchema).default([])
|
|
496
482
|
});
|
|
497
|
-
var
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
ctr: z5.number(),
|
|
502
|
-
averagePosition: z5.number()
|
|
483
|
+
var indexingNotificationDtoSchema = z5.object({
|
|
484
|
+
url: z5.string(),
|
|
485
|
+
type: z5.enum(["URL_UPDATED", "URL_DELETED"]),
|
|
486
|
+
notifiedAt: z5.string()
|
|
503
487
|
});
|
|
504
|
-
var
|
|
488
|
+
var indexingRequestResultDtoSchema = z5.object({
|
|
505
489
|
url: z5.string(),
|
|
490
|
+
type: z5.enum(["URL_UPDATED", "URL_DELETED"]),
|
|
491
|
+
notifiedAt: z5.string(),
|
|
506
492
|
status: z5.enum(["success", "error"]),
|
|
507
|
-
submittedAt: z5.string(),
|
|
508
493
|
error: z5.string().optional()
|
|
509
494
|
});
|
|
495
|
+
var gscCoverageSnapshotDtoSchema = z5.object({
|
|
496
|
+
date: z5.string(),
|
|
497
|
+
indexed: z5.number(),
|
|
498
|
+
notIndexed: z5.number(),
|
|
499
|
+
reasonBreakdown: z5.record(z5.string(), z5.number()).default({})
|
|
500
|
+
});
|
|
510
501
|
|
|
511
|
-
// ../contracts/src/
|
|
502
|
+
// ../contracts/src/bing.ts
|
|
512
503
|
import { z as z6 } from "zod";
|
|
513
|
-
var
|
|
514
|
-
var projectDtoSchema = z6.object({
|
|
504
|
+
var bingConnectionDtoSchema = z6.object({
|
|
515
505
|
id: z6.string(),
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
506
|
+
domain: z6.string(),
|
|
507
|
+
siteUrl: z6.string().nullable().optional(),
|
|
508
|
+
createdAt: z6.string(),
|
|
509
|
+
updatedAt: z6.string()
|
|
510
|
+
});
|
|
511
|
+
var bingUrlInspectionDtoSchema = z6.object({
|
|
512
|
+
id: z6.string(),
|
|
513
|
+
url: z6.string(),
|
|
514
|
+
httpCode: z6.number().nullable().optional(),
|
|
515
|
+
inIndex: z6.boolean().nullable().optional(),
|
|
516
|
+
lastCrawledDate: z6.string().nullable().optional(),
|
|
517
|
+
inIndexDate: z6.string().nullable().optional(),
|
|
518
|
+
inspectedAt: z6.string()
|
|
519
|
+
});
|
|
520
|
+
var bingCoverageSummaryDtoSchema = z6.object({
|
|
521
|
+
summary: z6.object({
|
|
522
|
+
total: z6.number(),
|
|
523
|
+
indexed: z6.number(),
|
|
524
|
+
notIndexed: z6.number(),
|
|
525
|
+
percentage: z6.number()
|
|
526
|
+
}),
|
|
527
|
+
lastInspectedAt: z6.string().nullable(),
|
|
528
|
+
indexed: z6.array(bingUrlInspectionDtoSchema).default([]),
|
|
529
|
+
notIndexed: z6.array(bingUrlInspectionDtoSchema).default([])
|
|
530
|
+
});
|
|
531
|
+
var bingKeywordStatsDtoSchema = z6.object({
|
|
532
|
+
query: z6.string(),
|
|
533
|
+
impressions: z6.number(),
|
|
534
|
+
clicks: z6.number(),
|
|
535
|
+
ctr: z6.number(),
|
|
536
|
+
averagePosition: z6.number()
|
|
537
|
+
});
|
|
538
|
+
var bingSubmitResultDtoSchema = z6.object({
|
|
539
|
+
url: z6.string(),
|
|
540
|
+
status: z6.enum(["success", "error"]),
|
|
541
|
+
submittedAt: z6.string(),
|
|
542
|
+
error: z6.string().optional()
|
|
530
543
|
});
|
|
531
|
-
function normalizeProjectDomain(input) {
|
|
532
|
-
let domain = input.trim().toLowerCase();
|
|
533
|
-
try {
|
|
534
|
-
if (domain.includes("://")) {
|
|
535
|
-
domain = new URL(domain).hostname.toLowerCase();
|
|
536
|
-
}
|
|
537
|
-
} catch {
|
|
538
|
-
}
|
|
539
|
-
return domain.replace(/^www\./, "");
|
|
540
|
-
}
|
|
541
|
-
function effectiveDomains(project) {
|
|
542
|
-
const all = [project.canonicalDomain, ...project.ownedDomains ?? []];
|
|
543
|
-
const seen = /* @__PURE__ */ new Set();
|
|
544
|
-
const result = [];
|
|
545
|
-
for (const d of all) {
|
|
546
|
-
const trimmed = d.trim();
|
|
547
|
-
if (!trimmed) continue;
|
|
548
|
-
const norm = normalizeProjectDomain(trimmed);
|
|
549
|
-
if (seen.has(norm)) continue;
|
|
550
|
-
seen.add(norm);
|
|
551
|
-
result.push(trimmed);
|
|
552
|
-
}
|
|
553
|
-
return result;
|
|
554
|
-
}
|
|
555
544
|
|
|
556
545
|
// ../contracts/src/run.ts
|
|
557
546
|
import { z as z7 } from "zod";
|
|
558
|
-
var runStatusSchema = z7.enum(["queued", "running", "completed", "partial", "failed"]);
|
|
547
|
+
var runStatusSchema = z7.enum(["queued", "running", "completed", "partial", "failed", "cancelled"]);
|
|
559
548
|
var runKindSchema = z7.enum(["answer-visibility", "site-audit", "gsc-sync", "inspect-sitemap"]);
|
|
560
549
|
var runTriggerSchema = z7.enum(["manual", "scheduled", "config-apply"]);
|
|
561
550
|
var citationStateSchema = z7.enum(["cited", "not-cited"]);
|
|
@@ -619,6 +608,16 @@ var scheduleDtoSchema = z8.object({
|
|
|
619
608
|
createdAt: z8.string(),
|
|
620
609
|
updatedAt: z8.string()
|
|
621
610
|
});
|
|
611
|
+
var scheduleUpsertRequestSchema = z8.object({
|
|
612
|
+
preset: z8.string().optional(),
|
|
613
|
+
cron: z8.string().optional(),
|
|
614
|
+
timezone: z8.string().optional().default("UTC"),
|
|
615
|
+
enabled: z8.boolean().optional().default(true),
|
|
616
|
+
providers: z8.array(providerNameSchema).optional().default([])
|
|
617
|
+
}).refine(
|
|
618
|
+
(data) => data.preset && !data.cron || !data.preset && data.cron,
|
|
619
|
+
{ message: 'Exactly one of "preset" or "cron" must be provided' }
|
|
620
|
+
);
|
|
622
621
|
|
|
623
622
|
// ../contracts/src/source-categories.ts
|
|
624
623
|
var SOURCE_CATEGORY_RULES = [
|
|
@@ -1211,7 +1210,44 @@ var MIGRATIONS = [
|
|
|
1211
1210
|
// v10: Add sitemapUrl to google_connections for persistent sitemap storage
|
|
1212
1211
|
`ALTER TABLE google_connections ADD COLUMN sitemap_url TEXT`,
|
|
1213
1212
|
// v11: CDP browser provider — screenshot path for captured evidence
|
|
1214
|
-
`ALTER TABLE query_snapshots ADD COLUMN screenshot_path TEXT
|
|
1213
|
+
`ALTER TABLE query_snapshots ADD COLUMN screenshot_path TEXT`,
|
|
1214
|
+
// v12: Bing Webmaster Tools — bing_connections table
|
|
1215
|
+
`CREATE TABLE IF NOT EXISTS bing_connections (
|
|
1216
|
+
id TEXT PRIMARY KEY,
|
|
1217
|
+
domain TEXT NOT NULL,
|
|
1218
|
+
site_url TEXT,
|
|
1219
|
+
created_at TEXT NOT NULL,
|
|
1220
|
+
updated_at TEXT NOT NULL
|
|
1221
|
+
)`,
|
|
1222
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS idx_bing_conn_domain ON bing_connections(domain)`,
|
|
1223
|
+
// v12: Bing Webmaster Tools — bing_url_inspections table
|
|
1224
|
+
`CREATE TABLE IF NOT EXISTS bing_url_inspections (
|
|
1225
|
+
id TEXT PRIMARY KEY,
|
|
1226
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
1227
|
+
url TEXT NOT NULL,
|
|
1228
|
+
http_code INTEGER,
|
|
1229
|
+
in_index INTEGER,
|
|
1230
|
+
last_crawled_date TEXT,
|
|
1231
|
+
in_index_date TEXT,
|
|
1232
|
+
inspected_at TEXT NOT NULL,
|
|
1233
|
+
created_at TEXT NOT NULL
|
|
1234
|
+
)`,
|
|
1235
|
+
`CREATE INDEX IF NOT EXISTS idx_bing_inspect_project_url ON bing_url_inspections(project_id, url)`,
|
|
1236
|
+
`CREATE INDEX IF NOT EXISTS idx_bing_inspect_url_time ON bing_url_inspections(url, inspected_at)`,
|
|
1237
|
+
// v12: Bing Webmaster Tools — bing_keyword_stats table
|
|
1238
|
+
`CREATE TABLE IF NOT EXISTS bing_keyword_stats (
|
|
1239
|
+
id TEXT PRIMARY KEY,
|
|
1240
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
1241
|
+
query TEXT NOT NULL,
|
|
1242
|
+
impressions INTEGER NOT NULL DEFAULT 0,
|
|
1243
|
+
clicks INTEGER NOT NULL DEFAULT 0,
|
|
1244
|
+
ctr TEXT NOT NULL DEFAULT '0',
|
|
1245
|
+
average_position TEXT NOT NULL DEFAULT '0',
|
|
1246
|
+
synced_at TEXT NOT NULL,
|
|
1247
|
+
created_at TEXT NOT NULL
|
|
1248
|
+
)`,
|
|
1249
|
+
`CREATE INDEX IF NOT EXISTS idx_bing_keyword_project ON bing_keyword_stats(project_id)`,
|
|
1250
|
+
`CREATE INDEX IF NOT EXISTS idx_bing_keyword_query ON bing_keyword_stats(query)`
|
|
1215
1251
|
];
|
|
1216
1252
|
function migrate(db) {
|
|
1217
1253
|
const statements = MIGRATION_SQL.split(";").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
@@ -1294,17 +1330,46 @@ function writeAuditLog(db, entry) {
|
|
|
1294
1330
|
async function projectRoutes(app, opts) {
|
|
1295
1331
|
app.put("/projects/:name", async (request, reply) => {
|
|
1296
1332
|
const { name } = request.params;
|
|
1297
|
-
const
|
|
1298
|
-
if (!
|
|
1299
|
-
const err = validationError("
|
|
1333
|
+
const parsedBody = projectUpsertRequestSchema.safeParse(request.body);
|
|
1334
|
+
if (!parsedBody.success) {
|
|
1335
|
+
const err = validationError("Invalid project payload", {
|
|
1336
|
+
issues: parsedBody.error.issues.map((issue) => ({
|
|
1337
|
+
path: issue.path.join("."),
|
|
1338
|
+
message: issue.message
|
|
1339
|
+
}))
|
|
1340
|
+
});
|
|
1300
1341
|
return reply.status(err.statusCode).send(err.toJSON());
|
|
1301
1342
|
}
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1343
|
+
const body = parsedBody.data;
|
|
1344
|
+
const validNames = opts.validProviderNames ?? [];
|
|
1345
|
+
if (validNames.length && body.providers?.length) {
|
|
1346
|
+
const invalid = body.providers.filter((p) => !validNames.includes(p));
|
|
1347
|
+
if (invalid.length) {
|
|
1348
|
+
const err = validationError(`Invalid provider(s): ${invalid.join(", ")}. Must be one of: ${validNames.join(", ")}`, {
|
|
1349
|
+
invalidProviders: invalid,
|
|
1350
|
+
validProviders: validNames
|
|
1351
|
+
});
|
|
1352
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
1353
|
+
}
|
|
1305
1354
|
}
|
|
1306
1355
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1307
1356
|
const existing = app.db.select().from(projects).where(eq3(projects.name, name)).get();
|
|
1357
|
+
const existingLocations = existing ? JSON.parse(existing.locations || "[]") : [];
|
|
1358
|
+
const nextLocations = body.locations ?? existingLocations;
|
|
1359
|
+
const duplicateLabels = findDuplicateLocationLabels(nextLocations);
|
|
1360
|
+
if (duplicateLabels.length > 0) {
|
|
1361
|
+
const err = validationError(`Duplicate location labels are not allowed: ${duplicateLabels.join(", ")}`, {
|
|
1362
|
+
duplicateLabels
|
|
1363
|
+
});
|
|
1364
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
1365
|
+
}
|
|
1366
|
+
const nextDefaultLocation = body.defaultLocation !== void 0 ? body.defaultLocation ?? null : existing?.defaultLocation ?? null;
|
|
1367
|
+
if (!hasLocationLabel(nextLocations, nextDefaultLocation)) {
|
|
1368
|
+
const err = validationError(`defaultLocation "${nextDefaultLocation}" must match a configured location label`, {
|
|
1369
|
+
defaultLocation: nextDefaultLocation
|
|
1370
|
+
});
|
|
1371
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
1372
|
+
}
|
|
1308
1373
|
if (existing) {
|
|
1309
1374
|
app.db.update(projects).set({
|
|
1310
1375
|
displayName: body.displayName,
|
|
@@ -1315,8 +1380,8 @@ async function projectRoutes(app, opts) {
|
|
|
1315
1380
|
tags: JSON.stringify(body.tags ?? []),
|
|
1316
1381
|
labels: JSON.stringify(body.labels ?? {}),
|
|
1317
1382
|
providers: JSON.stringify(body.providers ?? []),
|
|
1318
|
-
locations: JSON.stringify(
|
|
1319
|
-
defaultLocation:
|
|
1383
|
+
locations: JSON.stringify(nextLocations),
|
|
1384
|
+
defaultLocation: nextDefaultLocation,
|
|
1320
1385
|
configSource: body.configSource ?? "api",
|
|
1321
1386
|
configRevision: existing.configRevision + 1,
|
|
1322
1387
|
updatedAt: now
|
|
@@ -1343,8 +1408,8 @@ async function projectRoutes(app, opts) {
|
|
|
1343
1408
|
tags: JSON.stringify(body.tags ?? []),
|
|
1344
1409
|
labels: JSON.stringify(body.labels ?? {}),
|
|
1345
1410
|
providers: JSON.stringify(body.providers ?? []),
|
|
1346
|
-
locations: JSON.stringify(
|
|
1347
|
-
defaultLocation:
|
|
1411
|
+
locations: JSON.stringify(nextLocations),
|
|
1412
|
+
defaultLocation: nextDefaultLocation,
|
|
1348
1413
|
configSource: body.configSource ?? "api",
|
|
1349
1414
|
configRevision: 1,
|
|
1350
1415
|
createdAt: now,
|
|
@@ -1709,9 +1774,13 @@ async function keywordRoutes(app, opts) {
|
|
|
1709
1774
|
const err = validationError('Body must contain a "provider" string');
|
|
1710
1775
|
return reply.status(err.statusCode).send(err.toJSON());
|
|
1711
1776
|
}
|
|
1712
|
-
const provider =
|
|
1713
|
-
|
|
1714
|
-
|
|
1777
|
+
const provider = body.provider.trim().toLowerCase();
|
|
1778
|
+
const validNames = opts.validProviderNames ?? [];
|
|
1779
|
+
if (validNames.length && !validNames.includes(provider)) {
|
|
1780
|
+
const err = validationError(`Unknown provider "${body.provider}". Valid providers: ${validNames.join(", ")}`, {
|
|
1781
|
+
provider: body.provider,
|
|
1782
|
+
validProviders: validNames
|
|
1783
|
+
});
|
|
1715
1784
|
return reply.status(err.statusCode).send(err.toJSON());
|
|
1716
1785
|
}
|
|
1717
1786
|
if (body.count !== void 0 && (typeof body.count !== "number" || !Number.isFinite(body.count) || !Number.isInteger(body.count))) {
|
|
@@ -1720,7 +1789,8 @@ async function keywordRoutes(app, opts) {
|
|
|
1720
1789
|
}
|
|
1721
1790
|
const count = Math.min(Math.max(body.count ?? 5, 1), 20);
|
|
1722
1791
|
if (!opts.onGenerateKeywords) {
|
|
1723
|
-
|
|
1792
|
+
const err = notImplemented("Key phrase generation is not supported in this deployment");
|
|
1793
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
1724
1794
|
}
|
|
1725
1795
|
const existingRows = app.db.select().from(keywords).where(eq4(keywords.projectId, project.id)).all();
|
|
1726
1796
|
const existingKeywords = existingRows.map((r) => r.keyword);
|
|
@@ -1736,7 +1806,10 @@ async function keywordRoutes(app, opts) {
|
|
|
1736
1806
|
} catch (err) {
|
|
1737
1807
|
request.log.error({ err }, "Key phrase generation failed");
|
|
1738
1808
|
return reply.status(500).send({
|
|
1739
|
-
error:
|
|
1809
|
+
error: {
|
|
1810
|
+
code: "INTERNAL_ERROR",
|
|
1811
|
+
message: err instanceof Error ? err.message : "Failed to generate key phrases"
|
|
1812
|
+
}
|
|
1740
1813
|
});
|
|
1741
1814
|
}
|
|
1742
1815
|
});
|
|
@@ -1810,7 +1883,7 @@ function resolveProjectSafe2(app, name, reply) {
|
|
|
1810
1883
|
|
|
1811
1884
|
// ../api-routes/src/runs.ts
|
|
1812
1885
|
import crypto8 from "crypto";
|
|
1813
|
-
import { eq as eq7, asc } from "drizzle-orm";
|
|
1886
|
+
import { eq as eq7, asc, desc } from "drizzle-orm";
|
|
1814
1887
|
|
|
1815
1888
|
// ../api-routes/src/run-queue.ts
|
|
1816
1889
|
import crypto7 from "crypto";
|
|
@@ -1857,12 +1930,19 @@ async function runRoutes(app, opts) {
|
|
|
1857
1930
|
const trigger = request.body?.trigger ?? "manual";
|
|
1858
1931
|
const rawProviders = request.body?.providers;
|
|
1859
1932
|
if (rawProviders?.length) {
|
|
1860
|
-
const
|
|
1861
|
-
const
|
|
1862
|
-
if (
|
|
1863
|
-
|
|
1933
|
+
const normalized = rawProviders.map((p) => p.trim().toLowerCase()).filter(Boolean);
|
|
1934
|
+
const validNames = opts.validProviderNames ?? [];
|
|
1935
|
+
if (validNames.length) {
|
|
1936
|
+
const invalid = normalized.filter((p) => !validNames.includes(p));
|
|
1937
|
+
if (invalid.length) {
|
|
1938
|
+
const err = validationError(`Invalid provider(s): ${invalid.join(", ")}. Must be one of: ${validNames.join(", ")}`, {
|
|
1939
|
+
invalidProviders: invalid,
|
|
1940
|
+
validProviders: validNames
|
|
1941
|
+
});
|
|
1942
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
1943
|
+
}
|
|
1864
1944
|
}
|
|
1865
|
-
rawProviders.splice(0, rawProviders.length, ...
|
|
1945
|
+
rawProviders.splice(0, rawProviders.length, ...normalized);
|
|
1866
1946
|
}
|
|
1867
1947
|
const providers = rawProviders?.length ? rawProviders : void 0;
|
|
1868
1948
|
let resolvedLocation;
|
|
@@ -1941,7 +2021,9 @@ async function runRoutes(app, opts) {
|
|
|
1941
2021
|
app.get("/projects/:name/runs", async (request, reply) => {
|
|
1942
2022
|
const project = resolveProjectSafe3(app, request.params.name, reply);
|
|
1943
2023
|
if (!project) return;
|
|
1944
|
-
const
|
|
2024
|
+
const parsedLimit = parseInt(request.query.limit ?? "", 10);
|
|
2025
|
+
const limit = Number.isNaN(parsedLimit) || parsedLimit <= 0 ? void 0 : parsedLimit;
|
|
2026
|
+
const rows = limit == null ? app.db.select().from(runs).where(eq7(runs.projectId, project.id)).orderBy(asc(runs.createdAt)).all() : app.db.select().from(runs).where(eq7(runs.projectId, project.id)).orderBy(desc(runs.createdAt)).limit(limit).all().reverse();
|
|
1945
2027
|
return reply.send(rows.map(formatRun));
|
|
1946
2028
|
});
|
|
1947
2029
|
app.get("/runs", async (_request, reply) => {
|
|
@@ -1960,12 +2042,19 @@ async function runRoutes(app, opts) {
|
|
|
1960
2042
|
}
|
|
1961
2043
|
const rawProviders = request.body?.providers;
|
|
1962
2044
|
if (rawProviders?.length) {
|
|
1963
|
-
const
|
|
1964
|
-
const
|
|
1965
|
-
if (
|
|
1966
|
-
|
|
2045
|
+
const normalized = rawProviders.map((p) => p.trim().toLowerCase()).filter(Boolean);
|
|
2046
|
+
const validNames = opts.validProviderNames ?? [];
|
|
2047
|
+
if (validNames.length) {
|
|
2048
|
+
const invalid = normalized.filter((p) => !validNames.includes(p));
|
|
2049
|
+
if (invalid.length) {
|
|
2050
|
+
const err = validationError(`Invalid provider(s): ${invalid.join(", ")}. Must be one of: ${validNames.join(", ")}`, {
|
|
2051
|
+
invalidProviders: invalid,
|
|
2052
|
+
validProviders: validNames
|
|
2053
|
+
});
|
|
2054
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
2055
|
+
}
|
|
1967
2056
|
}
|
|
1968
|
-
rawProviders.splice(0, rawProviders.length, ...
|
|
2057
|
+
rawProviders.splice(0, rawProviders.length, ...normalized);
|
|
1969
2058
|
}
|
|
1970
2059
|
const providers = rawProviders?.length ? rawProviders : void 0;
|
|
1971
2060
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -1997,6 +2086,29 @@ async function runRoutes(app, opts) {
|
|
|
1997
2086
|
}
|
|
1998
2087
|
return reply.status(207).send(results);
|
|
1999
2088
|
});
|
|
2089
|
+
app.post("/runs/:id/cancel", async (request, reply) => {
|
|
2090
|
+
const run = app.db.select().from(runs).where(eq7(runs.id, request.params.id)).get();
|
|
2091
|
+
if (!run) {
|
|
2092
|
+
const err = notFound("Run", request.params.id);
|
|
2093
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
2094
|
+
}
|
|
2095
|
+
const terminalStatuses = /* @__PURE__ */ new Set(["completed", "partial", "failed", "cancelled"]);
|
|
2096
|
+
if (terminalStatuses.has(run.status)) {
|
|
2097
|
+
const err = runNotCancellable(run.id, run.status);
|
|
2098
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
2099
|
+
}
|
|
2100
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2101
|
+
app.db.update(runs).set({ status: "cancelled", finishedAt: now, error: "Cancelled by user" }).where(eq7(runs.id, run.id)).run();
|
|
2102
|
+
writeAuditLog(app.db, {
|
|
2103
|
+
projectId: run.projectId,
|
|
2104
|
+
actor: "api",
|
|
2105
|
+
action: "run.cancelled",
|
|
2106
|
+
entityType: "run",
|
|
2107
|
+
entityId: run.id
|
|
2108
|
+
});
|
|
2109
|
+
const updated = app.db.select().from(runs).where(eq7(runs.id, run.id)).get();
|
|
2110
|
+
return reply.send(formatRun(updated));
|
|
2111
|
+
});
|
|
2000
2112
|
app.get("/runs/:id", async (request, reply) => {
|
|
2001
2113
|
const run = app.db.select().from(runs).where(eq7(runs.id, request.params.id)).get();
|
|
2002
2114
|
if (!run) {
|
|
@@ -2345,6 +2457,23 @@ async function applyRoutes(app, opts) {
|
|
|
2345
2457
|
return reply.status(err.statusCode).send(err.toJSON());
|
|
2346
2458
|
}
|
|
2347
2459
|
const config = parsed.data;
|
|
2460
|
+
const validNames = opts?.validProviderNames ?? [];
|
|
2461
|
+
if (validNames.length) {
|
|
2462
|
+
const allProviders = [
|
|
2463
|
+
...config.spec.providers ?? [],
|
|
2464
|
+
...config.spec.schedule?.providers ?? []
|
|
2465
|
+
];
|
|
2466
|
+
if (allProviders.length) {
|
|
2467
|
+
const invalid = allProviders.filter((p) => !validNames.includes(p));
|
|
2468
|
+
if (invalid.length) {
|
|
2469
|
+
const err = validationError(`Invalid provider(s): ${[...new Set(invalid)].join(", ")}. Must be one of: ${validNames.join(", ")}`, {
|
|
2470
|
+
invalidProviders: [...new Set(invalid)],
|
|
2471
|
+
validProviders: validNames
|
|
2472
|
+
});
|
|
2473
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2348
2477
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2349
2478
|
const name = config.metadata.name;
|
|
2350
2479
|
const existing = app.db.select().from(projects).where(eq8(projects.name, name)).get();
|
|
@@ -2552,16 +2681,16 @@ async function applyRoutes(app, opts) {
|
|
|
2552
2681
|
}
|
|
2553
2682
|
|
|
2554
2683
|
// ../api-routes/src/history.ts
|
|
2555
|
-
import { eq as eq9, desc, inArray } from "drizzle-orm";
|
|
2684
|
+
import { eq as eq9, desc as desc2, inArray } from "drizzle-orm";
|
|
2556
2685
|
async function historyRoutes(app) {
|
|
2557
2686
|
app.get("/projects/:name/history", async (request, reply) => {
|
|
2558
2687
|
const project = resolveProjectSafe4(app, request.params.name, reply);
|
|
2559
2688
|
if (!project) return;
|
|
2560
|
-
const rows = app.db.select().from(auditLog).where(eq9(auditLog.projectId, project.id)).orderBy(
|
|
2689
|
+
const rows = app.db.select().from(auditLog).where(eq9(auditLog.projectId, project.id)).orderBy(desc2(auditLog.createdAt)).all();
|
|
2561
2690
|
return reply.send(rows.map(formatAuditEntry));
|
|
2562
2691
|
});
|
|
2563
2692
|
app.get("/history", async (_request, reply) => {
|
|
2564
|
-
const rows = app.db.select().from(auditLog).orderBy(
|
|
2693
|
+
const rows = app.db.select().from(auditLog).orderBy(desc2(auditLog.createdAt)).all();
|
|
2565
2694
|
return reply.send(rows.map(formatAuditEntry));
|
|
2566
2695
|
});
|
|
2567
2696
|
app.get("/projects/:name/snapshots", async (request, reply) => {
|
|
@@ -2586,7 +2715,7 @@ async function historyRoutes(app) {
|
|
|
2586
2715
|
competitorOverlap: querySnapshots.competitorOverlap,
|
|
2587
2716
|
location: querySnapshots.location,
|
|
2588
2717
|
createdAt: querySnapshots.createdAt
|
|
2589
|
-
}).from(querySnapshots).leftJoin(keywords, eq9(querySnapshots.keywordId, keywords.id)).where(inArray(querySnapshots.runId, projectRuns.map((r) => r.id))).orderBy(
|
|
2718
|
+
}).from(querySnapshots).leftJoin(keywords, eq9(querySnapshots.keywordId, keywords.id)).where(inArray(querySnapshots.runId, projectRuns.map((r) => r.id))).orderBy(desc2(querySnapshots.createdAt)).all();
|
|
2590
2719
|
const locationFilter = request.query.location;
|
|
2591
2720
|
const filtered = locationFilter !== void 0 ? allSnapshots.filter((s) => s.location === (locationFilter || null)) : allSnapshots;
|
|
2592
2721
|
const total = filtered.length;
|
|
@@ -2767,7 +2896,7 @@ function resolveProjectSafe4(app, name, reply) {
|
|
|
2767
2896
|
}
|
|
2768
2897
|
|
|
2769
2898
|
// ../api-routes/src/analytics.ts
|
|
2770
|
-
import { eq as eq10, desc as
|
|
2899
|
+
import { eq as eq10, desc as desc3, inArray as inArray2 } from "drizzle-orm";
|
|
2771
2900
|
async function analyticsRoutes(app) {
|
|
2772
2901
|
app.get("/projects/:name/analytics/metrics", async (request, reply) => {
|
|
2773
2902
|
const project = resolveProjectSafe5(app, request.params.name, reply);
|
|
@@ -2811,7 +2940,7 @@ async function analyticsRoutes(app) {
|
|
|
2811
2940
|
if (!project) return;
|
|
2812
2941
|
const window = parseWindow(request.query.window);
|
|
2813
2942
|
const cutoff = windowCutoff(window);
|
|
2814
|
-
const latestRun = app.db.select().from(runs).where(eq10(runs.projectId, project.id)).orderBy(
|
|
2943
|
+
const latestRun = app.db.select().from(runs).where(eq10(runs.projectId, project.id)).orderBy(desc3(runs.createdAt)).all().find((r) => r.status === "completed" || r.status === "partial");
|
|
2815
2944
|
if (!latestRun) {
|
|
2816
2945
|
return reply.send({ cited: [], gap: [], uncited: [], runId: "", window });
|
|
2817
2946
|
}
|
|
@@ -2893,7 +3022,7 @@ async function analyticsRoutes(app) {
|
|
|
2893
3022
|
if (!project) return;
|
|
2894
3023
|
const window = parseWindow(request.query.window);
|
|
2895
3024
|
const cutoff = windowCutoff(window);
|
|
2896
|
-
const windowRuns = app.db.select().from(runs).where(eq10(runs.projectId, project.id)).orderBy(
|
|
3025
|
+
const windowRuns = app.db.select().from(runs).where(eq10(runs.projectId, project.id)).orderBy(desc3(runs.createdAt)).all().filter((r) => r.status === "completed" || r.status === "partial").filter((r) => !cutoff || r.createdAt >= cutoff);
|
|
2897
3026
|
if (windowRuns.length === 0) {
|
|
2898
3027
|
return reply.send({ overall: [], byKeyword: {}, runId: "", window });
|
|
2899
3028
|
}
|
|
@@ -3066,6 +3195,18 @@ var booleanSchema = { type: "boolean" };
|
|
|
3066
3195
|
var integerSchema = { type: "integer" };
|
|
3067
3196
|
var objectSchema = { type: "object", additionalProperties: true };
|
|
3068
3197
|
var stringArraySchema = { type: "array", items: stringSchema };
|
|
3198
|
+
var googleConnectionTypeSchema2 = { type: "string", enum: ["gsc", "ga4"] };
|
|
3199
|
+
var locationSchema = {
|
|
3200
|
+
type: "object",
|
|
3201
|
+
required: ["label", "city", "region", "country"],
|
|
3202
|
+
properties: {
|
|
3203
|
+
label: stringSchema,
|
|
3204
|
+
city: stringSchema,
|
|
3205
|
+
region: stringSchema,
|
|
3206
|
+
country: stringSchema,
|
|
3207
|
+
timezone: stringSchema
|
|
3208
|
+
}
|
|
3209
|
+
};
|
|
3069
3210
|
var nameParameter = {
|
|
3070
3211
|
name: "name",
|
|
3071
3212
|
in: "path",
|
|
@@ -3092,7 +3233,59 @@ var providerNameParameter = {
|
|
|
3092
3233
|
in: "path",
|
|
3093
3234
|
required: true,
|
|
3094
3235
|
description: "Provider name.",
|
|
3095
|
-
schema: { type: "string", enum: ["gemini", "openai", "claude", "local"] }
|
|
3236
|
+
schema: { type: "string", enum: ["gemini", "openai", "claude", "perplexity", "local"] }
|
|
3237
|
+
};
|
|
3238
|
+
var locationLabelParameter = {
|
|
3239
|
+
name: "label",
|
|
3240
|
+
in: "path",
|
|
3241
|
+
required: true,
|
|
3242
|
+
description: "Location label.",
|
|
3243
|
+
schema: stringSchema
|
|
3244
|
+
};
|
|
3245
|
+
var googleTypeParameter = {
|
|
3246
|
+
name: "type",
|
|
3247
|
+
in: "path",
|
|
3248
|
+
required: true,
|
|
3249
|
+
description: "Google connection type.",
|
|
3250
|
+
schema: googleConnectionTypeSchema2
|
|
3251
|
+
};
|
|
3252
|
+
var projectRunIdParameter = {
|
|
3253
|
+
name: "runId",
|
|
3254
|
+
in: "path",
|
|
3255
|
+
required: true,
|
|
3256
|
+
description: "Run ID for a project run.",
|
|
3257
|
+
schema: stringSchema
|
|
3258
|
+
};
|
|
3259
|
+
var snapshotIdParameter = {
|
|
3260
|
+
name: "snapshotId",
|
|
3261
|
+
in: "path",
|
|
3262
|
+
required: true,
|
|
3263
|
+
description: "Snapshot ID.",
|
|
3264
|
+
schema: stringSchema
|
|
3265
|
+
};
|
|
3266
|
+
var limitQueryParameter = {
|
|
3267
|
+
name: "limit",
|
|
3268
|
+
in: "query",
|
|
3269
|
+
description: "Maximum number of records to return.",
|
|
3270
|
+
schema: integerSchema
|
|
3271
|
+
};
|
|
3272
|
+
var offsetQueryParameter = {
|
|
3273
|
+
name: "offset",
|
|
3274
|
+
in: "query",
|
|
3275
|
+
description: "Number of records to skip.",
|
|
3276
|
+
schema: integerSchema
|
|
3277
|
+
};
|
|
3278
|
+
var locationQueryParameter = {
|
|
3279
|
+
name: "location",
|
|
3280
|
+
in: "query",
|
|
3281
|
+
description: "Filter by location label. Use an empty value to request locationless results.",
|
|
3282
|
+
schema: stringSchema
|
|
3283
|
+
};
|
|
3284
|
+
var analyticsWindowParameter = {
|
|
3285
|
+
name: "window",
|
|
3286
|
+
in: "query",
|
|
3287
|
+
description: "Time window for analytics queries.",
|
|
3288
|
+
schema: { type: "string", enum: ["7d", "30d", "90d", "all"] }
|
|
3096
3289
|
};
|
|
3097
3290
|
var routeCatalog = [
|
|
3098
3291
|
{
|
|
@@ -3122,11 +3315,14 @@ var routeCatalog = [
|
|
|
3122
3315
|
properties: {
|
|
3123
3316
|
displayName: stringSchema,
|
|
3124
3317
|
canonicalDomain: stringSchema,
|
|
3318
|
+
ownedDomains: stringArraySchema,
|
|
3125
3319
|
country: stringSchema,
|
|
3126
3320
|
language: stringSchema,
|
|
3127
3321
|
tags: stringArraySchema,
|
|
3128
3322
|
labels: objectSchema,
|
|
3129
3323
|
providers: stringArraySchema,
|
|
3324
|
+
locations: { type: "array", items: locationSchema },
|
|
3325
|
+
defaultLocation: stringSchema,
|
|
3130
3326
|
configSource: stringSchema
|
|
3131
3327
|
}
|
|
3132
3328
|
}
|
|
@@ -3170,16 +3366,85 @@ var routeCatalog = [
|
|
|
3170
3366
|
}
|
|
3171
3367
|
},
|
|
3172
3368
|
{
|
|
3173
|
-
method: "
|
|
3174
|
-
path: "/api/v1/projects/{name}/
|
|
3175
|
-
summary: "
|
|
3369
|
+
method: "post",
|
|
3370
|
+
path: "/api/v1/projects/{name}/locations",
|
|
3371
|
+
summary: "Add a project location",
|
|
3176
3372
|
tags: ["projects"],
|
|
3177
3373
|
parameters: [nameParameter],
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3374
|
+
requestBody: {
|
|
3375
|
+
required: true,
|
|
3376
|
+
content: {
|
|
3377
|
+
"application/json": {
|
|
3378
|
+
schema: locationSchema
|
|
3379
|
+
}
|
|
3380
|
+
}
|
|
3381
|
+
},
|
|
3382
|
+
responses: {
|
|
3383
|
+
201: { description: "Location created." },
|
|
3384
|
+
400: { description: "Invalid location." },
|
|
3385
|
+
404: { description: "Project not found." }
|
|
3386
|
+
}
|
|
3387
|
+
},
|
|
3388
|
+
{
|
|
3389
|
+
method: "get",
|
|
3390
|
+
path: "/api/v1/projects/{name}/locations",
|
|
3391
|
+
summary: "List project locations",
|
|
3392
|
+
tags: ["projects"],
|
|
3393
|
+
parameters: [nameParameter],
|
|
3394
|
+
responses: {
|
|
3395
|
+
200: { description: "Locations returned." },
|
|
3396
|
+
404: { description: "Project not found." }
|
|
3397
|
+
}
|
|
3398
|
+
},
|
|
3399
|
+
{
|
|
3400
|
+
method: "delete",
|
|
3401
|
+
path: "/api/v1/projects/{name}/locations/{label}",
|
|
3402
|
+
summary: "Remove a project location",
|
|
3403
|
+
tags: ["projects"],
|
|
3404
|
+
parameters: [nameParameter, locationLabelParameter],
|
|
3405
|
+
responses: {
|
|
3406
|
+
204: { description: "Location removed." },
|
|
3407
|
+
400: { description: "Invalid location." },
|
|
3408
|
+
404: { description: "Project or location not found." }
|
|
3409
|
+
}
|
|
3410
|
+
},
|
|
3411
|
+
{
|
|
3412
|
+
method: "put",
|
|
3413
|
+
path: "/api/v1/projects/{name}/locations/default",
|
|
3414
|
+
summary: "Set the default project location",
|
|
3415
|
+
tags: ["projects"],
|
|
3416
|
+
parameters: [nameParameter],
|
|
3417
|
+
requestBody: {
|
|
3418
|
+
required: true,
|
|
3419
|
+
content: {
|
|
3420
|
+
"application/json": {
|
|
3421
|
+
schema: {
|
|
3422
|
+
type: "object",
|
|
3423
|
+
required: ["label"],
|
|
3424
|
+
properties: {
|
|
3425
|
+
label: stringSchema
|
|
3426
|
+
}
|
|
3427
|
+
}
|
|
3428
|
+
}
|
|
3429
|
+
}
|
|
3430
|
+
},
|
|
3431
|
+
responses: {
|
|
3432
|
+
200: { description: "Default location updated." },
|
|
3433
|
+
400: { description: "Invalid location." },
|
|
3434
|
+
404: { description: "Project not found." }
|
|
3435
|
+
}
|
|
3436
|
+
},
|
|
3437
|
+
{
|
|
3438
|
+
method: "get",
|
|
3439
|
+
path: "/api/v1/projects/{name}/export",
|
|
3440
|
+
summary: "Export a project as config",
|
|
3441
|
+
tags: ["projects"],
|
|
3442
|
+
parameters: [nameParameter],
|
|
3443
|
+
responses: {
|
|
3444
|
+
200: { description: "Project configuration returned." },
|
|
3445
|
+
404: { description: "Project not found." }
|
|
3446
|
+
}
|
|
3447
|
+
},
|
|
3183
3448
|
{
|
|
3184
3449
|
method: "get",
|
|
3185
3450
|
path: "/api/v1/projects/{name}/keywords",
|
|
@@ -3214,6 +3479,31 @@ var routeCatalog = [
|
|
|
3214
3479
|
200: { description: "Keywords replaced." }
|
|
3215
3480
|
}
|
|
3216
3481
|
},
|
|
3482
|
+
{
|
|
3483
|
+
method: "delete",
|
|
3484
|
+
path: "/api/v1/projects/{name}/keywords",
|
|
3485
|
+
summary: "Delete specific keywords",
|
|
3486
|
+
tags: ["keywords"],
|
|
3487
|
+
parameters: [nameParameter],
|
|
3488
|
+
requestBody: {
|
|
3489
|
+
required: true,
|
|
3490
|
+
content: {
|
|
3491
|
+
"application/json": {
|
|
3492
|
+
schema: {
|
|
3493
|
+
type: "object",
|
|
3494
|
+
required: ["keywords"],
|
|
3495
|
+
properties: {
|
|
3496
|
+
keywords: stringArraySchema
|
|
3497
|
+
}
|
|
3498
|
+
}
|
|
3499
|
+
}
|
|
3500
|
+
}
|
|
3501
|
+
},
|
|
3502
|
+
responses: {
|
|
3503
|
+
200: { description: "Remaining keywords returned." },
|
|
3504
|
+
400: { description: "Invalid keyword delete request." }
|
|
3505
|
+
}
|
|
3506
|
+
},
|
|
3217
3507
|
{
|
|
3218
3508
|
method: "post",
|
|
3219
3509
|
path: "/api/v1/projects/{name}/keywords",
|
|
@@ -3252,7 +3542,7 @@ var routeCatalog = [
|
|
|
3252
3542
|
type: "object",
|
|
3253
3543
|
required: ["provider"],
|
|
3254
3544
|
properties: {
|
|
3255
|
-
provider: { type: "string", enum: ["gemini", "openai", "claude", "local"] },
|
|
3545
|
+
provider: { type: "string", enum: ["gemini", "openai", "claude", "perplexity", "local"] },
|
|
3256
3546
|
count: integerSchema
|
|
3257
3547
|
}
|
|
3258
3548
|
}
|
|
@@ -3312,7 +3602,10 @@ var routeCatalog = [
|
|
|
3312
3602
|
properties: {
|
|
3313
3603
|
kind: stringSchema,
|
|
3314
3604
|
trigger: stringSchema,
|
|
3315
|
-
providers: stringArraySchema
|
|
3605
|
+
providers: stringArraySchema,
|
|
3606
|
+
location: stringSchema,
|
|
3607
|
+
allLocations: booleanSchema,
|
|
3608
|
+
noLocation: booleanSchema
|
|
3316
3609
|
}
|
|
3317
3610
|
}
|
|
3318
3611
|
}
|
|
@@ -3328,7 +3621,7 @@ var routeCatalog = [
|
|
|
3328
3621
|
path: "/api/v1/projects/{name}/runs",
|
|
3329
3622
|
summary: "List project runs",
|
|
3330
3623
|
tags: ["runs"],
|
|
3331
|
-
parameters: [nameParameter],
|
|
3624
|
+
parameters: [nameParameter, limitQueryParameter],
|
|
3332
3625
|
responses: {
|
|
3333
3626
|
200: { description: "Runs returned." }
|
|
3334
3627
|
}
|
|
@@ -3375,6 +3668,18 @@ var routeCatalog = [
|
|
|
3375
3668
|
404: { description: "Run not found." }
|
|
3376
3669
|
}
|
|
3377
3670
|
},
|
|
3671
|
+
{
|
|
3672
|
+
method: "post",
|
|
3673
|
+
path: "/api/v1/runs/{id}/cancel",
|
|
3674
|
+
summary: "Cancel a queued or running run",
|
|
3675
|
+
tags: ["runs"],
|
|
3676
|
+
parameters: [runIdParameter],
|
|
3677
|
+
responses: {
|
|
3678
|
+
200: { description: "Run cancelled." },
|
|
3679
|
+
404: { description: "Run not found." },
|
|
3680
|
+
409: { description: "Run is not cancellable." }
|
|
3681
|
+
}
|
|
3682
|
+
},
|
|
3378
3683
|
{
|
|
3379
3684
|
method: "post",
|
|
3380
3685
|
path: "/api/v1/apply",
|
|
@@ -3420,18 +3725,9 @@ var routeCatalog = [
|
|
|
3420
3725
|
tags: ["history"],
|
|
3421
3726
|
parameters: [
|
|
3422
3727
|
nameParameter,
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
description: "Maximum number of snapshots to return.",
|
|
3427
|
-
schema: integerSchema
|
|
3428
|
-
},
|
|
3429
|
-
{
|
|
3430
|
-
name: "offset",
|
|
3431
|
-
in: "query",
|
|
3432
|
-
description: "Number of snapshots to skip.",
|
|
3433
|
-
schema: integerSchema
|
|
3434
|
-
}
|
|
3728
|
+
limitQueryParameter,
|
|
3729
|
+
offsetQueryParameter,
|
|
3730
|
+
locationQueryParameter
|
|
3435
3731
|
],
|
|
3436
3732
|
responses: {
|
|
3437
3733
|
200: { description: "Snapshots returned." }
|
|
@@ -3442,11 +3738,44 @@ var routeCatalog = [
|
|
|
3442
3738
|
path: "/api/v1/projects/{name}/timeline",
|
|
3443
3739
|
summary: "Get keyword timeline",
|
|
3444
3740
|
tags: ["history"],
|
|
3445
|
-
parameters: [nameParameter],
|
|
3741
|
+
parameters: [nameParameter, locationQueryParameter],
|
|
3446
3742
|
responses: {
|
|
3447
3743
|
200: { description: "Timeline returned." }
|
|
3448
3744
|
}
|
|
3449
3745
|
},
|
|
3746
|
+
{
|
|
3747
|
+
method: "get",
|
|
3748
|
+
path: "/api/v1/projects/{name}/analytics/metrics",
|
|
3749
|
+
summary: "Get citation trend analytics",
|
|
3750
|
+
tags: ["analytics"],
|
|
3751
|
+
parameters: [nameParameter, analyticsWindowParameter],
|
|
3752
|
+
responses: {
|
|
3753
|
+
200: { description: "Citation metrics returned." },
|
|
3754
|
+
404: { description: "Project not found." }
|
|
3755
|
+
}
|
|
3756
|
+
},
|
|
3757
|
+
{
|
|
3758
|
+
method: "get",
|
|
3759
|
+
path: "/api/v1/projects/{name}/analytics/gaps",
|
|
3760
|
+
summary: "Get brand gap analysis",
|
|
3761
|
+
tags: ["analytics"],
|
|
3762
|
+
parameters: [nameParameter, analyticsWindowParameter],
|
|
3763
|
+
responses: {
|
|
3764
|
+
200: { description: "Gap analysis returned." },
|
|
3765
|
+
404: { description: "Project not found." }
|
|
3766
|
+
}
|
|
3767
|
+
},
|
|
3768
|
+
{
|
|
3769
|
+
method: "get",
|
|
3770
|
+
path: "/api/v1/projects/{name}/analytics/sources",
|
|
3771
|
+
summary: "Get source origin analytics",
|
|
3772
|
+
tags: ["analytics"],
|
|
3773
|
+
parameters: [nameParameter, analyticsWindowParameter],
|
|
3774
|
+
responses: {
|
|
3775
|
+
200: { description: "Source breakdown returned." },
|
|
3776
|
+
404: { description: "Project not found." }
|
|
3777
|
+
}
|
|
3778
|
+
},
|
|
3450
3779
|
{
|
|
3451
3780
|
method: "get",
|
|
3452
3781
|
path: "/api/v1/projects/{name}/snapshots/diff",
|
|
@@ -3511,6 +3840,83 @@ var routeCatalog = [
|
|
|
3511
3840
|
501: { description: "Provider updates are not supported." }
|
|
3512
3841
|
}
|
|
3513
3842
|
},
|
|
3843
|
+
{
|
|
3844
|
+
method: "put",
|
|
3845
|
+
path: "/api/v1/settings/google",
|
|
3846
|
+
summary: "Update Google OAuth settings",
|
|
3847
|
+
tags: ["settings"],
|
|
3848
|
+
requestBody: {
|
|
3849
|
+
required: true,
|
|
3850
|
+
content: {
|
|
3851
|
+
"application/json": {
|
|
3852
|
+
schema: {
|
|
3853
|
+
type: "object",
|
|
3854
|
+
required: ["clientId", "clientSecret"],
|
|
3855
|
+
properties: {
|
|
3856
|
+
clientId: stringSchema,
|
|
3857
|
+
clientSecret: stringSchema
|
|
3858
|
+
}
|
|
3859
|
+
}
|
|
3860
|
+
}
|
|
3861
|
+
}
|
|
3862
|
+
},
|
|
3863
|
+
responses: {
|
|
3864
|
+
200: { description: "Google settings updated." },
|
|
3865
|
+
400: { description: "Invalid Google settings." },
|
|
3866
|
+
501: { description: "Google settings updates are not supported." }
|
|
3867
|
+
}
|
|
3868
|
+
},
|
|
3869
|
+
{
|
|
3870
|
+
method: "put",
|
|
3871
|
+
path: "/api/v1/settings/bing",
|
|
3872
|
+
summary: "Update Bing settings",
|
|
3873
|
+
tags: ["settings"],
|
|
3874
|
+
requestBody: {
|
|
3875
|
+
required: true,
|
|
3876
|
+
content: {
|
|
3877
|
+
"application/json": {
|
|
3878
|
+
schema: {
|
|
3879
|
+
type: "object",
|
|
3880
|
+
required: ["apiKey"],
|
|
3881
|
+
properties: {
|
|
3882
|
+
apiKey: stringSchema
|
|
3883
|
+
}
|
|
3884
|
+
}
|
|
3885
|
+
}
|
|
3886
|
+
}
|
|
3887
|
+
},
|
|
3888
|
+
responses: {
|
|
3889
|
+
200: { description: "Bing settings updated." },
|
|
3890
|
+
400: { description: "Invalid Bing settings." },
|
|
3891
|
+
501: { description: "Bing settings updates are not supported." }
|
|
3892
|
+
}
|
|
3893
|
+
},
|
|
3894
|
+
{
|
|
3895
|
+
method: "put",
|
|
3896
|
+
path: "/api/v1/settings/cdp",
|
|
3897
|
+
summary: "Update CDP endpoint settings",
|
|
3898
|
+
tags: ["settings", "cdp"],
|
|
3899
|
+
requestBody: {
|
|
3900
|
+
required: true,
|
|
3901
|
+
content: {
|
|
3902
|
+
"application/json": {
|
|
3903
|
+
schema: {
|
|
3904
|
+
type: "object",
|
|
3905
|
+
required: ["host"],
|
|
3906
|
+
properties: {
|
|
3907
|
+
host: stringSchema,
|
|
3908
|
+
port: integerSchema
|
|
3909
|
+
}
|
|
3910
|
+
}
|
|
3911
|
+
}
|
|
3912
|
+
}
|
|
3913
|
+
},
|
|
3914
|
+
responses: {
|
|
3915
|
+
200: { description: "CDP endpoint updated." },
|
|
3916
|
+
400: { description: "Invalid CDP settings." },
|
|
3917
|
+
501: { description: "CDP updates are not supported." }
|
|
3918
|
+
}
|
|
3919
|
+
},
|
|
3514
3920
|
{
|
|
3515
3921
|
method: "put",
|
|
3516
3922
|
path: "/api/v1/projects/{name}/schedule",
|
|
@@ -3636,33 +4042,600 @@ var routeCatalog = [
|
|
|
3636
4042
|
summary: "Get telemetry status",
|
|
3637
4043
|
tags: ["telemetry"],
|
|
3638
4044
|
responses: {
|
|
3639
|
-
200: { description: "Telemetry status returned." },
|
|
3640
|
-
501: { description: "Telemetry status is not available." }
|
|
4045
|
+
200: { description: "Telemetry status returned." },
|
|
4046
|
+
501: { description: "Telemetry status is not available." }
|
|
4047
|
+
}
|
|
4048
|
+
},
|
|
4049
|
+
{
|
|
4050
|
+
method: "put",
|
|
4051
|
+
path: "/api/v1/telemetry",
|
|
4052
|
+
summary: "Update telemetry status",
|
|
4053
|
+
tags: ["telemetry"],
|
|
4054
|
+
requestBody: {
|
|
4055
|
+
required: true,
|
|
4056
|
+
content: {
|
|
4057
|
+
"application/json": {
|
|
4058
|
+
schema: {
|
|
4059
|
+
type: "object",
|
|
4060
|
+
required: ["enabled"],
|
|
4061
|
+
properties: {
|
|
4062
|
+
enabled: booleanSchema
|
|
4063
|
+
}
|
|
4064
|
+
}
|
|
4065
|
+
}
|
|
4066
|
+
}
|
|
4067
|
+
},
|
|
4068
|
+
responses: {
|
|
4069
|
+
200: { description: "Telemetry updated." },
|
|
4070
|
+
400: { description: "Invalid telemetry request." },
|
|
4071
|
+
501: { description: "Telemetry configuration is not available." }
|
|
4072
|
+
}
|
|
4073
|
+
},
|
|
4074
|
+
{
|
|
4075
|
+
method: "get",
|
|
4076
|
+
path: "/api/v1/screenshots/{snapshotId}",
|
|
4077
|
+
summary: "Fetch a stored browser screenshot",
|
|
4078
|
+
tags: ["cdp"],
|
|
4079
|
+
parameters: [snapshotIdParameter],
|
|
4080
|
+
responses: {
|
|
4081
|
+
200: { description: "Screenshot returned." },
|
|
4082
|
+
404: { description: "Screenshot not found." }
|
|
4083
|
+
}
|
|
4084
|
+
},
|
|
4085
|
+
{
|
|
4086
|
+
method: "get",
|
|
4087
|
+
path: "/api/v1/cdp/status",
|
|
4088
|
+
summary: "Get CDP connection status",
|
|
4089
|
+
tags: ["cdp"],
|
|
4090
|
+
responses: {
|
|
4091
|
+
200: { description: "CDP status returned." },
|
|
4092
|
+
501: { description: "CDP is not configured." }
|
|
4093
|
+
}
|
|
4094
|
+
},
|
|
4095
|
+
{
|
|
4096
|
+
method: "post",
|
|
4097
|
+
path: "/api/v1/cdp/screenshot",
|
|
4098
|
+
summary: "Run a one-off browser query and capture screenshots",
|
|
4099
|
+
tags: ["cdp"],
|
|
4100
|
+
requestBody: {
|
|
4101
|
+
required: true,
|
|
4102
|
+
content: {
|
|
4103
|
+
"application/json": {
|
|
4104
|
+
schema: {
|
|
4105
|
+
type: "object",
|
|
4106
|
+
required: ["query"],
|
|
4107
|
+
properties: {
|
|
4108
|
+
query: stringSchema,
|
|
4109
|
+
targets: stringArraySchema
|
|
4110
|
+
}
|
|
4111
|
+
}
|
|
4112
|
+
}
|
|
4113
|
+
}
|
|
4114
|
+
},
|
|
4115
|
+
responses: {
|
|
4116
|
+
200: { description: "CDP screenshot results returned." },
|
|
4117
|
+
400: { description: "Invalid CDP screenshot request." },
|
|
4118
|
+
501: { description: "CDP screenshot support is not available." }
|
|
4119
|
+
}
|
|
4120
|
+
},
|
|
4121
|
+
{
|
|
4122
|
+
method: "get",
|
|
4123
|
+
path: "/api/v1/projects/{name}/runs/{runId}/browser-diff",
|
|
4124
|
+
summary: "Compare API and browser provider results for a run",
|
|
4125
|
+
tags: ["cdp", "runs"],
|
|
4126
|
+
parameters: [nameParameter, projectRunIdParameter],
|
|
4127
|
+
responses: {
|
|
4128
|
+
200: { description: "Browser diff returned." },
|
|
4129
|
+
404: { description: "Project or run not found." }
|
|
4130
|
+
}
|
|
4131
|
+
},
|
|
4132
|
+
{
|
|
4133
|
+
method: "get",
|
|
4134
|
+
path: "/api/v1/google/callback",
|
|
4135
|
+
summary: "Handle the shared Google OAuth callback",
|
|
4136
|
+
tags: ["google"],
|
|
4137
|
+
auth: false,
|
|
4138
|
+
parameters: [
|
|
4139
|
+
{ name: "code", in: "query", description: "OAuth authorization code.", schema: stringSchema },
|
|
4140
|
+
{ name: "state", in: "query", description: "Signed OAuth state payload.", schema: stringSchema },
|
|
4141
|
+
{ name: "error", in: "query", description: "OAuth error code.", schema: stringSchema }
|
|
4142
|
+
],
|
|
4143
|
+
responses: {
|
|
4144
|
+
200: { description: "OAuth callback handled." },
|
|
4145
|
+
400: { description: "Invalid callback request." },
|
|
4146
|
+
500: { description: "OAuth configuration is incomplete." }
|
|
4147
|
+
}
|
|
4148
|
+
},
|
|
4149
|
+
{
|
|
4150
|
+
method: "get",
|
|
4151
|
+
path: "/api/v1/projects/{name}/google/callback",
|
|
4152
|
+
summary: "Handle the legacy project-scoped Google OAuth callback",
|
|
4153
|
+
tags: ["google"],
|
|
4154
|
+
auth: false,
|
|
4155
|
+
parameters: [
|
|
4156
|
+
nameParameter,
|
|
4157
|
+
{ name: "code", in: "query", description: "OAuth authorization code.", schema: stringSchema },
|
|
4158
|
+
{ name: "state", in: "query", description: "Signed OAuth state payload.", schema: stringSchema },
|
|
4159
|
+
{ name: "error", in: "query", description: "OAuth error code.", schema: stringSchema }
|
|
4160
|
+
],
|
|
4161
|
+
responses: {
|
|
4162
|
+
200: { description: "OAuth callback handled." },
|
|
4163
|
+
400: { description: "Invalid callback request." },
|
|
4164
|
+
500: { description: "OAuth configuration is incomplete." }
|
|
4165
|
+
}
|
|
4166
|
+
},
|
|
4167
|
+
{
|
|
4168
|
+
method: "get",
|
|
4169
|
+
path: "/api/v1/projects/{name}/google/connections",
|
|
4170
|
+
summary: "List Google connections for a project",
|
|
4171
|
+
tags: ["google"],
|
|
4172
|
+
parameters: [nameParameter],
|
|
4173
|
+
responses: {
|
|
4174
|
+
200: { description: "Google connections returned." },
|
|
4175
|
+
404: { description: "Project not found." }
|
|
4176
|
+
}
|
|
4177
|
+
},
|
|
4178
|
+
{
|
|
4179
|
+
method: "post",
|
|
4180
|
+
path: "/api/v1/projects/{name}/google/connect",
|
|
4181
|
+
summary: "Start a Google OAuth connection flow",
|
|
4182
|
+
tags: ["google"],
|
|
4183
|
+
parameters: [nameParameter],
|
|
4184
|
+
requestBody: {
|
|
4185
|
+
required: true,
|
|
4186
|
+
content: {
|
|
4187
|
+
"application/json": {
|
|
4188
|
+
schema: {
|
|
4189
|
+
type: "object",
|
|
4190
|
+
required: ["type"],
|
|
4191
|
+
properties: {
|
|
4192
|
+
type: googleConnectionTypeSchema2,
|
|
4193
|
+
propertyId: stringSchema,
|
|
4194
|
+
publicUrl: stringSchema
|
|
4195
|
+
}
|
|
4196
|
+
}
|
|
4197
|
+
}
|
|
4198
|
+
}
|
|
4199
|
+
},
|
|
4200
|
+
responses: {
|
|
4201
|
+
200: { description: "Google auth URL returned." },
|
|
4202
|
+
400: { description: "Invalid Google connection request." }
|
|
4203
|
+
}
|
|
4204
|
+
},
|
|
4205
|
+
{
|
|
4206
|
+
method: "delete",
|
|
4207
|
+
path: "/api/v1/projects/{name}/google/connections/{type}",
|
|
4208
|
+
summary: "Delete a Google connection",
|
|
4209
|
+
tags: ["google"],
|
|
4210
|
+
parameters: [nameParameter, googleTypeParameter],
|
|
4211
|
+
responses: {
|
|
4212
|
+
204: { description: "Google connection deleted." },
|
|
4213
|
+
404: { description: "Project or connection not found." }
|
|
4214
|
+
}
|
|
4215
|
+
},
|
|
4216
|
+
{
|
|
4217
|
+
method: "get",
|
|
4218
|
+
path: "/api/v1/projects/{name}/google/properties",
|
|
4219
|
+
summary: "List available Google Search Console properties",
|
|
4220
|
+
tags: ["google"],
|
|
4221
|
+
parameters: [nameParameter],
|
|
4222
|
+
responses: {
|
|
4223
|
+
200: { description: "Google properties returned." },
|
|
4224
|
+
400: { description: "Google OAuth is not configured." },
|
|
4225
|
+
404: { description: "Project not found." }
|
|
4226
|
+
}
|
|
4227
|
+
},
|
|
4228
|
+
{
|
|
4229
|
+
method: "put",
|
|
4230
|
+
path: "/api/v1/projects/{name}/google/connections/{type}/property",
|
|
4231
|
+
summary: "Set the property for a Google connection",
|
|
4232
|
+
tags: ["google"],
|
|
4233
|
+
parameters: [nameParameter, googleTypeParameter],
|
|
4234
|
+
requestBody: {
|
|
4235
|
+
required: true,
|
|
4236
|
+
content: {
|
|
4237
|
+
"application/json": {
|
|
4238
|
+
schema: {
|
|
4239
|
+
type: "object",
|
|
4240
|
+
required: ["propertyId"],
|
|
4241
|
+
properties: {
|
|
4242
|
+
propertyId: stringSchema
|
|
4243
|
+
}
|
|
4244
|
+
}
|
|
4245
|
+
}
|
|
4246
|
+
}
|
|
4247
|
+
},
|
|
4248
|
+
responses: {
|
|
4249
|
+
200: { description: "Google property updated." },
|
|
4250
|
+
400: { description: "Invalid property request." },
|
|
4251
|
+
404: { description: "Project or connection not found." }
|
|
4252
|
+
}
|
|
4253
|
+
},
|
|
4254
|
+
{
|
|
4255
|
+
method: "put",
|
|
4256
|
+
path: "/api/v1/projects/{name}/google/connections/{type}/sitemap",
|
|
4257
|
+
summary: "Set the sitemap URL for a Google connection",
|
|
4258
|
+
tags: ["google"],
|
|
4259
|
+
parameters: [nameParameter, googleTypeParameter],
|
|
4260
|
+
requestBody: {
|
|
4261
|
+
required: true,
|
|
4262
|
+
content: {
|
|
4263
|
+
"application/json": {
|
|
4264
|
+
schema: {
|
|
4265
|
+
type: "object",
|
|
4266
|
+
required: ["sitemapUrl"],
|
|
4267
|
+
properties: {
|
|
4268
|
+
sitemapUrl: stringSchema
|
|
4269
|
+
}
|
|
4270
|
+
}
|
|
4271
|
+
}
|
|
4272
|
+
}
|
|
4273
|
+
},
|
|
4274
|
+
responses: {
|
|
4275
|
+
200: { description: "Google sitemap updated." },
|
|
4276
|
+
400: { description: "Invalid sitemap request." },
|
|
4277
|
+
404: { description: "Project or connection not found." }
|
|
4278
|
+
}
|
|
4279
|
+
},
|
|
4280
|
+
{
|
|
4281
|
+
method: "post",
|
|
4282
|
+
path: "/api/v1/projects/{name}/google/gsc/sync",
|
|
4283
|
+
summary: "Queue a GSC sync run",
|
|
4284
|
+
tags: ["google"],
|
|
4285
|
+
parameters: [nameParameter],
|
|
4286
|
+
requestBody: {
|
|
4287
|
+
content: {
|
|
4288
|
+
"application/json": {
|
|
4289
|
+
schema: {
|
|
4290
|
+
type: "object",
|
|
4291
|
+
properties: {
|
|
4292
|
+
days: integerSchema,
|
|
4293
|
+
full: booleanSchema
|
|
4294
|
+
}
|
|
4295
|
+
}
|
|
4296
|
+
}
|
|
4297
|
+
}
|
|
4298
|
+
},
|
|
4299
|
+
responses: {
|
|
4300
|
+
200: { description: "GSC sync run returned." },
|
|
4301
|
+
400: { description: "Invalid GSC sync request." },
|
|
4302
|
+
404: { description: "Project or connection not found." }
|
|
4303
|
+
}
|
|
4304
|
+
},
|
|
4305
|
+
{
|
|
4306
|
+
method: "get",
|
|
4307
|
+
path: "/api/v1/projects/{name}/google/gsc/performance",
|
|
4308
|
+
summary: "Get GSC search performance data",
|
|
4309
|
+
tags: ["google"],
|
|
4310
|
+
parameters: [
|
|
4311
|
+
nameParameter,
|
|
4312
|
+
{ name: "startDate", in: "query", description: "Filter by start date.", schema: stringSchema },
|
|
4313
|
+
{ name: "endDate", in: "query", description: "Filter by end date.", schema: stringSchema },
|
|
4314
|
+
{ name: "query", in: "query", description: "Filter by search query.", schema: stringSchema },
|
|
4315
|
+
{ name: "page", in: "query", description: "Filter by page URL.", schema: stringSchema },
|
|
4316
|
+
limitQueryParameter
|
|
4317
|
+
],
|
|
4318
|
+
responses: {
|
|
4319
|
+
200: { description: "GSC performance rows returned." },
|
|
4320
|
+
404: { description: "Project not found." }
|
|
4321
|
+
}
|
|
4322
|
+
},
|
|
4323
|
+
{
|
|
4324
|
+
method: "post",
|
|
4325
|
+
path: "/api/v1/projects/{name}/google/gsc/inspect",
|
|
4326
|
+
summary: "Inspect a URL through Google Search Console",
|
|
4327
|
+
tags: ["google"],
|
|
4328
|
+
parameters: [nameParameter],
|
|
4329
|
+
requestBody: {
|
|
4330
|
+
required: true,
|
|
4331
|
+
content: {
|
|
4332
|
+
"application/json": {
|
|
4333
|
+
schema: {
|
|
4334
|
+
type: "object",
|
|
4335
|
+
required: ["url"],
|
|
4336
|
+
properties: {
|
|
4337
|
+
url: stringSchema
|
|
4338
|
+
}
|
|
4339
|
+
}
|
|
4340
|
+
}
|
|
4341
|
+
}
|
|
4342
|
+
},
|
|
4343
|
+
responses: {
|
|
4344
|
+
200: { description: "GSC inspection result returned." },
|
|
4345
|
+
400: { description: "Invalid inspection request." },
|
|
4346
|
+
404: { description: "Project or connection not found." }
|
|
4347
|
+
}
|
|
4348
|
+
},
|
|
4349
|
+
{
|
|
4350
|
+
method: "get",
|
|
4351
|
+
path: "/api/v1/projects/{name}/google/gsc/inspections",
|
|
4352
|
+
summary: "List GSC URL inspections",
|
|
4353
|
+
tags: ["google"],
|
|
4354
|
+
parameters: [nameParameter, { name: "url", in: "query", description: "Filter by URL.", schema: stringSchema }, limitQueryParameter],
|
|
4355
|
+
responses: {
|
|
4356
|
+
200: { description: "GSC inspections returned." },
|
|
4357
|
+
404: { description: "Project not found." }
|
|
4358
|
+
}
|
|
4359
|
+
},
|
|
4360
|
+
{
|
|
4361
|
+
method: "get",
|
|
4362
|
+
path: "/api/v1/projects/{name}/google/gsc/deindexed",
|
|
4363
|
+
summary: "List GSC deindexed pages",
|
|
4364
|
+
tags: ["google"],
|
|
4365
|
+
parameters: [nameParameter],
|
|
4366
|
+
responses: {
|
|
4367
|
+
200: { description: "Deindexed pages returned." },
|
|
4368
|
+
404: { description: "Project not found." }
|
|
4369
|
+
}
|
|
4370
|
+
},
|
|
4371
|
+
{
|
|
4372
|
+
method: "get",
|
|
4373
|
+
path: "/api/v1/projects/{name}/google/gsc/coverage",
|
|
4374
|
+
summary: "Get GSC coverage summary",
|
|
4375
|
+
tags: ["google"],
|
|
4376
|
+
parameters: [nameParameter],
|
|
4377
|
+
responses: {
|
|
4378
|
+
200: { description: "GSC coverage returned." },
|
|
4379
|
+
404: { description: "Project not found." }
|
|
4380
|
+
}
|
|
4381
|
+
},
|
|
4382
|
+
{
|
|
4383
|
+
method: "get",
|
|
4384
|
+
path: "/api/v1/projects/{name}/google/gsc/coverage/history",
|
|
4385
|
+
summary: "Get GSC coverage history",
|
|
4386
|
+
tags: ["google"],
|
|
4387
|
+
parameters: [nameParameter, limitQueryParameter],
|
|
4388
|
+
responses: {
|
|
4389
|
+
200: { description: "GSC coverage history returned." },
|
|
4390
|
+
404: { description: "Project not found." }
|
|
4391
|
+
}
|
|
4392
|
+
},
|
|
4393
|
+
{
|
|
4394
|
+
method: "get",
|
|
4395
|
+
path: "/api/v1/projects/{name}/google/gsc/sitemaps",
|
|
4396
|
+
summary: "List GSC sitemaps",
|
|
4397
|
+
tags: ["google"],
|
|
4398
|
+
parameters: [nameParameter],
|
|
4399
|
+
responses: {
|
|
4400
|
+
200: { description: "GSC sitemaps returned." },
|
|
4401
|
+
400: { description: "Invalid sitemap request." },
|
|
4402
|
+
404: { description: "Project or connection not found." }
|
|
4403
|
+
}
|
|
4404
|
+
},
|
|
4405
|
+
{
|
|
4406
|
+
method: "post",
|
|
4407
|
+
path: "/api/v1/projects/{name}/google/gsc/discover-sitemaps",
|
|
4408
|
+
summary: "Discover sitemaps and queue sitemap inspection",
|
|
4409
|
+
tags: ["google"],
|
|
4410
|
+
parameters: [nameParameter],
|
|
4411
|
+
responses: {
|
|
4412
|
+
200: { description: "Discovered sitemaps and queued run returned." },
|
|
4413
|
+
400: { description: "Invalid sitemap discovery request." },
|
|
4414
|
+
404: { description: "Project or connection not found." }
|
|
4415
|
+
}
|
|
4416
|
+
},
|
|
4417
|
+
{
|
|
4418
|
+
method: "post",
|
|
4419
|
+
path: "/api/v1/projects/{name}/google/gsc/inspect-sitemap",
|
|
4420
|
+
summary: "Queue a sitemap inspection run",
|
|
4421
|
+
tags: ["google"],
|
|
4422
|
+
parameters: [nameParameter],
|
|
4423
|
+
requestBody: {
|
|
4424
|
+
content: {
|
|
4425
|
+
"application/json": {
|
|
4426
|
+
schema: {
|
|
4427
|
+
type: "object",
|
|
4428
|
+
properties: {
|
|
4429
|
+
sitemapUrl: stringSchema
|
|
4430
|
+
}
|
|
4431
|
+
}
|
|
4432
|
+
}
|
|
4433
|
+
}
|
|
4434
|
+
},
|
|
4435
|
+
responses: {
|
|
4436
|
+
200: { description: "Sitemap inspection run returned." },
|
|
4437
|
+
400: { description: "Invalid sitemap inspection request." },
|
|
4438
|
+
404: { description: "Project or connection not found." }
|
|
4439
|
+
}
|
|
4440
|
+
},
|
|
4441
|
+
{
|
|
4442
|
+
method: "post",
|
|
4443
|
+
path: "/api/v1/projects/{name}/google/indexing/request",
|
|
4444
|
+
summary: "Request Google indexing notifications",
|
|
4445
|
+
tags: ["google"],
|
|
4446
|
+
parameters: [nameParameter],
|
|
4447
|
+
requestBody: {
|
|
4448
|
+
required: true,
|
|
4449
|
+
content: {
|
|
4450
|
+
"application/json": {
|
|
4451
|
+
schema: {
|
|
4452
|
+
type: "object",
|
|
4453
|
+
properties: {
|
|
4454
|
+
urls: stringArraySchema,
|
|
4455
|
+
allUnindexed: booleanSchema
|
|
4456
|
+
}
|
|
4457
|
+
}
|
|
4458
|
+
}
|
|
4459
|
+
}
|
|
4460
|
+
},
|
|
4461
|
+
responses: {
|
|
4462
|
+
200: { description: "Indexing request results returned." },
|
|
4463
|
+
400: { description: "Invalid indexing request." },
|
|
4464
|
+
404: { description: "Project or connection not found." }
|
|
4465
|
+
}
|
|
4466
|
+
},
|
|
4467
|
+
{
|
|
4468
|
+
method: "post",
|
|
4469
|
+
path: "/api/v1/projects/{name}/bing/connect",
|
|
4470
|
+
summary: "Connect Bing Webmaster Tools",
|
|
4471
|
+
tags: ["bing"],
|
|
4472
|
+
parameters: [nameParameter],
|
|
4473
|
+
requestBody: {
|
|
4474
|
+
required: true,
|
|
4475
|
+
content: {
|
|
4476
|
+
"application/json": {
|
|
4477
|
+
schema: {
|
|
4478
|
+
type: "object",
|
|
4479
|
+
required: ["apiKey"],
|
|
4480
|
+
properties: {
|
|
4481
|
+
apiKey: stringSchema
|
|
4482
|
+
}
|
|
4483
|
+
}
|
|
4484
|
+
}
|
|
4485
|
+
}
|
|
4486
|
+
},
|
|
4487
|
+
responses: {
|
|
4488
|
+
200: { description: "Bing connection returned." },
|
|
4489
|
+
400: { description: "Invalid Bing connection request." },
|
|
4490
|
+
404: { description: "Project not found." }
|
|
4491
|
+
}
|
|
4492
|
+
},
|
|
4493
|
+
{
|
|
4494
|
+
method: "delete",
|
|
4495
|
+
path: "/api/v1/projects/{name}/bing/disconnect",
|
|
4496
|
+
summary: "Disconnect Bing Webmaster Tools",
|
|
4497
|
+
tags: ["bing"],
|
|
4498
|
+
parameters: [nameParameter],
|
|
4499
|
+
responses: {
|
|
4500
|
+
204: { description: "Bing connection deleted." },
|
|
4501
|
+
404: { description: "Project or connection not found." }
|
|
4502
|
+
}
|
|
4503
|
+
},
|
|
4504
|
+
{
|
|
4505
|
+
method: "get",
|
|
4506
|
+
path: "/api/v1/projects/{name}/bing/status",
|
|
4507
|
+
summary: "Get Bing connection status",
|
|
4508
|
+
tags: ["bing"],
|
|
4509
|
+
parameters: [nameParameter],
|
|
4510
|
+
responses: {
|
|
4511
|
+
200: { description: "Bing status returned." },
|
|
4512
|
+
404: { description: "Project not found." }
|
|
4513
|
+
}
|
|
4514
|
+
},
|
|
4515
|
+
{
|
|
4516
|
+
method: "get",
|
|
4517
|
+
path: "/api/v1/projects/{name}/bing/sites",
|
|
4518
|
+
summary: "List Bing sites for the current connection",
|
|
4519
|
+
tags: ["bing"],
|
|
4520
|
+
parameters: [nameParameter],
|
|
4521
|
+
responses: {
|
|
4522
|
+
200: { description: "Bing sites returned." },
|
|
4523
|
+
400: { description: "Bing is not configured for this project." },
|
|
4524
|
+
404: { description: "Project not found." }
|
|
4525
|
+
}
|
|
4526
|
+
},
|
|
4527
|
+
{
|
|
4528
|
+
method: "post",
|
|
4529
|
+
path: "/api/v1/projects/{name}/bing/set-site",
|
|
4530
|
+
summary: "Set the active Bing site",
|
|
4531
|
+
tags: ["bing"],
|
|
4532
|
+
parameters: [nameParameter],
|
|
4533
|
+
requestBody: {
|
|
4534
|
+
required: true,
|
|
4535
|
+
content: {
|
|
4536
|
+
"application/json": {
|
|
4537
|
+
schema: {
|
|
4538
|
+
type: "object",
|
|
4539
|
+
required: ["siteUrl"],
|
|
4540
|
+
properties: {
|
|
4541
|
+
siteUrl: stringSchema
|
|
4542
|
+
}
|
|
4543
|
+
}
|
|
4544
|
+
}
|
|
4545
|
+
}
|
|
4546
|
+
},
|
|
4547
|
+
responses: {
|
|
4548
|
+
200: { description: "Active Bing site updated." },
|
|
4549
|
+
400: { description: "Invalid Bing site request." },
|
|
4550
|
+
404: { description: "Project or connection not found." }
|
|
4551
|
+
}
|
|
4552
|
+
},
|
|
4553
|
+
{
|
|
4554
|
+
method: "get",
|
|
4555
|
+
path: "/api/v1/projects/{name}/bing/coverage",
|
|
4556
|
+
summary: "Get Bing index coverage",
|
|
4557
|
+
tags: ["bing"],
|
|
4558
|
+
parameters: [nameParameter],
|
|
4559
|
+
responses: {
|
|
4560
|
+
200: { description: "Bing coverage returned." },
|
|
4561
|
+
400: { description: "Bing is not configured for this project." },
|
|
4562
|
+
404: { description: "Project not found." }
|
|
4563
|
+
}
|
|
4564
|
+
},
|
|
4565
|
+
{
|
|
4566
|
+
method: "get",
|
|
4567
|
+
path: "/api/v1/projects/{name}/bing/inspections",
|
|
4568
|
+
summary: "List Bing URL inspections",
|
|
4569
|
+
tags: ["bing"],
|
|
4570
|
+
parameters: [nameParameter, { name: "url", in: "query", description: "Filter by URL.", schema: stringSchema }, limitQueryParameter],
|
|
4571
|
+
responses: {
|
|
4572
|
+
200: { description: "Bing inspections returned." },
|
|
4573
|
+
400: { description: "Bing is not configured for this project." },
|
|
4574
|
+
404: { description: "Project not found." }
|
|
4575
|
+
}
|
|
4576
|
+
},
|
|
4577
|
+
{
|
|
4578
|
+
method: "post",
|
|
4579
|
+
path: "/api/v1/projects/{name}/bing/inspect-url",
|
|
4580
|
+
summary: "Inspect a URL through Bing Webmaster Tools",
|
|
4581
|
+
tags: ["bing"],
|
|
4582
|
+
parameters: [nameParameter],
|
|
4583
|
+
requestBody: {
|
|
4584
|
+
required: true,
|
|
4585
|
+
content: {
|
|
4586
|
+
"application/json": {
|
|
4587
|
+
schema: {
|
|
4588
|
+
type: "object",
|
|
4589
|
+
required: ["url"],
|
|
4590
|
+
properties: {
|
|
4591
|
+
url: stringSchema
|
|
4592
|
+
}
|
|
4593
|
+
}
|
|
4594
|
+
}
|
|
4595
|
+
}
|
|
4596
|
+
},
|
|
4597
|
+
responses: {
|
|
4598
|
+
200: { description: "Bing inspection result returned." },
|
|
4599
|
+
400: { description: "Invalid inspection request." },
|
|
4600
|
+
404: { description: "Project or connection not found." }
|
|
3641
4601
|
}
|
|
3642
4602
|
},
|
|
3643
4603
|
{
|
|
3644
|
-
method: "
|
|
3645
|
-
path: "/api/v1/
|
|
3646
|
-
summary: "
|
|
3647
|
-
tags: ["
|
|
4604
|
+
method: "post",
|
|
4605
|
+
path: "/api/v1/projects/{name}/bing/request-indexing",
|
|
4606
|
+
summary: "Submit URLs to Bing for indexing",
|
|
4607
|
+
tags: ["bing"],
|
|
4608
|
+
parameters: [nameParameter],
|
|
3648
4609
|
requestBody: {
|
|
3649
4610
|
required: true,
|
|
3650
4611
|
content: {
|
|
3651
4612
|
"application/json": {
|
|
3652
4613
|
schema: {
|
|
3653
4614
|
type: "object",
|
|
3654
|
-
required: ["enabled"],
|
|
3655
4615
|
properties: {
|
|
3656
|
-
|
|
4616
|
+
urls: stringArraySchema,
|
|
4617
|
+
allUnindexed: booleanSchema
|
|
3657
4618
|
}
|
|
3658
4619
|
}
|
|
3659
4620
|
}
|
|
3660
4621
|
}
|
|
3661
4622
|
},
|
|
3662
4623
|
responses: {
|
|
3663
|
-
200: { description: "
|
|
3664
|
-
400: { description: "Invalid
|
|
3665
|
-
|
|
4624
|
+
200: { description: "Bing indexing request results returned." },
|
|
4625
|
+
400: { description: "Invalid indexing request." },
|
|
4626
|
+
404: { description: "Project or connection not found." }
|
|
4627
|
+
}
|
|
4628
|
+
},
|
|
4629
|
+
{
|
|
4630
|
+
method: "get",
|
|
4631
|
+
path: "/api/v1/projects/{name}/bing/performance",
|
|
4632
|
+
summary: "Get Bing keyword performance",
|
|
4633
|
+
tags: ["bing"],
|
|
4634
|
+
parameters: [nameParameter, limitQueryParameter],
|
|
4635
|
+
responses: {
|
|
4636
|
+
200: { description: "Bing performance returned." },
|
|
4637
|
+
400: { description: "Bing is not configured for this project." },
|
|
4638
|
+
404: { description: "Project not found." }
|
|
3666
4639
|
}
|
|
3667
4640
|
}
|
|
3668
4641
|
];
|
|
@@ -3688,7 +4661,7 @@ function buildOpenApiDocument(info = {}) {
|
|
|
3688
4661
|
info: {
|
|
3689
4662
|
title: info.title ?? "Canonry API",
|
|
3690
4663
|
version: info.version ?? "0.0.0",
|
|
3691
|
-
description: info.description ?? "REST API for Canonry projects, runs,
|
|
4664
|
+
description: info.description ?? "REST API for Canonry projects, runs, analytics, integrations, and operator workflows."
|
|
3692
4665
|
},
|
|
3693
4666
|
servers: [
|
|
3694
4667
|
{
|
|
@@ -3731,31 +4704,40 @@ async function settingsRoutes(app, opts) {
|
|
|
3731
4704
|
bing: opts.bing ?? { configured: false }
|
|
3732
4705
|
}));
|
|
3733
4706
|
app.put("/settings/providers/:name", async (request, reply) => {
|
|
3734
|
-
const providerName = parseProviderName(request.params.name);
|
|
3735
4707
|
const { apiKey, baseUrl, model, quota } = request.body ?? {};
|
|
3736
|
-
|
|
3737
|
-
|
|
4708
|
+
const name = request.params.name;
|
|
4709
|
+
const adapters = opts.providerAdapters ?? [];
|
|
4710
|
+
const apiAdapters = adapters.filter((a) => a.mode === "api");
|
|
4711
|
+
const adapterInfo = apiAdapters.find((a) => a.name === name);
|
|
4712
|
+
if (!adapterInfo) {
|
|
4713
|
+
const validNames = apiAdapters.map((a) => a.name);
|
|
4714
|
+
const err = validationError(`Invalid provider: ${name}. Must be one of: ${validNames.join(", ")}`, {
|
|
4715
|
+
provider: name,
|
|
4716
|
+
validProviders: validNames
|
|
4717
|
+
});
|
|
4718
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
3738
4719
|
}
|
|
3739
|
-
const name = providerName;
|
|
3740
4720
|
if (name === "local") {
|
|
3741
4721
|
if (!baseUrl || typeof baseUrl !== "string") {
|
|
3742
|
-
|
|
4722
|
+
const err = validationError("baseUrl is required for local provider");
|
|
4723
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
3743
4724
|
}
|
|
3744
4725
|
} else {
|
|
3745
4726
|
if (!apiKey || typeof apiKey !== "string") {
|
|
3746
|
-
|
|
4727
|
+
const err = validationError("apiKey is required");
|
|
4728
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
3747
4729
|
}
|
|
3748
4730
|
}
|
|
3749
4731
|
if (model !== void 0) {
|
|
3750
|
-
|
|
3751
|
-
if (!registry.validationPattern.test(model)) {
|
|
4732
|
+
if (!adapterInfo.modelValidationPattern.test(model)) {
|
|
3752
4733
|
return reply.status(400).send({
|
|
3753
|
-
error: { code: "VALIDATION_ERROR", message: `Invalid model "${model}" for provider "${name}" \u2014 ${
|
|
4734
|
+
error: { code: "VALIDATION_ERROR", message: `Invalid model "${model}" for provider "${name}" \u2014 ${adapterInfo.modelValidationHint}` }
|
|
3754
4735
|
});
|
|
3755
4736
|
}
|
|
3756
4737
|
}
|
|
3757
4738
|
if (!opts.onProviderUpdate) {
|
|
3758
|
-
|
|
4739
|
+
const err = notImplemented("Provider configuration updates are not supported in this deployment");
|
|
4740
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
3759
4741
|
}
|
|
3760
4742
|
if (quota !== void 0) {
|
|
3761
4743
|
if (typeof quota !== "object" || quota === null) {
|
|
@@ -3772,7 +4754,12 @@ async function settingsRoutes(app, opts) {
|
|
|
3772
4754
|
}
|
|
3773
4755
|
const result = opts.onProviderUpdate(name, apiKey ?? "", model, baseUrl, quota);
|
|
3774
4756
|
if (!result) {
|
|
3775
|
-
return reply.status(500).send({
|
|
4757
|
+
return reply.status(500).send({
|
|
4758
|
+
error: {
|
|
4759
|
+
code: "INTERNAL_ERROR",
|
|
4760
|
+
message: "Failed to update provider configuration"
|
|
4761
|
+
}
|
|
4762
|
+
});
|
|
3776
4763
|
}
|
|
3777
4764
|
return result;
|
|
3778
4765
|
});
|
|
@@ -3784,11 +4771,17 @@ async function settingsRoutes(app, opts) {
|
|
|
3784
4771
|
});
|
|
3785
4772
|
}
|
|
3786
4773
|
if (!opts.onGoogleUpdate) {
|
|
3787
|
-
|
|
4774
|
+
const err = notImplemented("Google OAuth configuration updates are not supported in this deployment");
|
|
4775
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
3788
4776
|
}
|
|
3789
4777
|
const result = opts.onGoogleUpdate(clientId, clientSecret);
|
|
3790
4778
|
if (!result) {
|
|
3791
|
-
return reply.status(500).send({
|
|
4779
|
+
return reply.status(500).send({
|
|
4780
|
+
error: {
|
|
4781
|
+
code: "INTERNAL_ERROR",
|
|
4782
|
+
message: "Failed to update Google OAuth configuration"
|
|
4783
|
+
}
|
|
4784
|
+
});
|
|
3792
4785
|
}
|
|
3793
4786
|
return result;
|
|
3794
4787
|
});
|
|
@@ -3800,11 +4793,17 @@ async function settingsRoutes(app, opts) {
|
|
|
3800
4793
|
});
|
|
3801
4794
|
}
|
|
3802
4795
|
if (!opts.onBingUpdate) {
|
|
3803
|
-
|
|
4796
|
+
const err = notImplemented("Bing configuration updates are not supported in this deployment");
|
|
4797
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
3804
4798
|
}
|
|
3805
4799
|
const result = opts.onBingUpdate(apiKey);
|
|
3806
4800
|
if (!result) {
|
|
3807
|
-
return reply.status(500).send({
|
|
4801
|
+
return reply.status(500).send({
|
|
4802
|
+
error: {
|
|
4803
|
+
code: "INTERNAL_ERROR",
|
|
4804
|
+
message: "Failed to update Bing configuration"
|
|
4805
|
+
}
|
|
4806
|
+
});
|
|
3808
4807
|
}
|
|
3809
4808
|
return result;
|
|
3810
4809
|
});
|
|
@@ -3814,7 +4813,8 @@ async function settingsRoutes(app, opts) {
|
|
|
3814
4813
|
async function telemetryRoutes(app, opts) {
|
|
3815
4814
|
app.get("/telemetry", async (_request, reply) => {
|
|
3816
4815
|
if (!opts.getTelemetryStatus) {
|
|
3817
|
-
|
|
4816
|
+
const err = notImplemented("Telemetry status is not available in this deployment");
|
|
4817
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
3818
4818
|
}
|
|
3819
4819
|
const status = opts.getTelemetryStatus();
|
|
3820
4820
|
return {
|
|
@@ -3824,11 +4824,13 @@ async function telemetryRoutes(app, opts) {
|
|
|
3824
4824
|
});
|
|
3825
4825
|
app.put("/telemetry", async (request, reply) => {
|
|
3826
4826
|
if (!opts.setTelemetryEnabled) {
|
|
3827
|
-
|
|
4827
|
+
const err = notImplemented("Telemetry configuration is not available in this deployment");
|
|
4828
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
3828
4829
|
}
|
|
3829
4830
|
const { enabled } = request.body ?? {};
|
|
3830
4831
|
if (typeof enabled !== "boolean") {
|
|
3831
|
-
|
|
4832
|
+
const err = validationError("enabled (boolean) is required");
|
|
4833
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
3832
4834
|
}
|
|
3833
4835
|
opts.setTelemetryEnabled(enabled);
|
|
3834
4836
|
const status = opts.getTelemetryStatus?.();
|
|
@@ -3846,11 +4848,27 @@ async function scheduleRoutes(app, opts) {
|
|
|
3846
4848
|
app.put("/projects/:name/schedule", async (request, reply) => {
|
|
3847
4849
|
const project = resolveProjectSafe6(app, request.params.name, reply);
|
|
3848
4850
|
if (!project) return;
|
|
3849
|
-
const
|
|
3850
|
-
if (!
|
|
3851
|
-
|
|
3852
|
-
|
|
4851
|
+
const parsedBody = scheduleUpsertRequestSchema.safeParse(request.body);
|
|
4852
|
+
if (!parsedBody.success) {
|
|
4853
|
+
const err = validationError("Invalid schedule payload", {
|
|
4854
|
+
issues: parsedBody.error.issues.map((issue) => ({
|
|
4855
|
+
path: issue.path.join("."),
|
|
4856
|
+
message: issue.message
|
|
4857
|
+
}))
|
|
3853
4858
|
});
|
|
4859
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
4860
|
+
}
|
|
4861
|
+
const { preset, cron: cron2, timezone, providers, enabled } = parsedBody.data;
|
|
4862
|
+
const validNames = opts.validProviderNames ?? [];
|
|
4863
|
+
if (validNames.length && providers?.length) {
|
|
4864
|
+
const invalid = providers.filter((p) => !validNames.includes(p));
|
|
4865
|
+
if (invalid.length) {
|
|
4866
|
+
const err = validationError(`Invalid provider(s): ${invalid.join(", ")}. Must be one of: ${validNames.join(", ")}`, {
|
|
4867
|
+
invalidProviders: invalid,
|
|
4868
|
+
validProviders: validNames
|
|
4869
|
+
});
|
|
4870
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
4871
|
+
}
|
|
3854
4872
|
}
|
|
3855
4873
|
if (!isValidTimezone(timezone)) {
|
|
3856
4874
|
return reply.status(400).send({
|
|
@@ -4121,7 +5139,7 @@ function resolveProjectSafe7(app, name, reply) {
|
|
|
4121
5139
|
|
|
4122
5140
|
// ../api-routes/src/google.ts
|
|
4123
5141
|
import crypto13 from "crypto";
|
|
4124
|
-
import { eq as eq13, and as and3, desc as
|
|
5142
|
+
import { eq as eq13, and as and3, desc as desc4, sql as sql2 } from "drizzle-orm";
|
|
4125
5143
|
|
|
4126
5144
|
// ../integration-google/src/constants.ts
|
|
4127
5145
|
var GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
@@ -4562,7 +5580,7 @@ async function googleRoutes(app, opts) {
|
|
|
4562
5580
|
if (endDate) conditions.push(sql2`${gscSearchData.date} <= ${endDate}`);
|
|
4563
5581
|
if (query) conditions.push(sql2`${gscSearchData.query} LIKE ${"%" + query + "%"}`);
|
|
4564
5582
|
if (page) conditions.push(sql2`${gscSearchData.page} LIKE ${"%" + page + "%"}`);
|
|
4565
|
-
const rows = app.db.select().from(gscSearchData).where(and3(...conditions)).orderBy(
|
|
5583
|
+
const rows = app.db.select().from(gscSearchData).where(and3(...conditions)).orderBy(desc4(gscSearchData.date)).limit(parseInt(limit ?? "500", 10)).all();
|
|
4566
5584
|
return rows.map((r) => ({
|
|
4567
5585
|
date: r.date,
|
|
4568
5586
|
query: r.query,
|
|
@@ -4640,7 +5658,7 @@ async function googleRoutes(app, opts) {
|
|
|
4640
5658
|
const { url, limit } = request.query;
|
|
4641
5659
|
const conditions = [eq13(gscUrlInspections.projectId, project.id)];
|
|
4642
5660
|
if (url) conditions.push(eq13(gscUrlInspections.url, url));
|
|
4643
|
-
const rows = app.db.select().from(gscUrlInspections).where(and3(...conditions)).orderBy(
|
|
5661
|
+
const rows = app.db.select().from(gscUrlInspections).where(and3(...conditions)).orderBy(desc4(gscUrlInspections.inspectedAt)).limit(parseInt(limit ?? "100", 10)).all();
|
|
4644
5662
|
return rows.map((r) => ({
|
|
4645
5663
|
id: r.id,
|
|
4646
5664
|
url: r.url,
|
|
@@ -4659,7 +5677,7 @@ async function googleRoutes(app, opts) {
|
|
|
4659
5677
|
});
|
|
4660
5678
|
app.get("/projects/:name/google/gsc/deindexed", async (request) => {
|
|
4661
5679
|
const project = resolveProject(app.db, request.params.name);
|
|
4662
|
-
const allInspections = app.db.select().from(gscUrlInspections).where(eq13(gscUrlInspections.projectId, project.id)).orderBy(
|
|
5680
|
+
const allInspections = app.db.select().from(gscUrlInspections).where(eq13(gscUrlInspections.projectId, project.id)).orderBy(desc4(gscUrlInspections.inspectedAt)).all();
|
|
4663
5681
|
const byUrl = /* @__PURE__ */ new Map();
|
|
4664
5682
|
for (const row of allInspections) {
|
|
4665
5683
|
const existing = byUrl.get(row.url);
|
|
@@ -4687,7 +5705,7 @@ async function googleRoutes(app, opts) {
|
|
|
4687
5705
|
});
|
|
4688
5706
|
app.get("/projects/:name/google/gsc/coverage", async (request) => {
|
|
4689
5707
|
const project = resolveProject(app.db, request.params.name);
|
|
4690
|
-
const allInspections = app.db.select().from(gscUrlInspections).where(eq13(gscUrlInspections.projectId, project.id)).orderBy(
|
|
5708
|
+
const allInspections = app.db.select().from(gscUrlInspections).where(eq13(gscUrlInspections.projectId, project.id)).orderBy(desc4(gscUrlInspections.inspectedAt)).all();
|
|
4691
5709
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
4692
5710
|
const historyByUrl = /* @__PURE__ */ new Map();
|
|
4693
5711
|
for (const row of allInspections) {
|
|
@@ -4779,7 +5797,7 @@ async function googleRoutes(app, opts) {
|
|
|
4779
5797
|
const project = resolveProject(app.db, request.params.name);
|
|
4780
5798
|
const parsed = parseInt(request.query.limit ?? "90", 10);
|
|
4781
5799
|
const limit = Number.isNaN(parsed) || parsed <= 0 ? 90 : parsed;
|
|
4782
|
-
const rows = app.db.select().from(gscCoverageSnapshots).where(eq13(gscCoverageSnapshots.projectId, project.id)).orderBy(
|
|
5800
|
+
const rows = app.db.select().from(gscCoverageSnapshots).where(eq13(gscCoverageSnapshots.projectId, project.id)).orderBy(desc4(gscCoverageSnapshots.date)).limit(limit).all();
|
|
4783
5801
|
return rows.map((r) => ({
|
|
4784
5802
|
date: r.date,
|
|
4785
5803
|
indexed: r.indexed,
|
|
@@ -4932,7 +5950,7 @@ async function googleRoutes(app, opts) {
|
|
|
4932
5950
|
const { accessToken } = await getValidToken(store, project.canonicalDomain, "gsc", googleClientId, googleClientSecret);
|
|
4933
5951
|
let urlsToNotify = request.body?.urls ?? [];
|
|
4934
5952
|
if (request.body?.allUnindexed) {
|
|
4935
|
-
const allInspections = app.db.select().from(gscUrlInspections).where(eq13(gscUrlInspections.projectId, project.id)).orderBy(
|
|
5953
|
+
const allInspections = app.db.select().from(gscUrlInspections).where(eq13(gscUrlInspections.projectId, project.id)).orderBy(desc4(gscUrlInspections.inspectedAt)).all();
|
|
4936
5954
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
4937
5955
|
for (const row of allInspections) {
|
|
4938
5956
|
if (!latestByUrl.has(row.url)) {
|
|
@@ -5007,7 +6025,7 @@ async function googleRoutes(app, opts) {
|
|
|
5007
6025
|
|
|
5008
6026
|
// ../api-routes/src/bing.ts
|
|
5009
6027
|
import crypto14 from "crypto";
|
|
5010
|
-
import { eq as eq14, and as and4, desc as
|
|
6028
|
+
import { eq as eq14, and as and4, desc as desc5 } from "drizzle-orm";
|
|
5011
6029
|
|
|
5012
6030
|
// ../integration-bing/src/constants.ts
|
|
5013
6031
|
var BING_WMT_API_BASE = "https://ssl.bing.com/webmaster/api.svc/json";
|
|
@@ -5211,7 +6229,7 @@ async function bingRoutes(app, opts) {
|
|
|
5211
6229
|
const project = resolveProject(app.db, request.params.name);
|
|
5212
6230
|
const conn = requireConnection(store, project.canonicalDomain, reply);
|
|
5213
6231
|
if (!conn) return;
|
|
5214
|
-
const allInspections = app.db.select().from(bingUrlInspections).where(eq14(bingUrlInspections.projectId, project.id)).orderBy(
|
|
6232
|
+
const allInspections = app.db.select().from(bingUrlInspections).where(eq14(bingUrlInspections.projectId, project.id)).orderBy(desc5(bingUrlInspections.inspectedAt)).all();
|
|
5215
6233
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
5216
6234
|
for (const row of allInspections) {
|
|
5217
6235
|
if (!latestByUrl.has(row.url)) {
|
|
@@ -5261,7 +6279,7 @@ async function bingRoutes(app, opts) {
|
|
|
5261
6279
|
const project = resolveProject(app.db, request.params.name);
|
|
5262
6280
|
const { url, limit } = request.query;
|
|
5263
6281
|
const whereClause = url ? and4(eq14(bingUrlInspections.projectId, project.id), eq14(bingUrlInspections.url, url)) : eq14(bingUrlInspections.projectId, project.id);
|
|
5264
|
-
const filtered = app.db.select().from(bingUrlInspections).where(whereClause).orderBy(
|
|
6282
|
+
const filtered = app.db.select().from(bingUrlInspections).where(whereClause).orderBy(desc5(bingUrlInspections.inspectedAt)).limit(Math.max(1, Math.min(parseInt(limit ?? "100", 10) || 100, 1e3))).all();
|
|
5265
6283
|
return filtered.map((r) => ({
|
|
5266
6284
|
id: r.id,
|
|
5267
6285
|
url: r.url,
|
|
@@ -5323,7 +6341,7 @@ async function bingRoutes(app, opts) {
|
|
|
5323
6341
|
}
|
|
5324
6342
|
let urlsToSubmit = request.body?.urls ?? [];
|
|
5325
6343
|
if (request.body?.allUnindexed) {
|
|
5326
|
-
const allInspections = app.db.select().from(bingUrlInspections).where(eq14(bingUrlInspections.projectId, project.id)).orderBy(
|
|
6344
|
+
const allInspections = app.db.select().from(bingUrlInspections).where(eq14(bingUrlInspections.projectId, project.id)).orderBy(desc5(bingUrlInspections.inspectedAt)).all();
|
|
5327
6345
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
5328
6346
|
for (const row of allInspections) {
|
|
5329
6347
|
if (!latestByUrl.has(row.url)) {
|
|
@@ -5419,51 +6437,61 @@ async function cdpRoutes(app, opts) {
|
|
|
5419
6437
|
const { snapshotId } = request.params;
|
|
5420
6438
|
const snapshot = app.db.select({ screenshotPath: querySnapshots.screenshotPath }).from(querySnapshots).where(eq15(querySnapshots.id, snapshotId)).get();
|
|
5421
6439
|
if (!snapshot?.screenshotPath) {
|
|
5422
|
-
|
|
6440
|
+
const err = notFound("Screenshot", snapshotId);
|
|
6441
|
+
return reply.code(err.statusCode).send(err.toJSON());
|
|
5423
6442
|
}
|
|
5424
6443
|
const base = path2.resolve(getScreenshotDir());
|
|
5425
6444
|
const fullPath = path2.resolve(path2.join(base, snapshot.screenshotPath));
|
|
5426
6445
|
if (!fullPath.startsWith(base + path2.sep) && fullPath !== base) {
|
|
5427
|
-
|
|
6446
|
+
const err = notFound("Screenshot", snapshotId);
|
|
6447
|
+
return reply.code(err.statusCode).send(err.toJSON());
|
|
5428
6448
|
}
|
|
5429
6449
|
if (!fs2.existsSync(fullPath)) {
|
|
5430
|
-
|
|
6450
|
+
const err = notFound("Screenshot file", snapshotId);
|
|
6451
|
+
return reply.code(err.statusCode).send(err.toJSON());
|
|
5431
6452
|
}
|
|
5432
6453
|
const stream = fs2.createReadStream(fullPath);
|
|
5433
6454
|
return reply.type("image/png").send(stream);
|
|
5434
6455
|
});
|
|
5435
6456
|
app.put("/settings/cdp", async (request, reply) => {
|
|
5436
6457
|
if (!opts.onCdpConfigure) {
|
|
5437
|
-
|
|
6458
|
+
const err = notImplemented("CDP configuration not supported in this deployment");
|
|
6459
|
+
return reply.code(err.statusCode).send(err.toJSON());
|
|
5438
6460
|
}
|
|
5439
6461
|
const { host, port = 9222 } = request.body;
|
|
5440
6462
|
if (!host || typeof host !== "string") {
|
|
5441
|
-
|
|
6463
|
+
const err = validationError("host is required");
|
|
6464
|
+
return reply.code(err.statusCode).send(err.toJSON());
|
|
5442
6465
|
}
|
|
5443
6466
|
const ALLOWED_HOSTS = ["localhost", "127.0.0.1", "::1"];
|
|
5444
6467
|
if (!ALLOWED_HOSTS.includes(host)) {
|
|
5445
|
-
|
|
6468
|
+
const err = validationError("host must be localhost, 127.0.0.1, or ::1");
|
|
6469
|
+
return reply.code(err.statusCode).send(err.toJSON());
|
|
5446
6470
|
}
|
|
5447
6471
|
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
5448
|
-
|
|
6472
|
+
const err = validationError("port must be an integer between 1 and 65535");
|
|
6473
|
+
return reply.code(err.statusCode).send(err.toJSON());
|
|
5449
6474
|
}
|
|
5450
6475
|
await opts.onCdpConfigure(host, port);
|
|
5451
6476
|
return reply.code(200).send({ endpoint: `ws://${host}:${port}` });
|
|
5452
6477
|
});
|
|
5453
6478
|
app.get("/cdp/status", async (_request, reply) => {
|
|
5454
6479
|
if (!opts.getCdpStatus) {
|
|
5455
|
-
|
|
6480
|
+
const err = notImplemented("CDP not configured");
|
|
6481
|
+
return reply.code(err.statusCode).send(err.toJSON());
|
|
5456
6482
|
}
|
|
5457
6483
|
const status = await opts.getCdpStatus();
|
|
5458
6484
|
return reply.send(status);
|
|
5459
6485
|
});
|
|
5460
6486
|
app.post("/cdp/screenshot", async (request, reply) => {
|
|
5461
6487
|
if (!opts.onCdpScreenshot) {
|
|
5462
|
-
|
|
6488
|
+
const err = notImplemented("CDP not configured");
|
|
6489
|
+
return reply.code(err.statusCode).send(err.toJSON());
|
|
5463
6490
|
}
|
|
5464
6491
|
const { query, targets } = request.body;
|
|
5465
6492
|
if (!query || typeof query !== "string") {
|
|
5466
|
-
|
|
6493
|
+
const err = validationError("query is required");
|
|
6494
|
+
return reply.code(err.statusCode).send(err.toJSON());
|
|
5467
6495
|
}
|
|
5468
6496
|
const results = await opts.onCdpScreenshot(query, targets);
|
|
5469
6497
|
return reply.code(200).send({ results });
|
|
@@ -5474,7 +6502,10 @@ async function cdpRoutes(app, opts) {
|
|
|
5474
6502
|
const project = resolveProject(app.db, request.params.name);
|
|
5475
6503
|
const { runId } = request.params;
|
|
5476
6504
|
const run = app.db.select().from(runs).where(and5(eq15(runs.id, runId), eq15(runs.projectId, project.id))).get();
|
|
5477
|
-
if (!run)
|
|
6505
|
+
if (!run) {
|
|
6506
|
+
const err = notFound("Run", runId);
|
|
6507
|
+
return reply.code(err.statusCode).send(err.toJSON());
|
|
6508
|
+
}
|
|
5478
6509
|
const snapshots = app.db.select({
|
|
5479
6510
|
id: querySnapshots.id,
|
|
5480
6511
|
keywordId: querySnapshots.keywordId,
|
|
@@ -5595,15 +6626,21 @@ async function apiRoutes(app, opts) {
|
|
|
5595
6626
|
await app.register(async (api) => {
|
|
5596
6627
|
await api.register(openApiRoutes, opts.openApiInfo ?? {});
|
|
5597
6628
|
await api.register(projectRoutes, {
|
|
5598
|
-
onProjectDeleted: opts.onProjectDeleted
|
|
6629
|
+
onProjectDeleted: opts.onProjectDeleted,
|
|
6630
|
+
validProviderNames: opts.providerAdapters?.map((a) => a.name)
|
|
5599
6631
|
});
|
|
5600
6632
|
await api.register(keywordRoutes, {
|
|
5601
|
-
onGenerateKeywords: opts.onGenerateKeywords
|
|
6633
|
+
onGenerateKeywords: opts.onGenerateKeywords,
|
|
6634
|
+
validProviderNames: opts.providerAdapters?.filter((a) => a.mode === "api").map((a) => a.name)
|
|
5602
6635
|
});
|
|
5603
6636
|
await api.register(competitorRoutes);
|
|
5604
|
-
await api.register(runRoutes, {
|
|
6637
|
+
await api.register(runRoutes, {
|
|
6638
|
+
onRunCreated: opts.onRunCreated,
|
|
6639
|
+
validProviderNames: opts.providerAdapters?.map((a) => a.name)
|
|
6640
|
+
});
|
|
5605
6641
|
await api.register(applyRoutes, {
|
|
5606
6642
|
onScheduleUpdated: opts.onScheduleUpdated,
|
|
6643
|
+
validProviderNames: opts.providerAdapters?.map((a) => a.name),
|
|
5607
6644
|
onGoogleConnectionPropertyUpdated: (domain, connectionType, propertyId) => {
|
|
5608
6645
|
opts.googleConnectionStore?.updateConnection(domain, connectionType, {
|
|
5609
6646
|
propertyId,
|
|
@@ -5615,6 +6652,7 @@ async function apiRoutes(app, opts) {
|
|
|
5615
6652
|
await api.register(analyticsRoutes);
|
|
5616
6653
|
await api.register(settingsRoutes, {
|
|
5617
6654
|
providerSummary: opts.providerSummary,
|
|
6655
|
+
providerAdapters: opts.providerAdapters,
|
|
5618
6656
|
onProviderUpdate: opts.onProviderUpdate,
|
|
5619
6657
|
google: opts.googleSettingsSummary,
|
|
5620
6658
|
onGoogleUpdate: opts.onGoogleSettingsUpdate,
|
|
@@ -5622,7 +6660,8 @@ async function apiRoutes(app, opts) {
|
|
|
5622
6660
|
onBingUpdate: opts.onBingSettingsUpdate
|
|
5623
6661
|
});
|
|
5624
6662
|
await api.register(scheduleRoutes, {
|
|
5625
|
-
onScheduleUpdated: opts.onScheduleUpdated
|
|
6663
|
+
onScheduleUpdated: opts.onScheduleUpdated,
|
|
6664
|
+
validProviderNames: opts.providerAdapters?.map((a) => a.name)
|
|
5626
6665
|
});
|
|
5627
6666
|
await api.register(notificationRoutes);
|
|
5628
6667
|
await api.register(telemetryRoutes, {
|
|
@@ -5650,11 +6689,12 @@ async function apiRoutes(app, opts) {
|
|
|
5650
6689
|
|
|
5651
6690
|
// ../provider-gemini/src/normalize.ts
|
|
5652
6691
|
import { GoogleGenerativeAI } from "@google/generative-ai";
|
|
5653
|
-
var DEFAULT_MODEL =
|
|
6692
|
+
var DEFAULT_MODEL = "gemini-3-flash";
|
|
6693
|
+
var VALIDATION_PATTERN = /^gemini-/;
|
|
5654
6694
|
function resolveModel(config) {
|
|
5655
6695
|
const m = config.model;
|
|
5656
6696
|
if (!m) return DEFAULT_MODEL;
|
|
5657
|
-
if (
|
|
6697
|
+
if (VALIDATION_PATTERN.test(m)) return m;
|
|
5658
6698
|
console.warn(
|
|
5659
6699
|
`[provider-gemini] Invalid model name "${m}" \u2014 this provider uses the Gemini AI Studio API (generativelanguage.googleapis.com) which only accepts "gemini-*" model names. Falling back to ${DEFAULT_MODEL}.`
|
|
5660
6700
|
);
|
|
@@ -5665,7 +6705,7 @@ function validateConfig(config) {
|
|
|
5665
6705
|
return { ok: false, provider: "gemini", message: "missing api key" };
|
|
5666
6706
|
}
|
|
5667
6707
|
const model = resolveModel(config);
|
|
5668
|
-
const warning = config.model && !
|
|
6708
|
+
const warning = config.model && !VALIDATION_PATTERN.test(config.model) ? ` (invalid model "${config.model}" replaced with default)` : "";
|
|
5669
6709
|
return {
|
|
5670
6710
|
ok: true,
|
|
5671
6711
|
provider: "gemini",
|
|
@@ -5849,6 +6889,20 @@ function toGeminiConfig(config) {
|
|
|
5849
6889
|
}
|
|
5850
6890
|
var geminiAdapter = {
|
|
5851
6891
|
name: "gemini",
|
|
6892
|
+
displayName: "Gemini",
|
|
6893
|
+
mode: "api",
|
|
6894
|
+
keyUrl: "https://aistudio.google.com/apikey",
|
|
6895
|
+
modelRegistry: {
|
|
6896
|
+
defaultModel: "gemini-3-flash",
|
|
6897
|
+
validationPattern: /^gemini-/,
|
|
6898
|
+
validationHint: 'model name must start with "gemini-" (e.g. gemini-3-flash)',
|
|
6899
|
+
knownModels: [
|
|
6900
|
+
{ id: "gemini-3.1-pro-preview", displayName: "Gemini 3.1 Pro (Preview)", tier: "flagship" },
|
|
6901
|
+
{ id: "gemini-3-flash-preview", displayName: "Gemini 3 Flash (Preview)", tier: "standard" },
|
|
6902
|
+
{ id: "gemini-3.1-flash-lite-preview", displayName: "Gemini 3.1 Flash-Lite (Preview)", tier: "economy" },
|
|
6903
|
+
{ id: "gemini-2.5-flash", displayName: "Gemini 2.5 Flash", tier: "standard" }
|
|
6904
|
+
]
|
|
6905
|
+
},
|
|
5852
6906
|
validateConfig(config) {
|
|
5853
6907
|
const result = validateConfig(toGeminiConfig(config));
|
|
5854
6908
|
return {
|
|
@@ -5907,7 +6961,7 @@ var geminiAdapter = {
|
|
|
5907
6961
|
|
|
5908
6962
|
// ../provider-openai/src/normalize.ts
|
|
5909
6963
|
import OpenAI from "openai";
|
|
5910
|
-
var DEFAULT_MODEL2 =
|
|
6964
|
+
var DEFAULT_MODEL2 = "gpt-5.4";
|
|
5911
6965
|
function validateConfig2(config) {
|
|
5912
6966
|
if (!config.apiKey || config.apiKey.length === 0) {
|
|
5913
6967
|
return { ok: false, provider: "openai", message: "missing api key" };
|
|
@@ -6101,6 +7155,22 @@ function toOpenAIConfig(config) {
|
|
|
6101
7155
|
}
|
|
6102
7156
|
var openaiAdapter = {
|
|
6103
7157
|
name: "openai",
|
|
7158
|
+
displayName: "OpenAI",
|
|
7159
|
+
mode: "api",
|
|
7160
|
+
keyUrl: "https://platform.openai.com/api-keys",
|
|
7161
|
+
modelRegistry: {
|
|
7162
|
+
defaultModel: "gpt-5.4",
|
|
7163
|
+
validationPattern: /^(gpt-|o\d)/,
|
|
7164
|
+
validationHint: "expected a GPT or o-series model name (e.g. gpt-5.4, o3)",
|
|
7165
|
+
knownModels: [
|
|
7166
|
+
{ id: "gpt-5.4", displayName: "GPT-5.4", tier: "flagship" },
|
|
7167
|
+
{ id: "gpt-5.4-pro", displayName: "GPT-5.4 Pro", tier: "flagship" },
|
|
7168
|
+
{ id: "gpt-5-mini", displayName: "GPT-5 Mini", tier: "fast" },
|
|
7169
|
+
{ id: "gpt-5-nano", displayName: "GPT-5 Nano", tier: "economy" },
|
|
7170
|
+
{ id: "gpt-5", displayName: "GPT-5", tier: "standard" },
|
|
7171
|
+
{ id: "gpt-4.1", displayName: "GPT-4.1", tier: "standard" }
|
|
7172
|
+
]
|
|
7173
|
+
},
|
|
6104
7174
|
validateConfig(config) {
|
|
6105
7175
|
const result = validateConfig2(toOpenAIConfig(config));
|
|
6106
7176
|
return {
|
|
@@ -6159,7 +7229,7 @@ var openaiAdapter = {
|
|
|
6159
7229
|
|
|
6160
7230
|
// ../provider-claude/src/normalize.ts
|
|
6161
7231
|
import Anthropic from "@anthropic-ai/sdk";
|
|
6162
|
-
var DEFAULT_MODEL3 =
|
|
7232
|
+
var DEFAULT_MODEL3 = "claude-sonnet-4-6";
|
|
6163
7233
|
function validateConfig3(config) {
|
|
6164
7234
|
if (!config.apiKey || config.apiKey.length === 0) {
|
|
6165
7235
|
return { ok: false, provider: "claude", message: "missing api key" };
|
|
@@ -6347,6 +7417,19 @@ function toClaudeConfig(config) {
|
|
|
6347
7417
|
}
|
|
6348
7418
|
var claudeAdapter = {
|
|
6349
7419
|
name: "claude",
|
|
7420
|
+
displayName: "Claude",
|
|
7421
|
+
mode: "api",
|
|
7422
|
+
keyUrl: "https://platform.claude.com/settings/keys",
|
|
7423
|
+
modelRegistry: {
|
|
7424
|
+
defaultModel: "claude-sonnet-4-6",
|
|
7425
|
+
validationPattern: /^claude-/,
|
|
7426
|
+
validationHint: 'model name must start with "claude-" (e.g. claude-sonnet-4-6)',
|
|
7427
|
+
knownModels: [
|
|
7428
|
+
{ id: "claude-opus-4-6", displayName: "Claude Opus 4.6", tier: "flagship" },
|
|
7429
|
+
{ id: "claude-sonnet-4-6", displayName: "Claude Sonnet 4.6", tier: "standard" },
|
|
7430
|
+
{ id: "claude-haiku-4-5", displayName: "Claude Haiku 4.5", tier: "fast" }
|
|
7431
|
+
]
|
|
7432
|
+
},
|
|
6350
7433
|
validateConfig(config) {
|
|
6351
7434
|
const result = validateConfig3(toClaudeConfig(config));
|
|
6352
7435
|
return {
|
|
@@ -6405,7 +7488,7 @@ var claudeAdapter = {
|
|
|
6405
7488
|
|
|
6406
7489
|
// ../provider-local/src/normalize.ts
|
|
6407
7490
|
import OpenAI2 from "openai";
|
|
6408
|
-
var DEFAULT_MODEL4 =
|
|
7491
|
+
var DEFAULT_MODEL4 = "llama3";
|
|
6409
7492
|
function validateConfig4(config) {
|
|
6410
7493
|
if (!config.baseUrl || config.baseUrl.length === 0) {
|
|
6411
7494
|
return { ok: false, provider: "local", message: "missing base URL" };
|
|
@@ -6534,6 +7617,16 @@ function toLocalConfig(config) {
|
|
|
6534
7617
|
}
|
|
6535
7618
|
var localAdapter = {
|
|
6536
7619
|
name: "local",
|
|
7620
|
+
displayName: "Local",
|
|
7621
|
+
mode: "api",
|
|
7622
|
+
modelRegistry: {
|
|
7623
|
+
defaultModel: "llama3",
|
|
7624
|
+
validationPattern: /./,
|
|
7625
|
+
validationHint: "any model name accepted",
|
|
7626
|
+
knownModels: [
|
|
7627
|
+
{ id: "llama3", displayName: "Llama 3", tier: "standard" }
|
|
7628
|
+
]
|
|
7629
|
+
},
|
|
6537
7630
|
validateConfig(config) {
|
|
6538
7631
|
const result = validateConfig4(toLocalConfig(config));
|
|
6539
7632
|
return {
|
|
@@ -7050,6 +8143,16 @@ function getScreenshotDir2() {
|
|
|
7050
8143
|
}
|
|
7051
8144
|
var cdpChatgptAdapter = {
|
|
7052
8145
|
name: "cdp:chatgpt",
|
|
8146
|
+
displayName: "ChatGPT (Browser)",
|
|
8147
|
+
mode: "browser",
|
|
8148
|
+
modelRegistry: {
|
|
8149
|
+
defaultModel: "chatgpt-web",
|
|
8150
|
+
validationPattern: /./,
|
|
8151
|
+
validationHint: "model is detected from the ChatGPT web UI",
|
|
8152
|
+
knownModels: [
|
|
8153
|
+
{ id: "chatgpt-web", displayName: "ChatGPT (Web UI)", tier: "standard" }
|
|
8154
|
+
]
|
|
8155
|
+
},
|
|
7053
8156
|
validateConfig(config) {
|
|
7054
8157
|
if (!config.cdpEndpoint) {
|
|
7055
8158
|
return {
|
|
@@ -7059,74 +8162,275 @@ var cdpChatgptAdapter = {
|
|
|
7059
8162
|
};
|
|
7060
8163
|
}
|
|
7061
8164
|
return {
|
|
7062
|
-
ok: true,
|
|
7063
|
-
provider: "cdp:chatgpt",
|
|
7064
|
-
message: `CDP endpoint: ${config.cdpEndpoint}`
|
|
8165
|
+
ok: true,
|
|
8166
|
+
provider: "cdp:chatgpt",
|
|
8167
|
+
message: `CDP endpoint: ${config.cdpEndpoint}`
|
|
8168
|
+
};
|
|
8169
|
+
},
|
|
8170
|
+
async healthcheck(config) {
|
|
8171
|
+
try {
|
|
8172
|
+
const conn = getConnection(config);
|
|
8173
|
+
const health = await conn.healthcheck();
|
|
8174
|
+
if (!health.connected) {
|
|
8175
|
+
return {
|
|
8176
|
+
ok: false,
|
|
8177
|
+
provider: "cdp:chatgpt",
|
|
8178
|
+
message: `Chrome not reachable at ${config.cdpEndpoint}`
|
|
8179
|
+
};
|
|
8180
|
+
}
|
|
8181
|
+
return {
|
|
8182
|
+
ok: true,
|
|
8183
|
+
provider: "cdp:chatgpt",
|
|
8184
|
+
message: `Connected to ${health.browserVersion ?? "Chrome"} via CDP`,
|
|
8185
|
+
model: "chatgpt-web"
|
|
8186
|
+
};
|
|
8187
|
+
} catch (err) {
|
|
8188
|
+
return {
|
|
8189
|
+
ok: false,
|
|
8190
|
+
provider: "cdp:chatgpt",
|
|
8191
|
+
message: err instanceof Error ? err.message : String(err)
|
|
8192
|
+
};
|
|
8193
|
+
}
|
|
8194
|
+
},
|
|
8195
|
+
async executeTrackedQuery(input, config) {
|
|
8196
|
+
const conn = getConnection(config);
|
|
8197
|
+
const target = chatgptTarget;
|
|
8198
|
+
const client = await conn.prepareForQuery(target);
|
|
8199
|
+
await target.submitQuery(client, input.keyword);
|
|
8200
|
+
await target.waitForResponse(client);
|
|
8201
|
+
const answerText = await target.extractAnswer(client);
|
|
8202
|
+
const groundingSources = await target.extractCitations(client);
|
|
8203
|
+
const screenshotId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
8204
|
+
const screenshotPath = path4.join(getScreenshotDir2(), `${screenshotId}.png`);
|
|
8205
|
+
let capturedScreenshotPath;
|
|
8206
|
+
try {
|
|
8207
|
+
capturedScreenshotPath = await captureElementScreenshot(
|
|
8208
|
+
client,
|
|
8209
|
+
target.responseSelector,
|
|
8210
|
+
screenshotPath
|
|
8211
|
+
);
|
|
8212
|
+
} catch {
|
|
8213
|
+
}
|
|
8214
|
+
return {
|
|
8215
|
+
provider: "cdp:chatgpt",
|
|
8216
|
+
rawResponse: {
|
|
8217
|
+
answerText,
|
|
8218
|
+
groundingSources,
|
|
8219
|
+
extractedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8220
|
+
targetUrl: target.newConversationUrl
|
|
8221
|
+
},
|
|
8222
|
+
model: "chatgpt-web",
|
|
8223
|
+
groundingSources,
|
|
8224
|
+
searchQueries: [input.keyword],
|
|
8225
|
+
screenshotPath: capturedScreenshotPath
|
|
8226
|
+
};
|
|
8227
|
+
},
|
|
8228
|
+
normalizeResult(raw) {
|
|
8229
|
+
return normalizeResult5(raw);
|
|
8230
|
+
},
|
|
8231
|
+
async generateText() {
|
|
8232
|
+
throw new Error("generateText is not supported for browser-based CDP providers");
|
|
8233
|
+
}
|
|
8234
|
+
};
|
|
8235
|
+
|
|
8236
|
+
// ../provider-perplexity/src/normalize.ts
|
|
8237
|
+
import OpenAI3 from "openai";
|
|
8238
|
+
var DEFAULT_MODEL5 = "sonar";
|
|
8239
|
+
var BASE_URL = "https://api.perplexity.ai";
|
|
8240
|
+
function validateConfig5(config) {
|
|
8241
|
+
if (!config.apiKey || config.apiKey.length === 0) {
|
|
8242
|
+
return { ok: false, provider: "perplexity", message: "missing api key" };
|
|
8243
|
+
}
|
|
8244
|
+
return {
|
|
8245
|
+
ok: true,
|
|
8246
|
+
provider: "perplexity",
|
|
8247
|
+
message: "config valid",
|
|
8248
|
+
model: config.model ?? DEFAULT_MODEL5
|
|
8249
|
+
};
|
|
8250
|
+
}
|
|
8251
|
+
async function healthcheck5(config) {
|
|
8252
|
+
const validation = validateConfig5(config);
|
|
8253
|
+
if (!validation.ok) return validation;
|
|
8254
|
+
try {
|
|
8255
|
+
const client = new OpenAI3({ apiKey: config.apiKey, baseURL: BASE_URL });
|
|
8256
|
+
const response = await client.chat.completions.create({
|
|
8257
|
+
model: config.model ?? DEFAULT_MODEL5,
|
|
8258
|
+
messages: [{ role: "user", content: 'Say "ok"' }]
|
|
8259
|
+
});
|
|
8260
|
+
const text2 = response.choices[0]?.message?.content ?? "";
|
|
8261
|
+
return {
|
|
8262
|
+
ok: text2.length > 0,
|
|
8263
|
+
provider: "perplexity",
|
|
8264
|
+
message: text2.length > 0 ? "perplexity api key verified" : "empty response from perplexity",
|
|
8265
|
+
model: config.model ?? DEFAULT_MODEL5
|
|
8266
|
+
};
|
|
8267
|
+
} catch (err) {
|
|
8268
|
+
return {
|
|
8269
|
+
ok: false,
|
|
8270
|
+
provider: "perplexity",
|
|
8271
|
+
message: err instanceof Error ? err.message : String(err),
|
|
8272
|
+
model: config.model ?? DEFAULT_MODEL5
|
|
8273
|
+
};
|
|
8274
|
+
}
|
|
8275
|
+
}
|
|
8276
|
+
async function executeTrackedQuery5(input) {
|
|
8277
|
+
const model = input.config.model ?? DEFAULT_MODEL5;
|
|
8278
|
+
const client = new OpenAI3({ apiKey: input.config.apiKey, baseURL: BASE_URL });
|
|
8279
|
+
const response = await client.chat.completions.create({
|
|
8280
|
+
model,
|
|
8281
|
+
messages: [
|
|
8282
|
+
{ role: "user", content: input.keyword }
|
|
8283
|
+
]
|
|
8284
|
+
});
|
|
8285
|
+
const rawResponse = responseToRecord4(response);
|
|
8286
|
+
const citations = extractCitations(rawResponse);
|
|
8287
|
+
const groundingSources = citations.map((url) => ({
|
|
8288
|
+
uri: url,
|
|
8289
|
+
title: ""
|
|
8290
|
+
}));
|
|
8291
|
+
return {
|
|
8292
|
+
provider: "perplexity",
|
|
8293
|
+
rawResponse,
|
|
8294
|
+
model,
|
|
8295
|
+
groundingSources,
|
|
8296
|
+
searchQueries: [input.keyword]
|
|
8297
|
+
};
|
|
8298
|
+
}
|
|
8299
|
+
function normalizeResult6(raw) {
|
|
8300
|
+
const answerText = extractAnswerText3(raw.rawResponse);
|
|
8301
|
+
const citedDomains = extractCitedDomains5(raw.groundingSources);
|
|
8302
|
+
return {
|
|
8303
|
+
provider: "perplexity",
|
|
8304
|
+
answerText,
|
|
8305
|
+
citedDomains,
|
|
8306
|
+
groundingSources: raw.groundingSources,
|
|
8307
|
+
searchQueries: raw.searchQueries
|
|
8308
|
+
};
|
|
8309
|
+
}
|
|
8310
|
+
function extractCitations(rawResponse) {
|
|
8311
|
+
const citations = rawResponse.citations;
|
|
8312
|
+
if (!Array.isArray(citations)) return [];
|
|
8313
|
+
return citations.filter((c) => typeof c === "string");
|
|
8314
|
+
}
|
|
8315
|
+
function extractAnswerText3(rawResponse) {
|
|
8316
|
+
try {
|
|
8317
|
+
const choices = rawResponse.choices;
|
|
8318
|
+
if (!choices?.length) return "";
|
|
8319
|
+
return choices[0].message?.content ?? "";
|
|
8320
|
+
} catch {
|
|
8321
|
+
return "";
|
|
8322
|
+
}
|
|
8323
|
+
}
|
|
8324
|
+
function extractCitedDomains5(groundingSources) {
|
|
8325
|
+
const domains = /* @__PURE__ */ new Set();
|
|
8326
|
+
for (const source of groundingSources) {
|
|
8327
|
+
const domain = extractDomainFromUri4(source.uri);
|
|
8328
|
+
if (domain) domains.add(domain);
|
|
8329
|
+
}
|
|
8330
|
+
return [...domains];
|
|
8331
|
+
}
|
|
8332
|
+
function extractDomainFromUri4(uri) {
|
|
8333
|
+
try {
|
|
8334
|
+
const url = new URL(uri);
|
|
8335
|
+
return url.hostname.replace(/^www\./, "");
|
|
8336
|
+
} catch {
|
|
8337
|
+
return null;
|
|
8338
|
+
}
|
|
8339
|
+
}
|
|
8340
|
+
async function generateText5(prompt, config) {
|
|
8341
|
+
const model = config.model ?? DEFAULT_MODEL5;
|
|
8342
|
+
const client = new OpenAI3({ apiKey: config.apiKey, baseURL: BASE_URL });
|
|
8343
|
+
const response = await client.chat.completions.create({
|
|
8344
|
+
model,
|
|
8345
|
+
messages: [{ role: "user", content: prompt }]
|
|
8346
|
+
});
|
|
8347
|
+
return response.choices[0]?.message?.content ?? "";
|
|
8348
|
+
}
|
|
8349
|
+
function responseToRecord4(response) {
|
|
8350
|
+
try {
|
|
8351
|
+
return JSON.parse(JSON.stringify(response));
|
|
8352
|
+
} catch {
|
|
8353
|
+
return { error: "failed to serialize response" };
|
|
8354
|
+
}
|
|
8355
|
+
}
|
|
8356
|
+
|
|
8357
|
+
// ../provider-perplexity/src/adapter.ts
|
|
8358
|
+
function toPerplexityConfig(config) {
|
|
8359
|
+
return {
|
|
8360
|
+
apiKey: config.apiKey ?? "",
|
|
8361
|
+
model: config.model,
|
|
8362
|
+
quotaPolicy: config.quotaPolicy
|
|
8363
|
+
};
|
|
8364
|
+
}
|
|
8365
|
+
var perplexityAdapter = {
|
|
8366
|
+
name: "perplexity",
|
|
8367
|
+
displayName: "Perplexity",
|
|
8368
|
+
mode: "api",
|
|
8369
|
+
keyUrl: "https://www.perplexity.ai/settings/api",
|
|
8370
|
+
modelRegistry: {
|
|
8371
|
+
defaultModel: "sonar",
|
|
8372
|
+
validationPattern: /^sonar/,
|
|
8373
|
+
validationHint: "expected a sonar model (e.g. sonar, sonar-pro, sonar-reasoning)",
|
|
8374
|
+
knownModels: [
|
|
8375
|
+
{ id: "sonar", displayName: "Sonar", tier: "standard" },
|
|
8376
|
+
{ id: "sonar-pro", displayName: "Sonar Pro", tier: "flagship" },
|
|
8377
|
+
{ id: "sonar-reasoning", displayName: "Sonar Reasoning", tier: "flagship" },
|
|
8378
|
+
{ id: "sonar-reasoning-pro", displayName: "Sonar Reasoning Pro", tier: "flagship" }
|
|
8379
|
+
]
|
|
8380
|
+
},
|
|
8381
|
+
validateConfig(config) {
|
|
8382
|
+
const result = validateConfig5(toPerplexityConfig(config));
|
|
8383
|
+
return {
|
|
8384
|
+
ok: result.ok,
|
|
8385
|
+
provider: "perplexity",
|
|
8386
|
+
message: result.message,
|
|
8387
|
+
model: result.model
|
|
7065
8388
|
};
|
|
7066
8389
|
},
|
|
7067
8390
|
async healthcheck(config) {
|
|
7068
|
-
|
|
7069
|
-
|
|
7070
|
-
|
|
7071
|
-
|
|
7072
|
-
|
|
7073
|
-
|
|
7074
|
-
|
|
7075
|
-
message: `Chrome not reachable at ${config.cdpEndpoint}`
|
|
7076
|
-
};
|
|
7077
|
-
}
|
|
7078
|
-
return {
|
|
7079
|
-
ok: true,
|
|
7080
|
-
provider: "cdp:chatgpt",
|
|
7081
|
-
message: `Connected to ${health.browserVersion ?? "Chrome"} via CDP`,
|
|
7082
|
-
model: "chatgpt-web"
|
|
7083
|
-
};
|
|
7084
|
-
} catch (err) {
|
|
7085
|
-
return {
|
|
7086
|
-
ok: false,
|
|
7087
|
-
provider: "cdp:chatgpt",
|
|
7088
|
-
message: err instanceof Error ? err.message : String(err)
|
|
7089
|
-
};
|
|
7090
|
-
}
|
|
8391
|
+
const result = await healthcheck5(toPerplexityConfig(config));
|
|
8392
|
+
return {
|
|
8393
|
+
ok: result.ok,
|
|
8394
|
+
provider: "perplexity",
|
|
8395
|
+
message: result.message,
|
|
8396
|
+
model: result.model
|
|
8397
|
+
};
|
|
7091
8398
|
},
|
|
7092
8399
|
async executeTrackedQuery(input, config) {
|
|
7093
|
-
const
|
|
7094
|
-
|
|
7095
|
-
|
|
7096
|
-
|
|
7097
|
-
|
|
7098
|
-
|
|
7099
|
-
|
|
7100
|
-
const screenshotId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
7101
|
-
const screenshotPath = path4.join(getScreenshotDir2(), `${screenshotId}.png`);
|
|
7102
|
-
let capturedScreenshotPath;
|
|
7103
|
-
try {
|
|
7104
|
-
capturedScreenshotPath = await captureElementScreenshot(
|
|
7105
|
-
client,
|
|
7106
|
-
target.responseSelector,
|
|
7107
|
-
screenshotPath
|
|
7108
|
-
);
|
|
7109
|
-
} catch {
|
|
7110
|
-
}
|
|
8400
|
+
const raw = await executeTrackedQuery5({
|
|
8401
|
+
keyword: input.keyword,
|
|
8402
|
+
canonicalDomains: input.canonicalDomains,
|
|
8403
|
+
competitorDomains: input.competitorDomains,
|
|
8404
|
+
config: toPerplexityConfig(config),
|
|
8405
|
+
location: input.location
|
|
8406
|
+
});
|
|
7111
8407
|
return {
|
|
7112
|
-
provider: "
|
|
7113
|
-
rawResponse:
|
|
7114
|
-
|
|
7115
|
-
|
|
7116
|
-
|
|
7117
|
-
targetUrl: target.newConversationUrl
|
|
7118
|
-
},
|
|
7119
|
-
model: "chatgpt-web",
|
|
7120
|
-
groundingSources,
|
|
7121
|
-
searchQueries: [input.keyword],
|
|
7122
|
-
screenshotPath: capturedScreenshotPath
|
|
8408
|
+
provider: "perplexity",
|
|
8409
|
+
rawResponse: raw.rawResponse,
|
|
8410
|
+
model: raw.model,
|
|
8411
|
+
groundingSources: raw.groundingSources,
|
|
8412
|
+
searchQueries: raw.searchQueries
|
|
7123
8413
|
};
|
|
7124
8414
|
},
|
|
7125
8415
|
normalizeResult(raw) {
|
|
7126
|
-
|
|
8416
|
+
const perplexityRaw = {
|
|
8417
|
+
provider: "perplexity",
|
|
8418
|
+
rawResponse: raw.rawResponse,
|
|
8419
|
+
model: raw.model,
|
|
8420
|
+
groundingSources: raw.groundingSources,
|
|
8421
|
+
searchQueries: raw.searchQueries
|
|
8422
|
+
};
|
|
8423
|
+
const normalized = normalizeResult6(perplexityRaw);
|
|
8424
|
+
return {
|
|
8425
|
+
provider: "perplexity",
|
|
8426
|
+
answerText: normalized.answerText,
|
|
8427
|
+
citedDomains: normalized.citedDomains,
|
|
8428
|
+
groundingSources: normalized.groundingSources,
|
|
8429
|
+
searchQueries: normalized.searchQueries
|
|
8430
|
+
};
|
|
7127
8431
|
},
|
|
7128
|
-
async generateText() {
|
|
7129
|
-
|
|
8432
|
+
async generateText(prompt, config) {
|
|
8433
|
+
return generateText5(prompt, toPerplexityConfig(config));
|
|
7130
8434
|
}
|
|
7131
8435
|
};
|
|
7132
8436
|
|
|
@@ -7208,7 +8512,75 @@ import crypto15 from "crypto";
|
|
|
7208
8512
|
import fs4 from "fs";
|
|
7209
8513
|
import path5 from "path";
|
|
7210
8514
|
import os4 from "os";
|
|
7211
|
-
import { eq as eq16, inArray as inArray3 } from "drizzle-orm";
|
|
8515
|
+
import { and as and6, eq as eq16, inArray as inArray3 } from "drizzle-orm";
|
|
8516
|
+
var RunCancelledError = class extends Error {
|
|
8517
|
+
constructor(runId) {
|
|
8518
|
+
super(`Run ${runId} was cancelled`);
|
|
8519
|
+
this.name = "RunCancelledError";
|
|
8520
|
+
}
|
|
8521
|
+
};
|
|
8522
|
+
var ProviderExecutionGate = class {
|
|
8523
|
+
constructor(maxConcurrency, maxPerMinute) {
|
|
8524
|
+
this.maxConcurrency = maxConcurrency;
|
|
8525
|
+
this.maxPerMinute = maxPerMinute;
|
|
8526
|
+
}
|
|
8527
|
+
window = [];
|
|
8528
|
+
waiters = [];
|
|
8529
|
+
rateLimitChain = Promise.resolve();
|
|
8530
|
+
inFlight = 0;
|
|
8531
|
+
async run(task) {
|
|
8532
|
+
await this.acquire();
|
|
8533
|
+
try {
|
|
8534
|
+
await this.waitForRateLimit();
|
|
8535
|
+
return await task();
|
|
8536
|
+
} finally {
|
|
8537
|
+
this.release();
|
|
8538
|
+
}
|
|
8539
|
+
}
|
|
8540
|
+
async acquire() {
|
|
8541
|
+
if (this.inFlight < Math.max(1, this.maxConcurrency)) {
|
|
8542
|
+
this.inFlight++;
|
|
8543
|
+
return;
|
|
8544
|
+
}
|
|
8545
|
+
await new Promise((resolve) => {
|
|
8546
|
+
this.waiters.push(resolve);
|
|
8547
|
+
});
|
|
8548
|
+
this.inFlight++;
|
|
8549
|
+
}
|
|
8550
|
+
release() {
|
|
8551
|
+
this.inFlight = Math.max(0, this.inFlight - 1);
|
|
8552
|
+
const next = this.waiters.shift();
|
|
8553
|
+
next?.();
|
|
8554
|
+
}
|
|
8555
|
+
async waitForRateLimit() {
|
|
8556
|
+
let releaseChain;
|
|
8557
|
+
const previousChain = this.rateLimitChain;
|
|
8558
|
+
this.rateLimitChain = new Promise((resolve) => {
|
|
8559
|
+
releaseChain = resolve;
|
|
8560
|
+
});
|
|
8561
|
+
await previousChain;
|
|
8562
|
+
try {
|
|
8563
|
+
const now = Date.now();
|
|
8564
|
+
const windowStart = now - 6e4;
|
|
8565
|
+
while (this.window.length > 0 && this.window[0] < windowStart) {
|
|
8566
|
+
this.window.shift();
|
|
8567
|
+
}
|
|
8568
|
+
if (this.window.length >= this.maxPerMinute) {
|
|
8569
|
+
const oldestInWindow = this.window[0];
|
|
8570
|
+
const waitMs = oldestInWindow + 6e4 - now + 50;
|
|
8571
|
+
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
8572
|
+
const nowAfterWait = Date.now();
|
|
8573
|
+
const newWindowStart = nowAfterWait - 6e4;
|
|
8574
|
+
while (this.window.length > 0 && this.window[0] < newWindowStart) {
|
|
8575
|
+
this.window.shift();
|
|
8576
|
+
}
|
|
8577
|
+
}
|
|
8578
|
+
this.window.push(Date.now());
|
|
8579
|
+
} finally {
|
|
8580
|
+
releaseChain?.();
|
|
8581
|
+
}
|
|
8582
|
+
}
|
|
8583
|
+
};
|
|
7212
8584
|
var JobRunner = class {
|
|
7213
8585
|
db;
|
|
7214
8586
|
registry;
|
|
@@ -7229,13 +8601,34 @@ var JobRunner = class {
|
|
|
7229
8601
|
async executeRun(runId, projectId, providerOverride, locationOverride) {
|
|
7230
8602
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
7231
8603
|
const startTime = Date.now();
|
|
8604
|
+
let runLocation;
|
|
8605
|
+
let activeProviders = [];
|
|
8606
|
+
let projectKeywords = [];
|
|
8607
|
+
const providerDispatchCounts = /* @__PURE__ */ new Map();
|
|
7232
8608
|
try {
|
|
7233
|
-
|
|
8609
|
+
const existingRun = this.getRunState(runId);
|
|
8610
|
+
if (!existingRun) {
|
|
8611
|
+
throw new Error(`Run ${runId} not found`);
|
|
8612
|
+
}
|
|
8613
|
+
if (existingRun.status === "cancelled") {
|
|
8614
|
+
this.handleCancelledRun(runId, projectId, startTime, {
|
|
8615
|
+
providerCount: 0,
|
|
8616
|
+
providers: [],
|
|
8617
|
+
keywordCount: 0
|
|
8618
|
+
});
|
|
8619
|
+
return;
|
|
8620
|
+
}
|
|
8621
|
+
if (existingRun.status !== "queued" && existingRun.status !== "running") {
|
|
8622
|
+
throw new Error(`Run ${runId} is not executable from status '${existingRun.status}'`);
|
|
8623
|
+
}
|
|
8624
|
+
if (existingRun.status === "queued") {
|
|
8625
|
+
this.db.update(runs).set({ status: "running", startedAt: now }).where(and6(eq16(runs.id, runId), eq16(runs.status, "queued"))).run();
|
|
8626
|
+
}
|
|
8627
|
+
this.throwIfRunCancelled(runId);
|
|
7234
8628
|
const project = this.db.select().from(projects).where(eq16(projects.id, projectId)).get();
|
|
7235
8629
|
if (!project) {
|
|
7236
8630
|
throw new Error(`Project ${projectId} not found`);
|
|
7237
8631
|
}
|
|
7238
|
-
let runLocation;
|
|
7239
8632
|
if (locationOverride === null) {
|
|
7240
8633
|
runLocation = void 0;
|
|
7241
8634
|
} else if (locationOverride) {
|
|
@@ -7247,16 +8640,26 @@ var JobRunner = class {
|
|
|
7247
8640
|
}
|
|
7248
8641
|
}
|
|
7249
8642
|
const projectProviders = providerOverride ?? JSON.parse(project.providers || "[]");
|
|
7250
|
-
|
|
8643
|
+
activeProviders = this.registry.getForProject(projectProviders);
|
|
7251
8644
|
if (activeProviders.length === 0) {
|
|
7252
8645
|
throw new Error("No providers configured. Add at least one provider API key.");
|
|
7253
8646
|
}
|
|
7254
8647
|
console.log(`[JobRunner] Run ${runId}: dispatching to ${activeProviders.length} providers: ${activeProviders.map((p) => p.adapter.name).join(", ")}`);
|
|
7255
|
-
|
|
8648
|
+
projectKeywords = this.db.select().from(keywords).where(eq16(keywords.projectId, projectId)).all();
|
|
7256
8649
|
const projectCompetitors = this.db.select().from(competitors).where(eq16(competitors.projectId, projectId)).all();
|
|
7257
8650
|
const competitorDomains = projectCompetitors.map((c) => c.domain);
|
|
8651
|
+
const allDomains = effectiveDomains({
|
|
8652
|
+
canonicalDomain: project.canonicalDomain,
|
|
8653
|
+
ownedDomains: JSON.parse(project.ownedDomains || "[]")
|
|
8654
|
+
});
|
|
8655
|
+
const executionContext = {
|
|
8656
|
+
providerCount: activeProviders.length,
|
|
8657
|
+
providers: activeProviders.map((provider) => provider.adapter.name),
|
|
8658
|
+
keywordCount: projectKeywords.length,
|
|
8659
|
+
...runLocation ? { location: runLocation.label } : {}
|
|
8660
|
+
};
|
|
7258
8661
|
const queriesPerProvider = projectKeywords.length;
|
|
7259
|
-
const todayPeriod =
|
|
8662
|
+
const todayPeriod = getCurrentUsageDay();
|
|
7260
8663
|
for (const p of activeProviders) {
|
|
7261
8664
|
const providerScope = `${projectId}:${p.adapter.name}`;
|
|
7262
8665
|
const providerUsage = this.db.select().from(usageCounters).where(eq16(usageCounters.scope, providerScope)).all().filter((r) => r.period === todayPeriod && r.metric === "queries").reduce((sum, r) => sum + r.count, 0);
|
|
@@ -7267,27 +8670,31 @@ var JobRunner = class {
|
|
|
7267
8670
|
);
|
|
7268
8671
|
}
|
|
7269
8672
|
}
|
|
7270
|
-
const
|
|
7271
|
-
for (const
|
|
7272
|
-
|
|
8673
|
+
const executionGates = /* @__PURE__ */ new Map();
|
|
8674
|
+
for (const provider of activeProviders) {
|
|
8675
|
+
executionGates.set(
|
|
8676
|
+
provider.adapter.name,
|
|
8677
|
+
new ProviderExecutionGate(
|
|
8678
|
+
provider.config.quotaPolicy.maxConcurrency,
|
|
8679
|
+
provider.config.quotaPolicy.maxRequestsPerMinute
|
|
8680
|
+
)
|
|
8681
|
+
);
|
|
7273
8682
|
}
|
|
7274
8683
|
const providerErrors = /* @__PURE__ */ new Map();
|
|
7275
8684
|
let totalSnapshotsInserted = 0;
|
|
7276
8685
|
const apiProviders = activeProviders.filter((p) => !isBrowserProvider(p.adapter.name));
|
|
7277
8686
|
const browserProviders = activeProviders.filter((p) => isBrowserProvider(p.adapter.name));
|
|
7278
|
-
|
|
7279
|
-
const
|
|
7280
|
-
|
|
7281
|
-
|
|
7282
|
-
|
|
7283
|
-
|
|
7284
|
-
|
|
7285
|
-
|
|
7286
|
-
|
|
7287
|
-
|
|
7288
|
-
|
|
7289
|
-
ownedDomains: JSON.parse(project.ownedDomains || "[]")
|
|
7290
|
-
});
|
|
8687
|
+
const processKeywordForProvider = async (registeredProvider, kw) => {
|
|
8688
|
+
const { adapter, config } = registeredProvider;
|
|
8689
|
+
const providerName = adapter.name;
|
|
8690
|
+
const gate = executionGates.get(providerName);
|
|
8691
|
+
if (!gate) {
|
|
8692
|
+
throw new Error(`Missing execution gate for provider ${providerName}`);
|
|
8693
|
+
}
|
|
8694
|
+
try {
|
|
8695
|
+
await gate.run(async () => {
|
|
8696
|
+
this.throwIfRunCancelled(runId);
|
|
8697
|
+
providerDispatchCounts.set(providerName, (providerDispatchCounts.get(providerName) ?? 0) + 1);
|
|
7291
8698
|
const raw = await adapter.executeTrackedQuery(
|
|
7292
8699
|
{
|
|
7293
8700
|
keyword: kw.keyword,
|
|
@@ -7297,6 +8704,7 @@ var JobRunner = class {
|
|
|
7297
8704
|
},
|
|
7298
8705
|
config
|
|
7299
8706
|
);
|
|
8707
|
+
this.throwIfRunCancelled(runId);
|
|
7300
8708
|
const normalized = adapter.normalizeResult(raw);
|
|
7301
8709
|
console.log(`[JobRunner] ${providerName}: "${kw.keyword}" citedDomains=${JSON.stringify(normalized.citedDomains)}, groundingSources=${JSON.stringify(normalized.groundingSources.map((s) => s.uri))}, domains=${JSON.stringify(allDomains)}`);
|
|
7302
8710
|
const citationState = determineCitationState(normalized, allDomains);
|
|
@@ -7352,96 +8760,29 @@ var JobRunner = class {
|
|
|
7352
8760
|
}
|
|
7353
8761
|
totalSnapshotsInserted++;
|
|
7354
8762
|
console.log(`[JobRunner] ${providerName}: keyword "${kw.keyword}" \u2192 ${citationState}`);
|
|
7355
|
-
}
|
|
7356
|
-
|
|
7357
|
-
|
|
7358
|
-
|
|
8763
|
+
});
|
|
8764
|
+
} catch (err) {
|
|
8765
|
+
if (err instanceof RunCancelledError) {
|
|
8766
|
+
throw err;
|
|
7359
8767
|
}
|
|
7360
|
-
|
|
7361
|
-
|
|
7362
|
-
|
|
7363
|
-
const { adapter, config } = registeredProvider;
|
|
7364
|
-
const providerName = adapter.name;
|
|
7365
|
-
try {
|
|
7366
|
-
await this.waitForRateLimit(
|
|
7367
|
-
minuteWindows.get(providerName),
|
|
7368
|
-
config.quotaPolicy.maxRequestsPerMinute
|
|
7369
|
-
);
|
|
7370
|
-
const allDomains = effectiveDomains({
|
|
7371
|
-
canonicalDomain: project.canonicalDomain,
|
|
7372
|
-
ownedDomains: JSON.parse(project.ownedDomains || "[]")
|
|
7373
|
-
});
|
|
7374
|
-
const raw = await adapter.executeTrackedQuery(
|
|
7375
|
-
{
|
|
7376
|
-
keyword: kw.keyword,
|
|
7377
|
-
canonicalDomains: allDomains,
|
|
7378
|
-
competitorDomains,
|
|
7379
|
-
location: runLocation
|
|
7380
|
-
},
|
|
7381
|
-
config
|
|
7382
|
-
);
|
|
7383
|
-
const normalized = adapter.normalizeResult(raw);
|
|
7384
|
-
console.log(`[JobRunner] ${providerName}: "${kw.keyword}" citedDomains=${JSON.stringify(normalized.citedDomains)}, groundingSources=${JSON.stringify(normalized.groundingSources.map((s) => s.uri))}, domains=${JSON.stringify(allDomains)}`);
|
|
7385
|
-
const citationState = determineCitationState(normalized, allDomains);
|
|
7386
|
-
const overlap = computeCompetitorOverlap(normalized, competitorDomains);
|
|
7387
|
-
let screenshotRelPath = null;
|
|
7388
|
-
if (raw.screenshotPath && fs4.existsSync(raw.screenshotPath)) {
|
|
7389
|
-
const snapshotId = crypto15.randomUUID();
|
|
7390
|
-
const screenshotDir = path5.join(os4.homedir(), ".canonry", "screenshots", runId);
|
|
7391
|
-
if (!fs4.existsSync(screenshotDir)) fs4.mkdirSync(screenshotDir, { recursive: true });
|
|
7392
|
-
const destPath = path5.join(screenshotDir, `${snapshotId}.png`);
|
|
7393
|
-
fs4.renameSync(raw.screenshotPath, destPath);
|
|
7394
|
-
screenshotRelPath = `${runId}/${snapshotId}.png`;
|
|
7395
|
-
this.db.insert(querySnapshots).values({
|
|
7396
|
-
id: snapshotId,
|
|
7397
|
-
runId,
|
|
7398
|
-
keywordId: kw.id,
|
|
7399
|
-
provider: providerName,
|
|
7400
|
-
model: raw.model,
|
|
7401
|
-
citationState,
|
|
7402
|
-
answerText: normalized.answerText,
|
|
7403
|
-
citedDomains: JSON.stringify(normalized.citedDomains),
|
|
7404
|
-
competitorOverlap: JSON.stringify(overlap),
|
|
7405
|
-
location: runLocation?.label ?? null,
|
|
7406
|
-
screenshotPath: screenshotRelPath,
|
|
7407
|
-
rawResponse: JSON.stringify({
|
|
7408
|
-
model: raw.model,
|
|
7409
|
-
groundingSources: normalized.groundingSources,
|
|
7410
|
-
searchQueries: normalized.searchQueries,
|
|
7411
|
-
apiResponse: raw.rawResponse
|
|
7412
|
-
}),
|
|
7413
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
7414
|
-
}).run();
|
|
7415
|
-
} else {
|
|
7416
|
-
this.db.insert(querySnapshots).values({
|
|
7417
|
-
id: crypto15.randomUUID(),
|
|
7418
|
-
runId,
|
|
7419
|
-
keywordId: kw.id,
|
|
7420
|
-
provider: providerName,
|
|
7421
|
-
model: raw.model,
|
|
7422
|
-
citationState,
|
|
7423
|
-
answerText: normalized.answerText,
|
|
7424
|
-
citedDomains: JSON.stringify(normalized.citedDomains),
|
|
7425
|
-
competitorOverlap: JSON.stringify(overlap),
|
|
7426
|
-
location: runLocation?.label ?? null,
|
|
7427
|
-
rawResponse: JSON.stringify({
|
|
7428
|
-
model: raw.model,
|
|
7429
|
-
groundingSources: normalized.groundingSources,
|
|
7430
|
-
searchQueries: normalized.searchQueries,
|
|
7431
|
-
apiResponse: raw.rawResponse
|
|
7432
|
-
}),
|
|
7433
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
7434
|
-
}).run();
|
|
7435
|
-
}
|
|
7436
|
-
totalSnapshotsInserted++;
|
|
7437
|
-
console.log(`[JobRunner] ${providerName}: keyword "${kw.keyword}" \u2192 ${citationState}`);
|
|
7438
|
-
} catch (err) {
|
|
7439
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
7440
|
-
console.error(`[JobRunner] ${providerName}: keyword "${kw.keyword}" FAILED: ${msg}`);
|
|
8768
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
8769
|
+
console.error(`[JobRunner] ${providerName}: keyword "${kw.keyword}" FAILED: ${msg}`);
|
|
8770
|
+
if (!providerErrors.has(providerName)) {
|
|
7441
8771
|
providerErrors.set(providerName, msg);
|
|
7442
8772
|
}
|
|
7443
8773
|
}
|
|
8774
|
+
};
|
|
8775
|
+
await Promise.all(apiProviders.map(async (registeredProvider) => {
|
|
8776
|
+
await Promise.all(projectKeywords.map(async (kw) => {
|
|
8777
|
+
await processKeywordForProvider(registeredProvider, kw);
|
|
8778
|
+
}));
|
|
8779
|
+
}));
|
|
8780
|
+
for (const registeredProvider of browserProviders) {
|
|
8781
|
+
for (const kw of projectKeywords) {
|
|
8782
|
+
await processKeywordForProvider(registeredProvider, kw);
|
|
8783
|
+
}
|
|
7444
8784
|
}
|
|
8785
|
+
this.throwIfRunCancelled(runId);
|
|
7445
8786
|
const allFailed = totalSnapshotsInserted === 0 && providerErrors.size > 0;
|
|
7446
8787
|
const someFailed = providerErrors.size > 0;
|
|
7447
8788
|
if (allFailed) {
|
|
@@ -7453,18 +8794,16 @@ var JobRunner = class {
|
|
|
7453
8794
|
} else {
|
|
7454
8795
|
this.db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq16(runs.id, runId)).run();
|
|
7455
8796
|
}
|
|
8797
|
+
this.flushProviderUsage(projectId, providerDispatchCounts);
|
|
7456
8798
|
const finalStatus = allFailed ? "failed" : someFailed ? "partial" : "completed";
|
|
7457
8799
|
trackEvent("run.completed", {
|
|
7458
8800
|
status: finalStatus,
|
|
7459
|
-
providerCount:
|
|
7460
|
-
providers:
|
|
7461
|
-
keywordCount:
|
|
8801
|
+
providerCount: executionContext.providerCount,
|
|
8802
|
+
providers: executionContext.providers,
|
|
8803
|
+
keywordCount: executionContext.keywordCount,
|
|
7462
8804
|
durationMs: Date.now() - startTime,
|
|
7463
|
-
...
|
|
8805
|
+
...executionContext.location ? { location: executionContext.location } : {}
|
|
7464
8806
|
});
|
|
7465
|
-
for (const p of activeProviders) {
|
|
7466
|
-
this.incrementUsage(`${projectId}:${p.adapter.name}`, "queries", queriesPerProvider);
|
|
7467
|
-
}
|
|
7468
8807
|
this.incrementUsage(projectId, "runs", 1);
|
|
7469
8808
|
if (this.onRunCompleted) {
|
|
7470
8809
|
this.onRunCompleted(runId, projectId).catch((err) => {
|
|
@@ -7472,18 +8811,31 @@ var JobRunner = class {
|
|
|
7472
8811
|
});
|
|
7473
8812
|
}
|
|
7474
8813
|
} catch (err) {
|
|
8814
|
+
const executionContext = {
|
|
8815
|
+
providerCount: activeProviders.length,
|
|
8816
|
+
providers: activeProviders.map((provider) => provider.adapter.name),
|
|
8817
|
+
keywordCount: projectKeywords.length,
|
|
8818
|
+
...runLocation ? { location: runLocation.label } : {}
|
|
8819
|
+
};
|
|
8820
|
+
if (err instanceof RunCancelledError || this.isRunCancelled(runId)) {
|
|
8821
|
+
this.flushProviderUsage(projectId, providerDispatchCounts);
|
|
8822
|
+
this.handleCancelledRun(runId, projectId, startTime, executionContext);
|
|
8823
|
+
return;
|
|
8824
|
+
}
|
|
7475
8825
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
7476
8826
|
this.db.update(runs).set({
|
|
7477
8827
|
status: "failed",
|
|
7478
8828
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7479
8829
|
error: errorMessage
|
|
7480
8830
|
}).where(eq16(runs.id, runId)).run();
|
|
8831
|
+
this.flushProviderUsage(projectId, providerDispatchCounts);
|
|
7481
8832
|
trackEvent("run.completed", {
|
|
7482
8833
|
status: "failed",
|
|
7483
|
-
providerCount:
|
|
7484
|
-
providers:
|
|
7485
|
-
keywordCount:
|
|
7486
|
-
durationMs: Date.now() - startTime
|
|
8834
|
+
providerCount: executionContext.providerCount,
|
|
8835
|
+
providers: executionContext.providers,
|
|
8836
|
+
keywordCount: executionContext.keywordCount,
|
|
8837
|
+
durationMs: Date.now() - startTime,
|
|
8838
|
+
...executionContext.location ? { location: executionContext.location } : {}
|
|
7487
8839
|
});
|
|
7488
8840
|
if (this.onRunCompleted) {
|
|
7489
8841
|
this.onRunCompleted(runId, projectId).catch((notifErr) => {
|
|
@@ -7492,27 +8844,9 @@ var JobRunner = class {
|
|
|
7492
8844
|
}
|
|
7493
8845
|
}
|
|
7494
8846
|
}
|
|
7495
|
-
async waitForRateLimit(window, maxPerMinute) {
|
|
7496
|
-
const now = Date.now();
|
|
7497
|
-
const windowStart = now - 6e4;
|
|
7498
|
-
while (window.length > 0 && window[0] < windowStart) {
|
|
7499
|
-
window.shift();
|
|
7500
|
-
}
|
|
7501
|
-
if (window.length >= maxPerMinute) {
|
|
7502
|
-
const oldestInWindow = window[0];
|
|
7503
|
-
const waitMs = oldestInWindow + 6e4 - now + 50;
|
|
7504
|
-
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
7505
|
-
const nowAfterWait = Date.now();
|
|
7506
|
-
const newWindowStart = nowAfterWait - 6e4;
|
|
7507
|
-
while (window.length > 0 && window[0] < newWindowStart) {
|
|
7508
|
-
window.shift();
|
|
7509
|
-
}
|
|
7510
|
-
}
|
|
7511
|
-
window.push(Date.now());
|
|
7512
|
-
}
|
|
7513
8847
|
incrementUsage(scope, metric, count) {
|
|
7514
8848
|
const now = /* @__PURE__ */ new Date();
|
|
7515
|
-
const period =
|
|
8849
|
+
const period = now.toISOString().slice(0, 10);
|
|
7516
8850
|
const id = crypto15.randomUUID();
|
|
7517
8851
|
const existing = this.db.select().from(usageCounters).where(eq16(usageCounters.scope, scope)).all().find((r) => r.period === period && r.metric === metric);
|
|
7518
8852
|
if (existing) {
|
|
@@ -7528,10 +8862,52 @@ var JobRunner = class {
|
|
|
7528
8862
|
}).run();
|
|
7529
8863
|
}
|
|
7530
8864
|
}
|
|
8865
|
+
flushProviderUsage(projectId, providerDispatchCounts) {
|
|
8866
|
+
for (const [providerName, count] of providerDispatchCounts.entries()) {
|
|
8867
|
+
if (count <= 0) continue;
|
|
8868
|
+
this.incrementUsage(`${projectId}:${providerName}`, "queries", count);
|
|
8869
|
+
}
|
|
8870
|
+
}
|
|
8871
|
+
getRunState(runId) {
|
|
8872
|
+
return this.db.select({
|
|
8873
|
+
status: runs.status,
|
|
8874
|
+
finishedAt: runs.finishedAt,
|
|
8875
|
+
error: runs.error
|
|
8876
|
+
}).from(runs).where(eq16(runs.id, runId)).get();
|
|
8877
|
+
}
|
|
8878
|
+
isRunCancelled(runId) {
|
|
8879
|
+
return this.getRunState(runId)?.status === "cancelled";
|
|
8880
|
+
}
|
|
8881
|
+
throwIfRunCancelled(runId) {
|
|
8882
|
+
if (this.isRunCancelled(runId)) {
|
|
8883
|
+
throw new RunCancelledError(runId);
|
|
8884
|
+
}
|
|
8885
|
+
}
|
|
8886
|
+
handleCancelledRun(runId, projectId, startTime, context) {
|
|
8887
|
+
const currentRun = this.getRunState(runId);
|
|
8888
|
+
if (currentRun && !currentRun.finishedAt) {
|
|
8889
|
+
this.db.update(runs).set({
|
|
8890
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8891
|
+
error: currentRun.error ?? "Cancelled by user"
|
|
8892
|
+
}).where(eq16(runs.id, runId)).run();
|
|
8893
|
+
}
|
|
8894
|
+
trackEvent("run.completed", {
|
|
8895
|
+
status: "cancelled",
|
|
8896
|
+
providerCount: context.providerCount,
|
|
8897
|
+
providers: context.providers,
|
|
8898
|
+
keywordCount: context.keywordCount,
|
|
8899
|
+
durationMs: Date.now() - startTime,
|
|
8900
|
+
...context.location ? { location: context.location } : {}
|
|
8901
|
+
});
|
|
8902
|
+
if (this.onRunCompleted) {
|
|
8903
|
+
this.onRunCompleted(runId, projectId).catch((err) => {
|
|
8904
|
+
console.error("[JobRunner] Notification callback failed:", err);
|
|
8905
|
+
});
|
|
8906
|
+
}
|
|
8907
|
+
}
|
|
7531
8908
|
};
|
|
7532
|
-
function
|
|
7533
|
-
|
|
7534
|
-
return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, "0")}`;
|
|
8909
|
+
function getCurrentUsageDay() {
|
|
8910
|
+
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
7535
8911
|
}
|
|
7536
8912
|
function domainMatches(domain, canonicalDomain) {
|
|
7537
8913
|
const normalized = normalizeProjectDomain(canonicalDomain);
|
|
@@ -7597,7 +8973,7 @@ function computeCompetitorOverlap(normalized, competitorDomains) {
|
|
|
7597
8973
|
|
|
7598
8974
|
// src/gsc-sync.ts
|
|
7599
8975
|
import crypto16 from "crypto";
|
|
7600
|
-
import { eq as eq17, and as
|
|
8976
|
+
import { eq as eq17, and as and7, sql as sql3 } from "drizzle-orm";
|
|
7601
8977
|
function formatDate(d) {
|
|
7602
8978
|
return d.toISOString().split("T")[0];
|
|
7603
8979
|
}
|
|
@@ -7648,7 +9024,7 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
7648
9024
|
});
|
|
7649
9025
|
console.log(`[GSC Sync] Received ${rows.length} rows`);
|
|
7650
9026
|
db.delete(gscSearchData).where(
|
|
7651
|
-
|
|
9027
|
+
and7(
|
|
7652
9028
|
eq17(gscSearchData.projectId, projectId),
|
|
7653
9029
|
sql3`${gscSearchData.date} >= ${startDate}`,
|
|
7654
9030
|
sql3`${gscSearchData.date} <= ${endDate}`
|
|
@@ -7737,7 +9113,7 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
7737
9113
|
}
|
|
7738
9114
|
}
|
|
7739
9115
|
const snapshotDate = formatDate(/* @__PURE__ */ new Date());
|
|
7740
|
-
db.delete(gscCoverageSnapshots).where(
|
|
9116
|
+
db.delete(gscCoverageSnapshots).where(and7(eq17(gscCoverageSnapshots.projectId, projectId), eq17(gscCoverageSnapshots.date, snapshotDate))).run();
|
|
7741
9117
|
db.insert(gscCoverageSnapshots).values({
|
|
7742
9118
|
id: crypto16.randomUUID(),
|
|
7743
9119
|
projectId,
|
|
@@ -7760,7 +9136,7 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
7760
9136
|
|
|
7761
9137
|
// src/gsc-inspect-sitemap.ts
|
|
7762
9138
|
import crypto17 from "crypto";
|
|
7763
|
-
import { eq as eq18, and as
|
|
9139
|
+
import { eq as eq18, and as and8 } from "drizzle-orm";
|
|
7764
9140
|
|
|
7765
9141
|
// src/sitemap-parser.ts
|
|
7766
9142
|
var LOC_REGEX = /<loc>\s*([^<]+?)\s*<\/loc>/gi;
|
|
@@ -7923,7 +9299,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
|
|
|
7923
9299
|
}
|
|
7924
9300
|
}
|
|
7925
9301
|
const snapshotDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
7926
|
-
db.delete(gscCoverageSnapshots).where(
|
|
9302
|
+
db.delete(gscCoverageSnapshots).where(and8(eq18(gscCoverageSnapshots.projectId, projectId), eq18(gscCoverageSnapshots.date, snapshotDate))).run();
|
|
7927
9303
|
db.insert(gscCoverageSnapshots).values({
|
|
7928
9304
|
id: crypto17.randomUUID(),
|
|
7929
9305
|
projectId,
|
|
@@ -8115,7 +9491,7 @@ var Scheduler = class {
|
|
|
8115
9491
|
};
|
|
8116
9492
|
|
|
8117
9493
|
// src/notifier.ts
|
|
8118
|
-
import { eq as eq20, desc as
|
|
9494
|
+
import { eq as eq20, desc as desc6, and as and9, or as or2 } from "drizzle-orm";
|
|
8119
9495
|
import crypto18 from "crypto";
|
|
8120
9496
|
var Notifier = class {
|
|
8121
9497
|
db;
|
|
@@ -8178,11 +9554,11 @@ var Notifier = class {
|
|
|
8178
9554
|
}
|
|
8179
9555
|
computeTransitions(runId, projectId) {
|
|
8180
9556
|
const recentRuns = this.db.select().from(runs).where(
|
|
8181
|
-
|
|
9557
|
+
and9(
|
|
8182
9558
|
eq20(runs.projectId, projectId),
|
|
8183
9559
|
or2(eq20(runs.status, "completed"), eq20(runs.status, "partial"))
|
|
8184
9560
|
)
|
|
8185
|
-
).orderBy(
|
|
9561
|
+
).orderBy(desc6(runs.createdAt)).limit(2).all();
|
|
8186
9562
|
if (recentRuns.length < 2) return [];
|
|
8187
9563
|
const currentRunId = recentRuns[0].id;
|
|
8188
9564
|
const previousRunId = recentRuns[1].id;
|
|
@@ -8385,6 +9761,20 @@ var DEFAULT_QUOTA = {
|
|
|
8385
9761
|
maxRequestsPerMinute: 10,
|
|
8386
9762
|
maxRequestsPerDay: 1e3
|
|
8387
9763
|
};
|
|
9764
|
+
var API_ADAPTERS = [
|
|
9765
|
+
geminiAdapter,
|
|
9766
|
+
openaiAdapter,
|
|
9767
|
+
claudeAdapter,
|
|
9768
|
+
localAdapter,
|
|
9769
|
+
perplexityAdapter
|
|
9770
|
+
];
|
|
9771
|
+
var BROWSER_ADAPTERS = [
|
|
9772
|
+
cdpChatgptAdapter
|
|
9773
|
+
];
|
|
9774
|
+
var ALL_ADAPTERS = [...API_ADAPTERS, ...BROWSER_ADAPTERS];
|
|
9775
|
+
var adapterMap = Object.fromEntries(
|
|
9776
|
+
API_ADAPTERS.map((a) => [a.name, a])
|
|
9777
|
+
);
|
|
8388
9778
|
function summarizeProviderConfig(provider, config) {
|
|
8389
9779
|
return {
|
|
8390
9780
|
configured: Boolean(config?.apiKey || config?.baseUrl),
|
|
@@ -8421,38 +9811,19 @@ async function createServer(opts) {
|
|
|
8421
9811
|
const p = providers[k];
|
|
8422
9812
|
return p?.apiKey || p?.baseUrl;
|
|
8423
9813
|
}));
|
|
8424
|
-
|
|
8425
|
-
|
|
8426
|
-
|
|
8427
|
-
|
|
8428
|
-
|
|
8429
|
-
|
|
8430
|
-
|
|
8431
|
-
|
|
8432
|
-
|
|
8433
|
-
|
|
8434
|
-
|
|
8435
|
-
|
|
8436
|
-
|
|
8437
|
-
quotaPolicy: providers.openai.quota ?? DEFAULT_QUOTA
|
|
8438
|
-
});
|
|
8439
|
-
}
|
|
8440
|
-
if (providers.claude?.apiKey) {
|
|
8441
|
-
registry.register(claudeAdapter, {
|
|
8442
|
-
provider: "claude",
|
|
8443
|
-
apiKey: providers.claude.apiKey,
|
|
8444
|
-
model: providers.claude.model,
|
|
8445
|
-
quotaPolicy: providers.claude.quota ?? DEFAULT_QUOTA
|
|
8446
|
-
});
|
|
8447
|
-
}
|
|
8448
|
-
if (providers.local?.baseUrl) {
|
|
8449
|
-
registry.register(localAdapter, {
|
|
8450
|
-
provider: "local",
|
|
8451
|
-
apiKey: providers.local.apiKey,
|
|
8452
|
-
baseUrl: providers.local.baseUrl,
|
|
8453
|
-
model: providers.local.model,
|
|
8454
|
-
quotaPolicy: providers.local.quota ?? DEFAULT_QUOTA
|
|
8455
|
-
});
|
|
9814
|
+
for (const adapter of API_ADAPTERS) {
|
|
9815
|
+
const entry = providers[adapter.name];
|
|
9816
|
+
if (!entry) continue;
|
|
9817
|
+
const isConfigured = adapter.name === "local" ? !!entry.baseUrl : !!entry.apiKey;
|
|
9818
|
+
if (isConfigured) {
|
|
9819
|
+
registry.register(adapter, {
|
|
9820
|
+
provider: adapter.name,
|
|
9821
|
+
apiKey: entry.apiKey,
|
|
9822
|
+
baseUrl: entry.baseUrl,
|
|
9823
|
+
model: entry.model,
|
|
9824
|
+
quotaPolicy: entry.quota ?? DEFAULT_QUOTA
|
|
9825
|
+
});
|
|
9826
|
+
}
|
|
8456
9827
|
}
|
|
8457
9828
|
const cdpConfig = opts.config.cdp;
|
|
8458
9829
|
if (cdpConfig?.host || cdpConfig?.port) {
|
|
@@ -8477,11 +9848,14 @@ async function createServer(opts) {
|
|
|
8477
9848
|
});
|
|
8478
9849
|
}
|
|
8479
9850
|
});
|
|
8480
|
-
const providerSummary =
|
|
8481
|
-
name,
|
|
8482
|
-
|
|
8483
|
-
|
|
8484
|
-
|
|
9851
|
+
const providerSummary = API_ADAPTERS.map((adapter) => ({
|
|
9852
|
+
name: adapter.name,
|
|
9853
|
+
displayName: adapter.displayName,
|
|
9854
|
+
keyUrl: adapter.keyUrl,
|
|
9855
|
+
modelHint: `e.g. ${adapter.modelRegistry.defaultModel}`,
|
|
9856
|
+
model: registry.get(adapter.name)?.config.model,
|
|
9857
|
+
configured: !!registry.get(adapter.name),
|
|
9858
|
+
quota: registry.get(adapter.name)?.config.quotaPolicy
|
|
8485
9859
|
}));
|
|
8486
9860
|
const googleSettingsSummary = {
|
|
8487
9861
|
configured: Boolean(opts.config.google?.clientId && opts.config.google?.clientSecret)
|
|
@@ -8521,7 +9895,6 @@ async function createServer(opts) {
|
|
|
8521
9895
|
return true;
|
|
8522
9896
|
}
|
|
8523
9897
|
};
|
|
8524
|
-
const adapterMap = { gemini: geminiAdapter, openai: openaiAdapter, claude: claudeAdapter, local: localAdapter };
|
|
8525
9898
|
const googleStateSecret = process.env.GOOGLE_STATE_SECRET ?? crypto19.randomBytes(32).toString("hex");
|
|
8526
9899
|
const googleConnectionStore = {
|
|
8527
9900
|
listConnections: (domain) => listGoogleConnections(opts.config, domain),
|
|
@@ -8585,6 +9958,13 @@ async function createServer(opts) {
|
|
|
8585
9958
|
version: PKG_VERSION
|
|
8586
9959
|
},
|
|
8587
9960
|
providerSummary,
|
|
9961
|
+
providerAdapters: [...API_ADAPTERS, ...BROWSER_ADAPTERS].map((a) => ({
|
|
9962
|
+
name: a.name,
|
|
9963
|
+
displayName: a.displayName,
|
|
9964
|
+
mode: a.mode,
|
|
9965
|
+
modelValidationPattern: a.modelRegistry.validationPattern,
|
|
9966
|
+
modelValidationHint: a.modelRegistry.validationHint
|
|
9967
|
+
})),
|
|
8588
9968
|
googleSettingsSummary,
|
|
8589
9969
|
bingSettingsSummary,
|
|
8590
9970
|
bingConnectionStore,
|
|
@@ -8595,7 +9975,7 @@ async function createServer(opts) {
|
|
|
8595
9975
|
},
|
|
8596
9976
|
onProviderUpdate: (providerName, apiKey, model, baseUrl, incomingQuota) => {
|
|
8597
9977
|
const name = providerName;
|
|
8598
|
-
if (!
|
|
9978
|
+
if (!adapterMap[name]) return null;
|
|
8599
9979
|
if (!opts.config.providers) opts.config.providers = {};
|
|
8600
9980
|
const existing = opts.config.providers[name];
|
|
8601
9981
|
const beforeConfig = summarizeProviderConfig(name, existing);
|
|
@@ -8878,14 +10258,6 @@ function parseKeywordResponse(raw, count) {
|
|
|
8878
10258
|
}
|
|
8879
10259
|
|
|
8880
10260
|
export {
|
|
8881
|
-
providerQuotaPolicySchema,
|
|
8882
|
-
resolveProviderInput,
|
|
8883
|
-
notificationEventSchema,
|
|
8884
|
-
getDefaultModel,
|
|
8885
|
-
effectiveDomains,
|
|
8886
|
-
apiKeys,
|
|
8887
|
-
createClient,
|
|
8888
|
-
migrate,
|
|
8889
10261
|
getConfigDir,
|
|
8890
10262
|
getConfigPath,
|
|
8891
10263
|
loadConfig,
|
|
@@ -8896,6 +10268,13 @@ export {
|
|
|
8896
10268
|
isFirstRun,
|
|
8897
10269
|
showFirstRunNotice,
|
|
8898
10270
|
trackEvent,
|
|
10271
|
+
providerQuotaPolicySchema,
|
|
10272
|
+
resolveProviderInput,
|
|
10273
|
+
notificationEventSchema,
|
|
10274
|
+
effectiveDomains,
|
|
8899
10275
|
setGoogleAuthConfig,
|
|
10276
|
+
apiKeys,
|
|
10277
|
+
createClient,
|
|
10278
|
+
migrate,
|
|
8900
10279
|
createServer
|
|
8901
10280
|
};
|