@ainyc/canonry 1.7.1 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -0
- package/assets/assets/{index-BxWGYuSH.css → index-Co-wrY40.css} +1 -1
- package/assets/assets/index-xvy_ShfV.js +243 -0
- package/assets/index.html +2 -2
- package/dist/{chunk-WTI5ALRV.js → chunk-3FHF3YVA.js} +739 -33
- package/dist/cli.js +74 -5
- package/dist/index.js +1 -1
- package/package.json +6 -6
- package/assets/assets/index-D_jajzpq.js +0 -205
|
@@ -175,6 +175,7 @@ var projects = sqliteTable("projects", {
|
|
|
175
175
|
name: text("name").notNull().unique(),
|
|
176
176
|
displayName: text("display_name").notNull(),
|
|
177
177
|
canonicalDomain: text("canonical_domain").notNull(),
|
|
178
|
+
ownedDomains: text("owned_domains").notNull().default("[]"),
|
|
178
179
|
country: text("country").notNull(),
|
|
179
180
|
language: text("language").notNull(),
|
|
180
181
|
tags: text("tags").notNull().default("[]"),
|
|
@@ -313,6 +314,7 @@ CREATE TABLE IF NOT EXISTS projects (
|
|
|
313
314
|
name TEXT NOT NULL UNIQUE,
|
|
314
315
|
display_name TEXT NOT NULL,
|
|
315
316
|
canonical_domain TEXT NOT NULL,
|
|
317
|
+
owned_domains TEXT NOT NULL DEFAULT '[]',
|
|
316
318
|
country TEXT NOT NULL,
|
|
317
319
|
language TEXT NOT NULL,
|
|
318
320
|
tags TEXT NOT NULL DEFAULT '[]',
|
|
@@ -439,7 +441,9 @@ var MIGRATIONS = [
|
|
|
439
441
|
// v2: Add providers column to projects for multi-provider support
|
|
440
442
|
`ALTER TABLE projects ADD COLUMN providers TEXT NOT NULL DEFAULT '[]'`,
|
|
441
443
|
// v3: Add webhook_secret column to notifications for HMAC signing
|
|
442
|
-
`ALTER TABLE notifications ADD COLUMN webhook_secret TEXT
|
|
444
|
+
`ALTER TABLE notifications ADD COLUMN webhook_secret TEXT`,
|
|
445
|
+
// v4: Add owned_domains column to projects for multi-domain citation matching
|
|
446
|
+
`ALTER TABLE projects ADD COLUMN owned_domains TEXT NOT NULL DEFAULT '[]'`
|
|
443
447
|
];
|
|
444
448
|
function migrate(db) {
|
|
445
449
|
const statements = MIGRATION_SQL.split(";").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
@@ -515,6 +519,7 @@ var configNotificationSchema = z3.object({
|
|
|
515
519
|
var configSpecSchema = z3.object({
|
|
516
520
|
displayName: z3.string().min(1),
|
|
517
521
|
canonicalDomain: z3.string().min(1),
|
|
522
|
+
ownedDomains: z3.array(z3.string().min(1)).optional().default([]),
|
|
518
523
|
country: z3.string().length(2),
|
|
519
524
|
language: z3.string().min(2),
|
|
520
525
|
keywords: z3.array(z3.string().min(1)).optional().default([]),
|
|
@@ -579,6 +584,7 @@ var projectDtoSchema = z4.object({
|
|
|
579
584
|
name: z4.string(),
|
|
580
585
|
displayName: z4.string().optional(),
|
|
581
586
|
canonicalDomain: z4.string(),
|
|
587
|
+
ownedDomains: z4.array(z4.string()).default([]),
|
|
582
588
|
country: z4.string().length(2),
|
|
583
589
|
language: z4.string().min(2),
|
|
584
590
|
tags: z4.array(z4.string()).default([]),
|
|
@@ -588,6 +594,30 @@ var projectDtoSchema = z4.object({
|
|
|
588
594
|
createdAt: z4.string().optional(),
|
|
589
595
|
updatedAt: z4.string().optional()
|
|
590
596
|
});
|
|
597
|
+
function normalizeProjectDomain(input) {
|
|
598
|
+
let domain = input.trim().toLowerCase();
|
|
599
|
+
try {
|
|
600
|
+
if (domain.includes("://")) {
|
|
601
|
+
domain = new URL(domain).hostname.toLowerCase();
|
|
602
|
+
}
|
|
603
|
+
} catch {
|
|
604
|
+
}
|
|
605
|
+
return domain.replace(/^www\./, "");
|
|
606
|
+
}
|
|
607
|
+
function effectiveDomains(project) {
|
|
608
|
+
const all = [project.canonicalDomain, ...project.ownedDomains ?? []];
|
|
609
|
+
const seen = /* @__PURE__ */ new Set();
|
|
610
|
+
const result = [];
|
|
611
|
+
for (const d of all) {
|
|
612
|
+
const trimmed = d.trim();
|
|
613
|
+
if (!trimmed) continue;
|
|
614
|
+
const norm = normalizeProjectDomain(trimmed);
|
|
615
|
+
if (seen.has(norm)) continue;
|
|
616
|
+
seen.add(norm);
|
|
617
|
+
result.push(trimmed);
|
|
618
|
+
}
|
|
619
|
+
return result;
|
|
620
|
+
}
|
|
591
621
|
|
|
592
622
|
// ../contracts/src/run.ts
|
|
593
623
|
import { z as z5 } from "zod";
|
|
@@ -726,12 +756,17 @@ async function projectRoutes(app, opts) {
|
|
|
726
756
|
const err = validationError("Missing required fields: displayName, canonicalDomain, country, language");
|
|
727
757
|
return reply.status(err.statusCode).send(err.toJSON());
|
|
728
758
|
}
|
|
759
|
+
if (body.ownedDomains !== void 0 && (!Array.isArray(body.ownedDomains) || body.ownedDomains.some((d) => typeof d !== "string" || d.trim() === ""))) {
|
|
760
|
+
const err = validationError("ownedDomains must be an array of non-empty strings");
|
|
761
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
762
|
+
}
|
|
729
763
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
730
764
|
const existing = app.db.select().from(projects).where(eq3(projects.name, name)).get();
|
|
731
765
|
if (existing) {
|
|
732
766
|
app.db.update(projects).set({
|
|
733
767
|
displayName: body.displayName,
|
|
734
768
|
canonicalDomain: body.canonicalDomain,
|
|
769
|
+
ownedDomains: JSON.stringify(body.ownedDomains ?? []),
|
|
735
770
|
country: body.country,
|
|
736
771
|
language: body.language,
|
|
737
772
|
tags: JSON.stringify(body.tags ?? []),
|
|
@@ -757,6 +792,7 @@ async function projectRoutes(app, opts) {
|
|
|
757
792
|
name,
|
|
758
793
|
displayName: body.displayName,
|
|
759
794
|
canonicalDomain: body.canonicalDomain,
|
|
795
|
+
ownedDomains: JSON.stringify(body.ownedDomains ?? []),
|
|
760
796
|
country: body.country,
|
|
761
797
|
language: body.language,
|
|
762
798
|
tags: JSON.stringify(body.tags ?? []),
|
|
@@ -840,6 +876,7 @@ async function projectRoutes(app, opts) {
|
|
|
840
876
|
spec: {
|
|
841
877
|
displayName: project.displayName,
|
|
842
878
|
canonicalDomain: project.canonicalDomain,
|
|
879
|
+
ownedDomains: JSON.parse(project.ownedDomains || "[]"),
|
|
843
880
|
country: project.country,
|
|
844
881
|
language: project.language,
|
|
845
882
|
keywords: kws.map((k) => k.keyword),
|
|
@@ -871,6 +908,7 @@ function formatProject(row) {
|
|
|
871
908
|
name: row.name,
|
|
872
909
|
displayName: row.displayName,
|
|
873
910
|
canonicalDomain: row.canonicalDomain,
|
|
911
|
+
ownedDomains: JSON.parse(row.ownedDomains || "[]"),
|
|
874
912
|
country: row.country,
|
|
875
913
|
language: row.language,
|
|
876
914
|
tags: JSON.parse(row.tags),
|
|
@@ -1552,6 +1590,7 @@ async function applyRoutes(app, opts) {
|
|
|
1552
1590
|
app.db.update(projects).set({
|
|
1553
1591
|
displayName: config.spec.displayName,
|
|
1554
1592
|
canonicalDomain: config.spec.canonicalDomain,
|
|
1593
|
+
ownedDomains: JSON.stringify(config.spec.ownedDomains ?? []),
|
|
1555
1594
|
country: config.spec.country,
|
|
1556
1595
|
language: config.spec.language,
|
|
1557
1596
|
labels: JSON.stringify(config.metadata.labels),
|
|
@@ -1574,6 +1613,7 @@ async function applyRoutes(app, opts) {
|
|
|
1574
1613
|
name,
|
|
1575
1614
|
displayName: config.spec.displayName,
|
|
1576
1615
|
canonicalDomain: config.spec.canonicalDomain,
|
|
1616
|
+
ownedDomains: JSON.stringify(config.spec.ownedDomains ?? []),
|
|
1577
1617
|
country: config.spec.country,
|
|
1578
1618
|
language: config.spec.language,
|
|
1579
1619
|
tags: "[]",
|
|
@@ -1724,6 +1764,7 @@ async function applyRoutes(app, opts) {
|
|
|
1724
1764
|
name: project.name,
|
|
1725
1765
|
displayName: project.displayName,
|
|
1726
1766
|
canonicalDomain: project.canonicalDomain,
|
|
1767
|
+
ownedDomains: JSON.parse(project.ownedDomains || "[]"),
|
|
1727
1768
|
country: project.country,
|
|
1728
1769
|
language: project.language,
|
|
1729
1770
|
tags: JSON.parse(project.tags),
|
|
@@ -1929,6 +1970,669 @@ function resolveProjectSafe4(app, name, reply) {
|
|
|
1929
1970
|
}
|
|
1930
1971
|
}
|
|
1931
1972
|
|
|
1973
|
+
// ../api-routes/src/openapi.ts
|
|
1974
|
+
var stringSchema = { type: "string" };
|
|
1975
|
+
var booleanSchema = { type: "boolean" };
|
|
1976
|
+
var integerSchema = { type: "integer" };
|
|
1977
|
+
var objectSchema = { type: "object", additionalProperties: true };
|
|
1978
|
+
var stringArraySchema = { type: "array", items: stringSchema };
|
|
1979
|
+
var nameParameter = {
|
|
1980
|
+
name: "name",
|
|
1981
|
+
in: "path",
|
|
1982
|
+
required: true,
|
|
1983
|
+
description: "Project name.",
|
|
1984
|
+
schema: stringSchema
|
|
1985
|
+
};
|
|
1986
|
+
var runIdParameter = {
|
|
1987
|
+
name: "id",
|
|
1988
|
+
in: "path",
|
|
1989
|
+
required: true,
|
|
1990
|
+
description: "Run ID.",
|
|
1991
|
+
schema: stringSchema
|
|
1992
|
+
};
|
|
1993
|
+
var notificationIdParameter = {
|
|
1994
|
+
name: "id",
|
|
1995
|
+
in: "path",
|
|
1996
|
+
required: true,
|
|
1997
|
+
description: "Notification ID.",
|
|
1998
|
+
schema: stringSchema
|
|
1999
|
+
};
|
|
2000
|
+
var providerNameParameter = {
|
|
2001
|
+
name: "name",
|
|
2002
|
+
in: "path",
|
|
2003
|
+
required: true,
|
|
2004
|
+
description: "Provider name.",
|
|
2005
|
+
schema: { type: "string", enum: ["gemini", "openai", "claude", "local"] }
|
|
2006
|
+
};
|
|
2007
|
+
var routeCatalog = [
|
|
2008
|
+
{
|
|
2009
|
+
method: "get",
|
|
2010
|
+
path: "/api/v1/openapi.json",
|
|
2011
|
+
summary: "Get the OpenAPI document",
|
|
2012
|
+
description: "Machine-readable description of the Canonry API surface.",
|
|
2013
|
+
tags: ["meta"],
|
|
2014
|
+
auth: false,
|
|
2015
|
+
responses: {
|
|
2016
|
+
200: { description: "OpenAPI document." }
|
|
2017
|
+
}
|
|
2018
|
+
},
|
|
2019
|
+
{
|
|
2020
|
+
method: "put",
|
|
2021
|
+
path: "/api/v1/projects/{name}",
|
|
2022
|
+
summary: "Create or update a project",
|
|
2023
|
+
tags: ["projects"],
|
|
2024
|
+
parameters: [nameParameter],
|
|
2025
|
+
requestBody: {
|
|
2026
|
+
required: true,
|
|
2027
|
+
content: {
|
|
2028
|
+
"application/json": {
|
|
2029
|
+
schema: {
|
|
2030
|
+
type: "object",
|
|
2031
|
+
required: ["displayName", "canonicalDomain", "country", "language"],
|
|
2032
|
+
properties: {
|
|
2033
|
+
displayName: stringSchema,
|
|
2034
|
+
canonicalDomain: stringSchema,
|
|
2035
|
+
country: stringSchema,
|
|
2036
|
+
language: stringSchema,
|
|
2037
|
+
tags: stringArraySchema,
|
|
2038
|
+
labels: objectSchema,
|
|
2039
|
+
providers: stringArraySchema,
|
|
2040
|
+
configSource: stringSchema
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
},
|
|
2046
|
+
responses: {
|
|
2047
|
+
200: { description: "Project updated." },
|
|
2048
|
+
201: { description: "Project created." }
|
|
2049
|
+
}
|
|
2050
|
+
},
|
|
2051
|
+
{
|
|
2052
|
+
method: "get",
|
|
2053
|
+
path: "/api/v1/projects",
|
|
2054
|
+
summary: "List projects",
|
|
2055
|
+
tags: ["projects"],
|
|
2056
|
+
responses: {
|
|
2057
|
+
200: { description: "Projects returned." }
|
|
2058
|
+
}
|
|
2059
|
+
},
|
|
2060
|
+
{
|
|
2061
|
+
method: "get",
|
|
2062
|
+
path: "/api/v1/projects/{name}",
|
|
2063
|
+
summary: "Get a project",
|
|
2064
|
+
tags: ["projects"],
|
|
2065
|
+
parameters: [nameParameter],
|
|
2066
|
+
responses: {
|
|
2067
|
+
200: { description: "Project returned." },
|
|
2068
|
+
404: { description: "Project not found." }
|
|
2069
|
+
}
|
|
2070
|
+
},
|
|
2071
|
+
{
|
|
2072
|
+
method: "delete",
|
|
2073
|
+
path: "/api/v1/projects/{name}",
|
|
2074
|
+
summary: "Delete a project",
|
|
2075
|
+
tags: ["projects"],
|
|
2076
|
+
parameters: [nameParameter],
|
|
2077
|
+
responses: {
|
|
2078
|
+
204: { description: "Project deleted." },
|
|
2079
|
+
404: { description: "Project not found." }
|
|
2080
|
+
}
|
|
2081
|
+
},
|
|
2082
|
+
{
|
|
2083
|
+
method: "get",
|
|
2084
|
+
path: "/api/v1/projects/{name}/export",
|
|
2085
|
+
summary: "Export a project as config",
|
|
2086
|
+
tags: ["projects"],
|
|
2087
|
+
parameters: [nameParameter],
|
|
2088
|
+
responses: {
|
|
2089
|
+
200: { description: "Project configuration returned." },
|
|
2090
|
+
404: { description: "Project not found." }
|
|
2091
|
+
}
|
|
2092
|
+
},
|
|
2093
|
+
{
|
|
2094
|
+
method: "get",
|
|
2095
|
+
path: "/api/v1/projects/{name}/keywords",
|
|
2096
|
+
summary: "List keywords",
|
|
2097
|
+
tags: ["keywords"],
|
|
2098
|
+
parameters: [nameParameter],
|
|
2099
|
+
responses: {
|
|
2100
|
+
200: { description: "Keywords returned." }
|
|
2101
|
+
}
|
|
2102
|
+
},
|
|
2103
|
+
{
|
|
2104
|
+
method: "put",
|
|
2105
|
+
path: "/api/v1/projects/{name}/keywords",
|
|
2106
|
+
summary: "Replace keywords",
|
|
2107
|
+
tags: ["keywords"],
|
|
2108
|
+
parameters: [nameParameter],
|
|
2109
|
+
requestBody: {
|
|
2110
|
+
required: true,
|
|
2111
|
+
content: {
|
|
2112
|
+
"application/json": {
|
|
2113
|
+
schema: {
|
|
2114
|
+
type: "object",
|
|
2115
|
+
required: ["keywords"],
|
|
2116
|
+
properties: {
|
|
2117
|
+
keywords: stringArraySchema
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
},
|
|
2123
|
+
responses: {
|
|
2124
|
+
200: { description: "Keywords replaced." }
|
|
2125
|
+
}
|
|
2126
|
+
},
|
|
2127
|
+
{
|
|
2128
|
+
method: "post",
|
|
2129
|
+
path: "/api/v1/projects/{name}/keywords",
|
|
2130
|
+
summary: "Append keywords",
|
|
2131
|
+
tags: ["keywords"],
|
|
2132
|
+
parameters: [nameParameter],
|
|
2133
|
+
requestBody: {
|
|
2134
|
+
required: true,
|
|
2135
|
+
content: {
|
|
2136
|
+
"application/json": {
|
|
2137
|
+
schema: {
|
|
2138
|
+
type: "object",
|
|
2139
|
+
required: ["keywords"],
|
|
2140
|
+
properties: {
|
|
2141
|
+
keywords: stringArraySchema
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
},
|
|
2147
|
+
responses: {
|
|
2148
|
+
200: { description: "Keywords appended." }
|
|
2149
|
+
}
|
|
2150
|
+
},
|
|
2151
|
+
{
|
|
2152
|
+
method: "post",
|
|
2153
|
+
path: "/api/v1/projects/{name}/keywords/generate",
|
|
2154
|
+
summary: "Generate keyword suggestions",
|
|
2155
|
+
tags: ["keywords"],
|
|
2156
|
+
parameters: [nameParameter],
|
|
2157
|
+
requestBody: {
|
|
2158
|
+
required: true,
|
|
2159
|
+
content: {
|
|
2160
|
+
"application/json": {
|
|
2161
|
+
schema: {
|
|
2162
|
+
type: "object",
|
|
2163
|
+
required: ["provider"],
|
|
2164
|
+
properties: {
|
|
2165
|
+
provider: { type: "string", enum: ["gemini", "openai", "claude", "local"] },
|
|
2166
|
+
count: integerSchema
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
},
|
|
2172
|
+
responses: {
|
|
2173
|
+
200: { description: "Keyword suggestions returned." },
|
|
2174
|
+
501: { description: "Keyword generation is not available." }
|
|
2175
|
+
}
|
|
2176
|
+
},
|
|
2177
|
+
{
|
|
2178
|
+
method: "get",
|
|
2179
|
+
path: "/api/v1/projects/{name}/competitors",
|
|
2180
|
+
summary: "List competitors",
|
|
2181
|
+
tags: ["competitors"],
|
|
2182
|
+
parameters: [nameParameter],
|
|
2183
|
+
responses: {
|
|
2184
|
+
200: { description: "Competitors returned." }
|
|
2185
|
+
}
|
|
2186
|
+
},
|
|
2187
|
+
{
|
|
2188
|
+
method: "put",
|
|
2189
|
+
path: "/api/v1/projects/{name}/competitors",
|
|
2190
|
+
summary: "Replace competitors",
|
|
2191
|
+
tags: ["competitors"],
|
|
2192
|
+
parameters: [nameParameter],
|
|
2193
|
+
requestBody: {
|
|
2194
|
+
required: true,
|
|
2195
|
+
content: {
|
|
2196
|
+
"application/json": {
|
|
2197
|
+
schema: {
|
|
2198
|
+
type: "object",
|
|
2199
|
+
required: ["competitors"],
|
|
2200
|
+
properties: {
|
|
2201
|
+
competitors: stringArraySchema
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
},
|
|
2207
|
+
responses: {
|
|
2208
|
+
200: { description: "Competitors replaced." }
|
|
2209
|
+
}
|
|
2210
|
+
},
|
|
2211
|
+
{
|
|
2212
|
+
method: "post",
|
|
2213
|
+
path: "/api/v1/projects/{name}/runs",
|
|
2214
|
+
summary: "Trigger a project run",
|
|
2215
|
+
tags: ["runs"],
|
|
2216
|
+
parameters: [nameParameter],
|
|
2217
|
+
requestBody: {
|
|
2218
|
+
content: {
|
|
2219
|
+
"application/json": {
|
|
2220
|
+
schema: {
|
|
2221
|
+
type: "object",
|
|
2222
|
+
properties: {
|
|
2223
|
+
kind: stringSchema,
|
|
2224
|
+
trigger: stringSchema,
|
|
2225
|
+
providers: stringArraySchema
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
},
|
|
2231
|
+
responses: {
|
|
2232
|
+
201: { description: "Run queued." },
|
|
2233
|
+
409: { description: "Run already in progress." }
|
|
2234
|
+
}
|
|
2235
|
+
},
|
|
2236
|
+
{
|
|
2237
|
+
method: "get",
|
|
2238
|
+
path: "/api/v1/projects/{name}/runs",
|
|
2239
|
+
summary: "List project runs",
|
|
2240
|
+
tags: ["runs"],
|
|
2241
|
+
parameters: [nameParameter],
|
|
2242
|
+
responses: {
|
|
2243
|
+
200: { description: "Runs returned." }
|
|
2244
|
+
}
|
|
2245
|
+
},
|
|
2246
|
+
{
|
|
2247
|
+
method: "get",
|
|
2248
|
+
path: "/api/v1/runs",
|
|
2249
|
+
summary: "List all runs",
|
|
2250
|
+
tags: ["runs"],
|
|
2251
|
+
responses: {
|
|
2252
|
+
200: { description: "Runs returned." }
|
|
2253
|
+
}
|
|
2254
|
+
},
|
|
2255
|
+
{
|
|
2256
|
+
method: "post",
|
|
2257
|
+
path: "/api/v1/runs",
|
|
2258
|
+
summary: "Trigger runs for all projects",
|
|
2259
|
+
tags: ["runs"],
|
|
2260
|
+
requestBody: {
|
|
2261
|
+
content: {
|
|
2262
|
+
"application/json": {
|
|
2263
|
+
schema: {
|
|
2264
|
+
type: "object",
|
|
2265
|
+
properties: {
|
|
2266
|
+
kind: stringSchema,
|
|
2267
|
+
providers: stringArraySchema
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
},
|
|
2273
|
+
responses: {
|
|
2274
|
+
207: { description: "Run results returned." }
|
|
2275
|
+
}
|
|
2276
|
+
},
|
|
2277
|
+
{
|
|
2278
|
+
method: "get",
|
|
2279
|
+
path: "/api/v1/runs/{id}",
|
|
2280
|
+
summary: "Get a run and its snapshots",
|
|
2281
|
+
tags: ["runs"],
|
|
2282
|
+
parameters: [runIdParameter],
|
|
2283
|
+
responses: {
|
|
2284
|
+
200: { description: "Run returned." },
|
|
2285
|
+
404: { description: "Run not found." }
|
|
2286
|
+
}
|
|
2287
|
+
},
|
|
2288
|
+
{
|
|
2289
|
+
method: "post",
|
|
2290
|
+
path: "/api/v1/apply",
|
|
2291
|
+
summary: "Apply a Canonry config document",
|
|
2292
|
+
tags: ["config"],
|
|
2293
|
+
requestBody: {
|
|
2294
|
+
required: true,
|
|
2295
|
+
description: "Canonry project configuration as JSON.",
|
|
2296
|
+
content: {
|
|
2297
|
+
"application/json": {
|
|
2298
|
+
schema: objectSchema
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
},
|
|
2302
|
+
responses: {
|
|
2303
|
+
200: { description: "Config applied." },
|
|
2304
|
+
400: { description: "Invalid config." }
|
|
2305
|
+
}
|
|
2306
|
+
},
|
|
2307
|
+
{
|
|
2308
|
+
method: "get",
|
|
2309
|
+
path: "/api/v1/projects/{name}/history",
|
|
2310
|
+
summary: "Get project audit history",
|
|
2311
|
+
tags: ["history"],
|
|
2312
|
+
parameters: [nameParameter],
|
|
2313
|
+
responses: {
|
|
2314
|
+
200: { description: "Audit history returned." }
|
|
2315
|
+
}
|
|
2316
|
+
},
|
|
2317
|
+
{
|
|
2318
|
+
method: "get",
|
|
2319
|
+
path: "/api/v1/history",
|
|
2320
|
+
summary: "Get global audit history",
|
|
2321
|
+
tags: ["history"],
|
|
2322
|
+
responses: {
|
|
2323
|
+
200: { description: "Audit history returned." }
|
|
2324
|
+
}
|
|
2325
|
+
},
|
|
2326
|
+
{
|
|
2327
|
+
method: "get",
|
|
2328
|
+
path: "/api/v1/projects/{name}/snapshots",
|
|
2329
|
+
summary: "List query snapshots",
|
|
2330
|
+
tags: ["history"],
|
|
2331
|
+
parameters: [
|
|
2332
|
+
nameParameter,
|
|
2333
|
+
{
|
|
2334
|
+
name: "limit",
|
|
2335
|
+
in: "query",
|
|
2336
|
+
description: "Maximum number of snapshots to return.",
|
|
2337
|
+
schema: integerSchema
|
|
2338
|
+
},
|
|
2339
|
+
{
|
|
2340
|
+
name: "offset",
|
|
2341
|
+
in: "query",
|
|
2342
|
+
description: "Number of snapshots to skip.",
|
|
2343
|
+
schema: integerSchema
|
|
2344
|
+
}
|
|
2345
|
+
],
|
|
2346
|
+
responses: {
|
|
2347
|
+
200: { description: "Snapshots returned." }
|
|
2348
|
+
}
|
|
2349
|
+
},
|
|
2350
|
+
{
|
|
2351
|
+
method: "get",
|
|
2352
|
+
path: "/api/v1/projects/{name}/timeline",
|
|
2353
|
+
summary: "Get keyword timeline",
|
|
2354
|
+
tags: ["history"],
|
|
2355
|
+
parameters: [nameParameter],
|
|
2356
|
+
responses: {
|
|
2357
|
+
200: { description: "Timeline returned." }
|
|
2358
|
+
}
|
|
2359
|
+
},
|
|
2360
|
+
{
|
|
2361
|
+
method: "get",
|
|
2362
|
+
path: "/api/v1/projects/{name}/snapshots/diff",
|
|
2363
|
+
summary: "Compare two runs",
|
|
2364
|
+
tags: ["history"],
|
|
2365
|
+
parameters: [
|
|
2366
|
+
nameParameter,
|
|
2367
|
+
{
|
|
2368
|
+
name: "run1",
|
|
2369
|
+
in: "query",
|
|
2370
|
+
required: true,
|
|
2371
|
+
description: "First run ID.",
|
|
2372
|
+
schema: stringSchema
|
|
2373
|
+
},
|
|
2374
|
+
{
|
|
2375
|
+
name: "run2",
|
|
2376
|
+
in: "query",
|
|
2377
|
+
required: true,
|
|
2378
|
+
description: "Second run ID.",
|
|
2379
|
+
schema: stringSchema
|
|
2380
|
+
}
|
|
2381
|
+
],
|
|
2382
|
+
responses: {
|
|
2383
|
+
200: { description: "Diff returned." },
|
|
2384
|
+
400: { description: "Missing run IDs." }
|
|
2385
|
+
}
|
|
2386
|
+
},
|
|
2387
|
+
{
|
|
2388
|
+
method: "get",
|
|
2389
|
+
path: "/api/v1/settings",
|
|
2390
|
+
summary: "Get provider settings summary",
|
|
2391
|
+
tags: ["settings"],
|
|
2392
|
+
responses: {
|
|
2393
|
+
200: { description: "Settings returned." }
|
|
2394
|
+
}
|
|
2395
|
+
},
|
|
2396
|
+
{
|
|
2397
|
+
method: "put",
|
|
2398
|
+
path: "/api/v1/settings/providers/{name}",
|
|
2399
|
+
summary: "Update provider settings",
|
|
2400
|
+
tags: ["settings"],
|
|
2401
|
+
parameters: [providerNameParameter],
|
|
2402
|
+
requestBody: {
|
|
2403
|
+
required: true,
|
|
2404
|
+
content: {
|
|
2405
|
+
"application/json": {
|
|
2406
|
+
schema: {
|
|
2407
|
+
type: "object",
|
|
2408
|
+
properties: {
|
|
2409
|
+
apiKey: stringSchema,
|
|
2410
|
+
baseUrl: stringSchema,
|
|
2411
|
+
model: stringSchema,
|
|
2412
|
+
quota: objectSchema
|
|
2413
|
+
}
|
|
2414
|
+
}
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
},
|
|
2418
|
+
responses: {
|
|
2419
|
+
200: { description: "Provider updated." },
|
|
2420
|
+
400: { description: "Invalid provider settings." },
|
|
2421
|
+
501: { description: "Provider updates are not supported." }
|
|
2422
|
+
}
|
|
2423
|
+
},
|
|
2424
|
+
{
|
|
2425
|
+
method: "put",
|
|
2426
|
+
path: "/api/v1/projects/{name}/schedule",
|
|
2427
|
+
summary: "Create or update a schedule",
|
|
2428
|
+
tags: ["schedules"],
|
|
2429
|
+
parameters: [nameParameter],
|
|
2430
|
+
requestBody: {
|
|
2431
|
+
required: true,
|
|
2432
|
+
content: {
|
|
2433
|
+
"application/json": {
|
|
2434
|
+
schema: {
|
|
2435
|
+
type: "object",
|
|
2436
|
+
properties: {
|
|
2437
|
+
preset: stringSchema,
|
|
2438
|
+
cron: stringSchema,
|
|
2439
|
+
timezone: stringSchema,
|
|
2440
|
+
providers: stringArraySchema,
|
|
2441
|
+
enabled: booleanSchema
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
},
|
|
2447
|
+
responses: {
|
|
2448
|
+
200: { description: "Schedule updated." },
|
|
2449
|
+
201: { description: "Schedule created." }
|
|
2450
|
+
}
|
|
2451
|
+
},
|
|
2452
|
+
{
|
|
2453
|
+
method: "get",
|
|
2454
|
+
path: "/api/v1/projects/{name}/schedule",
|
|
2455
|
+
summary: "Get a schedule",
|
|
2456
|
+
tags: ["schedules"],
|
|
2457
|
+
parameters: [nameParameter],
|
|
2458
|
+
responses: {
|
|
2459
|
+
200: { description: "Schedule returned." },
|
|
2460
|
+
404: { description: "Schedule not found." }
|
|
2461
|
+
}
|
|
2462
|
+
},
|
|
2463
|
+
{
|
|
2464
|
+
method: "delete",
|
|
2465
|
+
path: "/api/v1/projects/{name}/schedule",
|
|
2466
|
+
summary: "Delete a schedule",
|
|
2467
|
+
tags: ["schedules"],
|
|
2468
|
+
parameters: [nameParameter],
|
|
2469
|
+
responses: {
|
|
2470
|
+
204: { description: "Schedule deleted." },
|
|
2471
|
+
404: { description: "Schedule not found." }
|
|
2472
|
+
}
|
|
2473
|
+
},
|
|
2474
|
+
{
|
|
2475
|
+
method: "get",
|
|
2476
|
+
path: "/api/v1/notifications/events",
|
|
2477
|
+
summary: "List notification event types",
|
|
2478
|
+
tags: ["notifications"],
|
|
2479
|
+
responses: {
|
|
2480
|
+
200: { description: "Events returned." }
|
|
2481
|
+
}
|
|
2482
|
+
},
|
|
2483
|
+
{
|
|
2484
|
+
method: "post",
|
|
2485
|
+
path: "/api/v1/projects/{name}/notifications",
|
|
2486
|
+
summary: "Create a notification",
|
|
2487
|
+
tags: ["notifications"],
|
|
2488
|
+
parameters: [nameParameter],
|
|
2489
|
+
requestBody: {
|
|
2490
|
+
required: true,
|
|
2491
|
+
content: {
|
|
2492
|
+
"application/json": {
|
|
2493
|
+
schema: {
|
|
2494
|
+
type: "object",
|
|
2495
|
+
required: ["channel", "url", "events"],
|
|
2496
|
+
properties: {
|
|
2497
|
+
channel: stringSchema,
|
|
2498
|
+
url: stringSchema,
|
|
2499
|
+
events: stringArraySchema
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
2504
|
+
},
|
|
2505
|
+
responses: {
|
|
2506
|
+
201: { description: "Notification created." }
|
|
2507
|
+
}
|
|
2508
|
+
},
|
|
2509
|
+
{
|
|
2510
|
+
method: "get",
|
|
2511
|
+
path: "/api/v1/projects/{name}/notifications",
|
|
2512
|
+
summary: "List notifications",
|
|
2513
|
+
tags: ["notifications"],
|
|
2514
|
+
parameters: [nameParameter],
|
|
2515
|
+
responses: {
|
|
2516
|
+
200: { description: "Notifications returned." }
|
|
2517
|
+
}
|
|
2518
|
+
},
|
|
2519
|
+
{
|
|
2520
|
+
method: "delete",
|
|
2521
|
+
path: "/api/v1/projects/{name}/notifications/{id}",
|
|
2522
|
+
summary: "Delete a notification",
|
|
2523
|
+
tags: ["notifications"],
|
|
2524
|
+
parameters: [nameParameter, notificationIdParameter],
|
|
2525
|
+
responses: {
|
|
2526
|
+
204: { description: "Notification deleted." },
|
|
2527
|
+
404: { description: "Notification not found." }
|
|
2528
|
+
}
|
|
2529
|
+
},
|
|
2530
|
+
{
|
|
2531
|
+
method: "post",
|
|
2532
|
+
path: "/api/v1/projects/{name}/notifications/{id}/test",
|
|
2533
|
+
summary: "Send a test notification",
|
|
2534
|
+
tags: ["notifications"],
|
|
2535
|
+
parameters: [nameParameter, notificationIdParameter],
|
|
2536
|
+
responses: {
|
|
2537
|
+
200: { description: "Test notification sent." },
|
|
2538
|
+
400: { description: "Stored notification config is invalid." },
|
|
2539
|
+
404: { description: "Notification not found." },
|
|
2540
|
+
502: { description: "Notification delivery failed." }
|
|
2541
|
+
}
|
|
2542
|
+
},
|
|
2543
|
+
{
|
|
2544
|
+
method: "get",
|
|
2545
|
+
path: "/api/v1/telemetry",
|
|
2546
|
+
summary: "Get telemetry status",
|
|
2547
|
+
tags: ["telemetry"],
|
|
2548
|
+
responses: {
|
|
2549
|
+
200: { description: "Telemetry status returned." },
|
|
2550
|
+
501: { description: "Telemetry status is not available." }
|
|
2551
|
+
}
|
|
2552
|
+
},
|
|
2553
|
+
{
|
|
2554
|
+
method: "put",
|
|
2555
|
+
path: "/api/v1/telemetry",
|
|
2556
|
+
summary: "Update telemetry status",
|
|
2557
|
+
tags: ["telemetry"],
|
|
2558
|
+
requestBody: {
|
|
2559
|
+
required: true,
|
|
2560
|
+
content: {
|
|
2561
|
+
"application/json": {
|
|
2562
|
+
schema: {
|
|
2563
|
+
type: "object",
|
|
2564
|
+
required: ["enabled"],
|
|
2565
|
+
properties: {
|
|
2566
|
+
enabled: booleanSchema
|
|
2567
|
+
}
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2571
|
+
},
|
|
2572
|
+
responses: {
|
|
2573
|
+
200: { description: "Telemetry updated." },
|
|
2574
|
+
400: { description: "Invalid telemetry request." },
|
|
2575
|
+
501: { description: "Telemetry configuration is not available." }
|
|
2576
|
+
}
|
|
2577
|
+
}
|
|
2578
|
+
];
|
|
2579
|
+
function buildOpenApiDocument(info = {}) {
|
|
2580
|
+
const paths = routeCatalog.reduce((acc, route) => {
|
|
2581
|
+
const operation = {
|
|
2582
|
+
summary: route.summary,
|
|
2583
|
+
tags: route.tags,
|
|
2584
|
+
responses: route.responses,
|
|
2585
|
+
operationId: buildOperationId(route.method, route.path)
|
|
2586
|
+
};
|
|
2587
|
+
if (route.description) operation.description = route.description;
|
|
2588
|
+
if (route.parameters) operation.parameters = route.parameters;
|
|
2589
|
+
if (route.requestBody) operation.requestBody = route.requestBody;
|
|
2590
|
+
if (route.auth === false) operation.security = [];
|
|
2591
|
+
const pathItem = acc[route.path] ?? {};
|
|
2592
|
+
pathItem[route.method] = operation;
|
|
2593
|
+
acc[route.path] = pathItem;
|
|
2594
|
+
return acc;
|
|
2595
|
+
}, {});
|
|
2596
|
+
return {
|
|
2597
|
+
openapi: "3.1.0",
|
|
2598
|
+
info: {
|
|
2599
|
+
title: info.title ?? "Canonry API",
|
|
2600
|
+
version: info.version ?? "0.0.0",
|
|
2601
|
+
description: info.description ?? "REST API for Canonry projects, runs, schedules, and notifications."
|
|
2602
|
+
},
|
|
2603
|
+
servers: [
|
|
2604
|
+
{
|
|
2605
|
+
url: "/"
|
|
2606
|
+
}
|
|
2607
|
+
],
|
|
2608
|
+
security: [{ bearerAuth: [] }],
|
|
2609
|
+
components: {
|
|
2610
|
+
securitySchemes: {
|
|
2611
|
+
bearerAuth: {
|
|
2612
|
+
type: "http",
|
|
2613
|
+
scheme: "bearer",
|
|
2614
|
+
bearerFormat: "API key"
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
},
|
|
2618
|
+
paths
|
|
2619
|
+
};
|
|
2620
|
+
}
|
|
2621
|
+
async function openApiRoutes(app, opts = {}) {
|
|
2622
|
+
app.get("/openapi.json", async (_request, reply) => {
|
|
2623
|
+
return reply.type("application/json").send(buildOpenApiDocument(opts));
|
|
2624
|
+
});
|
|
2625
|
+
}
|
|
2626
|
+
function buildOperationId(method, path3) {
|
|
2627
|
+
const parts = path3.split("/").filter(Boolean).map((part) => {
|
|
2628
|
+
if (part.startsWith("{") && part.endsWith("}")) {
|
|
2629
|
+
return `by-${part.slice(1, -1)}`;
|
|
2630
|
+
}
|
|
2631
|
+
return part;
|
|
2632
|
+
});
|
|
2633
|
+
return [method, ...parts].join("-").replace(/[^a-zA-Z0-9]+(.)/g, (_match, char) => char.toUpperCase()).replace(/^[^a-zA-Z]+/, "");
|
|
2634
|
+
}
|
|
2635
|
+
|
|
1932
2636
|
// ../api-routes/src/settings.ts
|
|
1933
2637
|
async function settingsRoutes(app, opts) {
|
|
1934
2638
|
app.get("/settings", async () => ({
|
|
@@ -2307,6 +3011,7 @@ async function apiRoutes(app, opts) {
|
|
|
2307
3011
|
await app.register(authPlugin);
|
|
2308
3012
|
}
|
|
2309
3013
|
await app.register(async (api) => {
|
|
3014
|
+
await api.register(openApiRoutes, opts.openApiInfo ?? {});
|
|
2310
3015
|
await api.register(projectRoutes, {
|
|
2311
3016
|
onProjectDeleted: opts.onProjectDeleted
|
|
2312
3017
|
});
|
|
@@ -3315,17 +4020,21 @@ var JobRunner = class {
|
|
|
3315
4020
|
minuteWindows.get(providerName),
|
|
3316
4021
|
config.quotaPolicy.maxRequestsPerMinute
|
|
3317
4022
|
);
|
|
4023
|
+
const allDomains = effectiveDomains({
|
|
4024
|
+
canonicalDomain: project.canonicalDomain,
|
|
4025
|
+
ownedDomains: JSON.parse(project.ownedDomains || "[]")
|
|
4026
|
+
});
|
|
3318
4027
|
const raw = await adapter.executeTrackedQuery(
|
|
3319
4028
|
{
|
|
3320
4029
|
keyword: kw.keyword,
|
|
3321
|
-
canonicalDomains:
|
|
4030
|
+
canonicalDomains: allDomains,
|
|
3322
4031
|
competitorDomains
|
|
3323
4032
|
},
|
|
3324
4033
|
config
|
|
3325
4034
|
);
|
|
3326
4035
|
const normalized = adapter.normalizeResult(raw);
|
|
3327
|
-
console.log(`[JobRunner] ${providerName}: "${kw.keyword}" citedDomains=${JSON.stringify(normalized.citedDomains)}, groundingSources=${JSON.stringify(normalized.groundingSources.map((s) => s.uri))},
|
|
3328
|
-
const citationState = determineCitationState(normalized,
|
|
4036
|
+
console.log(`[JobRunner] ${providerName}: "${kw.keyword}" citedDomains=${JSON.stringify(normalized.citedDomains)}, groundingSources=${JSON.stringify(normalized.groundingSources.map((s) => s.uri))}, domains=${JSON.stringify(allDomains)}`);
|
|
4037
|
+
const citationState = determineCitationState(normalized, allDomains);
|
|
3329
4038
|
const overlap = computeCompetitorOverlap(normalized, competitorDomains);
|
|
3330
4039
|
this.db.insert(querySnapshots).values({
|
|
3331
4040
|
id: crypto12.randomUUID(),
|
|
@@ -3444,39 +4153,31 @@ function getCurrentPeriod() {
|
|
|
3444
4153
|
const d = /* @__PURE__ */ new Date();
|
|
3445
4154
|
return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, "0")}`;
|
|
3446
4155
|
}
|
|
3447
|
-
function normalizeDomain(input) {
|
|
3448
|
-
let domain = input;
|
|
3449
|
-
try {
|
|
3450
|
-
if (domain.includes("://")) {
|
|
3451
|
-
domain = new URL(domain).hostname;
|
|
3452
|
-
}
|
|
3453
|
-
} catch {
|
|
3454
|
-
}
|
|
3455
|
-
return domain.replace(/^www\./, "");
|
|
3456
|
-
}
|
|
3457
4156
|
function domainMatches(domain, canonicalDomain) {
|
|
3458
|
-
const normalized =
|
|
3459
|
-
const d =
|
|
4157
|
+
const normalized = normalizeProjectDomain(canonicalDomain);
|
|
4158
|
+
const d = normalizeProjectDomain(domain);
|
|
3460
4159
|
return d === normalized || d.endsWith(`.${normalized}`);
|
|
3461
4160
|
}
|
|
3462
|
-
function determineCitationState(normalized,
|
|
3463
|
-
const
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
const lowerDomain = bareDomain.toLowerCase();
|
|
3468
|
-
for (const source of normalized.groundingSources) {
|
|
3469
|
-
try {
|
|
3470
|
-
const uri = source.uri.toLowerCase();
|
|
3471
|
-
if (uri.includes(lowerDomain)) {
|
|
3472
|
-
return "cited";
|
|
3473
|
-
}
|
|
3474
|
-
} catch {
|
|
4161
|
+
function determineCitationState(normalized, domains) {
|
|
4162
|
+
for (const canonicalDomain of domains) {
|
|
4163
|
+
const bareDomain = normalizeProjectDomain(canonicalDomain);
|
|
4164
|
+
if (normalized.citedDomains.some((d) => domainMatches(d, bareDomain))) {
|
|
4165
|
+
return "cited";
|
|
3475
4166
|
}
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
4167
|
+
const lowerDomain = bareDomain.toLowerCase();
|
|
4168
|
+
for (const source of normalized.groundingSources) {
|
|
4169
|
+
try {
|
|
4170
|
+
const uri = source.uri.toLowerCase();
|
|
4171
|
+
if (lowerDomain.includes(".") && uri.includes(lowerDomain)) {
|
|
4172
|
+
return "cited";
|
|
4173
|
+
}
|
|
4174
|
+
} catch {
|
|
4175
|
+
}
|
|
4176
|
+
if (source.title) {
|
|
4177
|
+
const titleLower = source.title.toLowerCase().replace(/^www\./, "");
|
|
4178
|
+
if (titleLower === lowerDomain || titleLower.endsWith(`.${lowerDomain}`)) {
|
|
4179
|
+
return "cited";
|
|
4180
|
+
}
|
|
3480
4181
|
}
|
|
3481
4182
|
}
|
|
3482
4183
|
}
|
|
@@ -4026,6 +4727,10 @@ async function createServer(opts) {
|
|
|
4026
4727
|
await app.register(apiRoutes, {
|
|
4027
4728
|
db: opts.db,
|
|
4028
4729
|
skipAuth: false,
|
|
4730
|
+
openApiInfo: {
|
|
4731
|
+
title: "Canonry API",
|
|
4732
|
+
version: PKG_VERSION
|
|
4733
|
+
},
|
|
4029
4734
|
providerSummary,
|
|
4030
4735
|
onRunCreated: (runId, projectId, providers2) => {
|
|
4031
4736
|
jobRunner.executeRun(runId, projectId, providers2).catch((err) => {
|
|
@@ -4205,6 +4910,7 @@ function parseKeywordResponse(raw, count) {
|
|
|
4205
4910
|
export {
|
|
4206
4911
|
providerQuotaPolicySchema,
|
|
4207
4912
|
notificationEventSchema,
|
|
4913
|
+
effectiveDomains,
|
|
4208
4914
|
apiKeys,
|
|
4209
4915
|
createClient,
|
|
4210
4916
|
migrate,
|