@ainyc/canonry 1.38.0 → 1.39.1
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-CM92zXYn.css → index-B7ZQYnAY.css} +1 -1
- package/assets/assets/index-Yx4uydah.js +281 -0
- package/assets/index.html +2 -2
- package/dist/{chunk-SAVCFB5B.js → chunk-2MJBBEGP.js} +996 -136
- package/dist/cli.js +352 -211
- package/dist/index.js +1 -1
- package/package.json +8 -7
- package/assets/assets/index-Du9ZvTq9.js +0 -281
|
@@ -243,11 +243,11 @@ function trackEvent(event, properties) {
|
|
|
243
243
|
|
|
244
244
|
// src/server.ts
|
|
245
245
|
import { createRequire as createRequire2 } from "module";
|
|
246
|
-
import
|
|
246
|
+
import crypto23 from "crypto";
|
|
247
247
|
import fs5 from "fs";
|
|
248
248
|
import path6 from "path";
|
|
249
249
|
import { fileURLToPath } from "url";
|
|
250
|
-
import { eq as
|
|
250
|
+
import { eq as eq24 } from "drizzle-orm";
|
|
251
251
|
import Fastify from "fastify";
|
|
252
252
|
|
|
253
253
|
// ../contracts/src/config-schema.ts
|
|
@@ -1224,6 +1224,8 @@ __export(schema_exports, {
|
|
|
1224
1224
|
gscCoverageSnapshots: () => gscCoverageSnapshots,
|
|
1225
1225
|
gscSearchData: () => gscSearchData,
|
|
1226
1226
|
gscUrlInspections: () => gscUrlInspections,
|
|
1227
|
+
healthSnapshots: () => healthSnapshots,
|
|
1228
|
+
insights: () => insights,
|
|
1227
1229
|
keywords: () => keywords,
|
|
1228
1230
|
notifications: () => notifications,
|
|
1229
1231
|
projects: () => projects,
|
|
@@ -1362,6 +1364,7 @@ var googleConnections = sqliteTable("google_connections", {
|
|
|
1362
1364
|
connectionType: text("connection_type").notNull(),
|
|
1363
1365
|
propertyId: text("property_id"),
|
|
1364
1366
|
sitemapUrl: text("sitemap_url"),
|
|
1367
|
+
// WARNING: Authentication material should be stored in config.yaml per CLAUDE.md
|
|
1365
1368
|
accessToken: text("access_token"),
|
|
1366
1369
|
refreshToken: text("refresh_token"),
|
|
1367
1370
|
tokenExpiresAt: text("token_expires_at"),
|
|
@@ -1470,6 +1473,7 @@ var gaConnections = sqliteTable("ga_connections", {
|
|
|
1470
1473
|
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
1471
1474
|
propertyId: text("property_id").notNull(),
|
|
1472
1475
|
clientEmail: text("client_email").notNull(),
|
|
1476
|
+
// WARNING: Authentication material should be stored in config.yaml per CLAUDE.md
|
|
1473
1477
|
privateKey: text("private_key").notNull(),
|
|
1474
1478
|
createdAt: text("created_at").notNull(),
|
|
1475
1479
|
updatedAt: text("updated_at").notNull()
|
|
@@ -1528,6 +1532,39 @@ var usageCounters = sqliteTable("usage_counters", {
|
|
|
1528
1532
|
uniqueIndex("idx_usage_scope_period_metric").on(table.scope, table.period, table.metric),
|
|
1529
1533
|
index("idx_usage_scope_period").on(table.scope, table.period)
|
|
1530
1534
|
]);
|
|
1535
|
+
var insights = sqliteTable("insights", {
|
|
1536
|
+
id: text("id").primaryKey(),
|
|
1537
|
+
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
1538
|
+
runId: text("run_id").references(() => runs.id, { onDelete: "cascade" }),
|
|
1539
|
+
type: text("type").notNull(),
|
|
1540
|
+
severity: text("severity").notNull(),
|
|
1541
|
+
title: text("title").notNull(),
|
|
1542
|
+
keyword: text("keyword").notNull(),
|
|
1543
|
+
provider: text("provider").notNull(),
|
|
1544
|
+
recommendation: text("recommendation"),
|
|
1545
|
+
cause: text("cause"),
|
|
1546
|
+
dismissed: integer("dismissed", { mode: "boolean" }).notNull().default(false),
|
|
1547
|
+
createdAt: text("created_at").notNull()
|
|
1548
|
+
}, (table) => [
|
|
1549
|
+
index("idx_insights_project").on(table.projectId),
|
|
1550
|
+
index("idx_insights_run").on(table.runId),
|
|
1551
|
+
index("idx_insights_created").on(table.createdAt),
|
|
1552
|
+
index("idx_insights_keyword_provider").on(table.keyword, table.provider)
|
|
1553
|
+
]);
|
|
1554
|
+
var healthSnapshots = sqliteTable("health_snapshots", {
|
|
1555
|
+
id: text("id").primaryKey(),
|
|
1556
|
+
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
1557
|
+
runId: text("run_id").references(() => runs.id, { onDelete: "cascade" }),
|
|
1558
|
+
overallCitedRate: text("overall_cited_rate").notNull(),
|
|
1559
|
+
totalPairs: integer("total_pairs").notNull(),
|
|
1560
|
+
citedPairs: integer("cited_pairs").notNull(),
|
|
1561
|
+
providerBreakdown: text("provider_breakdown").notNull().default("{}"),
|
|
1562
|
+
createdAt: text("created_at").notNull()
|
|
1563
|
+
}, (table) => [
|
|
1564
|
+
index("idx_health_snapshots_project").on(table.projectId),
|
|
1565
|
+
index("idx_health_snapshots_run").on(table.runId),
|
|
1566
|
+
index("idx_health_snapshots_created").on(table.createdAt)
|
|
1567
|
+
]);
|
|
1531
1568
|
|
|
1532
1569
|
// ../db/src/client.ts
|
|
1533
1570
|
function createClient(databasePath) {
|
|
@@ -1692,6 +1729,7 @@ var MIGRATIONS = [
|
|
|
1692
1729
|
// v5b: Backfill model from rawResponse JSON for existing snapshots
|
|
1693
1730
|
`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`,
|
|
1694
1731
|
// v6: Google Search Console integration — google_connections table (domain-scoped)
|
|
1732
|
+
// WARNING: access_token, refresh_token are authentication material; consider storing in config.yaml per CLAUDE.md
|
|
1695
1733
|
`CREATE TABLE IF NOT EXISTS google_connections (
|
|
1696
1734
|
id TEXT PRIMARY KEY,
|
|
1697
1735
|
domain TEXT NOT NULL,
|
|
@@ -1807,6 +1845,7 @@ var MIGRATIONS = [
|
|
|
1807
1845
|
`CREATE INDEX IF NOT EXISTS idx_bing_keyword_project ON bing_keyword_stats(project_id)`,
|
|
1808
1846
|
`CREATE INDEX IF NOT EXISTS idx_bing_keyword_query ON bing_keyword_stats(query)`,
|
|
1809
1847
|
// v13: Google Analytics 4 — ga_connections table (service account auth)
|
|
1848
|
+
// WARNING: private_key is authentication material; consider storing in config.yaml per CLAUDE.md
|
|
1810
1849
|
`CREATE TABLE IF NOT EXISTS ga_connections (
|
|
1811
1850
|
id TEXT PRIMARY KEY,
|
|
1812
1851
|
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
@@ -1876,7 +1915,45 @@ var MIGRATIONS = [
|
|
|
1876
1915
|
`ALTER TABLE ga_ai_referrals ADD COLUMN source_dimension TEXT NOT NULL DEFAULT 'session'`,
|
|
1877
1916
|
// Replace old unique index with one that includes source_dimension
|
|
1878
1917
|
`DROP INDEX IF EXISTS idx_ga_ai_ref_unique`,
|
|
1879
|
-
`CREATE UNIQUE INDEX IF NOT EXISTS idx_ga_ai_ref_unique_v2 ON ga_ai_referrals(project_id, date, source, medium, source_dimension)
|
|
1918
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS idx_ga_ai_ref_unique_v2 ON ga_ai_referrals(project_id, date, source, medium, source_dimension)`,
|
|
1919
|
+
// v21: Add missing indexes for query_snapshots filtering
|
|
1920
|
+
`CREATE INDEX IF NOT EXISTS idx_snapshots_citation_state ON query_snapshots(citation_state)`,
|
|
1921
|
+
`CREATE INDEX IF NOT EXISTS idx_snapshots_provider_model ON query_snapshots(provider, model)`,
|
|
1922
|
+
`CREATE INDEX IF NOT EXISTS idx_snapshots_location ON query_snapshots(location)`,
|
|
1923
|
+
// v22: Intelligence — insights table for regression/gain/opportunity tracking
|
|
1924
|
+
`CREATE TABLE IF NOT EXISTS insights (
|
|
1925
|
+
id TEXT PRIMARY KEY,
|
|
1926
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
1927
|
+
type TEXT NOT NULL,
|
|
1928
|
+
severity TEXT NOT NULL,
|
|
1929
|
+
title TEXT NOT NULL,
|
|
1930
|
+
keyword TEXT NOT NULL,
|
|
1931
|
+
provider TEXT NOT NULL,
|
|
1932
|
+
recommendation TEXT,
|
|
1933
|
+
cause TEXT,
|
|
1934
|
+
dismissed INTEGER NOT NULL DEFAULT 0,
|
|
1935
|
+
created_at TEXT NOT NULL
|
|
1936
|
+
)`,
|
|
1937
|
+
`CREATE INDEX IF NOT EXISTS idx_insights_project ON insights(project_id)`,
|
|
1938
|
+
`CREATE INDEX IF NOT EXISTS idx_insights_created ON insights(created_at)`,
|
|
1939
|
+
`CREATE INDEX IF NOT EXISTS idx_insights_keyword_provider ON insights(keyword, provider)`,
|
|
1940
|
+
// v23: Intelligence — health_snapshots table for citation health over time
|
|
1941
|
+
`CREATE TABLE IF NOT EXISTS health_snapshots (
|
|
1942
|
+
id TEXT PRIMARY KEY,
|
|
1943
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
1944
|
+
overall_cited_rate TEXT NOT NULL,
|
|
1945
|
+
total_pairs INTEGER NOT NULL,
|
|
1946
|
+
cited_pairs INTEGER NOT NULL,
|
|
1947
|
+
provider_breakdown TEXT NOT NULL DEFAULT '{}',
|
|
1948
|
+
created_at TEXT NOT NULL
|
|
1949
|
+
)`,
|
|
1950
|
+
`CREATE INDEX IF NOT EXISTS idx_health_snapshots_project ON health_snapshots(project_id)`,
|
|
1951
|
+
`CREATE INDEX IF NOT EXISTS idx_health_snapshots_created ON health_snapshots(created_at)`,
|
|
1952
|
+
// v24: Intelligence — add run_id to insights and health_snapshots for per-run correlation and idempotency
|
|
1953
|
+
`ALTER TABLE insights ADD COLUMN run_id TEXT REFERENCES runs(id) ON DELETE CASCADE`,
|
|
1954
|
+
`CREATE INDEX IF NOT EXISTS idx_insights_run ON insights(run_id)`,
|
|
1955
|
+
`ALTER TABLE health_snapshots ADD COLUMN run_id TEXT REFERENCES runs(id) ON DELETE CASCADE`,
|
|
1956
|
+
`CREATE INDEX IF NOT EXISTS idx_health_snapshots_run ON health_snapshots(run_id)`
|
|
1880
1957
|
];
|
|
1881
1958
|
function isDuplicateColumnError(err) {
|
|
1882
1959
|
if (!(err instanceof Error)) return false;
|
|
@@ -3813,6 +3890,82 @@ function buildCategoryCounts(counts) {
|
|
|
3813
3890
|
return result;
|
|
3814
3891
|
}
|
|
3815
3892
|
|
|
3893
|
+
// ../api-routes/src/intelligence.ts
|
|
3894
|
+
import { eq as eq11, desc as desc4, and as and2 } from "drizzle-orm";
|
|
3895
|
+
function mapInsightRow(r) {
|
|
3896
|
+
return {
|
|
3897
|
+
id: r.id,
|
|
3898
|
+
projectId: r.projectId,
|
|
3899
|
+
runId: r.runId ?? null,
|
|
3900
|
+
type: r.type,
|
|
3901
|
+
severity: r.severity,
|
|
3902
|
+
title: r.title,
|
|
3903
|
+
keyword: r.keyword,
|
|
3904
|
+
provider: r.provider,
|
|
3905
|
+
recommendation: parseJsonColumn(r.recommendation, void 0),
|
|
3906
|
+
cause: parseJsonColumn(r.cause, void 0),
|
|
3907
|
+
dismissed: r.dismissed,
|
|
3908
|
+
createdAt: r.createdAt
|
|
3909
|
+
};
|
|
3910
|
+
}
|
|
3911
|
+
function mapHealthRow(r) {
|
|
3912
|
+
return {
|
|
3913
|
+
id: r.id,
|
|
3914
|
+
projectId: r.projectId,
|
|
3915
|
+
runId: r.runId ?? null,
|
|
3916
|
+
overallCitedRate: Number(r.overallCitedRate),
|
|
3917
|
+
totalPairs: r.totalPairs,
|
|
3918
|
+
citedPairs: r.citedPairs,
|
|
3919
|
+
providerBreakdown: parseJsonColumn(r.providerBreakdown, {}),
|
|
3920
|
+
createdAt: r.createdAt
|
|
3921
|
+
};
|
|
3922
|
+
}
|
|
3923
|
+
async function intelligenceRoutes(app) {
|
|
3924
|
+
app.get("/projects/:name/insights", async (request, reply) => {
|
|
3925
|
+
const project = resolveProject(app.db, request.params.name);
|
|
3926
|
+
const conditions = [eq11(insights.projectId, project.id)];
|
|
3927
|
+
if (request.query.runId) {
|
|
3928
|
+
conditions.push(eq11(insights.runId, request.query.runId));
|
|
3929
|
+
}
|
|
3930
|
+
const rows = app.db.select().from(insights).where(conditions.length === 1 ? conditions[0] : and2(...conditions)).orderBy(desc4(insights.createdAt)).all();
|
|
3931
|
+
const showDismissed = request.query.dismissed === "true";
|
|
3932
|
+
const result = rows.filter((r) => showDismissed || !r.dismissed).map(mapInsightRow);
|
|
3933
|
+
return reply.send(result);
|
|
3934
|
+
});
|
|
3935
|
+
app.get("/projects/:name/insights/:id", async (request, reply) => {
|
|
3936
|
+
const project = resolveProject(app.db, request.params.name);
|
|
3937
|
+
const row = app.db.select().from(insights).where(eq11(insights.id, request.params.id)).get();
|
|
3938
|
+
if (!row || row.projectId !== project.id) {
|
|
3939
|
+
throw notFound("Insight", request.params.id);
|
|
3940
|
+
}
|
|
3941
|
+
return reply.send(mapInsightRow(row));
|
|
3942
|
+
});
|
|
3943
|
+
app.post("/projects/:name/insights/:id/dismiss", async (request, reply) => {
|
|
3944
|
+
const project = resolveProject(app.db, request.params.name);
|
|
3945
|
+
const row = app.db.select().from(insights).where(eq11(insights.id, request.params.id)).get();
|
|
3946
|
+
if (!row || row.projectId !== project.id) {
|
|
3947
|
+
throw notFound("Insight", request.params.id);
|
|
3948
|
+
}
|
|
3949
|
+
app.db.update(insights).set({ dismissed: true }).where(eq11(insights.id, request.params.id)).run();
|
|
3950
|
+
return reply.send({ ok: true });
|
|
3951
|
+
});
|
|
3952
|
+
app.get("/projects/:name/health/latest", async (request, reply) => {
|
|
3953
|
+
const project = resolveProject(app.db, request.params.name);
|
|
3954
|
+
const row = app.db.select().from(healthSnapshots).where(eq11(healthSnapshots.projectId, project.id)).orderBy(desc4(healthSnapshots.createdAt)).limit(1).get();
|
|
3955
|
+
if (!row) {
|
|
3956
|
+
throw notFound("Health data for project", request.params.name);
|
|
3957
|
+
}
|
|
3958
|
+
return reply.send(mapHealthRow(row));
|
|
3959
|
+
});
|
|
3960
|
+
app.get("/projects/:name/health/history", async (request, reply) => {
|
|
3961
|
+
const project = resolveProject(app.db, request.params.name);
|
|
3962
|
+
const parsed = request.query.limit ? parseInt(request.query.limit, 10) : NaN;
|
|
3963
|
+
const limit = Number.isNaN(parsed) ? 30 : Math.min(Math.max(parsed, 1), 100);
|
|
3964
|
+
const rows = app.db.select().from(healthSnapshots).where(eq11(healthSnapshots.projectId, project.id)).orderBy(desc4(healthSnapshots.createdAt)).limit(limit).all();
|
|
3965
|
+
return reply.send(rows.map(mapHealthRow));
|
|
3966
|
+
});
|
|
3967
|
+
}
|
|
3968
|
+
|
|
3816
3969
|
// ../api-routes/src/openapi.ts
|
|
3817
3970
|
var stringSchema = { type: "string" };
|
|
3818
3971
|
var booleanSchema = { type: "boolean" };
|
|
@@ -5834,10 +5987,83 @@ var routeCatalog = [
|
|
|
5834
5987
|
400: { description: "GA4 is not connected." },
|
|
5835
5988
|
404: { description: "Project not found." }
|
|
5836
5989
|
}
|
|
5990
|
+
},
|
|
5991
|
+
// Intelligence
|
|
5992
|
+
{
|
|
5993
|
+
method: "get",
|
|
5994
|
+
path: "/api/v1/projects/{name}/insights",
|
|
5995
|
+
summary: "List intelligence insights for a project",
|
|
5996
|
+
tags: ["intelligence"],
|
|
5997
|
+
parameters: [
|
|
5998
|
+
nameParameter,
|
|
5999
|
+
{ name: "dismissed", in: "query", description: "Include dismissed insights (true/false).", schema: stringSchema },
|
|
6000
|
+
{ name: "runId", in: "query", description: "Filter by run ID.", schema: stringSchema }
|
|
6001
|
+
],
|
|
6002
|
+
responses: {
|
|
6003
|
+
200: { description: "Insights returned." },
|
|
6004
|
+
404: { description: "Project not found." }
|
|
6005
|
+
}
|
|
6006
|
+
},
|
|
6007
|
+
{
|
|
6008
|
+
method: "get",
|
|
6009
|
+
path: "/api/v1/projects/{name}/insights/{id}",
|
|
6010
|
+
summary: "Get a single insight",
|
|
6011
|
+
tags: ["intelligence"],
|
|
6012
|
+
parameters: [
|
|
6013
|
+
nameParameter,
|
|
6014
|
+
{ name: "id", in: "path", required: true, description: "Insight ID.", schema: stringSchema }
|
|
6015
|
+
],
|
|
6016
|
+
responses: {
|
|
6017
|
+
200: { description: "Insight returned." },
|
|
6018
|
+
404: { description: "Insight not found." }
|
|
6019
|
+
}
|
|
6020
|
+
},
|
|
6021
|
+
{
|
|
6022
|
+
method: "post",
|
|
6023
|
+
path: "/api/v1/projects/{name}/insights/{id}/dismiss",
|
|
6024
|
+
summary: "Dismiss an insight",
|
|
6025
|
+
tags: ["intelligence"],
|
|
6026
|
+
parameters: [
|
|
6027
|
+
nameParameter,
|
|
6028
|
+
{ name: "id", in: "path", required: true, description: "Insight ID.", schema: stringSchema }
|
|
6029
|
+
],
|
|
6030
|
+
responses: {
|
|
6031
|
+
200: { description: "Insight dismissed." },
|
|
6032
|
+
404: { description: "Insight not found." }
|
|
6033
|
+
}
|
|
6034
|
+
},
|
|
6035
|
+
{
|
|
6036
|
+
method: "get",
|
|
6037
|
+
path: "/api/v1/projects/{name}/health/latest",
|
|
6038
|
+
summary: "Get latest health snapshot",
|
|
6039
|
+
tags: ["intelligence"],
|
|
6040
|
+
parameters: [nameParameter],
|
|
6041
|
+
responses: {
|
|
6042
|
+
200: { description: "Health snapshot returned." },
|
|
6043
|
+
404: { description: "Project not found." }
|
|
6044
|
+
}
|
|
6045
|
+
},
|
|
6046
|
+
{
|
|
6047
|
+
method: "get",
|
|
6048
|
+
path: "/api/v1/projects/{name}/health/history",
|
|
6049
|
+
summary: "Get health trend over time",
|
|
6050
|
+
tags: ["intelligence"],
|
|
6051
|
+
parameters: [
|
|
6052
|
+
nameParameter,
|
|
6053
|
+
{ name: "limit", in: "query", description: "Max results.", schema: stringSchema }
|
|
6054
|
+
],
|
|
6055
|
+
responses: {
|
|
6056
|
+
200: { description: "Health history returned." },
|
|
6057
|
+
404: { description: "Project not found." }
|
|
6058
|
+
}
|
|
5837
6059
|
}
|
|
5838
6060
|
];
|
|
5839
6061
|
function buildOpenApiDocument(info = {}) {
|
|
6062
|
+
const BASE_PREFIX = "/api/v1";
|
|
6063
|
+
const prefix = info.routePrefix ?? BASE_PREFIX;
|
|
5840
6064
|
const paths = routeCatalog.reduce((acc, route) => {
|
|
6065
|
+
const subpath = route.path.startsWith(BASE_PREFIX) ? route.path.slice(BASE_PREFIX.length) : route.path;
|
|
6066
|
+
const fullPath = prefix + subpath;
|
|
5841
6067
|
const operation = {
|
|
5842
6068
|
summary: route.summary,
|
|
5843
6069
|
tags: route.tags,
|
|
@@ -5848,9 +6074,9 @@ function buildOpenApiDocument(info = {}) {
|
|
|
5848
6074
|
if (route.parameters) operation.parameters = route.parameters;
|
|
5849
6075
|
if (route.requestBody) operation.requestBody = route.requestBody;
|
|
5850
6076
|
if (route.auth === false) operation.security = [];
|
|
5851
|
-
const pathItem = acc[
|
|
6077
|
+
const pathItem = acc[fullPath] ?? {};
|
|
5852
6078
|
pathItem[route.method] = operation;
|
|
5853
|
-
acc[
|
|
6079
|
+
acc[fullPath] = pathItem;
|
|
5854
6080
|
return acc;
|
|
5855
6081
|
}, {});
|
|
5856
6082
|
return {
|
|
@@ -6080,7 +6306,7 @@ async function telemetryRoutes(app, opts) {
|
|
|
6080
6306
|
|
|
6081
6307
|
// ../api-routes/src/schedules.ts
|
|
6082
6308
|
import crypto11 from "crypto";
|
|
6083
|
-
import { eq as
|
|
6309
|
+
import { eq as eq12 } from "drizzle-orm";
|
|
6084
6310
|
async function scheduleRoutes(app, opts) {
|
|
6085
6311
|
app.put("/projects/:name/schedule", async (request, reply) => {
|
|
6086
6312
|
const project = resolveProject(app.db, request.params.name);
|
|
@@ -6123,7 +6349,7 @@ async function scheduleRoutes(app, opts) {
|
|
|
6123
6349
|
}
|
|
6124
6350
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
6125
6351
|
const enabledInt = enabled === false ? 0 : 1;
|
|
6126
|
-
const existing = app.db.select().from(schedules).where(
|
|
6352
|
+
const existing = app.db.select().from(schedules).where(eq12(schedules.projectId, project.id)).get();
|
|
6127
6353
|
if (existing) {
|
|
6128
6354
|
app.db.update(schedules).set({
|
|
6129
6355
|
cronExpr,
|
|
@@ -6132,7 +6358,7 @@ async function scheduleRoutes(app, opts) {
|
|
|
6132
6358
|
providers: JSON.stringify(providers),
|
|
6133
6359
|
enabled: enabledInt,
|
|
6134
6360
|
updatedAt: now
|
|
6135
|
-
}).where(
|
|
6361
|
+
}).where(eq12(schedules.id, existing.id)).run();
|
|
6136
6362
|
} else {
|
|
6137
6363
|
app.db.insert(schedules).values({
|
|
6138
6364
|
id: crypto11.randomUUID(),
|
|
@@ -6154,12 +6380,12 @@ async function scheduleRoutes(app, opts) {
|
|
|
6154
6380
|
diff: { cronExpr, preset, timezone, providers }
|
|
6155
6381
|
});
|
|
6156
6382
|
opts.onScheduleUpdated?.("upsert", project.id);
|
|
6157
|
-
const schedule = app.db.select().from(schedules).where(
|
|
6383
|
+
const schedule = app.db.select().from(schedules).where(eq12(schedules.projectId, project.id)).get();
|
|
6158
6384
|
return reply.status(existing ? 200 : 201).send(formatSchedule(schedule));
|
|
6159
6385
|
});
|
|
6160
6386
|
app.get("/projects/:name/schedule", async (request, reply) => {
|
|
6161
6387
|
const project = resolveProject(app.db, request.params.name);
|
|
6162
|
-
const schedule = app.db.select().from(schedules).where(
|
|
6388
|
+
const schedule = app.db.select().from(schedules).where(eq12(schedules.projectId, project.id)).get();
|
|
6163
6389
|
if (!schedule) {
|
|
6164
6390
|
throw notFound("Schedule", request.params.name);
|
|
6165
6391
|
}
|
|
@@ -6167,11 +6393,11 @@ async function scheduleRoutes(app, opts) {
|
|
|
6167
6393
|
});
|
|
6168
6394
|
app.delete("/projects/:name/schedule", async (request, reply) => {
|
|
6169
6395
|
const project = resolveProject(app.db, request.params.name);
|
|
6170
|
-
const schedule = app.db.select().from(schedules).where(
|
|
6396
|
+
const schedule = app.db.select().from(schedules).where(eq12(schedules.projectId, project.id)).get();
|
|
6171
6397
|
if (!schedule) {
|
|
6172
6398
|
throw notFound("Schedule", request.params.name);
|
|
6173
6399
|
}
|
|
6174
|
-
app.db.delete(schedules).where(
|
|
6400
|
+
app.db.delete(schedules).where(eq12(schedules.id, schedule.id)).run();
|
|
6175
6401
|
writeAuditLog(app.db, {
|
|
6176
6402
|
projectId: project.id,
|
|
6177
6403
|
actor: "api",
|
|
@@ -6201,7 +6427,7 @@ function formatSchedule(row) {
|
|
|
6201
6427
|
|
|
6202
6428
|
// ../api-routes/src/notifications.ts
|
|
6203
6429
|
import crypto12 from "crypto";
|
|
6204
|
-
import { eq as
|
|
6430
|
+
import { eq as eq13 } from "drizzle-orm";
|
|
6205
6431
|
var VALID_EVENTS = ["citation.lost", "citation.gained", "run.completed", "run.failed"];
|
|
6206
6432
|
async function notificationRoutes(app) {
|
|
6207
6433
|
app.get("/notifications/events", async (_request, reply) => {
|
|
@@ -6240,22 +6466,22 @@ async function notificationRoutes(app) {
|
|
|
6240
6466
|
diff: { channel, ...redactNotificationUrl(url), events }
|
|
6241
6467
|
});
|
|
6242
6468
|
return reply.status(201).send({
|
|
6243
|
-
...formatNotification(app.db.select().from(notifications).where(
|
|
6469
|
+
...formatNotification(app.db.select().from(notifications).where(eq13(notifications.id, id)).get()),
|
|
6244
6470
|
webhookSecret
|
|
6245
6471
|
});
|
|
6246
6472
|
});
|
|
6247
6473
|
app.get("/projects/:name/notifications", async (request, reply) => {
|
|
6248
6474
|
const project = resolveProject(app.db, request.params.name);
|
|
6249
|
-
const rows = app.db.select().from(notifications).where(
|
|
6475
|
+
const rows = app.db.select().from(notifications).where(eq13(notifications.projectId, project.id)).all();
|
|
6250
6476
|
return reply.send(rows.map(formatNotification));
|
|
6251
6477
|
});
|
|
6252
6478
|
app.delete("/projects/:name/notifications/:id", async (request, reply) => {
|
|
6253
6479
|
const project = resolveProject(app.db, request.params.name);
|
|
6254
|
-
const notification = app.db.select().from(notifications).where(
|
|
6480
|
+
const notification = app.db.select().from(notifications).where(eq13(notifications.id, request.params.id)).get();
|
|
6255
6481
|
if (!notification || notification.projectId !== project.id) {
|
|
6256
6482
|
throw notFound("Notification", request.params.id);
|
|
6257
6483
|
}
|
|
6258
|
-
app.db.delete(notifications).where(
|
|
6484
|
+
app.db.delete(notifications).where(eq13(notifications.id, notification.id)).run();
|
|
6259
6485
|
writeAuditLog(app.db, {
|
|
6260
6486
|
projectId: project.id,
|
|
6261
6487
|
actor: "api",
|
|
@@ -6267,7 +6493,7 @@ async function notificationRoutes(app) {
|
|
|
6267
6493
|
});
|
|
6268
6494
|
app.post("/projects/:name/notifications/:id/test", async (request, reply) => {
|
|
6269
6495
|
const project = resolveProject(app.db, request.params.name);
|
|
6270
|
-
const notification = app.db.select().from(notifications).where(
|
|
6496
|
+
const notification = app.db.select().from(notifications).where(eq13(notifications.id, request.params.id)).get();
|
|
6271
6497
|
if (!notification || notification.projectId !== project.id) {
|
|
6272
6498
|
throw notFound("Notification", request.params.id);
|
|
6273
6499
|
}
|
|
@@ -6319,7 +6545,7 @@ function formatNotification(row) {
|
|
|
6319
6545
|
|
|
6320
6546
|
// ../api-routes/src/google.ts
|
|
6321
6547
|
import crypto14 from "crypto";
|
|
6322
|
-
import { eq as
|
|
6548
|
+
import { eq as eq14, and as and3, desc as desc5, sql as sql3 } from "drizzle-orm";
|
|
6323
6549
|
|
|
6324
6550
|
// ../integration-google/src/constants.ts
|
|
6325
6551
|
var GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
@@ -6351,7 +6577,56 @@ var GoogleApiError = class extends Error {
|
|
|
6351
6577
|
};
|
|
6352
6578
|
|
|
6353
6579
|
// ../integration-google/src/oauth.ts
|
|
6580
|
+
function validateClientId(clientId) {
|
|
6581
|
+
if (!clientId || typeof clientId !== "string" || clientId.trim().length === 0) {
|
|
6582
|
+
throw new GoogleAuthError("Client ID is required and must be a non-empty string");
|
|
6583
|
+
}
|
|
6584
|
+
}
|
|
6585
|
+
function validateClientSecret(clientSecret) {
|
|
6586
|
+
if (!clientSecret || typeof clientSecret !== "string" || clientSecret.trim().length === 0) {
|
|
6587
|
+
throw new GoogleAuthError("Client secret is required and must be a non-empty string");
|
|
6588
|
+
}
|
|
6589
|
+
}
|
|
6590
|
+
function validateRedirectUri(redirectUri) {
|
|
6591
|
+
if (!redirectUri || typeof redirectUri !== "string" || redirectUri.trim().length === 0) {
|
|
6592
|
+
throw new GoogleAuthError("Redirect URI is required and must be a non-empty string");
|
|
6593
|
+
}
|
|
6594
|
+
try {
|
|
6595
|
+
const url = new URL(redirectUri);
|
|
6596
|
+
if (!url.protocol.startsWith("http")) {
|
|
6597
|
+
throw new GoogleAuthError("Redirect URI must be an HTTP or HTTPS URL");
|
|
6598
|
+
}
|
|
6599
|
+
} catch {
|
|
6600
|
+
throw new GoogleAuthError("Redirect URI must be a valid URL");
|
|
6601
|
+
}
|
|
6602
|
+
}
|
|
6603
|
+
function validateCode(code) {
|
|
6604
|
+
if (!code || typeof code !== "string" || code.trim().length === 0) {
|
|
6605
|
+
throw new GoogleAuthError("Authorization code is required and must be a non-empty string");
|
|
6606
|
+
}
|
|
6607
|
+
}
|
|
6608
|
+
function validateScopes(scopes) {
|
|
6609
|
+
if (!Array.isArray(scopes) || scopes.length === 0) {
|
|
6610
|
+
throw new GoogleAuthError("At least one scope is required");
|
|
6611
|
+
}
|
|
6612
|
+
for (const scope of scopes) {
|
|
6613
|
+
if (!scope || typeof scope !== "string" || scope.trim().length === 0) {
|
|
6614
|
+
throw new GoogleAuthError("Scope must be a non-empty string");
|
|
6615
|
+
}
|
|
6616
|
+
}
|
|
6617
|
+
}
|
|
6618
|
+
function validateRefreshToken(refreshToken) {
|
|
6619
|
+
if (!refreshToken || typeof refreshToken !== "string" || refreshToken.trim().length === 0) {
|
|
6620
|
+
throw new GoogleAuthError("Refresh token is required and must be a non-empty string");
|
|
6621
|
+
}
|
|
6622
|
+
}
|
|
6354
6623
|
function getAuthUrl(clientId, redirectUri, scopes, state) {
|
|
6624
|
+
validateClientId(clientId);
|
|
6625
|
+
validateRedirectUri(redirectUri);
|
|
6626
|
+
validateScopes(scopes);
|
|
6627
|
+
if (state && (typeof state !== "string" || state.trim().length === 0)) {
|
|
6628
|
+
throw new GoogleAuthError("State must be a non-empty string if provided");
|
|
6629
|
+
}
|
|
6355
6630
|
const params = new URLSearchParams({
|
|
6356
6631
|
client_id: clientId,
|
|
6357
6632
|
redirect_uri: redirectUri,
|
|
@@ -6364,6 +6639,10 @@ function getAuthUrl(clientId, redirectUri, scopes, state) {
|
|
|
6364
6639
|
return `${GOOGLE_AUTH_URL}?${params.toString()}`;
|
|
6365
6640
|
}
|
|
6366
6641
|
async function exchangeCode(clientId, clientSecret, code, redirectUri) {
|
|
6642
|
+
validateClientId(clientId);
|
|
6643
|
+
validateClientSecret(clientSecret);
|
|
6644
|
+
validateCode(code);
|
|
6645
|
+
validateRedirectUri(redirectUri);
|
|
6367
6646
|
const res = await fetch(GOOGLE_TOKEN_URL, {
|
|
6368
6647
|
method: "POST",
|
|
6369
6648
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
@@ -6383,6 +6662,9 @@ async function exchangeCode(clientId, clientSecret, code, redirectUri) {
|
|
|
6383
6662
|
return await res.json();
|
|
6384
6663
|
}
|
|
6385
6664
|
async function refreshAccessToken(clientId, clientSecret, currentRefreshToken) {
|
|
6665
|
+
validateClientId(clientId);
|
|
6666
|
+
validateClientSecret(clientSecret);
|
|
6667
|
+
validateRefreshToken(currentRefreshToken);
|
|
6386
6668
|
const res = await fetch(GOOGLE_TOKEN_URL, {
|
|
6387
6669
|
method: "POST",
|
|
6388
6670
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
@@ -6402,6 +6684,47 @@ async function refreshAccessToken(clientId, clientSecret, currentRefreshToken) {
|
|
|
6402
6684
|
}
|
|
6403
6685
|
|
|
6404
6686
|
// ../integration-google/src/gsc-client.ts
|
|
6687
|
+
function validateAccessToken(accessToken) {
|
|
6688
|
+
if (!accessToken || typeof accessToken !== "string" || accessToken.trim().length === 0) {
|
|
6689
|
+
throw new GoogleApiError("Access token is required and must be a non-empty string", 400);
|
|
6690
|
+
}
|
|
6691
|
+
}
|
|
6692
|
+
function validateSiteUrl(siteUrl) {
|
|
6693
|
+
if (!siteUrl || typeof siteUrl !== "string" || siteUrl.trim().length === 0) {
|
|
6694
|
+
throw new GoogleApiError("Site URL is required and must be a non-empty string", 400);
|
|
6695
|
+
}
|
|
6696
|
+
if (siteUrl.startsWith("sc-domain:")) {
|
|
6697
|
+
const domain = siteUrl.slice("sc-domain:".length);
|
|
6698
|
+
if (!domain) {
|
|
6699
|
+
throw new GoogleApiError("Site URL sc-domain must include a domain", 400);
|
|
6700
|
+
}
|
|
6701
|
+
if (!domain.includes(".")) {
|
|
6702
|
+
throw new GoogleApiError("Site URL sc-domain must be a valid domain", 400);
|
|
6703
|
+
}
|
|
6704
|
+
} else {
|
|
6705
|
+
try {
|
|
6706
|
+
const url = new URL(siteUrl);
|
|
6707
|
+
if (!url.protocol.startsWith("http")) {
|
|
6708
|
+
throw new GoogleApiError("Site URL must be an HTTP or HTTPS URL", 400);
|
|
6709
|
+
}
|
|
6710
|
+
} catch {
|
|
6711
|
+
throw new GoogleApiError("Site URL must be a valid URL", 400);
|
|
6712
|
+
}
|
|
6713
|
+
}
|
|
6714
|
+
}
|
|
6715
|
+
function validateUrl(urlParam) {
|
|
6716
|
+
if (!urlParam || typeof urlParam !== "string" || urlParam.trim().length === 0) {
|
|
6717
|
+
throw new GoogleApiError("URL is required and must be a non-empty string", 400);
|
|
6718
|
+
}
|
|
6719
|
+
try {
|
|
6720
|
+
const url = new URL(urlParam);
|
|
6721
|
+
if (!url.protocol.startsWith("http")) {
|
|
6722
|
+
throw new GoogleApiError("URL must be an HTTP or HTTPS URL", 400);
|
|
6723
|
+
}
|
|
6724
|
+
} catch {
|
|
6725
|
+
throw new GoogleApiError("URL must be a valid URL", 400);
|
|
6726
|
+
}
|
|
6727
|
+
}
|
|
6405
6728
|
function gscClientLog(level, action, ctx) {
|
|
6406
6729
|
const entry = { ts: (/* @__PURE__ */ new Date()).toISOString(), level, module: "GscClient", action, ...ctx };
|
|
6407
6730
|
const stream = level === "error" ? process.stderr : process.stdout;
|
|
@@ -6437,6 +6760,7 @@ async function gscFetch(accessToken, url, opts) {
|
|
|
6437
6760
|
return await res.json();
|
|
6438
6761
|
}
|
|
6439
6762
|
async function listSites(accessToken) {
|
|
6763
|
+
validateAccessToken(accessToken);
|
|
6440
6764
|
const data = await gscFetch(
|
|
6441
6765
|
accessToken,
|
|
6442
6766
|
`${GSC_API_BASE}/sites`
|
|
@@ -6444,6 +6768,8 @@ async function listSites(accessToken) {
|
|
|
6444
6768
|
return data.siteEntry ?? [];
|
|
6445
6769
|
}
|
|
6446
6770
|
async function listSitemaps(accessToken, siteUrl) {
|
|
6771
|
+
validateAccessToken(accessToken);
|
|
6772
|
+
validateSiteUrl(siteUrl);
|
|
6447
6773
|
const encodedSiteUrl = encodeURIComponent(siteUrl);
|
|
6448
6774
|
const data = await gscFetch(
|
|
6449
6775
|
accessToken,
|
|
@@ -6452,6 +6778,8 @@ async function listSitemaps(accessToken, siteUrl) {
|
|
|
6452
6778
|
return data.sitemap ?? [];
|
|
6453
6779
|
}
|
|
6454
6780
|
async function fetchSearchAnalytics(accessToken, siteUrl, opts) {
|
|
6781
|
+
validateAccessToken(accessToken);
|
|
6782
|
+
validateSiteUrl(siteUrl);
|
|
6455
6783
|
const allRows = [];
|
|
6456
6784
|
let startRow = 0;
|
|
6457
6785
|
const dimensions = opts.dimensions ?? ["query", "page", "country", "device", "date"];
|
|
@@ -6487,6 +6815,8 @@ async function fetchSearchAnalytics(accessToken, siteUrl, opts) {
|
|
|
6487
6815
|
return allRows;
|
|
6488
6816
|
}
|
|
6489
6817
|
async function publishUrlNotification(accessToken, url, type = "URL_UPDATED") {
|
|
6818
|
+
validateAccessToken(accessToken);
|
|
6819
|
+
validateUrl(url);
|
|
6490
6820
|
return gscFetch(
|
|
6491
6821
|
accessToken,
|
|
6492
6822
|
`${INDEXING_API_BASE}/urlNotifications:publish`,
|
|
@@ -6497,6 +6827,9 @@ async function publishUrlNotification(accessToken, url, type = "URL_UPDATED") {
|
|
|
6497
6827
|
);
|
|
6498
6828
|
}
|
|
6499
6829
|
async function inspectUrl(accessToken, inspectionUrl, siteUrl) {
|
|
6830
|
+
validateAccessToken(accessToken);
|
|
6831
|
+
validateUrl(inspectionUrl);
|
|
6832
|
+
validateSiteUrl(siteUrl);
|
|
6500
6833
|
return gscFetch(
|
|
6501
6834
|
accessToken,
|
|
6502
6835
|
URL_INSPECTION_API,
|
|
@@ -6532,12 +6865,46 @@ var GA4ApiError = class extends Error {
|
|
|
6532
6865
|
};
|
|
6533
6866
|
|
|
6534
6867
|
// ../integration-google-analytics/src/ga4-client.ts
|
|
6868
|
+
function validateClientEmail(clientEmail) {
|
|
6869
|
+
if (!clientEmail || typeof clientEmail !== "string" || clientEmail.trim().length === 0) {
|
|
6870
|
+
throw new GA4ApiError("Client email is required and must be a non-empty string", 400);
|
|
6871
|
+
}
|
|
6872
|
+
if (!clientEmail.includes("@")) {
|
|
6873
|
+
throw new GA4ApiError("Client email must be a valid email address", 400);
|
|
6874
|
+
}
|
|
6875
|
+
}
|
|
6876
|
+
function validatePrivateKey(privateKey) {
|
|
6877
|
+
if (!privateKey || typeof privateKey !== "string" || privateKey.trim().length === 0) {
|
|
6878
|
+
throw new GA4ApiError("Private key is required and must be a non-empty string", 400);
|
|
6879
|
+
}
|
|
6880
|
+
}
|
|
6881
|
+
function validatePropertyId(propertyId) {
|
|
6882
|
+
if (!propertyId || typeof propertyId !== "string" || propertyId.trim().length === 0) {
|
|
6883
|
+
throw new GA4ApiError("Property ID is required and must be a non-empty string", 400);
|
|
6884
|
+
}
|
|
6885
|
+
if (!/^\d+$/.test(propertyId)) {
|
|
6886
|
+
throw new GA4ApiError("Property ID must be a numeric string", 400);
|
|
6887
|
+
}
|
|
6888
|
+
}
|
|
6889
|
+
function validateAccessToken2(accessToken) {
|
|
6890
|
+
if (!accessToken || typeof accessToken !== "string" || accessToken.trim().length === 0) {
|
|
6891
|
+
throw new GA4ApiError("Access token is required and must be a non-empty string", 400);
|
|
6892
|
+
}
|
|
6893
|
+
}
|
|
6894
|
+
function validateScope(scope) {
|
|
6895
|
+
if (!scope || typeof scope !== "string" || scope.trim().length === 0) {
|
|
6896
|
+
throw new GA4ApiError("Scope is required and must be a non-empty string", 400);
|
|
6897
|
+
}
|
|
6898
|
+
}
|
|
6535
6899
|
function ga4Log(level, action, ctx) {
|
|
6536
6900
|
const entry = { ts: (/* @__PURE__ */ new Date()).toISOString(), level, module: "GA4Client", action, ...ctx };
|
|
6537
6901
|
const stream = level === "error" ? process.stderr : process.stdout;
|
|
6538
6902
|
stream.write(JSON.stringify(entry) + "\n");
|
|
6539
6903
|
}
|
|
6540
6904
|
function createServiceAccountJwt(clientEmail, privateKey, scope) {
|
|
6905
|
+
validateClientEmail(clientEmail);
|
|
6906
|
+
validatePrivateKey(privateKey);
|
|
6907
|
+
validateScope(scope);
|
|
6541
6908
|
const now = Math.floor(Date.now() / 1e3);
|
|
6542
6909
|
const header = { alg: "RS256", typ: "JWT" };
|
|
6543
6910
|
const payload = {
|
|
@@ -6664,6 +7031,8 @@ var AI_REFERRAL_SOURCE_FILTERS = [
|
|
|
6664
7031
|
{ matchType: "CONTAINS", value: "meta.ai" }
|
|
6665
7032
|
];
|
|
6666
7033
|
async function fetchTrafficByLandingPage(accessToken, propertyId, days) {
|
|
7034
|
+
validateAccessToken2(accessToken);
|
|
7035
|
+
validatePropertyId(propertyId);
|
|
6667
7036
|
const syncDays = Math.min(Math.max(1, days ?? GA4_DEFAULT_SYNC_DAYS), GA4_MAX_SYNC_DAYS);
|
|
6668
7037
|
const endDate = /* @__PURE__ */ new Date();
|
|
6669
7038
|
const startDate = /* @__PURE__ */ new Date();
|
|
@@ -6738,10 +7107,15 @@ async function fetchTrafficByLandingPage(accessToken, propertyId, days) {
|
|
|
6738
7107
|
return rows;
|
|
6739
7108
|
}
|
|
6740
7109
|
async function verifyConnection(clientEmail, privateKey, propertyId) {
|
|
7110
|
+
validateClientEmail(clientEmail);
|
|
7111
|
+
validatePrivateKey(privateKey);
|
|
7112
|
+
validatePropertyId(propertyId);
|
|
6741
7113
|
const accessToken = await getAccessToken(clientEmail, privateKey);
|
|
6742
7114
|
return verifyConnectionWithToken(accessToken, propertyId);
|
|
6743
7115
|
}
|
|
6744
7116
|
async function verifyConnectionWithToken(accessToken, propertyId) {
|
|
7117
|
+
validateAccessToken2(accessToken);
|
|
7118
|
+
validatePropertyId(propertyId);
|
|
6745
7119
|
const endDate = /* @__PURE__ */ new Date();
|
|
6746
7120
|
const startDate = /* @__PURE__ */ new Date();
|
|
6747
7121
|
startDate.setDate(startDate.getDate() - 1);
|
|
@@ -6754,6 +7128,8 @@ async function verifyConnectionWithToken(accessToken, propertyId) {
|
|
|
6754
7128
|
return true;
|
|
6755
7129
|
}
|
|
6756
7130
|
async function fetchAggregateSummary(accessToken, propertyId, days) {
|
|
7131
|
+
validateAccessToken2(accessToken);
|
|
7132
|
+
validatePropertyId(propertyId);
|
|
6757
7133
|
const syncDays = Math.min(Math.max(1, days ?? GA4_DEFAULT_SYNC_DAYS), GA4_MAX_SYNC_DAYS);
|
|
6758
7134
|
const endDate = /* @__PURE__ */ new Date();
|
|
6759
7135
|
const startDate = /* @__PURE__ */ new Date();
|
|
@@ -6793,6 +7169,8 @@ async function fetchAggregateSummary(accessToken, propertyId, days) {
|
|
|
6793
7169
|
return summary;
|
|
6794
7170
|
}
|
|
6795
7171
|
async function fetchAiReferrals(accessToken, propertyId, days) {
|
|
7172
|
+
validateAccessToken2(accessToken);
|
|
7173
|
+
validatePropertyId(propertyId);
|
|
6796
7174
|
const syncDays = Math.min(Math.max(1, days ?? GA4_DEFAULT_SYNC_DAYS), GA4_MAX_SYNC_DAYS);
|
|
6797
7175
|
const endDate = /* @__PURE__ */ new Date();
|
|
6798
7176
|
const startDate = /* @__PURE__ */ new Date();
|
|
@@ -6959,13 +7337,13 @@ async function googleRoutes(app, opts) {
|
|
|
6959
7337
|
const project = resolveProject(app.db, request.params.name);
|
|
6960
7338
|
let redirectUri;
|
|
6961
7339
|
if (publicUrl) {
|
|
6962
|
-
redirectUri = publicUrl.replace(/\/$/, "") + "/api/v1/google/callback";
|
|
7340
|
+
redirectUri = publicUrl.replace(/\/$/, "") + (opts.routePrefix ?? "/api/v1") + "/google/callback";
|
|
6963
7341
|
} else if (opts.publicUrl) {
|
|
6964
|
-
redirectUri = opts.publicUrl.replace(/\/$/, "") + "/api/v1/google/callback";
|
|
7342
|
+
redirectUri = opts.publicUrl.replace(/\/$/, "") + (opts.routePrefix ?? "/api/v1") + "/google/callback";
|
|
6965
7343
|
} else {
|
|
6966
7344
|
const proto = request.headers["x-forwarded-proto"] ?? "http";
|
|
6967
7345
|
const host = request.headers.host ?? "localhost:4100";
|
|
6968
|
-
redirectUri = `${proto}://${host}/api/v1/projects/${encodeURIComponent(request.params.name)}/google/callback`;
|
|
7346
|
+
redirectUri = `${proto}://${host}${opts.routePrefix ?? "/api/v1"}/projects/${encodeURIComponent(request.params.name)}/google/callback`;
|
|
6969
7347
|
}
|
|
6970
7348
|
const scopes = type === "gsc" ? [GSC_SCOPE, INDEXING_SCOPE] : [GA4_SCOPE];
|
|
6971
7349
|
const stateEncoded = buildSignedState(
|
|
@@ -7124,18 +7502,18 @@ async function googleRoutes(app, opts) {
|
|
|
7124
7502
|
if (opts.onGscSyncRequested) {
|
|
7125
7503
|
opts.onGscSyncRequested(runId, project.id, { days, full });
|
|
7126
7504
|
}
|
|
7127
|
-
const run = app.db.select().from(runs).where(
|
|
7505
|
+
const run = app.db.select().from(runs).where(eq14(runs.id, runId)).get();
|
|
7128
7506
|
return run;
|
|
7129
7507
|
});
|
|
7130
7508
|
app.get("/projects/:name/google/gsc/performance", async (request) => {
|
|
7131
7509
|
const project = resolveProject(app.db, request.params.name);
|
|
7132
7510
|
const { startDate, endDate, query, page, limit } = request.query;
|
|
7133
|
-
const conditions = [
|
|
7511
|
+
const conditions = [eq14(gscSearchData.projectId, project.id)];
|
|
7134
7512
|
if (startDate) conditions.push(sql3`${gscSearchData.date} >= ${startDate}`);
|
|
7135
7513
|
if (endDate) conditions.push(sql3`${gscSearchData.date} <= ${endDate}`);
|
|
7136
7514
|
if (query) conditions.push(sql3`${gscSearchData.query} LIKE ${"%" + query + "%"}`);
|
|
7137
7515
|
if (page) conditions.push(sql3`${gscSearchData.page} LIKE ${"%" + page + "%"}`);
|
|
7138
|
-
const rows = app.db.select().from(gscSearchData).where(
|
|
7516
|
+
const rows = app.db.select().from(gscSearchData).where(and3(...conditions)).orderBy(desc5(gscSearchData.date)).limit(parseInt(limit ?? "500", 10)).all();
|
|
7139
7517
|
return rows.map((r) => ({
|
|
7140
7518
|
date: r.date,
|
|
7141
7519
|
query: r.query,
|
|
@@ -7211,9 +7589,9 @@ async function googleRoutes(app, opts) {
|
|
|
7211
7589
|
app.get("/projects/:name/google/gsc/inspections", async (request) => {
|
|
7212
7590
|
const project = resolveProject(app.db, request.params.name);
|
|
7213
7591
|
const { url, limit } = request.query;
|
|
7214
|
-
const conditions = [
|
|
7215
|
-
if (url) conditions.push(
|
|
7216
|
-
const rows = app.db.select().from(gscUrlInspections).where(
|
|
7592
|
+
const conditions = [eq14(gscUrlInspections.projectId, project.id)];
|
|
7593
|
+
if (url) conditions.push(eq14(gscUrlInspections.url, url));
|
|
7594
|
+
const rows = app.db.select().from(gscUrlInspections).where(and3(...conditions)).orderBy(desc5(gscUrlInspections.inspectedAt)).limit(parseInt(limit ?? "100", 10)).all();
|
|
7217
7595
|
return rows.map((r) => ({
|
|
7218
7596
|
id: r.id,
|
|
7219
7597
|
url: r.url,
|
|
@@ -7232,7 +7610,7 @@ async function googleRoutes(app, opts) {
|
|
|
7232
7610
|
});
|
|
7233
7611
|
app.get("/projects/:name/google/gsc/deindexed", async (request) => {
|
|
7234
7612
|
const project = resolveProject(app.db, request.params.name);
|
|
7235
|
-
const allInspections = app.db.select().from(gscUrlInspections).where(
|
|
7613
|
+
const allInspections = app.db.select().from(gscUrlInspections).where(eq14(gscUrlInspections.projectId, project.id)).orderBy(desc5(gscUrlInspections.inspectedAt)).all();
|
|
7236
7614
|
const byUrl = /* @__PURE__ */ new Map();
|
|
7237
7615
|
for (const row of allInspections) {
|
|
7238
7616
|
const existing = byUrl.get(row.url);
|
|
@@ -7260,7 +7638,7 @@ async function googleRoutes(app, opts) {
|
|
|
7260
7638
|
});
|
|
7261
7639
|
app.get("/projects/:name/google/gsc/coverage", async (request) => {
|
|
7262
7640
|
const project = resolveProject(app.db, request.params.name);
|
|
7263
|
-
const allInspections = app.db.select().from(gscUrlInspections).where(
|
|
7641
|
+
const allInspections = app.db.select().from(gscUrlInspections).where(eq14(gscUrlInspections.projectId, project.id)).orderBy(desc5(gscUrlInspections.inspectedAt)).all();
|
|
7264
7642
|
const canonicalUrl = (url) => url.replace(/^http:\/\//, "https://");
|
|
7265
7643
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
7266
7644
|
const historyByUrl = /* @__PURE__ */ new Map();
|
|
@@ -7357,7 +7735,7 @@ async function googleRoutes(app, opts) {
|
|
|
7357
7735
|
const project = resolveProject(app.db, request.params.name);
|
|
7358
7736
|
const parsed = parseInt(request.query.limit ?? "90", 10);
|
|
7359
7737
|
const limit = Number.isNaN(parsed) || parsed <= 0 ? 90 : parsed;
|
|
7360
|
-
const rows = app.db.select().from(gscCoverageSnapshots).where(
|
|
7738
|
+
const rows = app.db.select().from(gscCoverageSnapshots).where(eq14(gscCoverageSnapshots.projectId, project.id)).orderBy(desc5(gscCoverageSnapshots.date)).limit(limit).all();
|
|
7361
7739
|
return rows.map((r) => ({
|
|
7362
7740
|
date: r.date,
|
|
7363
7741
|
indexed: r.indexed,
|
|
@@ -7425,7 +7803,7 @@ async function googleRoutes(app, opts) {
|
|
|
7425
7803
|
if (opts.onInspectSitemapRequested) {
|
|
7426
7804
|
opts.onInspectSitemapRequested(runId, project.id, { sitemapUrl });
|
|
7427
7805
|
}
|
|
7428
|
-
const run = app.db.select().from(runs).where(
|
|
7806
|
+
const run = app.db.select().from(runs).where(eq14(runs.id, runId)).get();
|
|
7429
7807
|
return { sitemaps, primarySitemapUrl: sitemapUrl, run };
|
|
7430
7808
|
});
|
|
7431
7809
|
app.post("/projects/:name/google/gsc/inspect-sitemap", async (request, reply) => {
|
|
@@ -7455,7 +7833,7 @@ async function googleRoutes(app, opts) {
|
|
|
7455
7833
|
if (opts.onInspectSitemapRequested) {
|
|
7456
7834
|
opts.onInspectSitemapRequested(runId, project.id, { sitemapUrl: sitemapUrl ?? void 0 });
|
|
7457
7835
|
}
|
|
7458
|
-
const run = app.db.select().from(runs).where(
|
|
7836
|
+
const run = app.db.select().from(runs).where(eq14(runs.id, runId)).get();
|
|
7459
7837
|
return run;
|
|
7460
7838
|
});
|
|
7461
7839
|
app.put("/projects/:name/google/connections/:type/sitemap", async (request, reply) => {
|
|
@@ -7510,7 +7888,7 @@ async function googleRoutes(app, opts) {
|
|
|
7510
7888
|
const { accessToken } = await getValidToken(store, project.canonicalDomain, "gsc", googleClientId, googleClientSecret);
|
|
7511
7889
|
let urlsToNotify = request.body?.urls ?? [];
|
|
7512
7890
|
if (request.body?.allUnindexed) {
|
|
7513
|
-
const allInspections = app.db.select().from(gscUrlInspections).where(
|
|
7891
|
+
const allInspections = app.db.select().from(gscUrlInspections).where(eq14(gscUrlInspections.projectId, project.id)).orderBy(desc5(gscUrlInspections.inspectedAt)).all();
|
|
7514
7892
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
7515
7893
|
for (const row of allInspections) {
|
|
7516
7894
|
if (!latestByUrl.has(row.url)) {
|
|
@@ -7585,7 +7963,7 @@ async function googleRoutes(app, opts) {
|
|
|
7585
7963
|
|
|
7586
7964
|
// ../api-routes/src/bing.ts
|
|
7587
7965
|
import crypto15 from "crypto";
|
|
7588
|
-
import { eq as
|
|
7966
|
+
import { eq as eq15, and as and4, desc as desc6 } from "drizzle-orm";
|
|
7589
7967
|
|
|
7590
7968
|
// ../integration-bing/src/constants.ts
|
|
7591
7969
|
var BING_WMT_API_BASE = "https://ssl.bing.com/webmaster/api.svc/json";
|
|
@@ -7604,6 +7982,45 @@ var BingApiError = class extends Error {
|
|
|
7604
7982
|
};
|
|
7605
7983
|
|
|
7606
7984
|
// ../integration-bing/src/bing-client.ts
|
|
7985
|
+
function validateApiKey(apiKey) {
|
|
7986
|
+
if (!apiKey || typeof apiKey !== "string" || apiKey.trim().length === 0) {
|
|
7987
|
+
throw new BingApiError("API key is required and must be a non-empty string", 400);
|
|
7988
|
+
}
|
|
7989
|
+
}
|
|
7990
|
+
function validateSiteUrl2(siteUrl) {
|
|
7991
|
+
if (!siteUrl || typeof siteUrl !== "string" || siteUrl.trim().length === 0) {
|
|
7992
|
+
throw new BingApiError("Site URL is required and must be a non-empty string", 400);
|
|
7993
|
+
}
|
|
7994
|
+
try {
|
|
7995
|
+
const url = new URL(siteUrl);
|
|
7996
|
+
if (!url.protocol.startsWith("http")) {
|
|
7997
|
+
throw new BingApiError("Site URL must be an HTTP or HTTPS URL", 400);
|
|
7998
|
+
}
|
|
7999
|
+
} catch {
|
|
8000
|
+
throw new BingApiError("Site URL must be a valid URL", 400);
|
|
8001
|
+
}
|
|
8002
|
+
}
|
|
8003
|
+
function validateUrl2(urlParam) {
|
|
8004
|
+
if (!urlParam || typeof urlParam !== "string" || urlParam.trim().length === 0) {
|
|
8005
|
+
throw new BingApiError("URL is required and must be a non-empty string", 400);
|
|
8006
|
+
}
|
|
8007
|
+
try {
|
|
8008
|
+
const url = new URL(urlParam);
|
|
8009
|
+
if (!url.protocol.startsWith("http")) {
|
|
8010
|
+
throw new BingApiError("URL must be an HTTP or HTTPS URL", 400);
|
|
8011
|
+
}
|
|
8012
|
+
} catch {
|
|
8013
|
+
throw new BingApiError("URL must be a valid URL", 400);
|
|
8014
|
+
}
|
|
8015
|
+
}
|
|
8016
|
+
function validateUrls(urls) {
|
|
8017
|
+
if (!Array.isArray(urls)) {
|
|
8018
|
+
throw new BingApiError("URLs must be an array", 400);
|
|
8019
|
+
}
|
|
8020
|
+
for (const url of urls) {
|
|
8021
|
+
validateUrl2(url);
|
|
8022
|
+
}
|
|
8023
|
+
}
|
|
7607
8024
|
function bingClientLog(level, action, ctx) {
|
|
7608
8025
|
const entry = { ts: (/* @__PURE__ */ new Date()).toISOString(), level, module: "BingClient", action, ...ctx };
|
|
7609
8026
|
const stream = level === "error" ? process.stderr : process.stdout;
|
|
@@ -7652,21 +8069,31 @@ async function bingFetch(apiKey, endpoint, opts) {
|
|
|
7652
8069
|
}
|
|
7653
8070
|
}
|
|
7654
8071
|
async function getSites(apiKey) {
|
|
8072
|
+
validateApiKey(apiKey);
|
|
7655
8073
|
const data = await bingFetch(apiKey, "GetUserSites");
|
|
7656
8074
|
return data ?? [];
|
|
7657
8075
|
}
|
|
7658
8076
|
async function getUrlInfo(apiKey, siteUrl, url) {
|
|
8077
|
+
validateApiKey(apiKey);
|
|
8078
|
+
validateSiteUrl2(siteUrl);
|
|
8079
|
+
validateUrl2(url);
|
|
7659
8080
|
const encodedSite = encodeURIComponent(siteUrl);
|
|
7660
8081
|
const encodedUrl = encodeURIComponent(url);
|
|
7661
8082
|
return bingFetch(apiKey, `GetUrlInfo?siteUrl=${encodedSite}&url=${encodedUrl}`);
|
|
7662
8083
|
}
|
|
7663
8084
|
async function submitUrl(apiKey, siteUrl, url) {
|
|
8085
|
+
validateApiKey(apiKey);
|
|
8086
|
+
validateSiteUrl2(siteUrl);
|
|
8087
|
+
validateUrl2(url);
|
|
7664
8088
|
await bingFetch(apiKey, "SubmitUrl", {
|
|
7665
8089
|
method: "POST",
|
|
7666
8090
|
body: { siteUrl, url }
|
|
7667
8091
|
});
|
|
7668
8092
|
}
|
|
7669
8093
|
async function submitUrlBatch(apiKey, siteUrl, urls) {
|
|
8094
|
+
validateApiKey(apiKey);
|
|
8095
|
+
validateSiteUrl2(siteUrl);
|
|
8096
|
+
validateUrls(urls);
|
|
7670
8097
|
for (let i = 0; i < urls.length; i += BING_SUBMIT_URL_BATCH_LIMIT) {
|
|
7671
8098
|
const batch = urls.slice(i, i + BING_SUBMIT_URL_BATCH_LIMIT);
|
|
7672
8099
|
await bingFetch(apiKey, "SubmitUrlbatch", {
|
|
@@ -7676,6 +8103,8 @@ async function submitUrlBatch(apiKey, siteUrl, urls) {
|
|
|
7676
8103
|
}
|
|
7677
8104
|
}
|
|
7678
8105
|
async function getKeywordStats(apiKey, siteUrl) {
|
|
8106
|
+
validateApiKey(apiKey);
|
|
8107
|
+
validateSiteUrl2(siteUrl);
|
|
7679
8108
|
const encodedSite = encodeURIComponent(siteUrl);
|
|
7680
8109
|
const data = await bingFetch(apiKey, `GetQueryStats?siteUrl=${encodedSite}`);
|
|
7681
8110
|
return data ?? [];
|
|
@@ -7816,7 +8245,7 @@ async function bingRoutes(app, opts) {
|
|
|
7816
8245
|
const project = resolveProject(app.db, request.params.name);
|
|
7817
8246
|
const conn = requireConnection(store, project.canonicalDomain, reply);
|
|
7818
8247
|
if (!conn) return;
|
|
7819
|
-
const allInspections = app.db.select().from(bingUrlInspections).where(
|
|
8248
|
+
const allInspections = app.db.select().from(bingUrlInspections).where(eq15(bingUrlInspections.projectId, project.id)).orderBy(desc6(bingUrlInspections.inspectedAt)).all();
|
|
7820
8249
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
7821
8250
|
const definitiveByUrl = /* @__PURE__ */ new Map();
|
|
7822
8251
|
for (const row of allInspections) {
|
|
@@ -7886,8 +8315,8 @@ async function bingRoutes(app, opts) {
|
|
|
7886
8315
|
if (!store) return;
|
|
7887
8316
|
const project = resolveProject(app.db, request.params.name);
|
|
7888
8317
|
const { url, limit } = request.query;
|
|
7889
|
-
const whereClause = url ?
|
|
7890
|
-
const filtered = app.db.select().from(bingUrlInspections).where(whereClause).orderBy(
|
|
8318
|
+
const whereClause = url ? and4(eq15(bingUrlInspections.projectId, project.id), eq15(bingUrlInspections.url, url)) : eq15(bingUrlInspections.projectId, project.id);
|
|
8319
|
+
const filtered = app.db.select().from(bingUrlInspections).where(whereClause).orderBy(desc6(bingUrlInspections.inspectedAt)).limit(Math.max(1, Math.min(parseInt(limit ?? "100", 10) || 100, 1e3))).all();
|
|
7891
8320
|
return filtered.map((r) => ({
|
|
7892
8321
|
id: r.id,
|
|
7893
8322
|
url: r.url,
|
|
@@ -7983,7 +8412,7 @@ async function bingRoutes(app, opts) {
|
|
|
7983
8412
|
}
|
|
7984
8413
|
let urlsToSubmit = request.body?.urls ?? [];
|
|
7985
8414
|
if (request.body?.allUnindexed) {
|
|
7986
|
-
const allInspections = app.db.select().from(bingUrlInspections).where(
|
|
8415
|
+
const allInspections = app.db.select().from(bingUrlInspections).where(eq15(bingUrlInspections.projectId, project.id)).orderBy(desc6(bingUrlInspections.inspectedAt)).all();
|
|
7987
8416
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
7988
8417
|
for (const row of allInspections) {
|
|
7989
8418
|
if (!latestByUrl.has(row.url)) {
|
|
@@ -8076,14 +8505,14 @@ async function bingRoutes(app, opts) {
|
|
|
8076
8505
|
import fs2 from "fs";
|
|
8077
8506
|
import path2 from "path";
|
|
8078
8507
|
import os2 from "os";
|
|
8079
|
-
import { eq as
|
|
8508
|
+
import { eq as eq16, and as and5 } from "drizzle-orm";
|
|
8080
8509
|
function getScreenshotDir() {
|
|
8081
8510
|
return path2.join(os2.homedir(), ".canonry", "screenshots");
|
|
8082
8511
|
}
|
|
8083
8512
|
async function cdpRoutes(app, opts) {
|
|
8084
8513
|
app.get("/screenshots/:snapshotId", async (request, reply) => {
|
|
8085
8514
|
const { snapshotId } = request.params;
|
|
8086
|
-
const snapshot = app.db.select({ screenshotPath: querySnapshots.screenshotPath }).from(querySnapshots).where(
|
|
8515
|
+
const snapshot = app.db.select({ screenshotPath: querySnapshots.screenshotPath }).from(querySnapshots).where(eq16(querySnapshots.id, snapshotId)).get();
|
|
8087
8516
|
if (!snapshot?.screenshotPath) {
|
|
8088
8517
|
const err = notFound("Screenshot", snapshotId);
|
|
8089
8518
|
return reply.code(err.statusCode).send(err.toJSON());
|
|
@@ -8149,7 +8578,7 @@ async function cdpRoutes(app, opts) {
|
|
|
8149
8578
|
async (request, reply) => {
|
|
8150
8579
|
const project = resolveProject(app.db, request.params.name);
|
|
8151
8580
|
const { runId } = request.params;
|
|
8152
|
-
const run = app.db.select().from(runs).where(
|
|
8581
|
+
const run = app.db.select().from(runs).where(and5(eq16(runs.id, runId), eq16(runs.projectId, project.id))).get();
|
|
8153
8582
|
if (!run) {
|
|
8154
8583
|
const err = notFound("Run", runId);
|
|
8155
8584
|
return reply.code(err.statusCode).send(err.toJSON());
|
|
@@ -8162,8 +8591,8 @@ async function cdpRoutes(app, opts) {
|
|
|
8162
8591
|
citedDomains: querySnapshots.citedDomains,
|
|
8163
8592
|
screenshotPath: querySnapshots.screenshotPath,
|
|
8164
8593
|
rawResponse: querySnapshots.rawResponse
|
|
8165
|
-
}).from(querySnapshots).where(
|
|
8166
|
-
const keywordRows = app.db.select({ id: keywords.id, keyword: keywords.keyword }).from(keywords).where(
|
|
8594
|
+
}).from(querySnapshots).where(eq16(querySnapshots.runId, runId)).all();
|
|
8595
|
+
const keywordRows = app.db.select({ id: keywords.id, keyword: keywords.keyword }).from(keywords).where(eq16(keywords.projectId, project.id)).all();
|
|
8167
8596
|
const keywordMap = new Map(keywordRows.map((k) => [k.id, k.keyword]));
|
|
8168
8597
|
const byKeyword = /* @__PURE__ */ new Map();
|
|
8169
8598
|
for (const snap of snapshots) {
|
|
@@ -8231,7 +8660,7 @@ async function cdpRoutes(app, opts) {
|
|
|
8231
8660
|
citationState: browser.citationState,
|
|
8232
8661
|
citedDomains: JSON.parse(browser.citedDomains || "[]"),
|
|
8233
8662
|
groundingSources: parseGroundingSources2(browser),
|
|
8234
|
-
screenshotUrl: browser.screenshotPath ?
|
|
8663
|
+
screenshotUrl: browser.screenshotPath ? `${opts.routePrefix ?? "/api/v1"}/screenshots/${browser.id}` : void 0
|
|
8235
8664
|
} : null,
|
|
8236
8665
|
agreement
|
|
8237
8666
|
};
|
|
@@ -8246,7 +8675,7 @@ async function cdpRoutes(app, opts) {
|
|
|
8246
8675
|
|
|
8247
8676
|
// ../api-routes/src/ga.ts
|
|
8248
8677
|
import crypto16 from "crypto";
|
|
8249
|
-
import { eq as
|
|
8678
|
+
import { eq as eq17, desc as desc7, and as and6, sql as sql4 } from "drizzle-orm";
|
|
8250
8679
|
function gaLog(level, action, ctx) {
|
|
8251
8680
|
const entry = { ts: (/* @__PURE__ */ new Date()).toISOString(), level, module: "GA4Routes", action, ...ctx };
|
|
8252
8681
|
const stream = level === "error" ? process.stderr : process.stdout;
|
|
@@ -8403,9 +8832,9 @@ async function ga4Routes(app, opts) {
|
|
|
8403
8832
|
if (!saConn && !oauthConn) {
|
|
8404
8833
|
throw notFound("GA4 connection", project.name);
|
|
8405
8834
|
}
|
|
8406
|
-
app.db.delete(gaTrafficSnapshots).where(
|
|
8407
|
-
app.db.delete(gaTrafficSummaries).where(
|
|
8408
|
-
app.db.delete(gaAiReferrals).where(
|
|
8835
|
+
app.db.delete(gaTrafficSnapshots).where(eq17(gaTrafficSnapshots.projectId, project.id)).run();
|
|
8836
|
+
app.db.delete(gaTrafficSummaries).where(eq17(gaTrafficSummaries.projectId, project.id)).run();
|
|
8837
|
+
app.db.delete(gaAiReferrals).where(eq17(gaAiReferrals.projectId, project.id)).run();
|
|
8409
8838
|
const propertyId = saConn?.propertyId ?? oauthConn?.propertyId ?? null;
|
|
8410
8839
|
opts.ga4CredentialStore?.deleteConnection(project.name);
|
|
8411
8840
|
opts.googleConnectionStore?.deleteConnection(project.canonicalDomain, "ga4");
|
|
@@ -8426,7 +8855,7 @@ async function ga4Routes(app, opts) {
|
|
|
8426
8855
|
if (!connected) {
|
|
8427
8856
|
return { connected: false, propertyId: null, clientEmail: null, authMethod: null, lastSyncedAt: null };
|
|
8428
8857
|
}
|
|
8429
|
-
const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(
|
|
8858
|
+
const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq17(gaTrafficSummaries.projectId, project.id)).orderBy(desc7(gaTrafficSummaries.syncedAt)).limit(1).get();
|
|
8430
8859
|
return {
|
|
8431
8860
|
connected: true,
|
|
8432
8861
|
propertyId: saConn?.propertyId ?? oauthConn?.propertyId ?? null,
|
|
@@ -8459,8 +8888,8 @@ async function ga4Routes(app, opts) {
|
|
|
8459
8888
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8460
8889
|
app.db.transaction((tx) => {
|
|
8461
8890
|
tx.delete(gaTrafficSnapshots).where(
|
|
8462
|
-
|
|
8463
|
-
|
|
8891
|
+
and6(
|
|
8892
|
+
eq17(gaTrafficSnapshots.projectId, project.id),
|
|
8464
8893
|
sql4`${gaTrafficSnapshots.date} >= ${summary.periodStart}`,
|
|
8465
8894
|
sql4`${gaTrafficSnapshots.date} <= ${summary.periodEnd}`
|
|
8466
8895
|
)
|
|
@@ -8480,8 +8909,8 @@ async function ga4Routes(app, opts) {
|
|
|
8480
8909
|
}
|
|
8481
8910
|
}
|
|
8482
8911
|
tx.delete(gaAiReferrals).where(
|
|
8483
|
-
|
|
8484
|
-
|
|
8912
|
+
and6(
|
|
8913
|
+
eq17(gaAiReferrals.projectId, project.id),
|
|
8485
8914
|
sql4`${gaAiReferrals.date} >= ${summary.periodStart}`,
|
|
8486
8915
|
sql4`${gaAiReferrals.date} <= ${summary.periodEnd}`
|
|
8487
8916
|
)
|
|
@@ -8501,7 +8930,7 @@ async function ga4Routes(app, opts) {
|
|
|
8501
8930
|
}).run();
|
|
8502
8931
|
}
|
|
8503
8932
|
}
|
|
8504
|
-
tx.delete(gaTrafficSummaries).where(
|
|
8933
|
+
tx.delete(gaTrafficSummaries).where(eq17(gaTrafficSummaries.projectId, project.id)).run();
|
|
8505
8934
|
tx.insert(gaTrafficSummaries).values({
|
|
8506
8935
|
id: crypto16.randomUUID(),
|
|
8507
8936
|
projectId: project.id,
|
|
@@ -8536,20 +8965,20 @@ async function ga4Routes(app, opts) {
|
|
|
8536
8965
|
totalSessions: gaTrafficSummaries.totalSessions,
|
|
8537
8966
|
totalOrganicSessions: gaTrafficSummaries.totalOrganicSessions,
|
|
8538
8967
|
totalUsers: gaTrafficSummaries.totalUsers
|
|
8539
|
-
}).from(gaTrafficSummaries).where(
|
|
8968
|
+
}).from(gaTrafficSummaries).where(eq17(gaTrafficSummaries.projectId, project.id)).get();
|
|
8540
8969
|
const rows = app.db.select({
|
|
8541
8970
|
landingPage: gaTrafficSnapshots.landingPage,
|
|
8542
8971
|
sessions: sql4`SUM(${gaTrafficSnapshots.sessions})`,
|
|
8543
8972
|
organicSessions: sql4`SUM(${gaTrafficSnapshots.organicSessions})`,
|
|
8544
8973
|
users: sql4`SUM(${gaTrafficSnapshots.users})`
|
|
8545
|
-
}).from(gaTrafficSnapshots).where(
|
|
8974
|
+
}).from(gaTrafficSnapshots).where(eq17(gaTrafficSnapshots.projectId, project.id)).groupBy(gaTrafficSnapshots.landingPage).orderBy(sql4`SUM(${gaTrafficSnapshots.sessions}) DESC`).limit(limit).all();
|
|
8546
8975
|
const aiReferrals = app.db.select({
|
|
8547
8976
|
source: gaAiReferrals.source,
|
|
8548
8977
|
medium: gaAiReferrals.medium,
|
|
8549
8978
|
sourceDimension: gaAiReferrals.sourceDimension,
|
|
8550
8979
|
sessions: sql4`SUM(${gaAiReferrals.sessions})`,
|
|
8551
8980
|
users: sql4`SUM(${gaAiReferrals.users})`
|
|
8552
|
-
}).from(gaAiReferrals).where(
|
|
8981
|
+
}).from(gaAiReferrals).where(eq17(gaAiReferrals.projectId, project.id)).groupBy(gaAiReferrals.source, gaAiReferrals.medium, gaAiReferrals.sourceDimension).orderBy(sql4`SUM(${gaAiReferrals.sessions}) DESC`).all();
|
|
8553
8982
|
const aiDeduped = app.db.select({
|
|
8554
8983
|
sessions: sql4`SUM(max_sessions)`,
|
|
8555
8984
|
users: sql4`SUM(max_users)`
|
|
@@ -8563,7 +8992,7 @@ async function ga4Routes(app, opts) {
|
|
|
8563
8992
|
GROUP BY date, source, medium
|
|
8564
8993
|
)`
|
|
8565
8994
|
).get();
|
|
8566
|
-
const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(
|
|
8995
|
+
const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq17(gaTrafficSummaries.projectId, project.id)).orderBy(desc7(gaTrafficSummaries.syncedAt)).limit(1).get();
|
|
8567
8996
|
return {
|
|
8568
8997
|
totalSessions: summary?.totalSessions ?? 0,
|
|
8569
8998
|
totalOrganicSessions: summary?.totalOrganicSessions ?? 0,
|
|
@@ -8596,7 +9025,7 @@ async function ga4Routes(app, opts) {
|
|
|
8596
9025
|
sourceDimension: gaAiReferrals.sourceDimension,
|
|
8597
9026
|
sessions: gaAiReferrals.sessions,
|
|
8598
9027
|
users: gaAiReferrals.users
|
|
8599
|
-
}).from(gaAiReferrals).where(
|
|
9028
|
+
}).from(gaAiReferrals).where(eq17(gaAiReferrals.projectId, project.id)).orderBy(gaAiReferrals.date).all();
|
|
8600
9029
|
return rows;
|
|
8601
9030
|
});
|
|
8602
9031
|
app.get("/projects/:name/ga/session-history", async (request, _reply) => {
|
|
@@ -8607,7 +9036,7 @@ async function ga4Routes(app, opts) {
|
|
|
8607
9036
|
sessions: sql4`SUM(${gaTrafficSnapshots.sessions})`,
|
|
8608
9037
|
organicSessions: sql4`SUM(${gaTrafficSnapshots.organicSessions})`,
|
|
8609
9038
|
users: sql4`SUM(${gaTrafficSnapshots.users})`
|
|
8610
|
-
}).from(gaTrafficSnapshots).where(
|
|
9039
|
+
}).from(gaTrafficSnapshots).where(eq17(gaTrafficSnapshots.projectId, project.id)).groupBy(gaTrafficSnapshots.date).orderBy(gaTrafficSnapshots.date).all();
|
|
8611
9040
|
return rows.map((r) => ({
|
|
8612
9041
|
date: r.date,
|
|
8613
9042
|
sessions: r.sessions ?? 0,
|
|
@@ -8623,7 +9052,7 @@ async function ga4Routes(app, opts) {
|
|
|
8623
9052
|
sessions: sql4`SUM(${gaTrafficSnapshots.sessions})`,
|
|
8624
9053
|
organicSessions: sql4`SUM(${gaTrafficSnapshots.organicSessions})`,
|
|
8625
9054
|
users: sql4`SUM(${gaTrafficSnapshots.users})`
|
|
8626
|
-
}).from(gaTrafficSnapshots).where(
|
|
9055
|
+
}).from(gaTrafficSnapshots).where(eq17(gaTrafficSnapshots.projectId, project.id)).groupBy(gaTrafficSnapshots.landingPage).orderBy(sql4`SUM(${gaTrafficSnapshots.sessions}) DESC`).all();
|
|
8627
9056
|
return {
|
|
8628
9057
|
pages: trafficPages.map((r) => ({
|
|
8629
9058
|
landingPage: r.landingPage,
|
|
@@ -8758,6 +9187,36 @@ function parseSchemaPageEntry(entry) {
|
|
|
8758
9187
|
|
|
8759
9188
|
// ../integration-wordpress/src/wordpress-client.ts
|
|
8760
9189
|
import crypto17 from "crypto";
|
|
9190
|
+
function validateUsername(username) {
|
|
9191
|
+
if (!username || typeof username !== "string" || username.trim().length === 0) {
|
|
9192
|
+
throw new WordpressApiError("AUTH_INVALID", "Username is required and must be a non-empty string", 400);
|
|
9193
|
+
}
|
|
9194
|
+
}
|
|
9195
|
+
function validateAppPassword(appPassword) {
|
|
9196
|
+
if (!appPassword || typeof appPassword !== "string" || appPassword.trim().length === 0) {
|
|
9197
|
+
throw new WordpressApiError("AUTH_INVALID", "Application password is required and must be a non-empty string", 400);
|
|
9198
|
+
}
|
|
9199
|
+
}
|
|
9200
|
+
function validateSiteUrl3(siteUrl) {
|
|
9201
|
+
if (!siteUrl || typeof siteUrl !== "string" || siteUrl.trim().length === 0) {
|
|
9202
|
+
throw new WordpressApiError("AUTH_INVALID", "Site URL is required and must be a non-empty string", 400);
|
|
9203
|
+
}
|
|
9204
|
+
try {
|
|
9205
|
+
const url = new URL(siteUrl);
|
|
9206
|
+
if (!url.protocol.startsWith("http")) {
|
|
9207
|
+
throw new WordpressApiError("AUTH_INVALID", "Site URL must be an HTTP or HTTPS URL", 400);
|
|
9208
|
+
}
|
|
9209
|
+
if (url.protocol !== "https:") {
|
|
9210
|
+
}
|
|
9211
|
+
} catch {
|
|
9212
|
+
throw new WordpressApiError("AUTH_INVALID", "Site URL must be a valid URL", 400);
|
|
9213
|
+
}
|
|
9214
|
+
}
|
|
9215
|
+
function validateConnection(connection, siteUrl) {
|
|
9216
|
+
validateUsername(connection.username);
|
|
9217
|
+
validateAppPassword(connection.appPassword);
|
|
9218
|
+
validateSiteUrl3(siteUrl);
|
|
9219
|
+
}
|
|
8761
9220
|
var WP_REQUEST_TIMEOUT_MS = 3e4;
|
|
8762
9221
|
var WP_FETCH_TEXT_TIMEOUT_MS = 15e3;
|
|
8763
9222
|
var PAGE_FIELDS = "id,slug,status,link,modified,modified_gmt,title,content,meta";
|
|
@@ -8999,6 +9458,7 @@ function getWpStagingAdminUrl(url) {
|
|
|
8999
9458
|
}
|
|
9000
9459
|
async function verifyWordpressConnection(connection) {
|
|
9001
9460
|
const site = resolveEnvironment({ ...connection, defaultEnv: "live" }, "live");
|
|
9461
|
+
validateConnection(connection, site.siteUrl);
|
|
9002
9462
|
const userInfo = await verifyAuthenticatedRestAccess(connection, site.siteUrl);
|
|
9003
9463
|
const response = await fetchPageCollectionSummary(connection, site.siteUrl, { context: "view" });
|
|
9004
9464
|
const homeHtml = await fetchText(site.siteUrl);
|
|
@@ -9013,6 +9473,7 @@ async function verifyWordpressConnection(connection) {
|
|
|
9013
9473
|
}
|
|
9014
9474
|
async function getSiteStatus(connection, env) {
|
|
9015
9475
|
const site = resolveEnvironment(connection, env);
|
|
9476
|
+
validateConnection(connection, site.siteUrl);
|
|
9016
9477
|
try {
|
|
9017
9478
|
const userInfo = await verifyAuthenticatedRestAccess(connection, site.siteUrl);
|
|
9018
9479
|
const response = await fetchPageCollectionSummary(connection, site.siteUrl, { context: "view" });
|
|
@@ -10317,7 +10778,7 @@ async function apiRoutes(app, opts) {
|
|
|
10317
10778
|
resolveSessionApiKeyId: opts.resolveSessionApiKeyId
|
|
10318
10779
|
});
|
|
10319
10780
|
}
|
|
10320
|
-
await api.register(openApiRoutes, opts.openApiInfo
|
|
10781
|
+
await api.register(openApiRoutes, { ...opts.openApiInfo, routePrefix: opts.routePrefix });
|
|
10321
10782
|
await api.register(projectRoutes, {
|
|
10322
10783
|
onProjectDeleted: opts.onProjectDeleted,
|
|
10323
10784
|
validProviderNames: opts.providerAdapters?.map((a) => a.name)
|
|
@@ -10343,6 +10804,7 @@ async function apiRoutes(app, opts) {
|
|
|
10343
10804
|
});
|
|
10344
10805
|
await api.register(historyRoutes);
|
|
10345
10806
|
await api.register(analyticsRoutes);
|
|
10807
|
+
await api.register(intelligenceRoutes);
|
|
10346
10808
|
await api.register(settingsRoutes, {
|
|
10347
10809
|
providerSummary: opts.providerSummary,
|
|
10348
10810
|
providerAdapters: opts.providerAdapters,
|
|
@@ -10372,6 +10834,7 @@ async function apiRoutes(app, opts) {
|
|
|
10372
10834
|
googleConnectionStore: opts.googleConnectionStore,
|
|
10373
10835
|
googleStateSecret: opts.googleStateSecret,
|
|
10374
10836
|
publicUrl: opts.publicUrl,
|
|
10837
|
+
routePrefix: opts.routePrefix,
|
|
10375
10838
|
onGscSyncRequested: opts.onGscSyncRequested,
|
|
10376
10839
|
onInspectSitemapRequested: opts.onInspectSitemapRequested
|
|
10377
10840
|
});
|
|
@@ -10382,7 +10845,8 @@ async function apiRoutes(app, opts) {
|
|
|
10382
10845
|
await api.register(cdpRoutes, {
|
|
10383
10846
|
getCdpStatus: opts.getCdpStatus,
|
|
10384
10847
|
onCdpScreenshot: opts.onCdpScreenshot,
|
|
10385
|
-
onCdpConfigure: opts.onCdpConfigure
|
|
10848
|
+
onCdpConfigure: opts.onCdpConfigure,
|
|
10849
|
+
routePrefix: opts.routePrefix
|
|
10386
10850
|
});
|
|
10387
10851
|
await api.register(ga4Routes, {
|
|
10388
10852
|
ga4CredentialStore: opts.ga4CredentialStore,
|
|
@@ -10406,7 +10870,7 @@ async function withRetry(fn, options = {}) {
|
|
|
10406
10870
|
lastError = err;
|
|
10407
10871
|
if (attempt < maxRetries) {
|
|
10408
10872
|
const delay = initialDelay * Math.pow(2, attempt);
|
|
10409
|
-
console.warn(`[provider] Attempt ${attempt + 1} failed, retrying in ${delay}ms...`, err);
|
|
10873
|
+
console.warn(`[provider] Attempt ${attempt + 1} failed, retrying in ${delay}ms...`, err instanceof Error ? err.message : String(err));
|
|
10410
10874
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
10411
10875
|
}
|
|
10412
10876
|
}
|
|
@@ -10740,7 +11204,7 @@ async function withRetry2(fn, options = {}) {
|
|
|
10740
11204
|
lastError = err;
|
|
10741
11205
|
if (attempt < maxRetries) {
|
|
10742
11206
|
const delay = initialDelay * Math.pow(2, attempt);
|
|
10743
|
-
console.warn(`[provider] Attempt ${attempt + 1} failed, retrying in ${delay}ms...`, err);
|
|
11207
|
+
console.warn(`[provider] Attempt ${attempt + 1} failed, retrying in ${delay}ms...`, err instanceof Error ? err.message : String(err));
|
|
10744
11208
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
10745
11209
|
}
|
|
10746
11210
|
}
|
|
@@ -11046,7 +11510,7 @@ async function withRetry3(fn, options = {}) {
|
|
|
11046
11510
|
lastError = err;
|
|
11047
11511
|
if (attempt < maxRetries) {
|
|
11048
11512
|
const delay = initialDelay * Math.pow(2, attempt);
|
|
11049
|
-
console.warn(`[provider] Attempt ${attempt + 1} failed, retrying in ${delay}ms...`, err);
|
|
11513
|
+
console.warn(`[provider] Attempt ${attempt + 1} failed, retrying in ${delay}ms...`, err instanceof Error ? err.message : String(err));
|
|
11050
11514
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
11051
11515
|
}
|
|
11052
11516
|
}
|
|
@@ -11351,7 +11815,7 @@ async function withRetry4(fn, options = {}) {
|
|
|
11351
11815
|
lastError = err;
|
|
11352
11816
|
if (attempt < maxRetries) {
|
|
11353
11817
|
const delay = initialDelay * Math.pow(2, attempt);
|
|
11354
|
-
console.warn(`[provider] Attempt ${attempt + 1} failed, retrying in ${delay}ms...`, err);
|
|
11818
|
+
console.warn(`[provider] Attempt ${attempt + 1} failed, retrying in ${delay}ms...`, err instanceof Error ? err.message : String(err));
|
|
11355
11819
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
11356
11820
|
}
|
|
11357
11821
|
}
|
|
@@ -12146,7 +12610,7 @@ async function withRetry5(fn, options = {}) {
|
|
|
12146
12610
|
lastError = err;
|
|
12147
12611
|
if (attempt < maxRetries) {
|
|
12148
12612
|
const delay = initialDelay * Math.pow(2, attempt);
|
|
12149
|
-
console.warn(`[provider] Attempt ${attempt + 1} failed, retrying in ${delay}ms...`, err);
|
|
12613
|
+
console.warn(`[provider] Attempt ${attempt + 1} failed, retrying in ${delay}ms...`, err instanceof Error ? err.message : String(err));
|
|
12150
12614
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
12151
12615
|
}
|
|
12152
12616
|
}
|
|
@@ -12540,7 +13004,7 @@ import crypto18 from "crypto";
|
|
|
12540
13004
|
import fs4 from "fs";
|
|
12541
13005
|
import path5 from "path";
|
|
12542
13006
|
import os4 from "os";
|
|
12543
|
-
import { and as
|
|
13007
|
+
import { and as and7, eq as eq18, inArray as inArray3, sql as sql5 } from "drizzle-orm";
|
|
12544
13008
|
|
|
12545
13009
|
// src/logger.ts
|
|
12546
13010
|
var IS_TTY = process.stdout.isTTY === true;
|
|
@@ -12562,7 +13026,7 @@ function emit(entry) {
|
|
|
12562
13026
|
}
|
|
12563
13027
|
}
|
|
12564
13028
|
function createLogger(module) {
|
|
12565
|
-
function
|
|
13029
|
+
function log10(level, action, ctx) {
|
|
12566
13030
|
const entry = {
|
|
12567
13031
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
12568
13032
|
level,
|
|
@@ -12573,9 +13037,9 @@ function createLogger(module) {
|
|
|
12573
13037
|
emit(entry);
|
|
12574
13038
|
}
|
|
12575
13039
|
return {
|
|
12576
|
-
info: (action, ctx) =>
|
|
12577
|
-
warn: (action, ctx) =>
|
|
12578
|
-
error: (action, ctx) =>
|
|
13040
|
+
info: (action, ctx) => log10("info", action, ctx),
|
|
13041
|
+
warn: (action, ctx) => log10("warn", action, ctx),
|
|
13042
|
+
error: (action, ctx) => log10("error", action, ctx)
|
|
12579
13043
|
};
|
|
12580
13044
|
}
|
|
12581
13045
|
|
|
@@ -12662,7 +13126,7 @@ var JobRunner = class {
|
|
|
12662
13126
|
if (stale.length === 0) return;
|
|
12663
13127
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
12664
13128
|
for (const run of stale) {
|
|
12665
|
-
this.db.update(runs).set({ status: "failed", finishedAt: now, error: "Server restarted while run was in progress" }).where(
|
|
13129
|
+
this.db.update(runs).set({ status: "failed", finishedAt: now, error: "Server restarted while run was in progress" }).where(eq18(runs.id, run.id)).run();
|
|
12666
13130
|
log.warn("run.recovered-stale", { runId: run.id, previousStatus: run.status });
|
|
12667
13131
|
}
|
|
12668
13132
|
}
|
|
@@ -12690,10 +13154,10 @@ var JobRunner = class {
|
|
|
12690
13154
|
throw new Error(`Run ${runId} is not executable from status '${existingRun.status}'`);
|
|
12691
13155
|
}
|
|
12692
13156
|
if (existingRun.status === "queued") {
|
|
12693
|
-
this.db.update(runs).set({ status: "running", startedAt: now }).where(
|
|
13157
|
+
this.db.update(runs).set({ status: "running", startedAt: now }).where(and7(eq18(runs.id, runId), eq18(runs.status, "queued"))).run();
|
|
12694
13158
|
}
|
|
12695
13159
|
this.throwIfRunCancelled(runId);
|
|
12696
|
-
const project = this.db.select().from(projects).where(
|
|
13160
|
+
const project = this.db.select().from(projects).where(eq18(projects.id, projectId)).get();
|
|
12697
13161
|
if (!project) {
|
|
12698
13162
|
throw new Error(`Project ${projectId} not found`);
|
|
12699
13163
|
}
|
|
@@ -12713,8 +13177,8 @@ var JobRunner = class {
|
|
|
12713
13177
|
throw new Error("No providers configured. Add at least one provider API key.");
|
|
12714
13178
|
}
|
|
12715
13179
|
log.info("run.dispatch", { runId, providerCount: activeProviders.length, providers: activeProviders.map((p) => p.adapter.name) });
|
|
12716
|
-
projectKeywords = this.db.select().from(keywords).where(
|
|
12717
|
-
const projectCompetitors = this.db.select().from(competitors).where(
|
|
13180
|
+
projectKeywords = this.db.select().from(keywords).where(eq18(keywords.projectId, projectId)).all();
|
|
13181
|
+
const projectCompetitors = this.db.select().from(competitors).where(eq18(competitors.projectId, projectId)).all();
|
|
12718
13182
|
const competitorDomains = projectCompetitors.map((c) => c.domain);
|
|
12719
13183
|
const allDomains = effectiveDomains({
|
|
12720
13184
|
canonicalDomain: project.canonicalDomain,
|
|
@@ -12730,7 +13194,7 @@ var JobRunner = class {
|
|
|
12730
13194
|
const todayPeriod = getCurrentUsageDay();
|
|
12731
13195
|
for (const p of activeProviders) {
|
|
12732
13196
|
const providerScope = `${projectId}:${p.adapter.name}`;
|
|
12733
|
-
const providerUsage = this.db.select().from(usageCounters).where(
|
|
13197
|
+
const providerUsage = this.db.select().from(usageCounters).where(eq18(usageCounters.scope, providerScope)).all().filter((r) => r.period === todayPeriod && r.metric === "queries").reduce((sum, r) => sum + r.count, 0);
|
|
12734
13198
|
const limit = p.config.quotaPolicy.maxRequestsPerDay;
|
|
12735
13199
|
if (providerUsage + queriesPerProvider > limit) {
|
|
12736
13200
|
throw new Error(
|
|
@@ -12871,12 +13335,12 @@ var JobRunner = class {
|
|
|
12871
13335
|
const someFailed = providerErrors.size > 0;
|
|
12872
13336
|
if (allFailed) {
|
|
12873
13337
|
const errorDetail = JSON.stringify(Object.fromEntries(providerErrors));
|
|
12874
|
-
this.db.update(runs).set({ status: "failed", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(
|
|
13338
|
+
this.db.update(runs).set({ status: "failed", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(eq18(runs.id, runId)).run();
|
|
12875
13339
|
} else if (someFailed) {
|
|
12876
13340
|
const errorDetail = JSON.stringify(Object.fromEntries(providerErrors));
|
|
12877
|
-
this.db.update(runs).set({ status: "partial", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(
|
|
13341
|
+
this.db.update(runs).set({ status: "partial", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(eq18(runs.id, runId)).run();
|
|
12878
13342
|
} else {
|
|
12879
|
-
this.db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
13343
|
+
this.db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq18(runs.id, runId)).run();
|
|
12880
13344
|
}
|
|
12881
13345
|
this.flushProviderUsage(projectId, providerDispatchCounts);
|
|
12882
13346
|
const finalStatus = allFailed ? "failed" : someFailed ? "partial" : "completed";
|
|
@@ -12911,7 +13375,7 @@ var JobRunner = class {
|
|
|
12911
13375
|
status: "failed",
|
|
12912
13376
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
12913
13377
|
error: errorMessage
|
|
12914
|
-
}).where(
|
|
13378
|
+
}).where(eq18(runs.id, runId)).run();
|
|
12915
13379
|
this.flushProviderUsage(projectId, providerDispatchCounts);
|
|
12916
13380
|
trackEvent("run.completed", {
|
|
12917
13381
|
status: "failed",
|
|
@@ -12954,7 +13418,7 @@ var JobRunner = class {
|
|
|
12954
13418
|
status: runs.status,
|
|
12955
13419
|
finishedAt: runs.finishedAt,
|
|
12956
13420
|
error: runs.error
|
|
12957
|
-
}).from(runs).where(
|
|
13421
|
+
}).from(runs).where(eq18(runs.id, runId)).get();
|
|
12958
13422
|
}
|
|
12959
13423
|
isRunCancelled(runId) {
|
|
12960
13424
|
return this.getRunState(runId)?.status === "cancelled";
|
|
@@ -12970,7 +13434,7 @@ var JobRunner = class {
|
|
|
12970
13434
|
this.db.update(runs).set({
|
|
12971
13435
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
12972
13436
|
error: currentRun.error ?? "Cancelled by user"
|
|
12973
|
-
}).where(
|
|
13437
|
+
}).where(eq18(runs.id, runId)).run();
|
|
12974
13438
|
}
|
|
12975
13439
|
trackEvent("run.completed", {
|
|
12976
13440
|
status: "cancelled",
|
|
@@ -13142,7 +13606,7 @@ function matchesBrandKey(candidateKey, brandKeys) {
|
|
|
13142
13606
|
|
|
13143
13607
|
// src/gsc-sync.ts
|
|
13144
13608
|
import crypto19 from "crypto";
|
|
13145
|
-
import { eq as
|
|
13609
|
+
import { eq as eq19, and as and8, sql as sql6 } from "drizzle-orm";
|
|
13146
13610
|
var log2 = createLogger("GscSync");
|
|
13147
13611
|
function formatDate2(d) {
|
|
13148
13612
|
return d.toISOString().split("T")[0];
|
|
@@ -13154,13 +13618,13 @@ function daysAgo(n) {
|
|
|
13154
13618
|
}
|
|
13155
13619
|
async function executeGscSync(db, runId, projectId, opts) {
|
|
13156
13620
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
13157
|
-
db.update(runs).set({ status: "running", startedAt: now }).where(
|
|
13621
|
+
db.update(runs).set({ status: "running", startedAt: now }).where(eq19(runs.id, runId)).run();
|
|
13158
13622
|
try {
|
|
13159
13623
|
const { clientId: googleClientId, clientSecret: googleClientSecret } = getGoogleAuthConfig(opts.config);
|
|
13160
13624
|
if (!googleClientId || !googleClientSecret) {
|
|
13161
13625
|
throw new Error("Google OAuth is not configured in the local Canonry config");
|
|
13162
13626
|
}
|
|
13163
|
-
const project = db.select().from(projects).where(
|
|
13627
|
+
const project = db.select().from(projects).where(eq19(projects.id, projectId)).get();
|
|
13164
13628
|
if (!project) {
|
|
13165
13629
|
throw new Error(`Project not found: ${projectId}`);
|
|
13166
13630
|
}
|
|
@@ -13194,8 +13658,8 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
13194
13658
|
});
|
|
13195
13659
|
log2.info("fetch.complete", { runId, projectId, rowCount: rows.length });
|
|
13196
13660
|
db.delete(gscSearchData).where(
|
|
13197
|
-
|
|
13198
|
-
|
|
13661
|
+
and8(
|
|
13662
|
+
eq19(gscSearchData.projectId, projectId),
|
|
13199
13663
|
sql6`${gscSearchData.date} >= ${startDate}`,
|
|
13200
13664
|
sql6`${gscSearchData.date} <= ${endDate}`
|
|
13201
13665
|
)
|
|
@@ -13262,7 +13726,7 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
13262
13726
|
log2.error("inspect.url-failed", { runId, projectId, url: pageUrl, error: err instanceof Error ? err.message : String(err) });
|
|
13263
13727
|
}
|
|
13264
13728
|
}
|
|
13265
|
-
const allInspections = db.select().from(gscUrlInspections).where(
|
|
13729
|
+
const allInspections = db.select().from(gscUrlInspections).where(eq19(gscUrlInspections.projectId, projectId)).all();
|
|
13266
13730
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
13267
13731
|
for (const row of allInspections) {
|
|
13268
13732
|
const existing = latestByUrl.get(row.url);
|
|
@@ -13283,7 +13747,7 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
13283
13747
|
}
|
|
13284
13748
|
}
|
|
13285
13749
|
const snapshotDate = formatDate2(/* @__PURE__ */ new Date());
|
|
13286
|
-
db.delete(gscCoverageSnapshots).where(
|
|
13750
|
+
db.delete(gscCoverageSnapshots).where(and8(eq19(gscCoverageSnapshots.projectId, projectId), eq19(gscCoverageSnapshots.date, snapshotDate))).run();
|
|
13287
13751
|
db.insert(gscCoverageSnapshots).values({
|
|
13288
13752
|
id: crypto19.randomUUID(),
|
|
13289
13753
|
projectId,
|
|
@@ -13294,11 +13758,11 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
13294
13758
|
reasonBreakdown: JSON.stringify(reasonCounts),
|
|
13295
13759
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
13296
13760
|
}).run();
|
|
13297
|
-
db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
13761
|
+
db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq19(runs.id, runId)).run();
|
|
13298
13762
|
log2.info("sync.completed", { runId, projectId, searchDataRows: rows.length, urlInspections: topPages.length, indexed: snapIndexed, notIndexed: snapNotIndexed });
|
|
13299
13763
|
} catch (err) {
|
|
13300
13764
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
13301
|
-
db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
13765
|
+
db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq19(runs.id, runId)).run();
|
|
13302
13766
|
log2.error("sync.failed", { runId, projectId, error: errorMsg });
|
|
13303
13767
|
throw err;
|
|
13304
13768
|
}
|
|
@@ -13306,7 +13770,7 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
13306
13770
|
|
|
13307
13771
|
// src/gsc-inspect-sitemap.ts
|
|
13308
13772
|
import crypto20 from "crypto";
|
|
13309
|
-
import { eq as
|
|
13773
|
+
import { eq as eq20, and as and9 } from "drizzle-orm";
|
|
13310
13774
|
|
|
13311
13775
|
// src/sitemap-parser.ts
|
|
13312
13776
|
var LOC_REGEX = /<loc>\s*([^<]+?)\s*<\/loc>/gi;
|
|
@@ -13375,13 +13839,13 @@ async function parseSitemapRecursive(url, urls, depth) {
|
|
|
13375
13839
|
var log3 = createLogger("InspectSitemap");
|
|
13376
13840
|
async function executeInspectSitemap(db, runId, projectId, opts) {
|
|
13377
13841
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
13378
|
-
db.update(runs).set({ status: "running", startedAt: now }).where(
|
|
13842
|
+
db.update(runs).set({ status: "running", startedAt: now }).where(eq20(runs.id, runId)).run();
|
|
13379
13843
|
try {
|
|
13380
13844
|
const { clientId: googleClientId, clientSecret: googleClientSecret } = getGoogleAuthConfig(opts.config);
|
|
13381
13845
|
if (!googleClientId || !googleClientSecret) {
|
|
13382
13846
|
throw new Error("Google OAuth is not configured in the local Canonry config");
|
|
13383
13847
|
}
|
|
13384
|
-
const project = db.select().from(projects).where(
|
|
13848
|
+
const project = db.select().from(projects).where(eq20(projects.id, projectId)).get();
|
|
13385
13849
|
if (!project) {
|
|
13386
13850
|
throw new Error(`Project not found: ${projectId}`);
|
|
13387
13851
|
}
|
|
@@ -13449,7 +13913,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
|
|
|
13449
13913
|
await new Promise((r) => setTimeout(r, 1e3));
|
|
13450
13914
|
}
|
|
13451
13915
|
}
|
|
13452
|
-
const allInspections = db.select().from(gscUrlInspections).where(
|
|
13916
|
+
const allInspections = db.select().from(gscUrlInspections).where(eq20(gscUrlInspections.projectId, projectId)).all();
|
|
13453
13917
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
13454
13918
|
for (const row of allInspections) {
|
|
13455
13919
|
const existing = latestByUrl.get(row.url);
|
|
@@ -13470,7 +13934,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
|
|
|
13470
13934
|
}
|
|
13471
13935
|
}
|
|
13472
13936
|
const snapshotDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
13473
|
-
db.delete(gscCoverageSnapshots).where(
|
|
13937
|
+
db.delete(gscCoverageSnapshots).where(and9(eq20(gscCoverageSnapshots.projectId, projectId), eq20(gscCoverageSnapshots.date, snapshotDate))).run();
|
|
13474
13938
|
db.insert(gscCoverageSnapshots).values({
|
|
13475
13939
|
id: crypto20.randomUUID(),
|
|
13476
13940
|
projectId,
|
|
@@ -13482,11 +13946,11 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
|
|
|
13482
13946
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
13483
13947
|
}).run();
|
|
13484
13948
|
const status = errors > 0 && inspected > 0 ? "partial" : errors === urls.length ? "failed" : "completed";
|
|
13485
|
-
db.update(runs).set({ status, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
13949
|
+
db.update(runs).set({ status, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq20(runs.id, runId)).run();
|
|
13486
13950
|
log3.info("inspect.completed", { runId, projectId, inspected, errors, total: urls.length, indexed: snapIndexed, notIndexed: snapNotIndexed });
|
|
13487
13951
|
} catch (err) {
|
|
13488
13952
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
13489
|
-
db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
13953
|
+
db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq20(runs.id, runId)).run();
|
|
13490
13954
|
log3.error("inspect.failed", { runId, projectId, error: errorMsg });
|
|
13491
13955
|
throw err;
|
|
13492
13956
|
}
|
|
@@ -13545,7 +14009,7 @@ var ProviderRegistry = class {
|
|
|
13545
14009
|
|
|
13546
14010
|
// src/scheduler.ts
|
|
13547
14011
|
import cron from "node-cron";
|
|
13548
|
-
import { eq as
|
|
14012
|
+
import { eq as eq21 } from "drizzle-orm";
|
|
13549
14013
|
var log4 = createLogger("Scheduler");
|
|
13550
14014
|
var Scheduler = class {
|
|
13551
14015
|
db;
|
|
@@ -13557,7 +14021,7 @@ var Scheduler = class {
|
|
|
13557
14021
|
}
|
|
13558
14022
|
/** Load all enabled schedules from DB and register cron jobs. */
|
|
13559
14023
|
start() {
|
|
13560
|
-
const allSchedules = this.db.select().from(schedules).where(
|
|
14024
|
+
const allSchedules = this.db.select().from(schedules).where(eq21(schedules.enabled, 1)).all();
|
|
13561
14025
|
for (const schedule of allSchedules) {
|
|
13562
14026
|
const missedRunAt = schedule.nextRunAt;
|
|
13563
14027
|
this.registerCronTask(schedule);
|
|
@@ -13582,7 +14046,7 @@ var Scheduler = class {
|
|
|
13582
14046
|
this.stopTask(projectId, existing, "Stopped");
|
|
13583
14047
|
this.tasks.delete(projectId);
|
|
13584
14048
|
}
|
|
13585
|
-
const schedule = this.db.select().from(schedules).where(
|
|
14049
|
+
const schedule = this.db.select().from(schedules).where(eq21(schedules.projectId, projectId)).get();
|
|
13586
14050
|
if (schedule && schedule.enabled === 1) {
|
|
13587
14051
|
this.registerCronTask(schedule);
|
|
13588
14052
|
}
|
|
@@ -13615,14 +14079,14 @@ var Scheduler = class {
|
|
|
13615
14079
|
this.db.update(schedules).set({
|
|
13616
14080
|
nextRunAt: task.getNextRun()?.toISOString() ?? null,
|
|
13617
14081
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
13618
|
-
}).where(
|
|
14082
|
+
}).where(eq21(schedules.id, scheduleId)).run();
|
|
13619
14083
|
const label = schedule.preset ?? cronExpr;
|
|
13620
14084
|
log4.info("cron.registered", { projectId, schedule: label, timezone });
|
|
13621
14085
|
}
|
|
13622
14086
|
triggerRun(scheduleId, projectId) {
|
|
13623
14087
|
try {
|
|
13624
14088
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
13625
|
-
const currentSchedule = this.db.select().from(schedules).where(
|
|
14089
|
+
const currentSchedule = this.db.select().from(schedules).where(eq21(schedules.id, scheduleId)).get();
|
|
13626
14090
|
if (!currentSchedule || currentSchedule.enabled !== 1) {
|
|
13627
14091
|
log4.warn("schedule.stale", { scheduleId, projectId, msg: "schedule no longer exists or is disabled" });
|
|
13628
14092
|
this.remove(projectId);
|
|
@@ -13630,7 +14094,7 @@ var Scheduler = class {
|
|
|
13630
14094
|
}
|
|
13631
14095
|
const task = this.tasks.get(projectId);
|
|
13632
14096
|
const nextRunAt = task?.getNextRun()?.toISOString() ?? null;
|
|
13633
|
-
const project = this.db.select().from(projects).where(
|
|
14097
|
+
const project = this.db.select().from(projects).where(eq21(projects.id, projectId)).get();
|
|
13634
14098
|
if (!project) {
|
|
13635
14099
|
log4.error("project.not-found", { projectId, msg: "skipping scheduled run" });
|
|
13636
14100
|
this.remove(projectId);
|
|
@@ -13647,7 +14111,7 @@ var Scheduler = class {
|
|
|
13647
14111
|
this.db.update(schedules).set({
|
|
13648
14112
|
nextRunAt,
|
|
13649
14113
|
updatedAt: now
|
|
13650
|
-
}).where(
|
|
14114
|
+
}).where(eq21(schedules.id, currentSchedule.id)).run();
|
|
13651
14115
|
return;
|
|
13652
14116
|
}
|
|
13653
14117
|
const runId = queueResult.runId;
|
|
@@ -13655,7 +14119,7 @@ var Scheduler = class {
|
|
|
13655
14119
|
lastRunAt: now,
|
|
13656
14120
|
nextRunAt,
|
|
13657
14121
|
updatedAt: now
|
|
13658
|
-
}).where(
|
|
14122
|
+
}).where(eq21(schedules.id, currentSchedule.id)).run();
|
|
13659
14123
|
const scheduleProviders = parseJsonColumn(currentSchedule.providers, []);
|
|
13660
14124
|
const providers = scheduleProviders.length > 0 ? scheduleProviders : void 0;
|
|
13661
14125
|
log4.info("run.triggered", { runId, projectName: project.name, providers: providers ?? "all" });
|
|
@@ -13667,7 +14131,7 @@ var Scheduler = class {
|
|
|
13667
14131
|
};
|
|
13668
14132
|
|
|
13669
14133
|
// src/notifier.ts
|
|
13670
|
-
import { eq as
|
|
14134
|
+
import { eq as eq22, desc as desc8, and as and10, or as or2 } from "drizzle-orm";
|
|
13671
14135
|
import crypto21 from "crypto";
|
|
13672
14136
|
var log5 = createLogger("Notifier");
|
|
13673
14137
|
var Notifier = class {
|
|
@@ -13680,18 +14144,18 @@ var Notifier = class {
|
|
|
13680
14144
|
/** Called after a run completes (success, partial, or failed). */
|
|
13681
14145
|
async onRunCompleted(runId, projectId) {
|
|
13682
14146
|
log5.info("run.completed", { runId, projectId });
|
|
13683
|
-
const notifs = this.db.select().from(notifications).where(
|
|
14147
|
+
const notifs = this.db.select().from(notifications).where(eq22(notifications.projectId, projectId)).all().filter((n) => n.enabled === 1);
|
|
13684
14148
|
if (notifs.length === 0) {
|
|
13685
14149
|
log5.info("notifications.none-enabled", { projectId });
|
|
13686
14150
|
return;
|
|
13687
14151
|
}
|
|
13688
14152
|
log5.info("notifications.found", { projectId, count: notifs.length });
|
|
13689
|
-
const run = this.db.select().from(runs).where(
|
|
14153
|
+
const run = this.db.select().from(runs).where(eq22(runs.id, runId)).get();
|
|
13690
14154
|
if (!run) {
|
|
13691
14155
|
log5.error("run.not-found", { runId, msg: "skipping notification dispatch" });
|
|
13692
14156
|
return;
|
|
13693
14157
|
}
|
|
13694
|
-
const project = this.db.select().from(projects).where(
|
|
14158
|
+
const project = this.db.select().from(projects).where(eq22(projects.id, projectId)).get();
|
|
13695
14159
|
if (!project) {
|
|
13696
14160
|
log5.error("project.not-found", { projectId, msg: "skipping notification dispatch" });
|
|
13697
14161
|
return;
|
|
@@ -13731,11 +14195,11 @@ var Notifier = class {
|
|
|
13731
14195
|
}
|
|
13732
14196
|
computeTransitions(runId, projectId) {
|
|
13733
14197
|
const recentRuns = this.db.select().from(runs).where(
|
|
13734
|
-
|
|
13735
|
-
|
|
13736
|
-
or2(
|
|
14198
|
+
and10(
|
|
14199
|
+
eq22(runs.projectId, projectId),
|
|
14200
|
+
or2(eq22(runs.status, "completed"), eq22(runs.status, "partial"))
|
|
13737
14201
|
)
|
|
13738
|
-
).orderBy(
|
|
14202
|
+
).orderBy(desc8(runs.createdAt)).limit(2).all();
|
|
13739
14203
|
if (recentRuns.length < 2) return [];
|
|
13740
14204
|
const currentRunId = recentRuns[0].id;
|
|
13741
14205
|
const previousRunId = recentRuns[1].id;
|
|
@@ -13745,12 +14209,12 @@ var Notifier = class {
|
|
|
13745
14209
|
keyword: keywords.keyword,
|
|
13746
14210
|
provider: querySnapshots.provider,
|
|
13747
14211
|
citationState: querySnapshots.citationState
|
|
13748
|
-
}).from(querySnapshots).leftJoin(keywords,
|
|
14212
|
+
}).from(querySnapshots).leftJoin(keywords, eq22(querySnapshots.keywordId, keywords.id)).where(eq22(querySnapshots.runId, currentRunId)).all();
|
|
13749
14213
|
const previousSnapshots = this.db.select({
|
|
13750
14214
|
keywordId: querySnapshots.keywordId,
|
|
13751
14215
|
provider: querySnapshots.provider,
|
|
13752
14216
|
citationState: querySnapshots.citationState
|
|
13753
|
-
}).from(querySnapshots).where(
|
|
14217
|
+
}).from(querySnapshots).where(eq22(querySnapshots.runId, previousRunId)).all();
|
|
13754
14218
|
const prevMap = /* @__PURE__ */ new Map();
|
|
13755
14219
|
for (const s of previousSnapshots) {
|
|
13756
14220
|
prevMap.set(`${s.keywordId}:${s.provider}`, s.citationState);
|
|
@@ -13820,6 +14284,399 @@ var Notifier = class {
|
|
|
13820
14284
|
}
|
|
13821
14285
|
};
|
|
13822
14286
|
|
|
14287
|
+
// src/intelligence-service.ts
|
|
14288
|
+
import { eq as eq23, desc as desc9, asc as asc2, and as and11, or as or3 } from "drizzle-orm";
|
|
14289
|
+
|
|
14290
|
+
// ../intelligence/src/regressions.ts
|
|
14291
|
+
function detectRegressions(currentRun, previousRun) {
|
|
14292
|
+
const regressions = [];
|
|
14293
|
+
const previousCited = /* @__PURE__ */ new Map();
|
|
14294
|
+
for (const snap of previousRun.snapshots) {
|
|
14295
|
+
if (snap.cited) {
|
|
14296
|
+
previousCited.set(`${snap.keyword}:${snap.provider}`, {
|
|
14297
|
+
citationUrl: snap.citationUrl,
|
|
14298
|
+
position: snap.position
|
|
14299
|
+
});
|
|
14300
|
+
}
|
|
14301
|
+
}
|
|
14302
|
+
for (const snap of currentRun.snapshots) {
|
|
14303
|
+
const key = `${snap.keyword}:${snap.provider}`;
|
|
14304
|
+
if (!snap.cited && previousCited.has(key)) {
|
|
14305
|
+
const prev = previousCited.get(key);
|
|
14306
|
+
regressions.push({
|
|
14307
|
+
keyword: snap.keyword,
|
|
14308
|
+
provider: snap.provider,
|
|
14309
|
+
previousCitationUrl: prev.citationUrl,
|
|
14310
|
+
previousPosition: prev.position,
|
|
14311
|
+
currentRunId: currentRun.runId,
|
|
14312
|
+
previousRunId: previousRun.runId
|
|
14313
|
+
});
|
|
14314
|
+
}
|
|
14315
|
+
}
|
|
14316
|
+
return regressions;
|
|
14317
|
+
}
|
|
14318
|
+
|
|
14319
|
+
// ../intelligence/src/gains.ts
|
|
14320
|
+
function detectGains(currentRun, previousRun) {
|
|
14321
|
+
const gains = [];
|
|
14322
|
+
const previousCited = /* @__PURE__ */ new Set();
|
|
14323
|
+
for (const snap of previousRun.snapshots) {
|
|
14324
|
+
if (snap.cited) {
|
|
14325
|
+
previousCited.add(`${snap.keyword}:${snap.provider}`);
|
|
14326
|
+
}
|
|
14327
|
+
}
|
|
14328
|
+
for (const snap of currentRun.snapshots) {
|
|
14329
|
+
const key = `${snap.keyword}:${snap.provider}`;
|
|
14330
|
+
if (snap.cited && !previousCited.has(key)) {
|
|
14331
|
+
gains.push({
|
|
14332
|
+
keyword: snap.keyword,
|
|
14333
|
+
provider: snap.provider,
|
|
14334
|
+
citationUrl: snap.citationUrl,
|
|
14335
|
+
position: snap.position,
|
|
14336
|
+
snippet: snap.snippet,
|
|
14337
|
+
runId: currentRun.runId
|
|
14338
|
+
});
|
|
14339
|
+
}
|
|
14340
|
+
}
|
|
14341
|
+
return gains;
|
|
14342
|
+
}
|
|
14343
|
+
|
|
14344
|
+
// ../intelligence/src/health.ts
|
|
14345
|
+
function computeHealth(run) {
|
|
14346
|
+
const providerStats = /* @__PURE__ */ new Map();
|
|
14347
|
+
let totalPairs = 0;
|
|
14348
|
+
let citedPairs = 0;
|
|
14349
|
+
for (const snap of run.snapshots) {
|
|
14350
|
+
totalPairs++;
|
|
14351
|
+
if (snap.cited) citedPairs++;
|
|
14352
|
+
const stats = providerStats.get(snap.provider) ?? { cited: 0, total: 0 };
|
|
14353
|
+
stats.total++;
|
|
14354
|
+
if (snap.cited) stats.cited++;
|
|
14355
|
+
providerStats.set(snap.provider, stats);
|
|
14356
|
+
}
|
|
14357
|
+
const providerBreakdown = {};
|
|
14358
|
+
for (const [provider, stats] of providerStats) {
|
|
14359
|
+
providerBreakdown[provider] = {
|
|
14360
|
+
citedRate: stats.total > 0 ? stats.cited / stats.total : 0,
|
|
14361
|
+
cited: stats.cited,
|
|
14362
|
+
total: stats.total
|
|
14363
|
+
};
|
|
14364
|
+
}
|
|
14365
|
+
return {
|
|
14366
|
+
overallCitedRate: totalPairs > 0 ? citedPairs / totalPairs : 0,
|
|
14367
|
+
totalPairs,
|
|
14368
|
+
citedPairs,
|
|
14369
|
+
providerBreakdown
|
|
14370
|
+
};
|
|
14371
|
+
}
|
|
14372
|
+
function computeHealthTrend(runs2) {
|
|
14373
|
+
if (runs2.length === 0) {
|
|
14374
|
+
return { current: 0, previous: 0, delta: 0 };
|
|
14375
|
+
}
|
|
14376
|
+
const current = computeHealth(runs2[runs2.length - 1]).overallCitedRate;
|
|
14377
|
+
if (runs2.length === 1) {
|
|
14378
|
+
return { current, previous: 0, delta: current };
|
|
14379
|
+
}
|
|
14380
|
+
const previous = computeHealth(runs2[runs2.length - 2]).overallCitedRate;
|
|
14381
|
+
return {
|
|
14382
|
+
current,
|
|
14383
|
+
previous,
|
|
14384
|
+
delta: current - previous
|
|
14385
|
+
};
|
|
14386
|
+
}
|
|
14387
|
+
|
|
14388
|
+
// ../intelligence/src/causes.ts
|
|
14389
|
+
function analyzeCause(regression, currentSnapshots) {
|
|
14390
|
+
const currentSnap = currentSnapshots.find(
|
|
14391
|
+
(s) => s.keyword === regression.keyword && s.provider === regression.provider && !s.cited && s.competitorDomain
|
|
14392
|
+
);
|
|
14393
|
+
if (currentSnap) {
|
|
14394
|
+
return {
|
|
14395
|
+
cause: "competitor_gain",
|
|
14396
|
+
competitorDomain: currentSnap.competitorDomain,
|
|
14397
|
+
details: `Competitor ${currentSnap.competitorDomain} now cited for "${regression.keyword}" on ${regression.provider}`
|
|
14398
|
+
};
|
|
14399
|
+
}
|
|
14400
|
+
return {
|
|
14401
|
+
cause: "unknown",
|
|
14402
|
+
details: `No specific cause identified for loss of "${regression.keyword}" on ${regression.provider}`
|
|
14403
|
+
};
|
|
14404
|
+
}
|
|
14405
|
+
|
|
14406
|
+
// ../intelligence/src/insights.ts
|
|
14407
|
+
import { randomUUID } from "crypto";
|
|
14408
|
+
function generateInsights(regressions, gains, health, causes) {
|
|
14409
|
+
const insights2 = [];
|
|
14410
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
14411
|
+
for (const reg of regressions) {
|
|
14412
|
+
const key = `${reg.keyword}:${reg.provider}`;
|
|
14413
|
+
const cause = causes.get(key);
|
|
14414
|
+
insights2.push({
|
|
14415
|
+
id: `ins_${randomUUID().slice(0, 8)}`,
|
|
14416
|
+
type: "regression",
|
|
14417
|
+
severity: "high",
|
|
14418
|
+
title: `Lost ${reg.provider} citation for "${reg.keyword}"`,
|
|
14419
|
+
keyword: reg.keyword,
|
|
14420
|
+
provider: reg.provider,
|
|
14421
|
+
recommendation: {
|
|
14422
|
+
action: "audit",
|
|
14423
|
+
target: reg.previousCitationUrl,
|
|
14424
|
+
reason: `Page was previously cited at position ${reg.previousPosition ?? "unknown"}. Run aeo-audit to check for content or schema issues.`
|
|
14425
|
+
},
|
|
14426
|
+
cause,
|
|
14427
|
+
createdAt: now
|
|
14428
|
+
});
|
|
14429
|
+
}
|
|
14430
|
+
for (const gain of gains) {
|
|
14431
|
+
insights2.push({
|
|
14432
|
+
id: `ins_${randomUUID().slice(0, 8)}`,
|
|
14433
|
+
type: "gain",
|
|
14434
|
+
severity: "low",
|
|
14435
|
+
title: `New ${gain.provider} citation for "${gain.keyword}"`,
|
|
14436
|
+
keyword: gain.keyword,
|
|
14437
|
+
provider: gain.provider,
|
|
14438
|
+
recommendation: {
|
|
14439
|
+
action: "monitor",
|
|
14440
|
+
target: gain.citationUrl,
|
|
14441
|
+
reason: `New citation appeared at position ${gain.position ?? "unknown"}. Monitor to confirm it persists.`
|
|
14442
|
+
},
|
|
14443
|
+
createdAt: now
|
|
14444
|
+
});
|
|
14445
|
+
}
|
|
14446
|
+
return insights2;
|
|
14447
|
+
}
|
|
14448
|
+
|
|
14449
|
+
// ../intelligence/src/analyzer.ts
|
|
14450
|
+
function analyzeRuns(currentRun, previousRun, allRuns) {
|
|
14451
|
+
const regressions = detectRegressions(currentRun, previousRun);
|
|
14452
|
+
const gains = detectGains(currentRun, previousRun);
|
|
14453
|
+
const health = computeHealth(currentRun);
|
|
14454
|
+
const trend = allRuns ? computeHealthTrend(allRuns) : void 0;
|
|
14455
|
+
const causes = /* @__PURE__ */ new Map();
|
|
14456
|
+
for (const reg of regressions) {
|
|
14457
|
+
const cause = analyzeCause(reg, currentRun.snapshots);
|
|
14458
|
+
causes.set(`${reg.keyword}:${reg.provider}`, cause);
|
|
14459
|
+
}
|
|
14460
|
+
const insights2 = generateInsights(regressions, gains, health, causes);
|
|
14461
|
+
return {
|
|
14462
|
+
regressions,
|
|
14463
|
+
gains,
|
|
14464
|
+
health,
|
|
14465
|
+
trend,
|
|
14466
|
+
insights: insights2
|
|
14467
|
+
};
|
|
14468
|
+
}
|
|
14469
|
+
|
|
14470
|
+
// src/intelligence-service.ts
|
|
14471
|
+
import crypto22 from "crypto";
|
|
14472
|
+
var log6 = createLogger("IntelligenceService");
|
|
14473
|
+
var IntelligenceService = class {
|
|
14474
|
+
constructor(db) {
|
|
14475
|
+
this.db = db;
|
|
14476
|
+
}
|
|
14477
|
+
/**
|
|
14478
|
+
* Analyze a completed run and persist insights + health snapshot.
|
|
14479
|
+
* Idempotent: deletes prior results for the same runId before inserting.
|
|
14480
|
+
* Returns the analysis result for the coordinator to inspect (e.g. for webhook dispatch).
|
|
14481
|
+
*/
|
|
14482
|
+
analyzeAndPersist(runId, projectId) {
|
|
14483
|
+
const recentRuns = this.db.select().from(runs).where(
|
|
14484
|
+
and11(
|
|
14485
|
+
eq23(runs.projectId, projectId),
|
|
14486
|
+
or3(eq23(runs.status, "completed"), eq23(runs.status, "partial"))
|
|
14487
|
+
)
|
|
14488
|
+
).orderBy(desc9(runs.createdAt)).limit(2).all();
|
|
14489
|
+
if (recentRuns.length === 0) {
|
|
14490
|
+
log6.info("intelligence.skip", { runId, reason: "no completed runs" });
|
|
14491
|
+
return null;
|
|
14492
|
+
}
|
|
14493
|
+
const currentRunRecord = recentRuns.find((r) => r.id === runId);
|
|
14494
|
+
if (!currentRunRecord) {
|
|
14495
|
+
log6.info("intelligence.skip", { runId, reason: "run not in recent completed list" });
|
|
14496
|
+
return null;
|
|
14497
|
+
}
|
|
14498
|
+
const currentRun = this.buildRunData(runId, projectId, currentRunRecord.finishedAt ?? currentRunRecord.createdAt);
|
|
14499
|
+
if (currentRun.snapshots.length === 0) {
|
|
14500
|
+
log6.info("intelligence.skip", { runId, reason: "no snapshots" });
|
|
14501
|
+
return null;
|
|
14502
|
+
}
|
|
14503
|
+
const previousRunRecord = recentRuns.find((r) => r.id !== runId);
|
|
14504
|
+
const previousRun = previousRunRecord ? this.buildRunData(previousRunRecord.id, projectId, previousRunRecord.finishedAt ?? previousRunRecord.createdAt) : null;
|
|
14505
|
+
if (!previousRun) {
|
|
14506
|
+
const result2 = analyzeRuns(currentRun, currentRun);
|
|
14507
|
+
log6.info("intelligence.analyzed", {
|
|
14508
|
+
runId,
|
|
14509
|
+
regressions: 0,
|
|
14510
|
+
gains: 0,
|
|
14511
|
+
citedRate: result2.health.overallCitedRate,
|
|
14512
|
+
insights: 0
|
|
14513
|
+
});
|
|
14514
|
+
this.persistResult({ ...result2, insights: [], regressions: [], gains: [] }, runId, projectId);
|
|
14515
|
+
return result2;
|
|
14516
|
+
}
|
|
14517
|
+
const result = analyzeRuns(currentRun, previousRun);
|
|
14518
|
+
log6.info("intelligence.analyzed", {
|
|
14519
|
+
runId,
|
|
14520
|
+
regressions: result.regressions.length,
|
|
14521
|
+
gains: result.gains.length,
|
|
14522
|
+
citedRate: result.health.overallCitedRate,
|
|
14523
|
+
insights: result.insights.length
|
|
14524
|
+
});
|
|
14525
|
+
this.persistResult(result, runId, projectId);
|
|
14526
|
+
return result;
|
|
14527
|
+
}
|
|
14528
|
+
/**
|
|
14529
|
+
* Analyze a single run given an explicit previous run (or null for first run).
|
|
14530
|
+
* Used by backfill where we control the run ordering.
|
|
14531
|
+
*/
|
|
14532
|
+
analyzeRunWithPrevious(runRecord, previousRunRecord) {
|
|
14533
|
+
const currentRun = this.buildRunData(runRecord.id, runRecord.projectId, runRecord.finishedAt ?? runRecord.createdAt);
|
|
14534
|
+
if (currentRun.snapshots.length === 0) {
|
|
14535
|
+
return null;
|
|
14536
|
+
}
|
|
14537
|
+
const previousRun = previousRunRecord ? this.buildRunData(previousRunRecord.id, previousRunRecord.projectId, previousRunRecord.finishedAt ?? previousRunRecord.createdAt) : null;
|
|
14538
|
+
if (!previousRun) {
|
|
14539
|
+
const result2 = analyzeRuns(currentRun, currentRun);
|
|
14540
|
+
this.persistResult({ ...result2, insights: [], regressions: [], gains: [] }, runRecord.id, runRecord.projectId);
|
|
14541
|
+
return result2;
|
|
14542
|
+
}
|
|
14543
|
+
const result = analyzeRuns(currentRun, previousRun);
|
|
14544
|
+
this.persistResult(result, runRecord.id, runRecord.projectId);
|
|
14545
|
+
return result;
|
|
14546
|
+
}
|
|
14547
|
+
/**
|
|
14548
|
+
* Backfill intelligence for all completed/partial runs of a project.
|
|
14549
|
+
* Processes runs in chronological order so each run compares against its predecessor.
|
|
14550
|
+
*/
|
|
14551
|
+
backfill(projectName, opts, onProgress) {
|
|
14552
|
+
const project = this.db.select().from(projects).where(eq23(projects.name, projectName)).get();
|
|
14553
|
+
if (!project) {
|
|
14554
|
+
throw new Error(`Project "${projectName}" not found`);
|
|
14555
|
+
}
|
|
14556
|
+
const allRuns = this.db.select().from(runs).where(
|
|
14557
|
+
and11(
|
|
14558
|
+
eq23(runs.projectId, project.id),
|
|
14559
|
+
or3(eq23(runs.status, "completed"), eq23(runs.status, "partial"))
|
|
14560
|
+
)
|
|
14561
|
+
).orderBy(asc2(runs.finishedAt)).all();
|
|
14562
|
+
let startIdx = 0;
|
|
14563
|
+
let endIdx = allRuns.length;
|
|
14564
|
+
if (opts?.fromRunId) {
|
|
14565
|
+
const idx = allRuns.findIndex((r) => r.id === opts.fromRunId);
|
|
14566
|
+
if (idx === -1) throw new Error(`Run "${opts.fromRunId}" not found in project`);
|
|
14567
|
+
startIdx = idx;
|
|
14568
|
+
}
|
|
14569
|
+
if (opts?.toRunId) {
|
|
14570
|
+
const idx = allRuns.findIndex((r) => r.id === opts.toRunId);
|
|
14571
|
+
if (idx === -1) throw new Error(`Run "${opts.toRunId}" not found in project`);
|
|
14572
|
+
endIdx = idx + 1;
|
|
14573
|
+
}
|
|
14574
|
+
const targetRuns = allRuns.slice(startIdx, endIdx);
|
|
14575
|
+
let processed = 0;
|
|
14576
|
+
let skipped = 0;
|
|
14577
|
+
let totalInsights = 0;
|
|
14578
|
+
for (let i = 0; i < targetRuns.length; i++) {
|
|
14579
|
+
const run = targetRuns[i];
|
|
14580
|
+
const globalIdx = allRuns.indexOf(run);
|
|
14581
|
+
const previousRun = globalIdx > 0 ? allRuns[globalIdx - 1] : null;
|
|
14582
|
+
const result = this.analyzeRunWithPrevious(run, previousRun);
|
|
14583
|
+
if (result) {
|
|
14584
|
+
processed++;
|
|
14585
|
+
totalInsights += result.insights.length;
|
|
14586
|
+
onProgress?.({ runId: run.id, index: i + 1, total: targetRuns.length, insights: result.insights.length });
|
|
14587
|
+
} else {
|
|
14588
|
+
skipped++;
|
|
14589
|
+
onProgress?.({ runId: run.id, index: i + 1, total: targetRuns.length, insights: 0 });
|
|
14590
|
+
}
|
|
14591
|
+
}
|
|
14592
|
+
return { processed, skipped, totalInsights };
|
|
14593
|
+
}
|
|
14594
|
+
persistResult(result, runId, projectId) {
|
|
14595
|
+
const previouslyDismissed = /* @__PURE__ */ new Set();
|
|
14596
|
+
const existingInsights = this.db.select({ keyword: insights.keyword, provider: insights.provider, type: insights.type, dismissed: insights.dismissed }).from(insights).where(eq23(insights.runId, runId)).all();
|
|
14597
|
+
for (const row of existingInsights) {
|
|
14598
|
+
if (row.dismissed) {
|
|
14599
|
+
previouslyDismissed.add(`${row.keyword}:${row.provider}:${row.type}`);
|
|
14600
|
+
}
|
|
14601
|
+
}
|
|
14602
|
+
this.db.transaction((tx) => {
|
|
14603
|
+
tx.delete(insights).where(eq23(insights.runId, runId)).run();
|
|
14604
|
+
tx.delete(healthSnapshots).where(eq23(healthSnapshots.runId, runId)).run();
|
|
14605
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
14606
|
+
for (const insight of result.insights) {
|
|
14607
|
+
const wasDismissed = previouslyDismissed.has(`${insight.keyword}:${insight.provider}:${insight.type}`);
|
|
14608
|
+
tx.insert(insights).values({
|
|
14609
|
+
id: insight.id,
|
|
14610
|
+
projectId,
|
|
14611
|
+
runId,
|
|
14612
|
+
type: insight.type,
|
|
14613
|
+
severity: insight.severity,
|
|
14614
|
+
title: insight.title,
|
|
14615
|
+
keyword: insight.keyword,
|
|
14616
|
+
provider: insight.provider,
|
|
14617
|
+
recommendation: insight.recommendation ? JSON.stringify(insight.recommendation) : null,
|
|
14618
|
+
cause: insight.cause ? JSON.stringify(insight.cause) : null,
|
|
14619
|
+
dismissed: wasDismissed,
|
|
14620
|
+
createdAt: insight.createdAt
|
|
14621
|
+
}).run();
|
|
14622
|
+
}
|
|
14623
|
+
tx.insert(healthSnapshots).values({
|
|
14624
|
+
id: crypto22.randomUUID(),
|
|
14625
|
+
projectId,
|
|
14626
|
+
runId,
|
|
14627
|
+
overallCitedRate: String(result.health.overallCitedRate),
|
|
14628
|
+
totalPairs: result.health.totalPairs,
|
|
14629
|
+
citedPairs: result.health.citedPairs,
|
|
14630
|
+
providerBreakdown: JSON.stringify(result.health.providerBreakdown),
|
|
14631
|
+
createdAt: now
|
|
14632
|
+
}).run();
|
|
14633
|
+
});
|
|
14634
|
+
log6.info("intelligence.persisted", { runId, insights: result.insights.length });
|
|
14635
|
+
}
|
|
14636
|
+
buildRunData(runId, projectId, completedAt) {
|
|
14637
|
+
const rows = this.db.select({
|
|
14638
|
+
keyword: keywords.keyword,
|
|
14639
|
+
provider: querySnapshots.provider,
|
|
14640
|
+
citationState: querySnapshots.citationState,
|
|
14641
|
+
citedDomains: querySnapshots.citedDomains,
|
|
14642
|
+
competitorOverlap: querySnapshots.competitorOverlap
|
|
14643
|
+
}).from(querySnapshots).leftJoin(keywords, eq23(querySnapshots.keywordId, keywords.id)).where(eq23(querySnapshots.runId, runId)).all();
|
|
14644
|
+
const snapshots = rows.map((r) => {
|
|
14645
|
+
const domains = parseJsonColumn(r.citedDomains, []);
|
|
14646
|
+
const competitors2 = parseJsonColumn(r.competitorOverlap, []);
|
|
14647
|
+
return {
|
|
14648
|
+
keyword: r.keyword ?? "",
|
|
14649
|
+
provider: r.provider,
|
|
14650
|
+
cited: r.citationState === "cited",
|
|
14651
|
+
citationUrl: domains[0] ?? void 0,
|
|
14652
|
+
competitorDomain: competitors2[0] ?? void 0
|
|
14653
|
+
};
|
|
14654
|
+
});
|
|
14655
|
+
return { runId, projectId, completedAt, snapshots };
|
|
14656
|
+
}
|
|
14657
|
+
};
|
|
14658
|
+
|
|
14659
|
+
// src/run-coordinator.ts
|
|
14660
|
+
var log7 = createLogger("RunCoordinator");
|
|
14661
|
+
var RunCoordinator = class {
|
|
14662
|
+
constructor(notifier, intelligenceService) {
|
|
14663
|
+
this.notifier = notifier;
|
|
14664
|
+
this.intelligenceService = intelligenceService;
|
|
14665
|
+
}
|
|
14666
|
+
async onRunCompleted(runId, projectId) {
|
|
14667
|
+
try {
|
|
14668
|
+
this.intelligenceService.analyzeAndPersist(runId, projectId);
|
|
14669
|
+
} catch (err) {
|
|
14670
|
+
log7.error("intelligence.failed", { runId, error: err instanceof Error ? err.message : String(err) });
|
|
14671
|
+
}
|
|
14672
|
+
try {
|
|
14673
|
+
await this.notifier.onRunCompleted(runId, projectId);
|
|
14674
|
+
} catch (err) {
|
|
14675
|
+
log7.error("notifier.failed", { runId, error: err instanceof Error ? err.message : String(err) });
|
|
14676
|
+
}
|
|
14677
|
+
}
|
|
14678
|
+
};
|
|
14679
|
+
|
|
13823
14680
|
// src/snapshot-service.ts
|
|
13824
14681
|
import { runAeoAudit } from "@ainyc/aeo-audit";
|
|
13825
14682
|
|
|
@@ -13940,7 +14797,7 @@ function formatAuditFactorScore(factor) {
|
|
|
13940
14797
|
}
|
|
13941
14798
|
|
|
13942
14799
|
// src/snapshot-service.ts
|
|
13943
|
-
var
|
|
14800
|
+
var log8 = createLogger("Snapshot");
|
|
13944
14801
|
var ANALYSIS_PROVIDER_PRIORITY = ["openai", "claude", "gemini", "perplexity", "local"];
|
|
13945
14802
|
var SNAPSHOT_QUERY_COUNT = 6;
|
|
13946
14803
|
var ProviderExecutionGate2 = class {
|
|
@@ -14083,7 +14940,7 @@ var SnapshotService = class {
|
|
|
14083
14940
|
return mapAuditReport(report);
|
|
14084
14941
|
} catch (err) {
|
|
14085
14942
|
const message = err instanceof Error ? err.message : String(err);
|
|
14086
|
-
|
|
14943
|
+
log8.warn("audit.failed", { homepageUrl, error: message });
|
|
14087
14944
|
return {
|
|
14088
14945
|
url: homepageUrl,
|
|
14089
14946
|
finalUrl: homepageUrl,
|
|
@@ -14113,7 +14970,7 @@ var SnapshotService = class {
|
|
|
14113
14970
|
phrases: parsedPhrases
|
|
14114
14971
|
};
|
|
14115
14972
|
} catch (err) {
|
|
14116
|
-
|
|
14973
|
+
log8.warn("profile.generation-failed", {
|
|
14117
14974
|
domain: ctx.domain,
|
|
14118
14975
|
provider: ctx.analysisProvider.adapter.name,
|
|
14119
14976
|
error: err instanceof Error ? err.message : String(err)
|
|
@@ -14255,7 +15112,7 @@ var SnapshotService = class {
|
|
|
14255
15112
|
recommendedActions: uniqueStrings(parsed.recommendedActions ?? []).slice(0, 4)
|
|
14256
15113
|
};
|
|
14257
15114
|
} catch (err) {
|
|
14258
|
-
|
|
15115
|
+
log8.warn("response.analysis-failed", {
|
|
14259
15116
|
provider: ctx.analysisProvider.adapter.name,
|
|
14260
15117
|
error: err instanceof Error ? err.message : String(err)
|
|
14261
15118
|
});
|
|
@@ -14540,7 +15397,7 @@ function clipText(value, length) {
|
|
|
14540
15397
|
// src/server.ts
|
|
14541
15398
|
var _require2 = createRequire2(import.meta.url);
|
|
14542
15399
|
var { version: PKG_VERSION } = _require2("../package.json");
|
|
14543
|
-
var
|
|
15400
|
+
var log9 = createLogger("Server");
|
|
14544
15401
|
var DEFAULT_QUOTA = {
|
|
14545
15402
|
maxConcurrency: 2,
|
|
14546
15403
|
maxRequestsPerMinute: 10,
|
|
@@ -14571,7 +15428,7 @@ function summarizeProviderConfig(provider, config) {
|
|
|
14571
15428
|
};
|
|
14572
15429
|
}
|
|
14573
15430
|
function hashApiKey(key) {
|
|
14574
|
-
return
|
|
15431
|
+
return crypto23.createHash("sha256").update(key).digest("hex");
|
|
14575
15432
|
}
|
|
14576
15433
|
function parseCookies2(header) {
|
|
14577
15434
|
if (!header) return {};
|
|
@@ -14630,7 +15487,7 @@ async function createServer(opts) {
|
|
|
14630
15487
|
quota: opts.config.geminiQuota
|
|
14631
15488
|
};
|
|
14632
15489
|
}
|
|
14633
|
-
|
|
15490
|
+
log9.info("providers.configured", { providers: Object.keys(providers).filter((k) => {
|
|
14634
15491
|
const p = providers[k];
|
|
14635
15492
|
return p?.apiKey || p?.baseUrl || p?.vertexProject;
|
|
14636
15493
|
}) });
|
|
@@ -14666,7 +15523,9 @@ async function createServer(opts) {
|
|
|
14666
15523
|
const jobRunner = new JobRunner(opts.db, registry);
|
|
14667
15524
|
jobRunner.recoverStaleRuns();
|
|
14668
15525
|
const notifier = new Notifier(opts.db, serverUrl);
|
|
14669
|
-
|
|
15526
|
+
const intelligenceService = new IntelligenceService(opts.db);
|
|
15527
|
+
const runCoordinator = new RunCoordinator(notifier, intelligenceService);
|
|
15528
|
+
jobRunner.onRunCompleted = (runId, projectId) => runCoordinator.onRunCompleted(runId, projectId);
|
|
14670
15529
|
const snapshotService = new SnapshotService(registry);
|
|
14671
15530
|
const scheduler = new Scheduler(opts.db, {
|
|
14672
15531
|
onRunCreated: (runId, projectId, providers2) => {
|
|
@@ -14743,7 +15602,7 @@ async function createServer(opts) {
|
|
|
14743
15602
|
return removed;
|
|
14744
15603
|
}
|
|
14745
15604
|
};
|
|
14746
|
-
const googleStateSecret = process.env.GOOGLE_STATE_SECRET ??
|
|
15605
|
+
const googleStateSecret = process.env.GOOGLE_STATE_SECRET ?? crypto23.randomBytes(32).toString("hex");
|
|
14747
15606
|
const googleConnectionStore = {
|
|
14748
15607
|
listConnections: (domain) => listGoogleConnections(opts.config, domain),
|
|
14749
15608
|
getConnection: (domain, connectionType) => getGoogleConnection(opts.config, domain, connectionType),
|
|
@@ -14789,11 +15648,11 @@ async function createServer(opts) {
|
|
|
14789
15648
|
const apiPrefix = basePath ? `${basePath}api/v1` : "/api/v1";
|
|
14790
15649
|
if (opts.config.apiKey) {
|
|
14791
15650
|
const keyHash = hashApiKey(opts.config.apiKey);
|
|
14792
|
-
const existing = opts.db.select().from(apiKeys).where(
|
|
15651
|
+
const existing = opts.db.select().from(apiKeys).where(eq24(apiKeys.keyHash, keyHash)).get();
|
|
14793
15652
|
if (!existing) {
|
|
14794
15653
|
const prefix = opts.config.apiKey.slice(0, 12);
|
|
14795
15654
|
opts.db.insert(apiKeys).values({
|
|
14796
|
-
id: `key_${
|
|
15655
|
+
id: `key_${crypto23.randomBytes(8).toString("hex")}`,
|
|
14797
15656
|
name: "default",
|
|
14798
15657
|
keyHash,
|
|
14799
15658
|
keyPrefix: prefix,
|
|
@@ -14817,7 +15676,7 @@ async function createServer(opts) {
|
|
|
14817
15676
|
};
|
|
14818
15677
|
const createSession = (apiKeyId) => {
|
|
14819
15678
|
pruneExpiredSessions();
|
|
14820
|
-
const sessionId =
|
|
15679
|
+
const sessionId = crypto23.randomBytes(32).toString("hex");
|
|
14821
15680
|
sessions.set(sessionId, {
|
|
14822
15681
|
apiKeyId,
|
|
14823
15682
|
expiresAt: Date.now() + SESSION_TTL_MS
|
|
@@ -14841,7 +15700,7 @@ async function createServer(opts) {
|
|
|
14841
15700
|
};
|
|
14842
15701
|
const getDefaultApiKey = () => {
|
|
14843
15702
|
if (!opts.config.apiKey) return void 0;
|
|
14844
|
-
return opts.db.select().from(apiKeys).where(
|
|
15703
|
+
return opts.db.select().from(apiKeys).where(eq24(apiKeys.keyHash, hashApiKey(opts.config.apiKey))).get();
|
|
14845
15704
|
};
|
|
14846
15705
|
const createPasswordSession = (reply) => {
|
|
14847
15706
|
const key = getDefaultApiKey();
|
|
@@ -14898,12 +15757,12 @@ async function createServer(opts) {
|
|
|
14898
15757
|
return reply.send({ authenticated: true });
|
|
14899
15758
|
}
|
|
14900
15759
|
if (apiKey) {
|
|
14901
|
-
const key = opts.db.select().from(apiKeys).where(
|
|
15760
|
+
const key = opts.db.select().from(apiKeys).where(eq24(apiKeys.keyHash, hashApiKey(apiKey))).get();
|
|
14902
15761
|
if (!key || key.revokedAt) {
|
|
14903
15762
|
const err2 = authInvalid();
|
|
14904
15763
|
return reply.status(err2.statusCode).send(err2.toJSON());
|
|
14905
15764
|
}
|
|
14906
|
-
opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
15765
|
+
opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq24(apiKeys.id, key.id)).run();
|
|
14907
15766
|
const sessionId = createSession(key.id);
|
|
14908
15767
|
reply.header("set-cookie", serializeSessionCookie({
|
|
14909
15768
|
name: SESSION_COOKIE_NAME,
|
|
@@ -15048,7 +15907,7 @@ async function createServer(opts) {
|
|
|
15048
15907
|
const targetProjectIds = affectedProjectIds.length > 0 ? affectedProjectIds : [null];
|
|
15049
15908
|
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
15050
15909
|
opts.db.insert(auditLog).values(targetProjectIds.map((projectId) => ({
|
|
15051
|
-
id:
|
|
15910
|
+
id: crypto23.randomUUID(),
|
|
15052
15911
|
projectId,
|
|
15053
15912
|
actor: "api",
|
|
15054
15913
|
action: existing ? "provider.updated" : "provider.created",
|
|
@@ -15314,6 +16173,7 @@ export {
|
|
|
15314
16173
|
notificationEventSchema,
|
|
15315
16174
|
effectiveDomains,
|
|
15316
16175
|
determineAnswerMentioned,
|
|
16176
|
+
IntelligenceService,
|
|
15317
16177
|
setGoogleAuthConfig,
|
|
15318
16178
|
formatAuditFactorScore,
|
|
15319
16179
|
createServer
|