@ainyc/canonry 1.37.1 → 1.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -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,
|
|
@@ -1528,6 +1530,39 @@ var usageCounters = sqliteTable("usage_counters", {
|
|
|
1528
1530
|
uniqueIndex("idx_usage_scope_period_metric").on(table.scope, table.period, table.metric),
|
|
1529
1531
|
index("idx_usage_scope_period").on(table.scope, table.period)
|
|
1530
1532
|
]);
|
|
1533
|
+
var insights = sqliteTable("insights", {
|
|
1534
|
+
id: text("id").primaryKey(),
|
|
1535
|
+
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
1536
|
+
runId: text("run_id").references(() => runs.id, { onDelete: "cascade" }),
|
|
1537
|
+
type: text("type").notNull(),
|
|
1538
|
+
severity: text("severity").notNull(),
|
|
1539
|
+
title: text("title").notNull(),
|
|
1540
|
+
keyword: text("keyword").notNull(),
|
|
1541
|
+
provider: text("provider").notNull(),
|
|
1542
|
+
recommendation: text("recommendation"),
|
|
1543
|
+
cause: text("cause"),
|
|
1544
|
+
dismissed: integer("dismissed", { mode: "boolean" }).notNull().default(false),
|
|
1545
|
+
createdAt: text("created_at").notNull()
|
|
1546
|
+
}, (table) => [
|
|
1547
|
+
index("idx_insights_project").on(table.projectId),
|
|
1548
|
+
index("idx_insights_run").on(table.runId),
|
|
1549
|
+
index("idx_insights_created").on(table.createdAt),
|
|
1550
|
+
index("idx_insights_keyword_provider").on(table.keyword, table.provider)
|
|
1551
|
+
]);
|
|
1552
|
+
var healthSnapshots = sqliteTable("health_snapshots", {
|
|
1553
|
+
id: text("id").primaryKey(),
|
|
1554
|
+
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
1555
|
+
runId: text("run_id").references(() => runs.id, { onDelete: "cascade" }),
|
|
1556
|
+
overallCitedRate: text("overall_cited_rate").notNull(),
|
|
1557
|
+
totalPairs: integer("total_pairs").notNull(),
|
|
1558
|
+
citedPairs: integer("cited_pairs").notNull(),
|
|
1559
|
+
providerBreakdown: text("provider_breakdown").notNull().default("{}"),
|
|
1560
|
+
createdAt: text("created_at").notNull()
|
|
1561
|
+
}, (table) => [
|
|
1562
|
+
index("idx_health_snapshots_project").on(table.projectId),
|
|
1563
|
+
index("idx_health_snapshots_run").on(table.runId),
|
|
1564
|
+
index("idx_health_snapshots_created").on(table.createdAt)
|
|
1565
|
+
]);
|
|
1531
1566
|
|
|
1532
1567
|
// ../db/src/client.ts
|
|
1533
1568
|
function createClient(databasePath) {
|
|
@@ -1692,6 +1727,7 @@ var MIGRATIONS = [
|
|
|
1692
1727
|
// v5b: Backfill model from rawResponse JSON for existing snapshots
|
|
1693
1728
|
`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
1729
|
// v6: Google Search Console integration — google_connections table (domain-scoped)
|
|
1730
|
+
// WARNING: access_token, refresh_token are authentication material; consider storing in config.yaml per CLAUDE.md
|
|
1695
1731
|
`CREATE TABLE IF NOT EXISTS google_connections (
|
|
1696
1732
|
id TEXT PRIMARY KEY,
|
|
1697
1733
|
domain TEXT NOT NULL,
|
|
@@ -1807,6 +1843,7 @@ var MIGRATIONS = [
|
|
|
1807
1843
|
`CREATE INDEX IF NOT EXISTS idx_bing_keyword_project ON bing_keyword_stats(project_id)`,
|
|
1808
1844
|
`CREATE INDEX IF NOT EXISTS idx_bing_keyword_query ON bing_keyword_stats(query)`,
|
|
1809
1845
|
// v13: Google Analytics 4 — ga_connections table (service account auth)
|
|
1846
|
+
// WARNING: private_key is authentication material; consider storing in config.yaml per CLAUDE.md
|
|
1810
1847
|
`CREATE TABLE IF NOT EXISTS ga_connections (
|
|
1811
1848
|
id TEXT PRIMARY KEY,
|
|
1812
1849
|
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
@@ -1876,8 +1913,52 @@ var MIGRATIONS = [
|
|
|
1876
1913
|
`ALTER TABLE ga_ai_referrals ADD COLUMN source_dimension TEXT NOT NULL DEFAULT 'session'`,
|
|
1877
1914
|
// Replace old unique index with one that includes source_dimension
|
|
1878
1915
|
`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)
|
|
1916
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS idx_ga_ai_ref_unique_v2 ON ga_ai_referrals(project_id, date, source, medium, source_dimension)`,
|
|
1917
|
+
// v21: Add missing indexes for query_snapshots filtering
|
|
1918
|
+
`CREATE INDEX IF NOT EXISTS idx_snapshots_citation_state ON query_snapshots(citation_state)`,
|
|
1919
|
+
`CREATE INDEX IF NOT EXISTS idx_snapshots_provider_model ON query_snapshots(provider, model)`,
|
|
1920
|
+
`CREATE INDEX IF NOT EXISTS idx_snapshots_location ON query_snapshots(location)`,
|
|
1921
|
+
// v22: Intelligence — insights table for regression/gain/opportunity tracking
|
|
1922
|
+
`CREATE TABLE IF NOT EXISTS insights (
|
|
1923
|
+
id TEXT PRIMARY KEY,
|
|
1924
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
1925
|
+
type TEXT NOT NULL,
|
|
1926
|
+
severity TEXT NOT NULL,
|
|
1927
|
+
title TEXT NOT NULL,
|
|
1928
|
+
keyword TEXT NOT NULL,
|
|
1929
|
+
provider TEXT NOT NULL,
|
|
1930
|
+
recommendation TEXT,
|
|
1931
|
+
cause TEXT,
|
|
1932
|
+
dismissed INTEGER NOT NULL DEFAULT 0,
|
|
1933
|
+
created_at TEXT NOT NULL
|
|
1934
|
+
)`,
|
|
1935
|
+
`CREATE INDEX IF NOT EXISTS idx_insights_project ON insights(project_id)`,
|
|
1936
|
+
`CREATE INDEX IF NOT EXISTS idx_insights_created ON insights(created_at)`,
|
|
1937
|
+
`CREATE INDEX IF NOT EXISTS idx_insights_keyword_provider ON insights(keyword, provider)`,
|
|
1938
|
+
// v23: Intelligence — health_snapshots table for citation health over time
|
|
1939
|
+
`CREATE TABLE IF NOT EXISTS health_snapshots (
|
|
1940
|
+
id TEXT PRIMARY KEY,
|
|
1941
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
1942
|
+
overall_cited_rate TEXT NOT NULL,
|
|
1943
|
+
total_pairs INTEGER NOT NULL,
|
|
1944
|
+
cited_pairs INTEGER NOT NULL,
|
|
1945
|
+
provider_breakdown TEXT NOT NULL DEFAULT '{}',
|
|
1946
|
+
created_at TEXT NOT NULL
|
|
1947
|
+
)`,
|
|
1948
|
+
`CREATE INDEX IF NOT EXISTS idx_health_snapshots_project ON health_snapshots(project_id)`,
|
|
1949
|
+
`CREATE INDEX IF NOT EXISTS idx_health_snapshots_created ON health_snapshots(created_at)`,
|
|
1950
|
+
// v24: Intelligence — add run_id to insights and health_snapshots for per-run correlation and idempotency
|
|
1951
|
+
`ALTER TABLE insights ADD COLUMN run_id TEXT REFERENCES runs(id) ON DELETE CASCADE`,
|
|
1952
|
+
`CREATE INDEX IF NOT EXISTS idx_insights_run ON insights(run_id)`,
|
|
1953
|
+
`ALTER TABLE health_snapshots ADD COLUMN run_id TEXT REFERENCES runs(id) ON DELETE CASCADE`,
|
|
1954
|
+
`CREATE INDEX IF NOT EXISTS idx_health_snapshots_run ON health_snapshots(run_id)`
|
|
1880
1955
|
];
|
|
1956
|
+
function isDuplicateColumnError(err) {
|
|
1957
|
+
if (!(err instanceof Error)) return false;
|
|
1958
|
+
if (err.message.includes("duplicate column name")) return true;
|
|
1959
|
+
if (err.cause instanceof Error && err.cause.message.includes("duplicate column name")) return true;
|
|
1960
|
+
return false;
|
|
1961
|
+
}
|
|
1881
1962
|
function migrate(db) {
|
|
1882
1963
|
const statements = MIGRATION_SQL.split(";").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
1883
1964
|
for (const statement of statements) {
|
|
@@ -1886,7 +1967,9 @@ function migrate(db) {
|
|
|
1886
1967
|
for (const migration of MIGRATIONS) {
|
|
1887
1968
|
try {
|
|
1888
1969
|
db.run(sql.raw(migration));
|
|
1889
|
-
} catch {
|
|
1970
|
+
} catch (err) {
|
|
1971
|
+
if (isDuplicateColumnError(err)) continue;
|
|
1972
|
+
throw err;
|
|
1890
1973
|
}
|
|
1891
1974
|
}
|
|
1892
1975
|
}
|
|
@@ -3805,6 +3888,81 @@ function buildCategoryCounts(counts) {
|
|
|
3805
3888
|
return result;
|
|
3806
3889
|
}
|
|
3807
3890
|
|
|
3891
|
+
// ../api-routes/src/intelligence.ts
|
|
3892
|
+
import { eq as eq11, desc as desc4, and as and2 } from "drizzle-orm";
|
|
3893
|
+
function mapInsightRow(r) {
|
|
3894
|
+
return {
|
|
3895
|
+
id: r.id,
|
|
3896
|
+
projectId: r.projectId,
|
|
3897
|
+
runId: r.runId ?? null,
|
|
3898
|
+
type: r.type,
|
|
3899
|
+
severity: r.severity,
|
|
3900
|
+
title: r.title,
|
|
3901
|
+
keyword: r.keyword,
|
|
3902
|
+
provider: r.provider,
|
|
3903
|
+
recommendation: parseJsonColumn(r.recommendation, void 0),
|
|
3904
|
+
cause: parseJsonColumn(r.cause, void 0),
|
|
3905
|
+
dismissed: r.dismissed,
|
|
3906
|
+
createdAt: r.createdAt
|
|
3907
|
+
};
|
|
3908
|
+
}
|
|
3909
|
+
function mapHealthRow(r) {
|
|
3910
|
+
return {
|
|
3911
|
+
id: r.id,
|
|
3912
|
+
projectId: r.projectId,
|
|
3913
|
+
runId: r.runId ?? null,
|
|
3914
|
+
overallCitedRate: Number(r.overallCitedRate),
|
|
3915
|
+
totalPairs: r.totalPairs,
|
|
3916
|
+
citedPairs: r.citedPairs,
|
|
3917
|
+
providerBreakdown: parseJsonColumn(r.providerBreakdown, {}),
|
|
3918
|
+
createdAt: r.createdAt
|
|
3919
|
+
};
|
|
3920
|
+
}
|
|
3921
|
+
async function intelligenceRoutes(app) {
|
|
3922
|
+
app.get("/projects/:name/insights", async (request, reply) => {
|
|
3923
|
+
const project = resolveProject(app.db, request.params.name);
|
|
3924
|
+
const conditions = [eq11(insights.projectId, project.id)];
|
|
3925
|
+
if (request.query.runId) {
|
|
3926
|
+
conditions.push(eq11(insights.runId, request.query.runId));
|
|
3927
|
+
}
|
|
3928
|
+
const rows = app.db.select().from(insights).where(conditions.length === 1 ? conditions[0] : and2(...conditions)).orderBy(desc4(insights.createdAt)).all();
|
|
3929
|
+
const showDismissed = request.query.dismissed === "true";
|
|
3930
|
+
const result = rows.filter((r) => showDismissed || !r.dismissed).map(mapInsightRow);
|
|
3931
|
+
return reply.send(result);
|
|
3932
|
+
});
|
|
3933
|
+
app.get("/projects/:name/insights/:id", async (request, reply) => {
|
|
3934
|
+
const project = resolveProject(app.db, request.params.name);
|
|
3935
|
+
const row = app.db.select().from(insights).where(eq11(insights.id, request.params.id)).get();
|
|
3936
|
+
if (!row || row.projectId !== project.id) {
|
|
3937
|
+
throw notFound("Insight", request.params.id);
|
|
3938
|
+
}
|
|
3939
|
+
return reply.send(mapInsightRow(row));
|
|
3940
|
+
});
|
|
3941
|
+
app.post("/projects/:name/insights/:id/dismiss", async (request, reply) => {
|
|
3942
|
+
const project = resolveProject(app.db, request.params.name);
|
|
3943
|
+
const row = app.db.select().from(insights).where(eq11(insights.id, request.params.id)).get();
|
|
3944
|
+
if (!row || row.projectId !== project.id) {
|
|
3945
|
+
throw notFound("Insight", request.params.id);
|
|
3946
|
+
}
|
|
3947
|
+
app.db.update(insights).set({ dismissed: true }).where(eq11(insights.id, request.params.id)).run();
|
|
3948
|
+
return reply.send({ ok: true });
|
|
3949
|
+
});
|
|
3950
|
+
app.get("/projects/:name/health/latest", async (request, reply) => {
|
|
3951
|
+
const project = resolveProject(app.db, request.params.name);
|
|
3952
|
+
const row = app.db.select().from(healthSnapshots).where(eq11(healthSnapshots.projectId, project.id)).orderBy(desc4(healthSnapshots.createdAt)).limit(1).get();
|
|
3953
|
+
if (!row) {
|
|
3954
|
+
throw notFound("Health data for project", request.params.name);
|
|
3955
|
+
}
|
|
3956
|
+
return reply.send(mapHealthRow(row));
|
|
3957
|
+
});
|
|
3958
|
+
app.get("/projects/:name/health/history", async (request, reply) => {
|
|
3959
|
+
const project = resolveProject(app.db, request.params.name);
|
|
3960
|
+
const limit = request.query.limit ? Math.min(Number(request.query.limit), 100) : 30;
|
|
3961
|
+
const rows = app.db.select().from(healthSnapshots).where(eq11(healthSnapshots.projectId, project.id)).orderBy(desc4(healthSnapshots.createdAt)).limit(limit).all();
|
|
3962
|
+
return reply.send(rows.map(mapHealthRow));
|
|
3963
|
+
});
|
|
3964
|
+
}
|
|
3965
|
+
|
|
3808
3966
|
// ../api-routes/src/openapi.ts
|
|
3809
3967
|
var stringSchema = { type: "string" };
|
|
3810
3968
|
var booleanSchema = { type: "boolean" };
|
|
@@ -5826,6 +5984,75 @@ var routeCatalog = [
|
|
|
5826
5984
|
400: { description: "GA4 is not connected." },
|
|
5827
5985
|
404: { description: "Project not found." }
|
|
5828
5986
|
}
|
|
5987
|
+
},
|
|
5988
|
+
// Intelligence
|
|
5989
|
+
{
|
|
5990
|
+
method: "get",
|
|
5991
|
+
path: "/api/v1/projects/{name}/insights",
|
|
5992
|
+
summary: "List intelligence insights for a project",
|
|
5993
|
+
tags: ["intelligence"],
|
|
5994
|
+
parameters: [
|
|
5995
|
+
nameParameter,
|
|
5996
|
+
{ name: "dismissed", in: "query", description: "Include dismissed insights (true/false).", schema: stringSchema },
|
|
5997
|
+
{ name: "runId", in: "query", description: "Filter by run ID.", schema: stringSchema }
|
|
5998
|
+
],
|
|
5999
|
+
responses: {
|
|
6000
|
+
200: { description: "Insights returned." },
|
|
6001
|
+
404: { description: "Project not found." }
|
|
6002
|
+
}
|
|
6003
|
+
},
|
|
6004
|
+
{
|
|
6005
|
+
method: "get",
|
|
6006
|
+
path: "/api/v1/projects/{name}/insights/{id}",
|
|
6007
|
+
summary: "Get a single insight",
|
|
6008
|
+
tags: ["intelligence"],
|
|
6009
|
+
parameters: [
|
|
6010
|
+
nameParameter,
|
|
6011
|
+
{ name: "id", in: "path", required: true, description: "Insight ID.", schema: stringSchema }
|
|
6012
|
+
],
|
|
6013
|
+
responses: {
|
|
6014
|
+
200: { description: "Insight returned." },
|
|
6015
|
+
404: { description: "Insight not found." }
|
|
6016
|
+
}
|
|
6017
|
+
},
|
|
6018
|
+
{
|
|
6019
|
+
method: "post",
|
|
6020
|
+
path: "/api/v1/projects/{name}/insights/{id}/dismiss",
|
|
6021
|
+
summary: "Dismiss an insight",
|
|
6022
|
+
tags: ["intelligence"],
|
|
6023
|
+
parameters: [
|
|
6024
|
+
nameParameter,
|
|
6025
|
+
{ name: "id", in: "path", required: true, description: "Insight ID.", schema: stringSchema }
|
|
6026
|
+
],
|
|
6027
|
+
responses: {
|
|
6028
|
+
200: { description: "Insight dismissed." },
|
|
6029
|
+
404: { description: "Insight not found." }
|
|
6030
|
+
}
|
|
6031
|
+
},
|
|
6032
|
+
{
|
|
6033
|
+
method: "get",
|
|
6034
|
+
path: "/api/v1/projects/{name}/health/latest",
|
|
6035
|
+
summary: "Get latest health snapshot",
|
|
6036
|
+
tags: ["intelligence"],
|
|
6037
|
+
parameters: [nameParameter],
|
|
6038
|
+
responses: {
|
|
6039
|
+
200: { description: "Health snapshot returned." },
|
|
6040
|
+
404: { description: "Project not found." }
|
|
6041
|
+
}
|
|
6042
|
+
},
|
|
6043
|
+
{
|
|
6044
|
+
method: "get",
|
|
6045
|
+
path: "/api/v1/projects/{name}/health/history",
|
|
6046
|
+
summary: "Get health trend over time",
|
|
6047
|
+
tags: ["intelligence"],
|
|
6048
|
+
parameters: [
|
|
6049
|
+
nameParameter,
|
|
6050
|
+
{ name: "limit", in: "query", description: "Max results.", schema: stringSchema }
|
|
6051
|
+
],
|
|
6052
|
+
responses: {
|
|
6053
|
+
200: { description: "Health history returned." },
|
|
6054
|
+
404: { description: "Project not found." }
|
|
6055
|
+
}
|
|
5829
6056
|
}
|
|
5830
6057
|
];
|
|
5831
6058
|
function buildOpenApiDocument(info = {}) {
|
|
@@ -6072,7 +6299,7 @@ async function telemetryRoutes(app, opts) {
|
|
|
6072
6299
|
|
|
6073
6300
|
// ../api-routes/src/schedules.ts
|
|
6074
6301
|
import crypto11 from "crypto";
|
|
6075
|
-
import { eq as
|
|
6302
|
+
import { eq as eq12 } from "drizzle-orm";
|
|
6076
6303
|
async function scheduleRoutes(app, opts) {
|
|
6077
6304
|
app.put("/projects/:name/schedule", async (request, reply) => {
|
|
6078
6305
|
const project = resolveProject(app.db, request.params.name);
|
|
@@ -6115,7 +6342,7 @@ async function scheduleRoutes(app, opts) {
|
|
|
6115
6342
|
}
|
|
6116
6343
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
6117
6344
|
const enabledInt = enabled === false ? 0 : 1;
|
|
6118
|
-
const existing = app.db.select().from(schedules).where(
|
|
6345
|
+
const existing = app.db.select().from(schedules).where(eq12(schedules.projectId, project.id)).get();
|
|
6119
6346
|
if (existing) {
|
|
6120
6347
|
app.db.update(schedules).set({
|
|
6121
6348
|
cronExpr,
|
|
@@ -6124,7 +6351,7 @@ async function scheduleRoutes(app, opts) {
|
|
|
6124
6351
|
providers: JSON.stringify(providers),
|
|
6125
6352
|
enabled: enabledInt,
|
|
6126
6353
|
updatedAt: now
|
|
6127
|
-
}).where(
|
|
6354
|
+
}).where(eq12(schedules.id, existing.id)).run();
|
|
6128
6355
|
} else {
|
|
6129
6356
|
app.db.insert(schedules).values({
|
|
6130
6357
|
id: crypto11.randomUUID(),
|
|
@@ -6146,12 +6373,12 @@ async function scheduleRoutes(app, opts) {
|
|
|
6146
6373
|
diff: { cronExpr, preset, timezone, providers }
|
|
6147
6374
|
});
|
|
6148
6375
|
opts.onScheduleUpdated?.("upsert", project.id);
|
|
6149
|
-
const schedule = app.db.select().from(schedules).where(
|
|
6376
|
+
const schedule = app.db.select().from(schedules).where(eq12(schedules.projectId, project.id)).get();
|
|
6150
6377
|
return reply.status(existing ? 200 : 201).send(formatSchedule(schedule));
|
|
6151
6378
|
});
|
|
6152
6379
|
app.get("/projects/:name/schedule", async (request, reply) => {
|
|
6153
6380
|
const project = resolveProject(app.db, request.params.name);
|
|
6154
|
-
const schedule = app.db.select().from(schedules).where(
|
|
6381
|
+
const schedule = app.db.select().from(schedules).where(eq12(schedules.projectId, project.id)).get();
|
|
6155
6382
|
if (!schedule) {
|
|
6156
6383
|
throw notFound("Schedule", request.params.name);
|
|
6157
6384
|
}
|
|
@@ -6159,11 +6386,11 @@ async function scheduleRoutes(app, opts) {
|
|
|
6159
6386
|
});
|
|
6160
6387
|
app.delete("/projects/:name/schedule", async (request, reply) => {
|
|
6161
6388
|
const project = resolveProject(app.db, request.params.name);
|
|
6162
|
-
const schedule = app.db.select().from(schedules).where(
|
|
6389
|
+
const schedule = app.db.select().from(schedules).where(eq12(schedules.projectId, project.id)).get();
|
|
6163
6390
|
if (!schedule) {
|
|
6164
6391
|
throw notFound("Schedule", request.params.name);
|
|
6165
6392
|
}
|
|
6166
|
-
app.db.delete(schedules).where(
|
|
6393
|
+
app.db.delete(schedules).where(eq12(schedules.id, schedule.id)).run();
|
|
6167
6394
|
writeAuditLog(app.db, {
|
|
6168
6395
|
projectId: project.id,
|
|
6169
6396
|
actor: "api",
|
|
@@ -6193,7 +6420,7 @@ function formatSchedule(row) {
|
|
|
6193
6420
|
|
|
6194
6421
|
// ../api-routes/src/notifications.ts
|
|
6195
6422
|
import crypto12 from "crypto";
|
|
6196
|
-
import { eq as
|
|
6423
|
+
import { eq as eq13 } from "drizzle-orm";
|
|
6197
6424
|
var VALID_EVENTS = ["citation.lost", "citation.gained", "run.completed", "run.failed"];
|
|
6198
6425
|
async function notificationRoutes(app) {
|
|
6199
6426
|
app.get("/notifications/events", async (_request, reply) => {
|
|
@@ -6232,22 +6459,22 @@ async function notificationRoutes(app) {
|
|
|
6232
6459
|
diff: { channel, ...redactNotificationUrl(url), events }
|
|
6233
6460
|
});
|
|
6234
6461
|
return reply.status(201).send({
|
|
6235
|
-
...formatNotification(app.db.select().from(notifications).where(
|
|
6462
|
+
...formatNotification(app.db.select().from(notifications).where(eq13(notifications.id, id)).get()),
|
|
6236
6463
|
webhookSecret
|
|
6237
6464
|
});
|
|
6238
6465
|
});
|
|
6239
6466
|
app.get("/projects/:name/notifications", async (request, reply) => {
|
|
6240
6467
|
const project = resolveProject(app.db, request.params.name);
|
|
6241
|
-
const rows = app.db.select().from(notifications).where(
|
|
6468
|
+
const rows = app.db.select().from(notifications).where(eq13(notifications.projectId, project.id)).all();
|
|
6242
6469
|
return reply.send(rows.map(formatNotification));
|
|
6243
6470
|
});
|
|
6244
6471
|
app.delete("/projects/:name/notifications/:id", async (request, reply) => {
|
|
6245
6472
|
const project = resolveProject(app.db, request.params.name);
|
|
6246
|
-
const notification = app.db.select().from(notifications).where(
|
|
6473
|
+
const notification = app.db.select().from(notifications).where(eq13(notifications.id, request.params.id)).get();
|
|
6247
6474
|
if (!notification || notification.projectId !== project.id) {
|
|
6248
6475
|
throw notFound("Notification", request.params.id);
|
|
6249
6476
|
}
|
|
6250
|
-
app.db.delete(notifications).where(
|
|
6477
|
+
app.db.delete(notifications).where(eq13(notifications.id, notification.id)).run();
|
|
6251
6478
|
writeAuditLog(app.db, {
|
|
6252
6479
|
projectId: project.id,
|
|
6253
6480
|
actor: "api",
|
|
@@ -6259,7 +6486,7 @@ async function notificationRoutes(app) {
|
|
|
6259
6486
|
});
|
|
6260
6487
|
app.post("/projects/:name/notifications/:id/test", async (request, reply) => {
|
|
6261
6488
|
const project = resolveProject(app.db, request.params.name);
|
|
6262
|
-
const notification = app.db.select().from(notifications).where(
|
|
6489
|
+
const notification = app.db.select().from(notifications).where(eq13(notifications.id, request.params.id)).get();
|
|
6263
6490
|
if (!notification || notification.projectId !== project.id) {
|
|
6264
6491
|
throw notFound("Notification", request.params.id);
|
|
6265
6492
|
}
|
|
@@ -6311,7 +6538,7 @@ function formatNotification(row) {
|
|
|
6311
6538
|
|
|
6312
6539
|
// ../api-routes/src/google.ts
|
|
6313
6540
|
import crypto14 from "crypto";
|
|
6314
|
-
import { eq as
|
|
6541
|
+
import { eq as eq14, and as and3, desc as desc5, sql as sql3 } from "drizzle-orm";
|
|
6315
6542
|
|
|
6316
6543
|
// ../integration-google/src/constants.ts
|
|
6317
6544
|
var GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
@@ -6343,7 +6570,56 @@ var GoogleApiError = class extends Error {
|
|
|
6343
6570
|
};
|
|
6344
6571
|
|
|
6345
6572
|
// ../integration-google/src/oauth.ts
|
|
6573
|
+
function validateClientId(clientId) {
|
|
6574
|
+
if (!clientId || typeof clientId !== "string" || clientId.trim().length === 0) {
|
|
6575
|
+
throw new GoogleAuthError("Client ID is required and must be a non-empty string");
|
|
6576
|
+
}
|
|
6577
|
+
}
|
|
6578
|
+
function validateClientSecret(clientSecret) {
|
|
6579
|
+
if (!clientSecret || typeof clientSecret !== "string" || clientSecret.trim().length === 0) {
|
|
6580
|
+
throw new GoogleAuthError("Client secret is required and must be a non-empty string");
|
|
6581
|
+
}
|
|
6582
|
+
}
|
|
6583
|
+
function validateRedirectUri(redirectUri) {
|
|
6584
|
+
if (!redirectUri || typeof redirectUri !== "string" || redirectUri.trim().length === 0) {
|
|
6585
|
+
throw new GoogleAuthError("Redirect URI is required and must be a non-empty string");
|
|
6586
|
+
}
|
|
6587
|
+
try {
|
|
6588
|
+
const url = new URL(redirectUri);
|
|
6589
|
+
if (!url.protocol.startsWith("http")) {
|
|
6590
|
+
throw new GoogleAuthError("Redirect URI must be an HTTP or HTTPS URL");
|
|
6591
|
+
}
|
|
6592
|
+
} catch {
|
|
6593
|
+
throw new GoogleAuthError("Redirect URI must be a valid URL");
|
|
6594
|
+
}
|
|
6595
|
+
}
|
|
6596
|
+
function validateCode(code) {
|
|
6597
|
+
if (!code || typeof code !== "string" || code.trim().length === 0) {
|
|
6598
|
+
throw new GoogleAuthError("Authorization code is required and must be a non-empty string");
|
|
6599
|
+
}
|
|
6600
|
+
}
|
|
6601
|
+
function validateScopes(scopes) {
|
|
6602
|
+
if (!Array.isArray(scopes) || scopes.length === 0) {
|
|
6603
|
+
throw new GoogleAuthError("At least one scope is required");
|
|
6604
|
+
}
|
|
6605
|
+
for (const scope of scopes) {
|
|
6606
|
+
if (!scope || typeof scope !== "string" || scope.trim().length === 0) {
|
|
6607
|
+
throw new GoogleAuthError("Scope must be a non-empty string");
|
|
6608
|
+
}
|
|
6609
|
+
}
|
|
6610
|
+
}
|
|
6611
|
+
function validateRefreshToken(refreshToken) {
|
|
6612
|
+
if (!refreshToken || typeof refreshToken !== "string" || refreshToken.trim().length === 0) {
|
|
6613
|
+
throw new GoogleAuthError("Refresh token is required and must be a non-empty string");
|
|
6614
|
+
}
|
|
6615
|
+
}
|
|
6346
6616
|
function getAuthUrl(clientId, redirectUri, scopes, state) {
|
|
6617
|
+
validateClientId(clientId);
|
|
6618
|
+
validateRedirectUri(redirectUri);
|
|
6619
|
+
validateScopes(scopes);
|
|
6620
|
+
if (state && (typeof state !== "string" || state.trim().length === 0)) {
|
|
6621
|
+
throw new GoogleAuthError("State must be a non-empty string if provided");
|
|
6622
|
+
}
|
|
6347
6623
|
const params = new URLSearchParams({
|
|
6348
6624
|
client_id: clientId,
|
|
6349
6625
|
redirect_uri: redirectUri,
|
|
@@ -6356,6 +6632,10 @@ function getAuthUrl(clientId, redirectUri, scopes, state) {
|
|
|
6356
6632
|
return `${GOOGLE_AUTH_URL}?${params.toString()}`;
|
|
6357
6633
|
}
|
|
6358
6634
|
async function exchangeCode(clientId, clientSecret, code, redirectUri) {
|
|
6635
|
+
validateClientId(clientId);
|
|
6636
|
+
validateClientSecret(clientSecret);
|
|
6637
|
+
validateCode(code);
|
|
6638
|
+
validateRedirectUri(redirectUri);
|
|
6359
6639
|
const res = await fetch(GOOGLE_TOKEN_URL, {
|
|
6360
6640
|
method: "POST",
|
|
6361
6641
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
@@ -6375,6 +6655,9 @@ async function exchangeCode(clientId, clientSecret, code, redirectUri) {
|
|
|
6375
6655
|
return await res.json();
|
|
6376
6656
|
}
|
|
6377
6657
|
async function refreshAccessToken(clientId, clientSecret, currentRefreshToken) {
|
|
6658
|
+
validateClientId(clientId);
|
|
6659
|
+
validateClientSecret(clientSecret);
|
|
6660
|
+
validateRefreshToken(currentRefreshToken);
|
|
6378
6661
|
const res = await fetch(GOOGLE_TOKEN_URL, {
|
|
6379
6662
|
method: "POST",
|
|
6380
6663
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
@@ -6394,6 +6677,47 @@ async function refreshAccessToken(clientId, clientSecret, currentRefreshToken) {
|
|
|
6394
6677
|
}
|
|
6395
6678
|
|
|
6396
6679
|
// ../integration-google/src/gsc-client.ts
|
|
6680
|
+
function validateAccessToken(accessToken) {
|
|
6681
|
+
if (!accessToken || typeof accessToken !== "string" || accessToken.trim().length === 0) {
|
|
6682
|
+
throw new GoogleApiError("Access token is required and must be a non-empty string", 400);
|
|
6683
|
+
}
|
|
6684
|
+
}
|
|
6685
|
+
function validateSiteUrl(siteUrl) {
|
|
6686
|
+
if (!siteUrl || typeof siteUrl !== "string" || siteUrl.trim().length === 0) {
|
|
6687
|
+
throw new GoogleApiError("Site URL is required and must be a non-empty string", 400);
|
|
6688
|
+
}
|
|
6689
|
+
if (siteUrl.startsWith("sc-domain:")) {
|
|
6690
|
+
const domain = siteUrl.slice("sc-domain:".length);
|
|
6691
|
+
if (!domain) {
|
|
6692
|
+
throw new GoogleApiError("Site URL sc-domain must include a domain", 400);
|
|
6693
|
+
}
|
|
6694
|
+
if (!domain.includes(".")) {
|
|
6695
|
+
throw new GoogleApiError("Site URL sc-domain must be a valid domain", 400);
|
|
6696
|
+
}
|
|
6697
|
+
} else {
|
|
6698
|
+
try {
|
|
6699
|
+
const url = new URL(siteUrl);
|
|
6700
|
+
if (!url.protocol.startsWith("http")) {
|
|
6701
|
+
throw new GoogleApiError("Site URL must be an HTTP or HTTPS URL", 400);
|
|
6702
|
+
}
|
|
6703
|
+
} catch {
|
|
6704
|
+
throw new GoogleApiError("Site URL must be a valid URL", 400);
|
|
6705
|
+
}
|
|
6706
|
+
}
|
|
6707
|
+
}
|
|
6708
|
+
function validateUrl(urlParam) {
|
|
6709
|
+
if (!urlParam || typeof urlParam !== "string" || urlParam.trim().length === 0) {
|
|
6710
|
+
throw new GoogleApiError("URL is required and must be a non-empty string", 400);
|
|
6711
|
+
}
|
|
6712
|
+
try {
|
|
6713
|
+
const url = new URL(urlParam);
|
|
6714
|
+
if (!url.protocol.startsWith("http")) {
|
|
6715
|
+
throw new GoogleApiError("URL must be an HTTP or HTTPS URL", 400);
|
|
6716
|
+
}
|
|
6717
|
+
} catch {
|
|
6718
|
+
throw new GoogleApiError("URL must be a valid URL", 400);
|
|
6719
|
+
}
|
|
6720
|
+
}
|
|
6397
6721
|
function gscClientLog(level, action, ctx) {
|
|
6398
6722
|
const entry = { ts: (/* @__PURE__ */ new Date()).toISOString(), level, module: "GscClient", action, ...ctx };
|
|
6399
6723
|
const stream = level === "error" ? process.stderr : process.stdout;
|
|
@@ -6429,6 +6753,7 @@ async function gscFetch(accessToken, url, opts) {
|
|
|
6429
6753
|
return await res.json();
|
|
6430
6754
|
}
|
|
6431
6755
|
async function listSites(accessToken) {
|
|
6756
|
+
validateAccessToken(accessToken);
|
|
6432
6757
|
const data = await gscFetch(
|
|
6433
6758
|
accessToken,
|
|
6434
6759
|
`${GSC_API_BASE}/sites`
|
|
@@ -6436,6 +6761,8 @@ async function listSites(accessToken) {
|
|
|
6436
6761
|
return data.siteEntry ?? [];
|
|
6437
6762
|
}
|
|
6438
6763
|
async function listSitemaps(accessToken, siteUrl) {
|
|
6764
|
+
validateAccessToken(accessToken);
|
|
6765
|
+
validateSiteUrl(siteUrl);
|
|
6439
6766
|
const encodedSiteUrl = encodeURIComponent(siteUrl);
|
|
6440
6767
|
const data = await gscFetch(
|
|
6441
6768
|
accessToken,
|
|
@@ -6444,6 +6771,8 @@ async function listSitemaps(accessToken, siteUrl) {
|
|
|
6444
6771
|
return data.sitemap ?? [];
|
|
6445
6772
|
}
|
|
6446
6773
|
async function fetchSearchAnalytics(accessToken, siteUrl, opts) {
|
|
6774
|
+
validateAccessToken(accessToken);
|
|
6775
|
+
validateSiteUrl(siteUrl);
|
|
6447
6776
|
const allRows = [];
|
|
6448
6777
|
let startRow = 0;
|
|
6449
6778
|
const dimensions = opts.dimensions ?? ["query", "page", "country", "device", "date"];
|
|
@@ -6479,6 +6808,8 @@ async function fetchSearchAnalytics(accessToken, siteUrl, opts) {
|
|
|
6479
6808
|
return allRows;
|
|
6480
6809
|
}
|
|
6481
6810
|
async function publishUrlNotification(accessToken, url, type = "URL_UPDATED") {
|
|
6811
|
+
validateAccessToken(accessToken);
|
|
6812
|
+
validateUrl(url);
|
|
6482
6813
|
return gscFetch(
|
|
6483
6814
|
accessToken,
|
|
6484
6815
|
`${INDEXING_API_BASE}/urlNotifications:publish`,
|
|
@@ -6489,6 +6820,9 @@ async function publishUrlNotification(accessToken, url, type = "URL_UPDATED") {
|
|
|
6489
6820
|
);
|
|
6490
6821
|
}
|
|
6491
6822
|
async function inspectUrl(accessToken, inspectionUrl, siteUrl) {
|
|
6823
|
+
validateAccessToken(accessToken);
|
|
6824
|
+
validateUrl(inspectionUrl);
|
|
6825
|
+
validateSiteUrl(siteUrl);
|
|
6492
6826
|
return gscFetch(
|
|
6493
6827
|
accessToken,
|
|
6494
6828
|
URL_INSPECTION_API,
|
|
@@ -6524,12 +6858,46 @@ var GA4ApiError = class extends Error {
|
|
|
6524
6858
|
};
|
|
6525
6859
|
|
|
6526
6860
|
// ../integration-google-analytics/src/ga4-client.ts
|
|
6861
|
+
function validateClientEmail(clientEmail) {
|
|
6862
|
+
if (!clientEmail || typeof clientEmail !== "string" || clientEmail.trim().length === 0) {
|
|
6863
|
+
throw new GA4ApiError("Client email is required and must be a non-empty string", 400);
|
|
6864
|
+
}
|
|
6865
|
+
if (!clientEmail.includes("@")) {
|
|
6866
|
+
throw new GA4ApiError("Client email must be a valid email address", 400);
|
|
6867
|
+
}
|
|
6868
|
+
}
|
|
6869
|
+
function validatePrivateKey(privateKey) {
|
|
6870
|
+
if (!privateKey || typeof privateKey !== "string" || privateKey.trim().length === 0) {
|
|
6871
|
+
throw new GA4ApiError("Private key is required and must be a non-empty string", 400);
|
|
6872
|
+
}
|
|
6873
|
+
}
|
|
6874
|
+
function validatePropertyId(propertyId) {
|
|
6875
|
+
if (!propertyId || typeof propertyId !== "string" || propertyId.trim().length === 0) {
|
|
6876
|
+
throw new GA4ApiError("Property ID is required and must be a non-empty string", 400);
|
|
6877
|
+
}
|
|
6878
|
+
if (!/^\d+$/.test(propertyId)) {
|
|
6879
|
+
throw new GA4ApiError("Property ID must be a numeric string", 400);
|
|
6880
|
+
}
|
|
6881
|
+
}
|
|
6882
|
+
function validateAccessToken2(accessToken) {
|
|
6883
|
+
if (!accessToken || typeof accessToken !== "string" || accessToken.trim().length === 0) {
|
|
6884
|
+
throw new GA4ApiError("Access token is required and must be a non-empty string", 400);
|
|
6885
|
+
}
|
|
6886
|
+
}
|
|
6887
|
+
function validateScope(scope) {
|
|
6888
|
+
if (!scope || typeof scope !== "string" || scope.trim().length === 0) {
|
|
6889
|
+
throw new GA4ApiError("Scope is required and must be a non-empty string", 400);
|
|
6890
|
+
}
|
|
6891
|
+
}
|
|
6527
6892
|
function ga4Log(level, action, ctx) {
|
|
6528
6893
|
const entry = { ts: (/* @__PURE__ */ new Date()).toISOString(), level, module: "GA4Client", action, ...ctx };
|
|
6529
6894
|
const stream = level === "error" ? process.stderr : process.stdout;
|
|
6530
6895
|
stream.write(JSON.stringify(entry) + "\n");
|
|
6531
6896
|
}
|
|
6532
6897
|
function createServiceAccountJwt(clientEmail, privateKey, scope) {
|
|
6898
|
+
validateClientEmail(clientEmail);
|
|
6899
|
+
validatePrivateKey(privateKey);
|
|
6900
|
+
validateScope(scope);
|
|
6533
6901
|
const now = Math.floor(Date.now() / 1e3);
|
|
6534
6902
|
const header = { alg: "RS256", typ: "JWT" };
|
|
6535
6903
|
const payload = {
|
|
@@ -6656,6 +7024,8 @@ var AI_REFERRAL_SOURCE_FILTERS = [
|
|
|
6656
7024
|
{ matchType: "CONTAINS", value: "meta.ai" }
|
|
6657
7025
|
];
|
|
6658
7026
|
async function fetchTrafficByLandingPage(accessToken, propertyId, days) {
|
|
7027
|
+
validateAccessToken2(accessToken);
|
|
7028
|
+
validatePropertyId(propertyId);
|
|
6659
7029
|
const syncDays = Math.min(Math.max(1, days ?? GA4_DEFAULT_SYNC_DAYS), GA4_MAX_SYNC_DAYS);
|
|
6660
7030
|
const endDate = /* @__PURE__ */ new Date();
|
|
6661
7031
|
const startDate = /* @__PURE__ */ new Date();
|
|
@@ -6730,10 +7100,15 @@ async function fetchTrafficByLandingPage(accessToken, propertyId, days) {
|
|
|
6730
7100
|
return rows;
|
|
6731
7101
|
}
|
|
6732
7102
|
async function verifyConnection(clientEmail, privateKey, propertyId) {
|
|
7103
|
+
validateClientEmail(clientEmail);
|
|
7104
|
+
validatePrivateKey(privateKey);
|
|
7105
|
+
validatePropertyId(propertyId);
|
|
6733
7106
|
const accessToken = await getAccessToken(clientEmail, privateKey);
|
|
6734
7107
|
return verifyConnectionWithToken(accessToken, propertyId);
|
|
6735
7108
|
}
|
|
6736
7109
|
async function verifyConnectionWithToken(accessToken, propertyId) {
|
|
7110
|
+
validateAccessToken2(accessToken);
|
|
7111
|
+
validatePropertyId(propertyId);
|
|
6737
7112
|
const endDate = /* @__PURE__ */ new Date();
|
|
6738
7113
|
const startDate = /* @__PURE__ */ new Date();
|
|
6739
7114
|
startDate.setDate(startDate.getDate() - 1);
|
|
@@ -6746,6 +7121,8 @@ async function verifyConnectionWithToken(accessToken, propertyId) {
|
|
|
6746
7121
|
return true;
|
|
6747
7122
|
}
|
|
6748
7123
|
async function fetchAggregateSummary(accessToken, propertyId, days) {
|
|
7124
|
+
validateAccessToken2(accessToken);
|
|
7125
|
+
validatePropertyId(propertyId);
|
|
6749
7126
|
const syncDays = Math.min(Math.max(1, days ?? GA4_DEFAULT_SYNC_DAYS), GA4_MAX_SYNC_DAYS);
|
|
6750
7127
|
const endDate = /* @__PURE__ */ new Date();
|
|
6751
7128
|
const startDate = /* @__PURE__ */ new Date();
|
|
@@ -6785,6 +7162,8 @@ async function fetchAggregateSummary(accessToken, propertyId, days) {
|
|
|
6785
7162
|
return summary;
|
|
6786
7163
|
}
|
|
6787
7164
|
async function fetchAiReferrals(accessToken, propertyId, days) {
|
|
7165
|
+
validateAccessToken2(accessToken);
|
|
7166
|
+
validatePropertyId(propertyId);
|
|
6788
7167
|
const syncDays = Math.min(Math.max(1, days ?? GA4_DEFAULT_SYNC_DAYS), GA4_MAX_SYNC_DAYS);
|
|
6789
7168
|
const endDate = /* @__PURE__ */ new Date();
|
|
6790
7169
|
const startDate = /* @__PURE__ */ new Date();
|
|
@@ -7116,18 +7495,18 @@ async function googleRoutes(app, opts) {
|
|
|
7116
7495
|
if (opts.onGscSyncRequested) {
|
|
7117
7496
|
opts.onGscSyncRequested(runId, project.id, { days, full });
|
|
7118
7497
|
}
|
|
7119
|
-
const run = app.db.select().from(runs).where(
|
|
7498
|
+
const run = app.db.select().from(runs).where(eq14(runs.id, runId)).get();
|
|
7120
7499
|
return run;
|
|
7121
7500
|
});
|
|
7122
7501
|
app.get("/projects/:name/google/gsc/performance", async (request) => {
|
|
7123
7502
|
const project = resolveProject(app.db, request.params.name);
|
|
7124
7503
|
const { startDate, endDate, query, page, limit } = request.query;
|
|
7125
|
-
const conditions = [
|
|
7504
|
+
const conditions = [eq14(gscSearchData.projectId, project.id)];
|
|
7126
7505
|
if (startDate) conditions.push(sql3`${gscSearchData.date} >= ${startDate}`);
|
|
7127
7506
|
if (endDate) conditions.push(sql3`${gscSearchData.date} <= ${endDate}`);
|
|
7128
7507
|
if (query) conditions.push(sql3`${gscSearchData.query} LIKE ${"%" + query + "%"}`);
|
|
7129
7508
|
if (page) conditions.push(sql3`${gscSearchData.page} LIKE ${"%" + page + "%"}`);
|
|
7130
|
-
const rows = app.db.select().from(gscSearchData).where(
|
|
7509
|
+
const rows = app.db.select().from(gscSearchData).where(and3(...conditions)).orderBy(desc5(gscSearchData.date)).limit(parseInt(limit ?? "500", 10)).all();
|
|
7131
7510
|
return rows.map((r) => ({
|
|
7132
7511
|
date: r.date,
|
|
7133
7512
|
query: r.query,
|
|
@@ -7203,9 +7582,9 @@ async function googleRoutes(app, opts) {
|
|
|
7203
7582
|
app.get("/projects/:name/google/gsc/inspections", async (request) => {
|
|
7204
7583
|
const project = resolveProject(app.db, request.params.name);
|
|
7205
7584
|
const { url, limit } = request.query;
|
|
7206
|
-
const conditions = [
|
|
7207
|
-
if (url) conditions.push(
|
|
7208
|
-
const rows = app.db.select().from(gscUrlInspections).where(
|
|
7585
|
+
const conditions = [eq14(gscUrlInspections.projectId, project.id)];
|
|
7586
|
+
if (url) conditions.push(eq14(gscUrlInspections.url, url));
|
|
7587
|
+
const rows = app.db.select().from(gscUrlInspections).where(and3(...conditions)).orderBy(desc5(gscUrlInspections.inspectedAt)).limit(parseInt(limit ?? "100", 10)).all();
|
|
7209
7588
|
return rows.map((r) => ({
|
|
7210
7589
|
id: r.id,
|
|
7211
7590
|
url: r.url,
|
|
@@ -7224,7 +7603,7 @@ async function googleRoutes(app, opts) {
|
|
|
7224
7603
|
});
|
|
7225
7604
|
app.get("/projects/:name/google/gsc/deindexed", async (request) => {
|
|
7226
7605
|
const project = resolveProject(app.db, request.params.name);
|
|
7227
|
-
const allInspections = app.db.select().from(gscUrlInspections).where(
|
|
7606
|
+
const allInspections = app.db.select().from(gscUrlInspections).where(eq14(gscUrlInspections.projectId, project.id)).orderBy(desc5(gscUrlInspections.inspectedAt)).all();
|
|
7228
7607
|
const byUrl = /* @__PURE__ */ new Map();
|
|
7229
7608
|
for (const row of allInspections) {
|
|
7230
7609
|
const existing = byUrl.get(row.url);
|
|
@@ -7252,7 +7631,7 @@ async function googleRoutes(app, opts) {
|
|
|
7252
7631
|
});
|
|
7253
7632
|
app.get("/projects/:name/google/gsc/coverage", async (request) => {
|
|
7254
7633
|
const project = resolveProject(app.db, request.params.name);
|
|
7255
|
-
const allInspections = app.db.select().from(gscUrlInspections).where(
|
|
7634
|
+
const allInspections = app.db.select().from(gscUrlInspections).where(eq14(gscUrlInspections.projectId, project.id)).orderBy(desc5(gscUrlInspections.inspectedAt)).all();
|
|
7256
7635
|
const canonicalUrl = (url) => url.replace(/^http:\/\//, "https://");
|
|
7257
7636
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
7258
7637
|
const historyByUrl = /* @__PURE__ */ new Map();
|
|
@@ -7349,7 +7728,7 @@ async function googleRoutes(app, opts) {
|
|
|
7349
7728
|
const project = resolveProject(app.db, request.params.name);
|
|
7350
7729
|
const parsed = parseInt(request.query.limit ?? "90", 10);
|
|
7351
7730
|
const limit = Number.isNaN(parsed) || parsed <= 0 ? 90 : parsed;
|
|
7352
|
-
const rows = app.db.select().from(gscCoverageSnapshots).where(
|
|
7731
|
+
const rows = app.db.select().from(gscCoverageSnapshots).where(eq14(gscCoverageSnapshots.projectId, project.id)).orderBy(desc5(gscCoverageSnapshots.date)).limit(limit).all();
|
|
7353
7732
|
return rows.map((r) => ({
|
|
7354
7733
|
date: r.date,
|
|
7355
7734
|
indexed: r.indexed,
|
|
@@ -7417,7 +7796,7 @@ async function googleRoutes(app, opts) {
|
|
|
7417
7796
|
if (opts.onInspectSitemapRequested) {
|
|
7418
7797
|
opts.onInspectSitemapRequested(runId, project.id, { sitemapUrl });
|
|
7419
7798
|
}
|
|
7420
|
-
const run = app.db.select().from(runs).where(
|
|
7799
|
+
const run = app.db.select().from(runs).where(eq14(runs.id, runId)).get();
|
|
7421
7800
|
return { sitemaps, primarySitemapUrl: sitemapUrl, run };
|
|
7422
7801
|
});
|
|
7423
7802
|
app.post("/projects/:name/google/gsc/inspect-sitemap", async (request, reply) => {
|
|
@@ -7447,7 +7826,7 @@ async function googleRoutes(app, opts) {
|
|
|
7447
7826
|
if (opts.onInspectSitemapRequested) {
|
|
7448
7827
|
opts.onInspectSitemapRequested(runId, project.id, { sitemapUrl: sitemapUrl ?? void 0 });
|
|
7449
7828
|
}
|
|
7450
|
-
const run = app.db.select().from(runs).where(
|
|
7829
|
+
const run = app.db.select().from(runs).where(eq14(runs.id, runId)).get();
|
|
7451
7830
|
return run;
|
|
7452
7831
|
});
|
|
7453
7832
|
app.put("/projects/:name/google/connections/:type/sitemap", async (request, reply) => {
|
|
@@ -7502,7 +7881,7 @@ async function googleRoutes(app, opts) {
|
|
|
7502
7881
|
const { accessToken } = await getValidToken(store, project.canonicalDomain, "gsc", googleClientId, googleClientSecret);
|
|
7503
7882
|
let urlsToNotify = request.body?.urls ?? [];
|
|
7504
7883
|
if (request.body?.allUnindexed) {
|
|
7505
|
-
const allInspections = app.db.select().from(gscUrlInspections).where(
|
|
7884
|
+
const allInspections = app.db.select().from(gscUrlInspections).where(eq14(gscUrlInspections.projectId, project.id)).orderBy(desc5(gscUrlInspections.inspectedAt)).all();
|
|
7506
7885
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
7507
7886
|
for (const row of allInspections) {
|
|
7508
7887
|
if (!latestByUrl.has(row.url)) {
|
|
@@ -7577,7 +7956,7 @@ async function googleRoutes(app, opts) {
|
|
|
7577
7956
|
|
|
7578
7957
|
// ../api-routes/src/bing.ts
|
|
7579
7958
|
import crypto15 from "crypto";
|
|
7580
|
-
import { eq as
|
|
7959
|
+
import { eq as eq15, and as and4, desc as desc6 } from "drizzle-orm";
|
|
7581
7960
|
|
|
7582
7961
|
// ../integration-bing/src/constants.ts
|
|
7583
7962
|
var BING_WMT_API_BASE = "https://ssl.bing.com/webmaster/api.svc/json";
|
|
@@ -7596,6 +7975,45 @@ var BingApiError = class extends Error {
|
|
|
7596
7975
|
};
|
|
7597
7976
|
|
|
7598
7977
|
// ../integration-bing/src/bing-client.ts
|
|
7978
|
+
function validateApiKey(apiKey) {
|
|
7979
|
+
if (!apiKey || typeof apiKey !== "string" || apiKey.trim().length === 0) {
|
|
7980
|
+
throw new BingApiError("API key is required and must be a non-empty string", 400);
|
|
7981
|
+
}
|
|
7982
|
+
}
|
|
7983
|
+
function validateSiteUrl2(siteUrl) {
|
|
7984
|
+
if (!siteUrl || typeof siteUrl !== "string" || siteUrl.trim().length === 0) {
|
|
7985
|
+
throw new BingApiError("Site URL is required and must be a non-empty string", 400);
|
|
7986
|
+
}
|
|
7987
|
+
try {
|
|
7988
|
+
const url = new URL(siteUrl);
|
|
7989
|
+
if (!url.protocol.startsWith("http")) {
|
|
7990
|
+
throw new BingApiError("Site URL must be an HTTP or HTTPS URL", 400);
|
|
7991
|
+
}
|
|
7992
|
+
} catch {
|
|
7993
|
+
throw new BingApiError("Site URL must be a valid URL", 400);
|
|
7994
|
+
}
|
|
7995
|
+
}
|
|
7996
|
+
function validateUrl2(urlParam) {
|
|
7997
|
+
if (!urlParam || typeof urlParam !== "string" || urlParam.trim().length === 0) {
|
|
7998
|
+
throw new BingApiError("URL is required and must be a non-empty string", 400);
|
|
7999
|
+
}
|
|
8000
|
+
try {
|
|
8001
|
+
const url = new URL(urlParam);
|
|
8002
|
+
if (!url.protocol.startsWith("http")) {
|
|
8003
|
+
throw new BingApiError("URL must be an HTTP or HTTPS URL", 400);
|
|
8004
|
+
}
|
|
8005
|
+
} catch {
|
|
8006
|
+
throw new BingApiError("URL must be a valid URL", 400);
|
|
8007
|
+
}
|
|
8008
|
+
}
|
|
8009
|
+
function validateUrls(urls) {
|
|
8010
|
+
if (!Array.isArray(urls)) {
|
|
8011
|
+
throw new BingApiError("URLs must be an array", 400);
|
|
8012
|
+
}
|
|
8013
|
+
for (const url of urls) {
|
|
8014
|
+
validateUrl2(url);
|
|
8015
|
+
}
|
|
8016
|
+
}
|
|
7599
8017
|
function bingClientLog(level, action, ctx) {
|
|
7600
8018
|
const entry = { ts: (/* @__PURE__ */ new Date()).toISOString(), level, module: "BingClient", action, ...ctx };
|
|
7601
8019
|
const stream = level === "error" ? process.stderr : process.stdout;
|
|
@@ -7644,21 +8062,31 @@ async function bingFetch(apiKey, endpoint, opts) {
|
|
|
7644
8062
|
}
|
|
7645
8063
|
}
|
|
7646
8064
|
async function getSites(apiKey) {
|
|
8065
|
+
validateApiKey(apiKey);
|
|
7647
8066
|
const data = await bingFetch(apiKey, "GetUserSites");
|
|
7648
8067
|
return data ?? [];
|
|
7649
8068
|
}
|
|
7650
8069
|
async function getUrlInfo(apiKey, siteUrl, url) {
|
|
8070
|
+
validateApiKey(apiKey);
|
|
8071
|
+
validateSiteUrl2(siteUrl);
|
|
8072
|
+
validateUrl2(url);
|
|
7651
8073
|
const encodedSite = encodeURIComponent(siteUrl);
|
|
7652
8074
|
const encodedUrl = encodeURIComponent(url);
|
|
7653
8075
|
return bingFetch(apiKey, `GetUrlInfo?siteUrl=${encodedSite}&url=${encodedUrl}`);
|
|
7654
8076
|
}
|
|
7655
8077
|
async function submitUrl(apiKey, siteUrl, url) {
|
|
8078
|
+
validateApiKey(apiKey);
|
|
8079
|
+
validateSiteUrl2(siteUrl);
|
|
8080
|
+
validateUrl2(url);
|
|
7656
8081
|
await bingFetch(apiKey, "SubmitUrl", {
|
|
7657
8082
|
method: "POST",
|
|
7658
8083
|
body: { siteUrl, url }
|
|
7659
8084
|
});
|
|
7660
8085
|
}
|
|
7661
8086
|
async function submitUrlBatch(apiKey, siteUrl, urls) {
|
|
8087
|
+
validateApiKey(apiKey);
|
|
8088
|
+
validateSiteUrl2(siteUrl);
|
|
8089
|
+
validateUrls(urls);
|
|
7662
8090
|
for (let i = 0; i < urls.length; i += BING_SUBMIT_URL_BATCH_LIMIT) {
|
|
7663
8091
|
const batch = urls.slice(i, i + BING_SUBMIT_URL_BATCH_LIMIT);
|
|
7664
8092
|
await bingFetch(apiKey, "SubmitUrlbatch", {
|
|
@@ -7668,6 +8096,8 @@ async function submitUrlBatch(apiKey, siteUrl, urls) {
|
|
|
7668
8096
|
}
|
|
7669
8097
|
}
|
|
7670
8098
|
async function getKeywordStats(apiKey, siteUrl) {
|
|
8099
|
+
validateApiKey(apiKey);
|
|
8100
|
+
validateSiteUrl2(siteUrl);
|
|
7671
8101
|
const encodedSite = encodeURIComponent(siteUrl);
|
|
7672
8102
|
const data = await bingFetch(apiKey, `GetQueryStats?siteUrl=${encodedSite}`);
|
|
7673
8103
|
return data ?? [];
|
|
@@ -7808,7 +8238,7 @@ async function bingRoutes(app, opts) {
|
|
|
7808
8238
|
const project = resolveProject(app.db, request.params.name);
|
|
7809
8239
|
const conn = requireConnection(store, project.canonicalDomain, reply);
|
|
7810
8240
|
if (!conn) return;
|
|
7811
|
-
const allInspections = app.db.select().from(bingUrlInspections).where(
|
|
8241
|
+
const allInspections = app.db.select().from(bingUrlInspections).where(eq15(bingUrlInspections.projectId, project.id)).orderBy(desc6(bingUrlInspections.inspectedAt)).all();
|
|
7812
8242
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
7813
8243
|
const definitiveByUrl = /* @__PURE__ */ new Map();
|
|
7814
8244
|
for (const row of allInspections) {
|
|
@@ -7878,8 +8308,8 @@ async function bingRoutes(app, opts) {
|
|
|
7878
8308
|
if (!store) return;
|
|
7879
8309
|
const project = resolveProject(app.db, request.params.name);
|
|
7880
8310
|
const { url, limit } = request.query;
|
|
7881
|
-
const whereClause = url ?
|
|
7882
|
-
const filtered = app.db.select().from(bingUrlInspections).where(whereClause).orderBy(
|
|
8311
|
+
const whereClause = url ? and4(eq15(bingUrlInspections.projectId, project.id), eq15(bingUrlInspections.url, url)) : eq15(bingUrlInspections.projectId, project.id);
|
|
8312
|
+
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();
|
|
7883
8313
|
return filtered.map((r) => ({
|
|
7884
8314
|
id: r.id,
|
|
7885
8315
|
url: r.url,
|
|
@@ -7975,7 +8405,7 @@ async function bingRoutes(app, opts) {
|
|
|
7975
8405
|
}
|
|
7976
8406
|
let urlsToSubmit = request.body?.urls ?? [];
|
|
7977
8407
|
if (request.body?.allUnindexed) {
|
|
7978
|
-
const allInspections = app.db.select().from(bingUrlInspections).where(
|
|
8408
|
+
const allInspections = app.db.select().from(bingUrlInspections).where(eq15(bingUrlInspections.projectId, project.id)).orderBy(desc6(bingUrlInspections.inspectedAt)).all();
|
|
7979
8409
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
7980
8410
|
for (const row of allInspections) {
|
|
7981
8411
|
if (!latestByUrl.has(row.url)) {
|
|
@@ -8068,14 +8498,14 @@ async function bingRoutes(app, opts) {
|
|
|
8068
8498
|
import fs2 from "fs";
|
|
8069
8499
|
import path2 from "path";
|
|
8070
8500
|
import os2 from "os";
|
|
8071
|
-
import { eq as
|
|
8501
|
+
import { eq as eq16, and as and5 } from "drizzle-orm";
|
|
8072
8502
|
function getScreenshotDir() {
|
|
8073
8503
|
return path2.join(os2.homedir(), ".canonry", "screenshots");
|
|
8074
8504
|
}
|
|
8075
8505
|
async function cdpRoutes(app, opts) {
|
|
8076
8506
|
app.get("/screenshots/:snapshotId", async (request, reply) => {
|
|
8077
8507
|
const { snapshotId } = request.params;
|
|
8078
|
-
const snapshot = app.db.select({ screenshotPath: querySnapshots.screenshotPath }).from(querySnapshots).where(
|
|
8508
|
+
const snapshot = app.db.select({ screenshotPath: querySnapshots.screenshotPath }).from(querySnapshots).where(eq16(querySnapshots.id, snapshotId)).get();
|
|
8079
8509
|
if (!snapshot?.screenshotPath) {
|
|
8080
8510
|
const err = notFound("Screenshot", snapshotId);
|
|
8081
8511
|
return reply.code(err.statusCode).send(err.toJSON());
|
|
@@ -8141,7 +8571,7 @@ async function cdpRoutes(app, opts) {
|
|
|
8141
8571
|
async (request, reply) => {
|
|
8142
8572
|
const project = resolveProject(app.db, request.params.name);
|
|
8143
8573
|
const { runId } = request.params;
|
|
8144
|
-
const run = app.db.select().from(runs).where(
|
|
8574
|
+
const run = app.db.select().from(runs).where(and5(eq16(runs.id, runId), eq16(runs.projectId, project.id))).get();
|
|
8145
8575
|
if (!run) {
|
|
8146
8576
|
const err = notFound("Run", runId);
|
|
8147
8577
|
return reply.code(err.statusCode).send(err.toJSON());
|
|
@@ -8154,8 +8584,8 @@ async function cdpRoutes(app, opts) {
|
|
|
8154
8584
|
citedDomains: querySnapshots.citedDomains,
|
|
8155
8585
|
screenshotPath: querySnapshots.screenshotPath,
|
|
8156
8586
|
rawResponse: querySnapshots.rawResponse
|
|
8157
|
-
}).from(querySnapshots).where(
|
|
8158
|
-
const keywordRows = app.db.select({ id: keywords.id, keyword: keywords.keyword }).from(keywords).where(
|
|
8587
|
+
}).from(querySnapshots).where(eq16(querySnapshots.runId, runId)).all();
|
|
8588
|
+
const keywordRows = app.db.select({ id: keywords.id, keyword: keywords.keyword }).from(keywords).where(eq16(keywords.projectId, project.id)).all();
|
|
8159
8589
|
const keywordMap = new Map(keywordRows.map((k) => [k.id, k.keyword]));
|
|
8160
8590
|
const byKeyword = /* @__PURE__ */ new Map();
|
|
8161
8591
|
for (const snap of snapshots) {
|
|
@@ -8238,7 +8668,7 @@ async function cdpRoutes(app, opts) {
|
|
|
8238
8668
|
|
|
8239
8669
|
// ../api-routes/src/ga.ts
|
|
8240
8670
|
import crypto16 from "crypto";
|
|
8241
|
-
import { eq as
|
|
8671
|
+
import { eq as eq17, desc as desc7, and as and6, sql as sql4 } from "drizzle-orm";
|
|
8242
8672
|
function gaLog(level, action, ctx) {
|
|
8243
8673
|
const entry = { ts: (/* @__PURE__ */ new Date()).toISOString(), level, module: "GA4Routes", action, ...ctx };
|
|
8244
8674
|
const stream = level === "error" ? process.stderr : process.stdout;
|
|
@@ -8395,9 +8825,9 @@ async function ga4Routes(app, opts) {
|
|
|
8395
8825
|
if (!saConn && !oauthConn) {
|
|
8396
8826
|
throw notFound("GA4 connection", project.name);
|
|
8397
8827
|
}
|
|
8398
|
-
app.db.delete(gaTrafficSnapshots).where(
|
|
8399
|
-
app.db.delete(gaTrafficSummaries).where(
|
|
8400
|
-
app.db.delete(gaAiReferrals).where(
|
|
8828
|
+
app.db.delete(gaTrafficSnapshots).where(eq17(gaTrafficSnapshots.projectId, project.id)).run();
|
|
8829
|
+
app.db.delete(gaTrafficSummaries).where(eq17(gaTrafficSummaries.projectId, project.id)).run();
|
|
8830
|
+
app.db.delete(gaAiReferrals).where(eq17(gaAiReferrals.projectId, project.id)).run();
|
|
8401
8831
|
const propertyId = saConn?.propertyId ?? oauthConn?.propertyId ?? null;
|
|
8402
8832
|
opts.ga4CredentialStore?.deleteConnection(project.name);
|
|
8403
8833
|
opts.googleConnectionStore?.deleteConnection(project.canonicalDomain, "ga4");
|
|
@@ -8418,7 +8848,7 @@ async function ga4Routes(app, opts) {
|
|
|
8418
8848
|
if (!connected) {
|
|
8419
8849
|
return { connected: false, propertyId: null, clientEmail: null, authMethod: null, lastSyncedAt: null };
|
|
8420
8850
|
}
|
|
8421
|
-
const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(
|
|
8851
|
+
const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq17(gaTrafficSummaries.projectId, project.id)).orderBy(desc7(gaTrafficSummaries.syncedAt)).limit(1).get();
|
|
8422
8852
|
return {
|
|
8423
8853
|
connected: true,
|
|
8424
8854
|
propertyId: saConn?.propertyId ?? oauthConn?.propertyId ?? null,
|
|
@@ -8451,8 +8881,8 @@ async function ga4Routes(app, opts) {
|
|
|
8451
8881
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8452
8882
|
app.db.transaction((tx) => {
|
|
8453
8883
|
tx.delete(gaTrafficSnapshots).where(
|
|
8454
|
-
|
|
8455
|
-
|
|
8884
|
+
and6(
|
|
8885
|
+
eq17(gaTrafficSnapshots.projectId, project.id),
|
|
8456
8886
|
sql4`${gaTrafficSnapshots.date} >= ${summary.periodStart}`,
|
|
8457
8887
|
sql4`${gaTrafficSnapshots.date} <= ${summary.periodEnd}`
|
|
8458
8888
|
)
|
|
@@ -8472,8 +8902,8 @@ async function ga4Routes(app, opts) {
|
|
|
8472
8902
|
}
|
|
8473
8903
|
}
|
|
8474
8904
|
tx.delete(gaAiReferrals).where(
|
|
8475
|
-
|
|
8476
|
-
|
|
8905
|
+
and6(
|
|
8906
|
+
eq17(gaAiReferrals.projectId, project.id),
|
|
8477
8907
|
sql4`${gaAiReferrals.date} >= ${summary.periodStart}`,
|
|
8478
8908
|
sql4`${gaAiReferrals.date} <= ${summary.periodEnd}`
|
|
8479
8909
|
)
|
|
@@ -8493,7 +8923,7 @@ async function ga4Routes(app, opts) {
|
|
|
8493
8923
|
}).run();
|
|
8494
8924
|
}
|
|
8495
8925
|
}
|
|
8496
|
-
tx.delete(gaTrafficSummaries).where(
|
|
8926
|
+
tx.delete(gaTrafficSummaries).where(eq17(gaTrafficSummaries.projectId, project.id)).run();
|
|
8497
8927
|
tx.insert(gaTrafficSummaries).values({
|
|
8498
8928
|
id: crypto16.randomUUID(),
|
|
8499
8929
|
projectId: project.id,
|
|
@@ -8528,20 +8958,20 @@ async function ga4Routes(app, opts) {
|
|
|
8528
8958
|
totalSessions: gaTrafficSummaries.totalSessions,
|
|
8529
8959
|
totalOrganicSessions: gaTrafficSummaries.totalOrganicSessions,
|
|
8530
8960
|
totalUsers: gaTrafficSummaries.totalUsers
|
|
8531
|
-
}).from(gaTrafficSummaries).where(
|
|
8961
|
+
}).from(gaTrafficSummaries).where(eq17(gaTrafficSummaries.projectId, project.id)).get();
|
|
8532
8962
|
const rows = app.db.select({
|
|
8533
8963
|
landingPage: gaTrafficSnapshots.landingPage,
|
|
8534
8964
|
sessions: sql4`SUM(${gaTrafficSnapshots.sessions})`,
|
|
8535
8965
|
organicSessions: sql4`SUM(${gaTrafficSnapshots.organicSessions})`,
|
|
8536
8966
|
users: sql4`SUM(${gaTrafficSnapshots.users})`
|
|
8537
|
-
}).from(gaTrafficSnapshots).where(
|
|
8967
|
+
}).from(gaTrafficSnapshots).where(eq17(gaTrafficSnapshots.projectId, project.id)).groupBy(gaTrafficSnapshots.landingPage).orderBy(sql4`SUM(${gaTrafficSnapshots.sessions}) DESC`).limit(limit).all();
|
|
8538
8968
|
const aiReferrals = app.db.select({
|
|
8539
8969
|
source: gaAiReferrals.source,
|
|
8540
8970
|
medium: gaAiReferrals.medium,
|
|
8541
8971
|
sourceDimension: gaAiReferrals.sourceDimension,
|
|
8542
8972
|
sessions: sql4`SUM(${gaAiReferrals.sessions})`,
|
|
8543
8973
|
users: sql4`SUM(${gaAiReferrals.users})`
|
|
8544
|
-
}).from(gaAiReferrals).where(
|
|
8974
|
+
}).from(gaAiReferrals).where(eq17(gaAiReferrals.projectId, project.id)).groupBy(gaAiReferrals.source, gaAiReferrals.medium, gaAiReferrals.sourceDimension).orderBy(sql4`SUM(${gaAiReferrals.sessions}) DESC`).all();
|
|
8545
8975
|
const aiDeduped = app.db.select({
|
|
8546
8976
|
sessions: sql4`SUM(max_sessions)`,
|
|
8547
8977
|
users: sql4`SUM(max_users)`
|
|
@@ -8555,7 +8985,7 @@ async function ga4Routes(app, opts) {
|
|
|
8555
8985
|
GROUP BY date, source, medium
|
|
8556
8986
|
)`
|
|
8557
8987
|
).get();
|
|
8558
|
-
const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(
|
|
8988
|
+
const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq17(gaTrafficSummaries.projectId, project.id)).orderBy(desc7(gaTrafficSummaries.syncedAt)).limit(1).get();
|
|
8559
8989
|
return {
|
|
8560
8990
|
totalSessions: summary?.totalSessions ?? 0,
|
|
8561
8991
|
totalOrganicSessions: summary?.totalOrganicSessions ?? 0,
|
|
@@ -8588,7 +9018,7 @@ async function ga4Routes(app, opts) {
|
|
|
8588
9018
|
sourceDimension: gaAiReferrals.sourceDimension,
|
|
8589
9019
|
sessions: gaAiReferrals.sessions,
|
|
8590
9020
|
users: gaAiReferrals.users
|
|
8591
|
-
}).from(gaAiReferrals).where(
|
|
9021
|
+
}).from(gaAiReferrals).where(eq17(gaAiReferrals.projectId, project.id)).orderBy(gaAiReferrals.date).all();
|
|
8592
9022
|
return rows;
|
|
8593
9023
|
});
|
|
8594
9024
|
app.get("/projects/:name/ga/session-history", async (request, _reply) => {
|
|
@@ -8599,7 +9029,7 @@ async function ga4Routes(app, opts) {
|
|
|
8599
9029
|
sessions: sql4`SUM(${gaTrafficSnapshots.sessions})`,
|
|
8600
9030
|
organicSessions: sql4`SUM(${gaTrafficSnapshots.organicSessions})`,
|
|
8601
9031
|
users: sql4`SUM(${gaTrafficSnapshots.users})`
|
|
8602
|
-
}).from(gaTrafficSnapshots).where(
|
|
9032
|
+
}).from(gaTrafficSnapshots).where(eq17(gaTrafficSnapshots.projectId, project.id)).groupBy(gaTrafficSnapshots.date).orderBy(gaTrafficSnapshots.date).all();
|
|
8603
9033
|
return rows.map((r) => ({
|
|
8604
9034
|
date: r.date,
|
|
8605
9035
|
sessions: r.sessions ?? 0,
|
|
@@ -8615,7 +9045,7 @@ async function ga4Routes(app, opts) {
|
|
|
8615
9045
|
sessions: sql4`SUM(${gaTrafficSnapshots.sessions})`,
|
|
8616
9046
|
organicSessions: sql4`SUM(${gaTrafficSnapshots.organicSessions})`,
|
|
8617
9047
|
users: sql4`SUM(${gaTrafficSnapshots.users})`
|
|
8618
|
-
}).from(gaTrafficSnapshots).where(
|
|
9048
|
+
}).from(gaTrafficSnapshots).where(eq17(gaTrafficSnapshots.projectId, project.id)).groupBy(gaTrafficSnapshots.landingPage).orderBy(sql4`SUM(${gaTrafficSnapshots.sessions}) DESC`).all();
|
|
8619
9049
|
return {
|
|
8620
9050
|
pages: trafficPages.map((r) => ({
|
|
8621
9051
|
landingPage: r.landingPage,
|
|
@@ -8750,6 +9180,36 @@ function parseSchemaPageEntry(entry) {
|
|
|
8750
9180
|
|
|
8751
9181
|
// ../integration-wordpress/src/wordpress-client.ts
|
|
8752
9182
|
import crypto17 from "crypto";
|
|
9183
|
+
function validateUsername(username) {
|
|
9184
|
+
if (!username || typeof username !== "string" || username.trim().length === 0) {
|
|
9185
|
+
throw new WordpressApiError("AUTH_INVALID", "Username is required and must be a non-empty string", 400);
|
|
9186
|
+
}
|
|
9187
|
+
}
|
|
9188
|
+
function validateAppPassword(appPassword) {
|
|
9189
|
+
if (!appPassword || typeof appPassword !== "string" || appPassword.trim().length === 0) {
|
|
9190
|
+
throw new WordpressApiError("AUTH_INVALID", "Application password is required and must be a non-empty string", 400);
|
|
9191
|
+
}
|
|
9192
|
+
}
|
|
9193
|
+
function validateSiteUrl3(siteUrl) {
|
|
9194
|
+
if (!siteUrl || typeof siteUrl !== "string" || siteUrl.trim().length === 0) {
|
|
9195
|
+
throw new WordpressApiError("AUTH_INVALID", "Site URL is required and must be a non-empty string", 400);
|
|
9196
|
+
}
|
|
9197
|
+
try {
|
|
9198
|
+
const url = new URL(siteUrl);
|
|
9199
|
+
if (!url.protocol.startsWith("http")) {
|
|
9200
|
+
throw new WordpressApiError("AUTH_INVALID", "Site URL must be an HTTP or HTTPS URL", 400);
|
|
9201
|
+
}
|
|
9202
|
+
if (url.protocol !== "https:") {
|
|
9203
|
+
}
|
|
9204
|
+
} catch {
|
|
9205
|
+
throw new WordpressApiError("AUTH_INVALID", "Site URL must be a valid URL", 400);
|
|
9206
|
+
}
|
|
9207
|
+
}
|
|
9208
|
+
function validateConnection(connection, siteUrl) {
|
|
9209
|
+
validateUsername(connection.username);
|
|
9210
|
+
validateAppPassword(connection.appPassword);
|
|
9211
|
+
validateSiteUrl3(siteUrl);
|
|
9212
|
+
}
|
|
8753
9213
|
var WP_REQUEST_TIMEOUT_MS = 3e4;
|
|
8754
9214
|
var WP_FETCH_TEXT_TIMEOUT_MS = 15e3;
|
|
8755
9215
|
var PAGE_FIELDS = "id,slug,status,link,modified,modified_gmt,title,content,meta";
|
|
@@ -8991,6 +9451,7 @@ function getWpStagingAdminUrl(url) {
|
|
|
8991
9451
|
}
|
|
8992
9452
|
async function verifyWordpressConnection(connection) {
|
|
8993
9453
|
const site = resolveEnvironment({ ...connection, defaultEnv: "live" }, "live");
|
|
9454
|
+
validateConnection(connection, site.siteUrl);
|
|
8994
9455
|
const userInfo = await verifyAuthenticatedRestAccess(connection, site.siteUrl);
|
|
8995
9456
|
const response = await fetchPageCollectionSummary(connection, site.siteUrl, { context: "view" });
|
|
8996
9457
|
const homeHtml = await fetchText(site.siteUrl);
|
|
@@ -9005,6 +9466,7 @@ async function verifyWordpressConnection(connection) {
|
|
|
9005
9466
|
}
|
|
9006
9467
|
async function getSiteStatus(connection, env) {
|
|
9007
9468
|
const site = resolveEnvironment(connection, env);
|
|
9469
|
+
validateConnection(connection, site.siteUrl);
|
|
9008
9470
|
try {
|
|
9009
9471
|
const userInfo = await verifyAuthenticatedRestAccess(connection, site.siteUrl);
|
|
9010
9472
|
const response = await fetchPageCollectionSummary(connection, site.siteUrl, { context: "view" });
|
|
@@ -10335,6 +10797,7 @@ async function apiRoutes(app, opts) {
|
|
|
10335
10797
|
});
|
|
10336
10798
|
await api.register(historyRoutes);
|
|
10337
10799
|
await api.register(analyticsRoutes);
|
|
10800
|
+
await api.register(intelligenceRoutes);
|
|
10338
10801
|
await api.register(settingsRoutes, {
|
|
10339
10802
|
providerSummary: opts.providerSummary,
|
|
10340
10803
|
providerAdapters: opts.providerAdapters,
|
|
@@ -10386,6 +10849,27 @@ async function apiRoutes(app, opts) {
|
|
|
10386
10849
|
|
|
10387
10850
|
// ../provider-gemini/src/normalize.ts
|
|
10388
10851
|
import { GoogleGenAI } from "@google/genai";
|
|
10852
|
+
|
|
10853
|
+
// ../provider-gemini/src/utils.ts
|
|
10854
|
+
async function withRetry(fn, options = {}) {
|
|
10855
|
+
const { maxRetries = 3, initialDelay = 1e3 } = options;
|
|
10856
|
+
let lastError;
|
|
10857
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
10858
|
+
try {
|
|
10859
|
+
return await fn();
|
|
10860
|
+
} catch (err) {
|
|
10861
|
+
lastError = err;
|
|
10862
|
+
if (attempt < maxRetries) {
|
|
10863
|
+
const delay = initialDelay * Math.pow(2, attempt);
|
|
10864
|
+
console.warn(`[provider] Attempt ${attempt + 1} failed, retrying in ${delay}ms...`, err instanceof Error ? err.message : String(err));
|
|
10865
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
10866
|
+
}
|
|
10867
|
+
}
|
|
10868
|
+
}
|
|
10869
|
+
throw lastError;
|
|
10870
|
+
}
|
|
10871
|
+
|
|
10872
|
+
// ../provider-gemini/src/normalize.ts
|
|
10389
10873
|
var DEFAULT_MODEL = "gemini-3-flash";
|
|
10390
10874
|
function isVertexConfig(config) {
|
|
10391
10875
|
return !!config.vertexProject;
|
|
@@ -10434,10 +10918,12 @@ async function healthcheck(config) {
|
|
|
10434
10918
|
try {
|
|
10435
10919
|
const model = resolveModel(config);
|
|
10436
10920
|
const client = createClient2(config);
|
|
10437
|
-
const result = await
|
|
10438
|
-
|
|
10439
|
-
|
|
10440
|
-
|
|
10921
|
+
const result = await withRetry(
|
|
10922
|
+
() => client.models.generateContent({
|
|
10923
|
+
model,
|
|
10924
|
+
contents: 'Say "ok"'
|
|
10925
|
+
})
|
|
10926
|
+
);
|
|
10441
10927
|
const text2 = result.text ?? "";
|
|
10442
10928
|
const backend = isVertexConfig(config) ? "vertex ai" : "api key";
|
|
10443
10929
|
return {
|
|
@@ -10459,22 +10945,29 @@ async function executeTrackedQuery(input) {
|
|
|
10459
10945
|
const model = resolveModel(input.config);
|
|
10460
10946
|
const prompt = buildPrompt(input.keyword, input.location);
|
|
10461
10947
|
const client = createClient2(input.config);
|
|
10462
|
-
|
|
10463
|
-
|
|
10464
|
-
|
|
10465
|
-
|
|
10466
|
-
|
|
10467
|
-
|
|
10468
|
-
|
|
10469
|
-
|
|
10470
|
-
|
|
10471
|
-
|
|
10472
|
-
|
|
10473
|
-
|
|
10474
|
-
|
|
10475
|
-
|
|
10476
|
-
|
|
10477
|
-
|
|
10948
|
+
try {
|
|
10949
|
+
const result = await withRetry(
|
|
10950
|
+
() => client.models.generateContent({
|
|
10951
|
+
model,
|
|
10952
|
+
contents: prompt,
|
|
10953
|
+
config: {
|
|
10954
|
+
tools: [{ googleSearch: {} }]
|
|
10955
|
+
}
|
|
10956
|
+
})
|
|
10957
|
+
);
|
|
10958
|
+
const groundingSources = extractGroundingMetadata(result);
|
|
10959
|
+
const searchQueries = extractSearchQueries(result);
|
|
10960
|
+
return {
|
|
10961
|
+
provider: "gemini",
|
|
10962
|
+
rawResponse: responseToRecord(result),
|
|
10963
|
+
model,
|
|
10964
|
+
groundingSources,
|
|
10965
|
+
searchQueries
|
|
10966
|
+
};
|
|
10967
|
+
} catch (err) {
|
|
10968
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
10969
|
+
throw new Error(`[provider-gemini] ${msg}`);
|
|
10970
|
+
}
|
|
10478
10971
|
}
|
|
10479
10972
|
function normalizeResult(raw) {
|
|
10480
10973
|
const answerText = extractAnswerText(raw.rawResponse);
|
|
@@ -10576,10 +11069,12 @@ function extractDomainFromUri(uri) {
|
|
|
10576
11069
|
async function generateText(prompt, config) {
|
|
10577
11070
|
const model = resolveModel(config);
|
|
10578
11071
|
const client = createClient2(config);
|
|
10579
|
-
const result = await
|
|
10580
|
-
|
|
10581
|
-
|
|
10582
|
-
|
|
11072
|
+
const result = await withRetry(
|
|
11073
|
+
() => client.models.generateContent({
|
|
11074
|
+
model,
|
|
11075
|
+
contents: prompt
|
|
11076
|
+
})
|
|
11077
|
+
);
|
|
10583
11078
|
return result.text ?? "";
|
|
10584
11079
|
}
|
|
10585
11080
|
function responseToRecord(response) {
|
|
@@ -10688,6 +11183,27 @@ var geminiAdapter = {
|
|
|
10688
11183
|
|
|
10689
11184
|
// ../provider-openai/src/normalize.ts
|
|
10690
11185
|
import OpenAI from "openai";
|
|
11186
|
+
|
|
11187
|
+
// ../provider-openai/src/utils.ts
|
|
11188
|
+
async function withRetry2(fn, options = {}) {
|
|
11189
|
+
const { maxRetries = 3, initialDelay = 1e3 } = options;
|
|
11190
|
+
let lastError;
|
|
11191
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
11192
|
+
try {
|
|
11193
|
+
return await fn();
|
|
11194
|
+
} catch (err) {
|
|
11195
|
+
lastError = err;
|
|
11196
|
+
if (attempt < maxRetries) {
|
|
11197
|
+
const delay = initialDelay * Math.pow(2, attempt);
|
|
11198
|
+
console.warn(`[provider] Attempt ${attempt + 1} failed, retrying in ${delay}ms...`, err instanceof Error ? err.message : String(err));
|
|
11199
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
11200
|
+
}
|
|
11201
|
+
}
|
|
11202
|
+
}
|
|
11203
|
+
throw lastError;
|
|
11204
|
+
}
|
|
11205
|
+
|
|
11206
|
+
// ../provider-openai/src/normalize.ts
|
|
10691
11207
|
var DEFAULT_MODEL2 = "gpt-5.4";
|
|
10692
11208
|
function validateConfig2(config) {
|
|
10693
11209
|
if (!config.apiKey || config.apiKey.length === 0) {
|
|
@@ -10705,10 +11221,12 @@ async function healthcheck2(config) {
|
|
|
10705
11221
|
if (!validation.ok) return validation;
|
|
10706
11222
|
try {
|
|
10707
11223
|
const client = new OpenAI({ apiKey: config.apiKey });
|
|
10708
|
-
const response = await
|
|
10709
|
-
|
|
10710
|
-
|
|
10711
|
-
|
|
11224
|
+
const response = await withRetry2(
|
|
11225
|
+
() => client.responses.create({
|
|
11226
|
+
model: config.model ?? DEFAULT_MODEL2,
|
|
11227
|
+
input: 'Say "ok"'
|
|
11228
|
+
})
|
|
11229
|
+
);
|
|
10712
11230
|
const text2 = extractResponseText(response);
|
|
10713
11231
|
return {
|
|
10714
11232
|
ok: text2.length > 0,
|
|
@@ -10738,21 +11256,28 @@ async function executeTrackedQuery2(input) {
|
|
|
10738
11256
|
...input.location.timezone ? { timezone: input.location.timezone } : {}
|
|
10739
11257
|
};
|
|
10740
11258
|
}
|
|
10741
|
-
|
|
10742
|
-
|
|
10743
|
-
|
|
10744
|
-
|
|
10745
|
-
|
|
10746
|
-
|
|
10747
|
-
|
|
10748
|
-
|
|
10749
|
-
|
|
10750
|
-
|
|
10751
|
-
|
|
10752
|
-
|
|
10753
|
-
|
|
10754
|
-
|
|
10755
|
-
|
|
11259
|
+
try {
|
|
11260
|
+
const response = await withRetry2(
|
|
11261
|
+
() => client.responses.create({
|
|
11262
|
+
model,
|
|
11263
|
+
tools: [webSearchTool],
|
|
11264
|
+
tool_choice: "required",
|
|
11265
|
+
input: buildPrompt2(input.keyword)
|
|
11266
|
+
})
|
|
11267
|
+
);
|
|
11268
|
+
const groundingSources = extractGroundingSources(response);
|
|
11269
|
+
const searchQueries = extractSearchQueries2(response);
|
|
11270
|
+
return {
|
|
11271
|
+
provider: "openai",
|
|
11272
|
+
rawResponse: responseToRecord2(response),
|
|
11273
|
+
model,
|
|
11274
|
+
groundingSources,
|
|
11275
|
+
searchQueries
|
|
11276
|
+
};
|
|
11277
|
+
} catch (err) {
|
|
11278
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
11279
|
+
throw new Error(`[provider-openai] ${msg}`);
|
|
11280
|
+
}
|
|
10756
11281
|
}
|
|
10757
11282
|
function normalizeResult2(raw) {
|
|
10758
11283
|
const answerText = extractAnswerTextFromRaw(raw.rawResponse);
|
|
@@ -10863,10 +11388,12 @@ function extractDomainFromUri2(uri) {
|
|
|
10863
11388
|
async function generateText2(prompt, config) {
|
|
10864
11389
|
const model = config.model ?? DEFAULT_MODEL2;
|
|
10865
11390
|
const client = new OpenAI({ apiKey: config.apiKey });
|
|
10866
|
-
const response = await
|
|
10867
|
-
|
|
10868
|
-
|
|
10869
|
-
|
|
11391
|
+
const response = await withRetry2(
|
|
11392
|
+
() => client.responses.create({
|
|
11393
|
+
model,
|
|
11394
|
+
input: prompt
|
|
11395
|
+
})
|
|
11396
|
+
);
|
|
10870
11397
|
return extractResponseText(response);
|
|
10871
11398
|
}
|
|
10872
11399
|
function responseToRecord2(response) {
|
|
@@ -10962,6 +11489,27 @@ var openaiAdapter = {
|
|
|
10962
11489
|
|
|
10963
11490
|
// ../provider-claude/src/normalize.ts
|
|
10964
11491
|
import Anthropic from "@anthropic-ai/sdk";
|
|
11492
|
+
|
|
11493
|
+
// ../provider-claude/src/utils.ts
|
|
11494
|
+
async function withRetry3(fn, options = {}) {
|
|
11495
|
+
const { maxRetries = 3, initialDelay = 1e3 } = options;
|
|
11496
|
+
let lastError;
|
|
11497
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
11498
|
+
try {
|
|
11499
|
+
return await fn();
|
|
11500
|
+
} catch (err) {
|
|
11501
|
+
lastError = err;
|
|
11502
|
+
if (attempt < maxRetries) {
|
|
11503
|
+
const delay = initialDelay * Math.pow(2, attempt);
|
|
11504
|
+
console.warn(`[provider] Attempt ${attempt + 1} failed, retrying in ${delay}ms...`, err instanceof Error ? err.message : String(err));
|
|
11505
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
11506
|
+
}
|
|
11507
|
+
}
|
|
11508
|
+
}
|
|
11509
|
+
throw lastError;
|
|
11510
|
+
}
|
|
11511
|
+
|
|
11512
|
+
// ../provider-claude/src/normalize.ts
|
|
10965
11513
|
var DEFAULT_MODEL3 = "claude-sonnet-4-6";
|
|
10966
11514
|
var VALIDATION_PATTERN = /^claude-/;
|
|
10967
11515
|
function resolveModel2(config) {
|
|
@@ -10992,11 +11540,13 @@ async function healthcheck3(config) {
|
|
|
10992
11540
|
try {
|
|
10993
11541
|
const model = resolveModel2(config);
|
|
10994
11542
|
const client = new Anthropic({ apiKey: config.apiKey });
|
|
10995
|
-
const response = await
|
|
10996
|
-
|
|
10997
|
-
|
|
10998
|
-
|
|
10999
|
-
|
|
11543
|
+
const response = await withRetry3(
|
|
11544
|
+
() => client.messages.create({
|
|
11545
|
+
model,
|
|
11546
|
+
max_tokens: 32,
|
|
11547
|
+
messages: [{ role: "user", content: 'Say "ok"' }]
|
|
11548
|
+
})
|
|
11549
|
+
);
|
|
11000
11550
|
const text2 = extractTextFromResponse(response);
|
|
11001
11551
|
return {
|
|
11002
11552
|
ok: text2.length > 0,
|
|
@@ -11030,21 +11580,28 @@ async function executeTrackedQuery3(input) {
|
|
|
11030
11580
|
...input.location.timezone ? { timezone: input.location.timezone } : {}
|
|
11031
11581
|
};
|
|
11032
11582
|
}
|
|
11033
|
-
|
|
11034
|
-
|
|
11035
|
-
|
|
11036
|
-
|
|
11037
|
-
|
|
11038
|
-
|
|
11039
|
-
|
|
11040
|
-
|
|
11041
|
-
|
|
11042
|
-
|
|
11043
|
-
|
|
11044
|
-
|
|
11045
|
-
|
|
11046
|
-
|
|
11047
|
-
|
|
11583
|
+
try {
|
|
11584
|
+
const response = await withRetry3(
|
|
11585
|
+
() => client.messages.create({
|
|
11586
|
+
model,
|
|
11587
|
+
max_tokens: 4096,
|
|
11588
|
+
tools: [webSearchTool],
|
|
11589
|
+
messages: [{ role: "user", content: input.keyword }]
|
|
11590
|
+
})
|
|
11591
|
+
);
|
|
11592
|
+
const groundingSources = extractGroundingSources2(response);
|
|
11593
|
+
const searchQueries = extractSearchQueries3(response);
|
|
11594
|
+
return {
|
|
11595
|
+
provider: "claude",
|
|
11596
|
+
rawResponse: responseToRecord3(response),
|
|
11597
|
+
model,
|
|
11598
|
+
groundingSources,
|
|
11599
|
+
searchQueries
|
|
11600
|
+
};
|
|
11601
|
+
} catch (err) {
|
|
11602
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
11603
|
+
throw new Error(`[provider-claude] ${msg}`);
|
|
11604
|
+
}
|
|
11048
11605
|
}
|
|
11049
11606
|
function normalizeResult3(raw) {
|
|
11050
11607
|
const answerText = extractAnswerTextFromRaw2(raw.rawResponse);
|
|
@@ -11138,11 +11695,13 @@ function extractDomainFromUri3(uri) {
|
|
|
11138
11695
|
async function generateText3(prompt, config) {
|
|
11139
11696
|
const model = resolveModel2(config);
|
|
11140
11697
|
const client = new Anthropic({ apiKey: config.apiKey });
|
|
11141
|
-
const response = await
|
|
11142
|
-
|
|
11143
|
-
|
|
11144
|
-
|
|
11145
|
-
|
|
11698
|
+
const response = await withRetry3(
|
|
11699
|
+
() => client.messages.create({
|
|
11700
|
+
model,
|
|
11701
|
+
max_tokens: 2048,
|
|
11702
|
+
messages: [{ role: "user", content: prompt }]
|
|
11703
|
+
})
|
|
11704
|
+
);
|
|
11146
11705
|
return extractTextFromResponse(response);
|
|
11147
11706
|
}
|
|
11148
11707
|
function responseToRecord3(response) {
|
|
@@ -11235,6 +11794,27 @@ var claudeAdapter = {
|
|
|
11235
11794
|
|
|
11236
11795
|
// ../provider-local/src/normalize.ts
|
|
11237
11796
|
import OpenAI2 from "openai";
|
|
11797
|
+
|
|
11798
|
+
// ../provider-local/src/utils.ts
|
|
11799
|
+
async function withRetry4(fn, options = {}) {
|
|
11800
|
+
const { maxRetries = 3, initialDelay = 1e3 } = options;
|
|
11801
|
+
let lastError;
|
|
11802
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
11803
|
+
try {
|
|
11804
|
+
return await fn();
|
|
11805
|
+
} catch (err) {
|
|
11806
|
+
lastError = err;
|
|
11807
|
+
if (attempt < maxRetries) {
|
|
11808
|
+
const delay = initialDelay * Math.pow(2, attempt);
|
|
11809
|
+
console.warn(`[provider] Attempt ${attempt + 1} failed, retrying in ${delay}ms...`, err instanceof Error ? err.message : String(err));
|
|
11810
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
11811
|
+
}
|
|
11812
|
+
}
|
|
11813
|
+
}
|
|
11814
|
+
throw lastError;
|
|
11815
|
+
}
|
|
11816
|
+
|
|
11817
|
+
// ../provider-local/src/normalize.ts
|
|
11238
11818
|
var DEFAULT_MODEL4 = "llama3";
|
|
11239
11819
|
function validateConfig4(config) {
|
|
11240
11820
|
if (!config.baseUrl || config.baseUrl.length === 0) {
|
|
@@ -11255,16 +11835,19 @@ async function healthcheck4(config) {
|
|
|
11255
11835
|
baseURL: config.baseUrl,
|
|
11256
11836
|
apiKey: config.apiKey || "not-needed"
|
|
11257
11837
|
});
|
|
11258
|
-
const models = await
|
|
11259
|
-
|
|
11260
|
-
|
|
11261
|
-
|
|
11262
|
-
|
|
11263
|
-
|
|
11838
|
+
const models = await withRetry4(async () => {
|
|
11839
|
+
const list = await client.models.list();
|
|
11840
|
+
const items = [];
|
|
11841
|
+
for await (const m of list) {
|
|
11842
|
+
items.push(m.id);
|
|
11843
|
+
if (items.length >= 5) break;
|
|
11844
|
+
}
|
|
11845
|
+
return items;
|
|
11846
|
+
});
|
|
11264
11847
|
return {
|
|
11265
11848
|
ok: true,
|
|
11266
11849
|
provider: "local",
|
|
11267
|
-
message: `connected, ${
|
|
11850
|
+
message: `connected, ${models.length} model(s) available`,
|
|
11268
11851
|
model: config.model ?? DEFAULT_MODEL4
|
|
11269
11852
|
};
|
|
11270
11853
|
} catch (err) {
|
|
@@ -11282,26 +11865,33 @@ async function executeTrackedQuery4(input) {
|
|
|
11282
11865
|
baseURL: input.config.baseUrl,
|
|
11283
11866
|
apiKey: input.config.apiKey || "not-needed"
|
|
11284
11867
|
});
|
|
11285
|
-
|
|
11286
|
-
|
|
11287
|
-
|
|
11288
|
-
|
|
11289
|
-
|
|
11290
|
-
|
|
11291
|
-
|
|
11292
|
-
|
|
11293
|
-
|
|
11294
|
-
|
|
11295
|
-
|
|
11296
|
-
|
|
11297
|
-
|
|
11298
|
-
|
|
11299
|
-
|
|
11300
|
-
|
|
11301
|
-
|
|
11302
|
-
|
|
11303
|
-
|
|
11304
|
-
|
|
11868
|
+
try {
|
|
11869
|
+
const response = await withRetry4(
|
|
11870
|
+
() => client.chat.completions.create({
|
|
11871
|
+
model,
|
|
11872
|
+
messages: [
|
|
11873
|
+
{
|
|
11874
|
+
role: "system",
|
|
11875
|
+
content: "You are a helpful assistant. Provide comprehensive, factual answers. When mentioning websites or services, include their domain names."
|
|
11876
|
+
},
|
|
11877
|
+
{
|
|
11878
|
+
role: "user",
|
|
11879
|
+
content: buildPrompt3(input.keyword, input.location)
|
|
11880
|
+
}
|
|
11881
|
+
]
|
|
11882
|
+
})
|
|
11883
|
+
);
|
|
11884
|
+
return {
|
|
11885
|
+
provider: "local",
|
|
11886
|
+
rawResponse: responseToRecord4(response),
|
|
11887
|
+
model,
|
|
11888
|
+
groundingSources: [],
|
|
11889
|
+
searchQueries: []
|
|
11890
|
+
};
|
|
11891
|
+
} catch (err) {
|
|
11892
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
11893
|
+
throw new Error(`[provider-local] ${msg}`);
|
|
11894
|
+
}
|
|
11305
11895
|
}
|
|
11306
11896
|
function normalizeResult4(raw) {
|
|
11307
11897
|
const answerText = extractAnswerText2(raw.rawResponse);
|
|
@@ -11333,10 +11923,12 @@ async function generateText4(prompt, config) {
|
|
|
11333
11923
|
baseURL: config.baseUrl,
|
|
11334
11924
|
apiKey: config.apiKey || "not-needed"
|
|
11335
11925
|
});
|
|
11336
|
-
const response = await
|
|
11337
|
-
|
|
11338
|
-
|
|
11339
|
-
|
|
11926
|
+
const response = await withRetry4(
|
|
11927
|
+
() => client.chat.completions.create({
|
|
11928
|
+
model,
|
|
11929
|
+
messages: [{ role: "user", content: prompt }]
|
|
11930
|
+
})
|
|
11931
|
+
);
|
|
11340
11932
|
return response.choices[0]?.message?.content ?? "";
|
|
11341
11933
|
}
|
|
11342
11934
|
function extractDomainMentions(text2) {
|
|
@@ -11952,35 +12544,40 @@ var cdpChatgptAdapter = {
|
|
|
11952
12544
|
async executeTrackedQuery(input, config) {
|
|
11953
12545
|
const conn = getConnection(config);
|
|
11954
12546
|
const target = chatgptTarget;
|
|
11955
|
-
const client = await conn.prepareForQuery(target);
|
|
11956
|
-
await target.submitQuery(client, input.keyword);
|
|
11957
|
-
await target.waitForResponse(client);
|
|
11958
|
-
const answerText = await target.extractAnswer(client);
|
|
11959
|
-
const groundingSources = await target.extractCitations(client);
|
|
11960
|
-
const screenshotId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
11961
|
-
const screenshotPath = path4.join(getScreenshotDir2(), `${screenshotId}.png`);
|
|
11962
|
-
let capturedScreenshotPath;
|
|
11963
12547
|
try {
|
|
11964
|
-
|
|
11965
|
-
|
|
11966
|
-
|
|
11967
|
-
|
|
11968
|
-
);
|
|
11969
|
-
|
|
11970
|
-
|
|
11971
|
-
|
|
11972
|
-
|
|
11973
|
-
|
|
11974
|
-
|
|
12548
|
+
const client = await conn.prepareForQuery(target);
|
|
12549
|
+
await target.submitQuery(client, input.keyword);
|
|
12550
|
+
await target.waitForResponse(client);
|
|
12551
|
+
const answerText = await target.extractAnswer(client);
|
|
12552
|
+
const groundingSources = await target.extractCitations(client);
|
|
12553
|
+
const screenshotId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
12554
|
+
const screenshotPath = path4.join(getScreenshotDir2(), `${screenshotId}.png`);
|
|
12555
|
+
let capturedScreenshotPath;
|
|
12556
|
+
try {
|
|
12557
|
+
capturedScreenshotPath = await captureElementScreenshot(
|
|
12558
|
+
client,
|
|
12559
|
+
target.responseSelector,
|
|
12560
|
+
screenshotPath
|
|
12561
|
+
);
|
|
12562
|
+
} catch {
|
|
12563
|
+
}
|
|
12564
|
+
return {
|
|
12565
|
+
provider: "cdp:chatgpt",
|
|
12566
|
+
rawResponse: {
|
|
12567
|
+
answerText,
|
|
12568
|
+
groundingSources,
|
|
12569
|
+
extractedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
12570
|
+
targetUrl: target.newConversationUrl
|
|
12571
|
+
},
|
|
12572
|
+
model: "chatgpt-web",
|
|
11975
12573
|
groundingSources,
|
|
11976
|
-
|
|
11977
|
-
|
|
11978
|
-
}
|
|
11979
|
-
|
|
11980
|
-
|
|
11981
|
-
|
|
11982
|
-
|
|
11983
|
-
};
|
|
12574
|
+
searchQueries: [input.keyword],
|
|
12575
|
+
screenshotPath: capturedScreenshotPath
|
|
12576
|
+
};
|
|
12577
|
+
} catch (err) {
|
|
12578
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
12579
|
+
throw new Error(`[provider-cdp] ${msg}`);
|
|
12580
|
+
}
|
|
11984
12581
|
},
|
|
11985
12582
|
normalizeResult(raw) {
|
|
11986
12583
|
return normalizeResult5(raw);
|
|
@@ -11992,6 +12589,27 @@ var cdpChatgptAdapter = {
|
|
|
11992
12589
|
|
|
11993
12590
|
// ../provider-perplexity/src/normalize.ts
|
|
11994
12591
|
import OpenAI3 from "openai";
|
|
12592
|
+
|
|
12593
|
+
// ../provider-perplexity/src/utils.ts
|
|
12594
|
+
async function withRetry5(fn, options = {}) {
|
|
12595
|
+
const { maxRetries = 3, initialDelay = 1e3 } = options;
|
|
12596
|
+
let lastError;
|
|
12597
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
12598
|
+
try {
|
|
12599
|
+
return await fn();
|
|
12600
|
+
} catch (err) {
|
|
12601
|
+
lastError = err;
|
|
12602
|
+
if (attempt < maxRetries) {
|
|
12603
|
+
const delay = initialDelay * Math.pow(2, attempt);
|
|
12604
|
+
console.warn(`[provider] Attempt ${attempt + 1} failed, retrying in ${delay}ms...`, err instanceof Error ? err.message : String(err));
|
|
12605
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
12606
|
+
}
|
|
12607
|
+
}
|
|
12608
|
+
}
|
|
12609
|
+
throw lastError;
|
|
12610
|
+
}
|
|
12611
|
+
|
|
12612
|
+
// ../provider-perplexity/src/normalize.ts
|
|
11995
12613
|
var DEFAULT_MODEL5 = "sonar";
|
|
11996
12614
|
var BASE_URL = "https://api.perplexity.ai";
|
|
11997
12615
|
function validateConfig5(config) {
|
|
@@ -12010,10 +12628,12 @@ async function healthcheck5(config) {
|
|
|
12010
12628
|
if (!validation.ok) return validation;
|
|
12011
12629
|
try {
|
|
12012
12630
|
const client = new OpenAI3({ apiKey: config.apiKey, baseURL: BASE_URL });
|
|
12013
|
-
const response = await
|
|
12014
|
-
|
|
12015
|
-
|
|
12016
|
-
|
|
12631
|
+
const response = await withRetry5(
|
|
12632
|
+
() => client.chat.completions.create({
|
|
12633
|
+
model: config.model ?? DEFAULT_MODEL5,
|
|
12634
|
+
messages: [{ role: "user", content: 'Say "ok"' }]
|
|
12635
|
+
})
|
|
12636
|
+
);
|
|
12017
12637
|
const text2 = response.choices[0]?.message?.content ?? "";
|
|
12018
12638
|
return {
|
|
12019
12639
|
ok: text2.length > 0,
|
|
@@ -12034,25 +12654,30 @@ async function executeTrackedQuery5(input) {
|
|
|
12034
12654
|
const model = input.config.model ?? DEFAULT_MODEL5;
|
|
12035
12655
|
const client = new OpenAI3({ apiKey: input.config.apiKey, baseURL: BASE_URL });
|
|
12036
12656
|
const prompt = buildPrompt4(input.keyword, input.location);
|
|
12037
|
-
|
|
12038
|
-
|
|
12039
|
-
|
|
12040
|
-
|
|
12041
|
-
|
|
12042
|
-
|
|
12043
|
-
|
|
12044
|
-
|
|
12045
|
-
|
|
12046
|
-
|
|
12047
|
-
|
|
12048
|
-
|
|
12049
|
-
|
|
12050
|
-
|
|
12051
|
-
|
|
12052
|
-
|
|
12053
|
-
|
|
12054
|
-
|
|
12055
|
-
|
|
12657
|
+
try {
|
|
12658
|
+
const response = await withRetry5(
|
|
12659
|
+
() => client.chat.completions.create({
|
|
12660
|
+
model,
|
|
12661
|
+
messages: [{ role: "user", content: prompt }]
|
|
12662
|
+
})
|
|
12663
|
+
);
|
|
12664
|
+
const rawResponse = responseToRecord5(response);
|
|
12665
|
+
const citations = extractCitations(rawResponse);
|
|
12666
|
+
const groundingSources = citations.map((url) => ({
|
|
12667
|
+
uri: url,
|
|
12668
|
+
title: ""
|
|
12669
|
+
}));
|
|
12670
|
+
return {
|
|
12671
|
+
provider: "perplexity",
|
|
12672
|
+
rawResponse,
|
|
12673
|
+
model,
|
|
12674
|
+
groundingSources,
|
|
12675
|
+
searchQueries: [input.keyword]
|
|
12676
|
+
};
|
|
12677
|
+
} catch (err) {
|
|
12678
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
12679
|
+
throw new Error(`[provider-perplexity] ${msg}`);
|
|
12680
|
+
}
|
|
12056
12681
|
}
|
|
12057
12682
|
function normalizeResult6(raw) {
|
|
12058
12683
|
const answerText = extractAnswerText3(raw.rawResponse);
|
|
@@ -12112,10 +12737,12 @@ function extractDomainFromUri4(uri) {
|
|
|
12112
12737
|
async function generateText5(prompt, config) {
|
|
12113
12738
|
const model = config.model ?? DEFAULT_MODEL5;
|
|
12114
12739
|
const client = new OpenAI3({ apiKey: config.apiKey, baseURL: BASE_URL });
|
|
12115
|
-
const response = await
|
|
12116
|
-
|
|
12117
|
-
|
|
12118
|
-
|
|
12740
|
+
const response = await withRetry5(
|
|
12741
|
+
() => client.chat.completions.create({
|
|
12742
|
+
model,
|
|
12743
|
+
messages: [{ role: "user", content: prompt }]
|
|
12744
|
+
})
|
|
12745
|
+
);
|
|
12119
12746
|
return response.choices[0]?.message?.content ?? "";
|
|
12120
12747
|
}
|
|
12121
12748
|
function responseToRecord5(response) {
|
|
@@ -12368,7 +12995,7 @@ import crypto18 from "crypto";
|
|
|
12368
12995
|
import fs4 from "fs";
|
|
12369
12996
|
import path5 from "path";
|
|
12370
12997
|
import os4 from "os";
|
|
12371
|
-
import { and as
|
|
12998
|
+
import { and as and7, eq as eq18, inArray as inArray3, sql as sql5 } from "drizzle-orm";
|
|
12372
12999
|
|
|
12373
13000
|
// src/logger.ts
|
|
12374
13001
|
var IS_TTY = process.stdout.isTTY === true;
|
|
@@ -12390,7 +13017,7 @@ function emit(entry) {
|
|
|
12390
13017
|
}
|
|
12391
13018
|
}
|
|
12392
13019
|
function createLogger(module) {
|
|
12393
|
-
function
|
|
13020
|
+
function log10(level, action, ctx) {
|
|
12394
13021
|
const entry = {
|
|
12395
13022
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
12396
13023
|
level,
|
|
@@ -12401,9 +13028,9 @@ function createLogger(module) {
|
|
|
12401
13028
|
emit(entry);
|
|
12402
13029
|
}
|
|
12403
13030
|
return {
|
|
12404
|
-
info: (action, ctx) =>
|
|
12405
|
-
warn: (action, ctx) =>
|
|
12406
|
-
error: (action, ctx) =>
|
|
13031
|
+
info: (action, ctx) => log10("info", action, ctx),
|
|
13032
|
+
warn: (action, ctx) => log10("warn", action, ctx),
|
|
13033
|
+
error: (action, ctx) => log10("error", action, ctx)
|
|
12407
13034
|
};
|
|
12408
13035
|
}
|
|
12409
13036
|
|
|
@@ -12490,7 +13117,7 @@ var JobRunner = class {
|
|
|
12490
13117
|
if (stale.length === 0) return;
|
|
12491
13118
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
12492
13119
|
for (const run of stale) {
|
|
12493
|
-
this.db.update(runs).set({ status: "failed", finishedAt: now, error: "Server restarted while run was in progress" }).where(
|
|
13120
|
+
this.db.update(runs).set({ status: "failed", finishedAt: now, error: "Server restarted while run was in progress" }).where(eq18(runs.id, run.id)).run();
|
|
12494
13121
|
log.warn("run.recovered-stale", { runId: run.id, previousStatus: run.status });
|
|
12495
13122
|
}
|
|
12496
13123
|
}
|
|
@@ -12518,10 +13145,10 @@ var JobRunner = class {
|
|
|
12518
13145
|
throw new Error(`Run ${runId} is not executable from status '${existingRun.status}'`);
|
|
12519
13146
|
}
|
|
12520
13147
|
if (existingRun.status === "queued") {
|
|
12521
|
-
this.db.update(runs).set({ status: "running", startedAt: now }).where(
|
|
13148
|
+
this.db.update(runs).set({ status: "running", startedAt: now }).where(and7(eq18(runs.id, runId), eq18(runs.status, "queued"))).run();
|
|
12522
13149
|
}
|
|
12523
13150
|
this.throwIfRunCancelled(runId);
|
|
12524
|
-
const project = this.db.select().from(projects).where(
|
|
13151
|
+
const project = this.db.select().from(projects).where(eq18(projects.id, projectId)).get();
|
|
12525
13152
|
if (!project) {
|
|
12526
13153
|
throw new Error(`Project ${projectId} not found`);
|
|
12527
13154
|
}
|
|
@@ -12541,8 +13168,8 @@ var JobRunner = class {
|
|
|
12541
13168
|
throw new Error("No providers configured. Add at least one provider API key.");
|
|
12542
13169
|
}
|
|
12543
13170
|
log.info("run.dispatch", { runId, providerCount: activeProviders.length, providers: activeProviders.map((p) => p.adapter.name) });
|
|
12544
|
-
projectKeywords = this.db.select().from(keywords).where(
|
|
12545
|
-
const projectCompetitors = this.db.select().from(competitors).where(
|
|
13171
|
+
projectKeywords = this.db.select().from(keywords).where(eq18(keywords.projectId, projectId)).all();
|
|
13172
|
+
const projectCompetitors = this.db.select().from(competitors).where(eq18(competitors.projectId, projectId)).all();
|
|
12546
13173
|
const competitorDomains = projectCompetitors.map((c) => c.domain);
|
|
12547
13174
|
const allDomains = effectiveDomains({
|
|
12548
13175
|
canonicalDomain: project.canonicalDomain,
|
|
@@ -12558,7 +13185,7 @@ var JobRunner = class {
|
|
|
12558
13185
|
const todayPeriod = getCurrentUsageDay();
|
|
12559
13186
|
for (const p of activeProviders) {
|
|
12560
13187
|
const providerScope = `${projectId}:${p.adapter.name}`;
|
|
12561
|
-
const providerUsage = this.db.select().from(usageCounters).where(
|
|
13188
|
+
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);
|
|
12562
13189
|
const limit = p.config.quotaPolicy.maxRequestsPerDay;
|
|
12563
13190
|
if (providerUsage + queriesPerProvider > limit) {
|
|
12564
13191
|
throw new Error(
|
|
@@ -12699,12 +13326,12 @@ var JobRunner = class {
|
|
|
12699
13326
|
const someFailed = providerErrors.size > 0;
|
|
12700
13327
|
if (allFailed) {
|
|
12701
13328
|
const errorDetail = JSON.stringify(Object.fromEntries(providerErrors));
|
|
12702
|
-
this.db.update(runs).set({ status: "failed", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(
|
|
13329
|
+
this.db.update(runs).set({ status: "failed", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(eq18(runs.id, runId)).run();
|
|
12703
13330
|
} else if (someFailed) {
|
|
12704
13331
|
const errorDetail = JSON.stringify(Object.fromEntries(providerErrors));
|
|
12705
|
-
this.db.update(runs).set({ status: "partial", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(
|
|
13332
|
+
this.db.update(runs).set({ status: "partial", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(eq18(runs.id, runId)).run();
|
|
12706
13333
|
} else {
|
|
12707
|
-
this.db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
13334
|
+
this.db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq18(runs.id, runId)).run();
|
|
12708
13335
|
}
|
|
12709
13336
|
this.flushProviderUsage(projectId, providerDispatchCounts);
|
|
12710
13337
|
const finalStatus = allFailed ? "failed" : someFailed ? "partial" : "completed";
|
|
@@ -12739,7 +13366,7 @@ var JobRunner = class {
|
|
|
12739
13366
|
status: "failed",
|
|
12740
13367
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
12741
13368
|
error: errorMessage
|
|
12742
|
-
}).where(
|
|
13369
|
+
}).where(eq18(runs.id, runId)).run();
|
|
12743
13370
|
this.flushProviderUsage(projectId, providerDispatchCounts);
|
|
12744
13371
|
trackEvent("run.completed", {
|
|
12745
13372
|
status: "failed",
|
|
@@ -12782,7 +13409,7 @@ var JobRunner = class {
|
|
|
12782
13409
|
status: runs.status,
|
|
12783
13410
|
finishedAt: runs.finishedAt,
|
|
12784
13411
|
error: runs.error
|
|
12785
|
-
}).from(runs).where(
|
|
13412
|
+
}).from(runs).where(eq18(runs.id, runId)).get();
|
|
12786
13413
|
}
|
|
12787
13414
|
isRunCancelled(runId) {
|
|
12788
13415
|
return this.getRunState(runId)?.status === "cancelled";
|
|
@@ -12798,7 +13425,7 @@ var JobRunner = class {
|
|
|
12798
13425
|
this.db.update(runs).set({
|
|
12799
13426
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
12800
13427
|
error: currentRun.error ?? "Cancelled by user"
|
|
12801
|
-
}).where(
|
|
13428
|
+
}).where(eq18(runs.id, runId)).run();
|
|
12802
13429
|
}
|
|
12803
13430
|
trackEvent("run.completed", {
|
|
12804
13431
|
status: "cancelled",
|
|
@@ -12970,7 +13597,7 @@ function matchesBrandKey(candidateKey, brandKeys) {
|
|
|
12970
13597
|
|
|
12971
13598
|
// src/gsc-sync.ts
|
|
12972
13599
|
import crypto19 from "crypto";
|
|
12973
|
-
import { eq as
|
|
13600
|
+
import { eq as eq19, and as and8, sql as sql6 } from "drizzle-orm";
|
|
12974
13601
|
var log2 = createLogger("GscSync");
|
|
12975
13602
|
function formatDate2(d) {
|
|
12976
13603
|
return d.toISOString().split("T")[0];
|
|
@@ -12982,13 +13609,13 @@ function daysAgo(n) {
|
|
|
12982
13609
|
}
|
|
12983
13610
|
async function executeGscSync(db, runId, projectId, opts) {
|
|
12984
13611
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
12985
|
-
db.update(runs).set({ status: "running", startedAt: now }).where(
|
|
13612
|
+
db.update(runs).set({ status: "running", startedAt: now }).where(eq19(runs.id, runId)).run();
|
|
12986
13613
|
try {
|
|
12987
13614
|
const { clientId: googleClientId, clientSecret: googleClientSecret } = getGoogleAuthConfig(opts.config);
|
|
12988
13615
|
if (!googleClientId || !googleClientSecret) {
|
|
12989
13616
|
throw new Error("Google OAuth is not configured in the local Canonry config");
|
|
12990
13617
|
}
|
|
12991
|
-
const project = db.select().from(projects).where(
|
|
13618
|
+
const project = db.select().from(projects).where(eq19(projects.id, projectId)).get();
|
|
12992
13619
|
if (!project) {
|
|
12993
13620
|
throw new Error(`Project not found: ${projectId}`);
|
|
12994
13621
|
}
|
|
@@ -13022,8 +13649,8 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
13022
13649
|
});
|
|
13023
13650
|
log2.info("fetch.complete", { runId, projectId, rowCount: rows.length });
|
|
13024
13651
|
db.delete(gscSearchData).where(
|
|
13025
|
-
|
|
13026
|
-
|
|
13652
|
+
and8(
|
|
13653
|
+
eq19(gscSearchData.projectId, projectId),
|
|
13027
13654
|
sql6`${gscSearchData.date} >= ${startDate}`,
|
|
13028
13655
|
sql6`${gscSearchData.date} <= ${endDate}`
|
|
13029
13656
|
)
|
|
@@ -13090,7 +13717,7 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
13090
13717
|
log2.error("inspect.url-failed", { runId, projectId, url: pageUrl, error: err instanceof Error ? err.message : String(err) });
|
|
13091
13718
|
}
|
|
13092
13719
|
}
|
|
13093
|
-
const allInspections = db.select().from(gscUrlInspections).where(
|
|
13720
|
+
const allInspections = db.select().from(gscUrlInspections).where(eq19(gscUrlInspections.projectId, projectId)).all();
|
|
13094
13721
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
13095
13722
|
for (const row of allInspections) {
|
|
13096
13723
|
const existing = latestByUrl.get(row.url);
|
|
@@ -13111,7 +13738,7 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
13111
13738
|
}
|
|
13112
13739
|
}
|
|
13113
13740
|
const snapshotDate = formatDate2(/* @__PURE__ */ new Date());
|
|
13114
|
-
db.delete(gscCoverageSnapshots).where(
|
|
13741
|
+
db.delete(gscCoverageSnapshots).where(and8(eq19(gscCoverageSnapshots.projectId, projectId), eq19(gscCoverageSnapshots.date, snapshotDate))).run();
|
|
13115
13742
|
db.insert(gscCoverageSnapshots).values({
|
|
13116
13743
|
id: crypto19.randomUUID(),
|
|
13117
13744
|
projectId,
|
|
@@ -13122,11 +13749,11 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
13122
13749
|
reasonBreakdown: JSON.stringify(reasonCounts),
|
|
13123
13750
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
13124
13751
|
}).run();
|
|
13125
|
-
db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
13752
|
+
db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq19(runs.id, runId)).run();
|
|
13126
13753
|
log2.info("sync.completed", { runId, projectId, searchDataRows: rows.length, urlInspections: topPages.length, indexed: snapIndexed, notIndexed: snapNotIndexed });
|
|
13127
13754
|
} catch (err) {
|
|
13128
13755
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
13129
|
-
db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
13756
|
+
db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq19(runs.id, runId)).run();
|
|
13130
13757
|
log2.error("sync.failed", { runId, projectId, error: errorMsg });
|
|
13131
13758
|
throw err;
|
|
13132
13759
|
}
|
|
@@ -13134,7 +13761,7 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
13134
13761
|
|
|
13135
13762
|
// src/gsc-inspect-sitemap.ts
|
|
13136
13763
|
import crypto20 from "crypto";
|
|
13137
|
-
import { eq as
|
|
13764
|
+
import { eq as eq20, and as and9 } from "drizzle-orm";
|
|
13138
13765
|
|
|
13139
13766
|
// src/sitemap-parser.ts
|
|
13140
13767
|
var LOC_REGEX = /<loc>\s*([^<]+?)\s*<\/loc>/gi;
|
|
@@ -13203,13 +13830,13 @@ async function parseSitemapRecursive(url, urls, depth) {
|
|
|
13203
13830
|
var log3 = createLogger("InspectSitemap");
|
|
13204
13831
|
async function executeInspectSitemap(db, runId, projectId, opts) {
|
|
13205
13832
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
13206
|
-
db.update(runs).set({ status: "running", startedAt: now }).where(
|
|
13833
|
+
db.update(runs).set({ status: "running", startedAt: now }).where(eq20(runs.id, runId)).run();
|
|
13207
13834
|
try {
|
|
13208
13835
|
const { clientId: googleClientId, clientSecret: googleClientSecret } = getGoogleAuthConfig(opts.config);
|
|
13209
13836
|
if (!googleClientId || !googleClientSecret) {
|
|
13210
13837
|
throw new Error("Google OAuth is not configured in the local Canonry config");
|
|
13211
13838
|
}
|
|
13212
|
-
const project = db.select().from(projects).where(
|
|
13839
|
+
const project = db.select().from(projects).where(eq20(projects.id, projectId)).get();
|
|
13213
13840
|
if (!project) {
|
|
13214
13841
|
throw new Error(`Project not found: ${projectId}`);
|
|
13215
13842
|
}
|
|
@@ -13277,7 +13904,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
|
|
|
13277
13904
|
await new Promise((r) => setTimeout(r, 1e3));
|
|
13278
13905
|
}
|
|
13279
13906
|
}
|
|
13280
|
-
const allInspections = db.select().from(gscUrlInspections).where(
|
|
13907
|
+
const allInspections = db.select().from(gscUrlInspections).where(eq20(gscUrlInspections.projectId, projectId)).all();
|
|
13281
13908
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
13282
13909
|
for (const row of allInspections) {
|
|
13283
13910
|
const existing = latestByUrl.get(row.url);
|
|
@@ -13298,7 +13925,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
|
|
|
13298
13925
|
}
|
|
13299
13926
|
}
|
|
13300
13927
|
const snapshotDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
13301
|
-
db.delete(gscCoverageSnapshots).where(
|
|
13928
|
+
db.delete(gscCoverageSnapshots).where(and9(eq20(gscCoverageSnapshots.projectId, projectId), eq20(gscCoverageSnapshots.date, snapshotDate))).run();
|
|
13302
13929
|
db.insert(gscCoverageSnapshots).values({
|
|
13303
13930
|
id: crypto20.randomUUID(),
|
|
13304
13931
|
projectId,
|
|
@@ -13310,11 +13937,11 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
|
|
|
13310
13937
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
13311
13938
|
}).run();
|
|
13312
13939
|
const status = errors > 0 && inspected > 0 ? "partial" : errors === urls.length ? "failed" : "completed";
|
|
13313
|
-
db.update(runs).set({ status, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
13940
|
+
db.update(runs).set({ status, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq20(runs.id, runId)).run();
|
|
13314
13941
|
log3.info("inspect.completed", { runId, projectId, inspected, errors, total: urls.length, indexed: snapIndexed, notIndexed: snapNotIndexed });
|
|
13315
13942
|
} catch (err) {
|
|
13316
13943
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
13317
|
-
db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
13944
|
+
db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq20(runs.id, runId)).run();
|
|
13318
13945
|
log3.error("inspect.failed", { runId, projectId, error: errorMsg });
|
|
13319
13946
|
throw err;
|
|
13320
13947
|
}
|
|
@@ -13373,7 +14000,7 @@ var ProviderRegistry = class {
|
|
|
13373
14000
|
|
|
13374
14001
|
// src/scheduler.ts
|
|
13375
14002
|
import cron from "node-cron";
|
|
13376
|
-
import { eq as
|
|
14003
|
+
import { eq as eq21 } from "drizzle-orm";
|
|
13377
14004
|
var log4 = createLogger("Scheduler");
|
|
13378
14005
|
var Scheduler = class {
|
|
13379
14006
|
db;
|
|
@@ -13385,7 +14012,7 @@ var Scheduler = class {
|
|
|
13385
14012
|
}
|
|
13386
14013
|
/** Load all enabled schedules from DB and register cron jobs. */
|
|
13387
14014
|
start() {
|
|
13388
|
-
const allSchedules = this.db.select().from(schedules).where(
|
|
14015
|
+
const allSchedules = this.db.select().from(schedules).where(eq21(schedules.enabled, 1)).all();
|
|
13389
14016
|
for (const schedule of allSchedules) {
|
|
13390
14017
|
const missedRunAt = schedule.nextRunAt;
|
|
13391
14018
|
this.registerCronTask(schedule);
|
|
@@ -13410,7 +14037,7 @@ var Scheduler = class {
|
|
|
13410
14037
|
this.stopTask(projectId, existing, "Stopped");
|
|
13411
14038
|
this.tasks.delete(projectId);
|
|
13412
14039
|
}
|
|
13413
|
-
const schedule = this.db.select().from(schedules).where(
|
|
14040
|
+
const schedule = this.db.select().from(schedules).where(eq21(schedules.projectId, projectId)).get();
|
|
13414
14041
|
if (schedule && schedule.enabled === 1) {
|
|
13415
14042
|
this.registerCronTask(schedule);
|
|
13416
14043
|
}
|
|
@@ -13443,14 +14070,14 @@ var Scheduler = class {
|
|
|
13443
14070
|
this.db.update(schedules).set({
|
|
13444
14071
|
nextRunAt: task.getNextRun()?.toISOString() ?? null,
|
|
13445
14072
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
13446
|
-
}).where(
|
|
14073
|
+
}).where(eq21(schedules.id, scheduleId)).run();
|
|
13447
14074
|
const label = schedule.preset ?? cronExpr;
|
|
13448
14075
|
log4.info("cron.registered", { projectId, schedule: label, timezone });
|
|
13449
14076
|
}
|
|
13450
14077
|
triggerRun(scheduleId, projectId) {
|
|
13451
14078
|
try {
|
|
13452
14079
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
13453
|
-
const currentSchedule = this.db.select().from(schedules).where(
|
|
14080
|
+
const currentSchedule = this.db.select().from(schedules).where(eq21(schedules.id, scheduleId)).get();
|
|
13454
14081
|
if (!currentSchedule || currentSchedule.enabled !== 1) {
|
|
13455
14082
|
log4.warn("schedule.stale", { scheduleId, projectId, msg: "schedule no longer exists or is disabled" });
|
|
13456
14083
|
this.remove(projectId);
|
|
@@ -13458,7 +14085,7 @@ var Scheduler = class {
|
|
|
13458
14085
|
}
|
|
13459
14086
|
const task = this.tasks.get(projectId);
|
|
13460
14087
|
const nextRunAt = task?.getNextRun()?.toISOString() ?? null;
|
|
13461
|
-
const project = this.db.select().from(projects).where(
|
|
14088
|
+
const project = this.db.select().from(projects).where(eq21(projects.id, projectId)).get();
|
|
13462
14089
|
if (!project) {
|
|
13463
14090
|
log4.error("project.not-found", { projectId, msg: "skipping scheduled run" });
|
|
13464
14091
|
this.remove(projectId);
|
|
@@ -13475,7 +14102,7 @@ var Scheduler = class {
|
|
|
13475
14102
|
this.db.update(schedules).set({
|
|
13476
14103
|
nextRunAt,
|
|
13477
14104
|
updatedAt: now
|
|
13478
|
-
}).where(
|
|
14105
|
+
}).where(eq21(schedules.id, currentSchedule.id)).run();
|
|
13479
14106
|
return;
|
|
13480
14107
|
}
|
|
13481
14108
|
const runId = queueResult.runId;
|
|
@@ -13483,7 +14110,7 @@ var Scheduler = class {
|
|
|
13483
14110
|
lastRunAt: now,
|
|
13484
14111
|
nextRunAt,
|
|
13485
14112
|
updatedAt: now
|
|
13486
|
-
}).where(
|
|
14113
|
+
}).where(eq21(schedules.id, currentSchedule.id)).run();
|
|
13487
14114
|
const scheduleProviders = parseJsonColumn(currentSchedule.providers, []);
|
|
13488
14115
|
const providers = scheduleProviders.length > 0 ? scheduleProviders : void 0;
|
|
13489
14116
|
log4.info("run.triggered", { runId, projectName: project.name, providers: providers ?? "all" });
|
|
@@ -13495,7 +14122,7 @@ var Scheduler = class {
|
|
|
13495
14122
|
};
|
|
13496
14123
|
|
|
13497
14124
|
// src/notifier.ts
|
|
13498
|
-
import { eq as
|
|
14125
|
+
import { eq as eq22, desc as desc8, and as and10, or as or2 } from "drizzle-orm";
|
|
13499
14126
|
import crypto21 from "crypto";
|
|
13500
14127
|
var log5 = createLogger("Notifier");
|
|
13501
14128
|
var Notifier = class {
|
|
@@ -13508,18 +14135,18 @@ var Notifier = class {
|
|
|
13508
14135
|
/** Called after a run completes (success, partial, or failed). */
|
|
13509
14136
|
async onRunCompleted(runId, projectId) {
|
|
13510
14137
|
log5.info("run.completed", { runId, projectId });
|
|
13511
|
-
const notifs = this.db.select().from(notifications).where(
|
|
14138
|
+
const notifs = this.db.select().from(notifications).where(eq22(notifications.projectId, projectId)).all().filter((n) => n.enabled === 1);
|
|
13512
14139
|
if (notifs.length === 0) {
|
|
13513
14140
|
log5.info("notifications.none-enabled", { projectId });
|
|
13514
14141
|
return;
|
|
13515
14142
|
}
|
|
13516
14143
|
log5.info("notifications.found", { projectId, count: notifs.length });
|
|
13517
|
-
const run = this.db.select().from(runs).where(
|
|
14144
|
+
const run = this.db.select().from(runs).where(eq22(runs.id, runId)).get();
|
|
13518
14145
|
if (!run) {
|
|
13519
14146
|
log5.error("run.not-found", { runId, msg: "skipping notification dispatch" });
|
|
13520
14147
|
return;
|
|
13521
14148
|
}
|
|
13522
|
-
const project = this.db.select().from(projects).where(
|
|
14149
|
+
const project = this.db.select().from(projects).where(eq22(projects.id, projectId)).get();
|
|
13523
14150
|
if (!project) {
|
|
13524
14151
|
log5.error("project.not-found", { projectId, msg: "skipping notification dispatch" });
|
|
13525
14152
|
return;
|
|
@@ -13559,11 +14186,11 @@ var Notifier = class {
|
|
|
13559
14186
|
}
|
|
13560
14187
|
computeTransitions(runId, projectId) {
|
|
13561
14188
|
const recentRuns = this.db.select().from(runs).where(
|
|
13562
|
-
|
|
13563
|
-
|
|
13564
|
-
or2(
|
|
14189
|
+
and10(
|
|
14190
|
+
eq22(runs.projectId, projectId),
|
|
14191
|
+
or2(eq22(runs.status, "completed"), eq22(runs.status, "partial"))
|
|
13565
14192
|
)
|
|
13566
|
-
).orderBy(
|
|
14193
|
+
).orderBy(desc8(runs.createdAt)).limit(2).all();
|
|
13567
14194
|
if (recentRuns.length < 2) return [];
|
|
13568
14195
|
const currentRunId = recentRuns[0].id;
|
|
13569
14196
|
const previousRunId = recentRuns[1].id;
|
|
@@ -13573,12 +14200,12 @@ var Notifier = class {
|
|
|
13573
14200
|
keyword: keywords.keyword,
|
|
13574
14201
|
provider: querySnapshots.provider,
|
|
13575
14202
|
citationState: querySnapshots.citationState
|
|
13576
|
-
}).from(querySnapshots).leftJoin(keywords,
|
|
14203
|
+
}).from(querySnapshots).leftJoin(keywords, eq22(querySnapshots.keywordId, keywords.id)).where(eq22(querySnapshots.runId, currentRunId)).all();
|
|
13577
14204
|
const previousSnapshots = this.db.select({
|
|
13578
14205
|
keywordId: querySnapshots.keywordId,
|
|
13579
14206
|
provider: querySnapshots.provider,
|
|
13580
14207
|
citationState: querySnapshots.citationState
|
|
13581
|
-
}).from(querySnapshots).where(
|
|
14208
|
+
}).from(querySnapshots).where(eq22(querySnapshots.runId, previousRunId)).all();
|
|
13582
14209
|
const prevMap = /* @__PURE__ */ new Map();
|
|
13583
14210
|
for (const s of previousSnapshots) {
|
|
13584
14211
|
prevMap.set(`${s.keywordId}:${s.provider}`, s.citationState);
|
|
@@ -13648,6 +14275,382 @@ var Notifier = class {
|
|
|
13648
14275
|
}
|
|
13649
14276
|
};
|
|
13650
14277
|
|
|
14278
|
+
// src/intelligence-service.ts
|
|
14279
|
+
import { eq as eq23, desc as desc9, asc as asc2, and as and11, or as or3 } from "drizzle-orm";
|
|
14280
|
+
|
|
14281
|
+
// ../intelligence/src/regressions.ts
|
|
14282
|
+
function detectRegressions(currentRun, previousRun) {
|
|
14283
|
+
const regressions = [];
|
|
14284
|
+
const previousCited = /* @__PURE__ */ new Map();
|
|
14285
|
+
for (const snap of previousRun.snapshots) {
|
|
14286
|
+
if (snap.cited) {
|
|
14287
|
+
previousCited.set(`${snap.keyword}:${snap.provider}`, {
|
|
14288
|
+
citationUrl: snap.citationUrl,
|
|
14289
|
+
position: snap.position
|
|
14290
|
+
});
|
|
14291
|
+
}
|
|
14292
|
+
}
|
|
14293
|
+
for (const snap of currentRun.snapshots) {
|
|
14294
|
+
const key = `${snap.keyword}:${snap.provider}`;
|
|
14295
|
+
if (!snap.cited && previousCited.has(key)) {
|
|
14296
|
+
const prev = previousCited.get(key);
|
|
14297
|
+
regressions.push({
|
|
14298
|
+
keyword: snap.keyword,
|
|
14299
|
+
provider: snap.provider,
|
|
14300
|
+
previousCitationUrl: prev.citationUrl,
|
|
14301
|
+
previousPosition: prev.position,
|
|
14302
|
+
currentRunId: currentRun.runId,
|
|
14303
|
+
previousRunId: previousRun.runId
|
|
14304
|
+
});
|
|
14305
|
+
}
|
|
14306
|
+
}
|
|
14307
|
+
return regressions;
|
|
14308
|
+
}
|
|
14309
|
+
|
|
14310
|
+
// ../intelligence/src/gains.ts
|
|
14311
|
+
function detectGains(currentRun, previousRun) {
|
|
14312
|
+
const gains = [];
|
|
14313
|
+
const previousCited = /* @__PURE__ */ new Set();
|
|
14314
|
+
for (const snap of previousRun.snapshots) {
|
|
14315
|
+
if (snap.cited) {
|
|
14316
|
+
previousCited.add(`${snap.keyword}:${snap.provider}`);
|
|
14317
|
+
}
|
|
14318
|
+
}
|
|
14319
|
+
for (const snap of currentRun.snapshots) {
|
|
14320
|
+
const key = `${snap.keyword}:${snap.provider}`;
|
|
14321
|
+
if (snap.cited && !previousCited.has(key)) {
|
|
14322
|
+
gains.push({
|
|
14323
|
+
keyword: snap.keyword,
|
|
14324
|
+
provider: snap.provider,
|
|
14325
|
+
citationUrl: snap.citationUrl,
|
|
14326
|
+
position: snap.position,
|
|
14327
|
+
snippet: snap.snippet,
|
|
14328
|
+
runId: currentRun.runId
|
|
14329
|
+
});
|
|
14330
|
+
}
|
|
14331
|
+
}
|
|
14332
|
+
return gains;
|
|
14333
|
+
}
|
|
14334
|
+
|
|
14335
|
+
// ../intelligence/src/health.ts
|
|
14336
|
+
function computeHealth(run) {
|
|
14337
|
+
const providerStats = /* @__PURE__ */ new Map();
|
|
14338
|
+
let totalPairs = 0;
|
|
14339
|
+
let citedPairs = 0;
|
|
14340
|
+
for (const snap of run.snapshots) {
|
|
14341
|
+
totalPairs++;
|
|
14342
|
+
if (snap.cited) citedPairs++;
|
|
14343
|
+
const stats = providerStats.get(snap.provider) ?? { cited: 0, total: 0 };
|
|
14344
|
+
stats.total++;
|
|
14345
|
+
if (snap.cited) stats.cited++;
|
|
14346
|
+
providerStats.set(snap.provider, stats);
|
|
14347
|
+
}
|
|
14348
|
+
const providerBreakdown = {};
|
|
14349
|
+
for (const [provider, stats] of providerStats) {
|
|
14350
|
+
providerBreakdown[provider] = {
|
|
14351
|
+
citedRate: stats.total > 0 ? stats.cited / stats.total : 0,
|
|
14352
|
+
cited: stats.cited,
|
|
14353
|
+
total: stats.total
|
|
14354
|
+
};
|
|
14355
|
+
}
|
|
14356
|
+
return {
|
|
14357
|
+
overallCitedRate: totalPairs > 0 ? citedPairs / totalPairs : 0,
|
|
14358
|
+
totalPairs,
|
|
14359
|
+
citedPairs,
|
|
14360
|
+
providerBreakdown
|
|
14361
|
+
};
|
|
14362
|
+
}
|
|
14363
|
+
function computeHealthTrend(runs2) {
|
|
14364
|
+
if (runs2.length === 0) {
|
|
14365
|
+
return { current: 0, previous: 0, delta: 0 };
|
|
14366
|
+
}
|
|
14367
|
+
const current = computeHealth(runs2[runs2.length - 1]).overallCitedRate;
|
|
14368
|
+
if (runs2.length === 1) {
|
|
14369
|
+
return { current, previous: 0, delta: current };
|
|
14370
|
+
}
|
|
14371
|
+
const previous = computeHealth(runs2[runs2.length - 2]).overallCitedRate;
|
|
14372
|
+
return {
|
|
14373
|
+
current,
|
|
14374
|
+
previous,
|
|
14375
|
+
delta: current - previous
|
|
14376
|
+
};
|
|
14377
|
+
}
|
|
14378
|
+
|
|
14379
|
+
// ../intelligence/src/causes.ts
|
|
14380
|
+
function analyzeCause(regression, currentSnapshots) {
|
|
14381
|
+
const currentSnap = currentSnapshots.find(
|
|
14382
|
+
(s) => s.keyword === regression.keyword && s.provider === regression.provider && !s.cited && s.competitorDomain
|
|
14383
|
+
);
|
|
14384
|
+
if (currentSnap) {
|
|
14385
|
+
return {
|
|
14386
|
+
cause: "competitor_gain",
|
|
14387
|
+
competitorDomain: currentSnap.competitorDomain,
|
|
14388
|
+
details: `Competitor ${currentSnap.competitorDomain} now cited for "${regression.keyword}" on ${regression.provider}`
|
|
14389
|
+
};
|
|
14390
|
+
}
|
|
14391
|
+
return {
|
|
14392
|
+
cause: "unknown",
|
|
14393
|
+
details: `No specific cause identified for loss of "${regression.keyword}" on ${regression.provider}`
|
|
14394
|
+
};
|
|
14395
|
+
}
|
|
14396
|
+
|
|
14397
|
+
// ../intelligence/src/insights.ts
|
|
14398
|
+
import { randomUUID } from "crypto";
|
|
14399
|
+
function generateInsights(regressions, gains, health, causes) {
|
|
14400
|
+
const insights2 = [];
|
|
14401
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
14402
|
+
for (const reg of regressions) {
|
|
14403
|
+
const key = `${reg.keyword}:${reg.provider}`;
|
|
14404
|
+
const cause = causes.get(key);
|
|
14405
|
+
insights2.push({
|
|
14406
|
+
id: `ins_${randomUUID().slice(0, 8)}`,
|
|
14407
|
+
type: "regression",
|
|
14408
|
+
severity: "high",
|
|
14409
|
+
title: `Lost ${reg.provider} citation for "${reg.keyword}"`,
|
|
14410
|
+
keyword: reg.keyword,
|
|
14411
|
+
provider: reg.provider,
|
|
14412
|
+
recommendation: {
|
|
14413
|
+
action: "audit",
|
|
14414
|
+
target: reg.previousCitationUrl,
|
|
14415
|
+
reason: `Page was previously cited at position ${reg.previousPosition ?? "unknown"}. Run aeo-audit to check for content or schema issues.`
|
|
14416
|
+
},
|
|
14417
|
+
cause,
|
|
14418
|
+
createdAt: now
|
|
14419
|
+
});
|
|
14420
|
+
}
|
|
14421
|
+
for (const gain of gains) {
|
|
14422
|
+
insights2.push({
|
|
14423
|
+
id: `ins_${randomUUID().slice(0, 8)}`,
|
|
14424
|
+
type: "gain",
|
|
14425
|
+
severity: "low",
|
|
14426
|
+
title: `New ${gain.provider} citation for "${gain.keyword}"`,
|
|
14427
|
+
keyword: gain.keyword,
|
|
14428
|
+
provider: gain.provider,
|
|
14429
|
+
recommendation: {
|
|
14430
|
+
action: "monitor",
|
|
14431
|
+
target: gain.citationUrl,
|
|
14432
|
+
reason: `New citation appeared at position ${gain.position ?? "unknown"}. Monitor to confirm it persists.`
|
|
14433
|
+
},
|
|
14434
|
+
createdAt: now
|
|
14435
|
+
});
|
|
14436
|
+
}
|
|
14437
|
+
return insights2;
|
|
14438
|
+
}
|
|
14439
|
+
|
|
14440
|
+
// ../intelligence/src/analyzer.ts
|
|
14441
|
+
function analyzeRuns(currentRun, previousRun, allRuns) {
|
|
14442
|
+
const regressions = detectRegressions(currentRun, previousRun);
|
|
14443
|
+
const gains = detectGains(currentRun, previousRun);
|
|
14444
|
+
const health = computeHealth(currentRun);
|
|
14445
|
+
const trend = allRuns ? computeHealthTrend(allRuns) : void 0;
|
|
14446
|
+
const causes = /* @__PURE__ */ new Map();
|
|
14447
|
+
for (const reg of regressions) {
|
|
14448
|
+
const cause = analyzeCause(reg, currentRun.snapshots);
|
|
14449
|
+
causes.set(`${reg.keyword}:${reg.provider}`, cause);
|
|
14450
|
+
}
|
|
14451
|
+
const insights2 = generateInsights(regressions, gains, health, causes);
|
|
14452
|
+
return {
|
|
14453
|
+
regressions,
|
|
14454
|
+
gains,
|
|
14455
|
+
health,
|
|
14456
|
+
trend,
|
|
14457
|
+
insights: insights2
|
|
14458
|
+
};
|
|
14459
|
+
}
|
|
14460
|
+
|
|
14461
|
+
// src/intelligence-service.ts
|
|
14462
|
+
import crypto22 from "crypto";
|
|
14463
|
+
var log6 = createLogger("IntelligenceService");
|
|
14464
|
+
var IntelligenceService = class {
|
|
14465
|
+
constructor(db) {
|
|
14466
|
+
this.db = db;
|
|
14467
|
+
}
|
|
14468
|
+
/**
|
|
14469
|
+
* Analyze a completed run and persist insights + health snapshot.
|
|
14470
|
+
* Idempotent: deletes prior results for the same runId before inserting.
|
|
14471
|
+
* Returns the analysis result for the coordinator to inspect (e.g. for webhook dispatch).
|
|
14472
|
+
*/
|
|
14473
|
+
analyzeAndPersist(runId, projectId) {
|
|
14474
|
+
const recentRuns = this.db.select().from(runs).where(
|
|
14475
|
+
and11(
|
|
14476
|
+
eq23(runs.projectId, projectId),
|
|
14477
|
+
or3(eq23(runs.status, "completed"), eq23(runs.status, "partial"))
|
|
14478
|
+
)
|
|
14479
|
+
).orderBy(desc9(runs.createdAt)).limit(2).all();
|
|
14480
|
+
if (recentRuns.length === 0) {
|
|
14481
|
+
log6.info("intelligence.skip", { runId, reason: "no completed runs" });
|
|
14482
|
+
return null;
|
|
14483
|
+
}
|
|
14484
|
+
const currentRunRecord = recentRuns.find((r) => r.id === runId);
|
|
14485
|
+
if (!currentRunRecord) {
|
|
14486
|
+
log6.info("intelligence.skip", { runId, reason: "run not in recent completed list" });
|
|
14487
|
+
return null;
|
|
14488
|
+
}
|
|
14489
|
+
const currentRun = this.buildRunData(runId, projectId, currentRunRecord.finishedAt ?? currentRunRecord.createdAt);
|
|
14490
|
+
if (currentRun.snapshots.length === 0) {
|
|
14491
|
+
log6.info("intelligence.skip", { runId, reason: "no snapshots" });
|
|
14492
|
+
return null;
|
|
14493
|
+
}
|
|
14494
|
+
const previousRunRecord = recentRuns.find((r) => r.id !== runId);
|
|
14495
|
+
const previousRun = previousRunRecord ? this.buildRunData(previousRunRecord.id, projectId, previousRunRecord.finishedAt ?? previousRunRecord.createdAt) : null;
|
|
14496
|
+
const result = previousRun ? analyzeRuns(currentRun, previousRun) : analyzeRuns(currentRun, { ...currentRun, snapshots: [] });
|
|
14497
|
+
log6.info("intelligence.analyzed", {
|
|
14498
|
+
runId,
|
|
14499
|
+
regressions: result.regressions.length,
|
|
14500
|
+
gains: result.gains.length,
|
|
14501
|
+
citedRate: result.health.overallCitedRate,
|
|
14502
|
+
insights: result.insights.length
|
|
14503
|
+
});
|
|
14504
|
+
this.persistResult(result, runId, projectId);
|
|
14505
|
+
return result;
|
|
14506
|
+
}
|
|
14507
|
+
/**
|
|
14508
|
+
* Analyze a single run given an explicit previous run (or null for first run).
|
|
14509
|
+
* Used by backfill where we control the run ordering.
|
|
14510
|
+
*/
|
|
14511
|
+
analyzeRunWithPrevious(runRecord, previousRunRecord) {
|
|
14512
|
+
const currentRun = this.buildRunData(runRecord.id, runRecord.projectId, runRecord.finishedAt ?? runRecord.createdAt);
|
|
14513
|
+
if (currentRun.snapshots.length === 0) {
|
|
14514
|
+
return null;
|
|
14515
|
+
}
|
|
14516
|
+
const previousRun = previousRunRecord ? this.buildRunData(previousRunRecord.id, previousRunRecord.projectId, previousRunRecord.finishedAt ?? previousRunRecord.createdAt) : null;
|
|
14517
|
+
const result = previousRun ? analyzeRuns(currentRun, previousRun) : analyzeRuns(currentRun, { ...currentRun, snapshots: [] });
|
|
14518
|
+
this.persistResult(result, runRecord.id, runRecord.projectId);
|
|
14519
|
+
return result;
|
|
14520
|
+
}
|
|
14521
|
+
/**
|
|
14522
|
+
* Backfill intelligence for all completed/partial runs of a project.
|
|
14523
|
+
* Processes runs in chronological order so each run compares against its predecessor.
|
|
14524
|
+
*/
|
|
14525
|
+
backfill(projectName, opts, onProgress) {
|
|
14526
|
+
const project = this.db.select().from(projects).where(eq23(projects.name, projectName)).get();
|
|
14527
|
+
if (!project) {
|
|
14528
|
+
throw new Error(`Project "${projectName}" not found`);
|
|
14529
|
+
}
|
|
14530
|
+
const allRuns = this.db.select().from(runs).where(
|
|
14531
|
+
and11(
|
|
14532
|
+
eq23(runs.projectId, project.id),
|
|
14533
|
+
or3(eq23(runs.status, "completed"), eq23(runs.status, "partial"))
|
|
14534
|
+
)
|
|
14535
|
+
).orderBy(asc2(runs.finishedAt)).all();
|
|
14536
|
+
let startIdx = 0;
|
|
14537
|
+
let endIdx = allRuns.length;
|
|
14538
|
+
if (opts?.fromRunId) {
|
|
14539
|
+
const idx = allRuns.findIndex((r) => r.id === opts.fromRunId);
|
|
14540
|
+
if (idx === -1) throw new Error(`Run "${opts.fromRunId}" not found in project`);
|
|
14541
|
+
startIdx = idx;
|
|
14542
|
+
}
|
|
14543
|
+
if (opts?.toRunId) {
|
|
14544
|
+
const idx = allRuns.findIndex((r) => r.id === opts.toRunId);
|
|
14545
|
+
if (idx === -1) throw new Error(`Run "${opts.toRunId}" not found in project`);
|
|
14546
|
+
endIdx = idx + 1;
|
|
14547
|
+
}
|
|
14548
|
+
const targetRuns = allRuns.slice(startIdx, endIdx);
|
|
14549
|
+
let processed = 0;
|
|
14550
|
+
let skipped = 0;
|
|
14551
|
+
let totalInsights = 0;
|
|
14552
|
+
for (let i = 0; i < targetRuns.length; i++) {
|
|
14553
|
+
const run = targetRuns[i];
|
|
14554
|
+
const globalIdx = allRuns.indexOf(run);
|
|
14555
|
+
const previousRun = globalIdx > 0 ? allRuns[globalIdx - 1] : null;
|
|
14556
|
+
const result = this.analyzeRunWithPrevious(run, previousRun);
|
|
14557
|
+
if (result) {
|
|
14558
|
+
processed++;
|
|
14559
|
+
totalInsights += result.insights.length;
|
|
14560
|
+
onProgress?.({ runId: run.id, index: i + 1, total: targetRuns.length, insights: result.insights.length });
|
|
14561
|
+
} else {
|
|
14562
|
+
skipped++;
|
|
14563
|
+
onProgress?.({ runId: run.id, index: i + 1, total: targetRuns.length, insights: 0 });
|
|
14564
|
+
}
|
|
14565
|
+
}
|
|
14566
|
+
return { processed, skipped, totalInsights };
|
|
14567
|
+
}
|
|
14568
|
+
persistResult(result, runId, projectId) {
|
|
14569
|
+
const previouslyDismissed = /* @__PURE__ */ new Set();
|
|
14570
|
+
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();
|
|
14571
|
+
for (const row of existingInsights) {
|
|
14572
|
+
if (row.dismissed) {
|
|
14573
|
+
previouslyDismissed.add(`${row.keyword}:${row.provider}:${row.type}`);
|
|
14574
|
+
}
|
|
14575
|
+
}
|
|
14576
|
+
this.db.transaction((tx) => {
|
|
14577
|
+
tx.delete(insights).where(eq23(insights.runId, runId)).run();
|
|
14578
|
+
tx.delete(healthSnapshots).where(eq23(healthSnapshots.runId, runId)).run();
|
|
14579
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
14580
|
+
for (const insight of result.insights) {
|
|
14581
|
+
const wasDismissed = previouslyDismissed.has(`${insight.keyword}:${insight.provider}:${insight.type}`);
|
|
14582
|
+
tx.insert(insights).values({
|
|
14583
|
+
id: insight.id,
|
|
14584
|
+
projectId,
|
|
14585
|
+
runId,
|
|
14586
|
+
type: insight.type,
|
|
14587
|
+
severity: insight.severity,
|
|
14588
|
+
title: insight.title,
|
|
14589
|
+
keyword: insight.keyword,
|
|
14590
|
+
provider: insight.provider,
|
|
14591
|
+
recommendation: insight.recommendation ? JSON.stringify(insight.recommendation) : null,
|
|
14592
|
+
cause: insight.cause ? JSON.stringify(insight.cause) : null,
|
|
14593
|
+
dismissed: wasDismissed,
|
|
14594
|
+
createdAt: insight.createdAt
|
|
14595
|
+
}).run();
|
|
14596
|
+
}
|
|
14597
|
+
tx.insert(healthSnapshots).values({
|
|
14598
|
+
id: crypto22.randomUUID(),
|
|
14599
|
+
projectId,
|
|
14600
|
+
runId,
|
|
14601
|
+
overallCitedRate: String(result.health.overallCitedRate),
|
|
14602
|
+
totalPairs: result.health.totalPairs,
|
|
14603
|
+
citedPairs: result.health.citedPairs,
|
|
14604
|
+
providerBreakdown: JSON.stringify(result.health.providerBreakdown),
|
|
14605
|
+
createdAt: now
|
|
14606
|
+
}).run();
|
|
14607
|
+
});
|
|
14608
|
+
log6.info("intelligence.persisted", { runId, insights: result.insights.length });
|
|
14609
|
+
}
|
|
14610
|
+
buildRunData(runId, projectId, completedAt) {
|
|
14611
|
+
const rows = this.db.select({
|
|
14612
|
+
keyword: keywords.keyword,
|
|
14613
|
+
provider: querySnapshots.provider,
|
|
14614
|
+
citationState: querySnapshots.citationState,
|
|
14615
|
+
citedDomains: querySnapshots.citedDomains,
|
|
14616
|
+
competitorOverlap: querySnapshots.competitorOverlap
|
|
14617
|
+
}).from(querySnapshots).leftJoin(keywords, eq23(querySnapshots.keywordId, keywords.id)).where(eq23(querySnapshots.runId, runId)).all();
|
|
14618
|
+
const snapshots = rows.map((r) => {
|
|
14619
|
+
const domains = parseJsonColumn(r.citedDomains, []);
|
|
14620
|
+
const competitors2 = parseJsonColumn(r.competitorOverlap, []);
|
|
14621
|
+
return {
|
|
14622
|
+
keyword: r.keyword ?? "",
|
|
14623
|
+
provider: r.provider,
|
|
14624
|
+
cited: r.citationState === "cited",
|
|
14625
|
+
citationUrl: domains[0] ?? void 0,
|
|
14626
|
+
competitorDomain: competitors2[0] ?? void 0
|
|
14627
|
+
};
|
|
14628
|
+
});
|
|
14629
|
+
return { runId, projectId, completedAt, snapshots };
|
|
14630
|
+
}
|
|
14631
|
+
};
|
|
14632
|
+
|
|
14633
|
+
// src/run-coordinator.ts
|
|
14634
|
+
var log7 = createLogger("RunCoordinator");
|
|
14635
|
+
var RunCoordinator = class {
|
|
14636
|
+
constructor(notifier, intelligenceService) {
|
|
14637
|
+
this.notifier = notifier;
|
|
14638
|
+
this.intelligenceService = intelligenceService;
|
|
14639
|
+
}
|
|
14640
|
+
async onRunCompleted(runId, projectId) {
|
|
14641
|
+
try {
|
|
14642
|
+
this.intelligenceService.analyzeAndPersist(runId, projectId);
|
|
14643
|
+
} catch (err) {
|
|
14644
|
+
log7.error("intelligence.failed", { runId, error: err instanceof Error ? err.message : String(err) });
|
|
14645
|
+
}
|
|
14646
|
+
try {
|
|
14647
|
+
await this.notifier.onRunCompleted(runId, projectId);
|
|
14648
|
+
} catch (err) {
|
|
14649
|
+
log7.error("notifier.failed", { runId, error: err instanceof Error ? err.message : String(err) });
|
|
14650
|
+
}
|
|
14651
|
+
}
|
|
14652
|
+
};
|
|
14653
|
+
|
|
13651
14654
|
// src/snapshot-service.ts
|
|
13652
14655
|
import { runAeoAudit } from "@ainyc/aeo-audit";
|
|
13653
14656
|
|
|
@@ -13768,7 +14771,7 @@ function formatAuditFactorScore(factor) {
|
|
|
13768
14771
|
}
|
|
13769
14772
|
|
|
13770
14773
|
// src/snapshot-service.ts
|
|
13771
|
-
var
|
|
14774
|
+
var log8 = createLogger("Snapshot");
|
|
13772
14775
|
var ANALYSIS_PROVIDER_PRIORITY = ["openai", "claude", "gemini", "perplexity", "local"];
|
|
13773
14776
|
var SNAPSHOT_QUERY_COUNT = 6;
|
|
13774
14777
|
var ProviderExecutionGate2 = class {
|
|
@@ -13911,7 +14914,7 @@ var SnapshotService = class {
|
|
|
13911
14914
|
return mapAuditReport(report);
|
|
13912
14915
|
} catch (err) {
|
|
13913
14916
|
const message = err instanceof Error ? err.message : String(err);
|
|
13914
|
-
|
|
14917
|
+
log8.warn("audit.failed", { homepageUrl, error: message });
|
|
13915
14918
|
return {
|
|
13916
14919
|
url: homepageUrl,
|
|
13917
14920
|
finalUrl: homepageUrl,
|
|
@@ -13941,7 +14944,7 @@ var SnapshotService = class {
|
|
|
13941
14944
|
phrases: parsedPhrases
|
|
13942
14945
|
};
|
|
13943
14946
|
} catch (err) {
|
|
13944
|
-
|
|
14947
|
+
log8.warn("profile.generation-failed", {
|
|
13945
14948
|
domain: ctx.domain,
|
|
13946
14949
|
provider: ctx.analysisProvider.adapter.name,
|
|
13947
14950
|
error: err instanceof Error ? err.message : String(err)
|
|
@@ -14083,7 +15086,7 @@ var SnapshotService = class {
|
|
|
14083
15086
|
recommendedActions: uniqueStrings(parsed.recommendedActions ?? []).slice(0, 4)
|
|
14084
15087
|
};
|
|
14085
15088
|
} catch (err) {
|
|
14086
|
-
|
|
15089
|
+
log8.warn("response.analysis-failed", {
|
|
14087
15090
|
provider: ctx.analysisProvider.adapter.name,
|
|
14088
15091
|
error: err instanceof Error ? err.message : String(err)
|
|
14089
15092
|
});
|
|
@@ -14368,7 +15371,7 @@ function clipText(value, length) {
|
|
|
14368
15371
|
// src/server.ts
|
|
14369
15372
|
var _require2 = createRequire2(import.meta.url);
|
|
14370
15373
|
var { version: PKG_VERSION } = _require2("../package.json");
|
|
14371
|
-
var
|
|
15374
|
+
var log9 = createLogger("Server");
|
|
14372
15375
|
var DEFAULT_QUOTA = {
|
|
14373
15376
|
maxConcurrency: 2,
|
|
14374
15377
|
maxRequestsPerMinute: 10,
|
|
@@ -14399,7 +15402,7 @@ function summarizeProviderConfig(provider, config) {
|
|
|
14399
15402
|
};
|
|
14400
15403
|
}
|
|
14401
15404
|
function hashApiKey(key) {
|
|
14402
|
-
return
|
|
15405
|
+
return crypto23.createHash("sha256").update(key).digest("hex");
|
|
14403
15406
|
}
|
|
14404
15407
|
function parseCookies2(header) {
|
|
14405
15408
|
if (!header) return {};
|
|
@@ -14458,7 +15461,7 @@ async function createServer(opts) {
|
|
|
14458
15461
|
quota: opts.config.geminiQuota
|
|
14459
15462
|
};
|
|
14460
15463
|
}
|
|
14461
|
-
|
|
15464
|
+
log9.info("providers.configured", { providers: Object.keys(providers).filter((k) => {
|
|
14462
15465
|
const p = providers[k];
|
|
14463
15466
|
return p?.apiKey || p?.baseUrl || p?.vertexProject;
|
|
14464
15467
|
}) });
|
|
@@ -14494,7 +15497,9 @@ async function createServer(opts) {
|
|
|
14494
15497
|
const jobRunner = new JobRunner(opts.db, registry);
|
|
14495
15498
|
jobRunner.recoverStaleRuns();
|
|
14496
15499
|
const notifier = new Notifier(opts.db, serverUrl);
|
|
14497
|
-
|
|
15500
|
+
const intelligenceService = new IntelligenceService(opts.db);
|
|
15501
|
+
const runCoordinator = new RunCoordinator(notifier, intelligenceService);
|
|
15502
|
+
jobRunner.onRunCompleted = (runId, projectId) => runCoordinator.onRunCompleted(runId, projectId);
|
|
14498
15503
|
const snapshotService = new SnapshotService(registry);
|
|
14499
15504
|
const scheduler = new Scheduler(opts.db, {
|
|
14500
15505
|
onRunCreated: (runId, projectId, providers2) => {
|
|
@@ -14571,7 +15576,7 @@ async function createServer(opts) {
|
|
|
14571
15576
|
return removed;
|
|
14572
15577
|
}
|
|
14573
15578
|
};
|
|
14574
|
-
const googleStateSecret = process.env.GOOGLE_STATE_SECRET ??
|
|
15579
|
+
const googleStateSecret = process.env.GOOGLE_STATE_SECRET ?? crypto23.randomBytes(32).toString("hex");
|
|
14575
15580
|
const googleConnectionStore = {
|
|
14576
15581
|
listConnections: (domain) => listGoogleConnections(opts.config, domain),
|
|
14577
15582
|
getConnection: (domain, connectionType) => getGoogleConnection(opts.config, domain, connectionType),
|
|
@@ -14617,11 +15622,11 @@ async function createServer(opts) {
|
|
|
14617
15622
|
const apiPrefix = basePath ? `${basePath}api/v1` : "/api/v1";
|
|
14618
15623
|
if (opts.config.apiKey) {
|
|
14619
15624
|
const keyHash = hashApiKey(opts.config.apiKey);
|
|
14620
|
-
const existing = opts.db.select().from(apiKeys).where(
|
|
15625
|
+
const existing = opts.db.select().from(apiKeys).where(eq24(apiKeys.keyHash, keyHash)).get();
|
|
14621
15626
|
if (!existing) {
|
|
14622
15627
|
const prefix = opts.config.apiKey.slice(0, 12);
|
|
14623
15628
|
opts.db.insert(apiKeys).values({
|
|
14624
|
-
id: `key_${
|
|
15629
|
+
id: `key_${crypto23.randomBytes(8).toString("hex")}`,
|
|
14625
15630
|
name: "default",
|
|
14626
15631
|
keyHash,
|
|
14627
15632
|
keyPrefix: prefix,
|
|
@@ -14645,7 +15650,7 @@ async function createServer(opts) {
|
|
|
14645
15650
|
};
|
|
14646
15651
|
const createSession = (apiKeyId) => {
|
|
14647
15652
|
pruneExpiredSessions();
|
|
14648
|
-
const sessionId =
|
|
15653
|
+
const sessionId = crypto23.randomBytes(32).toString("hex");
|
|
14649
15654
|
sessions.set(sessionId, {
|
|
14650
15655
|
apiKeyId,
|
|
14651
15656
|
expiresAt: Date.now() + SESSION_TTL_MS
|
|
@@ -14669,7 +15674,7 @@ async function createServer(opts) {
|
|
|
14669
15674
|
};
|
|
14670
15675
|
const getDefaultApiKey = () => {
|
|
14671
15676
|
if (!opts.config.apiKey) return void 0;
|
|
14672
|
-
return opts.db.select().from(apiKeys).where(
|
|
15677
|
+
return opts.db.select().from(apiKeys).where(eq24(apiKeys.keyHash, hashApiKey(opts.config.apiKey))).get();
|
|
14673
15678
|
};
|
|
14674
15679
|
const createPasswordSession = (reply) => {
|
|
14675
15680
|
const key = getDefaultApiKey();
|
|
@@ -14726,12 +15731,12 @@ async function createServer(opts) {
|
|
|
14726
15731
|
return reply.send({ authenticated: true });
|
|
14727
15732
|
}
|
|
14728
15733
|
if (apiKey) {
|
|
14729
|
-
const key = opts.db.select().from(apiKeys).where(
|
|
15734
|
+
const key = opts.db.select().from(apiKeys).where(eq24(apiKeys.keyHash, hashApiKey(apiKey))).get();
|
|
14730
15735
|
if (!key || key.revokedAt) {
|
|
14731
15736
|
const err2 = authInvalid();
|
|
14732
15737
|
return reply.status(err2.statusCode).send(err2.toJSON());
|
|
14733
15738
|
}
|
|
14734
|
-
opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
15739
|
+
opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq24(apiKeys.id, key.id)).run();
|
|
14735
15740
|
const sessionId = createSession(key.id);
|
|
14736
15741
|
reply.header("set-cookie", serializeSessionCookie({
|
|
14737
15742
|
name: SESSION_COOKIE_NAME,
|
|
@@ -14876,7 +15881,7 @@ async function createServer(opts) {
|
|
|
14876
15881
|
const targetProjectIds = affectedProjectIds.length > 0 ? affectedProjectIds : [null];
|
|
14877
15882
|
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
14878
15883
|
opts.db.insert(auditLog).values(targetProjectIds.map((projectId) => ({
|
|
14879
|
-
id:
|
|
15884
|
+
id: crypto23.randomUUID(),
|
|
14880
15885
|
projectId,
|
|
14881
15886
|
actor: "api",
|
|
14882
15887
|
action: existing ? "provider.updated" : "provider.created",
|
|
@@ -15142,6 +16147,7 @@ export {
|
|
|
15142
16147
|
notificationEventSchema,
|
|
15143
16148
|
effectiveDomains,
|
|
15144
16149
|
determineAnswerMentioned,
|
|
16150
|
+
IntelligenceService,
|
|
15145
16151
|
setGoogleAuthConfig,
|
|
15146
16152
|
formatAuditFactorScore,
|
|
15147
16153
|
createServer
|