nuxt-ai-ready 3.3.1 → 3.5.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.
Files changed (28) hide show
  1. package/dist/chunks/agent-skills.mjs +2 -1
  2. package/dist/chunks/prerender.mjs +14 -2
  3. package/dist/cli.mjs +28 -10
  4. package/dist/module.d.mts +1 -0
  5. package/dist/module.json +1 -1
  6. package/dist/module.mjs +3 -2
  7. package/dist/runtime/server/middleware/markdown.prerender.js +9 -1
  8. package/dist/runtime/server/plugins/content-signal-capture-nitro3.prerender.d.ts +2 -0
  9. package/dist/runtime/server/plugins/content-signal-capture-nitro3.prerender.js +13 -0
  10. package/dist/runtime/server/plugins/content-signal-capture.prerender.d.ts +2 -0
  11. package/dist/runtime/server/plugins/content-signal-capture.prerender.js +13 -0
  12. package/dist/runtime/server/routes/__ai-ready/cron.get.js +1 -1
  13. package/dist/runtime/server/routes/__ai-ready/poll.post.js +1 -1
  14. package/dist/runtime/server/routes/__ai-ready/prune.post.js +1 -1
  15. package/dist/runtime/server/routes/__ai-ready/reindex.post.js +1 -1
  16. package/dist/runtime/server/routes/__ai-ready/restore.post.js +1 -1
  17. package/dist/runtime/server/routes/__ai-ready/status.get.js +1 -1
  18. package/dist/runtime/server/utils/auth.d.ts +2 -4
  19. package/dist/runtime/server/utils/auth.js +26 -5
  20. package/dist/runtime/server/utils/cloudflare.d.ts +3 -0
  21. package/dist/runtime/server/utils/prerender-html.d.ts +3 -0
  22. package/dist/runtime/server/utils/prerender-html.js +12 -0
  23. package/dist/runtime/sync-secret.d.ts +4 -0
  24. package/dist/runtime/sync-secret.js +15 -0
  25. package/dist/runtime/types.d.ts +6 -5
  26. package/dist/shared/{nuxt-ai-ready.CKXM1jnE.mjs → nuxt-ai-ready.BaNK8yfw.mjs} +393 -266
  27. package/package.json +1 -1
  28. package/skills/nuxt-ai-ready/references/runtime-indexing.md +8 -2
@@ -1,5 +1,4 @@
1
- import { createHash, randomBytes } from 'node:crypto';
2
- import { readFile, access, mkdir, writeFile } from 'node:fs/promises';
1
+ import { readFile, access, mkdir, writeFile, rm } from 'node:fs/promises';
3
2
  import { createRequire } from 'node:module';
4
3
  import { dirname, basename, join, resolve, relative } from 'node:path';
5
4
  import { fileURLToPath, pathToFileURL } from 'node:url';
@@ -11,7 +10,9 @@ import { r as readPackageMetadata, a as resolvePackageMetadataPath } from './nux
11
10
  import { parseLlmsTxtConfig } from '../../dist/runtime/llms-txt-files.js';
12
11
  import { toDeployedRoute } from '../../dist/runtime/route-path.js';
13
12
  import { SITEMAP_MD_ROUTE } from '../../dist/runtime/server/utils/sitemap-md.js';
13
+ import { LEGACY_RUNTIME_SYNC_SECRET_WARNING, deriveRuntimeSyncSecret } from '../../dist/runtime/sync-secret.js';
14
14
  import { publicCacheControl, mergeVaryHeader } from '../../dist/runtime/cache-control.js';
15
+ import { createHash } from 'node:crypto';
15
16
  import { MCP_SERVER_CARD_MEDIA_TYPE, AI_CATALOG_MEDIA_TYPE } from '../../dist/runtime/server/utils/discovery-response.js';
16
17
  import { parseTOML, parseJSONC } from 'confbox';
17
18
  import { toMarkdownPath, hasMarkdownTwin, normalizePagePath } from '../../dist/runtime/markdown-path.js';
@@ -563,6 +564,334 @@ async function readCloudflareAssetRouting(rootDir) {
563
564
  }
564
565
  }
565
566
 
567
+ function escapeRegExp(value) {
568
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
569
+ }
570
+ function ensureStaticHeader(contents, route, name, value, mode = "default") {
571
+ const eol = contents.includes("\r\n") ? "\r\n" : "\n";
572
+ const routePattern = new RegExp(`^${escapeRegExp(route)}[\\t ]*\\r?$`, "gm");
573
+ const routeMatches = [...contents.matchAll(routePattern)];
574
+ const routeMatch = routeMatches.at(-1);
575
+ if (!routeMatch || routeMatch.index === void 0) {
576
+ const separator = contents.length === 0 ? "" : contents.endsWith(`${eol}${eol}`) ? "" : contents.endsWith(eol) ? eol : `${eol}${eol}`;
577
+ return `${contents}${separator}${route}${eol} ${name}: ${value}${eol}`;
578
+ }
579
+ const routeLineEnd = routeMatch.index + routeMatch[0].length;
580
+ const blockStart = contents[routeLineEnd] === "\n" ? routeLineEnd + 1 : routeLineEnd;
581
+ const nextBlockPattern = /^(?![\t ]|\r?$).+/gm;
582
+ nextBlockPattern.lastIndex = blockStart;
583
+ const nextBlock = nextBlockPattern.exec(contents);
584
+ const blockEnd = nextBlock?.index ?? contents.length;
585
+ const block = contents.slice(blockStart, blockEnd);
586
+ const escapedName = escapeRegExp(name);
587
+ const existingHeaderPattern = new RegExp(
588
+ `^[\\t ]+(?:${escapedName}[\\t ]*:|![\\t ]*${escapedName}[\\t ]*\\r?$)`,
589
+ "im"
590
+ );
591
+ if (existingHeaderPattern.test(block)) {
592
+ if (mode === "append") {
593
+ const removalPattern = new RegExp(`^[\\t ]+![\\t ]*${escapedName}[\\t ]*\\r?$`, "im");
594
+ if (removalPattern.test(block))
595
+ return contents;
596
+ const valuePattern = new RegExp(`^([\\t ]+${escapedName}[\\t ]*:[\\t ]*)([^\\r\\n]*)(\\r?$)`, "im");
597
+ const match = block.match(valuePattern);
598
+ if (match) {
599
+ const merged = block.replace(valuePattern, (_line, prefix2, current, suffix) => {
600
+ const appended = name.toLowerCase() === "link" ? current.includes(value) ? current : [current, value].filter(Boolean).join(", ") : mergeVaryHeader(current, value);
601
+ return `${prefix2}${appended}${suffix}`;
602
+ });
603
+ return `${contents.slice(0, blockStart)}${merged}${contents.slice(blockEnd)}`;
604
+ }
605
+ }
606
+ return contents;
607
+ }
608
+ const prefix = blockStart === routeLineEnd ? eol : "";
609
+ return `${contents.slice(0, blockStart)}${prefix} ${name}: ${value}${eol}${contents.slice(blockStart)}`;
610
+ }
611
+ function mergeStaticDiscoveryHeaders(contents, baseURL, entries) {
612
+ if (entries.length === 0)
613
+ return contents;
614
+ const route = toDeployedRoute("/*", baseURL);
615
+ const blocks = splitHeaderBlocks(contents);
616
+ const matching = blocks.filter((block) => block.route === route);
617
+ let merged = matching[0]?.text || "";
618
+ for (const block of matching.slice(1)) {
619
+ for (const line of block.text.split(/(?<=\n)/).slice(1)) {
620
+ const header = line.match(/^[\t ]+([^:\s]+):[\t ]*([^\r\n]*)/);
621
+ if (header?.[1]?.toLowerCase() === "link") {
622
+ merged = ensureStaticHeader(merged, route, header[1], header[2], "append");
623
+ } else if (header) {
624
+ const existing = new RegExp(`^[\\t ]+${escapeRegExp(header[1])}[\\t ]*:[^\\r\\n]*(?:\\r?\\n|$)`, "gim");
625
+ merged = ensureStaticHeader(merged.replace(existing, ""), route, header[1], header[2]);
626
+ } else {
627
+ merged += line;
628
+ }
629
+ }
630
+ }
631
+ if (matching.length > 1) {
632
+ let inserted = false;
633
+ contents = blocks.map((block) => {
634
+ if (block.route !== route)
635
+ return block.text;
636
+ if (inserted)
637
+ return "";
638
+ inserted = true;
639
+ return merged;
640
+ }).join("");
641
+ }
642
+ return entries.reduce((headers, entry) => ensureStaticHeader(headers, route, "Link", entry, "append"), contents);
643
+ }
644
+ function parseHeaderLine(line) {
645
+ const colon = line.indexOf(":");
646
+ if (!/^[\t ]/.test(line) || colon < 0)
647
+ return void 0;
648
+ const name = line.slice(0, colon).trim();
649
+ if (!name || name.startsWith("!") || /\s/.test(name))
650
+ return void 0;
651
+ const eol = line.endsWith("\r\n") ? "\r\n" : line.endsWith("\n") ? "\n" : "";
652
+ return {
653
+ name,
654
+ value: line.slice(colon + 1, line.length - eol.length).trim(),
655
+ withValue: (value) => `${line.slice(0, colon + 1)} ${value}${eol}`
656
+ };
657
+ }
658
+ const RE_DETACH_LINE = /^[\t ]+![\t ]*([^\s:]+)/;
659
+ function patternToRegExp(pattern) {
660
+ const source = pattern.split(/(\*|:\w+)/).map((part) => part === "*" ? ".*" : part.startsWith(":") ? "[^/]+" : escapeRegExp(part)).join("");
661
+ return new RegExp(`^${source}$`);
662
+ }
663
+ function matchingStaticContentSignals(routeRules, route) {
664
+ return Object.entries(routeRules).flatMap(([pattern, rule]) => {
665
+ if (!patternToRegExp(pattern).test(route))
666
+ return [];
667
+ const headers = rule?.headers;
668
+ return Object.entries(headers || {}).filter(([name]) => name.toLowerCase() === "content-signal").map(([, value]) => value);
669
+ });
670
+ }
671
+ function normalizeStaticContentSignalHeaders(contents) {
672
+ const parsed = splitHeaderBlocks(contents);
673
+ if (!parsed.some((block) => /^\s+content-signal\s*:/im.test(block.text)))
674
+ return contents;
675
+ const policyLine = /^[\t ]+(?:content-signal\s*:.*|!\s*content-signal\s*)$/i;
676
+ const hasPolicy = (block) => block.text.split(/\r?\n/).slice(1).some((line) => policyLine.test(line));
677
+ const repeated = /* @__PURE__ */ new Map();
678
+ for (const block of parsed) {
679
+ if (block.route !== null)
680
+ repeated.set(block.route, [...repeated.get(block.route) || [], block]);
681
+ }
682
+ const combined = /* @__PURE__ */ new Set();
683
+ const blocks = parsed.flatMap((block) => {
684
+ const group = block.route === null ? [] : repeated.get(block.route);
685
+ if (group.length < 2 || !group.some(hasPolicy))
686
+ return [block];
687
+ if (combined.has(block.route))
688
+ return [];
689
+ combined.add(block.route);
690
+ const lines = group.at(-1).text.split(/\r?\n/).slice(1).filter((line) => !policyLine.test(line));
691
+ const policy = group.filter(hasPolicy).at(-1).text.split(/\r?\n/).slice(1).filter((line) => policyLine.test(line));
692
+ return [{ route: block.route, text: `${block.route}
693
+ ${[...lines, ...policy].join("\n")}
694
+ ` }];
695
+ });
696
+ const policyOnly = (block) => hasPolicy(block) && block.text.split(/\r?\n/).slice(1).every((line) => !line.trim() || line.trimStart().startsWith("#") || policyLine.test(line));
697
+ const policyValue = (block) => block.text.match(/^[\t ]+content-signal[\t ]*:(.*)$/im)?.[1]?.trim() ?? null;
698
+ const edges = blocks.map(() => /* @__PURE__ */ new Set());
699
+ const incoming = blocks.map(() => 0);
700
+ const connect = (before, after) => {
701
+ if (!edges[before].has(after)) {
702
+ edges[before].add(after);
703
+ incoming[after]++;
704
+ }
705
+ };
706
+ let previousUnrelated;
707
+ for (let index = 0; index < blocks.length; index++) {
708
+ if (!policyOnly(blocks[index])) {
709
+ if (previousUnrelated !== void 0)
710
+ connect(previousUnrelated, index);
711
+ previousUnrelated = index;
712
+ }
713
+ }
714
+ for (let broad = 0; broad < blocks.length; broad++) {
715
+ const source = blocks[broad];
716
+ if (!hasPolicy(source) || source.route === null)
717
+ continue;
718
+ for (let narrow = 0; narrow < blocks.length; narrow++) {
719
+ const target = blocks[narrow];
720
+ if (broad !== narrow && hasPolicy(target) && target.route !== null && policyValue(source) !== policyValue(target) && coversContentSignalPattern(source.route, target.route)) {
721
+ connect(broad, narrow);
722
+ }
723
+ }
724
+ }
725
+ const ordered = [];
726
+ const emitted = /* @__PURE__ */ new Set();
727
+ while (ordered.length < blocks.length) {
728
+ const next = incoming.findIndex((count, index) => count === 0 && !emitted.has(index));
729
+ if (next === -1)
730
+ throw new Error("Content-Signal rules conflict with other header ordering. Place broader mixed policy rules before narrower rules.");
731
+ emitted.add(next);
732
+ ordered.push(blocks[next]);
733
+ for (const target of edges[next])
734
+ incoming[target]--;
735
+ }
736
+ return ordered.map((block) => {
737
+ const text = block.text.trimEnd();
738
+ if (!/^\s+content-signal\s*:/im.test(text) || /^\s+!\s*content-signal\s*$/im.test(text))
739
+ return `${text}
740
+ `;
741
+ return `${text.replace(/^(\s+)(content-signal\s*:)/im, "$1! Content-Signal\n$1$2")}
742
+ `;
743
+ }).join("\n");
744
+ }
745
+ function coversPattern(broad, narrow) {
746
+ if (broad === narrow || !broad.startsWith("/") || !narrow.startsWith("/"))
747
+ return false;
748
+ if (!/[*:]/.test(narrow))
749
+ return patternToRegExp(broad).test(narrow);
750
+ return broad.indexOf("*") === broad.length - 1 && !broad.includes(":") && narrow.startsWith(broad.slice(0, -1));
751
+ }
752
+ function coversContentSignalPattern(broad, narrow) {
753
+ if (coversPattern(broad, narrow))
754
+ return true;
755
+ if (broad === narrow || !broad.startsWith("/") || !narrow.startsWith("/") || broad.indexOf("*") !== broad.length - 1 || !broad.includes(":")) {
756
+ return false;
757
+ }
758
+ const prefix = narrow.split("*")[0];
759
+ return !prefix.includes(":") && patternToRegExp(broad).test(prefix);
760
+ }
761
+ function splitLinkEntries(value) {
762
+ const entries = [];
763
+ let current = "";
764
+ let inUrl = false;
765
+ let inQuote = false;
766
+ for (const char of value) {
767
+ if (char === "<" && !inQuote)
768
+ inUrl = true;
769
+ else if (char === ">" && !inQuote)
770
+ inUrl = false;
771
+ else if (char === '"' && !inUrl)
772
+ inQuote = !inQuote;
773
+ if (char === "," && !inUrl && !inQuote) {
774
+ entries.push(current.trim());
775
+ current = "";
776
+ continue;
777
+ }
778
+ current += char;
779
+ }
780
+ entries.push(current.trim());
781
+ return entries.filter(Boolean);
782
+ }
783
+ function headerTokens(name, value) {
784
+ if (name === "link")
785
+ return splitLinkEntries(value);
786
+ if (name === "vary")
787
+ return value.split(",").map((token) => token.trim()).filter(Boolean);
788
+ return [value.trim()];
789
+ }
790
+ function tokenKey(name, token) {
791
+ return name === "vary" ? token.toLowerCase() : token;
792
+ }
793
+ function dedupeStaticHeaders(contents, names) {
794
+ const owned = new Set(names.map((name) => name.toLowerCase()));
795
+ const blocks = splitHeaderBlocks(contents);
796
+ const detached = /* @__PURE__ */ new Set();
797
+ const parsed = blocks.map((block) => {
798
+ const headers = [];
799
+ for (const line of block.text.split(/(?<=\n)/).slice(block.route === null ? 0 : 1)) {
800
+ const detach = line.match(RE_DETACH_LINE);
801
+ if (detach) {
802
+ detached.add(detach[1].toLowerCase());
803
+ continue;
804
+ }
805
+ const header = parseHeaderLine(line);
806
+ if (!header)
807
+ continue;
808
+ const name = header.name.toLowerCase();
809
+ if (owned.has(name))
810
+ headers.push({ name, tokens: headerTokens(name, header.value) });
811
+ }
812
+ return headers;
813
+ });
814
+ const lastRuleIndex = /* @__PURE__ */ new Map();
815
+ blocks.forEach((block, index) => {
816
+ if (block.route !== null)
817
+ lastRuleIndex.set(block.route, index);
818
+ });
819
+ let changed = false;
820
+ const output = blocks.map((block) => {
821
+ const route = block.route;
822
+ if (route === null)
823
+ return block.text;
824
+ const provided = /* @__PURE__ */ new Map();
825
+ lastRuleIndex.forEach((index, pattern) => {
826
+ if (!coversPattern(pattern, route))
827
+ return;
828
+ for (const { name, tokens } of parsed[index]) {
829
+ if (detached.has(name))
830
+ continue;
831
+ const values = provided.get(name) ?? /* @__PURE__ */ new Set();
832
+ tokens.forEach((token) => values.add(tokenKey(name, token)));
833
+ provided.set(name, values);
834
+ }
835
+ });
836
+ if (provided.size === 0)
837
+ return block.text;
838
+ const [routeLine, ...lines] = block.text.split(/(?<=\n)/);
839
+ let headerLines = 0;
840
+ const kept = lines.flatMap((line) => {
841
+ const header = parseHeaderLine(line);
842
+ if (!header) {
843
+ if (RE_DETACH_LINE.test(line))
844
+ headerLines++;
845
+ return [line];
846
+ }
847
+ headerLines++;
848
+ const name = header.name.toLowerCase();
849
+ const covered = provided.get(name);
850
+ if (!covered)
851
+ return [line];
852
+ const tokens = headerTokens(name, header.value);
853
+ const remaining = tokens.filter((token) => !covered.has(tokenKey(name, token)));
854
+ if (remaining.length === tokens.length)
855
+ return [line];
856
+ changed = true;
857
+ if (remaining.length > 0)
858
+ return [header.withValue(remaining.join(", "))];
859
+ headerLines--;
860
+ return [];
861
+ });
862
+ return headerLines === 0 ? kept.join("") : `${routeLine}${kept.join("")}`;
863
+ });
864
+ return changed ? output.join("") : contents;
865
+ }
866
+ const CLOUDFLARE_STATIC_HEADER_RULE_LIMIT = 100;
867
+ const RE_EXACT_MARKDOWN_ROUTE$1 = /^\/[^*\s]*\.md[\t ]*\r?$/;
868
+ function splitHeaderBlocks(contents) {
869
+ const blocks = [];
870
+ let current = { route: null, text: "" };
871
+ for (const line of contents.split(/(?<=\n)/)) {
872
+ const isRoute = /^[^\s#]/.test(line);
873
+ if (isRoute) {
874
+ blocks.push(current);
875
+ current = { route: line.trimEnd(), text: "" };
876
+ }
877
+ current.text += line;
878
+ }
879
+ blocks.push(current);
880
+ return blocks;
881
+ }
882
+ function enforceStaticHeaderBudget(contents, limit) {
883
+ const blocks = splitHeaderBlocks(contents);
884
+ const ruleCount = blocks.filter((block) => block.route !== null).length;
885
+ if (ruleCount <= limit)
886
+ return { contents, total: ruleCount, dropped: 0 };
887
+ const kept = blocks.filter((block) => block.route === null || !RE_EXACT_MARKDOWN_ROUTE$1.test(block.route) || /^\s+(?:content-signal\s*:|!\s*content-signal\s*$)/im.test(block.text));
888
+ return {
889
+ contents: kept.map((block) => block.text).join(""),
890
+ total: kept.filter((block) => block.route !== null).length,
891
+ dropped: ruleCount - kept.filter((block) => block.route !== null).length
892
+ };
893
+ }
894
+
566
895
  const RE_MD_EXT = /\.md$/;
567
896
  function isStaticMarkdownSourceRoute(route) {
568
897
  if (route.includes("*") || route.includes(":"))
@@ -607,41 +936,68 @@ function prerenderedMarkdownHeaderRules(prerenderedRoutes, baseURL, describedby,
607
936
  headers: {
608
937
  "Content-Type": "text/markdown; charset=utf-8",
609
938
  "Vary": "Accept",
610
- "Link": buildStaticMarkdownLinkHeader(pageRoute, baseURL, describedby)
939
+ "Link": buildStaticMarkdownLinkHeader(pageRoute, baseURL, describedby),
940
+ ...entry.contentSignal !== void 0 && { "Content-Signal": entry.contentSignal }
611
941
  }
612
942
  });
613
943
  }
614
944
  }
615
945
  return [...rules.values()];
616
946
  }
617
- const RE_EXACT_MARKDOWN_ROUTE$1 = /^\/[^*:\s]*\.md$/;
947
+ const RE_EXACT_MARKDOWN_ROUTE = /^\/[^*:\s]*\.md$/;
618
948
  function isStaticHeaderRouteRule([, rule]) {
619
949
  return Boolean(rule && "headers" in rule && rule.headers);
620
950
  }
951
+ function hasContentSignal$1(headers) {
952
+ return Object.keys(headers || {}).some((name) => name.toLowerCase() === "content-signal");
953
+ }
621
954
  function planStaticMarkdownHeaderRules(routeRules, rules, limit) {
955
+ rules = rules.map((rule) => {
956
+ if (!hasContentSignal$1(rule.headers) || matchingStaticContentSignals(routeRules, rule.route).length === 0)
957
+ return rule;
958
+ return { ...rule, headers: Object.fromEntries(Object.entries(rule.headers).filter(([name]) => name.toLowerCase() !== "content-signal")) };
959
+ });
622
960
  if (limit === null)
623
961
  return { _tag: "apply", rules };
624
962
  const entries = Object.entries(routeRules).filter(isStaticHeaderRouteRule);
625
- const registeredMarkdown = entries.map(([route]) => route).filter((route) => RE_EXACT_MARKDOWN_ROUTE$1.test(route));
963
+ const registeredMarkdown = entries.map(([route]) => route).filter((route) => RE_EXACT_MARKDOWN_ROUTE.test(route));
626
964
  const other = entries.length - registeredMarkdown.length;
627
965
  const markdown = /* @__PURE__ */ new Set([...registeredMarkdown, ...rules.map((rule) => rule.route)]);
628
966
  const total = other + markdown.size;
629
967
  if (total <= limit)
630
968
  return { _tag: "apply", rules };
631
- return { _tag: "skip", drop: registeredMarkdown, total, dropped: markdown.size };
969
+ const policyRules = rules.filter((rule) => hasContentSignal$1(rule.headers));
970
+ const policyRoutes = /* @__PURE__ */ new Set([
971
+ ...policyRules.map((rule) => rule.route),
972
+ ...registeredMarkdown.filter((route) => hasContentSignal$1(routeRules[route].headers))
973
+ ]);
974
+ if (other + policyRoutes.size > limit)
975
+ return { _tag: "blocked", total: other + policyRoutes.size, limit };
976
+ return { _tag: "skip", drop: registeredMarkdown.filter((route) => !policyRoutes.has(route)), total, dropped: markdown.size - policyRoutes.size, rules: policyRules };
632
977
  }
633
978
  function applyStaticMarkdownHeaderPlan(routeRules, plan) {
979
+ if (plan._tag === "blocked")
980
+ throw new Error(`Content-Signal policy rules exceed the static header limit (${plan.total}/${plan.limit}). Reduce per-page policies or use a shared policy.`);
634
981
  if (plan._tag === "skip") {
635
982
  for (const route of plan.drop) {
636
983
  const rule = routeRules[route];
637
984
  if (rule)
638
985
  rule.headers = void 0;
639
986
  }
640
- return;
641
987
  }
642
988
  for (const { route, headers } of plan.rules) {
643
989
  const existing = routeRules[route];
644
990
  const merged = defu({ headers: { ...headers } }, existing);
991
+ if (hasContentSignal$1(existing?.headers)) {
992
+ for (const name of Object.keys(merged.headers)) {
993
+ if (name.toLowerCase() === "content-signal")
994
+ delete merged.headers[name];
995
+ }
996
+ for (const [name, value] of Object.entries(existing.headers)) {
997
+ if (name.toLowerCase() === "content-signal")
998
+ merged.headers[name] = value;
999
+ }
1000
+ }
645
1001
  const vary = Object.entries(existing?.headers || {}).filter(([name]) => name.toLowerCase() === "vary").map(([, value]) => value).join(", ");
646
1002
  if (headers.Vary) {
647
1003
  for (const name of Object.keys(merged.headers)) {
@@ -788,7 +1144,8 @@ function resolveContentSignalPolicy(option) {
788
1144
  _tag: "Configured",
789
1145
  // One comma-separated line: validators reject one line per signal.
790
1146
  contentSignal: `ai-train=${value(option.aiTrain)}, search=${value(option.search)}, ai-input=${value(option.aiInput)}`,
791
- ...option.contentUsage !== false && { contentUsage: `train-ai=${option.aiTrain ? "y" : "n"}` }
1147
+ // Opt-in: Lighthouse's robots-txt audit fails Content-Usage as an unknown directive.
1148
+ ...option.contentUsage === true && { contentUsage: `train-ai=${option.aiTrain ? "y" : "n"}` }
792
1149
  };
793
1150
  }
794
1151
  function hasContentSignal(value) {
@@ -829,6 +1186,8 @@ function applyContentSignalPolicy(groups, policy) {
829
1186
  }
830
1187
  }
831
1188
 
1189
+ const AI_READY_CRON_SCHEDULE = "*/5 * * * *";
1190
+
832
1191
  const DATABASE_TYPE_NONE = "none";
833
1192
  const DATABASE_DEPENDENT_OPTIONS = ["runtimeSync", "cron"];
834
1193
  const DATABASE_CONSUMER_LABELS = {
@@ -1256,243 +1615,6 @@ function opaqueJsonModule(exports) {
1256
1615
  ].join("\n");
1257
1616
  }
1258
1617
 
1259
- function escapeRegExp(value) {
1260
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1261
- }
1262
- function ensureStaticHeader(contents, route, name, value, mode = "default") {
1263
- const eol = contents.includes("\r\n") ? "\r\n" : "\n";
1264
- const routePattern = new RegExp(`^${escapeRegExp(route)}[\\t ]*\\r?$`, "gm");
1265
- const routeMatches = [...contents.matchAll(routePattern)];
1266
- const routeMatch = routeMatches.at(-1);
1267
- if (!routeMatch || routeMatch.index === void 0) {
1268
- const separator = contents.length === 0 ? "" : contents.endsWith(`${eol}${eol}`) ? "" : contents.endsWith(eol) ? eol : `${eol}${eol}`;
1269
- return `${contents}${separator}${route}${eol} ${name}: ${value}${eol}`;
1270
- }
1271
- const routeLineEnd = routeMatch.index + routeMatch[0].length;
1272
- const blockStart = contents[routeLineEnd] === "\n" ? routeLineEnd + 1 : routeLineEnd;
1273
- const nextBlockPattern = /^(?![\t ]|\r?$).+/gm;
1274
- nextBlockPattern.lastIndex = blockStart;
1275
- const nextBlock = nextBlockPattern.exec(contents);
1276
- const blockEnd = nextBlock?.index ?? contents.length;
1277
- const block = contents.slice(blockStart, blockEnd);
1278
- const escapedName = escapeRegExp(name);
1279
- const existingHeaderPattern = new RegExp(
1280
- `^[\\t ]+(?:${escapedName}[\\t ]*:|![\\t ]*${escapedName}[\\t ]*\\r?$)`,
1281
- "im"
1282
- );
1283
- if (existingHeaderPattern.test(block)) {
1284
- if (mode === "append") {
1285
- const removalPattern = new RegExp(`^[\\t ]+![\\t ]*${escapedName}[\\t ]*\\r?$`, "im");
1286
- if (removalPattern.test(block))
1287
- return contents;
1288
- const valuePattern = new RegExp(`^([\\t ]+${escapedName}[\\t ]*:[\\t ]*)([^\\r\\n]*)(\\r?$)`, "im");
1289
- const match = block.match(valuePattern);
1290
- if (match) {
1291
- const merged = block.replace(valuePattern, (_line, prefix2, current, suffix) => {
1292
- const appended = name.toLowerCase() === "link" ? current.includes(value) ? current : [current, value].filter(Boolean).join(", ") : mergeVaryHeader(current, value);
1293
- return `${prefix2}${appended}${suffix}`;
1294
- });
1295
- return `${contents.slice(0, blockStart)}${merged}${contents.slice(blockEnd)}`;
1296
- }
1297
- }
1298
- return contents;
1299
- }
1300
- const prefix = blockStart === routeLineEnd ? eol : "";
1301
- return `${contents.slice(0, blockStart)}${prefix} ${name}: ${value}${eol}${contents.slice(blockStart)}`;
1302
- }
1303
- function mergeStaticDiscoveryHeaders(contents, baseURL, entries) {
1304
- if (entries.length === 0)
1305
- return contents;
1306
- const route = toDeployedRoute("/*", baseURL);
1307
- const blocks = splitHeaderBlocks(contents);
1308
- const matching = blocks.filter((block) => block.route === route);
1309
- let merged = matching[0]?.text || "";
1310
- for (const block of matching.slice(1)) {
1311
- for (const line of block.text.split(/(?<=\n)/).slice(1)) {
1312
- const header = line.match(/^[\t ]+([^:\s]+):[\t ]*([^\r\n]*)/);
1313
- if (header?.[1]?.toLowerCase() === "link") {
1314
- merged = ensureStaticHeader(merged, route, header[1], header[2], "append");
1315
- } else if (header) {
1316
- const existing = new RegExp(`^[\\t ]+${escapeRegExp(header[1])}[\\t ]*:[^\\r\\n]*(?:\\r?\\n|$)`, "gim");
1317
- merged = ensureStaticHeader(merged.replace(existing, ""), route, header[1], header[2]);
1318
- } else {
1319
- merged += line;
1320
- }
1321
- }
1322
- }
1323
- if (matching.length > 1) {
1324
- let inserted = false;
1325
- contents = blocks.map((block) => {
1326
- if (block.route !== route)
1327
- return block.text;
1328
- if (inserted)
1329
- return "";
1330
- inserted = true;
1331
- return merged;
1332
- }).join("");
1333
- }
1334
- return entries.reduce((headers, entry) => ensureStaticHeader(headers, route, "Link", entry, "append"), contents);
1335
- }
1336
- function parseHeaderLine(line) {
1337
- const colon = line.indexOf(":");
1338
- if (!/^[\t ]/.test(line) || colon < 0)
1339
- return void 0;
1340
- const name = line.slice(0, colon).trim();
1341
- if (!name || name.startsWith("!") || /\s/.test(name))
1342
- return void 0;
1343
- const eol = line.endsWith("\r\n") ? "\r\n" : line.endsWith("\n") ? "\n" : "";
1344
- return {
1345
- name,
1346
- value: line.slice(colon + 1, line.length - eol.length).trim(),
1347
- withValue: (value) => `${line.slice(0, colon + 1)} ${value}${eol}`
1348
- };
1349
- }
1350
- const RE_DETACH_LINE = /^[\t ]+![\t ]*([^\s:]+)/;
1351
- function patternToRegExp(pattern) {
1352
- const source = pattern.split(/(\*|:\w+)/).map((part) => part === "*" ? ".*" : part.startsWith(":") ? "[^/]+" : escapeRegExp(part)).join("");
1353
- return new RegExp(`^${source}$`);
1354
- }
1355
- function coversPattern(broad, narrow) {
1356
- if (broad === narrow || !broad.startsWith("/") || !narrow.startsWith("/"))
1357
- return false;
1358
- if (!/[*:]/.test(narrow))
1359
- return patternToRegExp(broad).test(narrow);
1360
- return broad.indexOf("*") === broad.length - 1 && !broad.includes(":") && narrow.startsWith(broad.slice(0, -1));
1361
- }
1362
- function splitLinkEntries(value) {
1363
- const entries = [];
1364
- let current = "";
1365
- let inUrl = false;
1366
- let inQuote = false;
1367
- for (const char of value) {
1368
- if (char === "<" && !inQuote)
1369
- inUrl = true;
1370
- else if (char === ">" && !inQuote)
1371
- inUrl = false;
1372
- else if (char === '"' && !inUrl)
1373
- inQuote = !inQuote;
1374
- if (char === "," && !inUrl && !inQuote) {
1375
- entries.push(current.trim());
1376
- current = "";
1377
- continue;
1378
- }
1379
- current += char;
1380
- }
1381
- entries.push(current.trim());
1382
- return entries.filter(Boolean);
1383
- }
1384
- function headerTokens(name, value) {
1385
- if (name === "link")
1386
- return splitLinkEntries(value);
1387
- if (name === "vary")
1388
- return value.split(",").map((token) => token.trim()).filter(Boolean);
1389
- return [value.trim()];
1390
- }
1391
- function tokenKey(name, token) {
1392
- return name === "vary" ? token.toLowerCase() : token;
1393
- }
1394
- function dedupeStaticHeaders(contents, names) {
1395
- const owned = new Set(names.map((name) => name.toLowerCase()));
1396
- const blocks = splitHeaderBlocks(contents);
1397
- const detached = /* @__PURE__ */ new Set();
1398
- const parsed = blocks.map((block) => {
1399
- const headers = [];
1400
- for (const line of block.text.split(/(?<=\n)/).slice(block.route === null ? 0 : 1)) {
1401
- const detach = line.match(RE_DETACH_LINE);
1402
- if (detach) {
1403
- detached.add(detach[1].toLowerCase());
1404
- continue;
1405
- }
1406
- const header = parseHeaderLine(line);
1407
- if (!header)
1408
- continue;
1409
- const name = header.name.toLowerCase();
1410
- if (owned.has(name))
1411
- headers.push({ name, tokens: headerTokens(name, header.value) });
1412
- }
1413
- return headers;
1414
- });
1415
- const lastRuleIndex = /* @__PURE__ */ new Map();
1416
- blocks.forEach((block, index) => {
1417
- if (block.route !== null)
1418
- lastRuleIndex.set(block.route, index);
1419
- });
1420
- let changed = false;
1421
- const output = blocks.map((block) => {
1422
- const route = block.route;
1423
- if (route === null)
1424
- return block.text;
1425
- const provided = /* @__PURE__ */ new Map();
1426
- lastRuleIndex.forEach((index, pattern) => {
1427
- if (!coversPattern(pattern, route))
1428
- return;
1429
- for (const { name, tokens } of parsed[index]) {
1430
- if (detached.has(name))
1431
- continue;
1432
- const values = provided.get(name) ?? /* @__PURE__ */ new Set();
1433
- tokens.forEach((token) => values.add(tokenKey(name, token)));
1434
- provided.set(name, values);
1435
- }
1436
- });
1437
- if (provided.size === 0)
1438
- return block.text;
1439
- const [routeLine, ...lines] = block.text.split(/(?<=\n)/);
1440
- let headerLines = 0;
1441
- const kept = lines.flatMap((line) => {
1442
- const header = parseHeaderLine(line);
1443
- if (!header) {
1444
- if (RE_DETACH_LINE.test(line))
1445
- headerLines++;
1446
- return [line];
1447
- }
1448
- headerLines++;
1449
- const name = header.name.toLowerCase();
1450
- const covered = provided.get(name);
1451
- if (!covered)
1452
- return [line];
1453
- const tokens = headerTokens(name, header.value);
1454
- const remaining = tokens.filter((token) => !covered.has(tokenKey(name, token)));
1455
- if (remaining.length === tokens.length)
1456
- return [line];
1457
- changed = true;
1458
- if (remaining.length > 0)
1459
- return [header.withValue(remaining.join(", "))];
1460
- headerLines--;
1461
- return [];
1462
- });
1463
- return headerLines === 0 ? kept.join("") : `${routeLine}${kept.join("")}`;
1464
- });
1465
- return changed ? output.join("") : contents;
1466
- }
1467
- const CLOUDFLARE_STATIC_HEADER_RULE_LIMIT = 100;
1468
- const RE_EXACT_MARKDOWN_ROUTE = /^\/[^*\s]*\.md[\t ]*\r?$/;
1469
- function splitHeaderBlocks(contents) {
1470
- const blocks = [];
1471
- let current = { route: null, text: "" };
1472
- for (const line of contents.split(/(?<=\n)/)) {
1473
- const isRoute = /^[^\s#]/.test(line);
1474
- if (isRoute) {
1475
- blocks.push(current);
1476
- current = { route: line.trimEnd(), text: "" };
1477
- }
1478
- current.text += line;
1479
- }
1480
- blocks.push(current);
1481
- return blocks;
1482
- }
1483
- function enforceStaticHeaderBudget(contents, limit) {
1484
- const blocks = splitHeaderBlocks(contents);
1485
- const ruleCount = blocks.filter((block) => block.route !== null).length;
1486
- if (ruleCount <= limit)
1487
- return { contents, total: ruleCount, dropped: 0 };
1488
- const kept = blocks.filter((block) => block.route === null || !RE_EXACT_MARKDOWN_ROUTE.test(block.route));
1489
- return {
1490
- contents: kept.map((block) => block.text).join(""),
1491
- total: kept.filter((block) => block.route !== null).length,
1492
- dropped: ruleCount - kept.filter((block) => block.route !== null).length
1493
- };
1494
- }
1495
-
1496
1618
  const DEFAULT_MAX_OUTPUT_CHARS = 1500;
1497
1619
  const DEFAULT_LIST_LIMIT = 20;
1498
1620
  const DEFAULT_SEARCH_LIMIT = 10;
@@ -2121,18 +2243,17 @@ ${details}`);
2121
2243
  const sitemapConfig = nuxt.options.sitemap;
2122
2244
  const sitemapRouteRule = nuxt.options.nitro?.routeRules?.["/sitemap.xml"];
2123
2245
  const sitemapPrerendered = !!(sitemapConfig?.zeroRuntime || sitemapRouteRule?.prerender || process.argv.includes("generate") || process.env.NUXT_GENERATE === "true");
2124
- let runtimeSyncSecret = config.runtimeSyncSecret || process.env.NUXT_AI_READY_RUNTIME_SYNC_SECRET;
2125
- if (!runtimeSyncSecret && runtimeSyncEnabled) {
2126
- runtimeSyncSecret = randomBytes(32).toString("hex");
2127
- if (!nuxt.options.dev && !nuxt.options._prepare)
2128
- logger.info("Generated a new runtimeSyncSecret for this build. If an external scheduler calls /__ai-ready/*, set NUXT_AI_READY_RUNTIME_SYNC_SECRET; the generated secret changes on every build.");
2129
- }
2130
- if (runtimeSyncSecret) {
2131
- const cacheDir = join(nuxt.options.rootDir, "node_modules/.cache/nuxt/ai-ready");
2132
- logger.debug(`Creating cache directory for secret: ${cacheDir}`);
2133
- await mkdir(cacheDir, { recursive: true });
2134
- logger.debug(`Writing runtimeSyncSecret to cache`);
2135
- await writeFile(join(cacheDir, "secret"), runtimeSyncSecret);
2246
+ const secretPath = join(nuxt.options.rootDir, "node_modules/.cache/nuxt/ai-ready/secret");
2247
+ const appSecret = process.env.NITRO_APP_SECRET ?? process.env.NUXT_APP_SECRET ?? nuxt.options.runtimeConfig.appSecret;
2248
+ const legacySecret = process.env.NUXT_NUXT_AI_READY_RUNTIME_SYNC_SECRET ?? process.env.NUXT_AI_READY_RUNTIME_SYNC_SECRET ?? config.runtimeSyncSecret;
2249
+ if (legacySecret)
2250
+ logger.warn(LEGACY_RUNTIME_SYNC_SECRET_WARNING);
2251
+ const syncSecret = typeof appSecret === "string" && appSecret.length >= 32 ? await deriveRuntimeSyncSecret(appSecret) : appSecret === void 0 || appSecret === "" ? legacySecret : void 0;
2252
+ if (runtimeSyncEnabled && syncSecret) {
2253
+ await mkdir(dirname(secretPath), { recursive: true });
2254
+ await writeFile(secretPath, syncSecret, { mode: 384 });
2255
+ } else {
2256
+ await rm(secretPath, { force: true });
2136
2257
  }
2137
2258
  nuxt.hooks.hook("nitro:config", (nitroConfig) => {
2138
2259
  if (getNitroVersion(nuxt) === 3) {
@@ -2171,9 +2292,11 @@ ${details}`);
2171
2292
  const externals = nitroConfig.externals ||= {};
2172
2293
  externals.external ||= [];
2173
2294
  externals.external.push("mdream");
2295
+ const rollupExternal = (nitroConfig.rollupConfig ||= {}).external ||= [];
2296
+ if (Array.isArray(rollupExternal))
2297
+ rollupExternal.push("node:sqlite");
2174
2298
  }
2175
2299
  if (config.cron && !nuxt.options.dev) {
2176
- const cronSchedule = "*/5 * * * *";
2177
2300
  const preset2 = String(nitroConfig.preset || "");
2178
2301
  const isVercel = preset2 === "vercel" || preset2 === "vercel-edge";
2179
2302
  const isCloudflarePages = preset2 === "cloudflare-pages" || preset2 === "cloudflare_pages";
@@ -2184,7 +2307,7 @@ ${details}`);
2184
2307
  nitroConfig.vercel.config = nitroConfig.vercel.config || {};
2185
2308
  nitroConfig.vercel.config.crons = nitroConfig.vercel.config.crons || [];
2186
2309
  nitroConfig.vercel.config.crons.push({
2187
- schedule: cronSchedule,
2310
+ schedule: AI_READY_CRON_SCHEDULE,
2188
2311
  path: "/__ai-ready/cron"
2189
2312
  });
2190
2313
  } else {
@@ -2194,8 +2317,8 @@ ${details}`);
2194
2317
  handler: resolve$1("./runtime/server/tasks/ai-ready-cron")
2195
2318
  };
2196
2319
  nitroConfig.scheduledTasks = nitroConfig.scheduledTasks || {};
2197
- nitroConfig.scheduledTasks[cronSchedule] = nitroConfig.scheduledTasks[cronSchedule] || [];
2198
- nitroConfig.scheduledTasks[cronSchedule].push("ai-ready:cron");
2320
+ nitroConfig.scheduledTasks[AI_READY_CRON_SCHEDULE] = nitroConfig.scheduledTasks[AI_READY_CRON_SCHEDULE] || [];
2321
+ nitroConfig.scheduledTasks[AI_READY_CRON_SCHEDULE].push("ai-ready:cron");
2199
2322
  const isCloudflareWorkers = preset2.startsWith("cloudflare");
2200
2323
  if (isCloudflareWorkers) {
2201
2324
  nitroConfig.cloudflare = nitroConfig.cloudflare || {};
@@ -2203,8 +2326,8 @@ ${details}`);
2203
2326
  nitroConfig.cloudflare.wrangler = nitroConfig.cloudflare.wrangler || {};
2204
2327
  nitroConfig.cloudflare.wrangler.triggers = nitroConfig.cloudflare.wrangler.triggers || {};
2205
2328
  nitroConfig.cloudflare.wrangler.triggers.crons = nitroConfig.cloudflare.wrangler.triggers.crons || [];
2206
- if (!nitroConfig.cloudflare.wrangler.triggers.crons.includes(cronSchedule)) {
2207
- nitroConfig.cloudflare.wrangler.triggers.crons.push(cronSchedule);
2329
+ if (!nitroConfig.cloudflare.wrangler.triggers.crons.includes(AI_READY_CRON_SCHEDULE)) {
2330
+ nitroConfig.cloudflare.wrangler.triggers.crons.push(AI_READY_CRON_SCHEDULE);
2208
2331
  }
2209
2332
  }
2210
2333
  }
@@ -2322,7 +2445,7 @@ export function trackDrizzleWork(event, work) { return work }
2322
2445
  batchSize: runtimeSyncConfig.batchSize ?? 50,
2323
2446
  pruneTtl: runtimeSyncConfig.pruneTtl ?? 0
2324
2447
  },
2325
- runtimeSyncSecret,
2448
+ runtimeSyncSecret: legacySecret || "",
2326
2449
  sitemapPrerendered,
2327
2450
  i18n: i18nConfig,
2328
2451
  ftsTokenizer,
@@ -2335,9 +2458,10 @@ export function trackDrizzleWork(event, work) { return work }
2335
2458
  handler: resolve$1("./runtime/server/middleware/markdown.prerender")
2336
2459
  });
2337
2460
  const htmlCapturePlugin = resolve$1("./runtime/server/plugins/html-capture.prerender");
2461
+ const policyCapturePlugin = resolve$1(getNitroVersion(nuxt) === 3 ? "./runtime/server/plugins/content-signal-capture-nitro3.prerender" : "./runtime/server/plugins/content-signal-capture.prerender");
2338
2462
  nuxt.hook("nitro:init", (nitro) => {
2339
2463
  nitro.hooks.hook("prerender:config", (prerenderConfig) => {
2340
- prerenderConfig.plugins = [...prerenderConfig.plugins || [], htmlCapturePlugin];
2464
+ prerenderConfig.plugins = [...prerenderConfig.plugins || [], htmlCapturePlugin, policyCapturePlugin];
2341
2465
  });
2342
2466
  });
2343
2467
  addPlugin({
@@ -2598,11 +2722,14 @@ export function trackDrizzleWork(event, work) { return work }
2598
2722
  discovery.push(`<${encodeURI(apiCatalogConfig.href)}>; rel="api-catalog"`);
2599
2723
  mergedHeaders = mergeStaticDiscoveryHeaders(mergedHeaders, staticBaseURL, discovery);
2600
2724
  mergedHeaders = dedupeStaticHeaders(mergedHeaders, ["Content-Type", "Link", "Vary"]);
2725
+ mergedHeaders = normalizeStaticContentSignalHeaders(mergedHeaders);
2601
2726
  const budget = enforceStaticHeaderBudget(mergedHeaders, CLOUDFLARE_STATIC_HEADER_RULE_LIMIT);
2602
2727
  if (budget.dropped > 0) {
2603
2728
  mergedHeaders = budget.contents;
2604
2729
  noteSharedMarkdownRule(budget.total + budget.dropped, budget.dropped);
2605
2730
  }
2731
+ if (budget.total > CLOUDFLARE_STATIC_HEADER_RULE_LIMIT && /^\s+(?:content-signal\s*:|!\s*content-signal\s*$)/im.test(budget.contents))
2732
+ throw new Error(`Content-Signal policy rules exceed Cloudflare's ${CLOUDFLARE_STATIC_HEADER_RULE_LIMIT}-rule limit. Reduce per-page policies or use a shared policy.`);
2606
2733
  if (budget.total > CLOUDFLARE_STATIC_HEADER_RULE_LIMIT)
2607
2734
  logger.warn(`_headers still has ${budget.total} rules after dropping the .md rules. Cloudflare will reject the deploy until the other rules fit under ${CLOUDFLARE_STATIC_HEADER_RULE_LIMIT}.`);
2608
2735
  }
@@ -2616,4 +2743,4 @@ export function trackDrizzleWork(event, work) { return work }
2616
2743
  }
2617
2744
  });
2618
2745
 
2619
- export { AGENT_SKILLS_SCHEMA as A, MARKDOWN_LINK_AVAILABILITY_FILE as M, logger as l, module$1 as m };
2746
+ export { AGENT_SKILLS_SCHEMA as A, MARKDOWN_LINK_AVAILABILITY_FILE as M, module$1 as a, logger as l, matchingStaticContentSignals as m };