@ainyc/canonry 1.10.1 → 1.12.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 +27 -3
- package/assets/apple-touch-icon.png +0 -0
- package/assets/assets/index-BEsueXzg.css +1 -0
- package/assets/assets/index-Bol7Z6qk.js +243 -0
- package/assets/favicon-32.png +0 -0
- package/assets/favicon.svg +24 -0
- package/assets/index.html +7 -3
- package/dist/{chunk-2DC7RBXJ.js → chunk-O4HLQBL7.js} +324 -106
- package/dist/cli.js +114 -17
- package/dist/index.d.ts +20 -0
- package/dist/index.js +1 -1
- package/package.json +6 -6
- 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,64 +3460,110 @@ 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
|
-
const { type, propertyId } = request.body ?? {};
|
|
3510
|
+
const { type, propertyId, publicUrl } = request.body ?? {};
|
|
3473
3511
|
if (!type || type !== "gsc" && type !== "ga4") {
|
|
3474
3512
|
const err = validationError('type must be "gsc" or "ga4"');
|
|
3475
3513
|
return reply.status(err.statusCode).send(err.toJSON());
|
|
3476
3514
|
}
|
|
3477
3515
|
const project = resolveProject(app.db, request.params.name);
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3516
|
+
let redirectUri;
|
|
3517
|
+
if (publicUrl) {
|
|
3518
|
+
redirectUri = publicUrl.replace(/\/$/, "") + "/api/v1/google/callback";
|
|
3519
|
+
} else if (opts.publicUrl) {
|
|
3520
|
+
redirectUri = opts.publicUrl.replace(/\/$/, "") + "/api/v1/google/callback";
|
|
3521
|
+
} else {
|
|
3522
|
+
const proto = request.headers["x-forwarded-proto"] ?? "http";
|
|
3523
|
+
const host = request.headers.host ?? "localhost:4100";
|
|
3524
|
+
redirectUri = `${proto}://${host}/api/v1/projects/${encodeURIComponent(request.params.name)}/google/callback`;
|
|
3525
|
+
}
|
|
3481
3526
|
const scopes = type === "gsc" ? [GSC_SCOPE] : [];
|
|
3482
3527
|
const stateEncoded = buildSignedState(
|
|
3483
3528
|
{ domain: project.canonicalDomain, type, propertyId, redirectUri },
|
|
3484
3529
|
stateSecret
|
|
3485
3530
|
);
|
|
3486
3531
|
const authUrl = getAuthUrl(googleClientId, redirectUri, scopes, stateEncoded);
|
|
3487
|
-
return { authUrl };
|
|
3532
|
+
return { authUrl, redirectUri };
|
|
3488
3533
|
});
|
|
3489
|
-
|
|
3534
|
+
async function handleOAuthCallback(request, reply) {
|
|
3535
|
+
const { clientId: googleClientId, clientSecret: googleClientSecret } = getAuthConfig();
|
|
3490
3536
|
if (!googleClientId || !googleClientSecret) {
|
|
3491
3537
|
return reply.status(500).send("Google OAuth not configured");
|
|
3492
3538
|
}
|
|
3539
|
+
const store = requireConnectionStore(reply);
|
|
3540
|
+
if (!store) return;
|
|
3541
|
+
const escapeHtml = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
3493
3542
|
const { code, state, error } = request.query;
|
|
3494
3543
|
if (error) {
|
|
3495
|
-
const safeError = String(error)
|
|
3496
|
-
|
|
3544
|
+
const safeError = escapeHtml(String(error));
|
|
3545
|
+
const errorHtml = error === "redirect_uri_mismatch" ? `<html><body style="font-family:system-ui;padding:40px;max-width:600px;margin:0 auto">
|
|
3546
|
+
<h2 style="color:#ef4444">Redirect URI mismatch</h2>
|
|
3547
|
+
<p>Google rejected the OAuth callback because the redirect URI is not registered.</p>
|
|
3548
|
+
<p><strong>To fix this:</strong></p>
|
|
3549
|
+
<ol>
|
|
3550
|
+
<li>Go to the <a href="https://console.cloud.google.com/apis/credentials" target="_blank">Google Cloud Console \u2192 Credentials</a></li>
|
|
3551
|
+
<li>Click your OAuth 2.0 Client ID</li>
|
|
3552
|
+
<li>Under "Authorized redirect URIs", add:<br><code style="background:#1e1e1e;color:#e0e0e0;padding:4px 8px;border-radius:4px;display:inline-block;margin-top:4px">${request.query.state ? (() => {
|
|
3553
|
+
try {
|
|
3554
|
+
const s = verifySignedState(request.query.state, stateSecret);
|
|
3555
|
+
return escapeHtml(String(s?.redirectUri ?? "Could not determine URI"));
|
|
3556
|
+
} catch {
|
|
3557
|
+
return "Could not determine URI";
|
|
3558
|
+
}
|
|
3559
|
+
})() : "Could not determine URI"}</code></li>
|
|
3560
|
+
<li>Click Save, then retry the connection</li>
|
|
3561
|
+
</ol>
|
|
3562
|
+
<p style="color:#888">You can close this tab.</p>
|
|
3563
|
+
</body></html>` : `<html><body style="font-family:system-ui;text-align:center;padding:60px">
|
|
3564
|
+
<h2>Authorization failed</h2><p>${safeError}</p><p style="color:#888">You can close this tab.</p>
|
|
3565
|
+
</body></html>`;
|
|
3566
|
+
return reply.type("text/html").send(errorHtml);
|
|
3497
3567
|
}
|
|
3498
3568
|
if (!code || !state) {
|
|
3499
3569
|
return reply.status(400).send("Missing code or state parameter");
|
|
@@ -3503,33 +3573,37 @@ async function googleRoutes(app, opts) {
|
|
|
3503
3573
|
return reply.status(400).send("Invalid or tampered state parameter");
|
|
3504
3574
|
}
|
|
3505
3575
|
const { domain, type, propertyId, redirectUri } = stateData;
|
|
3506
|
-
|
|
3576
|
+
let tokens;
|
|
3577
|
+
try {
|
|
3578
|
+
tokens = await exchangeCode(googleClientId, googleClientSecret, code, redirectUri);
|
|
3579
|
+
} catch (err) {
|
|
3580
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3581
|
+
return reply.type("text/html").send(
|
|
3582
|
+
`<html><body style="font-family:system-ui;padding:40px;max-width:600px;margin:0 auto">
|
|
3583
|
+
<h2 style="color:#ef4444">Token exchange failed</h2>
|
|
3584
|
+
<p>${escapeHtml(msg)}</p>
|
|
3585
|
+
<p><strong>Redirect URI used:</strong><br>
|
|
3586
|
+
<code style="background:#1e1e1e;color:#e0e0e0;padding:4px 8px;border-radius:4px">${escapeHtml(redirectUri)}</code>
|
|
3587
|
+
</p>
|
|
3588
|
+
<p>Ensure this URI is listed in your <a href="https://console.cloud.google.com/apis/credentials" target="_blank">Google Cloud Console</a> OAuth client's authorized redirect URIs.</p>
|
|
3589
|
+
<p style="color:#888">You can close this tab.</p>
|
|
3590
|
+
</body></html>`
|
|
3591
|
+
);
|
|
3592
|
+
}
|
|
3507
3593
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3508
3594
|
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
|
-
}
|
|
3595
|
+
const existing = store.getConnection(domain, type);
|
|
3596
|
+
store.upsertConnection({
|
|
3597
|
+
domain,
|
|
3598
|
+
connectionType: type,
|
|
3599
|
+
propertyId: propertyId ?? existing?.propertyId ?? null,
|
|
3600
|
+
accessToken: tokens.access_token,
|
|
3601
|
+
refreshToken: tokens.refresh_token ?? existing?.refreshToken ?? null,
|
|
3602
|
+
tokenExpiresAt: expiresAt,
|
|
3603
|
+
scopes: tokens.scope?.split(" ") ?? [],
|
|
3604
|
+
createdAt: existing?.createdAt ?? now,
|
|
3605
|
+
updatedAt: now
|
|
3606
|
+
});
|
|
3533
3607
|
writeAuditLog(app.db, {
|
|
3534
3608
|
projectId: null,
|
|
3535
3609
|
actor: "oauth",
|
|
@@ -3545,11 +3619,19 @@ async function googleRoutes(app, opts) {
|
|
|
3545
3619
|
<p style="color:#888">You can close this tab.</p>
|
|
3546
3620
|
</body></html>`
|
|
3547
3621
|
);
|
|
3622
|
+
}
|
|
3623
|
+
app.get("/google/callback", async (request, reply) => {
|
|
3624
|
+
return handleOAuthCallback(request, reply);
|
|
3625
|
+
});
|
|
3626
|
+
app.get("/projects/:name/google/callback", async (request, reply) => {
|
|
3627
|
+
return handleOAuthCallback(request, reply);
|
|
3548
3628
|
});
|
|
3549
3629
|
app.delete("/projects/:name/google/connections/:type", async (request, reply) => {
|
|
3630
|
+
const store = requireConnectionStore(reply);
|
|
3631
|
+
if (!store) return;
|
|
3550
3632
|
const project = resolveProject(app.db, request.params.name);
|
|
3551
|
-
const deleted =
|
|
3552
|
-
if (deleted
|
|
3633
|
+
const deleted = store.deleteConnection(project.canonicalDomain, request.params.type);
|
|
3634
|
+
if (!deleted) {
|
|
3553
3635
|
const err = notFound("Google connection", request.params.type);
|
|
3554
3636
|
return reply.status(err.statusCode).send(err.toJSON());
|
|
3555
3637
|
}
|
|
@@ -3563,18 +3645,23 @@ async function googleRoutes(app, opts) {
|
|
|
3563
3645
|
return reply.status(204).send();
|
|
3564
3646
|
});
|
|
3565
3647
|
app.get("/projects/:name/google/properties", async (request, reply) => {
|
|
3648
|
+
const { clientId: googleClientId, clientSecret: googleClientSecret } = getAuthConfig();
|
|
3566
3649
|
if (!googleClientId || !googleClientSecret) {
|
|
3567
3650
|
const err = validationError("Google OAuth is not configured");
|
|
3568
3651
|
return reply.status(err.statusCode).send(err.toJSON());
|
|
3569
3652
|
}
|
|
3653
|
+
const store = requireConnectionStore(reply);
|
|
3654
|
+
if (!store) return;
|
|
3570
3655
|
const project = resolveProject(app.db, request.params.name);
|
|
3571
|
-
const { accessToken } = await getValidToken(
|
|
3656
|
+
const { accessToken } = await getValidToken(store, project.canonicalDomain, "gsc", googleClientId, googleClientSecret);
|
|
3572
3657
|
const sites = await listSites(accessToken);
|
|
3573
3658
|
return { sites };
|
|
3574
3659
|
});
|
|
3575
3660
|
app.post("/projects/:name/google/gsc/sync", async (request, reply) => {
|
|
3661
|
+
const store = requireConnectionStore(reply);
|
|
3662
|
+
if (!store) return;
|
|
3576
3663
|
const project = resolveProject(app.db, request.params.name);
|
|
3577
|
-
const conn =
|
|
3664
|
+
const conn = store.getConnection(project.canonicalDomain, "gsc");
|
|
3578
3665
|
if (!conn) {
|
|
3579
3666
|
const err = validationError('No GSC connection found for this domain. Run "canonry google connect" first.');
|
|
3580
3667
|
return reply.status(err.statusCode).send(err.toJSON());
|
|
@@ -3604,7 +3691,7 @@ async function googleRoutes(app, opts) {
|
|
|
3604
3691
|
if (endDate) conditions.push(sql2`${gscSearchData.date} <= ${endDate}`);
|
|
3605
3692
|
if (query) conditions.push(sql2`${gscSearchData.query} LIKE ${"%" + query + "%"}`);
|
|
3606
3693
|
if (page) conditions.push(sql2`${gscSearchData.page} LIKE ${"%" + page + "%"}`);
|
|
3607
|
-
const rows = app.db.select().from(gscSearchData).where(
|
|
3694
|
+
const rows = app.db.select().from(gscSearchData).where(and3(...conditions)).orderBy(desc2(gscSearchData.date)).limit(parseInt(limit ?? "500", 10)).all();
|
|
3608
3695
|
return rows.map((r) => ({
|
|
3609
3696
|
date: r.date,
|
|
3610
3697
|
query: r.query,
|
|
@@ -3618,17 +3705,20 @@ async function googleRoutes(app, opts) {
|
|
|
3618
3705
|
}));
|
|
3619
3706
|
});
|
|
3620
3707
|
app.post("/projects/:name/google/gsc/inspect", async (request, reply) => {
|
|
3708
|
+
const { clientId: googleClientId, clientSecret: googleClientSecret } = getAuthConfig();
|
|
3621
3709
|
if (!googleClientId || !googleClientSecret) {
|
|
3622
3710
|
const err = validationError("Google OAuth is not configured");
|
|
3623
3711
|
return reply.status(err.statusCode).send(err.toJSON());
|
|
3624
3712
|
}
|
|
3713
|
+
const store = requireConnectionStore(reply);
|
|
3714
|
+
if (!store) return;
|
|
3625
3715
|
const project = resolveProject(app.db, request.params.name);
|
|
3626
3716
|
const { url } = request.body ?? {};
|
|
3627
3717
|
if (!url) {
|
|
3628
3718
|
const err = validationError("url is required");
|
|
3629
3719
|
return reply.status(err.statusCode).send(err.toJSON());
|
|
3630
3720
|
}
|
|
3631
|
-
const { accessToken, propertyId } = await getValidToken(
|
|
3721
|
+
const { accessToken, propertyId } = await getValidToken(store, project.canonicalDomain, "gsc", googleClientId, googleClientSecret);
|
|
3632
3722
|
if (!propertyId) {
|
|
3633
3723
|
const err = validationError("No GSC property configured for this connection");
|
|
3634
3724
|
return reply.status(err.statusCode).send(err.toJSON());
|
|
@@ -3679,7 +3769,7 @@ async function googleRoutes(app, opts) {
|
|
|
3679
3769
|
const { url, limit } = request.query;
|
|
3680
3770
|
const conditions = [eq12(gscUrlInspections.projectId, project.id)];
|
|
3681
3771
|
if (url) conditions.push(eq12(gscUrlInspections.url, url));
|
|
3682
|
-
const rows = app.db.select().from(gscUrlInspections).where(
|
|
3772
|
+
const rows = app.db.select().from(gscUrlInspections).where(and3(...conditions)).orderBy(desc2(gscUrlInspections.inspectedAt)).limit(parseInt(limit ?? "100", 10)).all();
|
|
3683
3773
|
return rows.map((r) => ({
|
|
3684
3774
|
id: r.id,
|
|
3685
3775
|
url: r.url,
|
|
@@ -3725,18 +3815,23 @@ async function googleRoutes(app, opts) {
|
|
|
3725
3815
|
return deindexed;
|
|
3726
3816
|
});
|
|
3727
3817
|
app.put("/projects/:name/google/connections/:type/property", async (request, reply) => {
|
|
3818
|
+
const store = requireConnectionStore(reply);
|
|
3819
|
+
if (!store) return;
|
|
3728
3820
|
const project = resolveProject(app.db, request.params.name);
|
|
3729
3821
|
const { propertyId } = request.body ?? {};
|
|
3730
3822
|
if (!propertyId) {
|
|
3731
3823
|
const err = validationError("propertyId is required");
|
|
3732
3824
|
return reply.status(err.statusCode).send(err.toJSON());
|
|
3733
3825
|
}
|
|
3734
|
-
const conn =
|
|
3826
|
+
const conn = store.updateConnection(
|
|
3827
|
+
project.canonicalDomain,
|
|
3828
|
+
request.params.type,
|
|
3829
|
+
{ propertyId, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }
|
|
3830
|
+
);
|
|
3735
3831
|
if (!conn) {
|
|
3736
3832
|
const err = notFound("Google connection", request.params.type);
|
|
3737
3833
|
return reply.status(err.statusCode).send(err.toJSON());
|
|
3738
3834
|
}
|
|
3739
|
-
app.db.update(googleConnections).set({ propertyId, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq12(googleConnections.id, conn.id)).run();
|
|
3740
3835
|
return { propertyId };
|
|
3741
3836
|
});
|
|
3742
3837
|
}
|
|
@@ -3758,12 +3853,20 @@ async function apiRoutes(app, opts) {
|
|
|
3758
3853
|
await api.register(competitorRoutes);
|
|
3759
3854
|
await api.register(runRoutes, { onRunCreated: opts.onRunCreated });
|
|
3760
3855
|
await api.register(applyRoutes, {
|
|
3761
|
-
onScheduleUpdated: opts.onScheduleUpdated
|
|
3856
|
+
onScheduleUpdated: opts.onScheduleUpdated,
|
|
3857
|
+
onGoogleConnectionPropertyUpdated: (domain, connectionType, propertyId) => {
|
|
3858
|
+
opts.googleConnectionStore?.updateConnection(domain, connectionType, {
|
|
3859
|
+
propertyId,
|
|
3860
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3861
|
+
});
|
|
3862
|
+
}
|
|
3762
3863
|
});
|
|
3763
3864
|
await api.register(historyRoutes);
|
|
3764
3865
|
await api.register(settingsRoutes, {
|
|
3765
3866
|
providerSummary: opts.providerSummary,
|
|
3766
|
-
onProviderUpdate: opts.onProviderUpdate
|
|
3867
|
+
onProviderUpdate: opts.onProviderUpdate,
|
|
3868
|
+
google: opts.googleSettingsSummary,
|
|
3869
|
+
onGoogleUpdate: opts.onGoogleSettingsUpdate
|
|
3767
3870
|
});
|
|
3768
3871
|
await api.register(scheduleRoutes, {
|
|
3769
3872
|
onScheduleUpdated: opts.onScheduleUpdated
|
|
@@ -3774,9 +3877,10 @@ async function apiRoutes(app, opts) {
|
|
|
3774
3877
|
setTelemetryEnabled: opts.setTelemetryEnabled
|
|
3775
3878
|
});
|
|
3776
3879
|
await api.register(googleRoutes, {
|
|
3777
|
-
|
|
3778
|
-
|
|
3880
|
+
getGoogleAuthConfig: opts.getGoogleAuthConfig,
|
|
3881
|
+
googleConnectionStore: opts.googleConnectionStore,
|
|
3779
3882
|
googleStateSecret: opts.googleStateSecret,
|
|
3883
|
+
publicUrl: opts.publicUrl,
|
|
3780
3884
|
onGscSyncRequested: opts.onGscSyncRequested
|
|
3781
3885
|
});
|
|
3782
3886
|
}, { prefix: "/api/v1" });
|
|
@@ -4698,6 +4802,77 @@ var localAdapter = {
|
|
|
4698
4802
|
}
|
|
4699
4803
|
};
|
|
4700
4804
|
|
|
4805
|
+
// src/google-config.ts
|
|
4806
|
+
function ensureConnections(config) {
|
|
4807
|
+
if (!config.google) config.google = {};
|
|
4808
|
+
if (!config.google.connections) config.google.connections = [];
|
|
4809
|
+
return config.google.connections;
|
|
4810
|
+
}
|
|
4811
|
+
function getGoogleAuthConfig(config) {
|
|
4812
|
+
return {
|
|
4813
|
+
clientId: config.google?.clientId,
|
|
4814
|
+
clientSecret: config.google?.clientSecret
|
|
4815
|
+
};
|
|
4816
|
+
}
|
|
4817
|
+
function setGoogleAuthConfig(config, auth) {
|
|
4818
|
+
const hasValues = Boolean(auth.clientId || auth.clientSecret || config.google?.connections?.length);
|
|
4819
|
+
if (!hasValues) {
|
|
4820
|
+
delete config.google;
|
|
4821
|
+
return;
|
|
4822
|
+
}
|
|
4823
|
+
if (!config.google) config.google = {};
|
|
4824
|
+
config.google.clientId = auth.clientId;
|
|
4825
|
+
config.google.clientSecret = auth.clientSecret;
|
|
4826
|
+
config.google.connections = config.google.connections ?? [];
|
|
4827
|
+
}
|
|
4828
|
+
function listGoogleConnections(config, domain) {
|
|
4829
|
+
return (config.google?.connections ?? []).filter((connection) => connection.domain === domain);
|
|
4830
|
+
}
|
|
4831
|
+
function getGoogleConnection(config, domain, connectionType) {
|
|
4832
|
+
return (config.google?.connections ?? []).find((connection) => connection.domain === domain && connection.connectionType === connectionType);
|
|
4833
|
+
}
|
|
4834
|
+
function upsertGoogleConnection(config, connection) {
|
|
4835
|
+
const connections = ensureConnections(config);
|
|
4836
|
+
const index2 = connections.findIndex((entry) => entry.domain === connection.domain && entry.connectionType === connection.connectionType);
|
|
4837
|
+
const normalized = {
|
|
4838
|
+
...connection,
|
|
4839
|
+
propertyId: connection.propertyId ?? null,
|
|
4840
|
+
refreshToken: connection.refreshToken ?? null,
|
|
4841
|
+
tokenExpiresAt: connection.tokenExpiresAt ?? null,
|
|
4842
|
+
scopes: connection.scopes ?? []
|
|
4843
|
+
};
|
|
4844
|
+
if (index2 === -1) {
|
|
4845
|
+
connections.push(normalized);
|
|
4846
|
+
return normalized;
|
|
4847
|
+
}
|
|
4848
|
+
connections[index2] = normalized;
|
|
4849
|
+
return normalized;
|
|
4850
|
+
}
|
|
4851
|
+
function patchGoogleConnection(config, domain, connectionType, patch) {
|
|
4852
|
+
const existing = getGoogleConnection(config, domain, connectionType);
|
|
4853
|
+
if (!existing) return void 0;
|
|
4854
|
+
return upsertGoogleConnection(config, {
|
|
4855
|
+
...existing,
|
|
4856
|
+
...patch,
|
|
4857
|
+
propertyId: Object.prototype.hasOwnProperty.call(patch, "propertyId") ? patch.propertyId ?? null : existing.propertyId ?? null,
|
|
4858
|
+
refreshToken: Object.prototype.hasOwnProperty.call(patch, "refreshToken") ? patch.refreshToken ?? null : existing.refreshToken ?? null,
|
|
4859
|
+
tokenExpiresAt: Object.prototype.hasOwnProperty.call(patch, "tokenExpiresAt") ? patch.tokenExpiresAt ?? null : existing.tokenExpiresAt ?? null,
|
|
4860
|
+
scopes: patch.scopes ?? existing.scopes ?? []
|
|
4861
|
+
});
|
|
4862
|
+
}
|
|
4863
|
+
function removeGoogleConnection(config, domain, connectionType) {
|
|
4864
|
+
const connections = config.google?.connections;
|
|
4865
|
+
if (!connections?.length) return false;
|
|
4866
|
+
const next = connections.filter((connection) => connection.domain !== domain || connection.connectionType !== connectionType);
|
|
4867
|
+
if (next.length === connections.length) return false;
|
|
4868
|
+
if (!config.google) return false;
|
|
4869
|
+
config.google.connections = next;
|
|
4870
|
+
if (!config.google.clientId && !config.google.clientSecret && next.length === 0) {
|
|
4871
|
+
delete config.google;
|
|
4872
|
+
}
|
|
4873
|
+
return true;
|
|
4874
|
+
}
|
|
4875
|
+
|
|
4701
4876
|
// src/job-runner.ts
|
|
4702
4877
|
import crypto13 from "crypto";
|
|
4703
4878
|
import { eq as eq13, inArray as inArray2 } from "drizzle-orm";
|
|
@@ -4961,7 +5136,7 @@ function computeCompetitorOverlap(normalized, competitorDomains) {
|
|
|
4961
5136
|
|
|
4962
5137
|
// src/gsc-sync.ts
|
|
4963
5138
|
import crypto14 from "crypto";
|
|
4964
|
-
import { eq as eq14, and as
|
|
5139
|
+
import { eq as eq14, and as and4, sql as sql3 } from "drizzle-orm";
|
|
4965
5140
|
function formatDate(d) {
|
|
4966
5141
|
return d.toISOString().split("T")[0];
|
|
4967
5142
|
}
|
|
@@ -4974,11 +5149,15 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
4974
5149
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4975
5150
|
db.update(runs).set({ status: "running", startedAt: now }).where(eq14(runs.id, runId)).run();
|
|
4976
5151
|
try {
|
|
5152
|
+
const { clientId: googleClientId, clientSecret: googleClientSecret } = getGoogleAuthConfig(opts.config);
|
|
5153
|
+
if (!googleClientId || !googleClientSecret) {
|
|
5154
|
+
throw new Error("Google OAuth is not configured in the local Canonry config");
|
|
5155
|
+
}
|
|
4977
5156
|
const project = db.select().from(projects).where(eq14(projects.id, projectId)).get();
|
|
4978
5157
|
if (!project) {
|
|
4979
5158
|
throw new Error(`Project not found: ${projectId}`);
|
|
4980
5159
|
}
|
|
4981
|
-
const conn =
|
|
5160
|
+
const conn = getGoogleConnection(opts.config, project.canonicalDomain, "gsc");
|
|
4982
5161
|
if (!conn || !conn.refreshToken) {
|
|
4983
5162
|
throw new Error("No GSC connection found or connection is incomplete");
|
|
4984
5163
|
}
|
|
@@ -4988,13 +5167,14 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
4988
5167
|
let accessToken = conn.accessToken;
|
|
4989
5168
|
const expiresAt = conn.tokenExpiresAt ? new Date(conn.tokenExpiresAt).getTime() : 0;
|
|
4990
5169
|
if (Date.now() > expiresAt - 5 * 60 * 1e3) {
|
|
4991
|
-
const tokens = await refreshAccessToken(
|
|
5170
|
+
const tokens = await refreshAccessToken(googleClientId, googleClientSecret, conn.refreshToken);
|
|
4992
5171
|
accessToken = tokens.access_token;
|
|
4993
|
-
|
|
5172
|
+
patchGoogleConnection(opts.config, project.canonicalDomain, "gsc", {
|
|
4994
5173
|
accessToken: tokens.access_token,
|
|
4995
5174
|
tokenExpiresAt: new Date(Date.now() + tokens.expires_in * 1e3).toISOString(),
|
|
4996
5175
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4997
|
-
})
|
|
5176
|
+
});
|
|
5177
|
+
saveConfig(opts.config);
|
|
4998
5178
|
}
|
|
4999
5179
|
const lagOffset = GSC_DATA_LAG_DAYS;
|
|
5000
5180
|
const endDate = formatDate(daysAgo(lagOffset));
|
|
@@ -5007,7 +5187,7 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
5007
5187
|
});
|
|
5008
5188
|
console.log(`[GSC Sync] Received ${rows.length} rows`);
|
|
5009
5189
|
db.delete(gscSearchData).where(
|
|
5010
|
-
|
|
5190
|
+
and4(
|
|
5011
5191
|
eq14(gscSearchData.projectId, projectId),
|
|
5012
5192
|
sql3`${gscSearchData.date} >= ${startDate}`,
|
|
5013
5193
|
sql3`${gscSearchData.date} <= ${endDate}`
|
|
@@ -5155,8 +5335,7 @@ var Scheduler = class {
|
|
|
5155
5335
|
/** Stop all cron tasks for graceful shutdown. */
|
|
5156
5336
|
stop() {
|
|
5157
5337
|
for (const [projectId, task] of this.tasks) {
|
|
5158
|
-
|
|
5159
|
-
console.log(`[Scheduler] Stopped task for project ${projectId}`);
|
|
5338
|
+
this.stopTask(projectId, task, "Stopped");
|
|
5160
5339
|
}
|
|
5161
5340
|
this.tasks.clear();
|
|
5162
5341
|
}
|
|
@@ -5164,7 +5343,7 @@ var Scheduler = class {
|
|
|
5164
5343
|
upsert(projectId) {
|
|
5165
5344
|
const existing = this.tasks.get(projectId);
|
|
5166
5345
|
if (existing) {
|
|
5167
|
-
|
|
5346
|
+
this.stopTask(projectId, existing, "Stopped");
|
|
5168
5347
|
this.tasks.delete(projectId);
|
|
5169
5348
|
}
|
|
5170
5349
|
const schedule = this.db.select().from(schedules).where(eq15(schedules.projectId, projectId)).get();
|
|
@@ -5176,11 +5355,15 @@ var Scheduler = class {
|
|
|
5176
5355
|
remove(projectId) {
|
|
5177
5356
|
const existing = this.tasks.get(projectId);
|
|
5178
5357
|
if (existing) {
|
|
5179
|
-
|
|
5358
|
+
this.stopTask(projectId, existing, "Removed");
|
|
5180
5359
|
this.tasks.delete(projectId);
|
|
5181
|
-
console.log(`[Scheduler] Removed task for project ${projectId}`);
|
|
5182
5360
|
}
|
|
5183
5361
|
}
|
|
5362
|
+
stopTask(projectId, task, verb) {
|
|
5363
|
+
task.stop();
|
|
5364
|
+
task.destroy();
|
|
5365
|
+
console.log(`[Scheduler] ${verb} task for project ${projectId}`);
|
|
5366
|
+
}
|
|
5184
5367
|
registerCronTask(schedule) {
|
|
5185
5368
|
const { id: scheduleId, projectId, cronExpr, timezone } = schedule;
|
|
5186
5369
|
if (!cron.validate(cronExpr)) {
|
|
@@ -5244,7 +5427,7 @@ var Scheduler = class {
|
|
|
5244
5427
|
};
|
|
5245
5428
|
|
|
5246
5429
|
// src/notifier.ts
|
|
5247
|
-
import { eq as eq16, desc as desc3, and as
|
|
5430
|
+
import { eq as eq16, desc as desc3, and as and5, or as or2 } from "drizzle-orm";
|
|
5248
5431
|
import crypto15 from "crypto";
|
|
5249
5432
|
var Notifier = class {
|
|
5250
5433
|
db;
|
|
@@ -5307,7 +5490,7 @@ var Notifier = class {
|
|
|
5307
5490
|
}
|
|
5308
5491
|
computeTransitions(runId, projectId) {
|
|
5309
5492
|
const recentRuns = this.db.select().from(runs).where(
|
|
5310
|
-
|
|
5493
|
+
and5(
|
|
5311
5494
|
eq16(runs.projectId, projectId),
|
|
5312
5495
|
or2(eq16(runs.status, "completed"), eq16(runs.status, "partial"))
|
|
5313
5496
|
)
|
|
@@ -5515,18 +5698,19 @@ var DEFAULT_QUOTA = {
|
|
|
5515
5698
|
maxRequestsPerDay: 1e3
|
|
5516
5699
|
};
|
|
5517
5700
|
async function createServer(opts) {
|
|
5518
|
-
const
|
|
5519
|
-
|
|
5520
|
-
|
|
5521
|
-
|
|
5522
|
-
|
|
5523
|
-
|
|
5524
|
-
|
|
5525
|
-
|
|
5526
|
-
messageFormat: "{msg} {req.method} {req.url}"
|
|
5527
|
-
}
|
|
5701
|
+
const logger = opts.logger === false ? false : process.stdout.isTTY ? {
|
|
5702
|
+
transport: {
|
|
5703
|
+
target: "pino-pretty",
|
|
5704
|
+
options: {
|
|
5705
|
+
colorize: true,
|
|
5706
|
+
translateTime: "HH:MM:ss",
|
|
5707
|
+
ignore: "pid,hostname,reqId",
|
|
5708
|
+
messageFormat: "{msg} {req.method} {req.url}"
|
|
5528
5709
|
}
|
|
5529
5710
|
}
|
|
5711
|
+
} : true;
|
|
5712
|
+
const app = Fastify({
|
|
5713
|
+
logger
|
|
5530
5714
|
});
|
|
5531
5715
|
const registry = new ProviderRegistry();
|
|
5532
5716
|
const providers = opts.config.providers ?? {};
|
|
@@ -5593,25 +5777,46 @@ async function createServer(opts) {
|
|
|
5593
5777
|
configured: !!registry.get(name),
|
|
5594
5778
|
quota: registry.get(name)?.config.quotaPolicy
|
|
5595
5779
|
}));
|
|
5780
|
+
const googleSettingsSummary = {
|
|
5781
|
+
configured: Boolean(opts.config.google?.clientId && opts.config.google?.clientSecret)
|
|
5782
|
+
};
|
|
5596
5783
|
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
5784
|
const googleStateSecret = process.env.GOOGLE_STATE_SECRET ?? crypto16.randomBytes(32).toString("hex");
|
|
5785
|
+
const googleConnectionStore = {
|
|
5786
|
+
listConnections: (domain) => listGoogleConnections(opts.config, domain),
|
|
5787
|
+
getConnection: (domain, connectionType) => getGoogleConnection(opts.config, domain, connectionType),
|
|
5788
|
+
upsertConnection: (connection) => {
|
|
5789
|
+
const updated = upsertGoogleConnection(opts.config, connection);
|
|
5790
|
+
saveConfig(opts.config);
|
|
5791
|
+
return updated;
|
|
5792
|
+
},
|
|
5793
|
+
updateConnection: (domain, connectionType, patch) => {
|
|
5794
|
+
const updated = patchGoogleConnection(opts.config, domain, connectionType, patch);
|
|
5795
|
+
if (updated) saveConfig(opts.config);
|
|
5796
|
+
return updated;
|
|
5797
|
+
},
|
|
5798
|
+
deleteConnection: (domain, connectionType) => {
|
|
5799
|
+
const removed = removeGoogleConnection(opts.config, domain, connectionType);
|
|
5800
|
+
if (removed) saveConfig(opts.config);
|
|
5801
|
+
return removed;
|
|
5802
|
+
}
|
|
5803
|
+
};
|
|
5600
5804
|
await app.register(apiRoutes, {
|
|
5601
5805
|
db: opts.db,
|
|
5602
5806
|
skipAuth: false,
|
|
5603
|
-
|
|
5604
|
-
|
|
5807
|
+
getGoogleAuthConfig: () => getGoogleAuthConfig(opts.config),
|
|
5808
|
+
googleConnectionStore,
|
|
5605
5809
|
googleStateSecret,
|
|
5810
|
+
publicUrl: opts.config.publicUrl,
|
|
5606
5811
|
onGscSyncRequested: (runId, projectId, syncOpts) => {
|
|
5812
|
+
const { clientId: googleClientId, clientSecret: googleClientSecret } = getGoogleAuthConfig(opts.config);
|
|
5607
5813
|
if (!googleClientId || !googleClientSecret) {
|
|
5608
|
-
app.log.error("GSC sync requested but
|
|
5814
|
+
app.log.error("GSC sync requested but Google OAuth credentials are not configured in the local config");
|
|
5609
5815
|
return;
|
|
5610
5816
|
}
|
|
5611
5817
|
executeGscSync(opts.db, runId, projectId, {
|
|
5612
5818
|
...syncOpts,
|
|
5613
|
-
|
|
5614
|
-
googleClientSecret
|
|
5819
|
+
config: opts.config
|
|
5615
5820
|
}).catch((err) => {
|
|
5616
5821
|
app.log.error({ runId, err }, "GSC sync failed");
|
|
5617
5822
|
});
|
|
@@ -5621,6 +5826,7 @@ async function createServer(opts) {
|
|
|
5621
5826
|
version: PKG_VERSION
|
|
5622
5827
|
},
|
|
5623
5828
|
providerSummary,
|
|
5829
|
+
googleSettingsSummary,
|
|
5624
5830
|
onRunCreated: (runId, projectId, providers2) => {
|
|
5625
5831
|
jobRunner.executeRun(runId, projectId, providers2).catch((err) => {
|
|
5626
5832
|
app.log.error({ runId, err }, "Job runner failed");
|
|
@@ -5665,6 +5871,17 @@ async function createServer(opts) {
|
|
|
5665
5871
|
quota
|
|
5666
5872
|
};
|
|
5667
5873
|
},
|
|
5874
|
+
onGoogleSettingsUpdate: (clientId, clientSecret) => {
|
|
5875
|
+
try {
|
|
5876
|
+
setGoogleAuthConfig(opts.config, { clientId, clientSecret });
|
|
5877
|
+
saveConfig(opts.config);
|
|
5878
|
+
googleSettingsSummary.configured = true;
|
|
5879
|
+
return { ...googleSettingsSummary };
|
|
5880
|
+
} catch (err) {
|
|
5881
|
+
app.log.error({ err }, "Failed to save Google OAuth config");
|
|
5882
|
+
return null;
|
|
5883
|
+
}
|
|
5884
|
+
},
|
|
5668
5885
|
onScheduleUpdated: (action, projectId) => {
|
|
5669
5886
|
if (action === "upsert") scheduler.upsert(projectId);
|
|
5670
5887
|
if (action === "delete") scheduler.remove(projectId);
|
|
@@ -5814,5 +6031,6 @@ export {
|
|
|
5814
6031
|
isFirstRun,
|
|
5815
6032
|
showFirstRunNotice,
|
|
5816
6033
|
trackEvent,
|
|
6034
|
+
setGoogleAuthConfig,
|
|
5817
6035
|
createServer
|
|
5818
6036
|
};
|