@ainyc/canonry 1.19.2 → 1.19.4
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/assets/assets/index-DpXYVcNu.css +1 -0
- package/assets/assets/{index-BIXLW5Li.js → index-GGgJSHS2.js} +98 -98
- package/assets/index.html +2 -2
- package/dist/{chunk-GX673NKZ.js → chunk-PQLJ22YW.js} +990 -955
- package/dist/cli.js +1 -1
- package/dist/index.js +1 -1
- package/package.json +5 -5
- package/assets/assets/index-DdQKI2_i.css +0 -1
|
@@ -166,981 +166,981 @@ import path6 from "path";
|
|
|
166
166
|
import { fileURLToPath } from "url";
|
|
167
167
|
import Fastify from "fastify";
|
|
168
168
|
|
|
169
|
-
// ../
|
|
170
|
-
import
|
|
171
|
-
import { eq } from "drizzle-orm";
|
|
172
|
-
|
|
173
|
-
// ../db/src/client.ts
|
|
174
|
-
import { mkdirSync } from "fs";
|
|
175
|
-
import { dirname } from "path";
|
|
176
|
-
import Database from "better-sqlite3";
|
|
177
|
-
import { drizzle } from "drizzle-orm/better-sqlite3";
|
|
169
|
+
// ../contracts/src/config-schema.ts
|
|
170
|
+
import { z as z3 } from "zod";
|
|
178
171
|
|
|
179
|
-
// ../
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
googleConnections: () => googleConnections,
|
|
186
|
-
gscCoverageSnapshots: () => gscCoverageSnapshots,
|
|
187
|
-
gscSearchData: () => gscSearchData,
|
|
188
|
-
gscUrlInspections: () => gscUrlInspections,
|
|
189
|
-
keywords: () => keywords,
|
|
190
|
-
notifications: () => notifications,
|
|
191
|
-
projects: () => projects,
|
|
192
|
-
querySnapshots: () => querySnapshots,
|
|
193
|
-
runs: () => runs,
|
|
194
|
-
schedules: () => schedules,
|
|
195
|
-
usageCounters: () => usageCounters
|
|
172
|
+
// ../contracts/src/provider.ts
|
|
173
|
+
import { z } from "zod";
|
|
174
|
+
var providerQuotaPolicySchema = z.object({
|
|
175
|
+
maxConcurrency: z.number().int().positive(),
|
|
176
|
+
maxRequestsPerMinute: z.number().int().positive(),
|
|
177
|
+
maxRequestsPerDay: z.number().int().positive()
|
|
196
178
|
});
|
|
197
|
-
|
|
198
|
-
var
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
179
|
+
var PROVIDER_NAMES = ["gemini", "openai", "claude", "local", "cdp:chatgpt"];
|
|
180
|
+
var providerNameSchema = z.enum(PROVIDER_NAMES);
|
|
181
|
+
var PROVIDER_MODE = {
|
|
182
|
+
gemini: "api",
|
|
183
|
+
openai: "api",
|
|
184
|
+
claude: "api",
|
|
185
|
+
local: "api",
|
|
186
|
+
"cdp:chatgpt": "browser"
|
|
187
|
+
};
|
|
188
|
+
function isBrowserProvider(name) {
|
|
189
|
+
return PROVIDER_MODE[name] === "browser";
|
|
190
|
+
}
|
|
191
|
+
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
|
+
function resolveProviderInput(input) {
|
|
197
|
+
const lower = input.trim().toLowerCase();
|
|
198
|
+
if (lower === "cdp") {
|
|
199
|
+
return [...CDP_TARGETS];
|
|
200
|
+
}
|
|
201
|
+
const parsed = parseProviderName(lower);
|
|
202
|
+
return parsed ? [parsed] : [];
|
|
203
|
+
}
|
|
204
|
+
var locationContextSchema = z.object({
|
|
205
|
+
label: z.string().min(1),
|
|
206
|
+
city: z.string().min(1),
|
|
207
|
+
region: z.string().min(1),
|
|
208
|
+
country: z.string().length(2),
|
|
209
|
+
timezone: z.string().optional()
|
|
215
210
|
});
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
]);
|
|
225
|
-
var competitors = sqliteTable("competitors", {
|
|
226
|
-
id: text("id").primaryKey(),
|
|
227
|
-
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
228
|
-
domain: text("domain").notNull(),
|
|
229
|
-
createdAt: text("created_at").notNull()
|
|
230
|
-
}, (table) => [
|
|
231
|
-
index("idx_competitors_project").on(table.projectId),
|
|
232
|
-
uniqueIndex("idx_competitors_project_domain").on(table.projectId, table.domain)
|
|
233
|
-
]);
|
|
234
|
-
var runs = sqliteTable("runs", {
|
|
235
|
-
id: text("id").primaryKey(),
|
|
236
|
-
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
237
|
-
kind: text("kind").notNull().default("answer-visibility"),
|
|
238
|
-
status: text("status").notNull().default("queued"),
|
|
239
|
-
trigger: text("trigger").notNull().default("manual"),
|
|
240
|
-
location: text("location"),
|
|
241
|
-
startedAt: text("started_at"),
|
|
242
|
-
finishedAt: text("finished_at"),
|
|
243
|
-
error: text("error"),
|
|
244
|
-
createdAt: text("created_at").notNull()
|
|
245
|
-
}, (table) => [
|
|
246
|
-
index("idx_runs_project").on(table.projectId),
|
|
247
|
-
index("idx_runs_status").on(table.status)
|
|
248
|
-
]);
|
|
249
|
-
var querySnapshots = sqliteTable("query_snapshots", {
|
|
250
|
-
id: text("id").primaryKey(),
|
|
251
|
-
runId: text("run_id").notNull().references(() => runs.id, { onDelete: "cascade" }),
|
|
252
|
-
keywordId: text("keyword_id").notNull().references(() => keywords.id, { onDelete: "cascade" }),
|
|
253
|
-
provider: text("provider").notNull().default("gemini"),
|
|
254
|
-
model: text("model"),
|
|
255
|
-
citationState: text("citation_state").notNull(),
|
|
256
|
-
answerText: text("answer_text"),
|
|
257
|
-
citedDomains: text("cited_domains").notNull().default("[]"),
|
|
258
|
-
competitorOverlap: text("competitor_overlap").notNull().default("[]"),
|
|
259
|
-
location: text("location"),
|
|
260
|
-
screenshotPath: text("screenshot_path"),
|
|
261
|
-
rawResponse: text("raw_response"),
|
|
262
|
-
createdAt: text("created_at").notNull()
|
|
263
|
-
}, (table) => [
|
|
264
|
-
index("idx_snapshots_run").on(table.runId),
|
|
265
|
-
index("idx_snapshots_keyword").on(table.keywordId)
|
|
266
|
-
]);
|
|
267
|
-
var auditLog = sqliteTable("audit_log", {
|
|
268
|
-
id: text("id").primaryKey(),
|
|
269
|
-
projectId: text("project_id").references(() => projects.id, { onDelete: "cascade" }),
|
|
270
|
-
actor: text("actor").notNull(),
|
|
271
|
-
action: text("action").notNull(),
|
|
272
|
-
entityType: text("entity_type").notNull(),
|
|
273
|
-
entityId: text("entity_id"),
|
|
274
|
-
diff: text("diff"),
|
|
275
|
-
createdAt: text("created_at").notNull()
|
|
276
|
-
}, (table) => [
|
|
277
|
-
index("idx_audit_log_project").on(table.projectId),
|
|
278
|
-
index("idx_audit_log_created").on(table.createdAt)
|
|
279
|
-
]);
|
|
280
|
-
var apiKeys = sqliteTable("api_keys", {
|
|
281
|
-
id: text("id").primaryKey(),
|
|
282
|
-
name: text("name").notNull(),
|
|
283
|
-
keyHash: text("key_hash").notNull().unique(),
|
|
284
|
-
keyPrefix: text("key_prefix").notNull(),
|
|
285
|
-
scopes: text("scopes").notNull().default('["*"]'),
|
|
286
|
-
createdAt: text("created_at").notNull(),
|
|
287
|
-
lastUsedAt: text("last_used_at"),
|
|
288
|
-
revokedAt: text("revoked_at")
|
|
289
|
-
}, (table) => [
|
|
290
|
-
index("idx_api_keys_prefix").on(table.keyPrefix)
|
|
291
|
-
]);
|
|
292
|
-
var schedules = sqliteTable("schedules", {
|
|
293
|
-
id: text("id").primaryKey(),
|
|
294
|
-
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
295
|
-
cronExpr: text("cron_expr").notNull(),
|
|
296
|
-
preset: text("preset"),
|
|
297
|
-
timezone: text("timezone").notNull().default("UTC"),
|
|
298
|
-
enabled: integer("enabled").notNull().default(1),
|
|
299
|
-
providers: text("providers").notNull().default("[]"),
|
|
300
|
-
lastRunAt: text("last_run_at"),
|
|
301
|
-
nextRunAt: text("next_run_at"),
|
|
302
|
-
createdAt: text("created_at").notNull(),
|
|
303
|
-
updatedAt: text("updated_at").notNull()
|
|
304
|
-
}, (table) => [
|
|
305
|
-
uniqueIndex("idx_schedules_project").on(table.projectId)
|
|
306
|
-
]);
|
|
307
|
-
var notifications = sqliteTable("notifications", {
|
|
308
|
-
id: text("id").primaryKey(),
|
|
309
|
-
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
310
|
-
channel: text("channel").notNull(),
|
|
311
|
-
config: text("config").notNull(),
|
|
312
|
-
webhookSecret: text("webhook_secret"),
|
|
313
|
-
enabled: integer("enabled").notNull().default(1),
|
|
314
|
-
createdAt: text("created_at").notNull(),
|
|
315
|
-
updatedAt: text("updated_at").notNull()
|
|
316
|
-
}, (table) => [
|
|
317
|
-
index("idx_notifications_project").on(table.projectId)
|
|
318
|
-
]);
|
|
319
|
-
var googleConnections = sqliteTable("google_connections", {
|
|
320
|
-
id: text("id").primaryKey(),
|
|
321
|
-
domain: text("domain").notNull(),
|
|
322
|
-
connectionType: text("connection_type").notNull(),
|
|
323
|
-
propertyId: text("property_id"),
|
|
324
|
-
sitemapUrl: text("sitemap_url"),
|
|
325
|
-
accessToken: text("access_token"),
|
|
326
|
-
refreshToken: text("refresh_token"),
|
|
327
|
-
tokenExpiresAt: text("token_expires_at"),
|
|
328
|
-
scopes: text("scopes").notNull().default("[]"),
|
|
329
|
-
createdAt: text("created_at").notNull(),
|
|
330
|
-
updatedAt: text("updated_at").notNull()
|
|
331
|
-
}, (table) => [
|
|
332
|
-
uniqueIndex("idx_google_conn_domain_type").on(table.domain, table.connectionType)
|
|
333
|
-
]);
|
|
334
|
-
var gscSearchData = sqliteTable("gsc_search_data", {
|
|
335
|
-
id: text("id").primaryKey(),
|
|
336
|
-
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
337
|
-
syncRunId: text("sync_run_id").notNull().references(() => runs.id, { onDelete: "cascade" }),
|
|
338
|
-
date: text("date").notNull(),
|
|
339
|
-
query: text("query").notNull(),
|
|
340
|
-
page: text("page").notNull(),
|
|
341
|
-
country: text("country"),
|
|
342
|
-
device: text("device"),
|
|
343
|
-
clicks: integer("clicks").notNull().default(0),
|
|
344
|
-
impressions: integer("impressions").notNull().default(0),
|
|
345
|
-
ctr: text("ctr").notNull().default("0"),
|
|
346
|
-
position: text("position").notNull().default("0"),
|
|
347
|
-
createdAt: text("created_at").notNull()
|
|
348
|
-
}, (table) => [
|
|
349
|
-
index("idx_gsc_search_project_date").on(table.projectId, table.date),
|
|
350
|
-
index("idx_gsc_search_query").on(table.query),
|
|
351
|
-
index("idx_gsc_search_run").on(table.syncRunId)
|
|
352
|
-
]);
|
|
353
|
-
var gscUrlInspections = sqliteTable("gsc_url_inspections", {
|
|
354
|
-
id: text("id").primaryKey(),
|
|
355
|
-
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
356
|
-
syncRunId: text("sync_run_id").references(() => runs.id, { onDelete: "cascade" }),
|
|
357
|
-
url: text("url").notNull(),
|
|
358
|
-
indexingState: text("indexing_state"),
|
|
359
|
-
verdict: text("verdict"),
|
|
360
|
-
coverageState: text("coverage_state"),
|
|
361
|
-
pageFetchState: text("page_fetch_state"),
|
|
362
|
-
robotsTxtState: text("robots_txt_state"),
|
|
363
|
-
crawlTime: text("crawl_time"),
|
|
364
|
-
lastCrawlResult: text("last_crawl_result"),
|
|
365
|
-
isMobileFriendly: integer("is_mobile_friendly"),
|
|
366
|
-
richResults: text("rich_results").notNull().default("[]"),
|
|
367
|
-
referringUrls: text("referring_urls").notNull().default("[]"),
|
|
368
|
-
inspectedAt: text("inspected_at").notNull(),
|
|
369
|
-
createdAt: text("created_at").notNull()
|
|
370
|
-
}, (table) => [
|
|
371
|
-
index("idx_gsc_inspect_project_url").on(table.projectId, table.url),
|
|
372
|
-
index("idx_gsc_inspect_run").on(table.syncRunId),
|
|
373
|
-
index("idx_gsc_inspect_url_time").on(table.url, table.inspectedAt)
|
|
211
|
+
|
|
212
|
+
// ../contracts/src/notification.ts
|
|
213
|
+
import { z as z2 } from "zod";
|
|
214
|
+
var notificationEventSchema = z2.enum([
|
|
215
|
+
"citation.lost",
|
|
216
|
+
"citation.gained",
|
|
217
|
+
"run.completed",
|
|
218
|
+
"run.failed"
|
|
374
219
|
]);
|
|
375
|
-
var
|
|
376
|
-
id:
|
|
377
|
-
projectId:
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
createdAt:
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
index("idx_gsc_coverage_snap_run").on(table.syncRunId)
|
|
387
|
-
]);
|
|
388
|
-
var usageCounters = sqliteTable("usage_counters", {
|
|
389
|
-
id: text("id").primaryKey(),
|
|
390
|
-
scope: text("scope").notNull(),
|
|
391
|
-
period: text("period").notNull(),
|
|
392
|
-
metric: text("metric").notNull(),
|
|
393
|
-
count: integer("count").notNull().default(0),
|
|
394
|
-
updatedAt: text("updated_at").notNull()
|
|
395
|
-
}, (table) => [
|
|
396
|
-
uniqueIndex("idx_usage_scope_period_metric").on(table.scope, table.period, table.metric),
|
|
397
|
-
index("idx_usage_scope_period").on(table.scope, table.period)
|
|
398
|
-
]);
|
|
399
|
-
|
|
400
|
-
// ../db/src/client.ts
|
|
401
|
-
function createClient(databasePath) {
|
|
402
|
-
mkdirSync(dirname(databasePath), { recursive: true });
|
|
403
|
-
const sqlite = new Database(databasePath);
|
|
404
|
-
sqlite.pragma("journal_mode = WAL");
|
|
405
|
-
sqlite.pragma("foreign_keys = ON");
|
|
406
|
-
return drizzle(sqlite, { schema: schema_exports });
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
// ../db/src/migrate.ts
|
|
410
|
-
import { sql } from "drizzle-orm";
|
|
411
|
-
var MIGRATION_SQL = `
|
|
412
|
-
CREATE TABLE IF NOT EXISTS projects (
|
|
413
|
-
id TEXT PRIMARY KEY,
|
|
414
|
-
name TEXT NOT NULL UNIQUE,
|
|
415
|
-
display_name TEXT NOT NULL,
|
|
416
|
-
canonical_domain TEXT NOT NULL,
|
|
417
|
-
owned_domains TEXT NOT NULL DEFAULT '[]',
|
|
418
|
-
country TEXT NOT NULL,
|
|
419
|
-
language TEXT NOT NULL,
|
|
420
|
-
tags TEXT NOT NULL DEFAULT '[]',
|
|
421
|
-
labels TEXT NOT NULL DEFAULT '{}',
|
|
422
|
-
providers TEXT NOT NULL DEFAULT '[]',
|
|
423
|
-
config_source TEXT NOT NULL DEFAULT 'cli',
|
|
424
|
-
config_revision INTEGER NOT NULL DEFAULT 1,
|
|
425
|
-
created_at TEXT NOT NULL,
|
|
426
|
-
updated_at TEXT NOT NULL
|
|
427
|
-
);
|
|
428
|
-
|
|
429
|
-
CREATE TABLE IF NOT EXISTS keywords (
|
|
430
|
-
id TEXT PRIMARY KEY,
|
|
431
|
-
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
432
|
-
keyword TEXT NOT NULL,
|
|
433
|
-
created_at TEXT NOT NULL,
|
|
434
|
-
UNIQUE(project_id, keyword)
|
|
435
|
-
);
|
|
436
|
-
|
|
437
|
-
CREATE TABLE IF NOT EXISTS competitors (
|
|
438
|
-
id TEXT PRIMARY KEY,
|
|
439
|
-
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
440
|
-
domain TEXT NOT NULL,
|
|
441
|
-
created_at TEXT NOT NULL,
|
|
442
|
-
UNIQUE(project_id, domain)
|
|
443
|
-
);
|
|
220
|
+
var notificationDtoSchema = z2.object({
|
|
221
|
+
id: z2.string(),
|
|
222
|
+
projectId: z2.string(),
|
|
223
|
+
channel: z2.literal("webhook"),
|
|
224
|
+
url: z2.string().url(),
|
|
225
|
+
events: z2.array(notificationEventSchema),
|
|
226
|
+
enabled: z2.boolean().default(true),
|
|
227
|
+
webhookSecret: z2.string().optional(),
|
|
228
|
+
createdAt: z2.string(),
|
|
229
|
+
updatedAt: z2.string()
|
|
230
|
+
});
|
|
444
231
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
)
|
|
232
|
+
// ../contracts/src/config-schema.ts
|
|
233
|
+
var configMetadataSchema = z3.object({
|
|
234
|
+
name: z3.string().min(1).max(63).regex(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/, {
|
|
235
|
+
message: "Name must be a lowercase slug (letters, numbers, hyphens)"
|
|
236
|
+
}),
|
|
237
|
+
labels: z3.record(z3.string(), z3.string()).optional().default({})
|
|
238
|
+
});
|
|
239
|
+
var configScheduleSchema = z3.object({
|
|
240
|
+
preset: z3.string().optional(),
|
|
241
|
+
cron: z3.string().optional(),
|
|
242
|
+
timezone: z3.string().optional().default("UTC"),
|
|
243
|
+
providers: z3.array(providerNameSchema).optional().default([])
|
|
244
|
+
}).refine(
|
|
245
|
+
(data) => data.preset && !data.cron || !data.preset && data.cron,
|
|
246
|
+
{ message: 'Exactly one of "preset" or "cron" must be provided' }
|
|
247
|
+
).optional();
|
|
248
|
+
var configNotificationSchema = z3.object({
|
|
249
|
+
channel: z3.literal("webhook"),
|
|
250
|
+
url: z3.string().url(),
|
|
251
|
+
events: z3.array(notificationEventSchema).min(1)
|
|
252
|
+
});
|
|
253
|
+
var configGoogleSchema = z3.object({
|
|
254
|
+
gsc: z3.object({
|
|
255
|
+
propertyUrl: z3.string()
|
|
256
|
+
}).optional(),
|
|
257
|
+
syncSchedule: z3.object({
|
|
258
|
+
preset: z3.string().optional(),
|
|
259
|
+
cron: z3.string().optional()
|
|
260
|
+
}).optional()
|
|
261
|
+
}).optional();
|
|
262
|
+
var configSpecSchema = z3.object({
|
|
263
|
+
displayName: z3.string().min(1),
|
|
264
|
+
canonicalDomain: z3.string().min(1),
|
|
265
|
+
ownedDomains: z3.array(z3.string().min(1)).optional().default([]),
|
|
266
|
+
country: z3.string().length(2),
|
|
267
|
+
language: z3.string().min(2),
|
|
268
|
+
keywords: z3.array(z3.string().min(1)).optional().default([]),
|
|
269
|
+
competitors: z3.array(z3.string().min(1)).optional().default([]),
|
|
270
|
+
providers: z3.array(providerNameSchema).optional().default([]),
|
|
271
|
+
locations: z3.array(locationContextSchema).optional().default([]),
|
|
272
|
+
defaultLocation: z3.string().optional(),
|
|
273
|
+
schedule: configScheduleSchema,
|
|
274
|
+
notifications: z3.array(configNotificationSchema).optional().default([]),
|
|
275
|
+
google: configGoogleSchema
|
|
276
|
+
});
|
|
277
|
+
var projectConfigSchema = z3.object({
|
|
278
|
+
apiVersion: z3.literal("canonry/v1"),
|
|
279
|
+
kind: z3.literal("Project"),
|
|
280
|
+
metadata: configMetadataSchema,
|
|
281
|
+
spec: configSpecSchema
|
|
282
|
+
});
|
|
456
283
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
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
|
+
}
|
|
469
343
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
344
|
+
// ../contracts/src/errors.ts
|
|
345
|
+
var AppError = class extends Error {
|
|
346
|
+
code;
|
|
347
|
+
statusCode;
|
|
348
|
+
details;
|
|
349
|
+
constructor(code, message, statusCode, details) {
|
|
350
|
+
super(message);
|
|
351
|
+
this.name = "AppError";
|
|
352
|
+
this.code = code;
|
|
353
|
+
this.statusCode = statusCode;
|
|
354
|
+
this.details = details;
|
|
355
|
+
}
|
|
356
|
+
toJSON() {
|
|
357
|
+
return {
|
|
358
|
+
error: {
|
|
359
|
+
code: this.code,
|
|
360
|
+
message: this.message,
|
|
361
|
+
...this.details ? { details: this.details } : {}
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
function notFound(entity, id) {
|
|
367
|
+
return new AppError("NOT_FOUND", `${entity} '${id}' not found`, 404);
|
|
368
|
+
}
|
|
369
|
+
function validationError(message, details) {
|
|
370
|
+
return new AppError("VALIDATION_ERROR", message, 400, details);
|
|
371
|
+
}
|
|
372
|
+
function authRequired() {
|
|
373
|
+
return new AppError("AUTH_REQUIRED", "Authentication required", 401);
|
|
374
|
+
}
|
|
375
|
+
function authInvalid() {
|
|
376
|
+
return new AppError("AUTH_INVALID", "Invalid API key", 401);
|
|
377
|
+
}
|
|
378
|
+
function runInProgress(projectName) {
|
|
379
|
+
return new AppError("RUN_IN_PROGRESS", `A run is already in progress for '${projectName}'`, 409);
|
|
380
|
+
}
|
|
381
|
+
function unsupportedKind(kind) {
|
|
382
|
+
return new AppError("UNSUPPORTED_KIND", `Kind '${kind}' is not supported in this version`, 400);
|
|
383
|
+
}
|
|
480
384
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
)
|
|
385
|
+
// ../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
|
+
});
|
|
491
467
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
)
|
|
468
|
+
// ../contracts/src/project.ts
|
|
469
|
+
import { z as z5 } from "zod";
|
|
470
|
+
var configSourceSchema = z5.enum(["cli", "api", "config-file"]);
|
|
471
|
+
var projectDtoSchema = z5.object({
|
|
472
|
+
id: z5.string(),
|
|
473
|
+
name: z5.string(),
|
|
474
|
+
displayName: z5.string().optional(),
|
|
475
|
+
canonicalDomain: z5.string(),
|
|
476
|
+
ownedDomains: z5.array(z5.string()).default([]),
|
|
477
|
+
country: z5.string().length(2),
|
|
478
|
+
language: z5.string().min(2),
|
|
479
|
+
tags: z5.array(z5.string()).default([]),
|
|
480
|
+
labels: z5.record(z5.string(), z5.string()).default({}),
|
|
481
|
+
locations: z5.array(locationContextSchema).default([]),
|
|
482
|
+
defaultLocation: z5.string().nullable().optional(),
|
|
483
|
+
configSource: configSourceSchema.default("cli"),
|
|
484
|
+
configRevision: z5.number().int().positive().default(1),
|
|
485
|
+
createdAt: z5.string().optional(),
|
|
486
|
+
updatedAt: z5.string().optional()
|
|
487
|
+
});
|
|
488
|
+
function normalizeProjectDomain(input) {
|
|
489
|
+
let domain = input.trim().toLowerCase();
|
|
490
|
+
try {
|
|
491
|
+
if (domain.includes("://")) {
|
|
492
|
+
domain = new URL(domain).hostname.toLowerCase();
|
|
493
|
+
}
|
|
494
|
+
} catch {
|
|
495
|
+
}
|
|
496
|
+
return domain.replace(/^www\./, "");
|
|
497
|
+
}
|
|
498
|
+
function effectiveDomains(project) {
|
|
499
|
+
const all = [project.canonicalDomain, ...project.ownedDomains ?? []];
|
|
500
|
+
const seen = /* @__PURE__ */ new Set();
|
|
501
|
+
const result = [];
|
|
502
|
+
for (const d of all) {
|
|
503
|
+
const trimmed = d.trim();
|
|
504
|
+
if (!trimmed) continue;
|
|
505
|
+
const norm = normalizeProjectDomain(trimmed);
|
|
506
|
+
if (seen.has(norm)) continue;
|
|
507
|
+
seen.add(norm);
|
|
508
|
+
result.push(trimmed);
|
|
509
|
+
}
|
|
510
|
+
return result;
|
|
511
|
+
}
|
|
501
512
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
)
|
|
513
|
+
// ../contracts/src/run.ts
|
|
514
|
+
import { z as z6 } from "zod";
|
|
515
|
+
var runStatusSchema = z6.enum(["queued", "running", "completed", "partial", "failed"]);
|
|
516
|
+
var runKindSchema = z6.enum(["answer-visibility", "site-audit", "gsc-sync", "inspect-sitemap"]);
|
|
517
|
+
var runTriggerSchema = z6.enum(["manual", "scheduled", "config-apply"]);
|
|
518
|
+
var citationStateSchema = z6.enum(["cited", "not-cited"]);
|
|
519
|
+
var computedTransitionSchema = z6.enum(["new", "cited", "lost", "emerging", "not-cited"]);
|
|
520
|
+
var runDtoSchema = z6.object({
|
|
521
|
+
id: z6.string(),
|
|
522
|
+
projectId: z6.string(),
|
|
523
|
+
kind: runKindSchema,
|
|
524
|
+
status: runStatusSchema,
|
|
525
|
+
trigger: runTriggerSchema.default("manual"),
|
|
526
|
+
location: z6.string().nullable().optional(),
|
|
527
|
+
startedAt: z6.string().nullable().optional(),
|
|
528
|
+
finishedAt: z6.string().nullable().optional(),
|
|
529
|
+
error: z6.string().nullable().optional(),
|
|
530
|
+
createdAt: z6.string()
|
|
531
|
+
});
|
|
532
|
+
var groundingSourceSchema = z6.object({
|
|
533
|
+
uri: z6.string(),
|
|
534
|
+
title: z6.string()
|
|
535
|
+
});
|
|
536
|
+
var querySnapshotDtoSchema = z6.object({
|
|
537
|
+
id: z6.string(),
|
|
538
|
+
runId: z6.string(),
|
|
539
|
+
keywordId: z6.string(),
|
|
540
|
+
keyword: z6.string().optional(),
|
|
541
|
+
provider: providerNameSchema,
|
|
542
|
+
citationState: citationStateSchema,
|
|
543
|
+
transition: computedTransitionSchema.optional(),
|
|
544
|
+
answerText: z6.string().nullable().optional(),
|
|
545
|
+
citedDomains: z6.array(z6.string()).default([]),
|
|
546
|
+
competitorOverlap: z6.array(z6.string()).default([]),
|
|
547
|
+
groundingSources: z6.array(groundingSourceSchema).default([]),
|
|
548
|
+
searchQueries: z6.array(z6.string()).default([]),
|
|
549
|
+
model: z6.string().nullable().optional(),
|
|
550
|
+
location: z6.string().nullable().optional(),
|
|
551
|
+
createdAt: z6.string()
|
|
552
|
+
});
|
|
553
|
+
var auditLogEntrySchema = z6.object({
|
|
554
|
+
id: z6.string(),
|
|
555
|
+
projectId: z6.string().nullable().optional(),
|
|
556
|
+
actor: z6.string(),
|
|
557
|
+
action: z6.string(),
|
|
558
|
+
entityType: z6.string(),
|
|
559
|
+
entityId: z6.string().nullable().optional(),
|
|
560
|
+
diff: z6.unknown().optional(),
|
|
561
|
+
createdAt: z6.string()
|
|
562
|
+
});
|
|
524
563
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
)
|
|
564
|
+
// ../contracts/src/schedule.ts
|
|
565
|
+
import { z as z7 } from "zod";
|
|
566
|
+
var scheduleDtoSchema = z7.object({
|
|
567
|
+
id: z7.string(),
|
|
568
|
+
projectId: z7.string(),
|
|
569
|
+
cronExpr: z7.string(),
|
|
570
|
+
preset: z7.string().nullable().optional(),
|
|
571
|
+
timezone: z7.string().default("UTC"),
|
|
572
|
+
enabled: z7.boolean().default(true),
|
|
573
|
+
providers: z7.array(providerNameSchema).default([]),
|
|
574
|
+
lastRunAt: z7.string().nullable().optional(),
|
|
575
|
+
nextRunAt: z7.string().nullable().optional(),
|
|
576
|
+
createdAt: z7.string(),
|
|
577
|
+
updatedAt: z7.string()
|
|
578
|
+
});
|
|
534
579
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
//
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
//
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
crawl_time TEXT,
|
|
596
|
-
last_crawl_result TEXT,
|
|
597
|
-
is_mobile_friendly INTEGER,
|
|
598
|
-
rich_results TEXT NOT NULL DEFAULT '[]',
|
|
599
|
-
referring_urls TEXT NOT NULL DEFAULT '[]',
|
|
600
|
-
inspected_at TEXT NOT NULL,
|
|
601
|
-
created_at TEXT NOT NULL
|
|
602
|
-
)`,
|
|
603
|
-
`CREATE INDEX IF NOT EXISTS idx_gsc_inspect_project_url ON gsc_url_inspections(project_id, url)`,
|
|
604
|
-
`CREATE INDEX IF NOT EXISTS idx_gsc_inspect_run ON gsc_url_inspections(sync_run_id)`,
|
|
605
|
-
`CREATE INDEX IF NOT EXISTS idx_gsc_inspect_url_time ON gsc_url_inspections(url, inspected_at)`,
|
|
606
|
-
// v7: GSC coverage snapshots for historical tracking
|
|
607
|
-
`CREATE TABLE IF NOT EXISTS gsc_coverage_snapshots (
|
|
608
|
-
id TEXT PRIMARY KEY,
|
|
609
|
-
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
610
|
-
sync_run_id TEXT REFERENCES runs(id) ON DELETE CASCADE,
|
|
611
|
-
date TEXT NOT NULL,
|
|
612
|
-
indexed INTEGER NOT NULL DEFAULT 0,
|
|
613
|
-
not_indexed INTEGER NOT NULL DEFAULT 0,
|
|
614
|
-
reason_breakdown TEXT NOT NULL DEFAULT '{}',
|
|
615
|
-
created_at TEXT NOT NULL
|
|
616
|
-
)`,
|
|
617
|
-
`CREATE INDEX IF NOT EXISTS idx_gsc_coverage_snap_project_date ON gsc_coverage_snapshots(project_id, date)`,
|
|
618
|
-
`CREATE INDEX IF NOT EXISTS idx_gsc_coverage_snap_run ON gsc_coverage_snapshots(sync_run_id)`,
|
|
619
|
-
// v8: Location-aware sweeps — project locations + snapshot location tag
|
|
620
|
-
`ALTER TABLE projects ADD COLUMN locations TEXT NOT NULL DEFAULT '[]'`,
|
|
621
|
-
`ALTER TABLE projects ADD COLUMN default_location TEXT`,
|
|
622
|
-
`ALTER TABLE query_snapshots ADD COLUMN location TEXT`,
|
|
623
|
-
// v9: Add location column to runs for per-location run tracking
|
|
624
|
-
`ALTER TABLE runs ADD COLUMN location TEXT`,
|
|
625
|
-
// v10: Add sitemapUrl to google_connections for persistent sitemap storage
|
|
626
|
-
`ALTER TABLE google_connections ADD COLUMN sitemap_url TEXT`,
|
|
627
|
-
// v11: CDP browser provider — screenshot path for captured evidence
|
|
628
|
-
`ALTER TABLE query_snapshots ADD COLUMN screenshot_path TEXT`
|
|
580
|
+
// ../contracts/src/source-categories.ts
|
|
581
|
+
var SOURCE_CATEGORY_RULES = [
|
|
582
|
+
// Forums
|
|
583
|
+
{ pattern: "reddit.com", category: "forum", label: "Reddit" },
|
|
584
|
+
{ pattern: "quora.com", category: "forum", label: "Quora" },
|
|
585
|
+
{ pattern: "stackexchange.com", category: "forum", label: "Stack Exchange" },
|
|
586
|
+
{ pattern: "stackoverflow.com", category: "forum", label: "Stack Overflow" },
|
|
587
|
+
{ pattern: "discourse.org", category: "forum", label: "Discourse" },
|
|
588
|
+
// Social
|
|
589
|
+
{ pattern: "linkedin.com", category: "social", label: "LinkedIn" },
|
|
590
|
+
{ pattern: "twitter.com", category: "social", label: "X (Twitter)" },
|
|
591
|
+
{ pattern: "x.com", category: "social", label: "X (Twitter)" },
|
|
592
|
+
{ pattern: "facebook.com", category: "social", label: "Facebook" },
|
|
593
|
+
{ pattern: "instagram.com", category: "social", label: "Instagram" },
|
|
594
|
+
{ pattern: "threads.net", category: "social", label: "Threads" },
|
|
595
|
+
{ pattern: "pinterest.com", category: "social", label: "Pinterest" },
|
|
596
|
+
{ pattern: "tiktok.com", category: "social", label: "TikTok" },
|
|
597
|
+
// Video
|
|
598
|
+
{ pattern: "youtube.com", category: "video", label: "YouTube" },
|
|
599
|
+
{ pattern: "youtu.be", category: "video", label: "YouTube" },
|
|
600
|
+
{ pattern: "vimeo.com", category: "video", label: "Vimeo" },
|
|
601
|
+
// News
|
|
602
|
+
{ pattern: "nytimes.com", category: "news", label: "NY Times" },
|
|
603
|
+
{ pattern: "bbc.com", category: "news", label: "BBC" },
|
|
604
|
+
{ pattern: "bbc.co.uk", category: "news", label: "BBC" },
|
|
605
|
+
{ pattern: "cnn.com", category: "news", label: "CNN" },
|
|
606
|
+
{ pattern: "reuters.com", category: "news", label: "Reuters" },
|
|
607
|
+
{ pattern: "apnews.com", category: "news", label: "AP News" },
|
|
608
|
+
{ pattern: "theguardian.com", category: "news", label: "The Guardian" },
|
|
609
|
+
{ pattern: "washingtonpost.com", category: "news", label: "Washington Post" },
|
|
610
|
+
{ pattern: "wsj.com", category: "news", label: "WSJ" },
|
|
611
|
+
{ pattern: "forbes.com", category: "news", label: "Forbes" },
|
|
612
|
+
{ pattern: "techcrunch.com", category: "news", label: "TechCrunch" },
|
|
613
|
+
{ pattern: "theverge.com", category: "news", label: "The Verge" },
|
|
614
|
+
{ pattern: "wired.com", category: "news", label: "Wired" },
|
|
615
|
+
{ pattern: "arstechnica.com", category: "news", label: "Ars Technica" },
|
|
616
|
+
// Reference
|
|
617
|
+
{ pattern: "wikipedia.org", category: "reference", label: "Wikipedia" },
|
|
618
|
+
{ pattern: "wikimedia.org", category: "reference", label: "Wikimedia" },
|
|
619
|
+
{ pattern: "britannica.com", category: "reference", label: "Britannica" },
|
|
620
|
+
{ pattern: "merriam-webster.com", category: "reference", label: "Merriam-Webster" },
|
|
621
|
+
// Blog / Content platforms
|
|
622
|
+
{ pattern: "medium.com", category: "blog", label: "Medium" },
|
|
623
|
+
{ pattern: "substack.com", category: "blog", label: "Substack" },
|
|
624
|
+
{ pattern: "dev.to", category: "blog", label: "DEV Community" },
|
|
625
|
+
{ pattern: "hashnode.dev", category: "blog", label: "Hashnode" },
|
|
626
|
+
{ pattern: "wordpress.com", category: "blog", label: "WordPress" },
|
|
627
|
+
{ pattern: "blogger.com", category: "blog", label: "Blogger" },
|
|
628
|
+
{ pattern: "hubspot.com", category: "blog", label: "HubSpot" },
|
|
629
|
+
// E-commerce
|
|
630
|
+
{ pattern: "amazon.com", category: "ecommerce", label: "Amazon" },
|
|
631
|
+
{ pattern: "amazon.co.uk", category: "ecommerce", label: "Amazon UK" },
|
|
632
|
+
{ pattern: "shopify.com", category: "ecommerce", label: "Shopify" },
|
|
633
|
+
{ pattern: "ebay.com", category: "ecommerce", label: "eBay" },
|
|
634
|
+
// Academic
|
|
635
|
+
{ pattern: "scholar.google.com", category: "academic", label: "Google Scholar" },
|
|
636
|
+
{ pattern: "arxiv.org", category: "academic", label: "arXiv" },
|
|
637
|
+
{ pattern: "pubmed.ncbi.nlm.nih.gov", category: "academic", label: "PubMed" },
|
|
638
|
+
{ pattern: "researchgate.net", category: "academic", label: "ResearchGate" },
|
|
639
|
+
{ pattern: ".edu", category: "academic", label: "Academic (.edu)" }
|
|
629
640
|
];
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
641
|
+
var CATEGORY_LABELS = {
|
|
642
|
+
social: "Social Media",
|
|
643
|
+
forum: "Forums & Q&A",
|
|
644
|
+
news: "News & Media",
|
|
645
|
+
reference: "Reference",
|
|
646
|
+
blog: "Blogs & Content",
|
|
647
|
+
ecommerce: "E-commerce",
|
|
648
|
+
video: "Video",
|
|
649
|
+
academic: "Academic",
|
|
650
|
+
other: "Other"
|
|
651
|
+
};
|
|
652
|
+
function categorizeSource(uri) {
|
|
653
|
+
let domain;
|
|
654
|
+
try {
|
|
655
|
+
const url = new URL(uri.startsWith("http") ? uri : `https://${uri}`);
|
|
656
|
+
domain = url.hostname.replace(/^www\./, "");
|
|
657
|
+
} catch {
|
|
658
|
+
domain = uri.replace(/^https?:\/\//, "").replace(/^www\./, "").split("/")[0] ?? uri;
|
|
634
659
|
}
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
660
|
+
const domainLower = domain.toLowerCase();
|
|
661
|
+
for (const rule of SOURCE_CATEGORY_RULES) {
|
|
662
|
+
if (domainLower === rule.pattern || domainLower.endsWith(`.${rule.pattern}`) || rule.pattern.startsWith(".") && domainLower.endsWith(rule.pattern)) {
|
|
663
|
+
return { category: rule.category, label: rule.label, domain };
|
|
639
664
|
}
|
|
640
665
|
}
|
|
666
|
+
return { category: "other", label: CATEGORY_LABELS.other, domain };
|
|
667
|
+
}
|
|
668
|
+
function categoryLabel(category) {
|
|
669
|
+
return CATEGORY_LABELS[category];
|
|
641
670
|
}
|
|
642
671
|
|
|
643
|
-
// ../
|
|
644
|
-
import
|
|
672
|
+
// ../api-routes/src/auth.ts
|
|
673
|
+
import crypto2 from "crypto";
|
|
674
|
+
import { eq } from "drizzle-orm";
|
|
675
|
+
|
|
676
|
+
// ../db/src/client.ts
|
|
677
|
+
import { mkdirSync } from "fs";
|
|
678
|
+
import { dirname } from "path";
|
|
679
|
+
import Database from "better-sqlite3";
|
|
680
|
+
import { drizzle } from "drizzle-orm/better-sqlite3";
|
|
681
|
+
|
|
682
|
+
// ../db/src/schema.ts
|
|
683
|
+
var schema_exports = {};
|
|
684
|
+
__export(schema_exports, {
|
|
685
|
+
apiKeys: () => apiKeys,
|
|
686
|
+
auditLog: () => auditLog,
|
|
687
|
+
competitors: () => competitors,
|
|
688
|
+
googleConnections: () => googleConnections,
|
|
689
|
+
gscCoverageSnapshots: () => gscCoverageSnapshots,
|
|
690
|
+
gscSearchData: () => gscSearchData,
|
|
691
|
+
gscUrlInspections: () => gscUrlInspections,
|
|
692
|
+
keywords: () => keywords,
|
|
693
|
+
notifications: () => notifications,
|
|
694
|
+
projects: () => projects,
|
|
695
|
+
querySnapshots: () => querySnapshots,
|
|
696
|
+
runs: () => runs,
|
|
697
|
+
schedules: () => schedules,
|
|
698
|
+
usageCounters: () => usageCounters
|
|
699
|
+
});
|
|
700
|
+
import { index, integer, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core";
|
|
701
|
+
var projects = sqliteTable("projects", {
|
|
702
|
+
id: text("id").primaryKey(),
|
|
703
|
+
name: text("name").notNull().unique(),
|
|
704
|
+
displayName: text("display_name").notNull(),
|
|
705
|
+
canonicalDomain: text("canonical_domain").notNull(),
|
|
706
|
+
ownedDomains: text("owned_domains").notNull().default("[]"),
|
|
707
|
+
country: text("country").notNull(),
|
|
708
|
+
language: text("language").notNull(),
|
|
709
|
+
tags: text("tags").notNull().default("[]"),
|
|
710
|
+
labels: text("labels").notNull().default("{}"),
|
|
711
|
+
providers: text("providers").notNull().default("[]"),
|
|
712
|
+
locations: text("locations").notNull().default("[]"),
|
|
713
|
+
defaultLocation: text("default_location"),
|
|
714
|
+
configSource: text("config_source").notNull().default("cli"),
|
|
715
|
+
configRevision: integer("config_revision").notNull().default(1),
|
|
716
|
+
createdAt: text("created_at").notNull(),
|
|
717
|
+
updatedAt: text("updated_at").notNull()
|
|
718
|
+
});
|
|
719
|
+
var keywords = sqliteTable("keywords", {
|
|
720
|
+
id: text("id").primaryKey(),
|
|
721
|
+
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
722
|
+
keyword: text("keyword").notNull(),
|
|
723
|
+
createdAt: text("created_at").notNull()
|
|
724
|
+
}, (table) => [
|
|
725
|
+
index("idx_keywords_project").on(table.projectId),
|
|
726
|
+
uniqueIndex("idx_keywords_project_keyword").on(table.projectId, table.keyword)
|
|
727
|
+
]);
|
|
728
|
+
var competitors = sqliteTable("competitors", {
|
|
729
|
+
id: text("id").primaryKey(),
|
|
730
|
+
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
731
|
+
domain: text("domain").notNull(),
|
|
732
|
+
createdAt: text("created_at").notNull()
|
|
733
|
+
}, (table) => [
|
|
734
|
+
index("idx_competitors_project").on(table.projectId),
|
|
735
|
+
uniqueIndex("idx_competitors_project_domain").on(table.projectId, table.domain)
|
|
736
|
+
]);
|
|
737
|
+
var runs = sqliteTable("runs", {
|
|
738
|
+
id: text("id").primaryKey(),
|
|
739
|
+
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
740
|
+
kind: text("kind").notNull().default("answer-visibility"),
|
|
741
|
+
status: text("status").notNull().default("queued"),
|
|
742
|
+
trigger: text("trigger").notNull().default("manual"),
|
|
743
|
+
location: text("location"),
|
|
744
|
+
startedAt: text("started_at"),
|
|
745
|
+
finishedAt: text("finished_at"),
|
|
746
|
+
error: text("error"),
|
|
747
|
+
createdAt: text("created_at").notNull()
|
|
748
|
+
}, (table) => [
|
|
749
|
+
index("idx_runs_project").on(table.projectId),
|
|
750
|
+
index("idx_runs_status").on(table.status)
|
|
751
|
+
]);
|
|
752
|
+
var querySnapshots = sqliteTable("query_snapshots", {
|
|
753
|
+
id: text("id").primaryKey(),
|
|
754
|
+
runId: text("run_id").notNull().references(() => runs.id, { onDelete: "cascade" }),
|
|
755
|
+
keywordId: text("keyword_id").notNull().references(() => keywords.id, { onDelete: "cascade" }),
|
|
756
|
+
provider: text("provider").notNull().default("gemini"),
|
|
757
|
+
model: text("model"),
|
|
758
|
+
citationState: text("citation_state").notNull(),
|
|
759
|
+
answerText: text("answer_text"),
|
|
760
|
+
citedDomains: text("cited_domains").notNull().default("[]"),
|
|
761
|
+
competitorOverlap: text("competitor_overlap").notNull().default("[]"),
|
|
762
|
+
location: text("location"),
|
|
763
|
+
screenshotPath: text("screenshot_path"),
|
|
764
|
+
rawResponse: text("raw_response"),
|
|
765
|
+
createdAt: text("created_at").notNull()
|
|
766
|
+
}, (table) => [
|
|
767
|
+
index("idx_snapshots_run").on(table.runId),
|
|
768
|
+
index("idx_snapshots_keyword").on(table.keywordId)
|
|
769
|
+
]);
|
|
770
|
+
var auditLog = sqliteTable("audit_log", {
|
|
771
|
+
id: text("id").primaryKey(),
|
|
772
|
+
projectId: text("project_id").references(() => projects.id, { onDelete: "cascade" }),
|
|
773
|
+
actor: text("actor").notNull(),
|
|
774
|
+
action: text("action").notNull(),
|
|
775
|
+
entityType: text("entity_type").notNull(),
|
|
776
|
+
entityId: text("entity_id"),
|
|
777
|
+
diff: text("diff"),
|
|
778
|
+
createdAt: text("created_at").notNull()
|
|
779
|
+
}, (table) => [
|
|
780
|
+
index("idx_audit_log_project").on(table.projectId),
|
|
781
|
+
index("idx_audit_log_created").on(table.createdAt)
|
|
782
|
+
]);
|
|
783
|
+
var apiKeys = sqliteTable("api_keys", {
|
|
784
|
+
id: text("id").primaryKey(),
|
|
785
|
+
name: text("name").notNull(),
|
|
786
|
+
keyHash: text("key_hash").notNull().unique(),
|
|
787
|
+
keyPrefix: text("key_prefix").notNull(),
|
|
788
|
+
scopes: text("scopes").notNull().default('["*"]'),
|
|
789
|
+
createdAt: text("created_at").notNull(),
|
|
790
|
+
lastUsedAt: text("last_used_at"),
|
|
791
|
+
revokedAt: text("revoked_at")
|
|
792
|
+
}, (table) => [
|
|
793
|
+
index("idx_api_keys_prefix").on(table.keyPrefix)
|
|
794
|
+
]);
|
|
795
|
+
var schedules = sqliteTable("schedules", {
|
|
796
|
+
id: text("id").primaryKey(),
|
|
797
|
+
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
798
|
+
cronExpr: text("cron_expr").notNull(),
|
|
799
|
+
preset: text("preset"),
|
|
800
|
+
timezone: text("timezone").notNull().default("UTC"),
|
|
801
|
+
enabled: integer("enabled").notNull().default(1),
|
|
802
|
+
providers: text("providers").notNull().default("[]"),
|
|
803
|
+
lastRunAt: text("last_run_at"),
|
|
804
|
+
nextRunAt: text("next_run_at"),
|
|
805
|
+
createdAt: text("created_at").notNull(),
|
|
806
|
+
updatedAt: text("updated_at").notNull()
|
|
807
|
+
}, (table) => [
|
|
808
|
+
uniqueIndex("idx_schedules_project").on(table.projectId)
|
|
809
|
+
]);
|
|
810
|
+
var notifications = sqliteTable("notifications", {
|
|
811
|
+
id: text("id").primaryKey(),
|
|
812
|
+
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
813
|
+
channel: text("channel").notNull(),
|
|
814
|
+
config: text("config").notNull(),
|
|
815
|
+
webhookSecret: text("webhook_secret"),
|
|
816
|
+
enabled: integer("enabled").notNull().default(1),
|
|
817
|
+
createdAt: text("created_at").notNull(),
|
|
818
|
+
updatedAt: text("updated_at").notNull()
|
|
819
|
+
}, (table) => [
|
|
820
|
+
index("idx_notifications_project").on(table.projectId)
|
|
821
|
+
]);
|
|
822
|
+
var googleConnections = sqliteTable("google_connections", {
|
|
823
|
+
id: text("id").primaryKey(),
|
|
824
|
+
domain: text("domain").notNull(),
|
|
825
|
+
connectionType: text("connection_type").notNull(),
|
|
826
|
+
propertyId: text("property_id"),
|
|
827
|
+
sitemapUrl: text("sitemap_url"),
|
|
828
|
+
accessToken: text("access_token"),
|
|
829
|
+
refreshToken: text("refresh_token"),
|
|
830
|
+
tokenExpiresAt: text("token_expires_at"),
|
|
831
|
+
scopes: text("scopes").notNull().default("[]"),
|
|
832
|
+
createdAt: text("created_at").notNull(),
|
|
833
|
+
updatedAt: text("updated_at").notNull()
|
|
834
|
+
}, (table) => [
|
|
835
|
+
uniqueIndex("idx_google_conn_domain_type").on(table.domain, table.connectionType)
|
|
836
|
+
]);
|
|
837
|
+
var gscSearchData = sqliteTable("gsc_search_data", {
|
|
838
|
+
id: text("id").primaryKey(),
|
|
839
|
+
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
840
|
+
syncRunId: text("sync_run_id").notNull().references(() => runs.id, { onDelete: "cascade" }),
|
|
841
|
+
date: text("date").notNull(),
|
|
842
|
+
query: text("query").notNull(),
|
|
843
|
+
page: text("page").notNull(),
|
|
844
|
+
country: text("country"),
|
|
845
|
+
device: text("device"),
|
|
846
|
+
clicks: integer("clicks").notNull().default(0),
|
|
847
|
+
impressions: integer("impressions").notNull().default(0),
|
|
848
|
+
ctr: text("ctr").notNull().default("0"),
|
|
849
|
+
position: text("position").notNull().default("0"),
|
|
850
|
+
createdAt: text("created_at").notNull()
|
|
851
|
+
}, (table) => [
|
|
852
|
+
index("idx_gsc_search_project_date").on(table.projectId, table.date),
|
|
853
|
+
index("idx_gsc_search_query").on(table.query),
|
|
854
|
+
index("idx_gsc_search_run").on(table.syncRunId)
|
|
855
|
+
]);
|
|
856
|
+
var gscUrlInspections = sqliteTable("gsc_url_inspections", {
|
|
857
|
+
id: text("id").primaryKey(),
|
|
858
|
+
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
859
|
+
syncRunId: text("sync_run_id").references(() => runs.id, { onDelete: "cascade" }),
|
|
860
|
+
url: text("url").notNull(),
|
|
861
|
+
indexingState: text("indexing_state"),
|
|
862
|
+
verdict: text("verdict"),
|
|
863
|
+
coverageState: text("coverage_state"),
|
|
864
|
+
pageFetchState: text("page_fetch_state"),
|
|
865
|
+
robotsTxtState: text("robots_txt_state"),
|
|
866
|
+
crawlTime: text("crawl_time"),
|
|
867
|
+
lastCrawlResult: text("last_crawl_result"),
|
|
868
|
+
isMobileFriendly: integer("is_mobile_friendly"),
|
|
869
|
+
richResults: text("rich_results").notNull().default("[]"),
|
|
870
|
+
referringUrls: text("referring_urls").notNull().default("[]"),
|
|
871
|
+
inspectedAt: text("inspected_at").notNull(),
|
|
872
|
+
createdAt: text("created_at").notNull()
|
|
873
|
+
}, (table) => [
|
|
874
|
+
index("idx_gsc_inspect_project_url").on(table.projectId, table.url),
|
|
875
|
+
index("idx_gsc_inspect_run").on(table.syncRunId),
|
|
876
|
+
index("idx_gsc_inspect_url_time").on(table.url, table.inspectedAt)
|
|
877
|
+
]);
|
|
878
|
+
var gscCoverageSnapshots = sqliteTable("gsc_coverage_snapshots", {
|
|
879
|
+
id: text("id").primaryKey(),
|
|
880
|
+
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
881
|
+
syncRunId: text("sync_run_id").references(() => runs.id, { onDelete: "cascade" }),
|
|
882
|
+
date: text("date").notNull(),
|
|
883
|
+
indexed: integer("indexed").notNull().default(0),
|
|
884
|
+
notIndexed: integer("not_indexed").notNull().default(0),
|
|
885
|
+
reasonBreakdown: text("reason_breakdown").notNull().default("{}"),
|
|
886
|
+
createdAt: text("created_at").notNull()
|
|
887
|
+
}, (table) => [
|
|
888
|
+
index("idx_gsc_coverage_snap_project_date").on(table.projectId, table.date),
|
|
889
|
+
index("idx_gsc_coverage_snap_run").on(table.syncRunId)
|
|
890
|
+
]);
|
|
891
|
+
var usageCounters = sqliteTable("usage_counters", {
|
|
892
|
+
id: text("id").primaryKey(),
|
|
893
|
+
scope: text("scope").notNull(),
|
|
894
|
+
period: text("period").notNull(),
|
|
895
|
+
metric: text("metric").notNull(),
|
|
896
|
+
count: integer("count").notNull().default(0),
|
|
897
|
+
updatedAt: text("updated_at").notNull()
|
|
898
|
+
}, (table) => [
|
|
899
|
+
uniqueIndex("idx_usage_scope_period_metric").on(table.scope, table.period, table.metric),
|
|
900
|
+
index("idx_usage_scope_period").on(table.scope, table.period)
|
|
901
|
+
]);
|
|
645
902
|
|
|
646
|
-
// ../
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
});
|
|
653
|
-
var PROVIDER_NAMES = ["gemini", "openai", "claude", "local", "cdp:chatgpt"];
|
|
654
|
-
var providerNameSchema = z.enum(PROVIDER_NAMES);
|
|
655
|
-
var PROVIDER_MODE = {
|
|
656
|
-
gemini: "api",
|
|
657
|
-
openai: "api",
|
|
658
|
-
claude: "api",
|
|
659
|
-
local: "api",
|
|
660
|
-
"cdp:chatgpt": "browser"
|
|
661
|
-
};
|
|
662
|
-
function isBrowserProvider(name) {
|
|
663
|
-
return PROVIDER_MODE[name] === "browser";
|
|
664
|
-
}
|
|
665
|
-
var CDP_TARGETS = ["cdp:chatgpt"];
|
|
666
|
-
function parseProviderName(input) {
|
|
667
|
-
const lower = input.trim().toLowerCase();
|
|
668
|
-
return PROVIDER_NAMES.includes(lower) ? lower : void 0;
|
|
669
|
-
}
|
|
670
|
-
function resolveProviderInput(input) {
|
|
671
|
-
const lower = input.trim().toLowerCase();
|
|
672
|
-
if (lower === "cdp") {
|
|
673
|
-
return [...CDP_TARGETS];
|
|
674
|
-
}
|
|
675
|
-
const parsed = parseProviderName(lower);
|
|
676
|
-
return parsed ? [parsed] : [];
|
|
903
|
+
// ../db/src/client.ts
|
|
904
|
+
function createClient(databasePath) {
|
|
905
|
+
mkdirSync(dirname(databasePath), { recursive: true });
|
|
906
|
+
const sqlite = new Database(databasePath);
|
|
907
|
+
sqlite.pragma("journal_mode = WAL");
|
|
908
|
+
sqlite.pragma("foreign_keys = ON");
|
|
909
|
+
return drizzle(sqlite, { schema: schema_exports });
|
|
677
910
|
}
|
|
678
|
-
var locationContextSchema = z.object({
|
|
679
|
-
label: z.string().min(1),
|
|
680
|
-
city: z.string().min(1),
|
|
681
|
-
region: z.string().min(1),
|
|
682
|
-
country: z.string().length(2),
|
|
683
|
-
timezone: z.string().optional()
|
|
684
|
-
});
|
|
685
911
|
|
|
686
|
-
// ../
|
|
687
|
-
import {
|
|
688
|
-
var
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
912
|
+
// ../db/src/migrate.ts
|
|
913
|
+
import { sql } from "drizzle-orm";
|
|
914
|
+
var MIGRATION_SQL = `
|
|
915
|
+
CREATE TABLE IF NOT EXISTS projects (
|
|
916
|
+
id TEXT PRIMARY KEY,
|
|
917
|
+
name TEXT NOT NULL UNIQUE,
|
|
918
|
+
display_name TEXT NOT NULL,
|
|
919
|
+
canonical_domain TEXT NOT NULL,
|
|
920
|
+
owned_domains TEXT NOT NULL DEFAULT '[]',
|
|
921
|
+
country TEXT NOT NULL,
|
|
922
|
+
language TEXT NOT NULL,
|
|
923
|
+
tags TEXT NOT NULL DEFAULT '[]',
|
|
924
|
+
labels TEXT NOT NULL DEFAULT '{}',
|
|
925
|
+
providers TEXT NOT NULL DEFAULT '[]',
|
|
926
|
+
config_source TEXT NOT NULL DEFAULT 'cli',
|
|
927
|
+
config_revision INTEGER NOT NULL DEFAULT 1,
|
|
928
|
+
created_at TEXT NOT NULL,
|
|
929
|
+
updated_at TEXT NOT NULL
|
|
930
|
+
);
|
|
705
931
|
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
var configScheduleSchema = z3.object({
|
|
714
|
-
preset: z3.string().optional(),
|
|
715
|
-
cron: z3.string().optional(),
|
|
716
|
-
timezone: z3.string().optional().default("UTC"),
|
|
717
|
-
providers: z3.array(providerNameSchema).optional().default([])
|
|
718
|
-
}).refine(
|
|
719
|
-
(data) => data.preset && !data.cron || !data.preset && data.cron,
|
|
720
|
-
{ message: 'Exactly one of "preset" or "cron" must be provided' }
|
|
721
|
-
).optional();
|
|
722
|
-
var configNotificationSchema = z3.object({
|
|
723
|
-
channel: z3.literal("webhook"),
|
|
724
|
-
url: z3.string().url(),
|
|
725
|
-
events: z3.array(notificationEventSchema).min(1)
|
|
726
|
-
});
|
|
727
|
-
var configGoogleSchema = z3.object({
|
|
728
|
-
gsc: z3.object({
|
|
729
|
-
propertyUrl: z3.string()
|
|
730
|
-
}).optional(),
|
|
731
|
-
syncSchedule: z3.object({
|
|
732
|
-
preset: z3.string().optional(),
|
|
733
|
-
cron: z3.string().optional()
|
|
734
|
-
}).optional()
|
|
735
|
-
}).optional();
|
|
736
|
-
var configSpecSchema = z3.object({
|
|
737
|
-
displayName: z3.string().min(1),
|
|
738
|
-
canonicalDomain: z3.string().min(1),
|
|
739
|
-
ownedDomains: z3.array(z3.string().min(1)).optional().default([]),
|
|
740
|
-
country: z3.string().length(2),
|
|
741
|
-
language: z3.string().min(2),
|
|
742
|
-
keywords: z3.array(z3.string().min(1)).optional().default([]),
|
|
743
|
-
competitors: z3.array(z3.string().min(1)).optional().default([]),
|
|
744
|
-
providers: z3.array(providerNameSchema).optional().default([]),
|
|
745
|
-
locations: z3.array(locationContextSchema).optional().default([]),
|
|
746
|
-
defaultLocation: z3.string().optional(),
|
|
747
|
-
schedule: configScheduleSchema,
|
|
748
|
-
notifications: z3.array(configNotificationSchema).optional().default([]),
|
|
749
|
-
google: configGoogleSchema
|
|
750
|
-
});
|
|
751
|
-
var projectConfigSchema = z3.object({
|
|
752
|
-
apiVersion: z3.literal("canonry/v1"),
|
|
753
|
-
kind: z3.literal("Project"),
|
|
754
|
-
metadata: configMetadataSchema,
|
|
755
|
-
spec: configSpecSchema
|
|
756
|
-
});
|
|
932
|
+
CREATE TABLE IF NOT EXISTS keywords (
|
|
933
|
+
id TEXT PRIMARY KEY,
|
|
934
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
935
|
+
keyword TEXT NOT NULL,
|
|
936
|
+
created_at TEXT NOT NULL,
|
|
937
|
+
UNIQUE(project_id, keyword)
|
|
938
|
+
);
|
|
757
939
|
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
{ id: "gpt-5.4-pro", displayName: "GPT-5.4 Pro", tier: "flagship" },
|
|
778
|
-
{ id: "gpt-5-mini", displayName: "GPT-5 Mini", tier: "fast" },
|
|
779
|
-
{ id: "gpt-5-nano", displayName: "GPT-5 Nano", tier: "economy" },
|
|
780
|
-
{ id: "gpt-5", displayName: "GPT-5", tier: "standard" },
|
|
781
|
-
{ id: "gpt-4.1", displayName: "GPT-4.1", tier: "standard" }
|
|
782
|
-
]
|
|
783
|
-
},
|
|
784
|
-
claude: {
|
|
785
|
-
defaultModel: "claude-sonnet-4-6",
|
|
786
|
-
validationPattern: /^claude-/,
|
|
787
|
-
validationHint: 'model name must start with "claude-" (e.g. claude-sonnet-4-6)',
|
|
788
|
-
knownModels: [
|
|
789
|
-
{ id: "claude-opus-4-6", displayName: "Claude Opus 4.6", tier: "flagship" },
|
|
790
|
-
{ id: "claude-sonnet-4-6", displayName: "Claude Sonnet 4.6", tier: "standard" },
|
|
791
|
-
{ id: "claude-haiku-4-5", displayName: "Claude Haiku 4.5", tier: "fast" }
|
|
792
|
-
]
|
|
793
|
-
},
|
|
794
|
-
local: {
|
|
795
|
-
defaultModel: "llama3",
|
|
796
|
-
validationPattern: /./,
|
|
797
|
-
validationHint: "any model name accepted",
|
|
798
|
-
knownModels: [
|
|
799
|
-
{ id: "llama3", displayName: "Llama 3", tier: "standard" }
|
|
800
|
-
]
|
|
801
|
-
},
|
|
802
|
-
"cdp:chatgpt": {
|
|
803
|
-
defaultModel: "chatgpt-web",
|
|
804
|
-
validationPattern: /./,
|
|
805
|
-
validationHint: "model is detected from the ChatGPT web UI",
|
|
806
|
-
knownModels: [
|
|
807
|
-
{ id: "chatgpt-web", displayName: "ChatGPT (Web UI)", tier: "standard" }
|
|
808
|
-
]
|
|
809
|
-
}
|
|
810
|
-
};
|
|
811
|
-
function getDefaultModel(provider) {
|
|
812
|
-
return MODEL_REGISTRY[provider].defaultModel;
|
|
813
|
-
}
|
|
814
|
-
function isValidModelName(provider, model) {
|
|
815
|
-
return MODEL_REGISTRY[provider].validationPattern.test(model);
|
|
816
|
-
}
|
|
940
|
+
CREATE TABLE IF NOT EXISTS competitors (
|
|
941
|
+
id TEXT PRIMARY KEY,
|
|
942
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
943
|
+
domain TEXT NOT NULL,
|
|
944
|
+
created_at TEXT NOT NULL,
|
|
945
|
+
UNIQUE(project_id, domain)
|
|
946
|
+
);
|
|
947
|
+
|
|
948
|
+
CREATE TABLE IF NOT EXISTS runs (
|
|
949
|
+
id TEXT PRIMARY KEY,
|
|
950
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
951
|
+
kind TEXT NOT NULL DEFAULT 'answer-visibility',
|
|
952
|
+
status TEXT NOT NULL DEFAULT 'queued',
|
|
953
|
+
trigger TEXT NOT NULL DEFAULT 'manual',
|
|
954
|
+
started_at TEXT,
|
|
955
|
+
finished_at TEXT,
|
|
956
|
+
error TEXT,
|
|
957
|
+
created_at TEXT NOT NULL
|
|
958
|
+
);
|
|
817
959
|
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
toJSON() {
|
|
831
|
-
return {
|
|
832
|
-
error: {
|
|
833
|
-
code: this.code,
|
|
834
|
-
message: this.message,
|
|
835
|
-
...this.details ? { details: this.details } : {}
|
|
836
|
-
}
|
|
837
|
-
};
|
|
838
|
-
}
|
|
839
|
-
};
|
|
840
|
-
function notFound(entity, id) {
|
|
841
|
-
return new AppError("NOT_FOUND", `${entity} '${id}' not found`, 404);
|
|
842
|
-
}
|
|
843
|
-
function validationError(message, details) {
|
|
844
|
-
return new AppError("VALIDATION_ERROR", message, 400, details);
|
|
845
|
-
}
|
|
846
|
-
function authRequired() {
|
|
847
|
-
return new AppError("AUTH_REQUIRED", "Authentication required", 401);
|
|
848
|
-
}
|
|
849
|
-
function authInvalid() {
|
|
850
|
-
return new AppError("AUTH_INVALID", "Invalid API key", 401);
|
|
851
|
-
}
|
|
852
|
-
function runInProgress(projectName) {
|
|
853
|
-
return new AppError("RUN_IN_PROGRESS", `A run is already in progress for '${projectName}'`, 409);
|
|
854
|
-
}
|
|
855
|
-
function unsupportedKind(kind) {
|
|
856
|
-
return new AppError("UNSUPPORTED_KIND", `Kind '${kind}' is not supported in this version`, 400);
|
|
857
|
-
}
|
|
960
|
+
CREATE TABLE IF NOT EXISTS query_snapshots (
|
|
961
|
+
id TEXT PRIMARY KEY,
|
|
962
|
+
run_id TEXT NOT NULL REFERENCES runs(id) ON DELETE CASCADE,
|
|
963
|
+
keyword_id TEXT NOT NULL REFERENCES keywords(id) ON DELETE CASCADE,
|
|
964
|
+
provider TEXT NOT NULL DEFAULT 'gemini',
|
|
965
|
+
citation_state TEXT NOT NULL,
|
|
966
|
+
answer_text TEXT,
|
|
967
|
+
cited_domains TEXT NOT NULL DEFAULT '[]',
|
|
968
|
+
competitor_overlap TEXT NOT NULL DEFAULT '[]',
|
|
969
|
+
raw_response TEXT,
|
|
970
|
+
created_at TEXT NOT NULL
|
|
971
|
+
);
|
|
858
972
|
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
createdAt: z4.string(),
|
|
870
|
-
updatedAt: z4.string()
|
|
871
|
-
});
|
|
872
|
-
var gscSearchDataDtoSchema = z4.object({
|
|
873
|
-
date: z4.string(),
|
|
874
|
-
query: z4.string(),
|
|
875
|
-
page: z4.string(),
|
|
876
|
-
country: z4.string().nullable().optional(),
|
|
877
|
-
device: z4.string().nullable().optional(),
|
|
878
|
-
clicks: z4.number(),
|
|
879
|
-
impressions: z4.number(),
|
|
880
|
-
ctr: z4.number(),
|
|
881
|
-
position: z4.number()
|
|
882
|
-
});
|
|
883
|
-
var gscUrlInspectionDtoSchema = z4.object({
|
|
884
|
-
id: z4.string(),
|
|
885
|
-
url: z4.string(),
|
|
886
|
-
indexingState: z4.string().nullable().optional(),
|
|
887
|
-
verdict: z4.string().nullable().optional(),
|
|
888
|
-
coverageState: z4.string().nullable().optional(),
|
|
889
|
-
pageFetchState: z4.string().nullable().optional(),
|
|
890
|
-
robotsTxtState: z4.string().nullable().optional(),
|
|
891
|
-
crawlTime: z4.string().nullable().optional(),
|
|
892
|
-
lastCrawlResult: z4.string().nullable().optional(),
|
|
893
|
-
isMobileFriendly: z4.boolean().nullable().optional(),
|
|
894
|
-
richResults: z4.array(z4.string()).default([]),
|
|
895
|
-
inspectedAt: z4.string()
|
|
896
|
-
});
|
|
897
|
-
var indexTransitionSchema = z4.enum(["stable", "reindexed", "deindexed", "still-missing", "new"]);
|
|
898
|
-
var gscDeindexedRowSchema = z4.object({
|
|
899
|
-
url: z4.string(),
|
|
900
|
-
previousState: z4.string().nullable(),
|
|
901
|
-
currentState: z4.string().nullable(),
|
|
902
|
-
transitionDate: z4.string()
|
|
903
|
-
});
|
|
904
|
-
var gscReasonGroupSchema = z4.object({
|
|
905
|
-
reason: z4.string(),
|
|
906
|
-
count: z4.number(),
|
|
907
|
-
urls: z4.array(gscUrlInspectionDtoSchema).default([])
|
|
908
|
-
});
|
|
909
|
-
var gscCoverageSummaryDtoSchema = z4.object({
|
|
910
|
-
summary: z4.object({
|
|
911
|
-
total: z4.number(),
|
|
912
|
-
indexed: z4.number(),
|
|
913
|
-
notIndexed: z4.number(),
|
|
914
|
-
deindexed: z4.number(),
|
|
915
|
-
percentage: z4.number()
|
|
916
|
-
}),
|
|
917
|
-
lastInspectedAt: z4.string().nullable(),
|
|
918
|
-
indexed: z4.array(gscUrlInspectionDtoSchema).default([]),
|
|
919
|
-
notIndexed: z4.array(gscUrlInspectionDtoSchema).default([]),
|
|
920
|
-
deindexed: z4.array(gscDeindexedRowSchema).default([]),
|
|
921
|
-
reasonGroups: z4.array(gscReasonGroupSchema).default([])
|
|
922
|
-
});
|
|
923
|
-
var indexingNotificationDtoSchema = z4.object({
|
|
924
|
-
url: z4.string(),
|
|
925
|
-
type: z4.enum(["URL_UPDATED", "URL_DELETED"]),
|
|
926
|
-
notifiedAt: z4.string()
|
|
927
|
-
});
|
|
928
|
-
var indexingRequestResultDtoSchema = z4.object({
|
|
929
|
-
url: z4.string(),
|
|
930
|
-
type: z4.enum(["URL_UPDATED", "URL_DELETED"]),
|
|
931
|
-
notifiedAt: z4.string(),
|
|
932
|
-
status: z4.enum(["success", "error"]),
|
|
933
|
-
error: z4.string().optional()
|
|
934
|
-
});
|
|
935
|
-
var gscCoverageSnapshotDtoSchema = z4.object({
|
|
936
|
-
date: z4.string(),
|
|
937
|
-
indexed: z4.number(),
|
|
938
|
-
notIndexed: z4.number(),
|
|
939
|
-
reasonBreakdown: z4.record(z4.string(), z4.number()).default({})
|
|
940
|
-
});
|
|
973
|
+
CREATE TABLE IF NOT EXISTS audit_log (
|
|
974
|
+
id TEXT PRIMARY KEY,
|
|
975
|
+
project_id TEXT REFERENCES projects(id) ON DELETE CASCADE,
|
|
976
|
+
actor TEXT NOT NULL,
|
|
977
|
+
action TEXT NOT NULL,
|
|
978
|
+
entity_type TEXT NOT NULL,
|
|
979
|
+
entity_id TEXT,
|
|
980
|
+
diff TEXT,
|
|
981
|
+
created_at TEXT NOT NULL
|
|
982
|
+
);
|
|
941
983
|
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
language: z5.string().min(2),
|
|
953
|
-
tags: z5.array(z5.string()).default([]),
|
|
954
|
-
labels: z5.record(z5.string(), z5.string()).default({}),
|
|
955
|
-
locations: z5.array(locationContextSchema).default([]),
|
|
956
|
-
defaultLocation: z5.string().nullable().optional(),
|
|
957
|
-
configSource: configSourceSchema.default("cli"),
|
|
958
|
-
configRevision: z5.number().int().positive().default(1),
|
|
959
|
-
createdAt: z5.string().optional(),
|
|
960
|
-
updatedAt: z5.string().optional()
|
|
961
|
-
});
|
|
962
|
-
function normalizeProjectDomain(input) {
|
|
963
|
-
let domain = input.trim().toLowerCase();
|
|
964
|
-
try {
|
|
965
|
-
if (domain.includes("://")) {
|
|
966
|
-
domain = new URL(domain).hostname.toLowerCase();
|
|
967
|
-
}
|
|
968
|
-
} catch {
|
|
969
|
-
}
|
|
970
|
-
return domain.replace(/^www\./, "");
|
|
971
|
-
}
|
|
972
|
-
function effectiveDomains(project) {
|
|
973
|
-
const all = [project.canonicalDomain, ...project.ownedDomains ?? []];
|
|
974
|
-
const seen = /* @__PURE__ */ new Set();
|
|
975
|
-
const result = [];
|
|
976
|
-
for (const d of all) {
|
|
977
|
-
const trimmed = d.trim();
|
|
978
|
-
if (!trimmed) continue;
|
|
979
|
-
const norm = normalizeProjectDomain(trimmed);
|
|
980
|
-
if (seen.has(norm)) continue;
|
|
981
|
-
seen.add(norm);
|
|
982
|
-
result.push(trimmed);
|
|
983
|
-
}
|
|
984
|
-
return result;
|
|
985
|
-
}
|
|
984
|
+
CREATE TABLE IF NOT EXISTS api_keys (
|
|
985
|
+
id TEXT PRIMARY KEY,
|
|
986
|
+
name TEXT NOT NULL,
|
|
987
|
+
key_hash TEXT NOT NULL UNIQUE,
|
|
988
|
+
key_prefix TEXT NOT NULL,
|
|
989
|
+
scopes TEXT NOT NULL DEFAULT '["*"]',
|
|
990
|
+
created_at TEXT NOT NULL,
|
|
991
|
+
last_used_at TEXT,
|
|
992
|
+
revoked_at TEXT
|
|
993
|
+
);
|
|
986
994
|
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
projectId: z6.string(),
|
|
997
|
-
kind: runKindSchema,
|
|
998
|
-
status: runStatusSchema,
|
|
999
|
-
trigger: runTriggerSchema.default("manual"),
|
|
1000
|
-
location: z6.string().nullable().optional(),
|
|
1001
|
-
startedAt: z6.string().nullable().optional(),
|
|
1002
|
-
finishedAt: z6.string().nullable().optional(),
|
|
1003
|
-
error: z6.string().nullable().optional(),
|
|
1004
|
-
createdAt: z6.string()
|
|
1005
|
-
});
|
|
1006
|
-
var groundingSourceSchema = z6.object({
|
|
1007
|
-
uri: z6.string(),
|
|
1008
|
-
title: z6.string()
|
|
1009
|
-
});
|
|
1010
|
-
var querySnapshotDtoSchema = z6.object({
|
|
1011
|
-
id: z6.string(),
|
|
1012
|
-
runId: z6.string(),
|
|
1013
|
-
keywordId: z6.string(),
|
|
1014
|
-
keyword: z6.string().optional(),
|
|
1015
|
-
provider: providerNameSchema,
|
|
1016
|
-
citationState: citationStateSchema,
|
|
1017
|
-
transition: computedTransitionSchema.optional(),
|
|
1018
|
-
answerText: z6.string().nullable().optional(),
|
|
1019
|
-
citedDomains: z6.array(z6.string()).default([]),
|
|
1020
|
-
competitorOverlap: z6.array(z6.string()).default([]),
|
|
1021
|
-
groundingSources: z6.array(groundingSourceSchema).default([]),
|
|
1022
|
-
searchQueries: z6.array(z6.string()).default([]),
|
|
1023
|
-
model: z6.string().nullable().optional(),
|
|
1024
|
-
location: z6.string().nullable().optional(),
|
|
1025
|
-
createdAt: z6.string()
|
|
1026
|
-
});
|
|
1027
|
-
var auditLogEntrySchema = z6.object({
|
|
1028
|
-
id: z6.string(),
|
|
1029
|
-
projectId: z6.string().nullable().optional(),
|
|
1030
|
-
actor: z6.string(),
|
|
1031
|
-
action: z6.string(),
|
|
1032
|
-
entityType: z6.string(),
|
|
1033
|
-
entityId: z6.string().nullable().optional(),
|
|
1034
|
-
diff: z6.unknown().optional(),
|
|
1035
|
-
createdAt: z6.string()
|
|
1036
|
-
});
|
|
995
|
+
CREATE TABLE IF NOT EXISTS usage_counters (
|
|
996
|
+
id TEXT PRIMARY KEY,
|
|
997
|
+
scope TEXT NOT NULL,
|
|
998
|
+
period TEXT NOT NULL,
|
|
999
|
+
metric TEXT NOT NULL,
|
|
1000
|
+
count INTEGER NOT NULL DEFAULT 0,
|
|
1001
|
+
updated_at TEXT NOT NULL,
|
|
1002
|
+
UNIQUE(scope, period, metric)
|
|
1003
|
+
);
|
|
1037
1004
|
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1005
|
+
CREATE INDEX IF NOT EXISTS idx_keywords_project ON keywords(project_id);
|
|
1006
|
+
CREATE INDEX IF NOT EXISTS idx_competitors_project ON competitors(project_id);
|
|
1007
|
+
CREATE INDEX IF NOT EXISTS idx_runs_project ON runs(project_id);
|
|
1008
|
+
CREATE INDEX IF NOT EXISTS idx_runs_status ON runs(status);
|
|
1009
|
+
CREATE INDEX IF NOT EXISTS idx_snapshots_run ON query_snapshots(run_id);
|
|
1010
|
+
CREATE INDEX IF NOT EXISTS idx_snapshots_keyword ON query_snapshots(keyword_id);
|
|
1011
|
+
CREATE INDEX IF NOT EXISTS idx_audit_log_project ON audit_log(project_id);
|
|
1012
|
+
CREATE INDEX IF NOT EXISTS idx_audit_log_created ON audit_log(created_at);
|
|
1013
|
+
CREATE TABLE IF NOT EXISTS schedules (
|
|
1014
|
+
id TEXT PRIMARY KEY,
|
|
1015
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
1016
|
+
cron_expr TEXT NOT NULL,
|
|
1017
|
+
preset TEXT,
|
|
1018
|
+
timezone TEXT NOT NULL DEFAULT 'UTC',
|
|
1019
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
1020
|
+
providers TEXT NOT NULL DEFAULT '[]',
|
|
1021
|
+
last_run_at TEXT,
|
|
1022
|
+
next_run_at TEXT,
|
|
1023
|
+
created_at TEXT NOT NULL,
|
|
1024
|
+
updated_at TEXT NOT NULL,
|
|
1025
|
+
UNIQUE(project_id)
|
|
1026
|
+
);
|
|
1053
1027
|
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1028
|
+
CREATE TABLE IF NOT EXISTS notifications (
|
|
1029
|
+
id TEXT PRIMARY KEY,
|
|
1030
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
1031
|
+
channel TEXT NOT NULL,
|
|
1032
|
+
config TEXT NOT NULL,
|
|
1033
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
1034
|
+
created_at TEXT NOT NULL,
|
|
1035
|
+
updated_at TEXT NOT NULL
|
|
1036
|
+
);
|
|
1037
|
+
|
|
1038
|
+
CREATE INDEX IF NOT EXISTS idx_api_keys_prefix ON api_keys(key_prefix);
|
|
1039
|
+
CREATE INDEX IF NOT EXISTS idx_usage_scope_period ON usage_counters(scope, period);
|
|
1040
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_schedules_project ON schedules(project_id);
|
|
1041
|
+
CREATE INDEX IF NOT EXISTS idx_notifications_project ON notifications(project_id);
|
|
1042
|
+
`;
|
|
1043
|
+
var MIGRATIONS = [
|
|
1044
|
+
// v2: Add providers column to projects for multi-provider support
|
|
1045
|
+
`ALTER TABLE projects ADD COLUMN providers TEXT NOT NULL DEFAULT '[]'`,
|
|
1046
|
+
// v3: Add webhook_secret column to notifications for HMAC signing
|
|
1047
|
+
`ALTER TABLE notifications ADD COLUMN webhook_secret TEXT`,
|
|
1048
|
+
// v4: Add owned_domains column to projects for multi-domain citation matching
|
|
1049
|
+
`ALTER TABLE projects ADD COLUMN owned_domains TEXT NOT NULL DEFAULT '[]'`,
|
|
1050
|
+
// v5: Add model column to query_snapshots for per-model scoring
|
|
1051
|
+
`ALTER TABLE query_snapshots ADD COLUMN model TEXT`,
|
|
1052
|
+
// v5b: Backfill model from rawResponse JSON for existing snapshots
|
|
1053
|
+
`UPDATE query_snapshots SET model = json_extract(raw_response, '$.model') WHERE model IS NULL AND raw_response IS NOT NULL AND json_extract(raw_response, '$.model') IS NOT NULL`,
|
|
1054
|
+
// v6: Google Search Console integration — google_connections table (domain-scoped)
|
|
1055
|
+
`CREATE TABLE IF NOT EXISTS google_connections (
|
|
1056
|
+
id TEXT PRIMARY KEY,
|
|
1057
|
+
domain TEXT NOT NULL,
|
|
1058
|
+
connection_type TEXT NOT NULL,
|
|
1059
|
+
property_id TEXT,
|
|
1060
|
+
access_token TEXT,
|
|
1061
|
+
refresh_token TEXT,
|
|
1062
|
+
token_expires_at TEXT,
|
|
1063
|
+
scopes TEXT NOT NULL DEFAULT '[]',
|
|
1064
|
+
created_at TEXT NOT NULL,
|
|
1065
|
+
updated_at TEXT NOT NULL
|
|
1066
|
+
)`,
|
|
1067
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS idx_google_conn_domain_type ON google_connections(domain, connection_type)`,
|
|
1068
|
+
// v6: Google Search Console integration — gsc_search_data table
|
|
1069
|
+
`CREATE TABLE IF NOT EXISTS gsc_search_data (
|
|
1070
|
+
id TEXT PRIMARY KEY,
|
|
1071
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
1072
|
+
sync_run_id TEXT NOT NULL REFERENCES runs(id) ON DELETE CASCADE,
|
|
1073
|
+
date TEXT NOT NULL,
|
|
1074
|
+
query TEXT NOT NULL,
|
|
1075
|
+
page TEXT NOT NULL,
|
|
1076
|
+
country TEXT,
|
|
1077
|
+
device TEXT,
|
|
1078
|
+
clicks INTEGER NOT NULL DEFAULT 0,
|
|
1079
|
+
impressions INTEGER NOT NULL DEFAULT 0,
|
|
1080
|
+
ctr TEXT NOT NULL DEFAULT '0',
|
|
1081
|
+
position TEXT NOT NULL DEFAULT '0',
|
|
1082
|
+
created_at TEXT NOT NULL
|
|
1083
|
+
)`,
|
|
1084
|
+
`CREATE INDEX IF NOT EXISTS idx_gsc_search_project_date ON gsc_search_data(project_id, date)`,
|
|
1085
|
+
`CREATE INDEX IF NOT EXISTS idx_gsc_search_query ON gsc_search_data(query)`,
|
|
1086
|
+
`CREATE INDEX IF NOT EXISTS idx_gsc_search_run ON gsc_search_data(sync_run_id)`,
|
|
1087
|
+
// v6: Google Search Console integration — gsc_url_inspections table
|
|
1088
|
+
`CREATE TABLE IF NOT EXISTS gsc_url_inspections (
|
|
1089
|
+
id TEXT PRIMARY KEY,
|
|
1090
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
1091
|
+
sync_run_id TEXT REFERENCES runs(id) ON DELETE CASCADE,
|
|
1092
|
+
url TEXT NOT NULL,
|
|
1093
|
+
indexing_state TEXT,
|
|
1094
|
+
verdict TEXT,
|
|
1095
|
+
coverage_state TEXT,
|
|
1096
|
+
page_fetch_state TEXT,
|
|
1097
|
+
robots_txt_state TEXT,
|
|
1098
|
+
crawl_time TEXT,
|
|
1099
|
+
last_crawl_result TEXT,
|
|
1100
|
+
is_mobile_friendly INTEGER,
|
|
1101
|
+
rich_results TEXT NOT NULL DEFAULT '[]',
|
|
1102
|
+
referring_urls TEXT NOT NULL DEFAULT '[]',
|
|
1103
|
+
inspected_at TEXT NOT NULL,
|
|
1104
|
+
created_at TEXT NOT NULL
|
|
1105
|
+
)`,
|
|
1106
|
+
`CREATE INDEX IF NOT EXISTS idx_gsc_inspect_project_url ON gsc_url_inspections(project_id, url)`,
|
|
1107
|
+
`CREATE INDEX IF NOT EXISTS idx_gsc_inspect_run ON gsc_url_inspections(sync_run_id)`,
|
|
1108
|
+
`CREATE INDEX IF NOT EXISTS idx_gsc_inspect_url_time ON gsc_url_inspections(url, inspected_at)`,
|
|
1109
|
+
// v7: GSC coverage snapshots for historical tracking
|
|
1110
|
+
`CREATE TABLE IF NOT EXISTS gsc_coverage_snapshots (
|
|
1111
|
+
id TEXT PRIMARY KEY,
|
|
1112
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
1113
|
+
sync_run_id TEXT REFERENCES runs(id) ON DELETE CASCADE,
|
|
1114
|
+
date TEXT NOT NULL,
|
|
1115
|
+
indexed INTEGER NOT NULL DEFAULT 0,
|
|
1116
|
+
not_indexed INTEGER NOT NULL DEFAULT 0,
|
|
1117
|
+
reason_breakdown TEXT NOT NULL DEFAULT '{}',
|
|
1118
|
+
created_at TEXT NOT NULL
|
|
1119
|
+
)`,
|
|
1120
|
+
`CREATE INDEX IF NOT EXISTS idx_gsc_coverage_snap_project_date ON gsc_coverage_snapshots(project_id, date)`,
|
|
1121
|
+
`CREATE INDEX IF NOT EXISTS idx_gsc_coverage_snap_run ON gsc_coverage_snapshots(sync_run_id)`,
|
|
1122
|
+
// v8: Location-aware sweeps — project locations + snapshot location tag
|
|
1123
|
+
`ALTER TABLE projects ADD COLUMN locations TEXT NOT NULL DEFAULT '[]'`,
|
|
1124
|
+
`ALTER TABLE projects ADD COLUMN default_location TEXT`,
|
|
1125
|
+
`ALTER TABLE query_snapshots ADD COLUMN location TEXT`,
|
|
1126
|
+
// v9: Add location column to runs for per-location run tracking
|
|
1127
|
+
`ALTER TABLE runs ADD COLUMN location TEXT`,
|
|
1128
|
+
// v10: Add sitemapUrl to google_connections for persistent sitemap storage
|
|
1129
|
+
`ALTER TABLE google_connections ADD COLUMN sitemap_url TEXT`,
|
|
1130
|
+
// v11: CDP browser provider — screenshot path for captured evidence
|
|
1131
|
+
`ALTER TABLE query_snapshots ADD COLUMN screenshot_path TEXT`
|
|
1114
1132
|
];
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
reference: "Reference",
|
|
1120
|
-
blog: "Blogs & Content",
|
|
1121
|
-
ecommerce: "E-commerce",
|
|
1122
|
-
video: "Video",
|
|
1123
|
-
academic: "Academic",
|
|
1124
|
-
other: "Other"
|
|
1125
|
-
};
|
|
1126
|
-
function categorizeSource(uri) {
|
|
1127
|
-
let domain;
|
|
1128
|
-
try {
|
|
1129
|
-
const url = new URL(uri.startsWith("http") ? uri : `https://${uri}`);
|
|
1130
|
-
domain = url.hostname.replace(/^www\./, "");
|
|
1131
|
-
} catch {
|
|
1132
|
-
domain = uri.replace(/^https?:\/\//, "").replace(/^www\./, "").split("/")[0] ?? uri;
|
|
1133
|
+
function migrate(db) {
|
|
1134
|
+
const statements = MIGRATION_SQL.split(";").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
1135
|
+
for (const statement of statements) {
|
|
1136
|
+
db.run(sql.raw(statement));
|
|
1133
1137
|
}
|
|
1134
|
-
const
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
+
for (const migration of MIGRATIONS) {
|
|
1139
|
+
try {
|
|
1140
|
+
db.run(sql.raw(migration));
|
|
1141
|
+
} catch {
|
|
1138
1142
|
}
|
|
1139
1143
|
}
|
|
1140
|
-
return { category: "other", label: CATEGORY_LABELS.other, domain };
|
|
1141
|
-
}
|
|
1142
|
-
function categoryLabel(category) {
|
|
1143
|
-
return CATEGORY_LABELS[category];
|
|
1144
1144
|
}
|
|
1145
1145
|
|
|
1146
1146
|
// ../api-routes/src/auth.ts
|
|
@@ -5067,6 +5067,27 @@ async function cdpRoutes(app, opts) {
|
|
|
5067
5067
|
// ../api-routes/src/index.ts
|
|
5068
5068
|
async function apiRoutes(app, opts) {
|
|
5069
5069
|
app.decorate("db", opts.db);
|
|
5070
|
+
app.setErrorHandler((error, _request, reply) => {
|
|
5071
|
+
if (error instanceof AppError) {
|
|
5072
|
+
return reply.status(error.statusCode).send(error.toJSON());
|
|
5073
|
+
}
|
|
5074
|
+
const httpStatus = error.statusCode ?? error.status ?? 500;
|
|
5075
|
+
if (httpStatus >= 400 && httpStatus < 500) {
|
|
5076
|
+
return reply.status(httpStatus).send({
|
|
5077
|
+
error: {
|
|
5078
|
+
code: httpStatus === 401 ? "AUTH_INVALID" : httpStatus === 403 ? "FORBIDDEN" : httpStatus === 404 ? "NOT_FOUND" : httpStatus === 429 ? "QUOTA_EXCEEDED" : "VALIDATION_ERROR",
|
|
5079
|
+
message: error.message
|
|
5080
|
+
}
|
|
5081
|
+
});
|
|
5082
|
+
}
|
|
5083
|
+
app.log.error(error);
|
|
5084
|
+
return reply.status(500).send({
|
|
5085
|
+
error: {
|
|
5086
|
+
code: "INTERNAL_ERROR",
|
|
5087
|
+
message: "An unexpected error occurred"
|
|
5088
|
+
}
|
|
5089
|
+
});
|
|
5090
|
+
});
|
|
5070
5091
|
if (!opts.skipAuth) {
|
|
5071
5092
|
await app.register(authPlugin);
|
|
5072
5093
|
}
|
|
@@ -5118,7 +5139,7 @@ async function apiRoutes(app, opts) {
|
|
|
5118
5139
|
onCdpScreenshot: opts.onCdpScreenshot,
|
|
5119
5140
|
onCdpConfigure: opts.onCdpConfigure
|
|
5120
5141
|
});
|
|
5121
|
-
}, { prefix: "/api/v1" });
|
|
5142
|
+
}, { prefix: opts.routePrefix ?? "/api/v1" });
|
|
5122
5143
|
}
|
|
5123
5144
|
|
|
5124
5145
|
// ../provider-gemini/src/normalize.ts
|
|
@@ -7330,7 +7351,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
|
|
|
7330
7351
|
});
|
|
7331
7352
|
saveConfig(opts.config);
|
|
7332
7353
|
}
|
|
7333
|
-
const sitemapUrl = opts.sitemapUrl || `https://${project.canonicalDomain}/sitemap.xml`;
|
|
7354
|
+
const sitemapUrl = opts.sitemapUrl || conn.sitemapUrl || `https://${project.canonicalDomain}/sitemap.xml`;
|
|
7334
7355
|
console.log(`[Inspect Sitemap] Fetching sitemap from ${sitemapUrl}`);
|
|
7335
7356
|
const urls = await fetchAndParseSitemap(sitemapUrl);
|
|
7336
7357
|
console.log(`[Inspect Sitemap] Found ${urls.length} URLs in sitemap`);
|
|
@@ -7980,8 +8001,13 @@ async function createServer(opts) {
|
|
|
7980
8001
|
return removed;
|
|
7981
8002
|
}
|
|
7982
8003
|
};
|
|
8004
|
+
const rawBasePath = process.env.CANONRY_BASE_PATH ?? opts.config.basePath;
|
|
8005
|
+
const normalizedBasePath = rawBasePath ? "/" + rawBasePath.replace(/^\//, "").replace(/\/?$/, "/") : void 0;
|
|
8006
|
+
const basePath = normalizedBasePath === "/" ? void 0 : normalizedBasePath;
|
|
8007
|
+
const apiPrefix = basePath ? `${basePath}api/v1` : "/api/v1";
|
|
7983
8008
|
await app.register(apiRoutes, {
|
|
7984
8009
|
db: opts.db,
|
|
8010
|
+
routePrefix: apiPrefix,
|
|
7985
8011
|
skipAuth: false,
|
|
7986
8012
|
getGoogleAuthConfig: () => getGoogleAuthConfig(opts.config),
|
|
7987
8013
|
googleConnectionStore,
|
|
@@ -8194,8 +8220,6 @@ async function createServer(opts) {
|
|
|
8194
8220
|
const assetsDir = path6.join(dirname2, "..", "assets");
|
|
8195
8221
|
if (fs5.existsSync(assetsDir)) {
|
|
8196
8222
|
const indexPath = path6.join(assetsDir, "index.html");
|
|
8197
|
-
const rawBasePath = process.env.CANONRY_BASE_PATH ?? opts.config.basePath;
|
|
8198
|
-
const basePath = rawBasePath ? "/" + rawBasePath.replace(/^\//, "").replace(/\/?$/, "/") : void 0;
|
|
8199
8223
|
const injectConfig = (html) => {
|
|
8200
8224
|
const clientConfig = { apiKey: opts.config.apiKey };
|
|
8201
8225
|
if (basePath) clientConfig.basePath = basePath;
|
|
@@ -8206,21 +8230,32 @@ async function createServer(opts) {
|
|
|
8206
8230
|
const fastifyStatic = await import("@fastify/static");
|
|
8207
8231
|
await app.register(fastifyStatic.default, {
|
|
8208
8232
|
root: assetsDir,
|
|
8209
|
-
prefix: "/",
|
|
8233
|
+
prefix: basePath ?? "/",
|
|
8210
8234
|
wildcard: false,
|
|
8211
8235
|
// Don't serve index.html automatically — we handle it with config injection
|
|
8212
8236
|
serve: true,
|
|
8213
8237
|
index: false
|
|
8214
8238
|
});
|
|
8215
|
-
|
|
8239
|
+
const serveIndex = (_request, reply) => {
|
|
8216
8240
|
if (fs5.existsSync(indexPath)) {
|
|
8217
8241
|
const html = fs5.readFileSync(indexPath, "utf-8");
|
|
8218
8242
|
return reply.type("text/html").send(injectConfig(html));
|
|
8219
8243
|
}
|
|
8220
8244
|
return reply.status(404).send({ error: "Dashboard not built" });
|
|
8221
|
-
}
|
|
8245
|
+
};
|
|
8246
|
+
const rootRouteTrailing = basePath ?? "/";
|
|
8247
|
+
app.get(rootRouteTrailing, serveIndex);
|
|
8248
|
+
if (basePath) {
|
|
8249
|
+
const rootRouteBare = basePath.replace(/\/$/, "");
|
|
8250
|
+
if (rootRouteBare) app.get(rootRouteBare, serveIndex);
|
|
8251
|
+
}
|
|
8222
8252
|
app.setNotFoundHandler((request, reply) => {
|
|
8223
|
-
|
|
8253
|
+
const url = request.url.split("?")[0];
|
|
8254
|
+
const isApiRoute = url.startsWith("/api/") || basePath !== void 0 && url.startsWith(`${basePath}api/`);
|
|
8255
|
+
if (isApiRoute) {
|
|
8256
|
+
return reply.status(404).send({ error: "Not found", path: request.url });
|
|
8257
|
+
}
|
|
8258
|
+
if (basePath && !url.startsWith(basePath)) {
|
|
8224
8259
|
return reply.status(404).send({ error: "Not found", path: request.url });
|
|
8225
8260
|
}
|
|
8226
8261
|
if (fs5.existsSync(indexPath)) {
|
|
@@ -8230,11 +8265,11 @@ async function createServer(opts) {
|
|
|
8230
8265
|
return reply.status(404).send({ error: "Not found" });
|
|
8231
8266
|
});
|
|
8232
8267
|
}
|
|
8233
|
-
|
|
8234
|
-
|
|
8235
|
-
|
|
8236
|
-
|
|
8237
|
-
}
|
|
8268
|
+
const healthHandler = async () => ({ status: "ok", service: "canonry", version: PKG_VERSION });
|
|
8269
|
+
app.get("/health", healthHandler);
|
|
8270
|
+
if (basePath) {
|
|
8271
|
+
app.get(`${basePath}health`, healthHandler);
|
|
8272
|
+
}
|
|
8238
8273
|
scheduler.start();
|
|
8239
8274
|
app.addHook("onClose", async () => {
|
|
8240
8275
|
scheduler.stop();
|