@ainyc/canonry 1.10.1 → 1.11.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 +26 -2
- package/assets/assets/index-CVK9fWdD.css +1 -0
- package/assets/assets/index-DnEgRRTR.js +243 -0
- package/assets/index.html +2 -2
- package/dist/{chunk-2DC7RBXJ.js → chunk-MVIL2UGM.js} +262 -97
- package/dist/cli.js +99 -13
- package/dist/index.d.ts +19 -0
- package/dist/index.js +1 -1
- package/package.json +2 -2
- package/assets/assets/index-BDoQOjXO.js +0 -243
- package/assets/assets/index-Dhvjw6Lo.css +0 -1
|
@@ -9,6 +9,16 @@ import fs from "fs";
|
|
|
9
9
|
import path from "path";
|
|
10
10
|
import os from "os";
|
|
11
11
|
import { parse, stringify } from "yaml";
|
|
12
|
+
function normalizeGoogleConfig(config) {
|
|
13
|
+
if (!config.google) return;
|
|
14
|
+
config.google.connections = (config.google.connections ?? []).map((connection) => ({
|
|
15
|
+
...connection,
|
|
16
|
+
propertyId: connection.propertyId ?? null,
|
|
17
|
+
refreshToken: connection.refreshToken ?? null,
|
|
18
|
+
tokenExpiresAt: connection.tokenExpiresAt ?? null,
|
|
19
|
+
scopes: connection.scopes ?? []
|
|
20
|
+
}));
|
|
21
|
+
}
|
|
12
22
|
function getConfigDir() {
|
|
13
23
|
const override = process.env.CANONRY_CONFIG_DIR?.trim();
|
|
14
24
|
if (override) {
|
|
@@ -25,7 +35,7 @@ function loadConfig() {
|
|
|
25
35
|
throw new Error(
|
|
26
36
|
`Config not found at ${configPath}.
|
|
27
37
|
Run "canonry init" to set up interactively, or "canonry init --gemini-key <key>" for non-interactive setup.
|
|
28
|
-
For CI/Docker, use "canonry bootstrap" with env vars (GEMINI_API_KEY, OPENAI_API_KEY, ANTHROPIC_API_KEY).`
|
|
38
|
+
For CI/Docker, use "canonry bootstrap" with env vars (GEMINI_API_KEY, OPENAI_API_KEY, ANTHROPIC_API_KEY, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET).`
|
|
29
39
|
);
|
|
30
40
|
}
|
|
31
41
|
const raw = fs.readFileSync(configPath, "utf-8");
|
|
@@ -39,7 +49,7 @@ For CI/Docker, use "canonry bootstrap" with env vars (GEMINI_API_KEY, OPENAI_API
|
|
|
39
49
|
throw new Error(
|
|
40
50
|
`Invalid config at ${configPath} \u2014 missing: ${missing}.
|
|
41
51
|
These fields are auto-generated. Run "canonry init" (or "canonry init --gemini-key <key>" for non-interactive setup) to create a valid config.
|
|
42
|
-
Do not write config.yaml by hand; use "canonry init" or "canonry bootstrap" instead.`
|
|
52
|
+
Do not write config.yaml by hand; use "canonry init", "canonry settings", or "canonry bootstrap" instead.`
|
|
43
53
|
);
|
|
44
54
|
}
|
|
45
55
|
if (parsed.geminiApiKey && !parsed.providers?.gemini) {
|
|
@@ -52,6 +62,7 @@ Do not write config.yaml by hand; use "canonry init" or "canonry bootstrap" inst
|
|
|
52
62
|
}
|
|
53
63
|
};
|
|
54
64
|
}
|
|
65
|
+
normalizeGoogleConfig(parsed);
|
|
55
66
|
return parsed;
|
|
56
67
|
}
|
|
57
68
|
function saveConfig(config) {
|
|
@@ -1548,7 +1559,7 @@ function resolveProjectSafe3(app, name, reply) {
|
|
|
1548
1559
|
|
|
1549
1560
|
// ../api-routes/src/apply.ts
|
|
1550
1561
|
import crypto9 from "crypto";
|
|
1551
|
-
import { eq as eq8
|
|
1562
|
+
import { eq as eq8 } from "drizzle-orm";
|
|
1552
1563
|
|
|
1553
1564
|
// ../api-routes/src/schedule-utils.ts
|
|
1554
1565
|
var DAY_MAP = {
|
|
@@ -1986,11 +1997,7 @@ async function applyRoutes(app, opts) {
|
|
|
1986
1997
|
});
|
|
1987
1998
|
}
|
|
1988
1999
|
if ("google" in rawSpec && config.spec.google?.gsc?.propertyUrl) {
|
|
1989
|
-
|
|
1990
|
-
const conn = app.db.select().from(googleConnections).where(and3(eq8(googleConnections.domain, domain), eq8(googleConnections.connectionType, "gsc"))).get();
|
|
1991
|
-
if (conn) {
|
|
1992
|
-
app.db.update(googleConnections).set({ propertyId: config.spec.google.gsc.propertyUrl, updatedAt: now }).where(eq8(googleConnections.id, conn.id)).run();
|
|
1993
|
-
}
|
|
2000
|
+
opts?.onGoogleConnectionPropertyUpdated?.(config.spec.canonicalDomain, "gsc", config.spec.google.gsc.propertyUrl);
|
|
1994
2001
|
}
|
|
1995
2002
|
const project = app.db.select().from(projects).where(eq8(projects.id, projectId)).get();
|
|
1996
2003
|
return reply.status(200).send({
|
|
@@ -2887,7 +2894,8 @@ function buildOperationId(method, path3) {
|
|
|
2887
2894
|
// ../api-routes/src/settings.ts
|
|
2888
2895
|
async function settingsRoutes(app, opts) {
|
|
2889
2896
|
app.get("/settings", async () => ({
|
|
2890
|
-
providers: opts.providerSummary ?? []
|
|
2897
|
+
providers: opts.providerSummary ?? [],
|
|
2898
|
+
google: opts.google ?? { configured: false }
|
|
2891
2899
|
}));
|
|
2892
2900
|
app.put("/settings/providers/:name", async (request, reply) => {
|
|
2893
2901
|
const providerName = parseProviderName(request.params.name);
|
|
@@ -2935,6 +2943,22 @@ async function settingsRoutes(app, opts) {
|
|
|
2935
2943
|
}
|
|
2936
2944
|
return result;
|
|
2937
2945
|
});
|
|
2946
|
+
app.put("/settings/google", async (request, reply) => {
|
|
2947
|
+
const { clientId, clientSecret } = request.body ?? {};
|
|
2948
|
+
if (!clientId || typeof clientId !== "string" || !clientSecret || typeof clientSecret !== "string") {
|
|
2949
|
+
return reply.status(400).send({
|
|
2950
|
+
error: { code: "VALIDATION_ERROR", message: "clientId and clientSecret are required" }
|
|
2951
|
+
});
|
|
2952
|
+
}
|
|
2953
|
+
if (!opts.onGoogleUpdate) {
|
|
2954
|
+
return reply.status(501).send({ error: "Google OAuth configuration updates are not supported in this deployment" });
|
|
2955
|
+
}
|
|
2956
|
+
const result = opts.onGoogleUpdate(clientId, clientSecret);
|
|
2957
|
+
if (!result) {
|
|
2958
|
+
return reply.status(500).send({ error: "Failed to update Google OAuth configuration" });
|
|
2959
|
+
}
|
|
2960
|
+
return result;
|
|
2961
|
+
});
|
|
2938
2962
|
}
|
|
2939
2963
|
|
|
2940
2964
|
// ../api-routes/src/telemetry.ts
|
|
@@ -3248,7 +3272,7 @@ function resolveProjectSafe6(app, name, reply) {
|
|
|
3248
3272
|
|
|
3249
3273
|
// ../api-routes/src/google.ts
|
|
3250
3274
|
import crypto12 from "crypto";
|
|
3251
|
-
import { eq as eq12, and as
|
|
3275
|
+
import { eq as eq12, and as and3, desc as desc2, sql as sql2 } from "drizzle-orm";
|
|
3252
3276
|
|
|
3253
3277
|
// ../integration-google/src/constants.ts
|
|
3254
3278
|
var GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
@@ -3423,8 +3447,8 @@ function verifySignedState(encoded, secret) {
|
|
|
3423
3447
|
return null;
|
|
3424
3448
|
}
|
|
3425
3449
|
}
|
|
3426
|
-
async function getValidToken(
|
|
3427
|
-
const conn =
|
|
3450
|
+
async function getValidToken(store, domain, connectionType, clientId, clientSecret) {
|
|
3451
|
+
const conn = store.getConnection(domain, connectionType);
|
|
3428
3452
|
if (!conn) {
|
|
3429
3453
|
throw notFound("Google connection", connectionType);
|
|
3430
3454
|
}
|
|
@@ -3436,37 +3460,51 @@ async function getValidToken(app, domain, connectionType, clientId, clientSecret
|
|
|
3436
3460
|
if (Date.now() > expiresAt - fiveMinutes) {
|
|
3437
3461
|
const tokens = await refreshAccessToken(clientId, clientSecret, conn.refreshToken);
|
|
3438
3462
|
const newExpiresAt = new Date(Date.now() + tokens.expires_in * 1e3).toISOString();
|
|
3439
|
-
|
|
3463
|
+
const updated = store.updateConnection(domain, connectionType, {
|
|
3440
3464
|
accessToken: tokens.access_token,
|
|
3441
3465
|
tokenExpiresAt: newExpiresAt,
|
|
3442
3466
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3443
|
-
})
|
|
3444
|
-
return {
|
|
3467
|
+
});
|
|
3468
|
+
return {
|
|
3469
|
+
accessToken: tokens.access_token,
|
|
3470
|
+
connectionId: `${domain}:${connectionType}`,
|
|
3471
|
+
propertyId: updated?.propertyId ?? conn.propertyId ?? null
|
|
3472
|
+
};
|
|
3445
3473
|
}
|
|
3446
|
-
return {
|
|
3474
|
+
return {
|
|
3475
|
+
accessToken: conn.accessToken,
|
|
3476
|
+
connectionId: `${domain}:${connectionType}`,
|
|
3477
|
+
propertyId: conn.propertyId ?? null
|
|
3478
|
+
};
|
|
3447
3479
|
}
|
|
3448
3480
|
async function googleRoutes(app, opts) {
|
|
3449
|
-
const { googleClientId, googleClientSecret } = opts;
|
|
3450
3481
|
const stateSecret = opts.googleStateSecret ?? "insecure-default-secret";
|
|
3482
|
+
function getAuthConfig() {
|
|
3483
|
+
return opts.getGoogleAuthConfig?.() ?? {};
|
|
3484
|
+
}
|
|
3485
|
+
function requireConnectionStore(reply) {
|
|
3486
|
+
if (opts.googleConnectionStore) return opts.googleConnectionStore;
|
|
3487
|
+
const err = validationError("Google auth storage is not configured for this deployment");
|
|
3488
|
+
reply.status(err.statusCode).send(err.toJSON());
|
|
3489
|
+
return null;
|
|
3490
|
+
}
|
|
3451
3491
|
app.get("/projects/:name/google/connections", async (request) => {
|
|
3452
3492
|
const project = resolveProject(app.db, request.params.name);
|
|
3453
|
-
const conns =
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
return conns.map((c) => ({
|
|
3463
|
-
...c,
|
|
3464
|
-
scopes: JSON.parse(c.scopes)
|
|
3493
|
+
const conns = opts.googleConnectionStore?.listConnections(project.canonicalDomain) ?? [];
|
|
3494
|
+
return conns.map((connection) => ({
|
|
3495
|
+
id: `${connection.domain}:${connection.connectionType}`,
|
|
3496
|
+
domain: connection.domain,
|
|
3497
|
+
connectionType: connection.connectionType,
|
|
3498
|
+
propertyId: connection.propertyId ?? null,
|
|
3499
|
+
scopes: connection.scopes ?? [],
|
|
3500
|
+
createdAt: connection.createdAt,
|
|
3501
|
+
updatedAt: connection.updatedAt
|
|
3465
3502
|
}));
|
|
3466
3503
|
});
|
|
3467
3504
|
app.post("/projects/:name/google/connect", async (request, reply) => {
|
|
3505
|
+
const { clientId: googleClientId, clientSecret: googleClientSecret } = getAuthConfig();
|
|
3468
3506
|
if (!googleClientId || !googleClientSecret) {
|
|
3469
|
-
const err = validationError("Google OAuth is not configured. Set
|
|
3507
|
+
const err = validationError("Google OAuth is not configured. Set Google OAuth credentials in the local Canonry config.");
|
|
3470
3508
|
return reply.status(err.statusCode).send(err.toJSON());
|
|
3471
3509
|
}
|
|
3472
3510
|
const { type, propertyId } = request.body ?? {};
|
|
@@ -3487,9 +3525,12 @@ async function googleRoutes(app, opts) {
|
|
|
3487
3525
|
return { authUrl };
|
|
3488
3526
|
});
|
|
3489
3527
|
app.get("/projects/:name/google/callback", async (request, reply) => {
|
|
3528
|
+
const { clientId: googleClientId, clientSecret: googleClientSecret } = getAuthConfig();
|
|
3490
3529
|
if (!googleClientId || !googleClientSecret) {
|
|
3491
3530
|
return reply.status(500).send("Google OAuth not configured");
|
|
3492
3531
|
}
|
|
3532
|
+
const store = requireConnectionStore(reply);
|
|
3533
|
+
if (!store) return;
|
|
3493
3534
|
const { code, state, error } = request.query;
|
|
3494
3535
|
if (error) {
|
|
3495
3536
|
const safeError = String(error).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
@@ -3506,30 +3547,18 @@ async function googleRoutes(app, opts) {
|
|
|
3506
3547
|
const tokens = await exchangeCode(googleClientId, googleClientSecret, code, redirectUri);
|
|
3507
3548
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3508
3549
|
const expiresAt = new Date(Date.now() + tokens.expires_in * 1e3).toISOString();
|
|
3509
|
-
const existing =
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
id: crypto12.randomUUID(),
|
|
3522
|
-
domain,
|
|
3523
|
-
connectionType: type,
|
|
3524
|
-
propertyId: propertyId ?? null,
|
|
3525
|
-
accessToken: tokens.access_token,
|
|
3526
|
-
refreshToken: tokens.refresh_token ?? null,
|
|
3527
|
-
tokenExpiresAt: expiresAt,
|
|
3528
|
-
scopes: JSON.stringify(tokens.scope?.split(" ") ?? []),
|
|
3529
|
-
createdAt: now,
|
|
3530
|
-
updatedAt: now
|
|
3531
|
-
}).run();
|
|
3532
|
-
}
|
|
3550
|
+
const existing = store.getConnection(domain, type);
|
|
3551
|
+
store.upsertConnection({
|
|
3552
|
+
domain,
|
|
3553
|
+
connectionType: type,
|
|
3554
|
+
propertyId: propertyId ?? existing?.propertyId ?? null,
|
|
3555
|
+
accessToken: tokens.access_token,
|
|
3556
|
+
refreshToken: tokens.refresh_token ?? existing?.refreshToken ?? null,
|
|
3557
|
+
tokenExpiresAt: expiresAt,
|
|
3558
|
+
scopes: tokens.scope?.split(" ") ?? [],
|
|
3559
|
+
createdAt: existing?.createdAt ?? now,
|
|
3560
|
+
updatedAt: now
|
|
3561
|
+
});
|
|
3533
3562
|
writeAuditLog(app.db, {
|
|
3534
3563
|
projectId: null,
|
|
3535
3564
|
actor: "oauth",
|
|
@@ -3547,9 +3576,11 @@ async function googleRoutes(app, opts) {
|
|
|
3547
3576
|
);
|
|
3548
3577
|
});
|
|
3549
3578
|
app.delete("/projects/:name/google/connections/:type", async (request, reply) => {
|
|
3579
|
+
const store = requireConnectionStore(reply);
|
|
3580
|
+
if (!store) return;
|
|
3550
3581
|
const project = resolveProject(app.db, request.params.name);
|
|
3551
|
-
const deleted =
|
|
3552
|
-
if (deleted
|
|
3582
|
+
const deleted = store.deleteConnection(project.canonicalDomain, request.params.type);
|
|
3583
|
+
if (!deleted) {
|
|
3553
3584
|
const err = notFound("Google connection", request.params.type);
|
|
3554
3585
|
return reply.status(err.statusCode).send(err.toJSON());
|
|
3555
3586
|
}
|
|
@@ -3563,18 +3594,23 @@ async function googleRoutes(app, opts) {
|
|
|
3563
3594
|
return reply.status(204).send();
|
|
3564
3595
|
});
|
|
3565
3596
|
app.get("/projects/:name/google/properties", async (request, reply) => {
|
|
3597
|
+
const { clientId: googleClientId, clientSecret: googleClientSecret } = getAuthConfig();
|
|
3566
3598
|
if (!googleClientId || !googleClientSecret) {
|
|
3567
3599
|
const err = validationError("Google OAuth is not configured");
|
|
3568
3600
|
return reply.status(err.statusCode).send(err.toJSON());
|
|
3569
3601
|
}
|
|
3602
|
+
const store = requireConnectionStore(reply);
|
|
3603
|
+
if (!store) return;
|
|
3570
3604
|
const project = resolveProject(app.db, request.params.name);
|
|
3571
|
-
const { accessToken } = await getValidToken(
|
|
3605
|
+
const { accessToken } = await getValidToken(store, project.canonicalDomain, "gsc", googleClientId, googleClientSecret);
|
|
3572
3606
|
const sites = await listSites(accessToken);
|
|
3573
3607
|
return { sites };
|
|
3574
3608
|
});
|
|
3575
3609
|
app.post("/projects/:name/google/gsc/sync", async (request, reply) => {
|
|
3610
|
+
const store = requireConnectionStore(reply);
|
|
3611
|
+
if (!store) return;
|
|
3576
3612
|
const project = resolveProject(app.db, request.params.name);
|
|
3577
|
-
const conn =
|
|
3613
|
+
const conn = store.getConnection(project.canonicalDomain, "gsc");
|
|
3578
3614
|
if (!conn) {
|
|
3579
3615
|
const err = validationError('No GSC connection found for this domain. Run "canonry google connect" first.');
|
|
3580
3616
|
return reply.status(err.statusCode).send(err.toJSON());
|
|
@@ -3604,7 +3640,7 @@ async function googleRoutes(app, opts) {
|
|
|
3604
3640
|
if (endDate) conditions.push(sql2`${gscSearchData.date} <= ${endDate}`);
|
|
3605
3641
|
if (query) conditions.push(sql2`${gscSearchData.query} LIKE ${"%" + query + "%"}`);
|
|
3606
3642
|
if (page) conditions.push(sql2`${gscSearchData.page} LIKE ${"%" + page + "%"}`);
|
|
3607
|
-
const rows = app.db.select().from(gscSearchData).where(
|
|
3643
|
+
const rows = app.db.select().from(gscSearchData).where(and3(...conditions)).orderBy(desc2(gscSearchData.date)).limit(parseInt(limit ?? "500", 10)).all();
|
|
3608
3644
|
return rows.map((r) => ({
|
|
3609
3645
|
date: r.date,
|
|
3610
3646
|
query: r.query,
|
|
@@ -3618,17 +3654,20 @@ async function googleRoutes(app, opts) {
|
|
|
3618
3654
|
}));
|
|
3619
3655
|
});
|
|
3620
3656
|
app.post("/projects/:name/google/gsc/inspect", async (request, reply) => {
|
|
3657
|
+
const { clientId: googleClientId, clientSecret: googleClientSecret } = getAuthConfig();
|
|
3621
3658
|
if (!googleClientId || !googleClientSecret) {
|
|
3622
3659
|
const err = validationError("Google OAuth is not configured");
|
|
3623
3660
|
return reply.status(err.statusCode).send(err.toJSON());
|
|
3624
3661
|
}
|
|
3662
|
+
const store = requireConnectionStore(reply);
|
|
3663
|
+
if (!store) return;
|
|
3625
3664
|
const project = resolveProject(app.db, request.params.name);
|
|
3626
3665
|
const { url } = request.body ?? {};
|
|
3627
3666
|
if (!url) {
|
|
3628
3667
|
const err = validationError("url is required");
|
|
3629
3668
|
return reply.status(err.statusCode).send(err.toJSON());
|
|
3630
3669
|
}
|
|
3631
|
-
const { accessToken, propertyId } = await getValidToken(
|
|
3670
|
+
const { accessToken, propertyId } = await getValidToken(store, project.canonicalDomain, "gsc", googleClientId, googleClientSecret);
|
|
3632
3671
|
if (!propertyId) {
|
|
3633
3672
|
const err = validationError("No GSC property configured for this connection");
|
|
3634
3673
|
return reply.status(err.statusCode).send(err.toJSON());
|
|
@@ -3679,7 +3718,7 @@ async function googleRoutes(app, opts) {
|
|
|
3679
3718
|
const { url, limit } = request.query;
|
|
3680
3719
|
const conditions = [eq12(gscUrlInspections.projectId, project.id)];
|
|
3681
3720
|
if (url) conditions.push(eq12(gscUrlInspections.url, url));
|
|
3682
|
-
const rows = app.db.select().from(gscUrlInspections).where(
|
|
3721
|
+
const rows = app.db.select().from(gscUrlInspections).where(and3(...conditions)).orderBy(desc2(gscUrlInspections.inspectedAt)).limit(parseInt(limit ?? "100", 10)).all();
|
|
3683
3722
|
return rows.map((r) => ({
|
|
3684
3723
|
id: r.id,
|
|
3685
3724
|
url: r.url,
|
|
@@ -3725,18 +3764,23 @@ async function googleRoutes(app, opts) {
|
|
|
3725
3764
|
return deindexed;
|
|
3726
3765
|
});
|
|
3727
3766
|
app.put("/projects/:name/google/connections/:type/property", async (request, reply) => {
|
|
3767
|
+
const store = requireConnectionStore(reply);
|
|
3768
|
+
if (!store) return;
|
|
3728
3769
|
const project = resolveProject(app.db, request.params.name);
|
|
3729
3770
|
const { propertyId } = request.body ?? {};
|
|
3730
3771
|
if (!propertyId) {
|
|
3731
3772
|
const err = validationError("propertyId is required");
|
|
3732
3773
|
return reply.status(err.statusCode).send(err.toJSON());
|
|
3733
3774
|
}
|
|
3734
|
-
const conn =
|
|
3775
|
+
const conn = store.updateConnection(
|
|
3776
|
+
project.canonicalDomain,
|
|
3777
|
+
request.params.type,
|
|
3778
|
+
{ propertyId, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }
|
|
3779
|
+
);
|
|
3735
3780
|
if (!conn) {
|
|
3736
3781
|
const err = notFound("Google connection", request.params.type);
|
|
3737
3782
|
return reply.status(err.statusCode).send(err.toJSON());
|
|
3738
3783
|
}
|
|
3739
|
-
app.db.update(googleConnections).set({ propertyId, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq12(googleConnections.id, conn.id)).run();
|
|
3740
3784
|
return { propertyId };
|
|
3741
3785
|
});
|
|
3742
3786
|
}
|
|
@@ -3758,12 +3802,20 @@ async function apiRoutes(app, opts) {
|
|
|
3758
3802
|
await api.register(competitorRoutes);
|
|
3759
3803
|
await api.register(runRoutes, { onRunCreated: opts.onRunCreated });
|
|
3760
3804
|
await api.register(applyRoutes, {
|
|
3761
|
-
onScheduleUpdated: opts.onScheduleUpdated
|
|
3805
|
+
onScheduleUpdated: opts.onScheduleUpdated,
|
|
3806
|
+
onGoogleConnectionPropertyUpdated: (domain, connectionType, propertyId) => {
|
|
3807
|
+
opts.googleConnectionStore?.updateConnection(domain, connectionType, {
|
|
3808
|
+
propertyId,
|
|
3809
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3810
|
+
});
|
|
3811
|
+
}
|
|
3762
3812
|
});
|
|
3763
3813
|
await api.register(historyRoutes);
|
|
3764
3814
|
await api.register(settingsRoutes, {
|
|
3765
3815
|
providerSummary: opts.providerSummary,
|
|
3766
|
-
onProviderUpdate: opts.onProviderUpdate
|
|
3816
|
+
onProviderUpdate: opts.onProviderUpdate,
|
|
3817
|
+
google: opts.googleSettingsSummary,
|
|
3818
|
+
onGoogleUpdate: opts.onGoogleSettingsUpdate
|
|
3767
3819
|
});
|
|
3768
3820
|
await api.register(scheduleRoutes, {
|
|
3769
3821
|
onScheduleUpdated: opts.onScheduleUpdated
|
|
@@ -3774,8 +3826,8 @@ async function apiRoutes(app, opts) {
|
|
|
3774
3826
|
setTelemetryEnabled: opts.setTelemetryEnabled
|
|
3775
3827
|
});
|
|
3776
3828
|
await api.register(googleRoutes, {
|
|
3777
|
-
|
|
3778
|
-
|
|
3829
|
+
getGoogleAuthConfig: opts.getGoogleAuthConfig,
|
|
3830
|
+
googleConnectionStore: opts.googleConnectionStore,
|
|
3779
3831
|
googleStateSecret: opts.googleStateSecret,
|
|
3780
3832
|
onGscSyncRequested: opts.onGscSyncRequested
|
|
3781
3833
|
});
|
|
@@ -4698,6 +4750,77 @@ var localAdapter = {
|
|
|
4698
4750
|
}
|
|
4699
4751
|
};
|
|
4700
4752
|
|
|
4753
|
+
// src/google-config.ts
|
|
4754
|
+
function ensureConnections(config) {
|
|
4755
|
+
if (!config.google) config.google = {};
|
|
4756
|
+
if (!config.google.connections) config.google.connections = [];
|
|
4757
|
+
return config.google.connections;
|
|
4758
|
+
}
|
|
4759
|
+
function getGoogleAuthConfig(config) {
|
|
4760
|
+
return {
|
|
4761
|
+
clientId: config.google?.clientId,
|
|
4762
|
+
clientSecret: config.google?.clientSecret
|
|
4763
|
+
};
|
|
4764
|
+
}
|
|
4765
|
+
function setGoogleAuthConfig(config, auth) {
|
|
4766
|
+
const hasValues = Boolean(auth.clientId || auth.clientSecret || config.google?.connections?.length);
|
|
4767
|
+
if (!hasValues) {
|
|
4768
|
+
delete config.google;
|
|
4769
|
+
return;
|
|
4770
|
+
}
|
|
4771
|
+
if (!config.google) config.google = {};
|
|
4772
|
+
config.google.clientId = auth.clientId;
|
|
4773
|
+
config.google.clientSecret = auth.clientSecret;
|
|
4774
|
+
config.google.connections = config.google.connections ?? [];
|
|
4775
|
+
}
|
|
4776
|
+
function listGoogleConnections(config, domain) {
|
|
4777
|
+
return (config.google?.connections ?? []).filter((connection) => connection.domain === domain);
|
|
4778
|
+
}
|
|
4779
|
+
function getGoogleConnection(config, domain, connectionType) {
|
|
4780
|
+
return (config.google?.connections ?? []).find((connection) => connection.domain === domain && connection.connectionType === connectionType);
|
|
4781
|
+
}
|
|
4782
|
+
function upsertGoogleConnection(config, connection) {
|
|
4783
|
+
const connections = ensureConnections(config);
|
|
4784
|
+
const index2 = connections.findIndex((entry) => entry.domain === connection.domain && entry.connectionType === connection.connectionType);
|
|
4785
|
+
const normalized = {
|
|
4786
|
+
...connection,
|
|
4787
|
+
propertyId: connection.propertyId ?? null,
|
|
4788
|
+
refreshToken: connection.refreshToken ?? null,
|
|
4789
|
+
tokenExpiresAt: connection.tokenExpiresAt ?? null,
|
|
4790
|
+
scopes: connection.scopes ?? []
|
|
4791
|
+
};
|
|
4792
|
+
if (index2 === -1) {
|
|
4793
|
+
connections.push(normalized);
|
|
4794
|
+
return normalized;
|
|
4795
|
+
}
|
|
4796
|
+
connections[index2] = normalized;
|
|
4797
|
+
return normalized;
|
|
4798
|
+
}
|
|
4799
|
+
function patchGoogleConnection(config, domain, connectionType, patch) {
|
|
4800
|
+
const existing = getGoogleConnection(config, domain, connectionType);
|
|
4801
|
+
if (!existing) return void 0;
|
|
4802
|
+
return upsertGoogleConnection(config, {
|
|
4803
|
+
...existing,
|
|
4804
|
+
...patch,
|
|
4805
|
+
propertyId: Object.prototype.hasOwnProperty.call(patch, "propertyId") ? patch.propertyId ?? null : existing.propertyId ?? null,
|
|
4806
|
+
refreshToken: Object.prototype.hasOwnProperty.call(patch, "refreshToken") ? patch.refreshToken ?? null : existing.refreshToken ?? null,
|
|
4807
|
+
tokenExpiresAt: Object.prototype.hasOwnProperty.call(patch, "tokenExpiresAt") ? patch.tokenExpiresAt ?? null : existing.tokenExpiresAt ?? null,
|
|
4808
|
+
scopes: patch.scopes ?? existing.scopes ?? []
|
|
4809
|
+
});
|
|
4810
|
+
}
|
|
4811
|
+
function removeGoogleConnection(config, domain, connectionType) {
|
|
4812
|
+
const connections = config.google?.connections;
|
|
4813
|
+
if (!connections?.length) return false;
|
|
4814
|
+
const next = connections.filter((connection) => connection.domain !== domain || connection.connectionType !== connectionType);
|
|
4815
|
+
if (next.length === connections.length) return false;
|
|
4816
|
+
if (!config.google) return false;
|
|
4817
|
+
config.google.connections = next;
|
|
4818
|
+
if (!config.google.clientId && !config.google.clientSecret && next.length === 0) {
|
|
4819
|
+
delete config.google;
|
|
4820
|
+
}
|
|
4821
|
+
return true;
|
|
4822
|
+
}
|
|
4823
|
+
|
|
4701
4824
|
// src/job-runner.ts
|
|
4702
4825
|
import crypto13 from "crypto";
|
|
4703
4826
|
import { eq as eq13, inArray as inArray2 } from "drizzle-orm";
|
|
@@ -4961,7 +5084,7 @@ function computeCompetitorOverlap(normalized, competitorDomains) {
|
|
|
4961
5084
|
|
|
4962
5085
|
// src/gsc-sync.ts
|
|
4963
5086
|
import crypto14 from "crypto";
|
|
4964
|
-
import { eq as eq14, and as
|
|
5087
|
+
import { eq as eq14, and as and4, sql as sql3 } from "drizzle-orm";
|
|
4965
5088
|
function formatDate(d) {
|
|
4966
5089
|
return d.toISOString().split("T")[0];
|
|
4967
5090
|
}
|
|
@@ -4974,11 +5097,15 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
4974
5097
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4975
5098
|
db.update(runs).set({ status: "running", startedAt: now }).where(eq14(runs.id, runId)).run();
|
|
4976
5099
|
try {
|
|
5100
|
+
const { clientId: googleClientId, clientSecret: googleClientSecret } = getGoogleAuthConfig(opts.config);
|
|
5101
|
+
if (!googleClientId || !googleClientSecret) {
|
|
5102
|
+
throw new Error("Google OAuth is not configured in the local Canonry config");
|
|
5103
|
+
}
|
|
4977
5104
|
const project = db.select().from(projects).where(eq14(projects.id, projectId)).get();
|
|
4978
5105
|
if (!project) {
|
|
4979
5106
|
throw new Error(`Project not found: ${projectId}`);
|
|
4980
5107
|
}
|
|
4981
|
-
const conn =
|
|
5108
|
+
const conn = getGoogleConnection(opts.config, project.canonicalDomain, "gsc");
|
|
4982
5109
|
if (!conn || !conn.refreshToken) {
|
|
4983
5110
|
throw new Error("No GSC connection found or connection is incomplete");
|
|
4984
5111
|
}
|
|
@@ -4988,13 +5115,14 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
4988
5115
|
let accessToken = conn.accessToken;
|
|
4989
5116
|
const expiresAt = conn.tokenExpiresAt ? new Date(conn.tokenExpiresAt).getTime() : 0;
|
|
4990
5117
|
if (Date.now() > expiresAt - 5 * 60 * 1e3) {
|
|
4991
|
-
const tokens = await refreshAccessToken(
|
|
5118
|
+
const tokens = await refreshAccessToken(googleClientId, googleClientSecret, conn.refreshToken);
|
|
4992
5119
|
accessToken = tokens.access_token;
|
|
4993
|
-
|
|
5120
|
+
patchGoogleConnection(opts.config, project.canonicalDomain, "gsc", {
|
|
4994
5121
|
accessToken: tokens.access_token,
|
|
4995
5122
|
tokenExpiresAt: new Date(Date.now() + tokens.expires_in * 1e3).toISOString(),
|
|
4996
5123
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4997
|
-
})
|
|
5124
|
+
});
|
|
5125
|
+
saveConfig(opts.config);
|
|
4998
5126
|
}
|
|
4999
5127
|
const lagOffset = GSC_DATA_LAG_DAYS;
|
|
5000
5128
|
const endDate = formatDate(daysAgo(lagOffset));
|
|
@@ -5007,7 +5135,7 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
5007
5135
|
});
|
|
5008
5136
|
console.log(`[GSC Sync] Received ${rows.length} rows`);
|
|
5009
5137
|
db.delete(gscSearchData).where(
|
|
5010
|
-
|
|
5138
|
+
and4(
|
|
5011
5139
|
eq14(gscSearchData.projectId, projectId),
|
|
5012
5140
|
sql3`${gscSearchData.date} >= ${startDate}`,
|
|
5013
5141
|
sql3`${gscSearchData.date} <= ${endDate}`
|
|
@@ -5155,8 +5283,7 @@ var Scheduler = class {
|
|
|
5155
5283
|
/** Stop all cron tasks for graceful shutdown. */
|
|
5156
5284
|
stop() {
|
|
5157
5285
|
for (const [projectId, task] of this.tasks) {
|
|
5158
|
-
|
|
5159
|
-
console.log(`[Scheduler] Stopped task for project ${projectId}`);
|
|
5286
|
+
this.stopTask(projectId, task, "Stopped");
|
|
5160
5287
|
}
|
|
5161
5288
|
this.tasks.clear();
|
|
5162
5289
|
}
|
|
@@ -5164,7 +5291,7 @@ var Scheduler = class {
|
|
|
5164
5291
|
upsert(projectId) {
|
|
5165
5292
|
const existing = this.tasks.get(projectId);
|
|
5166
5293
|
if (existing) {
|
|
5167
|
-
|
|
5294
|
+
this.stopTask(projectId, existing, "Stopped");
|
|
5168
5295
|
this.tasks.delete(projectId);
|
|
5169
5296
|
}
|
|
5170
5297
|
const schedule = this.db.select().from(schedules).where(eq15(schedules.projectId, projectId)).get();
|
|
@@ -5176,11 +5303,15 @@ var Scheduler = class {
|
|
|
5176
5303
|
remove(projectId) {
|
|
5177
5304
|
const existing = this.tasks.get(projectId);
|
|
5178
5305
|
if (existing) {
|
|
5179
|
-
|
|
5306
|
+
this.stopTask(projectId, existing, "Removed");
|
|
5180
5307
|
this.tasks.delete(projectId);
|
|
5181
|
-
console.log(`[Scheduler] Removed task for project ${projectId}`);
|
|
5182
5308
|
}
|
|
5183
5309
|
}
|
|
5310
|
+
stopTask(projectId, task, verb) {
|
|
5311
|
+
task.stop();
|
|
5312
|
+
task.destroy();
|
|
5313
|
+
console.log(`[Scheduler] ${verb} task for project ${projectId}`);
|
|
5314
|
+
}
|
|
5184
5315
|
registerCronTask(schedule) {
|
|
5185
5316
|
const { id: scheduleId, projectId, cronExpr, timezone } = schedule;
|
|
5186
5317
|
if (!cron.validate(cronExpr)) {
|
|
@@ -5244,7 +5375,7 @@ var Scheduler = class {
|
|
|
5244
5375
|
};
|
|
5245
5376
|
|
|
5246
5377
|
// src/notifier.ts
|
|
5247
|
-
import { eq as eq16, desc as desc3, and as
|
|
5378
|
+
import { eq as eq16, desc as desc3, and as and5, or as or2 } from "drizzle-orm";
|
|
5248
5379
|
import crypto15 from "crypto";
|
|
5249
5380
|
var Notifier = class {
|
|
5250
5381
|
db;
|
|
@@ -5307,7 +5438,7 @@ var Notifier = class {
|
|
|
5307
5438
|
}
|
|
5308
5439
|
computeTransitions(runId, projectId) {
|
|
5309
5440
|
const recentRuns = this.db.select().from(runs).where(
|
|
5310
|
-
|
|
5441
|
+
and5(
|
|
5311
5442
|
eq16(runs.projectId, projectId),
|
|
5312
5443
|
or2(eq16(runs.status, "completed"), eq16(runs.status, "partial"))
|
|
5313
5444
|
)
|
|
@@ -5515,18 +5646,19 @@ var DEFAULT_QUOTA = {
|
|
|
5515
5646
|
maxRequestsPerDay: 1e3
|
|
5516
5647
|
};
|
|
5517
5648
|
async function createServer(opts) {
|
|
5518
|
-
const
|
|
5519
|
-
|
|
5520
|
-
|
|
5521
|
-
|
|
5522
|
-
|
|
5523
|
-
|
|
5524
|
-
|
|
5525
|
-
|
|
5526
|
-
messageFormat: "{msg} {req.method} {req.url}"
|
|
5527
|
-
}
|
|
5649
|
+
const logger = opts.logger === false ? false : process.stdout.isTTY ? {
|
|
5650
|
+
transport: {
|
|
5651
|
+
target: "pino-pretty",
|
|
5652
|
+
options: {
|
|
5653
|
+
colorize: true,
|
|
5654
|
+
translateTime: "HH:MM:ss",
|
|
5655
|
+
ignore: "pid,hostname,reqId",
|
|
5656
|
+
messageFormat: "{msg} {req.method} {req.url}"
|
|
5528
5657
|
}
|
|
5529
5658
|
}
|
|
5659
|
+
} : true;
|
|
5660
|
+
const app = Fastify({
|
|
5661
|
+
logger
|
|
5530
5662
|
});
|
|
5531
5663
|
const registry = new ProviderRegistry();
|
|
5532
5664
|
const providers = opts.config.providers ?? {};
|
|
@@ -5593,25 +5725,45 @@ async function createServer(opts) {
|
|
|
5593
5725
|
configured: !!registry.get(name),
|
|
5594
5726
|
quota: registry.get(name)?.config.quotaPolicy
|
|
5595
5727
|
}));
|
|
5728
|
+
const googleSettingsSummary = {
|
|
5729
|
+
configured: Boolean(opts.config.google?.clientId && opts.config.google?.clientSecret)
|
|
5730
|
+
};
|
|
5596
5731
|
const adapterMap = { gemini: geminiAdapter, openai: openaiAdapter, claude: claudeAdapter, local: localAdapter };
|
|
5597
|
-
const googleClientId = process.env.GOOGLE_CLIENT_ID;
|
|
5598
|
-
const googleClientSecret = process.env.GOOGLE_CLIENT_SECRET;
|
|
5599
5732
|
const googleStateSecret = process.env.GOOGLE_STATE_SECRET ?? crypto16.randomBytes(32).toString("hex");
|
|
5733
|
+
const googleConnectionStore = {
|
|
5734
|
+
listConnections: (domain) => listGoogleConnections(opts.config, domain),
|
|
5735
|
+
getConnection: (domain, connectionType) => getGoogleConnection(opts.config, domain, connectionType),
|
|
5736
|
+
upsertConnection: (connection) => {
|
|
5737
|
+
const updated = upsertGoogleConnection(opts.config, connection);
|
|
5738
|
+
saveConfig(opts.config);
|
|
5739
|
+
return updated;
|
|
5740
|
+
},
|
|
5741
|
+
updateConnection: (domain, connectionType, patch) => {
|
|
5742
|
+
const updated = patchGoogleConnection(opts.config, domain, connectionType, patch);
|
|
5743
|
+
if (updated) saveConfig(opts.config);
|
|
5744
|
+
return updated;
|
|
5745
|
+
},
|
|
5746
|
+
deleteConnection: (domain, connectionType) => {
|
|
5747
|
+
const removed = removeGoogleConnection(opts.config, domain, connectionType);
|
|
5748
|
+
if (removed) saveConfig(opts.config);
|
|
5749
|
+
return removed;
|
|
5750
|
+
}
|
|
5751
|
+
};
|
|
5600
5752
|
await app.register(apiRoutes, {
|
|
5601
5753
|
db: opts.db,
|
|
5602
5754
|
skipAuth: false,
|
|
5603
|
-
|
|
5604
|
-
|
|
5755
|
+
getGoogleAuthConfig: () => getGoogleAuthConfig(opts.config),
|
|
5756
|
+
googleConnectionStore,
|
|
5605
5757
|
googleStateSecret,
|
|
5606
5758
|
onGscSyncRequested: (runId, projectId, syncOpts) => {
|
|
5759
|
+
const { clientId: googleClientId, clientSecret: googleClientSecret } = getGoogleAuthConfig(opts.config);
|
|
5607
5760
|
if (!googleClientId || !googleClientSecret) {
|
|
5608
|
-
app.log.error("GSC sync requested but
|
|
5761
|
+
app.log.error("GSC sync requested but Google OAuth credentials are not configured in the local config");
|
|
5609
5762
|
return;
|
|
5610
5763
|
}
|
|
5611
5764
|
executeGscSync(opts.db, runId, projectId, {
|
|
5612
5765
|
...syncOpts,
|
|
5613
|
-
|
|
5614
|
-
googleClientSecret
|
|
5766
|
+
config: opts.config
|
|
5615
5767
|
}).catch((err) => {
|
|
5616
5768
|
app.log.error({ runId, err }, "GSC sync failed");
|
|
5617
5769
|
});
|
|
@@ -5621,6 +5773,7 @@ async function createServer(opts) {
|
|
|
5621
5773
|
version: PKG_VERSION
|
|
5622
5774
|
},
|
|
5623
5775
|
providerSummary,
|
|
5776
|
+
googleSettingsSummary,
|
|
5624
5777
|
onRunCreated: (runId, projectId, providers2) => {
|
|
5625
5778
|
jobRunner.executeRun(runId, projectId, providers2).catch((err) => {
|
|
5626
5779
|
app.log.error({ runId, err }, "Job runner failed");
|
|
@@ -5665,6 +5818,17 @@ async function createServer(opts) {
|
|
|
5665
5818
|
quota
|
|
5666
5819
|
};
|
|
5667
5820
|
},
|
|
5821
|
+
onGoogleSettingsUpdate: (clientId, clientSecret) => {
|
|
5822
|
+
try {
|
|
5823
|
+
setGoogleAuthConfig(opts.config, { clientId, clientSecret });
|
|
5824
|
+
saveConfig(opts.config);
|
|
5825
|
+
googleSettingsSummary.configured = true;
|
|
5826
|
+
return { ...googleSettingsSummary };
|
|
5827
|
+
} catch (err) {
|
|
5828
|
+
app.log.error({ err }, "Failed to save Google OAuth config");
|
|
5829
|
+
return null;
|
|
5830
|
+
}
|
|
5831
|
+
},
|
|
5668
5832
|
onScheduleUpdated: (action, projectId) => {
|
|
5669
5833
|
if (action === "upsert") scheduler.upsert(projectId);
|
|
5670
5834
|
if (action === "delete") scheduler.remove(projectId);
|
|
@@ -5814,5 +5978,6 @@ export {
|
|
|
5814
5978
|
isFirstRun,
|
|
5815
5979
|
showFirstRunNotice,
|
|
5816
5980
|
trackEvent,
|
|
5981
|
+
setGoogleAuthConfig,
|
|
5817
5982
|
createServer
|
|
5818
5983
|
};
|