@ainyc/canonry 1.27.2 → 1.28.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.
@@ -19,6 +19,15 @@ function normalizeGoogleConfig(config) {
19
19
  scopes: connection.scopes ?? []
20
20
  }));
21
21
  }
22
+ function normalizeWordpressConfig(config) {
23
+ if (!config.wordpress) return;
24
+ config.wordpress.connections = (config.wordpress.connections ?? []).map((connection) => ({
25
+ ...connection,
26
+ url: connection.url.replace(/\/$/, ""),
27
+ stagingUrl: connection.stagingUrl?.replace(/\/$/, ""),
28
+ defaultEnv: connection.defaultEnv ?? "live"
29
+ }));
30
+ }
22
31
  function getConfigDir() {
23
32
  const override = process.env.CANONRY_CONFIG_DIR?.trim();
24
33
  if (override) {
@@ -63,6 +72,7 @@ Do not write config.yaml by hand; use "canonry init", "canonry settings", or "ca
63
72
  };
64
73
  }
65
74
  normalizeGoogleConfig(parsed);
75
+ normalizeWordpressConfig(parsed);
66
76
  const portOverride = process.env.CANONRY_PORT?.trim();
67
77
  if (portOverride) {
68
78
  try {
@@ -125,6 +135,36 @@ function saveConfig(config) {
125
135
  const yaml = stringify(merged);
126
136
  fs.writeFileSync(configPath, yaml, { encoding: "utf-8", mode: 384 });
127
137
  }
138
+ function saveConfigPatch(patch) {
139
+ const configDir = getConfigDir();
140
+ if (!fs.existsSync(configDir)) {
141
+ fs.mkdirSync(configDir, { recursive: true });
142
+ }
143
+ const configPath = getConfigPath();
144
+ let base = {};
145
+ if (fs.existsSync(configPath)) {
146
+ try {
147
+ const raw = fs.readFileSync(configPath, "utf-8");
148
+ base = parse(raw) ?? {};
149
+ } catch {
150
+ base = {};
151
+ }
152
+ }
153
+ const merged = { ...base, ...patch };
154
+ if (base.database) merged.database = base.database;
155
+ if (base.apiKey) merged.apiKey = base.apiKey;
156
+ if (base.anonymousId) merged.anonymousId = base.anonymousId;
157
+ if (base.dashboardPasswordHash) merged.dashboardPasswordHash = base.dashboardPasswordHash;
158
+ if (base.providers && patch.providers) {
159
+ merged.providers = { ...base.providers };
160
+ for (const [key, patchEntry] of Object.entries(patch.providers)) {
161
+ const baseEntry = base.providers[key] ?? {};
162
+ merged.providers[key] = { ...baseEntry, ...patchEntry };
163
+ }
164
+ }
165
+ const yaml = stringify(merged);
166
+ fs.writeFileSync(configPath, yaml, { encoding: "utf-8", mode: 384 });
167
+ }
128
168
  function configExists() {
129
169
  return fs.existsSync(getConfigPath());
130
170
  }
@@ -155,7 +195,7 @@ function getOrCreateAnonymousId() {
155
195
  if (config.anonymousId) return config.anonymousId;
156
196
  const id = crypto.randomUUID();
157
197
  config.anonymousId = id;
158
- saveConfig(config);
198
+ saveConfigPatch(config);
159
199
  return id;
160
200
  } catch {
161
201
  return void 0;
@@ -203,7 +243,7 @@ function trackEvent(event, properties) {
203
243
 
204
244
  // src/server.ts
205
245
  import { createRequire as createRequire2 } from "module";
206
- import crypto21 from "crypto";
246
+ import crypto22 from "crypto";
207
247
  import fs5 from "fs";
208
248
  import path6 from "path";
209
249
  import { fileURLToPath } from "url";
@@ -439,6 +479,9 @@ function authRequired() {
439
479
  function authInvalid() {
440
480
  return new AppError("AUTH_INVALID", "Invalid API key", 401);
441
481
  }
482
+ function providerError(message, details) {
483
+ return new AppError("PROVIDER_ERROR", message, 502, details);
484
+ }
442
485
  function runInProgress(projectName) {
443
486
  return new AppError("RUN_IN_PROGRESS", `A run is already in progress for '${projectName}'`, 409);
444
487
  }
@@ -584,78 +627,179 @@ var bingSubmitResultDtoSchema = z6.object({
584
627
  error: z6.string().optional()
585
628
  });
586
629
 
587
- // ../contracts/src/run.ts
630
+ // ../contracts/src/wordpress.ts
588
631
  import { z as z7 } from "zod";
589
- var runStatusSchema = z7.enum(["queued", "running", "completed", "partial", "failed", "cancelled"]);
590
- var runKindSchema = z7.enum(["answer-visibility", "site-audit", "gsc-sync", "inspect-sitemap"]);
591
- var runTriggerSchema = z7.enum(["manual", "scheduled", "config-apply"]);
592
- var citationStateSchema = z7.enum(["cited", "not-cited"]);
593
- var computedTransitionSchema = z7.enum(["new", "cited", "lost", "emerging", "not-cited"]);
594
- var runDtoSchema = z7.object({
595
- id: z7.string(),
596
- projectId: z7.string(),
632
+ var wordpressEnvSchema = z7.enum(["live", "staging"]);
633
+ var wordpressConnectionDtoSchema = z7.object({
634
+ projectName: z7.string(),
635
+ url: z7.string(),
636
+ stagingUrl: z7.string().optional(),
637
+ username: z7.string(),
638
+ defaultEnv: wordpressEnvSchema,
639
+ createdAt: z7.string(),
640
+ updatedAt: z7.string()
641
+ });
642
+ var wordpressSiteStatusDtoSchema = z7.object({
643
+ url: z7.string(),
644
+ reachable: z7.boolean(),
645
+ pageCount: z7.number().nullable().optional(),
646
+ version: z7.string().nullable().optional(),
647
+ error: z7.string().nullable().optional(),
648
+ plugins: z7.array(z7.string()).optional()
649
+ });
650
+ var wordpressStatusDtoSchema = z7.object({
651
+ connected: z7.boolean(),
652
+ projectName: z7.string(),
653
+ defaultEnv: wordpressEnvSchema,
654
+ live: wordpressSiteStatusDtoSchema.nullable(),
655
+ staging: wordpressSiteStatusDtoSchema.nullable(),
656
+ adminUrl: z7.string().nullable().optional()
657
+ });
658
+ var wordpressPageSummaryDtoSchema = z7.object({
659
+ id: z7.number(),
660
+ slug: z7.string(),
661
+ title: z7.string(),
662
+ status: z7.string(),
663
+ modifiedAt: z7.string().nullable().optional(),
664
+ link: z7.string().nullable().optional()
665
+ });
666
+ var wordpressSeoStateDtoSchema = z7.object({
667
+ title: z7.string().nullable(),
668
+ description: z7.string().nullable(),
669
+ noindex: z7.boolean().nullable(),
670
+ writable: z7.boolean().default(false),
671
+ writeTargets: z7.array(z7.string()).default([])
672
+ });
673
+ var wordpressSchemaBlockDtoSchema = z7.object({
674
+ type: z7.string(),
675
+ json: z7.record(z7.string(), z7.unknown())
676
+ });
677
+ var wordpressPageDetailDtoSchema = wordpressPageSummaryDtoSchema.extend({
678
+ env: wordpressEnvSchema,
679
+ content: z7.string(),
680
+ seo: wordpressSeoStateDtoSchema,
681
+ schemaBlocks: z7.array(wordpressSchemaBlockDtoSchema).default([])
682
+ });
683
+ var wordpressDiffPageDtoSchema = wordpressPageDetailDtoSchema.extend({
684
+ contentHash: z7.string(),
685
+ contentSnippet: z7.string()
686
+ });
687
+ var wordpressManualAssistDtoSchema = z7.object({
688
+ manualRequired: z7.literal(true),
689
+ targetUrl: z7.string(),
690
+ adminUrl: z7.string().nullable().optional(),
691
+ content: z7.string(),
692
+ nextSteps: z7.array(z7.string()).default([])
693
+ });
694
+ var wordpressAuditIssueDtoSchema = z7.object({
695
+ slug: z7.string(),
696
+ severity: z7.enum(["high", "medium", "low"]),
697
+ code: z7.enum([
698
+ "noindex",
699
+ "missing-seo-title",
700
+ "missing-meta-description",
701
+ "missing-schema",
702
+ "thin-content"
703
+ ]),
704
+ message: z7.string()
705
+ });
706
+ var wordpressAuditPageDtoSchema = z7.object({
707
+ slug: z7.string(),
708
+ title: z7.string(),
709
+ status: z7.string(),
710
+ wordCount: z7.number(),
711
+ seo: wordpressSeoStateDtoSchema,
712
+ schemaPresent: z7.boolean(),
713
+ issues: z7.array(wordpressAuditIssueDtoSchema).default([])
714
+ });
715
+ var wordpressDiffDtoSchema = z7.object({
716
+ slug: z7.string(),
717
+ live: wordpressDiffPageDtoSchema,
718
+ staging: wordpressDiffPageDtoSchema,
719
+ hasDifferences: z7.boolean(),
720
+ differences: z7.object({
721
+ title: z7.boolean(),
722
+ slug: z7.boolean(),
723
+ content: z7.boolean(),
724
+ seoTitle: z7.boolean(),
725
+ seoDescription: z7.boolean(),
726
+ noindex: z7.boolean(),
727
+ schema: z7.boolean()
728
+ })
729
+ });
730
+
731
+ // ../contracts/src/run.ts
732
+ import { z as z8 } from "zod";
733
+ var runStatusSchema = z8.enum(["queued", "running", "completed", "partial", "failed", "cancelled"]);
734
+ var runKindSchema = z8.enum(["answer-visibility", "site-audit", "gsc-sync", "inspect-sitemap"]);
735
+ var runTriggerSchema = z8.enum(["manual", "scheduled", "config-apply"]);
736
+ var citationStateSchema = z8.enum(["cited", "not-cited"]);
737
+ var computedTransitionSchema = z8.enum(["new", "cited", "lost", "emerging", "not-cited"]);
738
+ var runDtoSchema = z8.object({
739
+ id: z8.string(),
740
+ projectId: z8.string(),
597
741
  kind: runKindSchema,
598
742
  status: runStatusSchema,
599
743
  trigger: runTriggerSchema.default("manual"),
600
- location: z7.string().nullable().optional(),
601
- startedAt: z7.string().nullable().optional(),
602
- finishedAt: z7.string().nullable().optional(),
603
- error: z7.string().nullable().optional(),
604
- createdAt: z7.string()
744
+ location: z8.string().nullable().optional(),
745
+ startedAt: z8.string().nullable().optional(),
746
+ finishedAt: z8.string().nullable().optional(),
747
+ error: z8.string().nullable().optional(),
748
+ createdAt: z8.string()
605
749
  });
606
- var groundingSourceSchema = z7.object({
607
- uri: z7.string(),
608
- title: z7.string()
750
+ var groundingSourceSchema = z8.object({
751
+ uri: z8.string(),
752
+ title: z8.string()
609
753
  });
610
- var querySnapshotDtoSchema = z7.object({
611
- id: z7.string(),
612
- runId: z7.string(),
613
- keywordId: z7.string(),
614
- keyword: z7.string().optional(),
754
+ var querySnapshotDtoSchema = z8.object({
755
+ id: z8.string(),
756
+ runId: z8.string(),
757
+ keywordId: z8.string(),
758
+ keyword: z8.string().optional(),
615
759
  provider: providerNameSchema,
616
760
  citationState: citationStateSchema,
617
761
  transition: computedTransitionSchema.optional(),
618
- answerText: z7.string().nullable().optional(),
619
- citedDomains: z7.array(z7.string()).default([]),
620
- competitorOverlap: z7.array(z7.string()).default([]),
621
- groundingSources: z7.array(groundingSourceSchema).default([]),
622
- searchQueries: z7.array(z7.string()).default([]),
623
- model: z7.string().nullable().optional(),
624
- location: z7.string().nullable().optional(),
625
- createdAt: z7.string()
762
+ answerText: z8.string().nullable().optional(),
763
+ citedDomains: z8.array(z8.string()).default([]),
764
+ competitorOverlap: z8.array(z8.string()).default([]),
765
+ groundingSources: z8.array(groundingSourceSchema).default([]),
766
+ searchQueries: z8.array(z8.string()).default([]),
767
+ model: z8.string().nullable().optional(),
768
+ location: z8.string().nullable().optional(),
769
+ createdAt: z8.string()
626
770
  });
627
- var auditLogEntrySchema = z7.object({
628
- id: z7.string(),
629
- projectId: z7.string().nullable().optional(),
630
- actor: z7.string(),
631
- action: z7.string(),
632
- entityType: z7.string(),
633
- entityId: z7.string().nullable().optional(),
634
- diff: z7.unknown().optional(),
635
- createdAt: z7.string()
771
+ var auditLogEntrySchema = z8.object({
772
+ id: z8.string(),
773
+ projectId: z8.string().nullable().optional(),
774
+ actor: z8.string(),
775
+ action: z8.string(),
776
+ entityType: z8.string(),
777
+ entityId: z8.string().nullable().optional(),
778
+ diff: z8.unknown().optional(),
779
+ createdAt: z8.string()
636
780
  });
637
781
 
638
782
  // ../contracts/src/schedule.ts
639
- import { z as z8 } from "zod";
640
- var scheduleDtoSchema = z8.object({
641
- id: z8.string(),
642
- projectId: z8.string(),
643
- cronExpr: z8.string(),
644
- preset: z8.string().nullable().optional(),
645
- timezone: z8.string().default("UTC"),
646
- enabled: z8.boolean().default(true),
647
- providers: z8.array(providerNameSchema).default([]),
648
- lastRunAt: z8.string().nullable().optional(),
649
- nextRunAt: z8.string().nullable().optional(),
650
- createdAt: z8.string(),
651
- updatedAt: z8.string()
783
+ import { z as z9 } from "zod";
784
+ var scheduleDtoSchema = z9.object({
785
+ id: z9.string(),
786
+ projectId: z9.string(),
787
+ cronExpr: z9.string(),
788
+ preset: z9.string().nullable().optional(),
789
+ timezone: z9.string().default("UTC"),
790
+ enabled: z9.boolean().default(true),
791
+ providers: z9.array(providerNameSchema).default([]),
792
+ lastRunAt: z9.string().nullable().optional(),
793
+ nextRunAt: z9.string().nullable().optional(),
794
+ createdAt: z9.string(),
795
+ updatedAt: z9.string()
652
796
  });
653
- var scheduleUpsertRequestSchema = z8.object({
654
- preset: z8.string().optional(),
655
- cron: z8.string().optional(),
656
- timezone: z8.string().optional().default("UTC"),
657
- enabled: z8.boolean().optional().default(true),
658
- providers: z8.array(providerNameSchema).optional().default([])
797
+ var scheduleUpsertRequestSchema = z9.object({
798
+ preset: z9.string().optional(),
799
+ cron: z9.string().optional(),
800
+ timezone: z9.string().optional().default("UTC"),
801
+ enabled: z9.boolean().optional().default(true),
802
+ providers: z9.array(providerNameSchema).optional().default([])
659
803
  }).refine(
660
804
  (data) => data.preset && !data.cron || !data.preset && data.cron,
661
805
  { message: 'Exactly one of "preset" or "cron" must be provided' }
@@ -754,34 +898,34 @@ function categoryLabel(category) {
754
898
  }
755
899
 
756
900
  // ../contracts/src/ga.ts
757
- import { z as z9 } from "zod";
758
- var ga4ConnectionDtoSchema = z9.object({
759
- id: z9.string(),
760
- projectId: z9.string(),
761
- propertyId: z9.string(),
762
- clientEmail: z9.string(),
763
- connected: z9.boolean(),
764
- createdAt: z9.string(),
765
- updatedAt: z9.string()
901
+ import { z as z10 } from "zod";
902
+ var ga4ConnectionDtoSchema = z10.object({
903
+ id: z10.string(),
904
+ projectId: z10.string(),
905
+ propertyId: z10.string(),
906
+ clientEmail: z10.string(),
907
+ connected: z10.boolean(),
908
+ createdAt: z10.string(),
909
+ updatedAt: z10.string()
766
910
  });
767
- var ga4TrafficSnapshotDtoSchema = z9.object({
768
- date: z9.string(),
769
- landingPage: z9.string(),
770
- sessions: z9.number(),
771
- organicSessions: z9.number(),
772
- users: z9.number()
911
+ var ga4TrafficSnapshotDtoSchema = z10.object({
912
+ date: z10.string(),
913
+ landingPage: z10.string(),
914
+ sessions: z10.number(),
915
+ organicSessions: z10.number(),
916
+ users: z10.number()
773
917
  });
774
- var ga4TrafficSummaryDtoSchema = z9.object({
775
- totalSessions: z9.number(),
776
- totalOrganicSessions: z9.number(),
777
- totalUsers: z9.number(),
778
- topPages: z9.array(z9.object({
779
- landingPage: z9.string(),
780
- sessions: z9.number(),
781
- organicSessions: z9.number(),
782
- users: z9.number()
918
+ var ga4TrafficSummaryDtoSchema = z10.object({
919
+ totalSessions: z10.number(),
920
+ totalOrganicSessions: z10.number(),
921
+ totalUsers: z10.number(),
922
+ topPages: z10.array(z10.object({
923
+ landingPage: z10.string(),
924
+ sessions: z10.number(),
925
+ organicSessions: z10.number(),
926
+ users: z10.number()
783
927
  })),
784
- lastSyncedAt: z9.string().nullable()
928
+ lastSyncedAt: z10.string().nullable()
785
929
  });
786
930
 
787
931
  // ../api-routes/src/auth.ts
@@ -3546,6 +3690,19 @@ var analyticsWindowParameter = {
3546
3690
  description: "Time window for analytics queries.",
3547
3691
  schema: { type: "string", enum: ["7d", "30d", "90d", "all"] }
3548
3692
  };
3693
+ var wordpressEnvQueryParameter = {
3694
+ name: "env",
3695
+ in: "query",
3696
+ description: "WordPress environment to target.",
3697
+ schema: { type: "string", enum: ["live", "staging"] }
3698
+ };
3699
+ var wordpressSlugQueryParameter = {
3700
+ name: "slug",
3701
+ in: "query",
3702
+ required: true,
3703
+ description: "WordPress page slug.",
3704
+ schema: stringSchema
3705
+ };
3549
3706
  var routeCatalog = [
3550
3707
  {
3551
3708
  method: "get",
@@ -4897,6 +5054,301 @@ var routeCatalog = [
4897
5054
  404: { description: "Project not found." }
4898
5055
  }
4899
5056
  },
5057
+ {
5058
+ method: "post",
5059
+ path: "/api/v1/projects/{name}/wordpress/connect",
5060
+ summary: "Connect WordPress REST access",
5061
+ tags: ["wordpress"],
5062
+ parameters: [nameParameter],
5063
+ requestBody: {
5064
+ required: true,
5065
+ content: {
5066
+ "application/json": {
5067
+ schema: {
5068
+ type: "object",
5069
+ required: ["url", "username", "appPassword"],
5070
+ properties: {
5071
+ url: stringSchema,
5072
+ stagingUrl: stringSchema,
5073
+ username: stringSchema,
5074
+ appPassword: stringSchema,
5075
+ defaultEnv: { type: "string", enum: ["live", "staging"] }
5076
+ }
5077
+ }
5078
+ }
5079
+ }
5080
+ },
5081
+ responses: {
5082
+ 200: { description: "WordPress connection status returned." },
5083
+ 400: { description: "Invalid WordPress connection request." },
5084
+ 404: { description: "Project not found." }
5085
+ }
5086
+ },
5087
+ {
5088
+ method: "delete",
5089
+ path: "/api/v1/projects/{name}/wordpress/disconnect",
5090
+ summary: "Disconnect WordPress",
5091
+ tags: ["wordpress"],
5092
+ parameters: [nameParameter],
5093
+ responses: {
5094
+ 204: { description: "WordPress connection deleted." },
5095
+ 404: { description: "Project or connection not found." }
5096
+ }
5097
+ },
5098
+ {
5099
+ method: "get",
5100
+ path: "/api/v1/projects/{name}/wordpress/status",
5101
+ summary: "Get WordPress connection status",
5102
+ tags: ["wordpress"],
5103
+ parameters: [nameParameter],
5104
+ responses: {
5105
+ 200: { description: "WordPress status returned." },
5106
+ 404: { description: "Project not found." }
5107
+ }
5108
+ },
5109
+ {
5110
+ method: "get",
5111
+ path: "/api/v1/projects/{name}/wordpress/pages",
5112
+ summary: "List WordPress pages",
5113
+ tags: ["wordpress"],
5114
+ parameters: [nameParameter, wordpressEnvQueryParameter],
5115
+ responses: {
5116
+ 200: { description: "WordPress pages returned." },
5117
+ 400: { description: "Invalid environment or missing connection." },
5118
+ 404: { description: "Project not found." }
5119
+ }
5120
+ },
5121
+ {
5122
+ method: "get",
5123
+ path: "/api/v1/projects/{name}/wordpress/page",
5124
+ summary: "Get a WordPress page by slug",
5125
+ tags: ["wordpress"],
5126
+ parameters: [nameParameter, wordpressSlugQueryParameter, wordpressEnvQueryParameter],
5127
+ responses: {
5128
+ 200: { description: "WordPress page returned." },
5129
+ 400: { description: "Invalid slug or environment." },
5130
+ 404: { description: "Project, connection, or page not found." }
5131
+ }
5132
+ },
5133
+ {
5134
+ method: "post",
5135
+ path: "/api/v1/projects/{name}/wordpress/pages",
5136
+ summary: "Create a WordPress page",
5137
+ tags: ["wordpress"],
5138
+ parameters: [nameParameter],
5139
+ requestBody: {
5140
+ required: true,
5141
+ content: {
5142
+ "application/json": {
5143
+ schema: {
5144
+ type: "object",
5145
+ required: ["title", "slug", "content"],
5146
+ properties: {
5147
+ title: stringSchema,
5148
+ slug: stringSchema,
5149
+ content: stringSchema,
5150
+ status: stringSchema,
5151
+ env: { type: "string", enum: ["live", "staging"] }
5152
+ }
5153
+ }
5154
+ }
5155
+ }
5156
+ },
5157
+ responses: {
5158
+ 200: { description: "WordPress page created." },
5159
+ 400: { description: "Invalid page creation request." },
5160
+ 404: { description: "Project or connection not found." }
5161
+ }
5162
+ },
5163
+ {
5164
+ method: "put",
5165
+ path: "/api/v1/projects/{name}/wordpress/page",
5166
+ summary: "Update a WordPress page by slug",
5167
+ tags: ["wordpress"],
5168
+ parameters: [nameParameter],
5169
+ requestBody: {
5170
+ required: true,
5171
+ content: {
5172
+ "application/json": {
5173
+ schema: {
5174
+ type: "object",
5175
+ required: ["currentSlug"],
5176
+ properties: {
5177
+ currentSlug: stringSchema,
5178
+ title: stringSchema,
5179
+ slug: stringSchema,
5180
+ content: stringSchema,
5181
+ status: stringSchema,
5182
+ env: { type: "string", enum: ["live", "staging"] }
5183
+ }
5184
+ }
5185
+ }
5186
+ }
5187
+ },
5188
+ responses: {
5189
+ 200: { description: "WordPress page updated." },
5190
+ 400: { description: "Invalid page update request." },
5191
+ 404: { description: "Project, connection, or page not found." }
5192
+ }
5193
+ },
5194
+ {
5195
+ method: "post",
5196
+ path: "/api/v1/projects/{name}/wordpress/page/meta",
5197
+ summary: "Update REST-exposed WordPress SEO meta",
5198
+ tags: ["wordpress"],
5199
+ parameters: [nameParameter],
5200
+ requestBody: {
5201
+ required: true,
5202
+ content: {
5203
+ "application/json": {
5204
+ schema: {
5205
+ type: "object",
5206
+ required: ["slug"],
5207
+ properties: {
5208
+ slug: stringSchema,
5209
+ title: stringSchema,
5210
+ description: stringSchema,
5211
+ noindex: booleanSchema,
5212
+ env: { type: "string", enum: ["live", "staging"] }
5213
+ }
5214
+ }
5215
+ }
5216
+ }
5217
+ },
5218
+ responses: {
5219
+ 200: { description: "WordPress SEO meta updated." },
5220
+ 400: { description: "SEO meta is unsupported or the request is invalid." },
5221
+ 404: { description: "Project, connection, or page not found." }
5222
+ }
5223
+ },
5224
+ {
5225
+ method: "get",
5226
+ path: "/api/v1/projects/{name}/wordpress/schema",
5227
+ summary: "Read rendered JSON-LD schema for a page",
5228
+ tags: ["wordpress"],
5229
+ parameters: [nameParameter, wordpressSlugQueryParameter, wordpressEnvQueryParameter],
5230
+ responses: {
5231
+ 200: { description: "WordPress schema blocks returned." },
5232
+ 400: { description: "Invalid slug or environment." },
5233
+ 404: { description: "Project, connection, or page not found." }
5234
+ }
5235
+ },
5236
+ {
5237
+ method: "post",
5238
+ path: "/api/v1/projects/{name}/wordpress/schema/manual",
5239
+ summary: "Generate a manual schema update payload",
5240
+ tags: ["wordpress"],
5241
+ parameters: [nameParameter],
5242
+ requestBody: {
5243
+ required: true,
5244
+ content: {
5245
+ "application/json": {
5246
+ schema: {
5247
+ type: "object",
5248
+ required: ["slug", "json"],
5249
+ properties: {
5250
+ slug: stringSchema,
5251
+ type: stringSchema,
5252
+ json: stringSchema,
5253
+ env: { type: "string", enum: ["live", "staging"] }
5254
+ }
5255
+ }
5256
+ }
5257
+ }
5258
+ },
5259
+ responses: {
5260
+ 200: { description: "Manual schema instructions returned." },
5261
+ 400: { description: "Invalid schema request." },
5262
+ 404: { description: "Project, connection, or page not found." }
5263
+ }
5264
+ },
5265
+ {
5266
+ method: "get",
5267
+ path: "/api/v1/projects/{name}/wordpress/llms-txt",
5268
+ summary: "Read /llms.txt for a WordPress environment",
5269
+ tags: ["wordpress"],
5270
+ parameters: [nameParameter, wordpressEnvQueryParameter],
5271
+ responses: {
5272
+ 200: { description: "llms.txt returned." },
5273
+ 400: { description: "Invalid environment or missing connection." },
5274
+ 404: { description: "Project not found." }
5275
+ }
5276
+ },
5277
+ {
5278
+ method: "post",
5279
+ path: "/api/v1/projects/{name}/wordpress/llms-txt/manual",
5280
+ summary: "Generate a manual llms.txt update payload",
5281
+ tags: ["wordpress"],
5282
+ parameters: [nameParameter],
5283
+ requestBody: {
5284
+ required: true,
5285
+ content: {
5286
+ "application/json": {
5287
+ schema: {
5288
+ type: "object",
5289
+ required: ["content"],
5290
+ properties: {
5291
+ content: stringSchema,
5292
+ env: { type: "string", enum: ["live", "staging"] }
5293
+ }
5294
+ }
5295
+ }
5296
+ }
5297
+ },
5298
+ responses: {
5299
+ 200: { description: "Manual llms.txt instructions returned." },
5300
+ 400: { description: "Invalid llms.txt request." },
5301
+ 404: { description: "Project or connection not found." }
5302
+ }
5303
+ },
5304
+ {
5305
+ method: "get",
5306
+ path: "/api/v1/projects/{name}/wordpress/audit",
5307
+ summary: "Audit WordPress pages for SEO and content issues",
5308
+ tags: ["wordpress"],
5309
+ parameters: [nameParameter, wordpressEnvQueryParameter],
5310
+ responses: {
5311
+ 200: { description: "WordPress audit returned." },
5312
+ 400: { description: "Invalid environment or missing connection." },
5313
+ 404: { description: "Project not found." }
5314
+ }
5315
+ },
5316
+ {
5317
+ method: "get",
5318
+ path: "/api/v1/projects/{name}/wordpress/diff",
5319
+ summary: "Compare live and staging versions of a WordPress page",
5320
+ tags: ["wordpress"],
5321
+ parameters: [nameParameter, wordpressSlugQueryParameter],
5322
+ responses: {
5323
+ 200: { description: "WordPress diff returned." },
5324
+ 400: { description: "Invalid slug or missing staging configuration." },
5325
+ 404: { description: "Project, connection, or page not found." }
5326
+ }
5327
+ },
5328
+ {
5329
+ method: "get",
5330
+ path: "/api/v1/projects/{name}/wordpress/staging/status",
5331
+ summary: "Get WordPress staging configuration status",
5332
+ tags: ["wordpress"],
5333
+ parameters: [nameParameter],
5334
+ responses: {
5335
+ 200: { description: "WordPress staging status returned." },
5336
+ 400: { description: "WordPress is not configured for this project." },
5337
+ 404: { description: "Project not found." }
5338
+ }
5339
+ },
5340
+ {
5341
+ method: "post",
5342
+ path: "/api/v1/projects/{name}/wordpress/staging/push",
5343
+ summary: "Generate a manual staging push handoff",
5344
+ tags: ["wordpress"],
5345
+ parameters: [nameParameter],
5346
+ responses: {
5347
+ 200: { description: "Manual staging push instructions returned." },
5348
+ 400: { description: "Missing staging configuration." },
5349
+ 404: { description: "Project or connection not found." }
5350
+ }
5351
+ },
4900
5352
  // GA4 routes
4901
5353
  {
4902
5354
  method: "post",
@@ -6086,17 +6538,22 @@ async function googleRoutes(app, opts) {
6086
6538
  app.get("/projects/:name/google/gsc/coverage", async (request) => {
6087
6539
  const project = resolveProject(app.db, request.params.name);
6088
6540
  const allInspections = app.db.select().from(gscUrlInspections).where(eq13(gscUrlInspections.projectId, project.id)).orderBy(desc4(gscUrlInspections.inspectedAt)).all();
6541
+ const canonicalUrl = (url) => url.replace(/^http:\/\//, "https://");
6089
6542
  const latestByUrl = /* @__PURE__ */ new Map();
6090
6543
  const historyByUrl = /* @__PURE__ */ new Map();
6091
6544
  for (const row of allInspections) {
6092
- if (!latestByUrl.has(row.url)) {
6093
- latestByUrl.set(row.url, row);
6094
- }
6095
- const history = historyByUrl.get(row.url);
6545
+ const key = canonicalUrl(row.url);
6546
+ const existing = latestByUrl.get(key);
6547
+ if (!existing) {
6548
+ latestByUrl.set(key, row);
6549
+ } else if (existing.url.startsWith("http://") && row.url.startsWith("https://")) {
6550
+ latestByUrl.set(key, row);
6551
+ }
6552
+ const history = historyByUrl.get(key);
6096
6553
  if (history) {
6097
6554
  history.push(row);
6098
6555
  } else {
6099
- historyByUrl.set(row.url, [row]);
6556
+ historyByUrl.set(key, [row]);
6100
6557
  }
6101
6558
  }
6102
6559
  const indexedUrls = [];
@@ -6636,10 +7093,22 @@ async function bingRoutes(app, opts) {
6636
7093
  if (!conn) return;
6637
7094
  const allInspections = app.db.select().from(bingUrlInspections).where(eq14(bingUrlInspections.projectId, project.id)).orderBy(desc5(bingUrlInspections.inspectedAt)).all();
6638
7095
  const latestByUrl = /* @__PURE__ */ new Map();
7096
+ const definitiveByUrl = /* @__PURE__ */ new Map();
6639
7097
  for (const row of allInspections) {
6640
7098
  if (!latestByUrl.has(row.url)) {
6641
7099
  latestByUrl.set(row.url, row);
6642
7100
  }
7101
+ if (!definitiveByUrl.has(row.url) && row.inIndex != null) {
7102
+ definitiveByUrl.set(row.url, row);
7103
+ }
7104
+ }
7105
+ for (const [url, latest] of latestByUrl) {
7106
+ if (latest.inIndex == null) {
7107
+ const definitive = definitiveByUrl.get(url);
7108
+ if (definitive) {
7109
+ latestByUrl.set(url, definitive);
7110
+ }
7111
+ }
6643
7112
  }
6644
7113
  const indexedUrls = [];
6645
7114
  const notIndexedUrls = [];
@@ -7569,22 +8038,1001 @@ async function ga4Routes(app, opts) {
7569
8038
  });
7570
8039
  }
7571
8040
 
7572
- // ../api-routes/src/index.ts
7573
- async function apiRoutes(app, opts) {
7574
- app.decorate("db", opts.db);
7575
- app.setErrorHandler((error, _request, reply) => {
7576
- if (error instanceof AppError) {
7577
- return reply.status(error.statusCode).send(error.toJSON());
7578
- }
7579
- const httpStatus = error.statusCode ?? error.status ?? 500;
7580
- if (httpStatus >= 400 && httpStatus < 500) {
7581
- return reply.status(httpStatus).send({
7582
- error: {
7583
- code: httpStatus === 401 ? "AUTH_INVALID" : httpStatus === 403 ? "FORBIDDEN" : httpStatus === 404 ? "NOT_FOUND" : httpStatus === 429 ? "QUOTA_EXCEEDED" : "VALIDATION_ERROR",
7584
- message: error.message
7585
- }
7586
- });
7587
- }
8041
+ // ../integration-wordpress/src/types.ts
8042
+ var WordpressApiError = class extends Error {
8043
+ statusCode;
8044
+ code;
8045
+ constructor(code, message, statusCode) {
8046
+ super(message);
8047
+ this.name = "WordpressApiError";
8048
+ this.code = code;
8049
+ this.statusCode = statusCode;
8050
+ }
8051
+ };
8052
+
8053
+ // ../integration-wordpress/src/wordpress-client.ts
8054
+ import crypto17 from "crypto";
8055
+ var PAGE_FIELDS = "id,slug,status,link,modified,modified_gmt,title,content,meta";
8056
+ var PAGE_LIST_FIELDS = "id,slug,status,link,modified,modified_gmt,title";
8057
+ var VERIFY_PAGE_FIELDS = "id,status";
8058
+ var SEO_TARGETS = [
8059
+ {
8060
+ pluginHints: ["wordpress-seo", "yoast"],
8061
+ titleKey: "_yoast_wpseo_title",
8062
+ descriptionKey: "_yoast_wpseo_metadesc",
8063
+ noindexKey: "_yoast_wpseo_meta-robots-noindex"
8064
+ },
8065
+ {
8066
+ pluginHints: ["all-in-one-seo-pack", "aioseo"],
8067
+ titleKey: "_aioseo_title",
8068
+ descriptionKey: "_aioseo_description",
8069
+ noindexKey: "_aioseo_noindex"
8070
+ },
8071
+ {
8072
+ pluginHints: ["seo-by-rank-math", "rank-math"],
8073
+ titleKey: "rank_math_title",
8074
+ descriptionKey: "rank_math_description",
8075
+ noindexKey: "rank_math_robots"
8076
+ }
8077
+ ];
8078
+ var THIN_CONTENT_WORD_COUNT = 250;
8079
+ function normalizeSiteUrl(url) {
8080
+ return url.replace(/\/$/, "");
8081
+ }
8082
+ function encodeBasicAuth(username, appPassword) {
8083
+ return Buffer.from(`${username}:${appPassword}`).toString("base64");
8084
+ }
8085
+ async function fetchJson(connection, siteUrl, path7, init) {
8086
+ const res = await fetch(`${normalizeSiteUrl(siteUrl)}${path7}`, {
8087
+ ...init,
8088
+ headers: {
8089
+ "Authorization": `Basic ${encodeBasicAuth(connection.username, connection.appPassword)}`,
8090
+ ...init?.body != null ? { "Content-Type": "application/json" } : {},
8091
+ ...init?.headers ?? {}
8092
+ }
8093
+ });
8094
+ if (res.status === 401 || res.status === 403) {
8095
+ throw new WordpressApiError("AUTH_INVALID", "WordPress credentials are invalid or lack permission for this action", res.status);
8096
+ }
8097
+ if (res.status === 404) {
8098
+ throw new WordpressApiError("NOT_FOUND", "WordPress endpoint not found", 404);
8099
+ }
8100
+ if (!res.ok) {
8101
+ const text2 = await res.text().catch(() => "");
8102
+ throw new WordpressApiError("UPSTREAM_ERROR", `WordPress API error (${res.status}): ${text2 || res.statusText}`, res.status);
8103
+ }
8104
+ return {
8105
+ body: await res.json(),
8106
+ response: res
8107
+ };
8108
+ }
8109
+ async function fetchPageCollectionSummary(connection, siteUrl, options) {
8110
+ const params = new URLSearchParams({
8111
+ per_page: "1",
8112
+ _fields: VERIFY_PAGE_FIELDS
8113
+ });
8114
+ if (options?.context) {
8115
+ params.set("context", options.context);
8116
+ }
8117
+ const { response } = await fetchJson(
8118
+ connection,
8119
+ siteUrl,
8120
+ `/wp-json/wp/v2/pages?${params.toString()}`
8121
+ );
8122
+ return response;
8123
+ }
8124
+ async function fetchText(url) {
8125
+ try {
8126
+ const res = await fetch(url);
8127
+ if (!res.ok) return null;
8128
+ return await res.text();
8129
+ } catch {
8130
+ return null;
8131
+ }
8132
+ }
8133
+ function stripHtml(input) {
8134
+ return input.replace(/<script[\s\S]*?<\/script>/gi, " ").replace(/<style[\s\S]*?<\/style>/gi, " ").replace(/<[^>]+>/g, " ").replace(/&nbsp;/gi, " ").replace(/&amp;/gi, "&").replace(/&quot;/gi, '"').replace(/&#39;/gi, "'").replace(/&lt;/gi, "<").replace(/&gt;/gi, ">").replace(/\s+/g, " ").trim();
8135
+ }
8136
+ function extractMetaContent(html, name) {
8137
+ const patterns = [
8138
+ new RegExp(`<meta[^>]+name=["']${name}["'][^>]+content=["']([^"']*)["'][^>]*>`, "i"),
8139
+ new RegExp(`<meta[^>]+content=["']([^"']*)["'][^>]+name=["']${name}["'][^>]*>`, "i")
8140
+ ];
8141
+ for (const pattern of patterns) {
8142
+ const match = pattern.exec(html);
8143
+ if (match?.[1] != null) return match[1].trim() || null;
8144
+ }
8145
+ return null;
8146
+ }
8147
+ function extractTitle(html) {
8148
+ const match = /<title[^>]*>([\s\S]*?)<\/title>/i.exec(html);
8149
+ return match?.[1] ? stripHtml(match[1]) || null : null;
8150
+ }
8151
+ function extractGeneratorVersion(html) {
8152
+ const generator = extractMetaContent(html, "generator");
8153
+ if (!generator) return null;
8154
+ const match = /WordPress\s+([0-9][^ ]*)/i.exec(generator);
8155
+ return match?.[1] ?? generator;
8156
+ }
8157
+ function extractSchemaBlocks(html) {
8158
+ const blocks = [];
8159
+ const regex = /<script[^>]+type=["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi;
8160
+ let match;
8161
+ while ((match = regex.exec(html)) !== null) {
8162
+ const raw = match[1]?.trim();
8163
+ if (!raw) continue;
8164
+ try {
8165
+ const parsed = JSON.parse(raw);
8166
+ const items = Array.isArray(parsed) ? parsed : [parsed];
8167
+ for (const item of items) {
8168
+ const type = typeof item["@type"] === "string" ? String(item["@type"]) : Array.isArray(item["@type"]) ? String(item["@type"][0] ?? "Unknown") : "Unknown";
8169
+ blocks.push({
8170
+ type,
8171
+ json: item
8172
+ });
8173
+ }
8174
+ } catch {
8175
+ continue;
8176
+ }
8177
+ }
8178
+ return blocks;
8179
+ }
8180
+ function summarizeSeoFromHtml(html) {
8181
+ const description = extractMetaContent(html, "description");
8182
+ const robots = extractMetaContent(html, "robots");
8183
+ return {
8184
+ title: extractTitle(html),
8185
+ description,
8186
+ noindex: robots == null ? null : /\bnoindex\b/i.test(robots)
8187
+ };
8188
+ }
8189
+ function computeWordCount(content) {
8190
+ const clean = stripHtml(content);
8191
+ if (!clean) return 0;
8192
+ return clean.split(/\s+/).filter(Boolean).length;
8193
+ }
8194
+ function buildSnippet(content) {
8195
+ const text2 = stripHtml(content);
8196
+ if (text2.length <= 160) return text2;
8197
+ return `${text2.slice(0, 157)}...`;
8198
+ }
8199
+ function contentHash(content) {
8200
+ return crypto17.createHash("sha256").update(content).digest("hex");
8201
+ }
8202
+ function buildAmbiguousSlugMessage(slug, pages) {
8203
+ const candidates = pages.map((page) => {
8204
+ const title = stripHtml(page.title?.rendered ?? "") || "(untitled)";
8205
+ return `#${page.id} "${title}"`;
8206
+ }).join(", ");
8207
+ return `Multiple pages matched slug "${slug}". Candidates: ${candidates}.`;
8208
+ }
8209
+ async function mapWithConcurrency(items, limit, iteratee) {
8210
+ const results = new Array(items.length);
8211
+ let nextIndex = 0;
8212
+ async function worker() {
8213
+ while (nextIndex < items.length) {
8214
+ const index2 = nextIndex;
8215
+ nextIndex += 1;
8216
+ results[index2] = await iteratee(items[index2], index2);
8217
+ }
8218
+ }
8219
+ const workerCount = Math.max(1, Math.min(limit, items.length));
8220
+ await Promise.all(Array.from({ length: workerCount }, () => worker()));
8221
+ return results;
8222
+ }
8223
+ function resolveSeoWriteTargets(meta, plugins) {
8224
+ if (!meta) return [];
8225
+ const keys = new Set(Object.keys(meta));
8226
+ const pluginList = plugins ?? [];
8227
+ const targets = [];
8228
+ for (const target of SEO_TARGETS) {
8229
+ const hinted = target.pluginHints.some((hint) => pluginList.some((plugin) => plugin.includes(hint)));
8230
+ if (!hinted && !keys.has(target.titleKey) && !keys.has(target.descriptionKey) && !keys.has(target.noindexKey)) {
8231
+ continue;
8232
+ }
8233
+ if (keys.has(target.titleKey)) targets.push(target.titleKey);
8234
+ if (keys.has(target.descriptionKey)) targets.push(target.descriptionKey);
8235
+ if (keys.has(target.noindexKey)) targets.push(target.noindexKey);
8236
+ }
8237
+ return [...new Set(targets)];
8238
+ }
8239
+ function buildSeoState(page, html, plugins) {
8240
+ const renderedSeo = html ? summarizeSeoFromHtml(html) : { title: null, description: null, noindex: null };
8241
+ const writeTargets = resolveSeoWriteTargets(page.meta, plugins);
8242
+ return {
8243
+ title: renderedSeo.title,
8244
+ description: renderedSeo.description,
8245
+ noindex: renderedSeo.noindex,
8246
+ writable: writeTargets.length > 0,
8247
+ writeTargets
8248
+ };
8249
+ }
8250
+ function resolveEnvironment(connection, requestedEnv) {
8251
+ const env = requestedEnv ?? connection.defaultEnv;
8252
+ if (env === "staging") {
8253
+ if (!connection.stagingUrl) {
8254
+ throw new WordpressApiError("VALIDATION_ERROR", "No staging URL configured for this project. Reconnect with --staging-url or use --live.", 400);
8255
+ }
8256
+ return { env, siteUrl: normalizeSiteUrl(connection.stagingUrl) };
8257
+ }
8258
+ return { env: "live", siteUrl: normalizeSiteUrl(connection.url) };
8259
+ }
8260
+ function getWpStagingAdminUrl(url) {
8261
+ return `${normalizeSiteUrl(url)}/wp-admin/admin.php?page=wpstg_clone`;
8262
+ }
8263
+ async function verifyWordpressConnection(connection) {
8264
+ const site = resolveEnvironment({ ...connection, defaultEnv: "live" }, "live");
8265
+ const response = await fetchPageCollectionSummary(connection, site.siteUrl, { context: "view" });
8266
+ const homeHtml = await fetchText(site.siteUrl);
8267
+ return {
8268
+ url: site.siteUrl,
8269
+ reachable: true,
8270
+ pageCount: Number.parseInt(response.headers.get("x-wp-total") ?? "0", 10) || 0,
8271
+ version: homeHtml ? extractGeneratorVersion(homeHtml) : null,
8272
+ plugins: []
8273
+ };
8274
+ }
8275
+ async function getSiteStatus(connection, env) {
8276
+ const site = resolveEnvironment(connection, env);
8277
+ try {
8278
+ const response = await fetchPageCollectionSummary(connection, site.siteUrl, { context: "view" });
8279
+ const homeHtml = await fetchText(site.siteUrl);
8280
+ const plugins = await listActivePlugins(connection, env);
8281
+ return {
8282
+ url: site.siteUrl,
8283
+ reachable: true,
8284
+ pageCount: Number.parseInt(response.headers.get("x-wp-total") ?? "0", 10) || 0,
8285
+ version: homeHtml ? extractGeneratorVersion(homeHtml) : null,
8286
+ plugins: plugins ?? []
8287
+ };
8288
+ } catch (error) {
8289
+ const message = error instanceof Error ? error.message : String(error);
8290
+ return {
8291
+ url: site.siteUrl,
8292
+ reachable: false,
8293
+ pageCount: null,
8294
+ version: null,
8295
+ error: message,
8296
+ plugins: []
8297
+ };
8298
+ }
8299
+ }
8300
+ async function listActivePlugins(connection, env) {
8301
+ const site = resolveEnvironment(connection, env);
8302
+ try {
8303
+ const { body } = await fetchJson(
8304
+ connection,
8305
+ site.siteUrl,
8306
+ "/wp-json/wp/v2/plugins?per_page=100&_fields=plugin,status"
8307
+ );
8308
+ return body.filter((plugin) => plugin.status === "active").map((plugin) => plugin.plugin).sort();
8309
+ } catch (error) {
8310
+ if (error instanceof WordpressApiError && (error.statusCode === 403 || error.statusCode === 404)) {
8311
+ return null;
8312
+ }
8313
+ return null;
8314
+ }
8315
+ }
8316
+ async function listPages(connection, env) {
8317
+ const site = resolveEnvironment(connection, env);
8318
+ const pages = [];
8319
+ let page = 1;
8320
+ let totalPages = 1;
8321
+ while (page <= totalPages) {
8322
+ const { body, response } = await fetchJson(
8323
+ connection,
8324
+ site.siteUrl,
8325
+ `/wp-json/wp/v2/pages?per_page=100&page=${page}&context=edit&_fields=${PAGE_LIST_FIELDS}`
8326
+ );
8327
+ totalPages = Number.parseInt(response.headers.get("x-wp-totalpages") ?? "1", 10) || 1;
8328
+ pages.push(...body.map((entry) => ({
8329
+ id: entry.id,
8330
+ slug: entry.slug,
8331
+ title: stripHtml(entry.title?.rendered ?? ""),
8332
+ status: entry.status,
8333
+ modifiedAt: entry.modified ?? entry.modified_gmt ?? null,
8334
+ link: entry.link ?? null
8335
+ })));
8336
+ page += 1;
8337
+ }
8338
+ return pages;
8339
+ }
8340
+ async function getPageBySlug(connection, slug, env) {
8341
+ const site = resolveEnvironment(connection, env);
8342
+ const { body } = await fetchJson(
8343
+ connection,
8344
+ site.siteUrl,
8345
+ `/wp-json/wp/v2/pages?slug=${encodeURIComponent(slug)}&per_page=100&context=edit&_fields=${PAGE_FIELDS}`
8346
+ );
8347
+ if (body.length === 0) {
8348
+ throw new WordpressApiError("NOT_FOUND", `No WordPress page found for slug "${slug}"`, 404);
8349
+ }
8350
+ const exact = body.filter((page) => page.slug === slug);
8351
+ if (exact.length === 1) return exact[0];
8352
+ if (exact.length > 1) {
8353
+ throw new WordpressApiError("VALIDATION_ERROR", buildAmbiguousSlugMessage(slug, exact), 400);
8354
+ }
8355
+ if (body.length > 1) {
8356
+ throw new WordpressApiError("VALIDATION_ERROR", buildAmbiguousSlugMessage(slug, body), 400);
8357
+ }
8358
+ return body[0];
8359
+ }
8360
+ async function fetchRenderedPage(link) {
8361
+ if (!link) return null;
8362
+ return fetchText(link);
8363
+ }
8364
+ async function getPageDetail(connection, slug, env, plugins) {
8365
+ const site = resolveEnvironment(connection, env);
8366
+ const resolvedPlugins = plugins === void 0 ? await listActivePlugins(connection, site.env) : plugins;
8367
+ const page = await getPageBySlug(connection, slug, site.env);
8368
+ const html = await fetchRenderedPage(page.link);
8369
+ const schemaBlocks = html ? extractSchemaBlocks(html) : [];
8370
+ const seo = buildSeoState(page, html, resolvedPlugins);
8371
+ return {
8372
+ id: page.id,
8373
+ slug: page.slug,
8374
+ title: stripHtml(page.title?.rendered ?? ""),
8375
+ status: page.status,
8376
+ modifiedAt: page.modified ?? page.modified_gmt ?? null,
8377
+ link: page.link ?? null,
8378
+ env: site.env,
8379
+ content: page.content?.raw ?? page.content?.rendered ?? "",
8380
+ seo,
8381
+ schemaBlocks
8382
+ };
8383
+ }
8384
+ async function createPage(connection, body, env) {
8385
+ const site = resolveEnvironment(connection, env);
8386
+ const { body: created } = await fetchJson(
8387
+ connection,
8388
+ site.siteUrl,
8389
+ "/wp-json/wp/v2/pages",
8390
+ {
8391
+ method: "POST",
8392
+ body: JSON.stringify({
8393
+ title: body.title,
8394
+ slug: body.slug,
8395
+ content: body.content,
8396
+ status: body.status ?? "draft"
8397
+ })
8398
+ }
8399
+ );
8400
+ return getPageDetail(connection, created.slug, site.env);
8401
+ }
8402
+ async function updatePageBySlug(connection, slug, body, env) {
8403
+ const site = resolveEnvironment(connection, env);
8404
+ const page = await getPageBySlug(connection, slug, site.env);
8405
+ const { body: updated } = await fetchJson(
8406
+ connection,
8407
+ site.siteUrl,
8408
+ `/wp-json/wp/v2/pages/${page.id}`,
8409
+ {
8410
+ method: "POST",
8411
+ body: JSON.stringify(body)
8412
+ }
8413
+ );
8414
+ return getPageDetail(connection, updated.slug, site.env);
8415
+ }
8416
+ function encodeNoindexValue(key, value) {
8417
+ if (key === "rank_math_robots") {
8418
+ return value ? ["noindex"] : ["index"];
8419
+ }
8420
+ return value ? "1" : "0";
8421
+ }
8422
+ async function setSeoMeta(connection, slug, body, env) {
8423
+ const site = resolveEnvironment(connection, env);
8424
+ const plugins = await listActivePlugins(connection, site.env);
8425
+ const page = await getPageBySlug(connection, slug, site.env);
8426
+ const writeTargets = resolveSeoWriteTargets(page.meta, plugins);
8427
+ if (writeTargets.length === 0 || !page.meta) {
8428
+ throw new WordpressApiError("UNSUPPORTED", "This WordPress site does not expose writable SEO meta fields through REST. Update the meta manually in WordPress.", 400);
8429
+ }
8430
+ const patch = {};
8431
+ for (const target of SEO_TARGETS) {
8432
+ if (body.title != null && writeTargets.includes(target.titleKey)) patch[target.titleKey] = body.title;
8433
+ if (body.description != null && writeTargets.includes(target.descriptionKey)) patch[target.descriptionKey] = body.description;
8434
+ if (body.noindex != null && writeTargets.includes(target.noindexKey)) {
8435
+ patch[target.noindexKey] = encodeNoindexValue(target.noindexKey, body.noindex);
8436
+ }
8437
+ }
8438
+ if (Object.keys(patch).length === 0) {
8439
+ throw new WordpressApiError("UNSUPPORTED", "No writable REST-exposed SEO fields matched the requested update.", 400);
8440
+ }
8441
+ await fetchJson(
8442
+ connection,
8443
+ site.siteUrl,
8444
+ `/wp-json/wp/v2/pages/${page.id}`,
8445
+ {
8446
+ method: "POST",
8447
+ body: JSON.stringify({
8448
+ meta: {
8449
+ ...page.meta ?? {},
8450
+ ...patch
8451
+ }
8452
+ })
8453
+ }
8454
+ );
8455
+ return getPageDetail(connection, slug, site.env, plugins);
8456
+ }
8457
+ async function getLlmsTxt(connection, env) {
8458
+ const site = resolveEnvironment(connection, env);
8459
+ const url = `${site.siteUrl}/llms.txt`;
8460
+ return {
8461
+ env: site.env,
8462
+ url,
8463
+ content: await fetchText(url)
8464
+ };
8465
+ }
8466
+ async function buildManualLlmsTxtUpdate(connection, content, env) {
8467
+ const site = resolveEnvironment(connection, env);
8468
+ return {
8469
+ manualRequired: true,
8470
+ targetUrl: `${site.siteUrl}/llms.txt`,
8471
+ adminUrl: `${site.siteUrl}/wp-admin/`,
8472
+ content,
8473
+ nextSteps: [
8474
+ `Open your hosting file manager or SSH session for ${site.siteUrl}.`,
8475
+ "Create or update the file llms.txt at the site root.",
8476
+ "Paste the generated content exactly as provided.",
8477
+ "Reload /llms.txt in a browser to confirm the file is publicly reachable."
8478
+ ]
8479
+ };
8480
+ }
8481
+ async function getPageSchema(connection, slug, env) {
8482
+ const detail = await getPageDetail(connection, slug, env);
8483
+ return {
8484
+ env: detail.env,
8485
+ slug: detail.slug,
8486
+ blocks: detail.schemaBlocks
8487
+ };
8488
+ }
8489
+ async function buildManualSchemaUpdate(connection, slug, body, env) {
8490
+ const site = resolveEnvironment(connection, env);
8491
+ const page = await getPageBySlug(connection, slug, site.env);
8492
+ return {
8493
+ manualRequired: true,
8494
+ targetUrl: page.link ?? `${site.siteUrl}/${slug}`,
8495
+ adminUrl: `${site.siteUrl}/wp-admin/`,
8496
+ content: body.json,
8497
+ nextSteps: [
8498
+ `Open the WordPress editor or theme/plugin settings for ${page.slug}.`,
8499
+ `Add the ${body.type ?? "custom"} JSON-LD block to the page or the site-level schema tool you use.`,
8500
+ "Publish/update the page and refresh the public URL to confirm the JSON-LD block is rendered."
8501
+ ]
8502
+ };
8503
+ }
8504
+ async function runAudit(connection, env) {
8505
+ const site = resolveEnvironment(connection, env);
8506
+ const pages = await listPages(connection, site.env);
8507
+ const plugins = await listActivePlugins(connection, site.env);
8508
+ const details = await mapWithConcurrency(
8509
+ pages,
8510
+ 5,
8511
+ async (page) => getPageDetail(connection, page.slug, site.env, plugins)
8512
+ );
8513
+ const auditPages = [];
8514
+ const allIssues = [];
8515
+ for (const page of details) {
8516
+ const issues = [];
8517
+ const wordCount = computeWordCount(page.content);
8518
+ const schemaPresent = page.schemaBlocks.length > 0;
8519
+ if (page.status === "publish" && page.seo.noindex === true) {
8520
+ issues.push({
8521
+ slug: page.slug,
8522
+ severity: "high",
8523
+ code: "noindex",
8524
+ message: "Published page is marked noindex."
8525
+ });
8526
+ }
8527
+ if (!page.seo.title) {
8528
+ issues.push({
8529
+ slug: page.slug,
8530
+ severity: "medium",
8531
+ code: "missing-seo-title",
8532
+ message: "Rendered page title is missing."
8533
+ });
8534
+ }
8535
+ if (!page.seo.description) {
8536
+ issues.push({
8537
+ slug: page.slug,
8538
+ severity: "medium",
8539
+ code: "missing-meta-description",
8540
+ message: "Rendered meta description is missing."
8541
+ });
8542
+ }
8543
+ if (!schemaPresent) {
8544
+ issues.push({
8545
+ slug: page.slug,
8546
+ severity: "medium",
8547
+ code: "missing-schema",
8548
+ message: "No JSON-LD schema was detected on the rendered page."
8549
+ });
8550
+ }
8551
+ if (wordCount < THIN_CONTENT_WORD_COUNT) {
8552
+ issues.push({
8553
+ slug: page.slug,
8554
+ severity: "low",
8555
+ code: "thin-content",
8556
+ message: `Page content is thin (${wordCount} words; target at least ${THIN_CONTENT_WORD_COUNT}).`
8557
+ });
8558
+ }
8559
+ auditPages.push({
8560
+ slug: page.slug,
8561
+ title: page.title,
8562
+ status: page.status,
8563
+ wordCount,
8564
+ seo: page.seo,
8565
+ schemaPresent,
8566
+ issues
8567
+ });
8568
+ allIssues.push(...issues);
8569
+ }
8570
+ const severityWeight = { high: 0, medium: 1, low: 2 };
8571
+ allIssues.sort((a, b) => {
8572
+ return severityWeight[a.severity] - severityWeight[b.severity] || a.slug.localeCompare(b.slug);
8573
+ });
8574
+ return {
8575
+ env: site.env,
8576
+ pages: auditPages,
8577
+ issues: allIssues
8578
+ };
8579
+ }
8580
+ async function diffPageAcrossEnvironments(connection, slug) {
8581
+ if (!connection.stagingUrl) {
8582
+ throw new WordpressApiError("VALIDATION_ERROR", "No staging URL configured for this project. Reconnect with --staging-url before using diff.", 400);
8583
+ }
8584
+ const [livePlugins, stagingPlugins] = await Promise.all([
8585
+ listActivePlugins(connection, "live"),
8586
+ listActivePlugins(connection, "staging")
8587
+ ]);
8588
+ const live = await getPageDetail(connection, slug, "live", livePlugins);
8589
+ const staging = await getPageDetail(connection, slug, "staging", stagingPlugins);
8590
+ const liveContentHash = contentHash(live.content);
8591
+ const stagingContentHash = contentHash(staging.content);
8592
+ const liveDiff = {
8593
+ ...live,
8594
+ contentHash: liveContentHash,
8595
+ contentSnippet: buildSnippet(live.content)
8596
+ };
8597
+ const stagingDiff = {
8598
+ ...staging,
8599
+ contentHash: stagingContentHash,
8600
+ contentSnippet: buildSnippet(staging.content)
8601
+ };
8602
+ const differences = {
8603
+ title: live.title !== staging.title,
8604
+ slug: live.slug !== staging.slug,
8605
+ content: liveContentHash !== stagingContentHash,
8606
+ seoTitle: live.seo.title !== staging.seo.title,
8607
+ seoDescription: live.seo.description !== staging.seo.description,
8608
+ noindex: live.seo.noindex !== staging.seo.noindex,
8609
+ schema: JSON.stringify(live.schemaBlocks) !== JSON.stringify(staging.schemaBlocks)
8610
+ };
8611
+ return {
8612
+ slug,
8613
+ live: liveDiff,
8614
+ staging: stagingDiff,
8615
+ hasDifferences: Object.values(differences).some(Boolean),
8616
+ differences
8617
+ };
8618
+ }
8619
+ async function buildManualStagingPush(connection) {
8620
+ const liveStatus = await getSiteStatus(connection, "live");
8621
+ const plugins = liveStatus.plugins ?? [];
8622
+ const wpStagingActive = plugins.some((plugin) => plugin.includes("wp-staging"));
8623
+ return {
8624
+ manualRequired: true,
8625
+ targetUrl: getWpStagingAdminUrl(connection.url),
8626
+ adminUrl: getWpStagingAdminUrl(connection.url),
8627
+ content: JSON.stringify({
8628
+ liveUrl: connection.url,
8629
+ stagingUrl: connection.stagingUrl ?? null,
8630
+ wpStagingActive
8631
+ }, null, 2),
8632
+ nextSteps: [
8633
+ "Open the WP STAGING admin page on the live site.",
8634
+ wpStagingActive ? "Review the staging site snapshot you want to push and use the plugin UI to start the push-to-live workflow." : "Confirm the WP STAGING plugin is installed and active before attempting a push-to-live operation.",
8635
+ "Complete the plugin confirmation steps in wp-admin and verify the live site after the push finishes."
8636
+ ]
8637
+ };
8638
+ }
8639
+ function parseEnv(value) {
8640
+ if (typeof value !== "string") return void 0;
8641
+ const parsed = wordpressEnvSchema.safeParse(value);
8642
+ return parsed.success ? parsed.data : void 0;
8643
+ }
8644
+
8645
+ // ../api-routes/src/wordpress.ts
8646
+ function parseEnvInput(value, fieldName = "env") {
8647
+ const env = parseEnv(value);
8648
+ if (!env && value != null) {
8649
+ throw validationError(`${fieldName} must be "live" or "staging"`);
8650
+ }
8651
+ return env;
8652
+ }
8653
+ function sendWordpressError(reply, error) {
8654
+ if (!(error instanceof WordpressApiError)) return false;
8655
+ let appError;
8656
+ switch (error.code) {
8657
+ case "AUTH_INVALID":
8658
+ appError = new AppError("AUTH_INVALID", error.message, error.statusCode);
8659
+ break;
8660
+ case "NOT_FOUND":
8661
+ appError = new AppError("NOT_FOUND", error.message, error.statusCode);
8662
+ break;
8663
+ case "UPSTREAM_ERROR":
8664
+ appError = providerError(error.message, { statusCode: error.statusCode });
8665
+ break;
8666
+ case "UNSUPPORTED":
8667
+ case "VALIDATION_ERROR":
8668
+ appError = validationError(error.message);
8669
+ break;
8670
+ default:
8671
+ appError = providerError(error.message, { statusCode: error.statusCode });
8672
+ break;
8673
+ }
8674
+ reply.status(appError.statusCode).send(appError.toJSON());
8675
+ return true;
8676
+ }
8677
+ async function withWordpressErrorHandling(reply, handler) {
8678
+ try {
8679
+ return await handler();
8680
+ } catch (error) {
8681
+ if (sendWordpressError(reply, error)) return;
8682
+ throw error;
8683
+ }
8684
+ }
8685
+ async function wordpressRoutes(app, opts) {
8686
+ function requireStore(reply) {
8687
+ if (opts.wordpressConnectionStore) return opts.wordpressConnectionStore;
8688
+ const err = validationError("WordPress connection storage is not configured for this deployment");
8689
+ reply.status(err.statusCode).send(err.toJSON());
8690
+ return null;
8691
+ }
8692
+ function requireConnection(store, projectName, reply) {
8693
+ const connection = store.getConnection(projectName);
8694
+ if (!connection) {
8695
+ const err = validationError(`No WordPress connection found for project "${projectName}". Run "canonry wordpress connect ${projectName}" first.`);
8696
+ reply.status(err.statusCode).send(err.toJSON());
8697
+ return null;
8698
+ }
8699
+ return connection;
8700
+ }
8701
+ app.post("/projects/:name/wordpress/connect", async (request, reply) => {
8702
+ return withWordpressErrorHandling(reply, async () => {
8703
+ const store = requireStore(reply);
8704
+ if (!store) return;
8705
+ const project = resolveProject(app.db, request.params.name);
8706
+ const { url, stagingUrl, username, appPassword } = request.body ?? {};
8707
+ if (!url || !username || !appPassword) {
8708
+ const err = validationError("url, username, and appPassword are required");
8709
+ return reply.status(err.statusCode).send(err.toJSON());
8710
+ }
8711
+ const defaultEnv = parseEnvInput(request.body?.defaultEnv, "defaultEnv") ?? (stagingUrl ? "staging" : "live");
8712
+ if (defaultEnv === "staging" && !stagingUrl) {
8713
+ const err = validationError('defaultEnv "staging" requires stagingUrl');
8714
+ return reply.status(err.statusCode).send(err.toJSON());
8715
+ }
8716
+ const now = (/* @__PURE__ */ new Date()).toISOString();
8717
+ const existing = store.getConnection(project.name);
8718
+ const nextConnection = {
8719
+ projectName: project.name,
8720
+ url,
8721
+ stagingUrl,
8722
+ username,
8723
+ appPassword,
8724
+ defaultEnv,
8725
+ createdAt: existing?.createdAt ?? now,
8726
+ updatedAt: now
8727
+ };
8728
+ await verifyWordpressConnection(nextConnection);
8729
+ const connection = store.upsertConnection(nextConnection);
8730
+ const live = await getSiteStatus(connection, "live");
8731
+ const staging = connection.stagingUrl ? await getSiteStatus(connection, "staging") : null;
8732
+ writeAuditLog(app.db, {
8733
+ projectId: project.id,
8734
+ actor: "api",
8735
+ action: "wordpress.connected",
8736
+ entityType: "wordpress_connection",
8737
+ entityId: project.name
8738
+ });
8739
+ return {
8740
+ connected: true,
8741
+ projectName: project.name,
8742
+ defaultEnv: connection.defaultEnv,
8743
+ live,
8744
+ staging,
8745
+ adminUrl: getWpStagingAdminUrl(connection.url)
8746
+ };
8747
+ });
8748
+ });
8749
+ app.delete("/projects/:name/wordpress/disconnect", async (request, reply) => {
8750
+ const store = requireStore(reply);
8751
+ if (!store) return;
8752
+ const project = resolveProject(app.db, request.params.name);
8753
+ const deleted = store.deleteConnection(project.name);
8754
+ if (!deleted) {
8755
+ const err = notFound("WordPress connection", project.name);
8756
+ return reply.status(err.statusCode).send(err.toJSON());
8757
+ }
8758
+ writeAuditLog(app.db, {
8759
+ projectId: project.id,
8760
+ actor: "api",
8761
+ action: "wordpress.disconnected",
8762
+ entityType: "wordpress_connection",
8763
+ entityId: project.name
8764
+ });
8765
+ return reply.status(204).send();
8766
+ });
8767
+ app.get("/projects/:name/wordpress/status", async (request) => {
8768
+ const project = resolveProject(app.db, request.params.name);
8769
+ const connection = opts.wordpressConnectionStore?.getConnection(project.name);
8770
+ if (!connection) {
8771
+ return {
8772
+ connected: false,
8773
+ projectName: project.name,
8774
+ defaultEnv: "live",
8775
+ live: null,
8776
+ staging: null,
8777
+ adminUrl: null
8778
+ };
8779
+ }
8780
+ const live = await getSiteStatus(connection, "live");
8781
+ const staging = connection.stagingUrl ? await getSiteStatus(connection, "staging") : null;
8782
+ return {
8783
+ connected: true,
8784
+ projectName: project.name,
8785
+ defaultEnv: connection.defaultEnv,
8786
+ live,
8787
+ staging,
8788
+ adminUrl: getWpStagingAdminUrl(connection.url)
8789
+ };
8790
+ });
8791
+ app.get("/projects/:name/wordpress/pages", async (request, reply) => {
8792
+ return withWordpressErrorHandling(reply, async () => {
8793
+ const store = requireStore(reply);
8794
+ if (!store) return;
8795
+ const project = resolveProject(app.db, request.params.name);
8796
+ const connection = requireConnection(store, project.name, reply);
8797
+ if (!connection) return;
8798
+ const env = parseEnvInput(request.query?.env);
8799
+ return {
8800
+ env: env ?? connection.defaultEnv,
8801
+ pages: await listPages(connection, env)
8802
+ };
8803
+ });
8804
+ });
8805
+ app.get("/projects/:name/wordpress/page", async (request, reply) => {
8806
+ return withWordpressErrorHandling(reply, async () => {
8807
+ const store = requireStore(reply);
8808
+ if (!store) return;
8809
+ const project = resolveProject(app.db, request.params.name);
8810
+ const connection = requireConnection(store, project.name, reply);
8811
+ if (!connection) return;
8812
+ const slug = request.query?.slug?.trim();
8813
+ if (!slug) {
8814
+ const err = validationError("slug is required");
8815
+ return reply.status(err.statusCode).send(err.toJSON());
8816
+ }
8817
+ const env = parseEnvInput(request.query?.env);
8818
+ return getPageDetail(connection, slug, env);
8819
+ });
8820
+ });
8821
+ app.post("/projects/:name/wordpress/pages", async (request, reply) => {
8822
+ return withWordpressErrorHandling(reply, async () => {
8823
+ const store = requireStore(reply);
8824
+ if (!store) return;
8825
+ const project = resolveProject(app.db, request.params.name);
8826
+ const connection = requireConnection(store, project.name, reply);
8827
+ if (!connection) return;
8828
+ const { title, slug, content, status } = request.body ?? {};
8829
+ const env = parseEnvInput(request.body?.env);
8830
+ if (!title || !slug || !content) {
8831
+ const err = validationError("title, slug, and content are required");
8832
+ return reply.status(err.statusCode).send(err.toJSON());
8833
+ }
8834
+ const created = await createPage(connection, { title, slug, content, status }, env);
8835
+ writeAuditLog(app.db, {
8836
+ projectId: project.id,
8837
+ actor: "api",
8838
+ action: "wordpress.page-created",
8839
+ entityType: "wordpress_page",
8840
+ entityId: created.slug
8841
+ });
8842
+ return created;
8843
+ });
8844
+ });
8845
+ app.put("/projects/:name/wordpress/page", async (request, reply) => {
8846
+ return withWordpressErrorHandling(reply, async () => {
8847
+ const store = requireStore(reply);
8848
+ if (!store) return;
8849
+ const project = resolveProject(app.db, request.params.name);
8850
+ const connection = requireConnection(store, project.name, reply);
8851
+ if (!connection) return;
8852
+ const currentSlug = request.body?.currentSlug?.trim();
8853
+ if (!currentSlug) {
8854
+ const err = validationError("currentSlug is required");
8855
+ return reply.status(err.statusCode).send(err.toJSON());
8856
+ }
8857
+ const env = parseEnvInput(request.body?.env);
8858
+ const updated = await updatePageBySlug(connection, currentSlug, {
8859
+ title: request.body?.title,
8860
+ slug: request.body?.slug,
8861
+ content: request.body?.content,
8862
+ status: request.body?.status
8863
+ }, env);
8864
+ writeAuditLog(app.db, {
8865
+ projectId: project.id,
8866
+ actor: "api",
8867
+ action: "wordpress.page-updated",
8868
+ entityType: "wordpress_page",
8869
+ entityId: currentSlug
8870
+ });
8871
+ return updated;
8872
+ });
8873
+ });
8874
+ app.post("/projects/:name/wordpress/page/meta", async (request, reply) => {
8875
+ return withWordpressErrorHandling(reply, async () => {
8876
+ const store = requireStore(reply);
8877
+ if (!store) return;
8878
+ const project = resolveProject(app.db, request.params.name);
8879
+ const connection = requireConnection(store, project.name, reply);
8880
+ if (!connection) return;
8881
+ const slug = request.body?.slug?.trim();
8882
+ if (!slug) {
8883
+ const err = validationError("slug is required");
8884
+ return reply.status(err.statusCode).send(err.toJSON());
8885
+ }
8886
+ const env = parseEnvInput(request.body?.env);
8887
+ const updated = await setSeoMeta(connection, slug, {
8888
+ title: request.body?.title,
8889
+ description: request.body?.description,
8890
+ noindex: request.body?.noindex
8891
+ }, env);
8892
+ writeAuditLog(app.db, {
8893
+ projectId: project.id,
8894
+ actor: "api",
8895
+ action: "wordpress.page-meta-updated",
8896
+ entityType: "wordpress_page",
8897
+ entityId: slug
8898
+ });
8899
+ return updated;
8900
+ });
8901
+ });
8902
+ app.get("/projects/:name/wordpress/schema", async (request, reply) => {
8903
+ return withWordpressErrorHandling(reply, async () => {
8904
+ const store = requireStore(reply);
8905
+ if (!store) return;
8906
+ const project = resolveProject(app.db, request.params.name);
8907
+ const connection = requireConnection(store, project.name, reply);
8908
+ if (!connection) return;
8909
+ const slug = request.query?.slug?.trim();
8910
+ if (!slug) {
8911
+ const err = validationError("slug is required");
8912
+ return reply.status(err.statusCode).send(err.toJSON());
8913
+ }
8914
+ const env = parseEnvInput(request.query?.env);
8915
+ return getPageSchema(connection, slug, env);
8916
+ });
8917
+ });
8918
+ app.post("/projects/:name/wordpress/schema/manual", async (request, reply) => {
8919
+ return withWordpressErrorHandling(reply, async () => {
8920
+ const store = requireStore(reply);
8921
+ if (!store) return;
8922
+ const project = resolveProject(app.db, request.params.name);
8923
+ const connection = requireConnection(store, project.name, reply);
8924
+ if (!connection) return;
8925
+ const slug = request.body?.slug?.trim();
8926
+ const json = request.body?.json;
8927
+ if (!slug || !json) {
8928
+ const err = validationError("slug and json are required");
8929
+ return reply.status(err.statusCode).send(err.toJSON());
8930
+ }
8931
+ const env = parseEnvInput(request.body?.env);
8932
+ return buildManualSchemaUpdate(connection, slug, { type: request.body?.type, json }, env);
8933
+ });
8934
+ });
8935
+ app.get("/projects/:name/wordpress/llms-txt", async (request, reply) => {
8936
+ return withWordpressErrorHandling(reply, async () => {
8937
+ const store = requireStore(reply);
8938
+ if (!store) return;
8939
+ const project = resolveProject(app.db, request.params.name);
8940
+ const connection = requireConnection(store, project.name, reply);
8941
+ if (!connection) return;
8942
+ const env = parseEnvInput(request.query?.env);
8943
+ return getLlmsTxt(connection, env);
8944
+ });
8945
+ });
8946
+ app.post("/projects/:name/wordpress/llms-txt/manual", async (request, reply) => {
8947
+ return withWordpressErrorHandling(reply, async () => {
8948
+ const store = requireStore(reply);
8949
+ if (!store) return;
8950
+ const project = resolveProject(app.db, request.params.name);
8951
+ const connection = requireConnection(store, project.name, reply);
8952
+ if (!connection) return;
8953
+ const content = request.body?.content;
8954
+ if (!content) {
8955
+ const err = validationError("content is required");
8956
+ return reply.status(err.statusCode).send(err.toJSON());
8957
+ }
8958
+ const env = parseEnvInput(request.body?.env);
8959
+ return buildManualLlmsTxtUpdate(connection, content, env);
8960
+ });
8961
+ });
8962
+ app.get("/projects/:name/wordpress/audit", async (request, reply) => {
8963
+ return withWordpressErrorHandling(reply, async () => {
8964
+ const store = requireStore(reply);
8965
+ if (!store) return;
8966
+ const project = resolveProject(app.db, request.params.name);
8967
+ const connection = requireConnection(store, project.name, reply);
8968
+ if (!connection) return;
8969
+ const env = parseEnvInput(request.query?.env);
8970
+ return runAudit(connection, env);
8971
+ });
8972
+ });
8973
+ app.get("/projects/:name/wordpress/diff", async (request, reply) => {
8974
+ return withWordpressErrorHandling(reply, async () => {
8975
+ const store = requireStore(reply);
8976
+ if (!store) return;
8977
+ const project = resolveProject(app.db, request.params.name);
8978
+ const connection = requireConnection(store, project.name, reply);
8979
+ if (!connection) return;
8980
+ const slug = request.query?.slug?.trim();
8981
+ if (!slug) {
8982
+ const err = validationError("slug is required");
8983
+ return reply.status(err.statusCode).send(err.toJSON());
8984
+ }
8985
+ return diffPageAcrossEnvironments(connection, slug);
8986
+ });
8987
+ });
8988
+ app.get("/projects/:name/wordpress/staging/status", async (request, reply) => {
8989
+ return withWordpressErrorHandling(reply, async () => {
8990
+ const store = requireStore(reply);
8991
+ if (!store) return;
8992
+ const project = resolveProject(app.db, request.params.name);
8993
+ const connection = requireConnection(store, project.name, reply);
8994
+ if (!connection) return;
8995
+ const plugins = await listActivePlugins(connection, "live");
8996
+ return {
8997
+ stagingConfigured: Boolean(connection.stagingUrl),
8998
+ stagingUrl: connection.stagingUrl ?? null,
8999
+ wpStagingActive: Boolean(plugins?.some((plugin) => plugin.includes("wp-staging"))),
9000
+ adminUrl: getWpStagingAdminUrl(connection.url)
9001
+ };
9002
+ });
9003
+ });
9004
+ app.post("/projects/:name/wordpress/staging/push", async (request, reply) => {
9005
+ return withWordpressErrorHandling(reply, async () => {
9006
+ const store = requireStore(reply);
9007
+ if (!store) return;
9008
+ const project = resolveProject(app.db, request.params.name);
9009
+ const connection = requireConnection(store, project.name, reply);
9010
+ if (!connection) return;
9011
+ if (!connection.stagingUrl) {
9012
+ const err = validationError("No staging URL configured for this project. Reconnect with --staging-url before using staging push.");
9013
+ return reply.status(err.statusCode).send(err.toJSON());
9014
+ }
9015
+ return buildManualStagingPush(connection);
9016
+ });
9017
+ });
9018
+ }
9019
+
9020
+ // ../api-routes/src/index.ts
9021
+ async function apiRoutes(app, opts) {
9022
+ app.decorate("db", opts.db);
9023
+ app.setErrorHandler((error, _request, reply) => {
9024
+ if (error instanceof AppError) {
9025
+ return reply.status(error.statusCode).send(error.toJSON());
9026
+ }
9027
+ const httpStatus = error.statusCode ?? error.status ?? 500;
9028
+ if (httpStatus >= 400 && httpStatus < 500) {
9029
+ return reply.status(httpStatus).send({
9030
+ error: {
9031
+ code: httpStatus === 401 ? "AUTH_INVALID" : httpStatus === 403 ? "FORBIDDEN" : httpStatus === 404 ? "NOT_FOUND" : httpStatus === 429 ? "QUOTA_EXCEEDED" : "VALIDATION_ERROR",
9032
+ message: error.message
9033
+ }
9034
+ });
9035
+ }
7588
9036
  app.log.error(error);
7589
9037
  return reply.status(500).send({
7590
9038
  error: {
@@ -7655,6 +9103,9 @@ async function apiRoutes(app, opts) {
7655
9103
  onGscSyncRequested: opts.onGscSyncRequested,
7656
9104
  onInspectSitemapRequested: opts.onInspectSitemapRequested
7657
9105
  });
9106
+ await api.register(wordpressRoutes, {
9107
+ wordpressConnectionStore: opts.wordpressConnectionStore
9108
+ });
7658
9109
  await api.register(cdpRoutes, {
7659
9110
  getCdpStatus: opts.getCdpStatus,
7660
9111
  onCdpScreenshot: opts.onCdpScreenshot,
@@ -7667,8 +9118,7 @@ async function apiRoutes(app, opts) {
7667
9118
  }
7668
9119
 
7669
9120
  // ../provider-gemini/src/normalize.ts
7670
- import { GoogleGenerativeAI } from "@google/generative-ai";
7671
- import { VertexAI } from "@google-cloud/vertexai";
9121
+ import { GoogleGenAI } from "@google/genai";
7672
9122
  var DEFAULT_MODEL = "gemini-3-flash";
7673
9123
  var VALIDATION_PATTERN = /^gemini-/;
7674
9124
  function isVertexConfig(config) {
@@ -7684,31 +9134,16 @@ function resolveModel(config) {
7684
9134
  );
7685
9135
  return DEFAULT_MODEL;
7686
9136
  }
7687
- function wrapVertexResponse(response) {
7688
- return {
7689
- text: () => {
7690
- const parts = response.candidates?.[0]?.content?.parts;
7691
- return parts?.map((p) => p.text ?? "").join("") ?? "";
7692
- },
7693
- candidates: response.candidates?.map((c) => ({
7694
- content: c.content,
7695
- finishReason: c.finishReason,
7696
- groundingMetadata: c.groundingMetadata
7697
- })),
7698
- usageMetadata: response.usageMetadata
7699
- };
7700
- }
7701
- function createVertexModel(config, model, tools) {
7702
- const vertexOpts = {
7703
- project: config.vertexProject,
7704
- location: config.vertexRegion || "us-central1",
7705
- googleAuthOptions: config.vertexCredentials ? { keyFilename: config.vertexCredentials } : void 0
7706
- };
7707
- const vertexAI = new VertexAI(vertexOpts);
7708
- return vertexAI.getGenerativeModel({
7709
- model,
7710
- ...tools ? { tools } : {}
7711
- });
9137
+ function createClient2(config) {
9138
+ if (isVertexConfig(config)) {
9139
+ return new GoogleGenAI({
9140
+ vertexai: true,
9141
+ project: config.vertexProject,
9142
+ location: config.vertexRegion || "us-central1",
9143
+ ...config.vertexCredentials ? { googleAuthOptions: { keyFilename: config.vertexCredentials } } : {}
9144
+ });
9145
+ }
9146
+ return new GoogleGenAI({ apiKey: config.apiKey });
7712
9147
  }
7713
9148
  function validateConfig(config) {
7714
9149
  if (isVertexConfig(config)) {
@@ -7737,28 +9172,19 @@ async function healthcheck(config) {
7737
9172
  if (!validation.ok) return validation;
7738
9173
  try {
7739
9174
  const model = resolveModel(config);
7740
- if (isVertexConfig(config)) {
7741
- const generativeModel = createVertexModel(config, model);
7742
- const result = await generativeModel.generateContent('Say "ok"');
7743
- const text2 = result.response.candidates?.[0]?.content?.parts?.map((p) => p.text ?? "").join("") ?? "";
7744
- return {
7745
- ok: text2.length > 0,
7746
- provider: "gemini",
7747
- message: text2.length > 0 ? "gemini vertex ai verified" : "empty response from gemini vertex ai",
7748
- model
7749
- };
7750
- } else {
7751
- const genAI = new GoogleGenerativeAI(config.apiKey);
7752
- const generativeModel = genAI.getGenerativeModel({ model });
7753
- const result = await generativeModel.generateContent('Say "ok"');
7754
- const text2 = result.response.text();
7755
- return {
7756
- ok: text2.length > 0,
7757
- provider: "gemini",
7758
- message: text2.length > 0 ? "gemini api key verified" : "empty response from gemini",
7759
- model
7760
- };
7761
- }
9175
+ const client = createClient2(config);
9176
+ const result = await client.models.generateContent({
9177
+ model,
9178
+ contents: 'Say "ok"'
9179
+ });
9180
+ const text2 = result.text ?? "";
9181
+ const backend = isVertexConfig(config) ? "vertex ai" : "api key";
9182
+ return {
9183
+ ok: text2.length > 0,
9184
+ provider: "gemini",
9185
+ message: text2.length > 0 ? `gemini ${backend} verified` : `empty response from gemini ${backend}`,
9186
+ model
9187
+ };
7762
9188
  } catch (err) {
7763
9189
  return {
7764
9190
  ok: false,
@@ -7771,40 +9197,23 @@ async function healthcheck(config) {
7771
9197
  async function executeTrackedQuery(input) {
7772
9198
  const model = resolveModel(input.config);
7773
9199
  const prompt = buildPrompt(input.keyword, input.location);
7774
- if (isVertexConfig(input.config)) {
7775
- const vertexSearchTool = { googleSearchRetrieval: {} };
7776
- const generativeModel = createVertexModel(input.config, model, [vertexSearchTool]);
7777
- const result = await generativeModel.generateContent(prompt);
7778
- const response = result.response;
7779
- const unified = wrapVertexResponse(response);
7780
- const groundingMetadata = extractGroundingMetadataFromUnified(unified);
7781
- const searchQueries = extractSearchQueriesFromUnified(unified);
7782
- return {
7783
- provider: "gemini",
7784
- rawResponse: unifiedToRecord(unified),
7785
- model,
7786
- groundingSources: groundingMetadata,
7787
- searchQueries
7788
- };
7789
- } else {
7790
- const searchTool = { googleSearch: {} };
7791
- const genAI = new GoogleGenerativeAI(input.config.apiKey);
7792
- const generativeModel = genAI.getGenerativeModel({
7793
- model,
7794
- tools: [searchTool]
7795
- });
7796
- const result = await generativeModel.generateContent(prompt);
7797
- const response = result.response;
7798
- const groundingMetadata = extractGroundingMetadata(response);
7799
- const searchQueries = extractSearchQueries(response);
7800
- return {
7801
- provider: "gemini",
7802
- rawResponse: responseToRecord(response),
7803
- model,
7804
- groundingSources: groundingMetadata,
7805
- searchQueries
7806
- };
7807
- }
9200
+ const client = createClient2(input.config);
9201
+ const result = await client.models.generateContent({
9202
+ model,
9203
+ contents: prompt,
9204
+ config: {
9205
+ tools: [{ googleSearch: {} }]
9206
+ }
9207
+ });
9208
+ const groundingSources = extractGroundingMetadata(result);
9209
+ const searchQueries = extractSearchQueries(result);
9210
+ return {
9211
+ provider: "gemini",
9212
+ rawResponse: responseToRecord(result),
9213
+ model,
9214
+ groundingSources,
9215
+ searchQueries
9216
+ };
7808
9217
  }
7809
9218
  function normalizeResult(raw) {
7810
9219
  const answerText = extractAnswerText(raw.rawResponse);
@@ -7850,22 +9259,6 @@ function extractGroundingMetadata(response) {
7850
9259
  return [];
7851
9260
  }
7852
9261
  }
7853
- function extractGroundingMetadataFromUnified(response) {
7854
- try {
7855
- const candidate = response.candidates?.[0];
7856
- if (!candidate) return [];
7857
- const metadata = candidate.groundingMetadata;
7858
- if (!metadata) return [];
7859
- const chunks = metadata.groundingChunks;
7860
- if (!chunks) return [];
7861
- return chunks.filter((chunk) => chunk.web?.uri).map((chunk) => ({
7862
- uri: chunk.web.uri,
7863
- title: chunk.web?.title ?? ""
7864
- }));
7865
- } catch {
7866
- return [];
7867
- }
7868
- }
7869
9262
  function extractSearchQueries(response) {
7870
9263
  try {
7871
9264
  const candidate = response.candidates?.[0];
@@ -7874,14 +9267,6 @@ function extractSearchQueries(response) {
7874
9267
  return [];
7875
9268
  }
7876
9269
  }
7877
- function extractSearchQueriesFromUnified(response) {
7878
- try {
7879
- const candidate = response.candidates?.[0];
7880
- return candidate?.groundingMetadata?.webSearchQueries ?? [];
7881
- } catch {
7882
- return [];
7883
- }
7884
- }
7885
9270
  function extractCitedDomains(raw) {
7886
9271
  const domains = /* @__PURE__ */ new Set();
7887
9272
  for (const source of raw.groundingSources) {
@@ -7929,16 +9314,12 @@ function extractDomainFromUri(uri) {
7929
9314
  }
7930
9315
  async function generateText(prompt, config) {
7931
9316
  const model = resolveModel(config);
7932
- if (isVertexConfig(config)) {
7933
- const generativeModel = createVertexModel(config, model);
7934
- const result = await generativeModel.generateContent(prompt);
7935
- return result.response.candidates?.[0]?.content?.parts?.map((p) => p.text ?? "").join("") ?? "";
7936
- } else {
7937
- const genAI = new GoogleGenerativeAI(config.apiKey);
7938
- const generativeModel = genAI.getGenerativeModel({ model });
7939
- const result = await generativeModel.generateContent(prompt);
7940
- return result.response.text();
7941
- }
9317
+ const client = createClient2(config);
9318
+ const result = await client.models.generateContent({
9319
+ model,
9320
+ contents: prompt
9321
+ });
9322
+ return result.text ?? "";
7942
9323
  }
7943
9324
  function responseToRecord(response) {
7944
9325
  try {
@@ -7958,24 +9339,6 @@ function responseToRecord(response) {
7958
9339
  return { error: "failed to serialize response" };
7959
9340
  }
7960
9341
  }
7961
- function unifiedToRecord(response) {
7962
- try {
7963
- const candidates = response.candidates?.map((c) => ({
7964
- content: c.content,
7965
- finishReason: c.finishReason,
7966
- groundingMetadata: c.groundingMetadata ? {
7967
- webSearchQueries: c.groundingMetadata.webSearchQueries,
7968
- groundingChunks: c.groundingMetadata.groundingChunks
7969
- } : void 0
7970
- }));
7971
- return {
7972
- candidates: candidates ?? [],
7973
- usageMetadata: response.usageMetadata ?? null
7974
- };
7975
- } catch {
7976
- return { error: "failed to serialize response" };
7977
- }
7978
- }
7979
9342
 
7980
9343
  // ../provider-gemini/src/adapter.ts
7981
9344
  function toGeminiConfig(config) {
@@ -9648,8 +11011,59 @@ function removeGa4Connection(config, projectName) {
9648
11011
  return true;
9649
11012
  }
9650
11013
 
11014
+ // src/wordpress-config.ts
11015
+ function ensureConnections3(config) {
11016
+ if (!config.wordpress) config.wordpress = {};
11017
+ if (!config.wordpress.connections) config.wordpress.connections = [];
11018
+ return config.wordpress.connections;
11019
+ }
11020
+ function normalizeConnection(connection) {
11021
+ return {
11022
+ ...connection,
11023
+ url: connection.url.replace(/\/$/, ""),
11024
+ stagingUrl: connection.stagingUrl?.replace(/\/$/, ""),
11025
+ defaultEnv: connection.defaultEnv ?? "live"
11026
+ };
11027
+ }
11028
+ function getWordpressConnection(config, projectName) {
11029
+ return (config.wordpress?.connections ?? []).find((connection) => connection.projectName === projectName);
11030
+ }
11031
+ function upsertWordpressConnection(config, connection) {
11032
+ const connections = ensureConnections3(config);
11033
+ const normalized = normalizeConnection(connection);
11034
+ const index2 = connections.findIndex((entry) => entry.projectName === connection.projectName);
11035
+ if (index2 === -1) {
11036
+ connections.push(normalized);
11037
+ return normalized;
11038
+ }
11039
+ connections[index2] = normalized;
11040
+ return normalized;
11041
+ }
11042
+ function patchWordpressConnection(config, projectName, patch) {
11043
+ const existing = getWordpressConnection(config, projectName);
11044
+ if (!existing) return void 0;
11045
+ return upsertWordpressConnection(config, {
11046
+ ...existing,
11047
+ ...patch,
11048
+ stagingUrl: Object.prototype.hasOwnProperty.call(patch, "stagingUrl") ? patch.stagingUrl : existing.stagingUrl,
11049
+ defaultEnv: patch.defaultEnv ?? existing.defaultEnv ?? "live"
11050
+ });
11051
+ }
11052
+ function removeWordpressConnection(config, projectName) {
11053
+ const connections = config.wordpress?.connections;
11054
+ if (!connections?.length) return false;
11055
+ const next = connections.filter((connection) => connection.projectName !== projectName);
11056
+ if (next.length === connections.length) return false;
11057
+ if (!config.wordpress) return false;
11058
+ config.wordpress.connections = next;
11059
+ if (next.length === 0) {
11060
+ delete config.wordpress;
11061
+ }
11062
+ return true;
11063
+ }
11064
+
9651
11065
  // src/job-runner.ts
9652
- import crypto17 from "crypto";
11066
+ import crypto18 from "crypto";
9653
11067
  import fs4 from "fs";
9654
11068
  import path5 from "path";
9655
11069
  import os4 from "os";
@@ -9892,7 +11306,7 @@ var JobRunner = class {
9892
11306
  const overlap = computeCompetitorOverlap(normalized, competitorDomains);
9893
11307
  let screenshotRelPath = null;
9894
11308
  if (raw.screenshotPath && fs4.existsSync(raw.screenshotPath)) {
9895
- const snapshotId = crypto17.randomUUID();
11309
+ const snapshotId = crypto18.randomUUID();
9896
11310
  const screenshotDir = path5.join(os4.homedir(), ".canonry", "screenshots", runId);
9897
11311
  if (!fs4.existsSync(screenshotDir)) fs4.mkdirSync(screenshotDir, { recursive: true });
9898
11312
  const destPath = path5.join(screenshotDir, `${snapshotId}.png`);
@@ -9920,7 +11334,7 @@ var JobRunner = class {
9920
11334
  }).run();
9921
11335
  } else {
9922
11336
  this.db.insert(querySnapshots).values({
9923
- id: crypto17.randomUUID(),
11337
+ id: crypto18.randomUUID(),
9924
11338
  runId,
9925
11339
  keywordId: kw.id,
9926
11340
  provider: providerName,
@@ -10029,7 +11443,7 @@ var JobRunner = class {
10029
11443
  incrementUsage(scope, metric, count) {
10030
11444
  const now = /* @__PURE__ */ new Date();
10031
11445
  const period = now.toISOString().slice(0, 10);
10032
- const id = crypto17.randomUUID();
11446
+ const id = crypto18.randomUUID();
10033
11447
  const existing = this.db.select().from(usageCounters).where(eq17(usageCounters.scope, scope)).all().find((r) => r.period === period && r.metric === metric);
10034
11448
  if (existing) {
10035
11449
  this.db.update(usageCounters).set({ count: existing.count + count, updatedAt: now.toISOString() }).where(eq17(usageCounters.id, existing.id)).run();
@@ -10154,7 +11568,7 @@ function computeCompetitorOverlap(normalized, competitorDomains) {
10154
11568
  }
10155
11569
 
10156
11570
  // src/gsc-sync.ts
10157
- import crypto18 from "crypto";
11571
+ import crypto19 from "crypto";
10158
11572
  import { eq as eq18, and as and8, sql as sql4 } from "drizzle-orm";
10159
11573
  var log2 = createLogger("GscSync");
10160
11574
  function formatDate2(d) {
@@ -10194,7 +11608,7 @@ async function executeGscSync(db, runId, projectId, opts) {
10194
11608
  tokenExpiresAt: new Date(Date.now() + tokens.expires_in * 1e3).toISOString(),
10195
11609
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
10196
11610
  });
10197
- saveConfig(opts.config);
11611
+ saveConfigPatch(opts.config);
10198
11612
  }
10199
11613
  const lagOffset = GSC_DATA_LAG_DAYS;
10200
11614
  const endDate = formatDate2(daysAgo(lagOffset));
@@ -10220,7 +11634,7 @@ async function executeGscSync(db, runId, projectId, opts) {
10220
11634
  for (const row of batch) {
10221
11635
  const [query, page, country, device, date] = row.keys;
10222
11636
  db.insert(gscSearchData).values({
10223
- id: crypto18.randomUUID(),
11637
+ id: crypto19.randomUUID(),
10224
11638
  projectId,
10225
11639
  syncRunId: runId,
10226
11640
  date: date ?? "",
@@ -10254,7 +11668,7 @@ async function executeGscSync(db, runId, projectId, opts) {
10254
11668
  const rich = ir.richResultsResult;
10255
11669
  const inspectedAt = (/* @__PURE__ */ new Date()).toISOString();
10256
11670
  db.insert(gscUrlInspections).values({
10257
- id: crypto18.randomUUID(),
11671
+ id: crypto19.randomUUID(),
10258
11672
  projectId,
10259
11673
  syncRunId: runId,
10260
11674
  url: pageUrl,
@@ -10298,7 +11712,7 @@ async function executeGscSync(db, runId, projectId, opts) {
10298
11712
  const snapshotDate = formatDate2(/* @__PURE__ */ new Date());
10299
11713
  db.delete(gscCoverageSnapshots).where(and8(eq18(gscCoverageSnapshots.projectId, projectId), eq18(gscCoverageSnapshots.date, snapshotDate))).run();
10300
11714
  db.insert(gscCoverageSnapshots).values({
10301
- id: crypto18.randomUUID(),
11715
+ id: crypto19.randomUUID(),
10302
11716
  projectId,
10303
11717
  syncRunId: runId,
10304
11718
  date: snapshotDate,
@@ -10318,7 +11732,7 @@ async function executeGscSync(db, runId, projectId, opts) {
10318
11732
  }
10319
11733
 
10320
11734
  // src/gsc-inspect-sitemap.ts
10321
- import crypto19 from "crypto";
11735
+ import crypto20 from "crypto";
10322
11736
  import { eq as eq19, and as and9 } from "drizzle-orm";
10323
11737
 
10324
11738
  // src/sitemap-parser.ts
@@ -10415,7 +11829,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
10415
11829
  tokenExpiresAt: new Date(Date.now() + tokens.expires_in * 1e3).toISOString(),
10416
11830
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
10417
11831
  });
10418
- saveConfig(opts.config);
11832
+ saveConfigPatch(opts.config);
10419
11833
  }
10420
11834
  const sitemapUrl = opts.sitemapUrl || conn.sitemapUrl || `https://${project.canonicalDomain}/sitemap.xml`;
10421
11835
  log3.info("sitemap.fetch", { runId, projectId, sitemapUrl });
@@ -10435,7 +11849,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
10435
11849
  const rich = ir.richResultsResult;
10436
11850
  const inspectedAt = (/* @__PURE__ */ new Date()).toISOString();
10437
11851
  db.insert(gscUrlInspections).values({
10438
- id: crypto19.randomUUID(),
11852
+ id: crypto20.randomUUID(),
10439
11853
  projectId,
10440
11854
  syncRunId: runId,
10441
11855
  url: pageUrl,
@@ -10485,7 +11899,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
10485
11899
  const snapshotDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
10486
11900
  db.delete(gscCoverageSnapshots).where(and9(eq19(gscCoverageSnapshots.projectId, projectId), eq19(gscCoverageSnapshots.date, snapshotDate))).run();
10487
11901
  db.insert(gscCoverageSnapshots).values({
10488
- id: crypto19.randomUUID(),
11902
+ id: crypto20.randomUUID(),
10489
11903
  projectId,
10490
11904
  syncRunId: runId,
10491
11905
  date: snapshotDate,
@@ -10677,7 +12091,7 @@ var Scheduler = class {
10677
12091
 
10678
12092
  // src/notifier.ts
10679
12093
  import { eq as eq21, desc as desc7, and as and10, or as or2 } from "drizzle-orm";
10680
- import crypto20 from "crypto";
12094
+ import crypto21 from "crypto";
10681
12095
  var log5 = createLogger("Notifier");
10682
12096
  var Notifier = class {
10683
12097
  db;
@@ -10817,7 +12231,7 @@ var Notifier = class {
10817
12231
  }
10818
12232
  logDelivery(projectId, notificationId, event, status, error) {
10819
12233
  this.db.insert(auditLog).values({
10820
- id: crypto20.randomUUID(),
12234
+ id: crypto21.randomUUID(),
10821
12235
  projectId,
10822
12236
  actor: "scheduler",
10823
12237
  action: `notification.${status}`,
@@ -10915,14 +12329,14 @@ async function fetchSiteText(domain) {
10915
12329
  if (!redirectCheck.ok) return "";
10916
12330
  const redirectResult = await fetchWithPinnedAddress(redirectCheck.target);
10917
12331
  if (redirectResult.startsWith("REDIRECT:")) return "";
10918
- return stripHtml(redirectResult);
12332
+ return stripHtml2(redirectResult);
10919
12333
  }
10920
- return stripHtml(result);
12334
+ return stripHtml2(result);
10921
12335
  } catch {
10922
12336
  return "";
10923
12337
  }
10924
12338
  }
10925
- function stripHtml(html) {
12339
+ function stripHtml2(html) {
10926
12340
  if (!html) return "";
10927
12341
  let text2 = html.replace(/<script[\s\S]*?<\/script>/gi, " ");
10928
12342
  text2 = text2.replace(/<style[\s\S]*?<\/style>/gi, " ");
@@ -10974,7 +12388,7 @@ function summarizeProviderConfig(provider, config) {
10974
12388
  };
10975
12389
  }
10976
12390
  function hashApiKey(key) {
10977
- return crypto21.createHash("sha256").update(key).digest("hex");
12391
+ return crypto22.createHash("sha256").update(key).digest("hex");
10978
12392
  }
10979
12393
  function parseCookies2(header) {
10980
12394
  if (!header) return {};
@@ -11111,14 +12525,14 @@ async function createServer(opts) {
11111
12525
  } else {
11112
12526
  opts.config.bing.connections.push(connection);
11113
12527
  }
11114
- saveConfig(opts.config);
12528
+ saveConfigPatch(opts.config);
11115
12529
  return connection;
11116
12530
  },
11117
12531
  updateConnection: (domain, patch) => {
11118
12532
  const conn = opts.config.bing?.connections?.find((c) => c.domain === domain);
11119
12533
  if (!conn) return void 0;
11120
12534
  Object.assign(conn, patch);
11121
- saveConfig(opts.config);
12535
+ saveConfigPatch(opts.config);
11122
12536
  return conn;
11123
12537
  },
11124
12538
  deleteConnection: (domain) => {
@@ -11126,7 +12540,7 @@ async function createServer(opts) {
11126
12540
  const idx = opts.config.bing.connections.findIndex((c) => c.domain === domain);
11127
12541
  if (idx < 0) return false;
11128
12542
  opts.config.bing.connections.splice(idx, 1);
11129
- saveConfig(opts.config);
12543
+ saveConfigPatch(opts.config);
11130
12544
  return true;
11131
12545
  }
11132
12546
  };
@@ -11136,32 +12550,52 @@ async function createServer(opts) {
11136
12550
  },
11137
12551
  upsertConnection: (connection) => {
11138
12552
  const updated = upsertGa4Connection(opts.config, connection);
11139
- saveConfig(opts.config);
12553
+ saveConfigPatch(opts.config);
11140
12554
  return updated;
11141
12555
  },
11142
12556
  deleteConnection: (projectName) => {
11143
12557
  const removed = removeGa4Connection(opts.config, projectName);
11144
- if (removed) saveConfig(opts.config);
12558
+ if (removed) saveConfigPatch(opts.config);
11145
12559
  return removed;
11146
12560
  }
11147
12561
  };
11148
- const googleStateSecret = process.env.GOOGLE_STATE_SECRET ?? crypto21.randomBytes(32).toString("hex");
12562
+ const googleStateSecret = process.env.GOOGLE_STATE_SECRET ?? crypto22.randomBytes(32).toString("hex");
11149
12563
  const googleConnectionStore = {
11150
12564
  listConnections: (domain) => listGoogleConnections(opts.config, domain),
11151
12565
  getConnection: (domain, connectionType) => getGoogleConnection(opts.config, domain, connectionType),
11152
12566
  upsertConnection: (connection) => {
11153
12567
  const updated = upsertGoogleConnection(opts.config, connection);
11154
- saveConfig(opts.config);
12568
+ saveConfigPatch(opts.config);
11155
12569
  return updated;
11156
12570
  },
11157
12571
  updateConnection: (domain, connectionType, patch) => {
11158
12572
  const updated = patchGoogleConnection(opts.config, domain, connectionType, patch);
11159
- if (updated) saveConfig(opts.config);
12573
+ if (updated) saveConfigPatch(opts.config);
11160
12574
  return updated;
11161
12575
  },
11162
12576
  deleteConnection: (domain, connectionType) => {
11163
12577
  const removed = removeGoogleConnection(opts.config, domain, connectionType);
11164
- if (removed) saveConfig(opts.config);
12578
+ if (removed) saveConfigPatch(opts.config);
12579
+ return removed;
12580
+ }
12581
+ };
12582
+ const wordpressConnectionStore = {
12583
+ getConnection: (projectName) => {
12584
+ return getWordpressConnection(opts.config, projectName);
12585
+ },
12586
+ upsertConnection: (connection) => {
12587
+ const updated = upsertWordpressConnection(opts.config, connection);
12588
+ saveConfigPatch(opts.config);
12589
+ return updated;
12590
+ },
12591
+ updateConnection: (projectName, patch) => {
12592
+ const updated = patchWordpressConnection(opts.config, projectName, patch);
12593
+ if (updated) saveConfigPatch(opts.config);
12594
+ return updated;
12595
+ },
12596
+ deleteConnection: (projectName) => {
12597
+ const removed = removeWordpressConnection(opts.config, projectName);
12598
+ if (removed) saveConfigPatch(opts.config);
11165
12599
  return removed;
11166
12600
  }
11167
12601
  };
@@ -11175,7 +12609,7 @@ async function createServer(opts) {
11175
12609
  if (!existing) {
11176
12610
  const prefix = opts.config.apiKey.slice(0, 12);
11177
12611
  opts.db.insert(apiKeys).values({
11178
- id: `key_${crypto21.randomBytes(8).toString("hex")}`,
12612
+ id: `key_${crypto22.randomBytes(8).toString("hex")}`,
11179
12613
  name: "default",
11180
12614
  keyHash,
11181
12615
  keyPrefix: prefix,
@@ -11199,7 +12633,7 @@ async function createServer(opts) {
11199
12633
  };
11200
12634
  const createSession = (apiKeyId) => {
11201
12635
  pruneExpiredSessions();
11202
- const sessionId = crypto21.randomBytes(32).toString("hex");
12636
+ const sessionId = crypto22.randomBytes(32).toString("hex");
11203
12637
  sessions.set(sessionId, {
11204
12638
  apiKeyId,
11205
12639
  expiresAt: Date.now() + SESSION_TTL_MS
@@ -11256,7 +12690,7 @@ async function createServer(opts) {
11256
12690
  return reply.status(err.statusCode).send(err.toJSON());
11257
12691
  }
11258
12692
  opts.config.dashboardPasswordHash = hashApiKey(password);
11259
- saveConfig(opts.config);
12693
+ saveConfigPatch(opts.config);
11260
12694
  if (!createPasswordSession(reply)) {
11261
12695
  const err = authInvalid();
11262
12696
  return reply.status(err.statusCode).send(err.toJSON());
@@ -11362,6 +12796,7 @@ async function createServer(opts) {
11362
12796
  googleSettingsSummary,
11363
12797
  bingSettingsSummary,
11364
12798
  bingConnectionStore,
12799
+ wordpressConnectionStore,
11365
12800
  ga4CredentialStore,
11366
12801
  onRunCreated: (runId, projectId, providers2, location) => {
11367
12802
  jobRunner.executeRun(runId, projectId, providers2, location).catch((err) => {
@@ -11387,7 +12822,7 @@ async function createServer(opts) {
11387
12822
  vertexCredentials: existing?.vertexCredentials
11388
12823
  };
11389
12824
  try {
11390
- saveConfig(opts.config);
12825
+ saveConfigPatch(opts.config);
11391
12826
  } catch (err) {
11392
12827
  app.log.error({ err }, "Failed to save config");
11393
12828
  return null;
@@ -11429,7 +12864,7 @@ async function createServer(opts) {
11429
12864
  const targetProjectIds = affectedProjectIds.length > 0 ? affectedProjectIds : [null];
11430
12865
  const createdAt = (/* @__PURE__ */ new Date()).toISOString();
11431
12866
  opts.db.insert(auditLog).values(targetProjectIds.map((projectId) => ({
11432
- id: crypto21.randomUUID(),
12867
+ id: crypto22.randomUUID(),
11433
12868
  projectId,
11434
12869
  actor: "api",
11435
12870
  action: existing ? "provider.updated" : "provider.created",
@@ -11449,7 +12884,7 @@ async function createServer(opts) {
11449
12884
  onGoogleSettingsUpdate: (clientId, clientSecret) => {
11450
12885
  try {
11451
12886
  setGoogleAuthConfig(opts.config, { clientId, clientSecret });
11452
- saveConfig(opts.config);
12887
+ saveConfigPatch(opts.config);
11453
12888
  googleSettingsSummary.configured = true;
11454
12889
  return { ...googleSettingsSummary };
11455
12890
  } catch (err) {
@@ -11461,7 +12896,7 @@ async function createServer(opts) {
11461
12896
  try {
11462
12897
  if (!opts.config.bing) opts.config.bing = {};
11463
12898
  opts.config.bing.apiKey = apiKey;
11464
- saveConfig(opts.config);
12899
+ saveConfigPatch(opts.config);
11465
12900
  bingSettingsSummary.configured = true;
11466
12901
  return { ...bingSettingsSummary };
11467
12902
  } catch (err) {
@@ -11488,7 +12923,7 @@ async function createServer(opts) {
11488
12923
  setTelemetryEnabled: (enabled) => {
11489
12924
  const config = loadConfig();
11490
12925
  config.telemetry = enabled;
11491
- saveConfig(config);
12926
+ saveConfigPatch(config);
11492
12927
  opts.config.telemetry = enabled;
11493
12928
  },
11494
12929
  onCdpConfigure: async (host, port2) => {
@@ -11496,7 +12931,7 @@ async function createServer(opts) {
11496
12931
  opts.config.cdp.host = host;
11497
12932
  opts.config.cdp.port = port2;
11498
12933
  try {
11499
- saveConfig(opts.config);
12934
+ saveConfigPatch(opts.config);
11500
12935
  } catch (err) {
11501
12936
  app.log.error({ err }, "Failed to save CDP config");
11502
12937
  throw err;
@@ -11673,6 +13108,7 @@ export {
11673
13108
  getConfigPath,
11674
13109
  loadConfig,
11675
13110
  saveConfig,
13111
+ saveConfigPatch,
11676
13112
  configExists,
11677
13113
  isTelemetryEnabled,
11678
13114
  getOrCreateAnonymousId,