@ainyc/canonry 1.11.0 → 1.13.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 +1 -1
- package/assets/apple-touch-icon.png +0 -0
- package/assets/assets/index-B2IIQpXZ.js +243 -0
- package/assets/assets/index-MP6oQcCa.css +1 -0
- package/assets/favicon-32.png +0 -0
- package/assets/favicon.svg +24 -0
- package/assets/index.html +7 -3
- package/dist/{chunk-MVIL2UGM.js → chunk-65PXJTPN.js} +441 -32
- package/dist/cli.js +161 -7
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -1
- package/package.json +4 -4
- package/assets/assets/index-CVK9fWdD.css +0 -1
- package/assets/assets/index-DnEgRRTR.js +0 -243
|
@@ -151,7 +151,7 @@ function trackEvent(event, properties) {
|
|
|
151
151
|
|
|
152
152
|
// src/server.ts
|
|
153
153
|
import { createRequire as createRequire2 } from "module";
|
|
154
|
-
import
|
|
154
|
+
import crypto17 from "crypto";
|
|
155
155
|
import fs2 from "fs";
|
|
156
156
|
import path2 from "path";
|
|
157
157
|
import { fileURLToPath } from "url";
|
|
@@ -806,6 +806,25 @@ var gscUrlInspectionDtoSchema = z4.object({
|
|
|
806
806
|
inspectedAt: z4.string()
|
|
807
807
|
});
|
|
808
808
|
var indexTransitionSchema = z4.enum(["stable", "reindexed", "deindexed", "still-missing", "new"]);
|
|
809
|
+
var gscDeindexedRowSchema = z4.object({
|
|
810
|
+
url: z4.string(),
|
|
811
|
+
previousState: z4.string().nullable(),
|
|
812
|
+
currentState: z4.string().nullable(),
|
|
813
|
+
transitionDate: z4.string()
|
|
814
|
+
});
|
|
815
|
+
var gscCoverageSummaryDtoSchema = z4.object({
|
|
816
|
+
summary: z4.object({
|
|
817
|
+
total: z4.number(),
|
|
818
|
+
indexed: z4.number(),
|
|
819
|
+
notIndexed: z4.number(),
|
|
820
|
+
deindexed: z4.number(),
|
|
821
|
+
percentage: z4.number()
|
|
822
|
+
}),
|
|
823
|
+
lastInspectedAt: z4.string().nullable(),
|
|
824
|
+
indexed: z4.array(gscUrlInspectionDtoSchema).default([]),
|
|
825
|
+
notIndexed: z4.array(gscUrlInspectionDtoSchema).default([]),
|
|
826
|
+
deindexed: z4.array(gscDeindexedRowSchema).default([])
|
|
827
|
+
});
|
|
809
828
|
|
|
810
829
|
// ../contracts/src/project.ts
|
|
811
830
|
import { z as z5 } from "zod";
|
|
@@ -853,7 +872,7 @@ function effectiveDomains(project) {
|
|
|
853
872
|
// ../contracts/src/run.ts
|
|
854
873
|
import { z as z6 } from "zod";
|
|
855
874
|
var runStatusSchema = z6.enum(["queued", "running", "completed", "partial", "failed"]);
|
|
856
|
-
var runKindSchema = z6.enum(["answer-visibility", "site-audit", "gsc-sync"]);
|
|
875
|
+
var runKindSchema = z6.enum(["answer-visibility", "site-audit", "gsc-sync", "inspect-sitemap"]);
|
|
857
876
|
var runTriggerSchema = z6.enum(["manual", "scheduled", "config-apply"]);
|
|
858
877
|
var citationStateSchema = z6.enum(["cited", "not-cited"]);
|
|
859
878
|
var computedTransitionSchema = z6.enum(["new", "cited", "lost", "emerging", "not-cited"]);
|
|
@@ -1193,6 +1212,34 @@ async function keywordRoutes(app, opts) {
|
|
|
1193
1212
|
const rows = app.db.select().from(keywords).where(eq4(keywords.projectId, project.id)).all();
|
|
1194
1213
|
return reply.send(rows.map((r) => ({ id: r.id, keyword: r.keyword, createdAt: r.createdAt })));
|
|
1195
1214
|
});
|
|
1215
|
+
app.delete("/projects/:name/keywords", async (request, reply) => {
|
|
1216
|
+
const project = resolveProjectSafe(app, request.params.name, reply);
|
|
1217
|
+
if (!project) return;
|
|
1218
|
+
const body = request.body;
|
|
1219
|
+
if (!body || !Array.isArray(body.keywords) || body.keywords.length === 0) {
|
|
1220
|
+
const err = validationError('Body must contain a non-empty "keywords" array');
|
|
1221
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
1222
|
+
}
|
|
1223
|
+
const existing = app.db.select().from(keywords).where(eq4(keywords.projectId, project.id)).all();
|
|
1224
|
+
const toDelete = new Set(body.keywords);
|
|
1225
|
+
const idsToDelete = existing.filter((k) => toDelete.has(k.keyword)).map((k) => k.id);
|
|
1226
|
+
if (idsToDelete.length > 0) {
|
|
1227
|
+
app.db.transaction((tx) => {
|
|
1228
|
+
for (const id of idsToDelete) {
|
|
1229
|
+
tx.delete(keywords).where(eq4(keywords.id, id)).run();
|
|
1230
|
+
}
|
|
1231
|
+
writeAuditLog(tx, {
|
|
1232
|
+
projectId: project.id,
|
|
1233
|
+
actor: "api",
|
|
1234
|
+
action: "keywords.deleted",
|
|
1235
|
+
entityType: "keyword",
|
|
1236
|
+
diff: { deleted: body.keywords.filter((kw) => existing.some((e) => e.keyword === kw)) }
|
|
1237
|
+
});
|
|
1238
|
+
});
|
|
1239
|
+
}
|
|
1240
|
+
const rows = app.db.select().from(keywords).where(eq4(keywords.projectId, project.id)).all();
|
|
1241
|
+
return reply.send(rows.map((r) => ({ id: r.id, keyword: r.keyword, createdAt: r.createdAt })));
|
|
1242
|
+
});
|
|
1196
1243
|
app.post("/projects/:name/keywords", async (request, reply) => {
|
|
1197
1244
|
const project = resolveProjectSafe(app, request.params.name, reply);
|
|
1198
1245
|
if (!project) return;
|
|
@@ -3507,34 +3554,63 @@ async function googleRoutes(app, opts) {
|
|
|
3507
3554
|
const err = validationError("Google OAuth is not configured. Set Google OAuth credentials in the local Canonry config.");
|
|
3508
3555
|
return reply.status(err.statusCode).send(err.toJSON());
|
|
3509
3556
|
}
|
|
3510
|
-
const { type, propertyId } = request.body ?? {};
|
|
3557
|
+
const { type, propertyId, publicUrl } = request.body ?? {};
|
|
3511
3558
|
if (!type || type !== "gsc" && type !== "ga4") {
|
|
3512
3559
|
const err = validationError('type must be "gsc" or "ga4"');
|
|
3513
3560
|
return reply.status(err.statusCode).send(err.toJSON());
|
|
3514
3561
|
}
|
|
3515
3562
|
const project = resolveProject(app.db, request.params.name);
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3563
|
+
let redirectUri;
|
|
3564
|
+
if (publicUrl) {
|
|
3565
|
+
redirectUri = publicUrl.replace(/\/$/, "") + "/api/v1/google/callback";
|
|
3566
|
+
} else if (opts.publicUrl) {
|
|
3567
|
+
redirectUri = opts.publicUrl.replace(/\/$/, "") + "/api/v1/google/callback";
|
|
3568
|
+
} else {
|
|
3569
|
+
const proto = request.headers["x-forwarded-proto"] ?? "http";
|
|
3570
|
+
const host = request.headers.host ?? "localhost:4100";
|
|
3571
|
+
redirectUri = `${proto}://${host}/api/v1/projects/${encodeURIComponent(request.params.name)}/google/callback`;
|
|
3572
|
+
}
|
|
3519
3573
|
const scopes = type === "gsc" ? [GSC_SCOPE] : [];
|
|
3520
3574
|
const stateEncoded = buildSignedState(
|
|
3521
3575
|
{ domain: project.canonicalDomain, type, propertyId, redirectUri },
|
|
3522
3576
|
stateSecret
|
|
3523
3577
|
);
|
|
3524
3578
|
const authUrl = getAuthUrl(googleClientId, redirectUri, scopes, stateEncoded);
|
|
3525
|
-
return { authUrl };
|
|
3579
|
+
return { authUrl, redirectUri };
|
|
3526
3580
|
});
|
|
3527
|
-
|
|
3581
|
+
async function handleOAuthCallback(request, reply) {
|
|
3528
3582
|
const { clientId: googleClientId, clientSecret: googleClientSecret } = getAuthConfig();
|
|
3529
3583
|
if (!googleClientId || !googleClientSecret) {
|
|
3530
3584
|
return reply.status(500).send("Google OAuth not configured");
|
|
3531
3585
|
}
|
|
3532
3586
|
const store = requireConnectionStore(reply);
|
|
3533
3587
|
if (!store) return;
|
|
3588
|
+
const escapeHtml = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
3534
3589
|
const { code, state, error } = request.query;
|
|
3535
3590
|
if (error) {
|
|
3536
|
-
const safeError = String(error)
|
|
3537
|
-
|
|
3591
|
+
const safeError = escapeHtml(String(error));
|
|
3592
|
+
const errorHtml = error === "redirect_uri_mismatch" ? `<html><body style="font-family:system-ui;padding:40px;max-width:600px;margin:0 auto">
|
|
3593
|
+
<h2 style="color:#ef4444">Redirect URI mismatch</h2>
|
|
3594
|
+
<p>Google rejected the OAuth callback because the redirect URI is not registered.</p>
|
|
3595
|
+
<p><strong>To fix this:</strong></p>
|
|
3596
|
+
<ol>
|
|
3597
|
+
<li>Go to the <a href="https://console.cloud.google.com/apis/credentials" target="_blank">Google Cloud Console \u2192 Credentials</a></li>
|
|
3598
|
+
<li>Click your OAuth 2.0 Client ID</li>
|
|
3599
|
+
<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 ? (() => {
|
|
3600
|
+
try {
|
|
3601
|
+
const s = verifySignedState(request.query.state, stateSecret);
|
|
3602
|
+
return escapeHtml(String(s?.redirectUri ?? "Could not determine URI"));
|
|
3603
|
+
} catch {
|
|
3604
|
+
return "Could not determine URI";
|
|
3605
|
+
}
|
|
3606
|
+
})() : "Could not determine URI"}</code></li>
|
|
3607
|
+
<li>Click Save, then retry the connection</li>
|
|
3608
|
+
</ol>
|
|
3609
|
+
<p style="color:#888">You can close this tab.</p>
|
|
3610
|
+
</body></html>` : `<html><body style="font-family:system-ui;text-align:center;padding:60px">
|
|
3611
|
+
<h2>Authorization failed</h2><p>${safeError}</p><p style="color:#888">You can close this tab.</p>
|
|
3612
|
+
</body></html>`;
|
|
3613
|
+
return reply.type("text/html").send(errorHtml);
|
|
3538
3614
|
}
|
|
3539
3615
|
if (!code || !state) {
|
|
3540
3616
|
return reply.status(400).send("Missing code or state parameter");
|
|
@@ -3544,7 +3620,23 @@ async function googleRoutes(app, opts) {
|
|
|
3544
3620
|
return reply.status(400).send("Invalid or tampered state parameter");
|
|
3545
3621
|
}
|
|
3546
3622
|
const { domain, type, propertyId, redirectUri } = stateData;
|
|
3547
|
-
|
|
3623
|
+
let tokens;
|
|
3624
|
+
try {
|
|
3625
|
+
tokens = await exchangeCode(googleClientId, googleClientSecret, code, redirectUri);
|
|
3626
|
+
} catch (err) {
|
|
3627
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3628
|
+
return reply.type("text/html").send(
|
|
3629
|
+
`<html><body style="font-family:system-ui;padding:40px;max-width:600px;margin:0 auto">
|
|
3630
|
+
<h2 style="color:#ef4444">Token exchange failed</h2>
|
|
3631
|
+
<p>${escapeHtml(msg)}</p>
|
|
3632
|
+
<p><strong>Redirect URI used:</strong><br>
|
|
3633
|
+
<code style="background:#1e1e1e;color:#e0e0e0;padding:4px 8px;border-radius:4px">${escapeHtml(redirectUri)}</code>
|
|
3634
|
+
</p>
|
|
3635
|
+
<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>
|
|
3636
|
+
<p style="color:#888">You can close this tab.</p>
|
|
3637
|
+
</body></html>`
|
|
3638
|
+
);
|
|
3639
|
+
}
|
|
3548
3640
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3549
3641
|
const expiresAt = new Date(Date.now() + tokens.expires_in * 1e3).toISOString();
|
|
3550
3642
|
const existing = store.getConnection(domain, type);
|
|
@@ -3574,6 +3666,12 @@ async function googleRoutes(app, opts) {
|
|
|
3574
3666
|
<p style="color:#888">You can close this tab.</p>
|
|
3575
3667
|
</body></html>`
|
|
3576
3668
|
);
|
|
3669
|
+
}
|
|
3670
|
+
app.get("/google/callback", async (request, reply) => {
|
|
3671
|
+
return handleOAuthCallback(request, reply);
|
|
3672
|
+
});
|
|
3673
|
+
app.get("/projects/:name/google/callback", async (request, reply) => {
|
|
3674
|
+
return handleOAuthCallback(request, reply);
|
|
3577
3675
|
});
|
|
3578
3676
|
app.delete("/projects/:name/google/connections/:type", async (request, reply) => {
|
|
3579
3677
|
const store = requireConnectionStore(reply);
|
|
@@ -3752,7 +3850,7 @@ async function googleRoutes(app, opts) {
|
|
|
3752
3850
|
if (inspections.length < 2) continue;
|
|
3753
3851
|
const latest = inspections[0];
|
|
3754
3852
|
const previous = inspections[1];
|
|
3755
|
-
if (previous.indexingState
|
|
3853
|
+
if (previous.indexingState === "INDEXING_ALLOWED" && latest.indexingState !== "INDEXING_ALLOWED") {
|
|
3756
3854
|
deindexed.push({
|
|
3757
3855
|
url,
|
|
3758
3856
|
previousState: previous.indexingState,
|
|
@@ -3763,6 +3861,110 @@ async function googleRoutes(app, opts) {
|
|
|
3763
3861
|
}
|
|
3764
3862
|
return deindexed;
|
|
3765
3863
|
});
|
|
3864
|
+
app.get("/projects/:name/google/gsc/coverage", async (request) => {
|
|
3865
|
+
const project = resolveProject(app.db, request.params.name);
|
|
3866
|
+
const allInspections = app.db.select().from(gscUrlInspections).where(eq12(gscUrlInspections.projectId, project.id)).orderBy(desc2(gscUrlInspections.inspectedAt)).all();
|
|
3867
|
+
const latestByUrl = /* @__PURE__ */ new Map();
|
|
3868
|
+
const historyByUrl = /* @__PURE__ */ new Map();
|
|
3869
|
+
for (const row of allInspections) {
|
|
3870
|
+
if (!latestByUrl.has(row.url)) {
|
|
3871
|
+
latestByUrl.set(row.url, row);
|
|
3872
|
+
}
|
|
3873
|
+
const history = historyByUrl.get(row.url);
|
|
3874
|
+
if (history) {
|
|
3875
|
+
history.push(row);
|
|
3876
|
+
} else {
|
|
3877
|
+
historyByUrl.set(row.url, [row]);
|
|
3878
|
+
}
|
|
3879
|
+
}
|
|
3880
|
+
const indexedUrls = [];
|
|
3881
|
+
const notIndexedUrls = [];
|
|
3882
|
+
let lastInspectedAt = null;
|
|
3883
|
+
for (const [, row] of latestByUrl) {
|
|
3884
|
+
if (row.indexingState === "INDEXING_ALLOWED") {
|
|
3885
|
+
indexedUrls.push(row);
|
|
3886
|
+
} else {
|
|
3887
|
+
notIndexedUrls.push(row);
|
|
3888
|
+
}
|
|
3889
|
+
if (!lastInspectedAt || row.inspectedAt > lastInspectedAt) {
|
|
3890
|
+
lastInspectedAt = row.inspectedAt;
|
|
3891
|
+
}
|
|
3892
|
+
}
|
|
3893
|
+
const deindexedUrls = [];
|
|
3894
|
+
for (const [url, history] of historyByUrl) {
|
|
3895
|
+
if (history.length < 2) continue;
|
|
3896
|
+
const latest = history[0];
|
|
3897
|
+
const previous = history[1];
|
|
3898
|
+
if (previous.indexingState === "INDEXING_ALLOWED" && latest.indexingState !== "INDEXING_ALLOWED") {
|
|
3899
|
+
deindexedUrls.push({
|
|
3900
|
+
url,
|
|
3901
|
+
previousState: previous.indexingState,
|
|
3902
|
+
currentState: latest.indexingState,
|
|
3903
|
+
transitionDate: latest.inspectedAt
|
|
3904
|
+
});
|
|
3905
|
+
}
|
|
3906
|
+
}
|
|
3907
|
+
const total = latestByUrl.size;
|
|
3908
|
+
const indexed = indexedUrls.length;
|
|
3909
|
+
const notIndexed = notIndexedUrls.length;
|
|
3910
|
+
const formatRow = (r) => ({
|
|
3911
|
+
id: r.id,
|
|
3912
|
+
url: r.url,
|
|
3913
|
+
indexingState: r.indexingState,
|
|
3914
|
+
verdict: r.verdict,
|
|
3915
|
+
coverageState: r.coverageState,
|
|
3916
|
+
pageFetchState: r.pageFetchState,
|
|
3917
|
+
robotsTxtState: r.robotsTxtState,
|
|
3918
|
+
crawlTime: r.crawlTime,
|
|
3919
|
+
lastCrawlResult: r.lastCrawlResult,
|
|
3920
|
+
isMobileFriendly: r.isMobileFriendly === 1 ? true : r.isMobileFriendly === 0 ? false : null,
|
|
3921
|
+
richResults: JSON.parse(r.richResults),
|
|
3922
|
+
inspectedAt: r.inspectedAt
|
|
3923
|
+
});
|
|
3924
|
+
return {
|
|
3925
|
+
summary: {
|
|
3926
|
+
total,
|
|
3927
|
+
indexed,
|
|
3928
|
+
notIndexed,
|
|
3929
|
+
deindexed: deindexedUrls.length,
|
|
3930
|
+
percentage: total > 0 ? Math.round(indexed / total * 1e3) / 10 : 0
|
|
3931
|
+
},
|
|
3932
|
+
lastInspectedAt,
|
|
3933
|
+
indexed: indexedUrls.map(formatRow),
|
|
3934
|
+
notIndexed: notIndexedUrls.map(formatRow),
|
|
3935
|
+
deindexed: deindexedUrls
|
|
3936
|
+
};
|
|
3937
|
+
});
|
|
3938
|
+
app.post("/projects/:name/google/gsc/inspect-sitemap", async (request, reply) => {
|
|
3939
|
+
const store = requireConnectionStore(reply);
|
|
3940
|
+
if (!store) return;
|
|
3941
|
+
const project = resolveProject(app.db, request.params.name);
|
|
3942
|
+
const conn = store.getConnection(project.canonicalDomain, "gsc");
|
|
3943
|
+
if (!conn) {
|
|
3944
|
+
const err = validationError('No GSC connection found for this domain. Run "canonry google connect" first.');
|
|
3945
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
3946
|
+
}
|
|
3947
|
+
if (!conn.propertyId) {
|
|
3948
|
+
const err = validationError("No GSC property configured for this connection");
|
|
3949
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
3950
|
+
}
|
|
3951
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3952
|
+
const runId = crypto12.randomUUID();
|
|
3953
|
+
app.db.insert(runs).values({
|
|
3954
|
+
id: runId,
|
|
3955
|
+
projectId: project.id,
|
|
3956
|
+
kind: "inspect-sitemap",
|
|
3957
|
+
status: "queued",
|
|
3958
|
+
trigger: "manual",
|
|
3959
|
+
createdAt: now
|
|
3960
|
+
}).run();
|
|
3961
|
+
const { sitemapUrl } = request.body ?? {};
|
|
3962
|
+
if (opts.onInspectSitemapRequested) {
|
|
3963
|
+
opts.onInspectSitemapRequested(runId, project.id, { sitemapUrl: sitemapUrl ?? void 0 });
|
|
3964
|
+
}
|
|
3965
|
+
const run = app.db.select().from(runs).where(eq12(runs.id, runId)).get();
|
|
3966
|
+
return run;
|
|
3967
|
+
});
|
|
3766
3968
|
app.put("/projects/:name/google/connections/:type/property", async (request, reply) => {
|
|
3767
3969
|
const store = requireConnectionStore(reply);
|
|
3768
3970
|
if (!store) return;
|
|
@@ -3829,7 +4031,9 @@ async function apiRoutes(app, opts) {
|
|
|
3829
4031
|
getGoogleAuthConfig: opts.getGoogleAuthConfig,
|
|
3830
4032
|
googleConnectionStore: opts.googleConnectionStore,
|
|
3831
4033
|
googleStateSecret: opts.googleStateSecret,
|
|
3832
|
-
|
|
4034
|
+
publicUrl: opts.publicUrl,
|
|
4035
|
+
onGscSyncRequested: opts.onGscSyncRequested,
|
|
4036
|
+
onInspectSitemapRequested: opts.onInspectSitemapRequested
|
|
3833
4037
|
});
|
|
3834
4038
|
}, { prefix: "/api/v1" });
|
|
3835
4039
|
}
|
|
@@ -5213,6 +5417,161 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
5213
5417
|
}
|
|
5214
5418
|
}
|
|
5215
5419
|
|
|
5420
|
+
// src/gsc-inspect-sitemap.ts
|
|
5421
|
+
import crypto15 from "crypto";
|
|
5422
|
+
import { eq as eq15 } from "drizzle-orm";
|
|
5423
|
+
|
|
5424
|
+
// src/sitemap-parser.ts
|
|
5425
|
+
var LOC_REGEX = /<loc>\s*([^<]+?)\s*<\/loc>/gi;
|
|
5426
|
+
var SITEMAP_TAG_REGEX = /<sitemap>[\s\S]*?<\/sitemap>/gi;
|
|
5427
|
+
var PRIVATE_IP_PATTERNS = [
|
|
5428
|
+
/^169\.254\./,
|
|
5429
|
+
// link-local (AWS metadata endpoint etc.)
|
|
5430
|
+
/^10\./,
|
|
5431
|
+
// private class A
|
|
5432
|
+
/^172\.(1[6-9]|2\d|3[01])\./,
|
|
5433
|
+
// private class B
|
|
5434
|
+
/^192\.168\./
|
|
5435
|
+
// private class C
|
|
5436
|
+
];
|
|
5437
|
+
function validateSitemapUrl(url) {
|
|
5438
|
+
let parsed;
|
|
5439
|
+
try {
|
|
5440
|
+
parsed = new URL(url);
|
|
5441
|
+
} catch {
|
|
5442
|
+
throw new Error(`Invalid sitemap URL: ${url}`);
|
|
5443
|
+
}
|
|
5444
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
5445
|
+
throw new Error(`Sitemap URL must use http or https protocol: ${url}`);
|
|
5446
|
+
}
|
|
5447
|
+
const host = parsed.hostname.toLowerCase();
|
|
5448
|
+
for (const pattern of PRIVATE_IP_PATTERNS) {
|
|
5449
|
+
if (pattern.test(host)) {
|
|
5450
|
+
throw new Error(`Sitemap URL points to a private or reserved IP range: ${url}`);
|
|
5451
|
+
}
|
|
5452
|
+
}
|
|
5453
|
+
}
|
|
5454
|
+
async function fetchAndParseSitemap(sitemapUrl) {
|
|
5455
|
+
const urls = /* @__PURE__ */ new Set();
|
|
5456
|
+
await parseSitemapRecursive(sitemapUrl, urls, 0);
|
|
5457
|
+
return [...urls];
|
|
5458
|
+
}
|
|
5459
|
+
async function parseSitemapRecursive(url, urls, depth) {
|
|
5460
|
+
if (depth > 3) return;
|
|
5461
|
+
validateSitemapUrl(url);
|
|
5462
|
+
const res = await fetch(url);
|
|
5463
|
+
if (!res.ok) {
|
|
5464
|
+
throw new Error(`Failed to fetch sitemap at ${url}: ${res.status} ${res.statusText}`);
|
|
5465
|
+
}
|
|
5466
|
+
const xml = await res.text();
|
|
5467
|
+
const sitemapEntries = xml.match(SITEMAP_TAG_REGEX);
|
|
5468
|
+
if (sitemapEntries) {
|
|
5469
|
+
for (const entry of sitemapEntries) {
|
|
5470
|
+
const locMatch = LOC_REGEX.exec(entry);
|
|
5471
|
+
LOC_REGEX.lastIndex = 0;
|
|
5472
|
+
if (locMatch?.[1]) {
|
|
5473
|
+
await parseSitemapRecursive(locMatch[1], urls, depth + 1);
|
|
5474
|
+
}
|
|
5475
|
+
}
|
|
5476
|
+
return;
|
|
5477
|
+
}
|
|
5478
|
+
let match;
|
|
5479
|
+
while ((match = LOC_REGEX.exec(xml)) !== null) {
|
|
5480
|
+
if (match[1]) {
|
|
5481
|
+
urls.add(match[1]);
|
|
5482
|
+
}
|
|
5483
|
+
}
|
|
5484
|
+
LOC_REGEX.lastIndex = 0;
|
|
5485
|
+
}
|
|
5486
|
+
|
|
5487
|
+
// src/gsc-inspect-sitemap.ts
|
|
5488
|
+
async function executeInspectSitemap(db, runId, projectId, opts) {
|
|
5489
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5490
|
+
db.update(runs).set({ status: "running", startedAt: now }).where(eq15(runs.id, runId)).run();
|
|
5491
|
+
try {
|
|
5492
|
+
const { clientId: googleClientId, clientSecret: googleClientSecret } = getGoogleAuthConfig(opts.config);
|
|
5493
|
+
if (!googleClientId || !googleClientSecret) {
|
|
5494
|
+
throw new Error("Google OAuth is not configured in the local Canonry config");
|
|
5495
|
+
}
|
|
5496
|
+
const project = db.select().from(projects).where(eq15(projects.id, projectId)).get();
|
|
5497
|
+
if (!project) {
|
|
5498
|
+
throw new Error(`Project not found: ${projectId}`);
|
|
5499
|
+
}
|
|
5500
|
+
const conn = getGoogleConnection(opts.config, project.canonicalDomain, "gsc");
|
|
5501
|
+
if (!conn || !conn.refreshToken) {
|
|
5502
|
+
throw new Error("No GSC connection found or connection is incomplete");
|
|
5503
|
+
}
|
|
5504
|
+
if (!conn.propertyId) {
|
|
5505
|
+
throw new Error('No GSC property selected. Use "canonry google properties" to list available sites, then set one.');
|
|
5506
|
+
}
|
|
5507
|
+
let accessToken = conn.accessToken;
|
|
5508
|
+
const expiresAt = conn.tokenExpiresAt ? new Date(conn.tokenExpiresAt).getTime() : 0;
|
|
5509
|
+
if (Date.now() > expiresAt - 5 * 60 * 1e3) {
|
|
5510
|
+
const tokens = await refreshAccessToken(googleClientId, googleClientSecret, conn.refreshToken);
|
|
5511
|
+
accessToken = tokens.access_token;
|
|
5512
|
+
patchGoogleConnection(opts.config, project.canonicalDomain, "gsc", {
|
|
5513
|
+
accessToken: tokens.access_token,
|
|
5514
|
+
tokenExpiresAt: new Date(Date.now() + tokens.expires_in * 1e3).toISOString(),
|
|
5515
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5516
|
+
});
|
|
5517
|
+
saveConfig(opts.config);
|
|
5518
|
+
}
|
|
5519
|
+
const sitemapUrl = opts.sitemapUrl || `https://${project.canonicalDomain}/sitemap.xml`;
|
|
5520
|
+
console.log(`[Inspect Sitemap] Fetching sitemap from ${sitemapUrl}`);
|
|
5521
|
+
const urls = await fetchAndParseSitemap(sitemapUrl);
|
|
5522
|
+
console.log(`[Inspect Sitemap] Found ${urls.length} URLs in sitemap`);
|
|
5523
|
+
if (urls.length === 0) {
|
|
5524
|
+
throw new Error("No URLs found in sitemap");
|
|
5525
|
+
}
|
|
5526
|
+
let inspected = 0;
|
|
5527
|
+
let errors = 0;
|
|
5528
|
+
for (const pageUrl of urls) {
|
|
5529
|
+
try {
|
|
5530
|
+
const result = await inspectUrl(accessToken, pageUrl, conn.propertyId);
|
|
5531
|
+
const ir = result.inspectionResult;
|
|
5532
|
+
const idx = ir.indexStatusResult;
|
|
5533
|
+
const mob = ir.mobileUsabilityResult;
|
|
5534
|
+
const rich = ir.richResultsResult;
|
|
5535
|
+
const inspectedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5536
|
+
db.insert(gscUrlInspections).values({
|
|
5537
|
+
id: crypto15.randomUUID(),
|
|
5538
|
+
projectId,
|
|
5539
|
+
syncRunId: runId,
|
|
5540
|
+
url: pageUrl,
|
|
5541
|
+
indexingState: idx?.indexingState ?? null,
|
|
5542
|
+
verdict: idx?.verdict ?? null,
|
|
5543
|
+
coverageState: idx?.coverageState ?? null,
|
|
5544
|
+
pageFetchState: idx?.pageFetchState ?? null,
|
|
5545
|
+
robotsTxtState: idx?.robotsTxtState ?? null,
|
|
5546
|
+
crawlTime: idx?.lastCrawlTime ?? null,
|
|
5547
|
+
lastCrawlResult: idx?.crawlResult ?? null,
|
|
5548
|
+
isMobileFriendly: mob?.verdict === "PASS" ? 1 : mob?.verdict === "FAIL" ? 0 : null,
|
|
5549
|
+
richResults: JSON.stringify(rich?.detectedItems?.map((d) => d.richResultType) ?? []),
|
|
5550
|
+
referringUrls: JSON.stringify(idx?.referringUrls ?? []),
|
|
5551
|
+
inspectedAt,
|
|
5552
|
+
createdAt: inspectedAt
|
|
5553
|
+
}).run();
|
|
5554
|
+
inspected++;
|
|
5555
|
+
console.log(`[Inspect Sitemap] ${inspected}/${urls.length} inspected: ${pageUrl}`);
|
|
5556
|
+
} catch (err) {
|
|
5557
|
+
errors++;
|
|
5558
|
+
console.error(`[Inspect Sitemap] Failed to inspect ${pageUrl}:`, err instanceof Error ? err.message : err);
|
|
5559
|
+
}
|
|
5560
|
+
if (inspected + errors < urls.length) {
|
|
5561
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
5562
|
+
}
|
|
5563
|
+
}
|
|
5564
|
+
const status = errors > 0 && inspected > 0 ? "partial" : errors === urls.length ? "failed" : "completed";
|
|
5565
|
+
db.update(runs).set({ status, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq15(runs.id, runId)).run();
|
|
5566
|
+
console.log(`[Inspect Sitemap] Done. ${inspected} inspected, ${errors} errors out of ${urls.length} URLs.`);
|
|
5567
|
+
} catch (err) {
|
|
5568
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
5569
|
+
db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq15(runs.id, runId)).run();
|
|
5570
|
+
console.error(`[Inspect Sitemap] Failed:`, errorMsg);
|
|
5571
|
+
throw err;
|
|
5572
|
+
}
|
|
5573
|
+
}
|
|
5574
|
+
|
|
5216
5575
|
// src/provider-registry.ts
|
|
5217
5576
|
var ProviderRegistry = class {
|
|
5218
5577
|
providers = /* @__PURE__ */ new Map();
|
|
@@ -5258,7 +5617,7 @@ var ProviderRegistry = class {
|
|
|
5258
5617
|
|
|
5259
5618
|
// src/scheduler.ts
|
|
5260
5619
|
import cron from "node-cron";
|
|
5261
|
-
import { eq as
|
|
5620
|
+
import { eq as eq16 } from "drizzle-orm";
|
|
5262
5621
|
var Scheduler = class {
|
|
5263
5622
|
db;
|
|
5264
5623
|
callbacks;
|
|
@@ -5269,7 +5628,7 @@ var Scheduler = class {
|
|
|
5269
5628
|
}
|
|
5270
5629
|
/** Load all enabled schedules from DB and register cron jobs. */
|
|
5271
5630
|
start() {
|
|
5272
|
-
const allSchedules = this.db.select().from(schedules).where(
|
|
5631
|
+
const allSchedules = this.db.select().from(schedules).where(eq16(schedules.enabled, 1)).all();
|
|
5273
5632
|
for (const schedule of allSchedules) {
|
|
5274
5633
|
const missedRunAt = schedule.nextRunAt;
|
|
5275
5634
|
this.registerCronTask(schedule);
|
|
@@ -5294,7 +5653,7 @@ var Scheduler = class {
|
|
|
5294
5653
|
this.stopTask(projectId, existing, "Stopped");
|
|
5295
5654
|
this.tasks.delete(projectId);
|
|
5296
5655
|
}
|
|
5297
|
-
const schedule = this.db.select().from(schedules).where(
|
|
5656
|
+
const schedule = this.db.select().from(schedules).where(eq16(schedules.projectId, projectId)).get();
|
|
5298
5657
|
if (schedule && schedule.enabled === 1) {
|
|
5299
5658
|
this.registerCronTask(schedule);
|
|
5300
5659
|
}
|
|
@@ -5327,13 +5686,13 @@ var Scheduler = class {
|
|
|
5327
5686
|
this.db.update(schedules).set({
|
|
5328
5687
|
nextRunAt: task.getNextRun()?.toISOString() ?? null,
|
|
5329
5688
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5330
|
-
}).where(
|
|
5689
|
+
}).where(eq16(schedules.id, scheduleId)).run();
|
|
5331
5690
|
const label = schedule.preset ?? cronExpr;
|
|
5332
5691
|
console.log(`[Scheduler] Registered "${label}" (${timezone}) for project ${projectId}`);
|
|
5333
5692
|
}
|
|
5334
5693
|
triggerRun(scheduleId, projectId) {
|
|
5335
5694
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5336
|
-
const currentSchedule = this.db.select().from(schedules).where(
|
|
5695
|
+
const currentSchedule = this.db.select().from(schedules).where(eq16(schedules.id, scheduleId)).get();
|
|
5337
5696
|
if (!currentSchedule || currentSchedule.enabled !== 1) {
|
|
5338
5697
|
console.log(`[Scheduler] Schedule ${scheduleId} no longer exists or is disabled, removing task for project ${projectId}`);
|
|
5339
5698
|
this.remove(projectId);
|
|
@@ -5341,7 +5700,7 @@ var Scheduler = class {
|
|
|
5341
5700
|
}
|
|
5342
5701
|
const task = this.tasks.get(projectId);
|
|
5343
5702
|
const nextRunAt = task?.getNextRun()?.toISOString() ?? null;
|
|
5344
|
-
const project = this.db.select().from(projects).where(
|
|
5703
|
+
const project = this.db.select().from(projects).where(eq16(projects.id, projectId)).get();
|
|
5345
5704
|
if (!project) {
|
|
5346
5705
|
console.error(`[Scheduler] Project ${projectId} not found, skipping scheduled run`);
|
|
5347
5706
|
this.remove(projectId);
|
|
@@ -5358,7 +5717,7 @@ var Scheduler = class {
|
|
|
5358
5717
|
this.db.update(schedules).set({
|
|
5359
5718
|
nextRunAt,
|
|
5360
5719
|
updatedAt: now
|
|
5361
|
-
}).where(
|
|
5720
|
+
}).where(eq16(schedules.id, currentSchedule.id)).run();
|
|
5362
5721
|
return;
|
|
5363
5722
|
}
|
|
5364
5723
|
const runId = queueResult.runId;
|
|
@@ -5366,7 +5725,7 @@ var Scheduler = class {
|
|
|
5366
5725
|
lastRunAt: now,
|
|
5367
5726
|
nextRunAt,
|
|
5368
5727
|
updatedAt: now
|
|
5369
|
-
}).where(
|
|
5728
|
+
}).where(eq16(schedules.id, currentSchedule.id)).run();
|
|
5370
5729
|
const scheduleProviders = JSON.parse(currentSchedule.providers);
|
|
5371
5730
|
const providers = scheduleProviders.length > 0 ? scheduleProviders : void 0;
|
|
5372
5731
|
console.log(`[Scheduler] Triggered scheduled run ${runId} for project ${project.name}`);
|
|
@@ -5375,8 +5734,8 @@ var Scheduler = class {
|
|
|
5375
5734
|
};
|
|
5376
5735
|
|
|
5377
5736
|
// src/notifier.ts
|
|
5378
|
-
import { eq as
|
|
5379
|
-
import
|
|
5737
|
+
import { eq as eq17, desc as desc3, and as and5, or as or2 } from "drizzle-orm";
|
|
5738
|
+
import crypto16 from "crypto";
|
|
5380
5739
|
var Notifier = class {
|
|
5381
5740
|
db;
|
|
5382
5741
|
serverUrl;
|
|
@@ -5387,18 +5746,18 @@ var Notifier = class {
|
|
|
5387
5746
|
/** Called after a run completes (success, partial, or failed). */
|
|
5388
5747
|
async onRunCompleted(runId, projectId) {
|
|
5389
5748
|
console.log(`[Notifier] onRunCompleted: runId=${runId} projectId=${projectId}`);
|
|
5390
|
-
const notifs = this.db.select().from(notifications).where(
|
|
5749
|
+
const notifs = this.db.select().from(notifications).where(eq17(notifications.projectId, projectId)).all().filter((n) => n.enabled === 1);
|
|
5391
5750
|
if (notifs.length === 0) {
|
|
5392
5751
|
console.log(`[Notifier] No enabled notifications for project ${projectId} \u2014 skipping`);
|
|
5393
5752
|
return;
|
|
5394
5753
|
}
|
|
5395
5754
|
console.log(`[Notifier] Found ${notifs.length} enabled notification(s) for project ${projectId}`);
|
|
5396
|
-
const run = this.db.select().from(runs).where(
|
|
5755
|
+
const run = this.db.select().from(runs).where(eq17(runs.id, runId)).get();
|
|
5397
5756
|
if (!run) {
|
|
5398
5757
|
console.error(`[Notifier] Run ${runId} not found \u2014 skipping notification dispatch`);
|
|
5399
5758
|
return;
|
|
5400
5759
|
}
|
|
5401
|
-
const project = this.db.select().from(projects).where(
|
|
5760
|
+
const project = this.db.select().from(projects).where(eq17(projects.id, projectId)).get();
|
|
5402
5761
|
if (!project) {
|
|
5403
5762
|
console.error(`[Notifier] Project ${projectId} not found \u2014 skipping notification dispatch`);
|
|
5404
5763
|
return;
|
|
@@ -5439,8 +5798,8 @@ var Notifier = class {
|
|
|
5439
5798
|
computeTransitions(runId, projectId) {
|
|
5440
5799
|
const recentRuns = this.db.select().from(runs).where(
|
|
5441
5800
|
and5(
|
|
5442
|
-
|
|
5443
|
-
or2(
|
|
5801
|
+
eq17(runs.projectId, projectId),
|
|
5802
|
+
or2(eq17(runs.status, "completed"), eq17(runs.status, "partial"))
|
|
5444
5803
|
)
|
|
5445
5804
|
).orderBy(desc3(runs.createdAt)).limit(2).all();
|
|
5446
5805
|
if (recentRuns.length < 2) return [];
|
|
@@ -5452,12 +5811,12 @@ var Notifier = class {
|
|
|
5452
5811
|
keyword: keywords.keyword,
|
|
5453
5812
|
provider: querySnapshots.provider,
|
|
5454
5813
|
citationState: querySnapshots.citationState
|
|
5455
|
-
}).from(querySnapshots).leftJoin(keywords,
|
|
5814
|
+
}).from(querySnapshots).leftJoin(keywords, eq17(querySnapshots.keywordId, keywords.id)).where(eq17(querySnapshots.runId, currentRunId)).all();
|
|
5456
5815
|
const previousSnapshots = this.db.select({
|
|
5457
5816
|
keywordId: querySnapshots.keywordId,
|
|
5458
5817
|
provider: querySnapshots.provider,
|
|
5459
5818
|
citationState: querySnapshots.citationState
|
|
5460
|
-
}).from(querySnapshots).where(
|
|
5819
|
+
}).from(querySnapshots).where(eq17(querySnapshots.runId, previousRunId)).all();
|
|
5461
5820
|
const prevMap = /* @__PURE__ */ new Map();
|
|
5462
5821
|
for (const s of previousSnapshots) {
|
|
5463
5822
|
prevMap.set(`${s.keywordId}:${s.provider}`, s.citationState);
|
|
@@ -5514,7 +5873,7 @@ var Notifier = class {
|
|
|
5514
5873
|
}
|
|
5515
5874
|
logDelivery(projectId, notificationId, event, status, error) {
|
|
5516
5875
|
this.db.insert(auditLog).values({
|
|
5517
|
-
id:
|
|
5876
|
+
id: crypto16.randomUUID(),
|
|
5518
5877
|
projectId,
|
|
5519
5878
|
actor: "scheduler",
|
|
5520
5879
|
action: `notification.${status}`,
|
|
@@ -5645,6 +6004,14 @@ var DEFAULT_QUOTA = {
|
|
|
5645
6004
|
maxRequestsPerMinute: 10,
|
|
5646
6005
|
maxRequestsPerDay: 1e3
|
|
5647
6006
|
};
|
|
6007
|
+
function summarizeProviderConfig(provider, config) {
|
|
6008
|
+
return {
|
|
6009
|
+
configured: Boolean(config?.apiKey || config?.baseUrl),
|
|
6010
|
+
model: config?.model ?? null,
|
|
6011
|
+
baseUrl: provider === "local" ? config?.baseUrl ?? null : null,
|
|
6012
|
+
quota: { ...config?.quota ?? DEFAULT_QUOTA }
|
|
6013
|
+
};
|
|
6014
|
+
}
|
|
5648
6015
|
async function createServer(opts) {
|
|
5649
6016
|
const logger = opts.logger === false ? false : process.stdout.isTTY ? {
|
|
5650
6017
|
transport: {
|
|
@@ -5729,7 +6096,7 @@ async function createServer(opts) {
|
|
|
5729
6096
|
configured: Boolean(opts.config.google?.clientId && opts.config.google?.clientSecret)
|
|
5730
6097
|
};
|
|
5731
6098
|
const adapterMap = { gemini: geminiAdapter, openai: openaiAdapter, claude: claudeAdapter, local: localAdapter };
|
|
5732
|
-
const googleStateSecret = process.env.GOOGLE_STATE_SECRET ??
|
|
6099
|
+
const googleStateSecret = process.env.GOOGLE_STATE_SECRET ?? crypto17.randomBytes(32).toString("hex");
|
|
5733
6100
|
const googleConnectionStore = {
|
|
5734
6101
|
listConnections: (domain) => listGoogleConnections(opts.config, domain),
|
|
5735
6102
|
getConnection: (domain, connectionType) => getGoogleConnection(opts.config, domain, connectionType),
|
|
@@ -5755,6 +6122,7 @@ async function createServer(opts) {
|
|
|
5755
6122
|
getGoogleAuthConfig: () => getGoogleAuthConfig(opts.config),
|
|
5756
6123
|
googleConnectionStore,
|
|
5757
6124
|
googleStateSecret,
|
|
6125
|
+
publicUrl: opts.config.publicUrl,
|
|
5758
6126
|
onGscSyncRequested: (runId, projectId, syncOpts) => {
|
|
5759
6127
|
const { clientId: googleClientId, clientSecret: googleClientSecret } = getGoogleAuthConfig(opts.config);
|
|
5760
6128
|
if (!googleClientId || !googleClientSecret) {
|
|
@@ -5768,6 +6136,19 @@ async function createServer(opts) {
|
|
|
5768
6136
|
app.log.error({ runId, err }, "GSC sync failed");
|
|
5769
6137
|
});
|
|
5770
6138
|
},
|
|
6139
|
+
onInspectSitemapRequested: (runId, projectId, inspectOpts) => {
|
|
6140
|
+
const { clientId: googleClientId, clientSecret: googleClientSecret } = getGoogleAuthConfig(opts.config);
|
|
6141
|
+
if (!googleClientId || !googleClientSecret) {
|
|
6142
|
+
app.log.error("Inspect sitemap requested but Google OAuth credentials are not configured");
|
|
6143
|
+
return;
|
|
6144
|
+
}
|
|
6145
|
+
executeInspectSitemap(opts.db, runId, projectId, {
|
|
6146
|
+
...inspectOpts,
|
|
6147
|
+
config: opts.config
|
|
6148
|
+
}).catch((err) => {
|
|
6149
|
+
app.log.error({ runId, err }, "Inspect sitemap failed");
|
|
6150
|
+
});
|
|
6151
|
+
},
|
|
5771
6152
|
openApiInfo: {
|
|
5772
6153
|
title: "Canonry API",
|
|
5773
6154
|
version: PKG_VERSION
|
|
@@ -5784,6 +6165,7 @@ async function createServer(opts) {
|
|
|
5784
6165
|
if (!(name in adapterMap)) return null;
|
|
5785
6166
|
if (!opts.config.providers) opts.config.providers = {};
|
|
5786
6167
|
const existing = opts.config.providers[name];
|
|
6168
|
+
const beforeConfig = summarizeProviderConfig(name, existing);
|
|
5787
6169
|
const mergedQuota = incomingQuota ? { ...existing?.quota ?? DEFAULT_QUOTA, ...incomingQuota } : existing?.quota;
|
|
5788
6170
|
opts.config.providers[name] = {
|
|
5789
6171
|
apiKey: apiKey || existing?.apiKey,
|
|
@@ -5811,6 +6193,33 @@ async function createServer(opts) {
|
|
|
5811
6193
|
entry.model = model || registry.get(name)?.config.model;
|
|
5812
6194
|
entry.quota = quota;
|
|
5813
6195
|
}
|
|
6196
|
+
const afterConfig = summarizeProviderConfig(name, opts.config.providers[name]);
|
|
6197
|
+
if (JSON.stringify(beforeConfig) !== JSON.stringify(afterConfig)) {
|
|
6198
|
+
const diff = JSON.stringify({
|
|
6199
|
+
before: existing ? beforeConfig : null,
|
|
6200
|
+
after: afterConfig
|
|
6201
|
+
});
|
|
6202
|
+
const affectedProjectIds = opts.db.select({ id: projects.id, providers: projects.providers }).from(projects).all().filter((project) => {
|
|
6203
|
+
try {
|
|
6204
|
+
const configuredProviders = JSON.parse(project.providers || "[]");
|
|
6205
|
+
return configuredProviders.length === 0 || configuredProviders.includes(name);
|
|
6206
|
+
} catch {
|
|
6207
|
+
return false;
|
|
6208
|
+
}
|
|
6209
|
+
}).map((project) => project.id);
|
|
6210
|
+
const targetProjectIds = affectedProjectIds.length > 0 ? affectedProjectIds : [null];
|
|
6211
|
+
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6212
|
+
opts.db.insert(auditLog).values(targetProjectIds.map((projectId) => ({
|
|
6213
|
+
id: crypto17.randomUUID(),
|
|
6214
|
+
projectId,
|
|
6215
|
+
actor: "api",
|
|
6216
|
+
action: existing ? "provider.updated" : "provider.created",
|
|
6217
|
+
entityType: "provider",
|
|
6218
|
+
entityId: name,
|
|
6219
|
+
diff,
|
|
6220
|
+
createdAt
|
|
6221
|
+
}))).run();
|
|
6222
|
+
}
|
|
5814
6223
|
return {
|
|
5815
6224
|
name,
|
|
5816
6225
|
model: entry?.model,
|